fix(tabpage): focusing a tab while closing it, fails assert #41475

Problem:
If an autocommand handler focuses the tab page we're closing during a
`:tabonly` (with some conditions), we hit an assert failure in
`win_close_othertab()`.

For this to occur, we need:
- `nvim_buf_delete()` to trigger `close_windows()` (as is done in
	`cmdwin.lua`'s `_cleanup()`)
- `close_windows()` then calls `win_close_othertab()`, removing the
	window (`win_free_mem()`)
- then in the caller, `tabpage_close_other()`, the loop continues (we
	don't detect `tp_lastwin == wp` since we've unlinked the window)
- the loop assumes that `curtab != tp`

but we've refocused `curtab` so the `ex_win_close()` call passes `tp` as
`curtab`, causing the assert to fail

Solution:
Detect the focus of `curtab` and abort closing the tab.
This commit is contained in:
Rob Pilling
2026-08-28 13:39:16 +01:00
committed by GitHub
parent fb083f3850
commit f5d4b5975d
2 changed files with 45 additions and 2 deletions

View File

@@ -5165,9 +5165,11 @@ static void ex_pclose(exarg_T *eap)
/// Close window "win" and take care of handling closing the last window for a
/// modified buffer.
///
/// @param tp NULL or the tab page "win" is in
/// @param tp NULL or the tab page "win" is in, if different from "curtab"
void ex_win_close(int forceit, win_T *win, tabpage_T *tp)
{
assert(tp == NULL || tp != curtab);
// Never close the autocommand window.
if (is_ctx_win(win)) {
emsg(_(e_autocmd_close));
@@ -5276,6 +5278,8 @@ static void ex_tabonly(exarg_T *eap)
/// Close the current tab page.
void tabpage_close(int forceit)
{
int done = 0;
if (window_layout_locked(CMD_tabclose)) {
return;
}
@@ -5286,9 +5290,13 @@ void tabpage_close(int forceit)
// First close all the windows but the current one. If that worked then
// close the last window in this tab, that will close it.
while (curwin->w_floating) {
while (curwin->w_floating && ++done < 1000) {
ex_win_close(forceit, curwin, NULL);
}
if (done == 1000) {
return;
}
if (!ONE_WINDOW) {
close_others(true, forceit, true);
}
@@ -5311,6 +5319,8 @@ void tabpage_close_other(tabpage_T *tp, int forceit)
int done = 0;
char prev_idx[NUMBUFLEN];
assert(tp != curtab);
if (window_layout_locked(CMD_SIZE)) {
return;
}
@@ -5323,12 +5333,20 @@ void tabpage_close_other(tabpage_T *tp, int forceit)
while (++done < 1000) {
snprintf(prev_idx, sizeof(prev_idx), "%i", tabpage_index(tp));
win_T *wp = tp->tp_lastwin;
// Autocommands (including TabClosedPre above) may change the current tab page,
// abort a `:tabonly` (etc) if we're now on the tab we're trying to close
if (tp == curtab) {
done = 1000;
break;
}
ex_win_close(forceit, wp, tp);
// Autocommands may delete the tab page under our fingers.
if (!valid_tabpage(tp)) {
break;
}
// We may fail to close a window with a modified buffer.
if (tp->tp_lastwin == wp) {
done = 1000;

View File

@@ -10,6 +10,7 @@ local neq = t.neq
local feed = n.feed
local eval = n.eval
local exec = n.exec
local exec_lua = n.exec_lua
local fn = n.fn
local api = n.api
local curwin = n.api.nvim_get_current_win
@@ -173,4 +174,28 @@ describe('tabpage', function()
quit
]])
end)
it(
'no crash when :tabclose/:tabonly and WinClosed autocmd wipes buf and switches to closing tab',
function()
exec_lua(function()
local win1 = vim.api.nvim_get_current_win()
vim.cmd('botright new')
local win2 = vim.api.nvim_get_current_win()
local buf2 = vim.api.nvim_get_current_buf()
vim.cmd('tabedit')
vim.api.nvim_create_autocmd('WinClosed', {
pattern = tostring(win2),
callback = function()
-- Wipe the closing window's buffer and switch back to the original window,
-- so `curtab == tp` in tabpage_close_other's loop.
vim.api.nvim_buf_delete(buf2, { force = true })
vim.api.nvim_set_current_win(win1)
end,
})
vim.cmd('tabonly')
end)
assert_alive()
end
)
end)