From c14de47f1a12e1479221ab525d224d77f7eb5953 Mon Sep 17 00:00:00 2001 From: Sean Dewar <6256228+seandewar@users.noreply.github.com> Date: Sun, 4 Jan 2026 13:15:08 +0000 Subject: [PATCH] fix(window): crash closing only non-float if autocmds :tabonly (#37218) Problem: null pointer member access when closing the only non-float in the current tab page if autocommands after closing all floats also close all other tab pages. (making it the last window) Solution: check last_window again after closing the floats. Also reduce the scope of "wp"; it would be bugprone to use it before it's later reassigned to the rv of win_free_mem if freed by Buf/WinLeave. --- src/nvim/window.c | 15 +++++++++++---- test/functional/ui/float_spec.lua | 5 +++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/nvim/window.c b/src/nvim/window.c index 276b2a44b4..193e4757e4 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -97,6 +97,8 @@ typedef enum { WEE_TRIGGER_LEAVE_AUTOCMDS = 0x10, } wee_flags_T; +static const char e_cannot_close_last_window[] + = N_("E444: Cannot close last window"); static const char e_cannot_split_window_when_closing_buffer[] = N_("E1159: Cannot split a window when closing the buffer"); @@ -2710,7 +2712,7 @@ int win_close(win_T *win, bool free_buf, bool force) const bool had_diffmode = win->w_p_diff; if (last_window(win)) { - emsg(_("E444: Cannot close last window")); + emsg(_(e_cannot_close_last_window)); return FAIL; } @@ -2739,6 +2741,11 @@ int win_close(win_T *win, bool free_buf, bool force) if (!win_valid_any_tab(win)) { return FAIL; // window already closed by autocommands } + // Autocommands may have closed all other tabpages; check again. + if (last_window(win)) { + emsg(_(e_cannot_close_last_window)); + return FAIL; + } } else { emsg(e_floatonly); return FAIL; @@ -2769,7 +2776,6 @@ int win_close(win_T *win, bool free_buf, bool force) clear_snapshot(curtab, SNAP_QUICKFIX_IDX); } - win_T *wp; bool other_buffer = false; if (win == curwin) { @@ -2777,7 +2783,8 @@ int win_close(win_T *win, bool free_buf, bool force) // Guess which window is going to be the new current window. // This may change because of the autocommands (sigh). - wp = win->w_floating ? win_float_find_altwin(win, NULL) : frame2win(win_altframe(win, NULL)); + win_T *wp = win->w_floating ? win_float_find_altwin(win, NULL) + : frame2win(win_altframe(win, NULL)); // Be careful: If autocommands delete the window or cause this window // to be the last one left, return now. @@ -2872,7 +2879,7 @@ int win_close(win_T *win, bool free_buf, bool force) // Free the memory used for the window and get the window that received // the screen space. int dir; - wp = win_free_mem(win, &dir, NULL); + win_T *wp = win_free_mem(win, &dir, NULL); if (help_window || quickfix_window) { // Closing the help window moves the cursor back to the current window diff --git a/test/functional/ui/float_spec.lua b/test/functional/ui/float_spec.lua index ff48007543..1414e1df91 100644 --- a/test/functional/ui/float_spec.lua +++ b/test/functional/ui/float_spec.lua @@ -524,6 +524,11 @@ describe('float window', function() it('if called from non-floating window', function() api.nvim_set_current_win(old_win) eq('Vim:E444: Cannot close last window', pcall_err(api.nvim_win_close, old_win, false)) + -- Start with many tab pages, but make autocommands from closing floats leave us with just + -- one (where we're now the last window). + command('tabnew | autocmd WinClosed * ++once tabonly') + api.nvim_open_win(0, false, float_opts) + eq('Vim:E444: Cannot close last window', pcall_err(api.nvim_win_close, 0, true)) end) it('if called from floating window', function() eq('Vim:E444: Cannot close last window', pcall_err(api.nvim_win_close, old_win, false))