Install vim-airline (8f0c931, 2026-07-28) and vim-fugitive (3b753cf, 2026-03-07).
Run:
vim --cmd 'let g:airline_extensions=["branch"]' file.txt -c 'split' -c 'vsplit' -c 'qall!'
Repeat about 8 times. The crash is non-deterministic and does not fire on
every run.
Vim aborts with SIGABRT (exit status 134):
___BUG_IN_CLIENT_OF_LIBMALLOC_POINTER_BEING_FREED_WAS_NOT_ALLOCATED
-> malloc_report -> abort
Observed rate: 60-100% of runs across many trials (12/12, 8/8, 7/8, 5/8 in
separate batches). Startup alone never crashes; a window must be created.
Creating a window should not abort Vim.
No sequence of Vim script should be able to make Vim call free() on a pointer
that was never allocated. vim-airline and vim-fugitive are pure Vim script and
cannot corrupt the heap directly, so this is being reported as a Vim
memory-safety defect that those plugins merely trigger.
Vim 9.1 is not affected by the identical configuration, so this appears to be a
9.2 regression.
VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Aug 23 2026 19:45:42) macOS version - arm64 Included patches: 1-1000 Huge version without GUI Compiled by Homebrew
Operating system: macOS 26.6.2 (25G83), Apple Silicon (arm64)
Terminal: iTerm2
Value of $TERM: xterm-256color
Shell: GNU bash 5.3.15(1)-release (aarch64-apple-darwin25.4.0)
Vim build: Homebrew bottle, vim 9.2.1000
Plugins: vim-airline 8f0c9312dd7db148c67d6ba281087d0e10bbab92 (2026-07-28)
vim-fugitive 3b753cf8c6a4dcde6edee8827d464ba9b8c4a6f0 (2026-03-07)
managed by Vundle
Crash signature (macOS .ips report, identical across 26+ reports): exception: EXC_CRASH (SIGABRT) asi: libsystem_c.dylib: "abort() called" Faulting thread (com.apple.main-thread): libsystem_kernel.dylib __kill vim +1192304 vim +2484188 libsystem_platform.dylib _sigtramp libsystem_pthread.dylib pthread_kill libsystem_c.dylib abort libsystem_malloc.dylib malloc_vreport libsystem_malloc.dylib malloc_report libsystem_malloc.dylib ___BUG_IN_CLIENT_OF_LIBMALLOC_POINTER_BEING_FREED_WAS_NOT_ALLOCATED vim +1163580 vim +1162988 ... nested user-function evaluation, ~3 levels deep ... dyld start All frames in the corrupting path are Vim's own. The crashing process loads only vim, libncursesw, libsodium, libintl and libsystem -- no plugin dylib is involved. Bisection (same machine, same .vimrc, N = 6-12 runs per row): Configuration SIGABRT -------------------------------------------- --------------------- baseline, window creation 12/12, 8/8, 7/8, 5/8 g:airline_extensions=['branch'] 6/6 g:airline_extensions=[] 0/6 all extensions EXCEPT branch 0/8 g:airline#extensions#branch#enabled=0 0/8 g:loaded_airline=1 0/12 g:loaded_fugitive=1 0/6 startup only, no window created 0/12 Apple system vim 9.1, identical config 0/20 The tabline, whitespace, syntastic and ctrlp extensions are each individually clean. Only 'branch' triggers the crash, and only with fugitive loaded. Regression evidence: Apple's /usr/bin/vim 9.1 (2024 Jan 02, compiled Jul 31 2026) runs the identical .vimrc and plugin set with zero crashes across 20 runs. Caveat: that is a -python3 -ruby -lua build, so the comparison is not perfectly controlled. However, removing the python3 block from the vimrc does not change the 9.2 crash rate, so the embedded interpreters are not implicated. Ruled out: - Python/Ruby/Lua interpreters: removing the python3 heredoc changes nothing. - ctrlp: appears at the top of the verbose log at crash time, but is the victim of the corruption rather than the trigger; disabling it changes nothing. - autocmds: removing all 21 augroup blocks changes nothing. - MallocScribble: raises the hit rate but is not required to reproduce. Not yet minimized: This could not be reduced to a standalone minimal vimrc. Vundle + fugitive + airline['branch'] alone is clean (0/6), so a further interaction in the full config is required. Full .vimrc available on request. No symbolicated backtrace is available -- the Homebrew bottle is stripped (nm yields a single symbol). An ASan build configured with --enable-fail-if-missing should name the offending free() directly.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
cannot reproduce. Can you get a backtrace or ASAN trace?
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
Here's an ASan trace. It's a double-free of 'eventignore', and the culprit
is undo_cmdmod() restoring 'ei' after a :noautocmd command modifier.
Built from tag v9.2.1000, all interpreters disabled to rule them out:
./configure --with-features=huge --enable-fail-if-missing --disable-gui --without-x \ --disable-netbeans \ --disable-pythoninterp --disable-python3interp \ --disable-rubyinterp --disable-luainterp --disable-perlinterp \ CFLAGS="-g -O1 -fsanitize=address -fno-omit-frame-pointer" \ LDFLAGS="-fsanitize=address"
Reproduces 6/6 runs under ASan, with byte-identical reports each time. Note this
build has no Python/Ruby/Lua at all, which rules out the embedded interpreters.
ERROR: AddressSanitizer: attempting double-free on 0x6020002b6c10 in thread T0
SUMMARY: AddressSanitizer: double-free optionstr.c:5570 in did_set_string_option
The buffer is a 1-byte region — an empty 'eventignore' string.
| Location | |
|---|---|
| allocated | optionstr.c:470 (vim_strsave) in set_string_option_direct, called from undo_cmdmod at ex_docmd.c:3408 |
| freed | optionstr.c:477 (free_string_option(*varp)) in set_string_option_direct |
| freed again | optionstr.c:5570 (free_string_option(oldval)) in did_set_string_option |
undo_cmdmod() restores 'ei' after :noautocmd:
if (cmod->cmod_save_ei != NULL) { // Restore 'eventignore' to the value before ":noautocmd". set_string_option_direct((char_u *)"ei", -1, cmod->cmod_save_ei, OPT_FREE, SID_NONE); free_string_option(cmod->cmod_save_ei); cmod->cmod_save_ei = NULL; }
Meanwhile set_string_option() captures oldval = *varp and hands that raw
pointer to did_set_string_option(), which frees it at the end. If a nested
undo_cmdmod() frees that same buffer in between, the outer frame frees it a
second time.
:split → win_split_ins → win_enter_ext → WinEnter autocmd → plugin
function → :let &<option> = ..., while a nested :noautocmd let x = <expr> is
unwinding on the same stack.
In practice this is vim-airline's branch extension,
autoload/airline/extensions/branch.vim:132:
noautocmd let ref = fugitive#repo().git_chomp('describe', '--all', '--exact-match', commit)
That is :noautocmd let x = <expr> where the RHS calls further user functions —
matching the "freed by" stack, which shows the free happening during RHS
evaluation at evalvars.c:1206.
#1 did_set_string_option optionstr.c:5570
#2 set_string_option optionstr.c:626
#3 set_option_value option.c:6178
#4 ex_let_one evalvars.c:1975
#5 ex_let_vars evalvars.c:1258
#6 ex_let evalvars.c:1216
#7 do_cmdline ex_docmd.c:1041
#8 call_user_func_check userfunc.c:3485
#9 call_func userfunc.c:4158
#10 get_func_tv userfunc.c:2183
#11 ex_call userfunc.c:6891
#12 do_cmdline ex_docmd.c:1041
#13 apply_autocmds_group autocmd.c:2501
#14 win_enter_ext window.c:5858
#15 win_split_ins window.c:1541
#16 ex_splitview ex_docmd.c:7394
#17 do_cmdline ex_docmd.c:1041
#18 vim_main2 main.c:866
#19 main main.c:453
#1 set_string_option_direct optionstr.c:477
#2 do_cmdline ex_docmd.c:1041
#3 call_user_func_check userfunc.c:3485
...
#26 ex_let evalvars.c:1206 <- RHS evaluation
#27 do_cmdline ex_docmd.c:1041
#1 lalloc alloc.c:246
#2 vim_strsave strings.c:27
#3 set_string_option_direct optionstr.c:470
#4 undo_cmdmod ex_docmd.c:3408 <- restoring 'ei' after :noautocmd
#5 do_cmdline ex_docmd.c:1041
I could not reduce this to a standalone vimrc. Hand-written nestings of
:noautocmd let x = F() inside a WinEnter autocmd, with and without
let &eventignore = ..., do not trip it (0/6 each). It currently needs the real
airline + fugitive call graph. Happy to run further instrumented builds on
request — I have the ASan build set up and the failure is reliable at 6/6.
vim 9.1 (Apple's system build, compiled Jul 2026) does not crash with the
identical config across 20 runs.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
I think this might be a fallout of #21074 (patch v9.2.0991)
ping @h-east
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
Confirmed, this is a fallout of #21074 (patch 9.2.0991).
While triggering the deferred events, did_set_eventignore() leaves the option
pointing at oldval, which the caller frees afterwards. Anything that changes
'eventignore' in between frees it first, hence the double free. The airline
branch extension gets there through :noautocmd.
A plain :set does it too, so no plugin is needed:
set eventignore=WinNew autocmd CursorMoved * set eventignore=WinLeave func F() normal! j set eventignore=all endfunc call F()
Thanks for the ASan trace, it pointed straight at the double free. This is my
regression, I have fixed it.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()