From eb5a7cc0ddf07a29b2c8c9c68ff50321dece845f Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 17 Jan 2026 05:53:55 +0800 Subject: [PATCH 1/2] vim-patch:9.1.2087: Crash when using :tabonly in BufUnload Problem: Crash when using :tabonly in BufUnload. Solution: Set curbuf when setting curwin->w_buffer. Don't wipe out a buffer if there are no other buffers. Don't decrement b_nwindows if it was 0 before buf_freeall() (zeertzjq). fixes: vim/vim#19088#issuecomment-3710172769 closes: vim/vim#19186 https://github.com/vim/vim/commit/fa64f92f6ab8b8080bdba77155e7bb3530fa21f6 --- src/nvim/buffer.c | 9 ++++--- src/nvim/window.c | 3 +++ test/old/testdir/test_autocmd.vim | 40 +++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 38008a1239..3aa9cd705a 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -718,13 +718,16 @@ bool close_buffer(win_T *win, buf_T *buf, int action, bool abort_if_last, bool i // Autocommands may have opened or closed windows for this buffer. // Decrement the count for the close we do here. - if (buf->b_nwindows > 0) { + // Don't decrement b_nwindows if the buffer wasn't displayed in any window + // before calling buf_freeall(), + if (nwindows > 0 && buf->b_nwindows > 0) { buf->b_nwindows--; } // Remove the buffer from the list. - // Do not wipe out the buffer if it is used in a window. - if (wipe_buf && buf->b_nwindows <= 0) { + // Do not wipe out the buffer if it is used in a window, or if autocommands + // wiped out all other buffers. + if (wipe_buf && buf->b_nwindows <= 0 && (buf->b_prev != NULL || buf->b_next != NULL)) { if (clear_w_buf) { win->w_buffer = NULL; } diff --git a/src/nvim/window.c b/src/nvim/window.c index 5383857051..bec3c0ddc6 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -2765,6 +2765,9 @@ static void win_unclose_buffer(win_T *win, bufref_T *bufref, bool did_decrement) // If the buffer was removed from the window we have to give it any buffer. win->w_buffer = firstbuf; firstbuf->b_nwindows++; + if (win == curwin) { + curbuf = curwin->w_buffer; + } win_init_empty(win); } else if (did_decrement && win->w_buffer == bufref->br_buf && bufref_valid(bufref)) { // close_buffer() decremented the window count, but we're keeping the window. diff --git a/test/old/testdir/test_autocmd.vim b/test/old/testdir/test_autocmd.vim index ac0e5d68fd..835146b752 100644 --- a/test/old/testdir/test_autocmd.vim +++ b/test/old/testdir/test_autocmd.vim @@ -862,6 +862,46 @@ func Test_BufUnload_close_other() call Run_test_BufUnload_close_other('setlocal bufhidden=wipe') endfunc +func Run_test_BufUnload_tabonly(first_cmd) + exe a:first_cmd + tabnew Xa + setlocal bufhidden=wipe + tabprevious + autocmd BufWinLeave Xa ++once tabnext + autocmd BufUnload Xa ++once tabonly + tabonly + + %bwipe! +endfunc + +func Test_BufUnload_tabonly() + " This used to dereference a NULL curbuf. + call Run_test_BufUnload_tabonly('setlocal bufhidden=hide') + " This used to dereference a NULL firstbuf. + call Run_test_BufUnload_tabonly('setlocal bufhidden=wipe') +endfunc + +func Run_test_BufUnload_tabonly_nested(second_autocmd) + file Xa + tabnew Xb + setlocal bufhidden=wipe + tabnew Xc + setlocal bufhidden=wipe + autocmd BufUnload Xb ++once ++nested bwipe! Xa + exe $'autocmd BufUnload Xa ++once ++nested {a:second_autocmd}' + autocmd BufWinLeave Xc ++once tabnext + tabfirst + 2tabclose + + %bwipe! +endfunc + +func Test_BufUnload_tabonly_nested() + " These used to cause heap-use-after-free. + call Run_test_BufUnload_tabonly_nested('tabonly') + call Run_test_BufUnload_tabonly_nested('tabonly | tabprevious') +endfunc + func s:AddAnAutocmd() augroup vimBarTest au BufReadCmd * echo 'hello' From 960cba7b3bf9d8987171a703c4587b79aaea4321 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 17 Jan 2026 18:35:41 +0800 Subject: [PATCH 2/2] vim-patch:9.1.2090: Last buffer not freed with EXITFREE Problem: Last buffer not freed with EXITFREE (after 9.1.2087). Solution: Free the last buffer when inside free_all_mem() (zeertzjq). This isn't really a memory leak, as the last buffer's memory is still reachable via pointers like firstbuf and lastbuf. But it's possible that this may cause false ASAN warnings in the future, which is what EXITFREE is supposed to prevent. closes: vim/vim#19194 https://github.com/vim/vim/commit/6c118afeaae756f3bbb9793e6b248517ad39a3aa --- src/nvim/buffer.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 3aa9cd705a..49852fd70c 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -719,15 +719,20 @@ bool close_buffer(win_T *win, buf_T *buf, int action, bool abort_if_last, bool i // Autocommands may have opened or closed windows for this buffer. // Decrement the count for the close we do here. // Don't decrement b_nwindows if the buffer wasn't displayed in any window - // before calling buf_freeall(), + // before calling buf_freeall(). if (nwindows > 0 && buf->b_nwindows > 0) { buf->b_nwindows--; } // Remove the buffer from the list. // Do not wipe out the buffer if it is used in a window, or if autocommands - // wiped out all other buffers. - if (wipe_buf && buf->b_nwindows <= 0 && (buf->b_prev != NULL || buf->b_next != NULL)) { + // wiped out all other buffers (unless when inside free_all_mem() where all + // buffers need to be freed and autocommands are blocked). + if (wipe_buf && buf->b_nwindows <= 0 && (buf->b_prev != NULL || buf->b_next != NULL +#if defined(EXITFREE) + || entered_free_all_mem +#endif + )) { if (clear_w_buf) { win->w_buffer = NULL; }