Commit: patch 9.2.0869: buf_copy_options() can lose the P_INSECURE flag

0 views
Skip to first unread message

Christian Brabandt

unread,
3:00 PM (6 hours ago) 3:00 PM
to vim...@googlegroups.com
patch 9.2.0869: buf_copy_options() can lose the P_INSECURE flag

Commit: https://github.com/vim/vim/commit/35f7fdfdfb036028cc8760d7c1556361bb85206a
Author: Christian Brabandt <c...@256bit.org>
Date: Tue Jul 28 18:52:44 2026 +0000

patch 9.2.0869: buf_copy_options() can lose the P_INSECURE flag

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 <c...@256bit.org>

diff --git a/src/option.c b/src/option.c
index f511b8ea5..aa4c990dc 100644
--- a/src/option.c
+++ b/src/option.c
@@ -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;
diff --git a/src/structs.h b/src/structs.h
index b112c6d18..d9b7b6b0a 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -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'
diff --git a/src/testdir/test_options.vim b/src/testdir/test_options.vim
index 7580ace6d..b1201eb39 100644
--- a/src/testdir/test_options.vim
+++ b/src/testdir/test_options.vim
@@ -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
diff --git a/src/version.c b/src/version.c
index 1c6be6d11..915e1c8e2 100644
--- a/src/version.c
+++ b/src/version.c
@@ -758,6 +758,8 @@ static char *(features[]) =

static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 869,
/**/
868,
/**/
Reply all
Reply to author
Forward
0 new messages