This test exposes a bug (present on current master).
Steps to reproduce:
CC=clang make CMAKE_BUILD_TYPE=Debug CMAKE_EXTRA_FLAGS="-DCLANG_ASAN_UBSAN=ON"
export ASAN_SYMBOLIZER_PATH=/usr/lib/llvm-5.0/bin/llvm-symbolizer
TEST_FILE=test_autocmd.res make oldtest
==3668==ERROR: AddressSanitizer: heap-use-after-free on address 0x6260001411c8 at pc 0x000000a38a47 bp 0x7fff9982ee50 sp 0x7fff9982ee48
READ of size 4 at 0x6260001411c8 thread T0
0 0xa38a46 in _typval_encode_nothing_convert_one_value /home/vagrant/neovim/build/../src/nvim/eval/typval_encode.c.h:320:15
1 0xa340d5 in encode_vim_to_nothing /home/vagrant/neovim/build/../src/nvim/eval/typval_encode.c.h:830:9
2 0xa0ad63 in tv_clear /home/vagrant/neovim/build/../src/nvim/eval/typval.c:2189:25
3 0x85d584 in vars_clear_ext /home/vagrant/neovim/build/../src/nvim/eval.c:18894:9
4 0x7efac9 in vars_clear /home/vagrant/neovim/build/../src/nvim/eval.c:18871:3
5 0x7ef471 in eval_clear /home/vagrant/neovim/build/../src/nvim/eval.c:638:3
6 0xf80a7c in free_all_mem /home/vagrant/neovim/build/../src/nvim/memory.c:676:3
7 0x12789dc in mch_exit /home/vagrant/neovim/build/../src/nvim/os_unix.c:152:3
8 0xe85039 in getout /home/vagrant/neovim/build/../src/nvim/main.c:671:3
9 0xbb4496 in ex_quit_all /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:6051:5
10 0xb4ba9c in do_one_cmd /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:2242:5
11 0xb2dd03 in do_cmdline /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:609:20
12 0x845302 in call_user_func /home/vagrant/neovim/build/../src/nvim/eval.c:21332:3
13 0x81097b in call_func /home/vagrant/neovim/build/../src/nvim/eval.c:6358:11
14 0x825778 in get_func_tv /home/vagrant/neovim/build/../src/nvim/eval.c:6120:11
15 0x81e361 in ex_call /home/vagrant/neovim/build/../src/nvim/eval.c:2735:9
16 0xb4ba9c in do_one_cmd /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:2242:5
17 0xb2dd03 in do_cmdline /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:609:20
18 0xb17fdd in do_source /home/vagrant/neovim/build/../src/nvim/ex_cmds2.c:2973:3
19 0xb14630 in cmd_source /home/vagrant/neovim/build/../src/nvim/ex_cmds2.c:2718:14
20 0xb14727 in ex_source /home/vagrant/neovim/build/../src/nvim/ex_cmds2.c:2699:3
21 0xb4ba9c in do_one_cmd /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:2242:5
22 0xb2dd03 in do_cmdline /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:609:20
23 0xb34015 in do_cmdline_cmd /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:280:10
24 0xe829d3 in exe_commands /home/vagrant/neovim/build/../src/nvim/main.c:1702:5
25 0xe708bd in main /home/vagrant/neovim/build/../src/nvim/main.c:524:5
26 0x7f0cd5b7e82f in __libc_start_main /build/glibc-Cl5G7W/glibc-2.23/csu/../csu/libc-start.c:291
27 0x44d628 in _start (/home/vagrant/neovim/build/bin/nvim+0x44d628)
0x6260001411c8 is located 200 bytes inside of 10256-byte region [0x626000141100,0x626000143910)
freed by thread T0 here:
0 0x50df60 in __interceptor_cfree.localalias.0 (/home/vagrant/neovim/build/bin/nvim+0x50df60)
1 0xf7dbe4 in xfree /home/vagrant/neovim/build/../src/nvim/memory.c:133:3
2 0x676577 in free_buffer /home/vagrant/neovim/build/../src/nvim/buffer.c:749:5
3 0x66e15b in close_buffer /home/vagrant/neovim/build/../src/nvim/buffer.c:590:5
4 0x67a835 in do_buffer /home/vagrant/neovim/build/../src/nvim/buffer.c:1216:9
5 0x681a1d in do_bufdel /home/vagrant/neovim/build/../src/nvim/buffer.c:945:16
6 0xb9cd72 in ex_bunload /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:4585:17
7 0xb4ba9c in do_one_cmd /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:2242:5
8 0xb2dd03 in do_cmdline /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:609:20
9 0x845302 in call_user_func /home/vagrant/neovim/build/../src/nvim/eval.c:21332:3
10 0x81097b in call_func /home/vagrant/neovim/build/../src/nvim/eval.c:6358:11
11 0x825778 in get_func_tv /home/vagrant/neovim/build/../src/nvim/eval.c:6120:11
12 0x81e361 in ex_call /home/vagrant/neovim/build/../src/nvim/eval.c:2735:9
13 0xb4ba9c in do_one_cmd /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:2242:5
14 0xb2dd03 in do_cmdline /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:609:20
15 0x8646b1 in ex_execute /home/vagrant/neovim/build/../src/nvim/eval.c:19478:7
16 0xb4ba9c in do_one_cmd /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:2242:5
17 0xb2dd03 in do_cmdline /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:609:20
18 0x845302 in call_user_func /home/vagrant/neovim/build/../src/nvim/eval.c:21332:3
19 0x81097b in call_func /home/vagrant/neovim/build/../src/nvim/eval.c:6358:11
20 0x825778 in get_func_tv /home/vagrant/neovim/build/../src/nvim/eval.c:6120:11
21 0x81e361 in ex_call /home/vagrant/neovim/build/../src/nvim/eval.c:2735:9
22 0xb4ba9c in do_one_cmd /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:2242:5
23 0xb2dd03 in do_cmdline /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:609:20
24 0xb17fdd in do_source /home/vagrant/neovim/build/../src/nvim/ex_cmds2.c:2973:3
25 0xb14630 in cmd_source /home/vagrant/neovim/build/../src/nvim/ex_cmds2.c:2718:14
26 0xb14727 in ex_source /home/vagrant/neovim/build/../src/nvim/ex_cmds2.c:2699:3
27 0xb4ba9c in do_one_cmd /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:2242:5
28 0xb2dd03 in do_cmdline /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:609:20
29 0xb34015 in do_cmdline_cmd /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:280:10
previously allocated by thread T0 here:
0 0x50e340 in calloc (/home/vagrant/neovim/build/bin/nvim+0x50e340)
1 0xf7dc97 in xcalloc /home/vagrant/neovim/build/../src/nvim/memory.c:147:15
2 0x67eb65 in buflist_new /home/vagrant/neovim/build/../src/nvim/buffer.c:1641:11
3 0xa8fdcc in do_ecmd /home/vagrant/neovim/build/../src/nvim/ex_cmds.c:2221:13
4 0x683b21 in empty_curbuf /home/vagrant/neovim/build/../src/nvim/buffer.c:1031:12
5 0x67a098 in do_buffer /home/vagrant/neovim/build/../src/nvim/buffer.c:1196:14
6 0x68173f in do_bufdel /home/vagrant/neovim/build/../src/nvim/buffer.c:927:11
7 0xb9cd72 in ex_bunload /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:4585:17
8 0xb4ba9c in do_one_cmd /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:2242:5
9 0xb2dd03 in do_cmdline /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:609:20
10 0x845302 in call_user_func /home/vagrant/neovim/build/../src/nvim/eval.c:21332:3
11 0x81097b in call_func /home/vagrant/neovim/build/../src/nvim/eval.c:6358:11
12 0x825778 in get_func_tv /home/vagrant/neovim/build/../src/nvim/eval.c:6120:11
13 0x81e361 in ex_call /home/vagrant/neovim/build/../src/nvim/eval.c:2735:9
14 0xb4ba9c in do_one_cmd /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:2242:5
15 0xb2dd03 in do_cmdline /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:609:20
16 0x8646b1 in ex_execute /home/vagrant/neovim/build/../src/nvim/eval.c:19478:7
17 0xb4ba9c in do_one_cmd /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:2242:5
18 0xb2dd03 in do_cmdline /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:609:20
19 0x845302 in call_user_func /home/vagrant/neovim/build/../src/nvim/eval.c:21332:3
20 0x81097b in call_func /home/vagrant/neovim/build/../src/nvim/eval.c:6358:11
21 0x825778 in get_func_tv /home/vagrant/neovim/build/../src/nvim/eval.c:6120:11
22 0x81e361 in ex_call /home/vagrant/neovim/build/../src/nvim/eval.c:2735:9
23 0xb4ba9c in do_one_cmd /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:2242:5
24 0xb2dd03 in do_cmdline /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:609:20
25 0xb17fdd in do_source /home/vagrant/neovim/build/../src/nvim/ex_cmds2.c:2973:3
26 0xb14630 in cmd_source /home/vagrant/neovim/build/../src/nvim/ex_cmds2.c:2718:14
27 0xb14727 in ex_source /home/vagrant/neovim/build/../src/nvim/ex_cmds2.c:2699:3
28 0xb4ba9c in do_one_cmd /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:2242:5
29 0xb2dd03 in do_cmdline /home/vagrant/neovim/build/../src/nvim/ex_docmd.c:609:20
SUMMARY: AddressSanitizer: heap-use-after-free /home/vagrant/neovim/build/../src/nvim/eval/typval_encode.c.h:320:15 in _typval_encode_nothing_convert_one_va
lue
Shadow bytes around the buggy address:
0x0c4c800201e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c4c800201f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c4c80020200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c4c80020210: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c4c80020220: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
=>0x0c4c80020230: fd fd fd fd fd fd fd fd fd[fd]fd fd fd fd fd fd
0x0c4c80020240: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x0c4c80020250: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x0c4c80020260: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x0c4c80020270: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x0c4c80020280: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==3668==ABORTING
Nvim core
Module-specific details are documented at the top of each module (terminal.c,
screen.c, …).
See :help dev for guidelines.
Filename conventions
The source files use extensions to hint about their purpose.
*.c,*.generated.c- full C files, with all includes, etc.*.c.h- parametrized C files, contain all necessary includes, but require defining macros before actually using. Example:typval_encode.c.h*.h- full headers, with all includes. Does not apply to*.generated.h.*.h.generated.h- exported functions’ declarations.*.c.generated.h- static functions’ declarations.
Logs
Low-level log messages sink to $NVIM_LOG_FILE.
You can use LOG_CALLSTACK(); anywhere in the source to log the current
stacktrace. To log in an alternate file, e.g. stderr, use
LOG_CALLSTACK_TO_FILE(FILE*). (Currently Linux-only.)
UI events are logged at level 0 (DEBUG_LOG_LEVEL).
rm -rf build/
make CMAKE_EXTRA_FLAGS="-DMIN_LOG_LEVEL=0"
Build with ASAN
Building Nvim with Clang sanitizers (Address Sanitizer: ASan, Undefined Behavior Sanitizer: UBSan, Memory Sanitizer: MSan, Thread Sanitizer: TSan) is a good way to catch undefined behavior, leaks and other errors as soon as they happen. It's significantly faster than Valgrind.
Requires clang 3.4 or later:
clang --version
Build Nvim with sanitizer instrumentation:
CC=clang make CMAKE_EXTRA_FLAGS="-DCLANG_ASAN_UBSAN=ON"
Create a directory to store logs:
mkdir -p "$HOME/logs"
Enable the sanitizer(s) via these environment variables:
# Change to detect_leaks=1 to detect memory leaks (slower).
export ASAN_OPTIONS="detect_leaks=0:log_path=$HOME/logs/asan"
export ASAN_SYMBOLIZER_PATH=/usr/lib/llvm-5.0/bin/llvm-symbolizer
export MSAN_SYMBOLIZER_PATH=/usr/lib/llvm-5.0/bin/llvm-symbolizer
export TSAN_OPTIONS="external_symbolizer_path=/usr/lib/llvm-5.0/bin/llvm-symbolizer log_path=${HOME}/logs/tsan"
Logs will be written to ${HOME}/logs/*san.PID.
TUI debugging
TUI troubleshoot
Nvim logs its internal terminfo state at 'verbose' level 3. This makes it possible to see exactly what terminfo values Nvim is using on any system.
nvim -V3log
TUI trace
The ancient script command is still the "state of the art" for tracing
terminal behavior. The libvterm vterm-dump utility formats the result for
human-readability.
Record a Nvim terminal session and format it with vterm-dump:
script foo
./build/bin/nvim -u NONE
# Exit the script session with CTRL-d
# Use `vterm-dump` utility to format the result.
./.deps/usr/bin/vterm-dump foo > bar
Then you can compare bar with another session, to debug TUI behavior.
TUI redraw
Set the 'writedelay' option to see where and when the UI is painted.
:set writedelay=1
Terminal reference
man terminfo- http://bazaar.launchpad.net/~libvterm/libvterm/trunk/view/head:/doc/seqs.txt
- http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
Nvim lifecycle
Following describes how Nvim processes input.
Consider a typical Vim-like editing session:
- Vim displays the welcome screen
- User types:
: - Vim enters command-line mode
- User types:
edit README.txt<CR> - Vim opens the file and returns to normal mode
- User types:
G - Vim navigates to the end of the file
- User types:
5 - Vim enters count-pending mode
- User types:
d - Vim enters operator-pending mode
- User types:
w - Vim deletes 5 words
- User types:
g - Vim enters the "g command mode"
- User types:
g - Vim goes to the beginning of the file
- User types:
i - Vim enters insert mode
- User types:
word<ESC> - Vim inserts "word" at the beginning and returns to normal mode
Note that we split user actions into sequences of inputs that change the state of the editor. While there's no documentation about a "g command mode" (step 16), internally it is implemented similarly to "operator-pending mode".
From this we can see that Vim has the behavior of an input-driven state machine (more specifically, a pushdown automaton since it requires a stack for transitioning back from states). Assuming each state has a callback responsible for handling keys, this pseudocode represents the main program loop:
def state_enter(state_callback, data):
do
key = readkey() # read a key from the user
while state_callback(data, key) # invoke the callback for the current state
That is, each state is entered by calling state_enter and passing a
state-specific callback and data. Here is a high-level pseudocode for a program
that implements something like the workflow described above:
def main()
state_enter(normal_state, {}):
def normal_state(data, key):
if key == ':':
state_enter(command_line_state, {})
elif key == 'i':
state_enter(insert_state, {})
elif key == 'd':
state_enter(delete_operator_state, {})
elif key == 'g':
state_enter(g_command_state, {})
elif is_number(key):
state_enter(get_operator_count_state, {'count': key})
elif key == 'G'
jump_to_eof()
return true
def command_line_state(data, key):
if key == '<cr>':
if data['input']:
execute_ex_command(data['input'])
return false
elif key == '<esc>'
return false
if not data['input']:
data['input'] = ''
data['input'] += key
return true
def delete_operator_state(data, key):
count = data['count'] or 1
if key == 'w':
delete_word(count)
elif key == '$':
delete_to_eol(count)
return false # return to normal mode
def g_command_state(data, key):
if key == 'g':
go_top()
elif key == 'v':
reselect()
return false # return to normal mode
def get_operator_count_state(data, key):
if is_number(key):
data['count'] += key
return true
unshift_key(key) # return key to the input buffer
state_enter(delete_operator_state, data)
return false
def insert_state(data, key):
if key == '<esc>':
return false # exit insert mode
self_insert(key)
return true
The above gives an idea of how Nvim is organized internally. Some states like
the g_command_state or get_operator_count_state do not have a dedicated
state_enter callback, but are implicitly embedded into other states (this
will change later as we continue the refactoring effort). To start reading the
actual code, here's the recommended order:
state_enter()function (state.c). This is the actual program loop, note that aVimStatestructure is used, which contains function pointers for the callback and state data.main()function (main.c). After all startup,normal_enteris called at the end of function to enter normal mode.normal_enter()function (normal.c) is a small wrapper for setting up the NormalState structure and callingstate_enter.normal_check()function (normal.c) is called before each iteration of normal mode.normal_execute()function (normal.c) is called when a key is read in normal mode.
The basic structure described for normal mode in 3, 4 and 5 is used for other
modes managed by the state_enter loop:
- command-line mode:
command_line_{enter,check,execute}()(ex_getln.c) - insert mode:
insert_{enter,check,execute}()(edit.c) - terminal mode:
terminal_{enter,execute}()(terminal.c)
Async event support
One of the features Nvim added is the support for handling arbitrary asynchronous events, which can include:
- RPC requests
- job control callbacks
- timers
Nvim implements this functionality by entering another event loop while waiting for characters, so instead of:
def state_enter(state_callback, data):
do
key = readkey() # read a key from the user
while state_callback(data, key) # invoke the callback for the current state
Nvim program loop is more like:
def state_enter(state_callback, data):
do
event = read_next_event() # read an event from the operating system
while state_callback(data, event) # invoke the callback for the current state
where event is something the operating system delivers to us, including (but
not limited to) user input. The read_next_event() part is internally
implemented by libuv, the platform layer used by Nvim.
Since Nvim inherited its code from Vim, the states are not prepared to receive "arbitrary events", so we use a special key to represent those (When a state receives an "arbitrary event", it normally doesn't do anything other update the screen).