]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0869: buf_copy_options() can lose the P_INSECURE flag v9.2.0869
authorChristian Brabandt <cb@256bit.org>
Tue, 28 Jul 2026 18:52:44 +0000 (18:52 +0000)
committerChristian Brabandt <cb@256bit.org>
Tue, 28 Jul 2026 18:52:44 +0000 (18:52 +0000)
Problem:  An insecurely-set 'indentexpr', 'formatexpr', 'includeexpr'
          or 'complete' value can end up evaluated outside the
          sandbox after buf_copy_options() and clears the flag.
Solution: Copy the insecure flag alongside the value in
          buf_copy_options(), and make 'complete' a per-buffer
          insecure-flags field

Supported by AI.

closes: #20861

Signed-off-by: Christian Brabandt <cb@256bit.org>
src/option.c
src/structs.h
src/testdir/test_options.vim
src/version.c

index f511b8ea5dffe5ef2f597f7ab718682322c9ac85..aa4c990dcba0f7bd2f4c84dbf8be1885d7e6f3b5 100644 (file)
@@ -3489,6 +3489,9 @@ insecure_flag(win_T *wp, int opt_idx, int opt_flags)
 #  ifdef FEAT_FIND_ID
            case PV_INEX:       return &wp->w_buffer->b_p_inex_flags;
 #  endif
+#  ifdef FEAT_COMPL_FUNC
+           case PV_CPT:        return &wp->w_buffer->b_p_cpt_flags;
+#  endif
 # endif
        }
     else
@@ -7786,6 +7789,8 @@ clear_winopt(winopt_T *wop UNUSED)
 // Index into the options table for a buffer-local option enum.
 static int buf_opt_idx[BV_COUNT];
 # define COPY_OPT_SCTX(buf, bv) buf->b_p_script_ctx[bv] = options[buf_opt_idx[bv]].script_ctx
