From: Christian Brabandt Date: Tue, 28 Jul 2026 18:52:44 +0000 (+0000) Subject: patch 9.2.0869: buf_copy_options() can lose the P_INSECURE flag X-Git-Tag: v9.2.0869^0 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=35f7fdfdfb036028cc8760d7c1556361bb85206a;p=thirdparty%2Fvim.git 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 --- diff --git a/src/option.c b/src/option.c index f511b8ea5d..aa4c990dcb 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 b112c6d182..d9b7b6b0a5 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 7580ace6db..b1201eb397 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\\", '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\\", "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 1c6be6d116..915e1c8e22 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, /**/