commit f9ef1a4cab74defde29afcecfc2c819ecc476b27
parent cdc6f8511131f6a769403b11dba9287524ffad3d
Author: CompileAndConquer <69109614+LorenzoPiombini@users.noreply.github.com>
Date: Sun, 30 Nov 2025 20:56:53 -0500
fix(buffer): defer w_buffer clearing to prevent dict watcher crash #36748
Diffstat:
2 files changed, 58 insertions(+), 1 deletion(-)
diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c
@@ -686,10 +686,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.
@@ -700,6 +704,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 true;
@@ -737,6 +744,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
@@ -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)