From 7b7a21bd2ac610e656f84a79ea96623230662332 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 29 Jul 2026 17:44:51 +0800 Subject: [PATCH] vim-patch:9.2.0869: buf_copy_options() can lose the P_INSECURE flag (#41040) 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: vim/vim#20861 https://github.com/vim/vim/commit/35f7fdfdfb036028cc8760d7c1556361bb85206a I'm a bit hesitant to port this, but it's a follow-up to #39452. Co-authored-by: Christian Brabandt --- src/nvim/buffer_defs.h | 1 + src/nvim/option.c | 8 ++ test/old/testdir/test_options.vim | 118 ++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+) diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 5c4b5dd86f..6403905074 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -557,6 +557,7 @@ struct file_buffer { #ifdef BACKSLASH_IN_FILENAME char *b_p_csl; ///< 'completeslash' #endif + uint32_t b_p_cpt_flags; ///< flags for 'complete' Callback *b_p_cpt_cb; ///< F{func} in 'complete' callback int b_p_cpt_count; ///< Count of values in 'complete' char *b_p_cfu; ///< 'completefunc' diff --git a/src/nvim/option.c b/src/nvim/option.c index 6f147ee6cf..96f3fe111a 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -1926,6 +1926,8 @@ uint32_t *insecure_flag(win_T *const wp, OptIndex opt_idx, int opt_flags) return &wp->w_buffer->b_p_fex_flags; case kOptIncludeexpr: return &wp->w_buffer->b_p_inex_flags; + case kOptComplete: + return &wp->w_buffer->b_p_cpt_flags; default: break; } @@ -5597,6 +5599,8 @@ void didset_window_options(win_T *wp, bool valid_cursor) } #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 & kOptFlagInsecure) /// Copy global option values to local options for one buffer. /// Used when creating a new buffer and sometimes when entering a buffer. @@ -5707,6 +5711,7 @@ void buf_copy_options(buf_T *buf, int flags) } buf->b_p_cpt = xstrdup(p_cpt); COPY_OPT_SCTX(buf, kBufOptComplete); + COPY_OPT_INSECURE(buf->b_p_cpt_flags, kBufOptComplete); set_buflocal_cpt_callbacks(buf); #ifdef BACKSLASH_IN_FILENAME buf->b_p_csl = xstrdup(p_csl); @@ -5786,11 +5791,13 @@ void buf_copy_options(buf_T *buf, int flags) buf->b_s.b_p_spo_flags = spo_flags; buf->b_p_inde = xstrdup(p_inde); COPY_OPT_SCTX(buf, kBufOptIndentexpr); + COPY_OPT_INSECURE(buf->b_p_inde_flags, kBufOptIndentexpr); buf->b_p_indk = xstrdup(p_indk); COPY_OPT_SCTX(buf, kBufOptIndentkeys); buf->b_p_fp = empty_string_option; buf->b_p_fex = xstrdup(p_fex); COPY_OPT_SCTX(buf, kBufOptFormatexpr); + COPY_OPT_INSECURE(buf->b_p_fex_flags, kBufOptFormatexpr); buf->b_p_sua = xstrdup(p_sua); COPY_OPT_SCTX(buf, kBufOptSuffixesadd); buf->b_p_keymap = xstrdup(p_keymap); @@ -5826,6 +5833,7 @@ void buf_copy_options(buf_T *buf, int flags) buf->b_p_inc = empty_string_option; buf->b_p_inex = xstrdup(p_inex); COPY_OPT_SCTX(buf, kBufOptIncludeexpr); + COPY_OPT_INSECURE(buf->b_p_inex_flags, kBufOptIncludeexpr); buf->b_p_cot = empty_string_option; buf->b_cot_flags = 0; buf->b_p_dict = empty_string_option; diff --git a/test/old/testdir/test_options.vim b/test/old/testdir/test_options.vim index b98799a9c9..a4ec33a976 100644 --- a/test/old/testdir/test_options.vim +++ b/test/old/testdir/test_options.vim @@ -3117,4 +3117,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