From a3304c9e3252a9e74bb114d5f72bd4df35017229 Mon Sep 17 00:00:00 2001 From: Hirohito Higashi Date: Fri, 26 Jun 2026 15:41:24 +0900 Subject: [PATCH] patch 9.2.0735: [security]: arbitrary Ex command execution during C omni-completion Problem: [security]: With C omni-completion, a crafted tags file can execute arbitrary Ex commands when completing a struct/union member (cipher-creator) Solution: Escape the type field before inserting it into the :vimgrep pattern so it cannot close the pattern and start a new command (Hirohito Higashi). Github Security Advisory: https://github.com/vim/vim/security/advisories/GHSA-mf92-v4xw-j45x Co-Authored-By: Claude Opus 4.8 (1M context) " Signed-off-by: Hirohito Higashi Signed-off-by: Christian Brabandt --- runtime/autoload/ccomplete.vim | 2 +- src/testdir/Make_all.mak | 2 + src/testdir/test_plugin_ccomplete.vim | 62 +++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 src/testdir/test_plugin_ccomplete.vim diff --git a/runtime/autoload/ccomplete.vim b/runtime/autoload/ccomplete.vim index 95a20e16b..ae1fcf7d6 100644 --- a/runtime/autoload/ccomplete.vim +++ b/runtime/autoload/ccomplete.vim @@ -514,7 +514,7 @@ func s:StructMembers(typename, items, all) endif if !cached while 1 - exe 'silent! keepj noautocmd ' . n . 'vimgrep /\t' . typename . '\(\t\|$\)/j ' . fnames + exe 'silent! keepj noautocmd ' . n . 'vimgrep /\t' . escape(typename, '/\') . '\(\t\|$\)/j ' . fnames let qflist = getqflist() if len(qflist) > 0 || match(typename, "::") < 0 diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak index 7fd4e89a2..2430bc78c 100644 --- a/src/testdir/Make_all.mak +++ b/src/testdir/Make_all.mak @@ -209,6 +209,7 @@ NEW_TESTS = \ test_partial \ test_paste \ test_perl \ + test_plugin_ccomplete \ test_plugin_tar \ test_plus_arg_edit \ test_popup \ @@ -448,6 +449,7 @@ NEW_TESTS_RES = \ test_partial.res \ test_paste.res \ test_perl.res \ + test_plugin_ccomplete.res \ test_plugin_tar.res \ test_plus_arg_edit.res \ test_popup.res \ diff --git a/src/testdir/test_plugin_ccomplete.vim b/src/testdir/test_plugin_ccomplete.vim new file mode 100644 index 000000000..a635bd50b --- /dev/null +++ b/src/testdir/test_plugin_ccomplete.vim @@ -0,0 +1,62 @@ +" Tests for the C omni-completion plugin (runtime/autoload/ccomplete.vim). + +func s:WriteTags(lines) + " Mark unsorted so lookup is a linear scan regardless of entry order. + let tagsfile = tempname() + call writefile(["!_TAG_FILE_SORTED\t0\t/0/"] + a:lines, tagsfile) + return tagsfile +endfunc + +" A crafted typeref field is interpolated into the :vimgrep pattern in +" StructMembers(). Without escaping, "/" closes the pattern and "|" starts a +" new Ex command, so the field runs as an Ex command during completion. +func Test_ccomplete_no_exec_via_typeref() + unlet! g:ccomplete_injected + let tagsfile = s:WriteTags([ + \ "myvar\tmain.c\t/^x$/;\"\tv\ttyperef:x/|let g:ccomplete_injected = 1|\"", + \ ]) + + let save_tags = &tags + let &tags = tagsfile + + new + call ccomplete#Complete(1, '') + call ccomplete#Complete(0, 'myvar.x') + + call assert_false(exists('g:ccomplete_injected'), + \ 'typeref field was executed as an Ex command during omni-completion') + + bwipe! + let &tags = save_tags + unlet! g:ccomplete_injected +endfunc + +" A legitimate typeref must still drive struct-member completion: escaping the +" field value must not break the normal path. +func Test_ccomplete_typeref_completion_still_works() + let tagsfile = s:WriteTags([ + \ "myvar\tmain.c\t/^x$/;\"\tv\ttyperef:struct:mystruct", + \ "alpha\tmain.c\t/^x$/;\"\tm\tstruct:mystruct", + \ "beta\tmain.c\t/^x$/;\"\tm\tstruct:mystruct", + \ ]) + + let save_tags = &tags + let &tags = tagsfile + + new + call ccomplete#Complete(1, '') + let items = ccomplete#Complete(0, 'myvar.') + + call assert_equal(type([]), type(items), + \ 'ccomplete#Complete did not return a list') + let names = map(copy(items), 'v:val.word') + call assert_true(index(names, 'alpha') >= 0, + \ 'struct member "alpha" missing from completion: ' . string(names)) + call assert_true(index(names, 'beta') >= 0, + \ 'struct member "beta" missing from completion: ' . string(names)) + + bwipe! + let &tags = save_tags +endfunc + +" vim: shiftwidth=2 sts=2 expandtab