From fa24e045e998e84c54694c41df948f822bb6bde6 Mon Sep 17 00:00:00 2001 From: CompileAndConquer <69109614+LorenzoPiombini@users.noreply.github.com> Date: Sun, 30 Nov 2025 20:56:53 -0500 Subject: [PATCH] fix(buffer): defer w_buffer clearing to prevent dict watcher crash #36748 (cherry picked from commit f9ef1a4cab74defde29afcecfc2c819ecc476b27) --- src/nvim/buffer.c | 12 ++++- .../ex_cmds/dict_notifications_spec.lua | 47 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 405c0bfbf3..1a76bf5ed7 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -679,10 +679,14 @@ bool close_buffer(win_T *win, buf_T *buf, int action, bool abort_if_last, bool i return false; } + bool clear_w_buf = false; if (win != NULL // Avoid bogus clang warning. && win_valid_any_tab(win) && win->w_buffer == buf) { - win->w_buffer = NULL; // make sure we don't use the buffer now + // Defer clearing w_buffer until after operations that may invoke dict + // watchers (e.g., buf_clear_file()), so callers like tabpagebuflist() + // never see a window in the winlist with a NULL buffer. + clear_w_buf = true; } // Autocommands may have opened or closed windows for this buffer. @@ -693,6 +697,9 @@ bool close_buffer(win_T *win, buf_T *buf, int action, bool abort_if_last, bool i // Remove the buffer from the list. if (wipe_buf) { + if (clear_w_buf) { + win->w_buffer = NULL; + } // Do not wipe out the buffer if it is used in a window. if (buf->b_nwindows > 0) { return false; @@ -730,6 +737,9 @@ bool close_buffer(win_T *win, buf_T *buf, int action, bool abort_if_last, bool i buf->b_p_initialized = false; } buf_clear_file(buf); + if (clear_w_buf) { + win->w_buffer = NULL; + } if (del_buf) { buf->b_p_bl = false; } diff --git a/test/functional/ex_cmds/dict_notifications_spec.lua b/test/functional/ex_cmds/dict_notifications_spec.lua index e8fa305d99..30d066a058 100644 --- a/test/functional/ex_cmds/dict_notifications_spec.lua +++ b/test/functional/ex_cmds/dict_notifications_spec.lua @@ -524,3 +524,50 @@ describe('Vimscript dictionary notifications', function() eq({ 'W1', 'W2', 'W2', 'W1' }, eval('g:calls')) end) end) +describe('tabpagebuflist() with dict watcher during buffer close/wipe', function() + before_each(function() + clear() + end) + + it( + 'does not segfault when called from dict watcher on b:changedtick (bufhidden=unload)', + function() + command([[ + new + set bufhidden=unload + call dictwatcheradd(b:, 'changedtick', {-> tabpagebuflist()}) + close + ]]) + + assert_alive() + end + ) + + it('does not segfault when wiping buffer with dict watcher', function() + command([[ + new + call setline(1, 'test') + call dictwatcheradd(b:, 'changedtick', {-> tabpagebuflist()}) + bwipeout! + ]]) + + assert_alive() + end) + + it('does not segfault with multiple windows in the tabpage', function() + command([[ + " create two windows in the current tab + edit foo + vnew + call setline(1, 'bar') + + " attach watcher to the current buffer in the split + call dictwatcheradd(b:, 'changedtick', {-> tabpagebuflist()}) + + " close the split window (triggers close_buffer on this buffer) + close + ]]) + + assert_alive() + end) +end)