From d844a276bb0ca7fa721f2d688f8060a990d6ec36 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 27 Jun 2026 09:07:56 +0800 Subject: [PATCH] vim-patch:9.2.0735: [security]: arbitrary Ex command execution during C omni-completion (#40441) 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 https://github.com/vim/vim/commit/6b611b0d15603c52ebdad17172b0232b4f65704e Co-authored-by: Hirohito Higashi --- runtime/autoload/ccomplete.vim | 4 +- test/old/testdir/test_plugin_ccomplete.vim | 62 ++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 test/old/testdir/test_plugin_ccomplete.vim diff --git a/runtime/autoload/ccomplete.vim b/runtime/autoload/ccomplete.vim index 9d3017b47e..1b09b88365 100644 --- a/runtime/autoload/ccomplete.vim +++ b/runtime/autoload/ccomplete.vim @@ -555,7 +555,9 @@ func s:StructMembers(typename, items, all) if complete_check() return [] endif - 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/test/old/testdir/test_plugin_ccomplete.vim b/test/old/testdir/test_plugin_ccomplete.vim new file mode 100644 index 0000000000..a635bd50bd --- /dev/null +++ b/test/old/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