]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0848: tagfunc "cmd" with a generic Ex command corrupts the tag entry v9.2.0848
authorHirohito Higashi <h.east.727@gmail.com>
Fri, 24 Jul 2026 20:04:21 +0000 (20:04 +0000)
committerChristian Brabandt <cb@256bit.org>
Fri, 24 Jul 2026 20:08:22 +0000 (20:08 +0000)
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) <noreply@anthropic.com>
Co-Authored-By: Mao-Yining <mao.yining@outlook.com>
Signed-off-by: Hirohito Higashi <h.east.727@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
runtime/doc/tagsrch.txt
src/tag.c
src/testdir/test_tagfunc.vim
src/version.c

index e381d5fc08f664fdd7d9d736b6579c6564988e83..2615cef9e2bd0e0fafdcaccfdde7ca5765d96afb 100644 (file)
@@ -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:
index 96afc699b43e0f9810466efa3bc7aa9efcd60e07..98b98897fcd480df76aea5437825c6999e5f000d 100644 (file)
--- 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 *)"\t\r\n") != 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;
        }
     }
index 4ce9d2164b4c9238b7b486977cdd7584ce96a8bf..4427839be802319c08a0923328e2c5e269c9ae50 100644 (file)
@@ -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/\tbar")', '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\tcursor(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("/^\tint foo$/", taglist("/^\tint 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
index a0b96e2ebbc50c331d4f2c141981bb6ffd054e2c..2cf1ab9425b337864f5ddd1543374322daef9fdb 100644 (file)
@@ -758,6 +758,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    848,
 /**/
     847,
 /**/