patch 9.2.0735: [security]: arbitrary Ex command execution during C omni-completion
Commit:
https://github.com/vim/vim/commit/6b611b0d15603c52ebdad17172b0232b4f65704e
Author: Hirohito Higashi <
h.eas...@gmail.com>
Date: Fri Jun 26 15:41:24 2026 +0900
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) <
nor...@anthropic.com>"
Signed-off-by: Hirohito Higashi <
h.eas...@gmail.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/runtime/autoload/ccomplete.vim b/runtime/autoload/ccomplete.vim
index 51237be98..dc3388b52 100644
--- a/runtime/autoload/ccomplete.vim
+++ b/runtime/autoload/ccomplete.vim
@@ -600,7 +600,7 @@ def StructMembers( # {{{1
return []
endif
execute 'silent! keepjumps noautocmd '
- .. n .. 'vimgrep ' .. '/ ' .. typename .. '\( \|$\)/j '
+ .. n .. 'vimgrep ' .. '/ ' .. escape(typename, '/\') .. '\( \|$\)/j '
.. fnames
qflist = getqflist()
diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak
index b81e7e6bf..dd0708c5c 100644
--- a/src/testdir/Make_all.mak
+++ b/src/testdir/Make_all.mak
@@ -243,6 +243,7 @@ NEW_TESTS = \
test_partial \
test_paste \
test_perl \
+ test_plugin_ccomplete \
test_plugin_comment \
test_plugin_glvs \
test_plugin_helpcurwin \
@@ -523,6 +524,7 @@ NEW_TESTS_RES = \
test_partial.res \
test_paste.res \
test_perl.res \
+ test_plugin_ccomplete.res \
test_plugin_comment.res \
test_plugin_glvs.res \
test_plugin_helpcurwin.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 0 /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 main.c /^x$/;\" v typeref: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 main.c /^x$/;\" v typeref:struct:mystruct",
+ \ "alpha main.c /^x$/;\" m struct:mystruct",
+ \ "beta main.c /^x$/;\" m struct: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
diff --git a/src/version.c b/src/version.c
index 85bb6d17e..5a976ac57 100644
--- a/src/version.c
+++ b/src/version.c
@@ -759,6 +759,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 735,
/**/
734,
/**/