Commit: patch 9.2.0848: tagfunc "cmd" with a generic Ex command corrupts the tag entry

Christian Brabandt <[email protected]>
Newsgroups gmane.editors.vim.devel
Message-ID <[email protected]>
patch 9.2.0848: tagfunc "cmd" with a generic Ex command corrupts the tag entry

Commit: https://github.com/vim/vim/commit/86adef19fc2f1d59a38f3ba978ce33f0b4329c0f
Author: Hirohito Higashi <[email protected]>
Date:   Fri Jul 24 20:04:21 2026 +0000

    patch 9.2.0848: tagfunc "cmd" with a generic Ex command corrupts the tag entry
    
    Problem:  When a tagfunc returns a "cmd" that is neither a line number nor
              a search pattern, the tag entry is corrupted: the "kind" field is
              lost and taglist() returns a mangled "cmd".
    Solution: Accept any Ex command in "cmd" as in a tags file, terminate a
              generic command with a bar so the trailing fields are preserved,
              and reject a value that cannot be stored in a tag line with E987
              (Hirohito Higashi).
    
    fixes:   #20781
    related: #20790
    closes:  #20828
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    Co-Authored-By: Mao-Yining <[email protected]>
    Signed-off-by: Hirohito Higashi <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

diff --git a/runtime/doc/tagsrch.txt b/runtime/doc/tagsrch.txt
index e381d5fc0..2615cef9e 100644
--- a/runtime/doc/tagsrch.txt
+++ b/runtime/doc/tagsrch.txt
@@ -1,4 +1,4 @@
-*tagsrch.txt*	For Vim version 9.2.  Last change: 2026 May 31
+*tagsrch.txt*	For Vim version 9.2.  Last change: 2026 Jul 24
 
 
 		  VIM REFERENCE MANUAL	  by Bram Moolenaar
@@ -936,7 +936,11 @@ include the following entries and each value must be a string:
 			either relative to the current directory or a full
 			path.
 	cmd		Ex command used to locate the tag in the file.  This
-			can be either an Ex search pattern or a line number.
+			can be a line number, a search pattern or any other
+			Ex command, like in a tags file (see
+			|tags-file-format|).  A search pattern must start and
+			end with "/" or "?".  An invalid value results in
+			|E987|.
 Note that the format is similar to that of |taglist()|, which makes it possible
 to use its output to generate the result.
 The following fields are optional:
diff --git a/src/tag.c b/src/tag.c
index 96afc699b..98b98897f 100644
--- a/src/tag.c
+++ b/src/tag.c
@@ -1425,6 +1425,38 @@ prepare_pats(pat_T *pats, int has_re)
 }
 
 #ifdef FEAT_EVAL
