fix(buffer): defer w_buffer clearing to prevent dict watcher crash #36748

(cherry picked from commit f9ef1a4cab)
This commit is contained in:
CompileAndConquer
2025-11-30 20:56:53 -05:00
committed by github-actions[bot]
parent 9fb49aacde
commit fa24e045e9
2 changed files with 58 additions and 1 deletions

View File

@@ -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;
}

View File

@@ -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)