From 92596a37e721c5d4cf4327e9c5348b0355ec29d0 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 14 Jan 2026 09:21:35 +0800 Subject: [PATCH 1/4] vim-patch:partial:9.0.0907: restoring window after WinScrolled may fail Problem: Restoring window after WinScrolled may fail. Solution: Lock the window layout when triggering WinScrolled. https://github.com/vim/vim/commit/d63a85592cef0ee4f0fec5efe2f8d66b31f01f05 Only check close_disallowed in window_layout_locked() for now. Also don't check window_layout_locked() when closing a floating window, as it's not checked when creating a floating window. Co-authored-by: Bram Moolenaar --- src/nvim/errors.h | 2 ++ src/nvim/ex_docmd.c | 35 ++++++++++++++++++++++++++--------- src/nvim/window.c | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/nvim/errors.h b/src/nvim/errors.h index 4ebf6d40c7..2982ee62e9 100644 --- a/src/nvim/errors.h +++ b/src/nvim/errors.h @@ -195,6 +195,8 @@ EXTERN const char e_stray_closing_curly_str[] INIT(= N_("E1278: Stray '}' without a matching '{': %s")); EXTERN const char e_missing_close_curly_str[] INIT(= N_("E1279: Missing '}': %s")); +EXTERN const char e_not_allowed_to_change_window_layout_in_this_autocmd[] +INIT(= N_("E1312: Not allowed to change the window layout in this autocmd")); EXTERN const char e_val_too_large[] INIT(= N_("E1510: Value too large: %s")); diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index e15c1d8b45..125646e348 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -5033,6 +5033,9 @@ void ex_win_close(int forceit, win_T *win, tabpage_T *tp) emsg(_(e_autocmd_close)); return; } + if (!win->w_floating && window_layout_locked()) { + return; + } buf_T *buf = win->w_buffer; @@ -5074,6 +5077,10 @@ static void ex_tabclose(exarg_T *eap) return; } + if (window_layout_locked()) { + return; + } + int tab_number = get_tabpage_arg(eap); if (eap->errmsg != NULL) { return; @@ -5105,6 +5112,10 @@ static void ex_tabonly(exarg_T *eap) return; } + if (window_layout_locked()) { + return; + } + int tab_number = get_tabpage_arg(eap); if (eap->errmsg != NULL) { return; @@ -5175,9 +5186,12 @@ void tabpage_close_other(tabpage_T *tp, int forceit) /// ":only". static void ex_only(exarg_T *eap) { - win_T *wp; + if (window_layout_locked()) { + return; + } if (eap->addr_count > 0) { + win_T *wp; linenr_T wnr = eap->line2; for (wp = firstwin; --wnr > 0;) { if (wp->w_next == NULL) { @@ -5185,11 +5199,9 @@ static void ex_only(exarg_T *eap) } wp = wp->w_next; } - } else { - wp = curwin; - } - if (wp != curwin) { - win_goto(wp); + if (wp != curwin) { + win_goto(wp); + } } close_others(true, eap->forceit); } @@ -5201,11 +5213,11 @@ static void ex_hide(exarg_T *eap) return; } + win_T *win = NULL; if (eap->addr_count == 0) { - win_close(curwin, false, eap->forceit); // don't free buffer + win = curwin; } else { int winnr = 0; - win_T *win = NULL; FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { winnr++; @@ -5217,8 +5229,13 @@ static void ex_hide(exarg_T *eap) if (win == NULL) { win = lastwin; } - win_close(win, false, eap->forceit); } + + if (!win->w_floating && window_layout_locked()) { + return; + } + + win_close(win, false, eap->forceit); // don't free buffer } /// ":stop" and ":suspend": Suspend Vim. diff --git a/src/nvim/window.c b/src/nvim/window.c index 2e1abf9a00..1514c26558 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -108,6 +108,38 @@ static char *m_onlyone = N_("Already only one window"); /// autocommands mess up the window structure. static int split_disallowed = 0; +/// When non-zero closing a window is forbidden. Used to avoid that nasty +/// autocommands mess up the window structure. +static int close_disallowed = 0; + +/// Disallow changing the window layout (split window, close window, move +/// window). Resizing is still allowed. +/// Used for autocommands that temporarily use another window and need to +/// make sure the previously selected window is still there. +/// Must be matched with exactly one call to window_layout_unlock()! +static void window_layout_lock(void) +{ + split_disallowed++; + close_disallowed++; +} + +static void window_layout_unlock(void) +{ + split_disallowed--; + close_disallowed--; +} + +/// When the window layout cannot be changed give an error and return true. +bool window_layout_locked(void) +{ + // if (split_disallowed > 0 || close_disallowed > 0) { + if (close_disallowed > 0) { + emsg(_(e_not_allowed_to_change_window_layout_in_this_autocmd)); + return true; + } + return false; +} + // #define WIN_DEBUG #ifdef WIN_DEBUG /// Call this method to log the current window layout. @@ -2739,6 +2771,9 @@ int win_close(win_T *win, bool free_buf, bool force) emsg(_(e_cannot_close_last_window)); return FAIL; } + if (!win->w_floating && window_layout_locked()) { + return FAIL; + } if (win_locked(win) || (win->w_buffer != NULL && win->w_buffer->b_locked > 0)) { @@ -4315,6 +4350,9 @@ int win_new_tabpage(int after, char *filename) emsg(_(e_cmdwin)); return FAIL; } + if (window_layout_locked()) { + return FAIL; + } tabpage_T *newtp = alloc_tabpage(); From 328640aed0ff43680e0d58a045ec3eca83f04571 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 14 Jan 2026 09:37:53 +0800 Subject: [PATCH 2/4] vim-patch:9.1.1323: b:undo_ftplugin not executed when re-using buffer Problem: b:undo_ftplugin not executed when re-using buffer (archy3) Solution: explicitly execute b:undo_ftplugin in buflist_new() when re-using the current buffer fixes: vim/vim#17113 closes: vim/vim#17133 https://github.com/vim/vim/commit/baa8c90cc0d214e036a3a7980d5cf95cae88a68d Cherry-pick test_filetype.vim changes from patch 9.1.1325. Co-authored-by: Christian Brabandt --- src/nvim/buffer.c | 13 +++++++++++++ src/nvim/window.c | 4 ++-- test/old/testdir/test_filetype.vim | 28 ++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 96ebfe6293..85cb824389 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -129,6 +129,18 @@ typedef enum { kBffInitChangedtick = 2, } BufFreeFlags; +static void trigger_undo_ftplugin(buf_T *buf, win_T *win) +{ + window_layout_lock(); + buf->b_locked++; + win->w_locked = true; + // b:undo_ftplugin may be set, undo it + do_cmdline_cmd("if exists('b:undo_ftplugin') | exe b:undo_ftplugin | endif"); + buf->b_locked--; + win->w_locked = false; + window_layout_unlock(); +} + /// Calculate the percentage that `part` is of the `whole`. int calc_percentage(int64_t part, int64_t whole) { @@ -1945,6 +1957,7 @@ buf_T *buflist_new(char *ffname_arg, char *sfname_arg, linenr_T lnum, int flags) assert(curbuf != NULL); buf = curbuf; set_bufref(&bufref, buf); + trigger_undo_ftplugin(buf, curwin); // It's like this buffer is deleted. Watch out for autocommands that // change curbuf! If that happens, allocate a new buffer anyway. buf_freeall(buf, BFA_WIPE | BFA_DEL); diff --git a/src/nvim/window.c b/src/nvim/window.c index 1514c26558..4fcf1ad85a 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -117,13 +117,13 @@ static int close_disallowed = 0; /// Used for autocommands that temporarily use another window and need to /// make sure the previously selected window is still there. /// Must be matched with exactly one call to window_layout_unlock()! -static void window_layout_lock(void) +void window_layout_lock(void) { split_disallowed++; close_disallowed++; } -static void window_layout_unlock(void) +void window_layout_unlock(void) { split_disallowed--; close_disallowed--; diff --git a/test/old/testdir/test_filetype.vim b/test/old/testdir/test_filetype.vim index 4cd6b764ca..022091b06a 100644 --- a/test/old/testdir/test_filetype.vim +++ b/test/old/testdir/test_filetype.vim @@ -1196,6 +1196,34 @@ func Test_filetype_indent_off() close endfunc +func Test_undo_ftplugin_on_buffer_reuse() + filetype on + + new + let b:undo_ftplugin = ":let g:var='exists'" + let g:bufnr = bufnr('%') + " no changes done to the buffer, so the buffer will be re-used + e $VIMRUNTIME/defaults.vim + call assert_equal(g:bufnr, bufnr('%')) + call assert_equal('exists', get(g:, 'var', 'fail')) + unlet! g:bufnr g:var + + " try to wipe the buffer + enew + bw defaults.vim + let b:undo_ftplugin = ':bw' + call assert_fails(':e $VIMRUNTIME/defaults.vim', 'E937:') + + " try to split the window + enew + bw defaults.vim + let b:undo_ftplugin = ':sp $VIMRUNTIME/defaults.vim' + call assert_fails(':e $VIMRUNTIME/defaults.vim', 'E242:') + + bwipe! + filetype off +endfunc + """"""""""""""""""""""""""""""""""""""""""""""""" " Tests for specific extensions and filetypes. " Keep sorted. From 6fa2ebec6b244eea8dde01ca72deeebd4546e537 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 14 Jan 2026 09:42:36 +0800 Subject: [PATCH 3/4] vim-patch:9.1.2023: [security]: Use-after-free in alist_add() with nasty autocmd Problem: A BufAdd autocommand may cause alist_add() to use freed memory, this is caused by the w_locked variable unset too early (henices) Solution: in trigger_undo_ftplugin() only set w_locked to false, if it was false when calling the function. related: v9.1.0678 closes: vim/vim#19023 https://github.com/vim/vim/commit/9266a2a19790dd3485b1dd32b3e27ba1d93e33d0 Co-authored-by: Christian Brabandt --- src/nvim/buffer.c | 3 ++- test/old/testdir/test_arglist.vim | 12 +++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 85cb824389..fb8235c1b3 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -131,13 +131,14 @@ typedef enum { static void trigger_undo_ftplugin(buf_T *buf, win_T *win) { + const bool win_was_locked = win->w_locked; window_layout_lock(); buf->b_locked++; win->w_locked = true; // b:undo_ftplugin may be set, undo it do_cmdline_cmd("if exists('b:undo_ftplugin') | exe b:undo_ftplugin | endif"); buf->b_locked--; - win->w_locked = false; + win->w_locked = win_was_locked; window_layout_unlock(); } diff --git a/test/old/testdir/test_arglist.vim b/test/old/testdir/test_arglist.vim index 9c4d4bb715..bcf2b055ec 100644 --- a/test/old/testdir/test_arglist.vim +++ b/test/old/testdir/test_arglist.vim @@ -776,7 +776,6 @@ func Test_crash_arglist_uaf() "%argdelete new one au BufAdd XUAFlocal :bw - "call assert_fails(':arglocal XUAFlocal', 'E163:') arglocal XUAFlocal au! BufAdd bw! XUAFlocal @@ -792,4 +791,15 @@ func Test_crash_arglist_uaf() au! BufAdd endfunc +" This was using freed memory again +func Test_crash_arglist_uaf2() + new + au BufAdd XUAFlocal :bw + arglocal XUAFlocal + redraw! + put ='abc' + 2# + au! BufAdd +endfunc + " vim: shiftwidth=2 sts=2 expandtab From 40fb2818b636b4d069a5bf6d16dff5d0f62a1775 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 14 Jan 2026 09:43:48 +0800 Subject: [PATCH 4/4] vim-patch:9.1.2085: Use-after-free in winframe_remove() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Use-after-free in winframe_remove() (henices) Solution: Set window_layout_locked() inside winframe_remove() and check that writing diff files is disallowed when the window layout is locked. It can happen with a custom diff expression when removing a window: 1. Buffer was removed, so win_frame_remove() is called to remove the window. 2. win_frame_remove() → frame_new_height() → scroll_to_fraction() → diff_check_fill() (checks for filler lines) 3. diff_check_fill() ends up causing a diff_try_update, and because we are not using internal diff, it has to first write the file to a buffer using buf_write() 4. buf_write() is called for a buffer that is not contained within a window, so it first calls aucmd_prepbuf() to create a new temporary window before writing the buffer and then later calls aucmd_restbuf(), which restores the previous window layout, calling winframe_remove() again, which will free the window/frame structure, eventually freeing stuff that will still be accessed at step 2. closes: vim/vim#19064 https://github.com/vim/vim/commit/ead1dda74a485ef0470e7252d07c1a36b8cde517 Nvim doesn't have this bug as Nvim uses a floating window as autocommand window, and removing it doesn't need winframe_remove(). Co-authored-by: Christian Brabandt --- src/nvim/diff.c | 8 ++++++++ src/nvim/window.c | 13 ++++++++++++ test/functional/ui/diff_spec.lua | 29 +++++++++++++++++++++++++++ test/old/testdir/test_diffmode.vim | 32 ++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+) diff --git a/src/nvim/diff.c b/src/nvim/diff.c index 094c4e612a..a56f371662 100644 --- a/src/nvim/diff.c +++ b/src/nvim/diff.c @@ -855,6 +855,14 @@ static int diff_write(buf_T *buf, diffin_T *din, linenr_T start, linenr_T end) return diff_write_buffer(buf, &din->din_mmfile, start, end); } + // Writing the diff buffers may trigger changes in the window structure + // via aucmd_prepbuf()/aucmd_restbuf() commands. + // This may cause recursively calling winframe_remove() which is not safe and causes + // use after free, so let's stop it here. + if (frames_locked()) { + return FAIL; + } + if (end < 0) { end = buf->b_ml.ml_line_count; } diff --git a/src/nvim/window.c b/src/nvim/window.c index 4fcf1ad85a..4142cd0053 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -112,6 +112,10 @@ static int split_disallowed = 0; /// autocommands mess up the window structure. static int close_disallowed = 0; +/// When non-zero changing the window frame structure is forbidden. Used +/// to avoid that winframe_remove() is called recursively +static int frame_locked = 0; + /// Disallow changing the window layout (split window, close window, move /// window). Resizing is still allowed. /// Used for autocommands that temporarily use another window and need to @@ -129,6 +133,11 @@ void window_layout_unlock(void) close_disallowed--; } +bool frames_locked(void) +{ + return frame_locked; +} + /// When the window layout cannot be changed give an error and return true. bool window_layout_locked(void) { @@ -3306,6 +3315,8 @@ win_T *winframe_remove(win_T *win, int *dirp, tabpage_T *tp, frame_T **unflat_al frame_T *frp_close = win->w_frame; + frame_locked++; + // Save the position of the containing frame (which will also contain the // altframe) before we remove anything, to recompute window positions later. const win_T *const topleft = frame2win(frp_close->fr_parent); @@ -3342,6 +3353,8 @@ win_T *winframe_remove(win_T *win, int *dirp, tabpage_T *tp, frame_T **unflat_al *unflat_altfr = altfr; } + frame_locked--; + return wp; } diff --git a/test/functional/ui/diff_spec.lua b/test/functional/ui/diff_spec.lua index 44711d174f..415085bb2b 100644 --- a/test/functional/ui/diff_spec.lua +++ b/test/functional/ui/diff_spec.lua @@ -3350,3 +3350,32 @@ describe("'diffanchors'", function() ]]) end) end) + +-- oldtest: Test_diffexpr_wipe_buffers() +it(':%bwipe does not crash when using diffexpr', function() + local screen = Screen.new(70, 20) + exec([[ + func DiffFuncExpr() + let in = readblob(v:fname_in) + let new = readblob(v:fname_new) + let out = v:lua.vim.text.diff(in, new) + call writefile(split(out, "\n"), v:fname_out) + endfunc + + new + vnew + set diffexpr=DiffFuncExpr() + wincmd l + new + call setline(1,range(20)) + windo diffthis + wincmd w + hide + %bw! + ]]) + screen:expect([[ + ^ | + {1:~ }|*18 + 4 buffers wiped out | + ]]) +end) diff --git a/test/old/testdir/test_diffmode.vim b/test/old/testdir/test_diffmode.vim index e7d8da98a9..3e6304a252 100644 --- a/test/old/testdir/test_diffmode.vim +++ b/test/old/testdir/test_diffmode.vim @@ -3311,4 +3311,36 @@ func Test_diff_add_prop_in_autocmd() call StopVimInTerminal(buf) endfunc +" this was causing a use-after-free by callig winframe_remove() rerursively +func Test_diffexpr_wipe_buffers() + CheckRunVimInTerminal + + let lines =<< trim END + def DiffFuncExpr() + var in: list = readfile(v:fname_in) + var new = readfile(v:fname_new) + var out: string = diff(in, new) + writefile(split(out, "n"), v:fname_out) + enddef + + new + vnew + set diffexpr=DiffFuncExpr() + wincmd l + new + cal setline(1,range(20)) + wind difft + wincm w + hid + %bw! + END + call writefile(lines, 'Xtest_diffexpr_wipe', 'D') + + let buf = RunVimInTerminal('Xtest_diffexpr_wipe', {}) + call term_sendkeys(buf, ":so\") + call WaitForAssert({-> assert_match('4 buffers wiped out', term_getline(buf, 20))}) + + call StopVimInTerminal(buf) +endfunc + " vim: shiftwidth=2 sts=2 expandtab