commit 0a0c349b6fee01a8365e13e5a9dc194fcad4fd0a
parent 87bd16e470b2d7a45f38c7402037ad1bcf91ded9
Author: Kent Sibilev <ksibilev@yahoo.com>
Date: Tue, 25 Nov 2025 21:27:52 -0500
fix(events): crash on WinScrolled #35995
Problem: apply_autocmds function can free both buf_T and win_T pointers
Solution: instead retain winids for WinResized and WinScrolled
autocmds and use curbuf pointer, which is consistent with other uses
of apply_autocmds function
Diffstat:
2 files changed, 69 insertions(+), 7 deletions(-)
diff --git a/src/nvim/window.c b/src/nvim/window.c
@@ -5788,6 +5788,21 @@ void may_trigger_win_scrolled_resized(void)
recursive = true;
+ // Save window info before autocmds since they can free windows
+ char resize_winid[NUMBUFLEN];
+ bufref_T resize_bufref;
+ if (trigger_resize) {
+ vim_snprintf(resize_winid, sizeof(resize_winid), "%d", first_size_win->handle);
+ set_bufref(&resize_bufref, first_size_win->w_buffer);
+ }
+
+ char scroll_winid[NUMBUFLEN];
+ bufref_T scroll_bufref;
+ if (trigger_scroll) {
+ vim_snprintf(scroll_winid, sizeof(scroll_winid), "%d", first_scroll_win->handle);
+ set_bufref(&scroll_bufref, first_scroll_win->w_buffer);
+ }
+
// If both are to be triggered do WinResized first.
if (trigger_resize) {
save_v_event_T save_v_event;
@@ -5795,10 +5810,8 @@ void may_trigger_win_scrolled_resized(void)
if (tv_dict_add_list(v_event, S_LEN("windows"), windows_list) == OK) {
tv_dict_set_keys_readonly(v_event);
-
- char winid[NUMBUFLEN];
- vim_snprintf(winid, sizeof(winid), "%d", first_size_win->handle);
- apply_autocmds(EVENT_WINRESIZED, winid, winid, false, first_size_win->w_buffer);
+ buf_T *buf = bufref_valid(&resize_bufref) ? resize_bufref.br_buf : curbuf;
+ apply_autocmds(EVENT_WINRESIZED, resize_winid, resize_winid, false, buf);
}
restore_v_event(v_event, &save_v_event);
}
@@ -5812,9 +5825,8 @@ void may_trigger_win_scrolled_resized(void)
tv_dict_set_keys_readonly(v_event);
tv_dict_unref(scroll_dict);
- char winid[NUMBUFLEN];
- vim_snprintf(winid, sizeof(winid), "%d", first_scroll_win->handle);
- apply_autocmds(EVENT_WINSCROLLED, winid, winid, false, first_scroll_win->w_buffer);
+ buf_T *buf = bufref_valid(&scroll_bufref) ? scroll_bufref.br_buf : curbuf;
+ apply_autocmds(EVENT_WINSCROLLED, scroll_winid, scroll_winid, false, buf);
restore_v_event(v_event, &save_v_event);
}
diff --git a/test/functional/autocmd/win_scrolled_resized_spec.lua b/test/functional/autocmd/win_scrolled_resized_spec.lua
@@ -349,4 +349,54 @@ describe('WinScrolled', function()
[winid_str] = { leftcol = 0, topline = -3, topfill = 0, width = 0, height = 0, skipcol = 0 },
}, eval('g:v_event'))
end)
+
+ it('does not crash when WinResized closes popup before WinScrolled #35803', function()
+ exec([[
+ set scrolloff=0
+ call setline(1, range(1, 100))
+
+ " Create first popup window (will be resized and closed)
+ let buf1 = nvim_create_buf(v:false, v:true)
+ call nvim_buf_set_lines(buf1, 0, -1, v:false, map(range(1, 50), 'string(v:val)'))
+ let popup1 = nvim_open_win(buf1, v:false, {
+ \ 'relative': 'editor',
+ \ 'width': 20,
+ \ 'height': 5,
+ \ 'col': 10,
+ \ 'row': 5
+ \ })
+
+ " Create second popup window (will be scrolled)
+ let buf2 = nvim_create_buf(v:false, v:true)
+ call nvim_buf_set_lines(buf2, 0, -1, v:false, map(range(1, 50), 'string(v:val)'))
+ let popup2 = nvim_open_win(buf2, v:false, {
+ \ 'relative': 'editor',
+ \ 'width': 20,
+ \ 'height': 5,
+ \ 'col': 35,
+ \ 'row': 5
+ \ })
+
+ let g:resized = 0
+ let g:scrolled = 0
+
+ " WinResized autocmd resizes and closes the first popup
+ autocmd WinResized * let g:resized += 1 | call nvim_win_set_height(popup1, 10) | call nvim_win_close(popup1, v:true)
+ " WinScrolled autocmd scrolls the second popup
+ autocmd WinScrolled * let g:scrolled += 1 | call nvim_win_call(popup2, {-> execute('normal! \<C-E>')})
+ ]])
+ eq(0, eval('g:resized'))
+ eq(0, eval('g:scrolled'))
+
+ -- Trigger a resize on popup1, which will close it
+ -- This should trigger WinResized (which closes popup1) and WinScrolled (which scrolls popup2)
+ -- Before the fix, WinScrolled would use the freed pointer causing a crash
+ api.nvim_win_set_height(eval('popup1'), 8)
+
+ -- The key is that it should not crash when WinResized closes a window
+ -- that WinScrolled might have referenced via a stale buf_T pointer
+ assert_alive()
+ -- Verify autocmds were actually triggered
+ eq(1, eval('g:resized > 0'))
+ end)
end)