From 30bcb25341b73882ed0ee839002fdf6c2847ebae Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Mon, 19 Jan 2026 10:23:33 +0800 Subject: [PATCH] vim-patch:9.1.2093: heap-use-after-free when wiping buffer in TabClosedPre Problem: heap-use-after-free when wiping buffer in TabClosedPre. Solution: Check window_layout_locked() when closing window(s) in another tabpage (zeertzjq). closes: vim/vim#19196 https://github.com/vim/vim/commit/8fc7042b3da3939213bb3f4216dc581703a954dd --- src/nvim/ex_docmd.c | 4 + src/nvim/window.c | 15 +++- test/old/testdir/test_autocmd.vim | 121 ++++++++++++++++++++++++++---- 3 files changed, 123 insertions(+), 17 deletions(-) diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 0ed02e168a..0428ab8ee7 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -5174,6 +5174,10 @@ void tabpage_close_other(tabpage_T *tp, int forceit) int done = 0; char prev_idx[NUMBUFLEN]; + if (window_layout_locked(CMD_SIZE)) { + return; + } + trigger_tabclosedpre(tp, true); // Limit to 1000 windows, autocommands may add a window while we close diff --git a/src/nvim/window.c b/src/nvim/window.c index 99ac0cc4f4..07402aaaec 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -140,7 +140,8 @@ bool frames_locked(void) /// When the window layout cannot be changed give an error and return true. /// "cmd" indicates the action being performed and is used to pick the relevant -/// error message. +/// error message. When closing window(s) and the command isn't easy to know, +/// passing CMD_SIZE will also work. bool window_layout_locked(cmdidx_T cmd) { if (split_disallowed > 0 || close_disallowed > 0) { @@ -2569,6 +2570,9 @@ void close_windows(buf_T *buf, bool keep_curwin) for (win_T *wp = lastwin; wp != NULL && (is_aucmd_win(lastwin) || !one_window(wp, NULL));) { if (wp->w_buffer == buf && (!keep_curwin || wp != curwin) && !(win_locked(wp) || wp->w_buffer->b_locked > 0)) { + if (window_layout_locked(CMD_SIZE)) { + goto theend; // Only give one error message. + } if (win_close(wp, false, false) == FAIL) { // If closing the window fails give up, to avoid looping forever. break; @@ -2591,6 +2595,9 @@ void close_windows(buf_T *buf, bool keep_curwin) for (win_T *wp = tp->tp_lastwin; wp != NULL; wp = wp->w_prev) { if (wp->w_buffer == buf && !(win_locked(wp) || wp->w_buffer->b_locked > 0)) { + if (window_layout_locked(CMD_SIZE)) { + goto theend; // Only give one error message. + } if (!win_close_othertab(wp, false, tp, false)) { // If closing the window fails give up, to avoid looping forever. break; @@ -2605,6 +2612,7 @@ void close_windows(buf_T *buf, bool keep_curwin) } } +theend: RedrawingDisabled--; } @@ -3156,6 +3164,11 @@ bool win_close_othertab(win_T *win, int free_buf, tabpage_T *tp, bool force) assert(tp != curtab); bool did_decrement = false; + // Commands that may call win_close_othertab() already check this, but + // check here again just in case. + if (window_layout_locked(CMD_SIZE)) { + return false; + } // Get here with win->w_buffer == NULL when win_close() detects the tab page // changed. if (win_locked(win) diff --git a/test/old/testdir/test_autocmd.vim b/test/old/testdir/test_autocmd.vim index f35d1f8fe9..5ad3ef6570 100644 --- a/test/old/testdir/test_autocmd.vim +++ b/test/old/testdir/test_autocmd.vim @@ -4566,6 +4566,7 @@ func Test_OptionSet_cmdheight() call Ntest_setmouse(&lines - 2, 1) call feedkeys("\", 'xt') call assert_equal(2, &l:ch) + call feedkeys("\", 'xt') tabnew | resize +1 call assert_equal(1, &l:ch) @@ -4637,7 +4638,7 @@ func Test_WinScrolled_Resized_eiw() endfunc " Test that TabClosedPre and TabClosed are triggered when closing a tab. -func Test_autocmd_tabclosedpre() +func Test_autocmd_TabClosedPre() augroup testing au TabClosedPre * call add(g:tabpagenr_pre, t:testvar) au TabClosed * call add(g:tabpagenr_post, t:testvar) @@ -4703,7 +4704,7 @@ func Test_autocmd_tabclosedpre() call assert_equal([1, 2], g:tabpagenr_pre) call assert_equal([2, 3], g:tabpagenr_post) - func ClearAutomcdAndCreateTabs() + func ClearAutocmdAndCreateTabs() au! TabClosedPre bw! e Z @@ -4727,41 +4728,41 @@ func Test_autocmd_tabclosedpre() call CleanUpTestAuGroup() " Close tab in TabClosedPre autocmd - call ClearAutomcdAndCreateTabs() + call ClearAutocmdAndCreateTabs() au TabClosedPre * tabclose call assert_fails('tabclose', 'E1312:') - call ClearAutomcdAndCreateTabs() + call ClearAutocmdAndCreateTabs() au TabClosedPre * tabclose call assert_fails('tabclose 2', 'E1312:') - call ClearAutomcdAndCreateTabs() + call ClearAutocmdAndCreateTabs() au TabClosedPre * tabclose 1 call assert_fails('tabclose', 'E1312:') " Close other (all) tabs in TabClosedPre autocmd - call ClearAutomcdAndCreateTabs() + call ClearAutocmdAndCreateTabs() au TabClosedPre * tabonly call assert_fails('tabclose', 'E1312:') - call ClearAutomcdAndCreateTabs() + call ClearAutocmdAndCreateTabs() au TabClosedPre * tabonly call assert_fails('tabclose 2', 'E1312:') - call ClearAutomcdAndCreateTabs() + call ClearAutocmdAndCreateTabs() au TabClosedPre * tabclose 4 call assert_fails('tabclose 2', 'E1312:') " Open new tabs in TabClosedPre autocmd - call ClearAutomcdAndCreateTabs() + call ClearAutocmdAndCreateTabs() au TabClosedPre * tabnew D call assert_fails('tabclose', 'E1312:') - call ClearAutomcdAndCreateTabs() + call ClearAutocmdAndCreateTabs() au TabClosedPre * tabnew D call assert_fails('tabclose 1', 'E1312:') " Moving the tab page in TabClosedPre autocmd - call ClearAutomcdAndCreateTabs() + call ClearAutocmdAndCreateTabs() au TabClosedPre * tabmove 0 tabclose call assert_equal('1>Z2A3B', GetTabs()) - call ClearAutomcdAndCreateTabs() + call ClearAutocmdAndCreateTabs() au TabClosedPre * tabmove 0 tabclose 1 call assert_equal('1A2B3>C', GetTabs()) @@ -4769,11 +4770,11 @@ func Test_autocmd_tabclosedpre() call assert_equal('1>C', GetTabs()) " Switching tab page in TabClosedPre autocmd - call ClearAutomcdAndCreateTabs() + call ClearAutocmdAndCreateTabs() au TabClosedPre * tabnext | e Y tabclose call assert_equal('1Y2A3>B', GetTabs()) - call ClearAutomcdAndCreateTabs() + call ClearAutocmdAndCreateTabs() au TabClosedPre * tabnext | e Y tabclose 1 call assert_equal('1Y2B3>C', GetTabs()) @@ -4781,10 +4782,10 @@ func Test_autocmd_tabclosedpre() call assert_equal('1>Y', GetTabs()) " Create new windows in TabClosedPre autocmd - call ClearAutomcdAndCreateTabs() + call ClearAutocmdAndCreateTabs() au TabClosedPre * split | e X| vsplit | e Y | split | e Z call assert_fails('tabclose', 'E242:') - call ClearAutomcdAndCreateTabs() + call ClearAutocmdAndCreateTabs() au TabClosedPre * new X | new Y | new Z call assert_fails('tabclose 1', 'E242:') @@ -4819,6 +4820,94 @@ func Test_autocmd_tabclosedpre() only tabonly bw! + delfunc ClearAutocmdAndCreateTabs + delfunc GetTabs +endfunc + +" This used to cause heap-use-after-free. +func Run_test_TabClosedPre_wipe_buffer(split_cmds) + file Xa + exe a:split_cmds + autocmd TabClosedPre * ++once tabnext | bwipe! Xa + " Closing window inside TabClosedPre is not allowed. + call assert_fails('tabonly', 'E1312:') + + %bwipe! +endfunc + +func Test_TabClosedPre_wipe_buffer() + " Test with Xa only in other tab pages. + call Run_test_TabClosedPre_wipe_buffer('split | tab split | tabnew Xb') + " Test with Xa in both current and other tab pages. + call Run_test_TabClosedPre_wipe_buffer('split | tab split | new Xb') +endfunc + +func Test_TabClosedPre_mouse() + func MyTabline() + let cnt = tabpagenr('$') + return range(1, cnt)->mapnew({_, n -> $'%{n}X|Close{n}|%X'})->join('') + endfunc + + let save_mouse = &mouse + if has('gui') + set guioptions-=e + endif + set mouse=a tabline=%!MyTabline() + + func OpenTwoTabPages() + %bwipe! + file Xa | split | split + let g:Xa_bufnr = bufnr() + tabnew Xb | split + let g:Xb_bufnr = bufnr() + redraw! + call assert_match('^|Close1||Close2| *$', Screenline(1)) + call assert_equal(2, tabpagenr('$')) + endfunc + + autocmd! TabClosedPre + call OpenTwoTabPages() + let g:autocmd_bufnrs = [] + autocmd TabClosedPre * let g:autocmd_bufnrs += [tabpagebuflist()] + call Ntest_setmouse(1, 2) + call feedkeys("\\", 'tx') + call assert_equal(1, tabpagenr('$')) + call assert_equal([[g:Xa_bufnr]->repeat(3)], g:autocmd_bufnrs) + call assert_equal([g:Xb_bufnr]->repeat(2), tabpagebuflist()) + + call OpenTwoTabPages() + let g:autocmd_bufnrs = [] + autocmd TabClosedPre * call feedkeys("\\", 'tx') + call Ntest_setmouse(1, 2) + " Closing tab page inside TabClosedPre is not allowed. + call assert_fails('call feedkeys("\", "tx")', 'E1312:') + call feedkeys("\", 'tx') + + autocmd! TabClosedPre + call OpenTwoTabPages() + let g:autocmd_bufnrs = [] + autocmd TabClosedPre * let g:autocmd_bufnrs += [tabpagebuflist()] + call Ntest_setmouse(1, 10) + call feedkeys("\\", 'tx') + call assert_equal(1, tabpagenr('$')) + call assert_equal([[g:Xb_bufnr]->repeat(2)], g:autocmd_bufnrs) + call assert_equal([g:Xa_bufnr]->repeat(3), tabpagebuflist()) + + call OpenTwoTabPages() + let g:autocmd_bufnrs = [] + autocmd TabClosedPre * call feedkeys("\\", 'tx') + call Ntest_setmouse(1, 10) + " Closing tab page inside TabClosedPre is not allowed. + call assert_fails('call feedkeys("\", "tx")', 'E1312:') + call feedkeys("\", 'tx') + + autocmd! TabClosedPre + %bwipe! + unlet g:Xa_bufnr g:Xb_bufnr g:autocmd_bufnrs + let &mouse = save_mouse + set tabline& guioptions& + delfunc MyTabline + delfunc OpenTwoTabPages endfunc func Test_eventignorewin_non_current()