commit b40880f88ffb5efb636b001faed6241a1cae91f7
parent e77a69c6e9b3b04cc1c3dfde812dad1a87e193e2
Author: zeertzjq <zeertzjq@outlook.com>
Date: Thu, 29 Jan 2026 13:02:36 +0800
fix(terminal): heap UAF if buffer deleted during TermRequest (#37612)
Problem: Heap UAF if a terminal buffer is deleted during TermRequest in
Normal mode.
Solution: Increment terminal refcount before triggering TermRequest, and
destroy the terminal if the buffer is closed during that.
Diffstat:
2 files changed, 70 insertions(+), 0 deletions(-)
diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c
@@ -268,8 +268,10 @@ static void emit_termrequest(void **argv)
terminator ==
VTERM_TERMINATOR_BEL ? STATIC_CSTR_AS_OBJ("\x07") : STATIC_CSTR_AS_OBJ("\x1b\\"));
+ term->refcount++;
apply_autocmds_group(EVENT_TERMREQUEST, NULL, NULL, true, AUGROUP_ALL, buf, NULL,
&DICT_OBJ(data));
+ term->refcount--;
xfree(sequence);
StringBuilder *term_pending_send = term->pending.send;
@@ -282,6 +284,13 @@ static void emit_termrequest(void **argv)
term->pending.send = term_pending_send;
}
xfree(pending_send);
+
+ // Terminal buffer closed during TermRequest in Normal mode: destroy the terminal.
+ // In Terminal mode term->refcount should still be non-zero here.
+ if (term->buf_handle == 0 && !term->refcount) {
+ term->destroy = true;
+ term->opts.close_cb(term->opts.data);
+ }
}
static void schedule_termrequest(Terminal *term)
diff --git a/test/functional/autocmd/termxx_spec.lua b/test/functional/autocmd/termxx_spec.lua
@@ -353,3 +353,64 @@ describe('autocmd TextChangedT,WinResized', function()
eq({}, api.nvim_get_chan_info(term2)) -- Channel should've been cleaned up.
end)
end)
+
+describe('no crash if :bwipe from TermClose is processed by', function()
+ local oldwin --- @type integer
+ local chan --- @type integer
+
+ before_each(function()
+ clear()
+ command('autocmd! nvim.terminal')
+ oldwin = api.nvim_get_current_win()
+ command('new')
+ local buf = api.nvim_get_current_buf()
+ chan = api.nvim_open_term(buf, {})
+ api.nvim_set_var('chan', chan)
+ command(('autocmd TermClose <buffer> bwipe! %d'):format(buf))
+ command('let g:done = 0')
+ feed('i')
+ eq({ mode = 't', blocking = false }, api.nvim_get_mode())
+ end)
+
+ --- @param event string Event name.
+ --- @param trigger_cmd string The Ex command to trigger the event.
+ local function test_case(event, trigger_cmd)
+ api.nvim_create_autocmd(
+ event,
+ { nested = true, once = true, command = 'sleep 40m | let g:done = 1' }
+ )
+ exec_lua(function()
+ vim.cmd(trigger_cmd)
+ vim.defer_fn(function()
+ vim.fn.chanclose(chan)
+ end, 25)
+ end)
+ retry(nil, 1000, function()
+ eq(1, api.nvim_get_var('done'))
+ end)
+ assert_alive()
+ eq({ mode = 'n', blocking = false }, api.nvim_get_mode())
+ eq({ oldwin }, api.nvim_list_wins())
+ feed('<Ignore>') -- Add input to separate two RPC requests.
+ -- Channel should have been released.
+ eq({}, api.nvim_get_chan_info(chan))
+ end
+
+ it('WinResized autocommand in Terminal mode', function()
+ test_case('WinResized', 'vsplit')
+ end)
+
+ it('TextChangedT autocommand in Terminal mode', function()
+ test_case('TextChangedT', [[call chansend(g:chan, "foo\r\nbar")]])
+ end)
+
+ it('TermRequest autocommand in Terminal mode', function()
+ test_case('TermRequest', [[call chansend(g:chan, "\x1b]11;?\x1b\\")]])
+ end)
+
+ it('TermRequest autocommand in Normal mode', function()
+ feed([[<C-\><C-N>]])
+ eq({ mode = 'nt', blocking = false }, api.nvim_get_mode())
+ test_case('TermRequest', [[call chansend(g:chan, "\x1b]11;?\x1b\\")]])
+ end)
+end)