+# define COPY_OPT_INSECURE(flagsfield, bv) \
+       (flagsfield) = (options[buf_opt_idx[bv]].flags & P_INSECURE)
 
 /*
  * Initialize buf_opt_idx[] if not done already.
@@ -7805,6 +7810,7 @@ init_buf_opt_idx(void)
 }
 #else
 # define COPY_OPT_SCTX(buf, bv)
+# define COPY_OPT_INSECURE(flagsfield, bv)
 #endif
 
 /*
@@ -7927,6 +7933,7 @@ buf_copy_options(buf_T *buf, int flags)
            buf->b_p_cpt = vim_strsave(p_cpt);
            COPY_OPT_SCTX(buf, BV_CPT);
 #ifdef FEAT_COMPL_FUNC
+           COPY_OPT_INSECURE(buf->b_p_cpt_flags, BV_CPT);
            set_buflocal_cpt_callbacks(buf);
 #endif
 #ifdef BACKSLASH_IN_FILENAME
@@ -8020,6 +8027,7 @@ buf_copy_options(buf_T *buf, int flags)
 #if defined(FEAT_EVAL)
            buf->b_p_inde = vim_strsave(p_inde);
            COPY_OPT_SCTX(buf, BV_INDE);
+           COPY_OPT_INSECURE(buf->b_p_inde_flags, BV_INDE);
            buf->b_p_indk = vim_strsave(p_indk);
            COPY_OPT_SCTX(buf, BV_INDK);
 #endif
@@ -8027,6 +8035,7 @@ buf_copy_options(buf_T *buf, int flags)
 #if defined(FEAT_EVAL)
            buf->b_p_fex = vim_strsave(p_fex);
            COPY_OPT_SCTX(buf, BV_FEX);
+           COPY_OPT_INSECURE(buf->b_p_fex_flags, BV_FEX);
 #endif
 #ifdef FEAT_CRYPT
            buf->b_p_key = vim_strsave(p_key);
@@ -8081,6 +8090,7 @@ buf_copy_options(buf_T *buf, int flags)
 # ifdef FEAT_EVAL
            buf->b_p_inex = vim_strsave(p_inex);
            COPY_OPT_SCTX(buf, BV_INEX);
+           COPY_OPT_INSECURE(buf->b_p_inex_flags, BV_INEX);
 # endif
 #endif
            buf->b_p_cot = empty_option;
index b112c6d182892b8bdb91e49d948801d6c37d0e84..d9b7b6b0a587e5f411cfebd4ff626a320c394f20 100644 (file)
@@ -3447,6 +3447,7 @@ struct file_buffer
     char_u     *b_p_csl;       // 'completeslash'
 #endif
 #ifdef FEAT_COMPL_FUNC
+    long_u     b_p_cpt_flags;  // flags for 'complete'
     callback_T *b_p_cpt_cb;    // F{func} in 'complete' callback
     int                b_p_cpt_count;  // Count of values in 'complete'
     char_u     *b_p_cfu;       // 'completefunc'
index 7580ace6db953f47726b78e327eb5cf00e42afc9..b1201eb39727571d7ba45e574e65057c234a30d1 100644 (file)
@@ -3167,4 +3167,122 @@ func Test_comma_option_key_value()
   set lcs&
 endfunc
 
+func Test_insecure_flag_copied_to_new_buffer_indentexpr()
+  let modeline = &modeline
+  let modelineexpr = &modelineexpr
+  let modelinestrict = &modelinestrict
+
+  func! Xindentexprpwn(findstart, base)
+    if a:findstart
+      sandbox setglobal indentexpr=writefile(['leak'],\ 'Xindentexpr_proof')
+      return 0
+    endif
+    return []
+  endfunc
+
+  try
+    set modeline modelineexpr nomodelinestrict
+
+    call writefile([
+          \ 'vim: set complete=FXindentexprpwn :',
+          \ 'body',
+          \ ], 'Xindentexpr_attack', 'D')
+    call delete('Xindentexpr_proof')
+    edit Xindentexpr_attack
+    call cursor(2, 1)
+    call feedkeys("i\<C-N>\<Esc>", 'xt')
+    bwipe!
+
+    " A brand new buffer now inherits the poisoned 'indentexpr' via
+    " buf_copy_options().  It must still be evaluated in the sandbox.
+    enew!
+    call setline(1, ['{', 'x', '}'])
+    normal! 2G==
+    call assert_false(filereadable('Xindentexpr_proof'))
+    bwipe!
+  finally
+    let &modeline = modeline
+    let &modelineexpr = modelineexpr
+    let &modelinestrict = modelinestrict
+    set indentexpr&
+    call delete('Xindentexpr_proof')
+    delfunc Xindentexprpwn
+  endtry
+endfunc
+
+func Test_insecure_flag_not_cleared_by_other_buffer_complete()
+  let modeline = &modeline
+  let modelineexpr = &modelineexpr
+  let modelinestrict = &modelinestrict
+
+  func! Xcompletepwn(findstart, base)
+    if a:findstart
+      call writefile(['leak'], 'Xcomplete_cross_proof')
+      return 0
+    endif
+    return ['match']
+  endfunc
+
+  try
+    set modeline modelineexpr nomodelinestrict
+
+    call writefile([
+          \ 'vim: set complete=FXcompletepwn :',
+          \ 'body',
+          \ ], 'Xcomplete_cross_attack', 'D')
+    call delete('Xcomplete_cross_proof')
+    edit Xcomplete_cross_attack
+    let bufA = bufnr('%')
+
+    " An unrelated buffer does a completely ordinary, trusted reset.
+    new
+    setlocal complete=.,w,b,u,t
+    bwipe!
+
+    " Back in the modeline-tainted buffer: must still be sandboxed.
+    exe 'buffer ' .. bufA
+    call cursor(2, 1)
+    call assert_fails('call feedkeys("i\<C-N>\<Esc>", "xt")', 'E48:')
+    call assert_false(filereadable('Xcomplete_cross_proof'))
+    bwipe!
+  finally
+    let &modeline = modeline
+    let &modelineexpr = modelineexpr
+    let &modelinestrict = modelinestrict
+    call delete('Xcomplete_cross_proof')
+    delfunc Xcompletepwn
+  endtry
+endfunc
+
+func Test_formatexpr_insecure_copied_to_new_buffer()
+  new
+  sandbox setglobal formatexpr=writefile(['leak'],\ 'Xleak_fex')
+  enew!
+  call setline(1, ['some text to format'])
+  set textwidth=10
+  silent! normal! gqq
+  call assert_false(filereadable('Xleak_fex'))
+  call delete('Xleak_fex')
+  set formatexpr& textwidth&
+  bwipe!
+endfunc
+
+" includeexpr: triggered via gf / find_pattern_in_path (used by [i, gf, etc.)
+func Test_includeexpr_insecure_copied_to_new_buffer()
+  func Xleakinclude(fname)
+    call writefile(['leak'], 'Xleak_inex')
+    return a:fname
+  endfunc
+  new
+  sandbox setglobal includeexpr=Xleakinclude(v:fname)
+  enew!
+  call setline(1, ['#include "foo.h"'])
+  silent! normal! [i
+  call assert_false(filereadable('Xleak_inex'))
+  call delete('Xleak_inex')
+  set includeexpr&
+  delfunc Xleakinclude
+  bwipe!
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab
index 1c6be6d116a4bdb6892975056aa9c4f5967b6328..915e1c8e2267bd42d34a3c4bed69b6a926b5670f 100644 (file)
@@ -758,6 +758,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    869,
 /**/
     868,
 /**/