+// Classification of a "cmd" value returned by a tagfunc.
+typedef enum {
+    TAGCMD_INVALID,	// cannot be stored in a tag line
+    TAGCMD_ADDRESS,	// line number or /pat/ or ?pat? (no "|" needed)
+    TAGCMD_GENERIC	// any other Ex command: needs a "|" terminator
+} tagcmd_T;
+
+/*
+ * Classify "cmd" from a tagfunc result (see tagcmd_T), like a tags file
+ * address, to decide whether a "|" terminator must be appended before storing
+ * it in a tag line.
+ */
+    static tagcmd_T
+tagfunc_cmd_kind(char_u *cmd)
+{
+    if (VIM_ISDIGIT(*cmd))
+	return *skipdigits(cmd) == NUL ? TAGCMD_ADDRESS : TAGCMD_INVALID;
+    if (*cmd == '/' || *cmd == '?')
+    {
+	// A Tab inside the pattern is fine (Universal Ctags emits one for a
+	// tab-indented line); only trailing content after the closing
+	// delimiter, which would break the tag line fields, is rejected.
+	char_u	*end = skip_regexp(cmd + 1, *cmd, FALSE);
+
+	return (*end == *cmd && end[1] == NUL)
+					? TAGCMD_ADDRESS : TAGCMD_INVALID;
+    }
+    if (vim_strpbrk(cmd, (char_u *)"	
") != NULL)
+	return TAGCMD_INVALID;
+    return TAGCMD_GENERIC;
+}
+
 /*
  * Call the user-defined function to generate a list of tags used by
  * find_tags().
@@ -1570,6 +1602,15 @@ find_tagfunc_tags(
 	    break;
 	}
 
+	tagcmd_T cmdkind = tagfunc_cmd_kind(res_cmd);
+	if (cmdkind == TAGCMD_INVALID)
+	{
+	    emsg(_(e_invalid_return_value_from_tagfunc));
+	    break;
+	}
+	if (cmdkind == TAGCMD_GENERIC)
+	    len += 3;	// need space for "|;\""
+
 	if (name_only)
 	    mfp = vim_strsave(res_name);
 	else
@@ -1599,7 +1640,10 @@ find_tagfunc_tags(
 	    STRCPY(p, res_cmd);
 	    p += STRLEN(p);
 
-	    if (has_extra)
+	    if (cmdkind == TAGCMD_GENERIC)
+		*p++ = '|';	// terminate the command
+
+	    if (cmdkind == TAGCMD_GENERIC || has_extra)
 	    {
 		STRCPY(p, ";\"");
 		p += STRLEN(p);
@@ -3781,7 +3825,12 @@ jumpto_tag(
 	    str++;
 	if (find_extra(&str) == OK)
 	{
-	    pbuf_end = str;
+	    // Drop a trailing "|" that terminates a generic Ex command, so it
+	    // is not executed as an empty command separator.
+	    if (str > pbuf && str[-1] == '|')
+		pbuf_end = str - 1;
+	    else
+		pbuf_end = str;
 	    *pbuf_end = NUL;
 	}
     }
diff --git a/src/testdir/test_tagfunc.vim b/src/testdir/test_tagfunc.vim
index 4ce9d2164..4427839be 100644
--- a/src/testdir/test_tagfunc.vim
+++ b/src/testdir/test_tagfunc.vim
@@ -456,4 +456,84 @@ func Test_tagfunc_deletes_lines()
   set tagfunc=
 endfunc
 
+" The 'cmd' field can be a line number, a search pattern or any other Ex
+" command, like in a tags file.  A value that would break the tag line (an
+" unterminated pattern, trailing junk, or a <Tab> outside a pattern) is
+" rejected with E987 instead of silently corrupting the result.
+func Test_tagfunc_invalid_cmd()
+  func InvalidCmd(pat, flags, info)
+    return [{'name': 'a', 'filename': 'Xtest', 'cmd': a:pat, 'kind': 'f'}]
+  endfunc
+  set tagfunc=InvalidCmd
+
+  " unterminated search pattern
+  call assert_fails("call taglist('/foo')", 'E987:')
+  " trailing content after the closing delimiter of a pattern
+  call assert_fails('call taglist("/foo/	bar")', 'E987:')
+  " a line number with trailing junk
+  call assert_fails("call taglist('3G5')", 'E987:')
+  " a <Tab> in a generic command would break the tag line fields
+  call assert_fails('call taglist("call	cursor(3, 5)")', 'E987:')
+
+  " valid forms are accepted and returned unchanged: line number, delimited
+  " patterns, and a Tab inside a pattern (Universal Ctags emits one like this
+  " for a tab-indented line)
+  call assert_equal('42', taglist('42')[0].cmd)
+  call assert_equal('/foo/', taglist('/foo/')[0].cmd)
+  call assert_equal('?foo?', taglist('?foo?')[0].cmd)
+  call assert_equal("/^	int foo$/", taglist("/^	int foo$/")[0].cmd)
+
+  " a generic command round-trips with its kind field intact: before the fix
+  " it swallowed the ';"<Tab>kind' part, so kind came back empty (#20781)
+  let t = taglist('call cursor(3, 5)')[0]
+  call assert_equal('call cursor(3, 5)', t.cmd)
+  call assert_equal('f', t.kind)
+
+  set tagfunc&
+  delfunc InvalidCmd
+endfunc
+
+" A tagfunc 'cmd' is executed on a tag jump.  A search pattern must reach the
+" matched column (no regression from the do_search() path), and a line number
+" or a generic Ex command must position the cursor as written.
+func Test_tagfunc_cmd_jump()
+  call writefile(['line one', 'line two', 'foo bar baz'], 'Xtagcmd', 'D')
+  func JumpCmd(pat, flags, info)
+    return [{'name': 'a', 'filename': 'Xtagcmd', 'cmd': s:cmd, 'kind': 'f'}]
+  endfunc
+  set tagfunc=JumpCmd
+
+  " '5|' / cursor col 5 / the 'b' of 'bar' all land on [3, 5]
+  for cmd in ['/bar/', 'call cursor(3, 5)', 'normal! 3G5|']
+    let s:cmd = cmd
+    enew
+    tag a
+    call assert_equal([3, 5], [line('.'), col('.')], 'cmd: ' .. cmd)
+    bw!
+  endfor
+
+  set tagfunc&
+  delfunc JumpCmd
+endfunc
+
+" A generic Ex command from a tagfunc runs under 'secure' and the sandbox, just
+" like one from a tags file, so it cannot delete a file or run a shell.
+func Test_tagfunc_cmd_secure()
+  call writefile(['one', 'two', 'three'], 'Xtagcmd', 'D')
+  call writefile(['keep me'], 'Xvictim', 'D')
+  func EvilTagFunc(pat, flags, info)
+    return [{'name': 'a', 'filename': 'Xtagcmd',
+          \ 'cmd': "call delete('Xvictim')", 'kind': 'f'}]
+  endfunc
+  set tagfunc=EvilTagFunc
+
+  enew
+  call assert_fails('tag a', 'E12:')
+  call assert_true(filereadable('Xvictim'))
+
+  bw!
+  set tagfunc&
+  delfunc EvilTagFunc
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/version.c b/src/version.c
index a0b96e2eb..2cf1ab942 100644
--- a/src/version.c
+++ b/src/version.c
@@ -758,6 +758,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    848,
 /**/
     847,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups "vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [email protected].
To view this discussion visit https://groups.google.com/d/msgid/vim_dev/E1wnMIT-00C60d-5U%40256bit.org.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.