neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

commit 50a38d9698e6740a8e0c17c826df94480baf1344
parent 69bddc089fb878c8bfb2d0ae35c783a3980aee6e
Author: zeertzjq <zeertzjq@outlook.com>
Date:   Thu, 30 Oct 2025 13:26:49 +0800

fix(tui): heap-use-after-free when resuming (#36387)

Discovered when writing more tests for suspend/resume.
It seems that this isn't always reproducible with ASAN due to the arena.
Diffstat:
Msrc/nvim/tui/tui.c | 8+++++---
Mtest/functional/terminal/tui_spec.lua | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 70 insertions(+), 3 deletions(-)

diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c @@ -368,6 +368,7 @@ static void terminfo_start(TUIData *tui) tui->input.tui_data = tui; tui->ti_arena = (Arena)ARENA_EMPTY; + assert(tui->term == NULL); char *term = os_getenv("TERM"); #ifdef MSWIN @@ -384,9 +385,7 @@ static void terminfo_start(TUIData *tui) bool found_in_db = false; if (term) { if (terminfo_from_unibilium(&tui->ti, term, &tui->ti_arena)) { - if (!tui->term) { - tui->term = arena_strdup(&tui->ti_arena, term); - } + tui->term = arena_strdup(&tui->ti_arena, term); found_in_db = true; } } @@ -590,6 +589,9 @@ static void terminfo_stop(TUIData *tui) abort(); } arena_mem_free(arena_finish(&tui->ti_arena)); + // Avoid using freed memory. + memset(&tui->ti, 0, sizeof(tui->ti)); + tui->term = NULL; } static void tui_terminal_start(TUIData *tui) diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua @@ -4141,4 +4141,69 @@ describe('TUI client', function() test_remote_tui_quit(42) end) end) + + it('suspend/resume works with multiple clients', function() + local server_super, screen_server, screen_client = start_tui_and_remote_client() + local server_super_exec_lua = tt.make_lua_executor(server_super) + + local screen_normal = [[ + Hello, Worl^d | + {100:~ }|*3 + {3:[No Name] [+] }| + | + {5:-- TERMINAL --} | + ]] + local screen_suspended = [[ + ^ | + |*5 + {5:-- TERMINAL --} | + ]] + + screen_client:expect({ grid = screen_normal, unchanged = true }) + screen_server:expect({ grid = screen_normal, unchanged = true }) + + -- Suspend both clients. + feed_data(':suspend\r') + screen_client:expect({ grid = screen_suspended }) + screen_server:expect({ grid = screen_suspended }) + + -- Resume the remote client. + exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigcont')]]) + screen_client:expect({ grid = screen_normal }) + screen_server:expect({ grid = screen_suspended, unchanged = true }) + + -- Resume the embedding client. + server_super_exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigcont')]]) + screen_server:expect({ grid = screen_normal }) + screen_client:expect({ grid = screen_normal, unchanged = true }) + + -- Suspend both clients again. + feed_data(':suspend\r') + screen_client:expect({ grid = screen_suspended }) + screen_server:expect({ grid = screen_suspended }) + + -- Resume the remote client. + exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigcont')]]) + screen_client:expect({ grid = screen_normal }) + screen_server:expect({ grid = screen_suspended, unchanged = true }) + + -- Suspend the remote client again. + feed_data(':suspend\r') + screen_client:expect({ grid = screen_suspended }) + screen_server:expect({ grid = screen_suspended, unchanged = true }) + + -- Resume the embedding client. + server_super_exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigcont')]]) + screen_server:expect({ grid = screen_normal }) + screen_client:expect({ grid = screen_suspended, unchanged = true }) + + -- Resume the remote client. + exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigcont')]]) + screen_client:expect({ grid = screen_normal }) + screen_server:expect({ grid = screen_normal, unchanged = true }) + + feed_data(':quit!\r') + screen_server:expect({ any = vim.pesc('[Process exited 0]') }) + screen_client:expect({ any = vim.pesc('[Process exited 0]') }) + end) end)