From d945a93d69c994dd7a88629ac3a59034f1184631 Mon Sep 17 00:00:00 2001 From: Sean Dewar <6256228+seandewar@users.noreply.github.com> Date: Fri, 6 Feb 2026 23:13:04 +0000 Subject: [PATCH] fix(autocmd): potential TabClosed UAF, always set abuf Problem: win_free_mem can free w_buffer (via qf_free_all), which may cause a heap use-after-free if used as TabClosed's . I think TabClosed is also the only event to conditionally set not based on event type. Solution: use the buffer saved by the bufref. Fall back to curbuf if invalid, like WinResized/WinScrolled. NOTE: Not always equivalent if close_buffer autocmds switch buffers at the last moment; previously would be set to that buffer. Fixed in next commit. https://github.com/neovim/neovim/actions/runs/21765657455/job/62800643599?pr=37758#step:9:159 for an example of qf_free_all being a nuisance. --- src/nvim/window.c | 4 ++-- test/functional/autocmd/tabclose_spec.lua | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/nvim/window.c b/src/nvim/window.c index f05cfca385..e1f29a62f3 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -3263,7 +3263,6 @@ bool win_close_othertab(win_T *win, int free_buf, tabpage_T *tp, bool force) } // Free the memory used for the window. - buf_T *buf = win->w_buffer; int dir; win_free_mem(win, &dir, tp); @@ -3276,7 +3275,8 @@ bool win_close_othertab(win_T *win, int free_buf, tabpage_T *tp, bool force) if (has_event(EVENT_TABCLOSED)) { char prev_idx[NUMBUFLEN]; vim_snprintf(prev_idx, NUMBUFLEN, "%i", free_tp_idx); - apply_autocmds(EVENT_TABCLOSED, prev_idx, prev_idx, false, buf); + apply_autocmds(EVENT_TABCLOSED, prev_idx, prev_idx, false, + bufref.br_buf && bufref_valid(&bufref) ? bufref.br_buf : curbuf); } } return true; diff --git a/test/functional/autocmd/tabclose_spec.lua b/test/functional/autocmd/tabclose_spec.lua index 3969f33cf1..b6f05ffcaa 100644 --- a/test/functional/autocmd/tabclose_spec.lua +++ b/test/functional/autocmd/tabclose_spec.lua @@ -59,13 +59,14 @@ describe('TabClosed', function() setlocal bufhidden=wipe tabnew au TabClosed * ++once let g:tp_valid = nvim_tabpage_is_valid(s:tp) + \| let g:curbuf = bufnr() \| let g:abuf = expand('') call nvim_buf_delete(g:buf, #{force: 1}) ]]) eq(false, eval('g:tp_valid')) eq(false, eval('nvim_buf_is_valid(g:buf)')) - eq('', eval('g:abuf')) + eq(eval('g:curbuf'), tonumber(eval('g:abuf'))) -- Falls back to curbuf. exec([[ tabnew