pathspec: use match for sparse-index expansion checks

The pathspec parser computes `len` and `nowildcard_len` from `item.match`, which includes any prefix added when a command is run from a subdirectory. `item.original` can still contain the shorter, unprefixed argument. Using `item.original + item.nowildcard_len` in `pathspec_needs_expanded_index()` can therefore read past the end of the allocation. AddressSanitizer reports a heap-buffer-overflow for prefixed wildcard pathspecs passed to `git rm` and `git reset` with a sparse index. The mismatch dates back to 4d1cfc1351 ("reset: make --mixed sparse-aware", 2021-11-29), which introduced the helper using `item.original`. b29ad38322 ("pathspec.h: move pathspec_needs_expanded_index() from reset.c to here", 2022-08-07) later moved it to `pathspec.c` and preserved the affected comparisons. Use `item.match` consistently when checking whether a pathspec can match a sparse-directory entry. Add coverage for prefixed wildcard pathspecs so both commands keep the index sparse. Signed-off-by: Ted Nyman <tnyman@openai.com> Reviewed-by: Taylor Blau <ttaylorr@openai.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Ted Nyman committed Jul 20, 2026 at 15:31 UTC dd674df3267112248b0012ce614168055a416920
2 files changed +13 -6
pathspec.c
+6 -6
@@ -847,9 +847,9 @@ int pathspec_needs_expanded_index(struct index_state *istate,
847 * - not-in-cone/bar*: may need expanded index
848 * - **.c: may need expanded index
849 */
850 - if (strspn(item.original + item.nowildcard_len, "*") ==
850 + if (strspn(item.match + item.nowildcard_len, "*") ==
851 (unsigned int)(item.len - item.nowildcard_len) &&
852 - path_in_cone_mode_sparse_checkout(item.original, istate))
852 + path_in_cone_mode_sparse_checkout(item.match, istate))
853 continue;
854
855 for (pos = 0; pos < istate->cache_nr; pos++) {
@@ -865,7 +865,7 @@ int pathspec_needs_expanded_index(struct index_state *istate,
865 */
866 if ((unsigned int)item.nowildcard_len >
867 ce_namelen(ce) &&
868 - !strncmp(item.original, ce->name,
868 + !strncmp(item.match, ce->name,
869 ce_namelen(ce))) {
870 res = 1;
871 break;
@@ -876,13 +876,13 @@ int pathspec_needs_expanded_index(struct index_state *istate,
876 * directory and the pathspec does not match the whole
877 * directory, need to expand the index.
878 */
879 - if (!strncmp(item.original, ce->name, item.nowildcard_len) &&
880 - wildmatch(item.original, ce->name, 0)) {
879 + if (!strncmp(item.match, ce->name, item.nowildcard_len) &&
880 + wildmatch(item.match, ce->name, 0)) {
881 res = 1;
882 break;
883 }
884 }
885 - } else if (!path_in_cone_mode_sparse_checkout(item.original, istate) &&
885 + } else if (!path_in_cone_mode_sparse_checkout(item.match, istate) &&
886 !matches_skip_worktree(pathspec, i, &skip_worktree_seen))
887 res = 1;
888
t/t1092-sparse-checkout-compatibility.sh
+7
@@ -2119,6 +2119,13 @@ test_expect_success 'sparse index is not expanded: rm' '
2119 ensure_not_expanded rm -r deep
2120 '
2121
2122 +test_expect_success 'sparse index is not expanded: prefixed wildcard pathspec' '
2123 + init_repos &&
2124 +
2125 + ensure_not_expanded -C deep rm --dry-run -- "a*" &&
2126 + ensure_not_expanded -C deep reset base -- "a*"
2127 +'
2128 +
2129 test_expect_success 'grep with and --cached' '
2130 init_repos &&
2131