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

35f7fdfdfb

I'm a bit hesitant to port this, but it's a follow-up to #39452.

Co-authored-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
zeertzjq 2026-07-29 17:44:51 +08:00 committed by GitHub
parent 1eba3a75da
commit 7b7a21bd2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 127 additions and 0 deletions

View File

@ -557,6 +557,7 @@ struct file_buffer {
#ifdef BACKSLASH_IN_FILENAME #ifdef BACKSLASH_IN_FILENAME
char *b_p_csl; ///< 'completeslash' char *b_p_csl; ///< 'completeslash'
#endif #endif
uint32_t b_p_cpt_flags; ///< flags for 'complete'
Callback *b_p_cpt_cb; ///< F{func} in 'complete' callback Callback *b_p_cpt_cb; ///< F{func} in 'complete' callback
int b_p_cpt_count; ///< Count of values in 'complete' int b_p_cpt_count; ///< Count of values in 'complete'
char *b_p_cfu; ///< 'completefunc' char *b_p_cfu; ///< 'completefunc'

View File

@ -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; return &wp->w_buffer->b_p_fex_flags;
case kOptIncludeexpr: case kOptIncludeexpr:
return &wp->w_buffer->b_p_inex_flags; return &wp->w_buffer->b_p_inex_flags;
case kOptComplete:
return &wp->w_buffer->b_p_cpt_flags;
default: default:
break; 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_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. /// Copy global option values to local options for one buffer.
/// Used when creating a new buffer and sometimes when entering a 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); buf->b_p_cpt = xstrdup(p_cpt);
COPY_OPT_SCTX(buf, kBufOptComplete); COPY_OPT_SCTX(buf, kBufOptComplete);
COPY_OPT_INSECURE(buf->b_p_cpt_flags, kBufOptComplete);
set_buflocal_cpt_callbacks(buf); set_buflocal_cpt_callbacks(buf);
#ifdef BACKSLASH_IN_FILENAME #ifdef BACKSLASH_IN_FILENAME
buf->b_p_csl = xstrdup(p_csl); 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_s.b_p_spo_flags = spo_flags;
buf->b_p_inde = xstrdup(p_inde); buf->b_p_inde = xstrdup(p_inde);
COPY_OPT_SCTX(buf, kBufOptIndentexpr); COPY_OPT_SCTX(buf, kBufOptIndentexpr);
COPY_OPT_INSECURE(buf->b_p_inde_flags, kBufOptIndentexpr);
buf->b_p_indk = xstrdup(p_indk); buf->b_p_indk = xstrdup(p_indk);
COPY_OPT_SCTX(buf, kBufOptIndentkeys); COPY_OPT_SCTX(buf, kBufOptIndentkeys);
buf->b_p_fp = empty_string_option; buf->b_p_fp = empty_string_option;
buf->b_p_fex = xstrdup(p_fex); buf->b_p_fex = xstrdup(p_fex);
COPY_OPT_SCTX(buf, kBufOptFormatexpr); COPY_OPT_SCTX(buf, kBufOptFormatexpr);
COPY_OPT_INSECURE(buf->b_p_fex_flags, kBufOptFormatexpr);
buf->b_p_sua = xstrdup(p_sua); buf->b_p_sua = xstrdup(p_sua);
COPY_OPT_SCTX(buf, kBufOptSuffixesadd); COPY_OPT_SCTX(buf, kBufOptSuffixesadd);
buf->b_p_keymap = xstrdup(p_keymap); 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_inc = empty_string_option;
buf->b_p_inex = xstrdup(p_inex); buf->b_p_inex = xstrdup(p_inex);
COPY_OPT_SCTX(buf, kBufOptIncludeexpr); COPY_OPT_SCTX(buf, kBufOptIncludeexpr);
COPY_OPT_INSECURE(buf->b_p_inex_flags, kBufOptIncludeexpr);
buf->b_p_cot = empty_string_option; buf->b_p_cot = empty_string_option;
buf->b_cot_flags = 0; buf->b_cot_flags = 0;
buf->b_p_dict = empty_string_option; buf->b_p_dict = empty_string_option;

View File

@ -3117,4 +3117,122 @@ func Test_comma_option_key_value()
set lcs& set lcs&
endfunc 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 " vim: shiftwidth=2 sts=2 expandtab