patch 9.1.1361: [security]: possible use-after-free when closing a buffer

Commit: 
https://github.com/vim/vim/commit/6cb1c828406dcbb9b67ee788501b94f3a0bac88a
Author: Sean Dewar <6256228+seande...@users.noreply.github.com>
Date:   Sat May 3 18:37:27 2025 +0200

    patch 9.1.1361: [security]: possible use-after-free when closing a buffer
    
    Problem:  [security]: Possible to open more windows into a closing
              buffer without splitting, bypassing existing "b_locked_split"
              checks and triggering use-after-free
    Solution: Disallow switching to a closing buffer. Editing a closing
              buffer (via ":edit", etc.) was fixed in v9.1.0764, but add an
              error message and check just "b_locked_split", as "b_locked"
              is necessary only when the buffer shouldn't be wiped, and may
              be set for buffers that are in-use but not actually closing.
              (Sean Dewar)
    
    closes: #17246
    
    Signed-off-by: Sean Dewar <6256228+seande...@users.noreply.github.com>
    Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/src/buffer.c b/src/buffer.c
index 697efa30c..b4481b2f1 100644
--- a/src/buffer.c
+++ b/src/buffer.c
@@ -526,12 +526,6 @@ can_unload_buffer(buf_T *buf)
     return can_unload;
 }
 
