commit b67ac8cc6b5e5c7a40d6a56a3c0db40c9a7b77b8
parent efa6d91132c71d6efca1b813b0129b1085389a48
Author: zeertzjq <zeertzjq@outlook.com>
Date: Mon, 5 Jan 2026 08:00:50 +0800
fix(terminal): crash with race between buffer close and OSC 2 (#37225)
Problem: Crash when a terminal receives OSC 2 just after closing its
buffer but before terminal job exits.
Solution: Remove FUNC_ATTR_NONNULL_ALL from buf_set_term_title() and
check for NULL.
Diffstat:
2 files changed, 35 insertions(+), 2 deletions(-)
diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c
@@ -1358,8 +1358,11 @@ static int term_movecursor(VTermPos new_pos, VTermPos old_pos, int visible, void
}
static void buf_set_term_title(buf_T *buf, const char *title, size_t len)
- FUNC_ATTR_NONNULL_ALL
{
+ if (!buf) {
+ return; // In case of receiving OSC 2 between buffer close and job exit.
+ }
+
Error err = ERROR_INIT;
dict_set_var(buf->b_vars,
STATIC_CSTR_AS_STRING("term_title"),
@@ -1386,7 +1389,7 @@ static int term_settermprop(VTermProp prop, VTermValue *val, void *data)
break;
case VTERM_PROP_TITLE: {
- buf_T *buf = handle_get_buffer(term->buf_handle);
+ buf_T *buf = handle_get_buffer(term->buf_handle); // May be NULL
VTermStringFragment frag = val->string;
if (frag.initial && frag.final) {
diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua
@@ -425,6 +425,36 @@ describe(':terminal buffer', function()
]])
eq('TermLeave bar false', exec_lua('return _G.last_event'))
end)
+
+ it('no crash with race between buffer close and OSC 2', function()
+ skip(is_os('win'), 'tty-test cannot forward OSC 2 on Windows?')
+ exec_lua(function()
+ vim.api.nvim_chan_send(vim.bo.channel, '\027]2;SOME_TITLE\007')
+ end)
+ retry(nil, 4000, function()
+ eq('SOME_TITLE', api.nvim_buf_get_var(0, 'term_title'))
+ end)
+ screen:expect_unchanged()
+ --- @type string
+ local title_before_del = exec_lua(function()
+ vim.wait(10) -- Ensure there are no pending events.
+ vim.api.nvim_chan_send(vim.bo.channel, '\027]2;OTHER_TITLE\007')
+ vim.uv.run('once') -- Only process the pending write.
+ vim.uv.sleep(50) -- Block the event loop and wait for tty-test to forward OSC 2.
+ local term_title = vim.b.term_title
+ vim.api.nvim_buf_delete(0, { force = true })
+ vim.wait(10, nil, nil, true) -- Process fast events only.
+ return term_title
+ end)
+ -- Title isn't changed until the second vim.wait().
+ eq('SOME_TITLE', title_before_del)
+ screen:expect([[
+ ^ |
+ {100:~ }|*5
+ |
+ ]])
+ assert_alive()
+ end)
end)
describe(':terminal buffer', function()