From 50a38d9698e6740a8e0c17c826df94480baf1344 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Thu, 30 Oct 2025 13:26:49 +0800 Subject: [PATCH] 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. --- src/nvim/tui/tui.c | 8 ++-- test/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 index 4da08a2209..e90de6f062 100644 --- 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 index 26f3867bd8..0b6e2c4e54 100644 --- 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)