From b4039ec0b026938051061ee93618a0d14e54581c Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Tue, 27 Jan 2026 12:45:49 +0800 Subject: [PATCH 1/2] fix(terminal): don't allow b:term_title watcher to delete buffer --- src/nvim/terminal.c | 2 ++ test/functional/terminal/buffer_spec.lua | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index 07c6d7bf4c..1053c43b7e 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -1416,6 +1416,7 @@ static void buf_set_term_title(buf_T *buf, const char *title, size_t len) } Error err = ERROR_INIT; + buf->b_locked++; dict_set_var(buf->b_vars, STATIC_CSTR_AS_STRING("term_title"), STRING_OBJ(((String){ .data = (char *)title, .size = len })), @@ -1423,6 +1424,7 @@ static void buf_set_term_title(buf_T *buf, const char *title, size_t len) false, NULL, &err); + buf->b_locked--; api_clear_error(&err); status_redraw_buf(buf); } diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua index f9f896a507..a8cc6bdb40 100644 --- a/test/functional/terminal/buffer_spec.lua +++ b/test/functional/terminal/buffer_spec.lua @@ -1134,6 +1134,16 @@ describe(':terminal buffer', function() feed([[]]) eq({ mode = 'nt', blocking = false }, api.nvim_get_mode()) end) + + it('does not allow b:term_title watcher to delete buffer', function() + local chan = api.nvim_open_term(0, {}) + api.nvim_chan_send(chan, '\027]2;SOME_TITLE\007') + eq('SOME_TITLE', api.nvim_buf_get_var(0, 'term_title')) + command([[call dictwatcheradd(b:, 'term_title', {-> execute('bwipe!')})]]) + api.nvim_chan_send(chan, '\027]2;OTHER_TITLE\007') + eq('OTHER_TITLE', api.nvim_buf_get_var(0, 'term_title')) + matches('^E937: ', api.nvim_get_vvar('errmsg')) + end) end) describe('on_lines does not emit out-of-bounds line indexes when', function() From 9540e7470beb1fce987c411564effe6625fd0528 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Tue, 27 Jan 2026 13:12:15 +0800 Subject: [PATCH 2/2] fix(terminal): possible heap-use-after-free during Nvim exit Usually, terminal_close() calls refresh_terminal(), which allocates the scrollback buffer, and term_may_alloc_scrollback() in terminal_open() won't dereference the buffer. However, refresh_terminal() is not called during Nvim exit, in which case a heap-use-after-free may happen if TermOpen wipes buffer. Check for non-NULL buf_handle to avoid that. --- src/nvim/terminal.c | 2 +- test/functional/terminal/channel_spec.lua | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index 1053c43b7e..1c9457b671 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -566,7 +566,7 @@ void terminal_open(Terminal **termpp, buf_T *buf, TerminalOptions opts) aucmd_restbuf(&aco); - if (*termpp == NULL) { + if (*termpp == NULL || term->buf_handle == 0) { return; // Terminal has already been destroyed. } diff --git a/test/functional/terminal/channel_spec.lua b/test/functional/terminal/channel_spec.lua index d18e8cc11e..924909f2d2 100644 --- a/test/functional/terminal/channel_spec.lua +++ b/test/functional/terminal/channel_spec.lua @@ -213,6 +213,16 @@ describe('no crash when TermOpen autocommand', function() ]]) assert_alive() end) + + it('wipes buffer when using jobstart(…,{term=true}) during Nvim exit', function() + n.expect_exit(n.exec_lua, function() + vim.schedule(function() + vim.fn.jobstart(term_args, { term = true }) + end) + vim.cmd('autocmd TermOpen * bwipe!') + vim.cmd('qall!') + end) + end) end) describe('nvim_open_term', function()