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>
main
Ted Nyman 2026-07-20 15:31:20 -07:00 committed by Junio C Hamano
parent e9019fcafe
commit dd674df326
2 changed files with 13 additions and 6 deletions

View File

@ -847,9 +847,9 @@ int pathspec_needs_expanded_index(struct index_state *istate,
* - not-in-cone/bar*: may need expanded index * - not-in-cone/bar*: may need expanded index
* - **.c: may need expanded index * - **.c: may need expanded index
*/ */
if (strspn(item.original + item.nowildcard_len, "*") == if (strspn(item.match + item.nowildcard_len, "*") ==
(unsigned int)(item.len - item.nowildcard_len) && (unsigned int)(item.len - item.nowildcard_len) &&
path_in_cone_mode_sparse_checkout(item.original, istate)) path_in_cone_mode_sparse_checkout(item.match, istate))
continue; continue;


for (pos = 0; pos < istate->cache_nr; pos++) { for (pos = 0; pos < istate->cache_nr; pos++) {
@ -865,7 +865,7 @@ int pathspec_needs_expanded_index(struct index_state *istate,
*/ */
if ((unsigned int)item.nowildcard_len > if ((unsigned int)item.nowildcard_len >
ce_namelen(ce) && ce_namelen(ce) &&
!strncmp(item.original, ce->name, !strncmp(item.match, ce->name,
ce_namelen(ce))) { ce_namelen(ce))) {
res = 1; res = 1;
break; break;
@ -876,13 +876,13 @@ int pathspec_needs_expanded_index(struct index_state *istate,
* directory and the pathspec does not match the whole * directory and the pathspec does not match the whole
* directory, need to expand the index. * directory, need to expand the index.
*/ */
if (!strncmp(item.original, ce->name, item.nowildcard_len) && if (!strncmp(item.match, ce->name, item.nowildcard_len) &&
wildmatch(item.original, ce->name, 0)) { wildmatch(item.match, ce->name, 0)) {
res = 1; res = 1;
break; break;
} }
} }
} else if (!path_in_cone_mode_sparse_checkout(item.original, istate) && } else if (!path_in_cone_mode_sparse_checkout(item.match, istate) &&
!matches_skip_worktree(pathspec, i, &skip_worktree_seen)) !matches_skip_worktree(pathspec, i, &skip_worktree_seen))
res = 1; res = 1;



View File

@ -2119,6 +2119,13 @@ test_expect_success 'sparse index is not expanded: rm' '
ensure_not_expanded rm -r deep ensure_not_expanded rm -r deep
' '


test_expect_success 'sparse index is not expanded: prefixed wildcard pathspec' '
init_repos &&

ensure_not_expanded -C deep rm --dry-run -- "a*" &&
ensure_not_expanded -C deep reset base -- "a*"
'

test_expect_success 'grep with and --cached' ' test_expect_success 'grep with and --cached' '
init_repos && init_repos &&