-    int
-buf_locked(buf_T *buf)
-{
-    return buf->b_locked || buf->b_locked_split;
-}
-
 /*
  * Close the link to a buffer.
  * "action" is used when there is no longer a window for the buffer.
@@ -1432,12 +1426,19 @@ do_buffer_ext(
     if ((flags & DOBUF_NOPOPUP) && bt_popup(buf) && !bt_terminal(buf))
        return OK;
 #endif
-    if (
-       action == DOBUF_GOTO
-       && buf != curbuf
-       && !check_can_set_curbuf_forceit((flags & DOBUF_FORCEIT) ? TRUE : 
FALSE))
-      // disallow navigating to another buffer when 'winfixbuf' is applied
-      return FAIL;
+    if (action == DOBUF_GOTO && buf != curbuf)
+    {
+       if (!check_can_set_curbuf_forceit((flags & DOBUF_FORCEIT) != 0))
+           // disallow navigating to another buffer when 'winfixbuf' is applied
+           return FAIL;
+       if (buf->b_locked_split)
+       {
+           // disallow navigating to a closing buffer, which like splitting,
+           // can result in more windows displaying it
+           emsg(_(e_cannot_switch_to_a_closing_buffer));
+           return FAIL;
+       }
+    }
 
     if ((action == DOBUF_GOTO || action == DOBUF_SPLIT)
                                                  && (buf->b_flags & BF_DUMMY))
diff --git a/src/errors.h b/src/errors.h
index 90cc2b143..d718507e0 100644
--- a/src/errors.h
+++ b/src/errors.h
@@ -3728,3 +3728,5 @@ EXTERN char e_failed_resizing_quickfix_stack[]
 EXTERN char e_no_quickfix_stack[]
        INIT(= N_("E1545: Quickfix list stack unavailable"));
 #endif
+EXTERN char e_cannot_switch_to_a_closing_buffer[]
+       INIT(= N_("E1546: Cannot switch to a closing buffer"));
diff --git a/src/ex_cmds.c b/src/ex_cmds.c
index e1e6c4ee7..4eb67e31f 100644
--- a/src/ex_cmds.c
+++ b/src/ex_cmds.c
@@ -2743,9 +2743,9 @@ do_ecmd(
        }
        if (buf == NULL)
            goto theend;
-       // autocommands try to edit a file that is going to be removed,
-       // abort
-       if (buf_locked(buf))
+       // autocommands try to edit a closing buffer, which like splitting, can
+       // result in more windows displaying it; abort
+       if (buf->b_locked_split)
        {
            // window was split, but not editing the new buffer,
            // reset b_nwindows again
@@ -2753,6 +2753,7 @@ do_ecmd(
                    && curwin->w_buffer != NULL
                    && curwin->w_buffer->b_nwindows > 1)
                --curwin->w_buffer->b_nwindows;
+           emsg(_(e_cannot_switch_to_a_closing_buffer));
            goto theend;
        }
        if (curwin->w_alt_fnum == buf->b_fnum && prev_alt_fnum != 0)
diff --git a/src/proto/buffer.pro b/src/proto/buffer.pro
index 8384601ce..9044bae83 100644
--- a/src/proto/buffer.pro
+++ b/src/proto/buffer.pro
@@ -5,7 +5,6 @@ int open_buffer(int read_stdin, exarg_T *eap, int flags_arg);
 void set_bufref(bufref_T *bufref, buf_T *buf);
 int bufref_valid(bufref_T *bufref);
 int buf_valid(buf_T *buf);
-int buf_locked(buf_T *buf);
 int close_buffer(win_T *win, buf_T *buf, int action, int abort_if_last, int 
ignore_abort);
 void buf_clear_file(buf_T *buf);
 void buf_freeall(buf_T *buf, int flags);
diff --git a/src/structs.h b/src/structs.h
index 898f620ef..caf61eda8 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -3072,7 +3072,7 @@ struct file_buffer
     int                b_locked;       // Buffer is being closed or 
referenced, don't
                                // let autocommands wipe it out.
     int                b_locked_split; // Buffer is being closed, don't allow 
opening
-                               // a new window with it.
+                               // it in more windows.
 
     /*
      * b_ffname has the full path of the file (NULL for no name).
diff --git a/src/testdir/test_autocmd.vim b/src/testdir/test_autocmd.vim
index d5d516ce2..8d083db74 100644
--- a/src/testdir/test_autocmd.vim
+++ b/src/testdir/test_autocmd.vim
@@ -5035,7 +5035,8 @@ func Test_autocmd_BufWinLeave_with_vsp()
   exe "e " fname
   vsp
   augroup testing
-    exe "au BufWinLeave " .. fname .. " :e " dummy .. "| vsp " .. fname
+    exe 'au BufWinLeave' fname 'e' dummy
+          \ '| call assert_fails(''vsp' fname ''', ''E1546:'')'
   augroup END
   bw
   call CleanUpTestAuGroup()
diff --git a/src/testdir/test_buffer.vim b/src/testdir/test_buffer.vim
index 757ba0578..36a6ef503 100644
--- a/src/testdir/test_buffer.vim
+++ b/src/testdir/test_buffer.vim
@@ -569,4 +569,39 @@ func Test_buflist_alloc_failure()
   call assert_fails('cexpr "XallocFail6:10:Line10"', 'E342:')
 endfunc
 
+func Test_closed_buffer_still_in_window()
+  %bw!
+
+  let s:w = win_getid()
+  new
+  let s:b = bufnr()
+  setl bufhidden=wipe
+
+  augroup ViewClosedBuffer
+    autocmd!
+    autocmd BufUnload * ++once call assert_fails(
+          \ 'call win_execute(s:w, "' .. s:b .. 'b")', 'E1546:')
+  augroup END
+  quit!
+  " Previously resulted in s:b being curbuf while unloaded (no memfile).
+  call assert_equal(1, bufloaded(bufnr()))
+  call assert_equal(0, bufexists(s:b))
+
+  let s:w = win_getid()
+  split
+  new
+  let s:b = bufnr()
+
+  augroup ViewClosedBuffer
+    autocmd!
+    autocmd BufWipeout * ++once call win_gotoid(s:w)
+          \| call assert_fails(s:b .. 'b', 'E1546:') | wincmd p
+  augroup END
+  bw! " Close only this buffer first; used to be a heap UAF.
+
+  unlet! s:w s:b
+  autocmd! ViewClosedBuffer
+  %bw!
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/version.c b/src/version.c
index c0c98b40b..7d29d7020 100644
--- a/src/version.c
+++ b/src/version.c
@@ -704,6 +704,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    1361,
 /**/
     1360,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to vim_dev+unsubscr...@googlegroups.com.
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1uBFz9-005keI-TT%40256bit.org.

Raspunde prin e-mail lui