diff --git a/src/nvim/api/events.c b/src/nvim/api/events.c index 6290e52ffa..2df152795b 100644 --- a/src/nvim/api/events.c +++ b/src/nvim/api/events.c @@ -67,10 +67,6 @@ void nvim_ui_term_event(uint64_t channel_id, String event, Object value, Error * const String termresponse = value.data.string; set_vim_var_string(VV_TERMRESPONSE, termresponse.data, (ptrdiff_t)termresponse.size); - - MAXSIZE_TEMP_DICT(data, 1); - PUT_C(data, "sequence", value); - apply_autocmds_group(EVENT_TERMRESPONSE, NULL, NULL, true, AUGROUP_ALL, NULL, NULL, - &DICT_OBJ(data)); + do_termresponse_autocmd(termresponse); } } diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c index fc9e28339a..183750857b 100644 --- a/src/nvim/autocmd.c +++ b/src/nvim/autocmd.c @@ -105,7 +105,7 @@ static int autocmd_blocked = 0; // block all autocmds static bool autocmd_nested = false; static bool autocmd_include_groups = false; -static char *old_termresponse = NULL; +static bool termresponse_changed = false; // Map of autocmd group names and ids. // name -> ID @@ -2033,13 +2033,22 @@ BYPASS_AU: return retval; } +void do_termresponse_autocmd(const String sequence) +{ + MAXSIZE_TEMP_DICT(data, 1); + PUT_C(data, "sequence", STRING_OBJ(sequence)); + apply_autocmds_group(EVENT_TERMRESPONSE, NULL, NULL, true, AUGROUP_ALL, NULL, NULL, + &DICT_OBJ(data)); + termresponse_changed = true; +} + // Block triggering autocommands until unblock_autocmd() is called. // Can be used recursively, so long as it's symmetric. void block_autocmds(void) { - // Remember the value of v:termresponse. + // Detect if v:termresponse is set while blocked. if (!is_autocmd_blocked()) { - old_termresponse = get_vim_var_str(VV_TERMRESPONSE); + termresponse_changed = false; } autocmd_blocked++; } @@ -2051,8 +2060,11 @@ void unblock_autocmds(void) // When v:termresponse was set while autocommands were blocked, trigger // the autocommands now. Esp. useful when executing a shell command // during startup (nvim -d). - if (!is_autocmd_blocked() && get_vim_var_str(VV_TERMRESPONSE) != old_termresponse) { - apply_autocmds(EVENT_TERMRESPONSE, NULL, NULL, false, curbuf); + if (!is_autocmd_blocked() && termresponse_changed && has_event(EVENT_TERMRESPONSE)) { + // Copied to a new allocation, as termresponse may be freed during the event. + const String sequence = cstr_to_string(get_vim_var_str(VV_TERMRESPONSE)); + do_termresponse_autocmd(sequence); + api_free_string(sequence); } } diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 6f2cc4db55..f9ecbeb865 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -2707,6 +2707,71 @@ describe('TUI', function() end) end) + it('TermResponse from unblock_autocmds() sets "data"', function() + if not child_exec_lua('return pcall(require, "ffi")') then + pending('N/A: missing LuaJIT FFI') + end + child_exec_lua([[ + local ffi = require('ffi') + ffi.cdef[=[ + void block_autocmds(void); + void unblock_autocmds(void); + ]=] + ffi.C.block_autocmds() + vim.api.nvim_create_autocmd('TermResponse', { + once = true, + callback = function(ev) + _G.data = ev.data + end, + }) + ]]) + feed_data('\027P0$r\027\\') + retry(nil, 4000, function() + eq('\027P0$r', child_exec_lua('return vim.v.termresponse')) + end) + eq(vim.NIL, child_exec_lua('return _G.data')) + child_exec_lua('require("ffi").C.unblock_autocmds()') + eq({ sequence = '\027P0$r' }, child_exec_lua('return _G.data')) + + -- If TermResponse during TermResponse changes v:termresponse, data.sequence contains the actual + -- response that triggered the autocommand. + -- The second autocommand below forces a use-after-free when v:termresponse's value changes + -- during TermResponse if data.sequence didn't allocate its own copy. + child_exec_lua([[ + require('ffi').C.block_autocmds() + vim.api.nvim_create_autocmd('TermResponse', { + once = true, + callback = function(ev) + _G.au1_termresponse1 = vim.v.termresponse + _G.au1_sequence1 = ev.data.sequence + local chan = vim.fn.sockconnect('pipe', vim.v.servername, { rpc = true }) + vim.rpcrequest(chan, 'nvim_ui_term_event', 'termresponse', 'baz') + _G.au1_termresponse2 = vim.v.termresponse + _G.au1_sequence2 = ev.data.sequence + end, + }) + _G.au2_sequences = {} + vim.api.nvim_create_autocmd('TermResponse', { + callback = function(ev) + table.insert(_G.au2_sequences, ev.data.sequence) + end, + }) + ]]) + child_session:request('nvim_ui_term_event', 'termresponse', 'foobar') + eq('foobar', child_exec_lua('return vim.v.termresponse')) + -- For good measure, check deferred TermResponse doesn't try to fire if autocmds are still + -- blocked after unblock_autocmds. + child_exec_lua('require("ffi").C.block_autocmds() require("ffi").C.unblock_autocmds()') + eq(vim.NIL, child_exec_lua('return _G.au1_termresponse1')) + child_exec_lua('require("ffi").C.unblock_autocmds()') + eq('foobar', child_exec_lua('return _G.au1_termresponse1')) + eq('foobar', child_exec_lua('return _G.au1_sequence1')) + eq('baz', child_exec_lua('return _G.au1_termresponse2')) + eq('foobar', child_exec_lua('return _G.au1_sequence2')) -- unchanged + -- Second autocmd triggers due to "baz" (via the nested TermResponse), then from "foobar". + eq({ 'baz', 'foobar' }, child_exec_lua('return _G.au2_sequences')) + end) + it('nvim_ui_send works', function() child_session:request('nvim_ui_send', '\027]2;TEST_TITLE\027\\') retry(nil, nil, function()