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:
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)