From 16abad336073665c13c09b1026dd1227688b9c10 Mon Sep 17 00:00:00 2001 From: Yannik Tausch Date: Mon, 14 Sep 2026 09:25:51 +0200 Subject: [PATCH 1/6] dir: do not apply prefix to negative pathspecs common_prefix_len() derives the common prefix solely from non-exclude pathspec items. However, match_pathspec_with_flags() also passes that prefix when matching exclude items. This can produce incorrect results because that prefix does not necessarily match an exclude item. For example, given non-exclude items "a/b" and "a/c" and an exclude item "x/b", stripping the two-byte prefix from both the pathname "a/b/m" and pattern "x/b" makes the remaining strings match and incorrectly excludes the pathname. If an exclude item is shorter than the prefix, match_pathspec_item() instead advances item->match beyond its allocation and subtracts the prefix from item->len, producing a negative matchlen. It then dereferences the out-of-bounds pointer. If the resulting byte is not NUL, matchlen is converted to size_t when passed to ps_strncmp(), which may cause a much larger out-of-bounds read. The out-of-bounds access can be reproduced with AddressSanitizer: make SANITIZE=address CFLAGS="-g -O0" git git init test && cd test && DIR=$(printf "a%.0s" {1..150}) && mkdir -p "$DIR" && touch "$DIR/f.txt" && git add -A && git commit -m test && ../git ls-files -- "$DIR/" ":(exclude)xy" Fix the bug by using a zero prefix when matching exclude items. Add regression tests for both the deterministic incorrect match and the shorter exclude item that causes the out-of-bounds access. Signed-off-by: Yannik Tausch Signed-off-by: Junio C Hamano --- dir.c | 2 +- t/t6132-pathspec-exclude.sh | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/dir.c b/dir.c index 32430090dcdf26..5f42c992d37cba 100644 --- a/dir.c +++ b/dir.c @@ -593,7 +593,7 @@ static int match_pathspec_with_flags(struct index_state *istate, if (!(ps->magic & PATHSPEC_EXCLUDE) || !positive) return positive; negative = do_match_pathspec(istate, ps, name, namelen, - prefix, seen, + 0, seen, flags | DO_MATCH_EXCLUDE); return negative ? 0 : positive; } diff --git a/t/t6132-pathspec-exclude.sh b/t/t6132-pathspec-exclude.sh index 9fdafeb1e907f4..e0c3f73ef07e2a 100755 --- a/t/t6132-pathspec-exclude.sh +++ b/t/t6132-pathspec-exclude.sh @@ -183,6 +183,24 @@ EOF test_cmp expect actual ' +test_expect_success 'negative pathspec shorter than positive pathspec prefix' ' + git ls-files -- sub/sub/ ":(exclude)sub2" >actual && + cat <<-\EOF >expect && + sub/sub/file + sub/sub/sub/file + EOF + test_cmp expect actual +' + +test_expect_success 'exclude is matched against the full path' ' + git ls-files -- sub/sub/ ":(exclude)zzzzzzz" >actual && + cat <<-\EOF >expect && + sub/sub/file + sub/sub/sub/file + EOF + test_cmp expect actual +' + test_expect_success 'multiple exclusions' ' git ls-files -- ":^*/file2" ":^sub2" >actual && cat <<-\EOF >expect && From b6f17686b20647fc904e6201fa6d168731e588eb Mon Sep 17 00:00:00 2001 From: Yannik Tausch Date: Mon, 14 Sep 2026 09:27:05 +0200 Subject: [PATCH 2/6] dir: preserve pathspec prefix optimization with leading excludes Directory walks use the common directory prefix of non-exclude pathspec items to avoid scanning unrelated portions of the working tree or index. Exclude items only remove paths from that candidate set, so they do not need to widen the traversal. When an exclude item is the first pathspec item, common_prefix_len() fails to establish a comparison base and returns a zero-length prefix. The result is correct, but Git unnecessarily traverses from a broader starting point even when all non-exclude items share a directory. Use the first non-exclude item as the comparison base and return its string together with the prefix length, allowing callers to start from the recovered directory prefix. Exclude matching continues to use full paths, so this restores the optimization without changing which paths are selected. Add a unit test covering an exclude item before two non-exclude items with a common directory. Signed-off-by: Yannik Tausch Signed-off-by: Junio C Hamano --- dir.c | 37 +++++++++++++++++++++---------------- t/unit-tests/u-dir.c | 28 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/dir.c b/dir.c index 5f42c992d37cba..abc4a78f31f2e7 100644 --- a/dir.c +++ b/dir.c @@ -212,9 +212,10 @@ static int fnmatch_icase_mem(const char *pattern, int patternlen, return match_status; } -static size_t common_prefix_len(const struct pathspec *pathspec) +static size_t common_prefix_len(const struct pathspec *pathspec, + const char **matched_prefix) { - int n; + int n, first = -1; size_t max = 0; /* @@ -237,43 +238,47 @@ static size_t common_prefix_len(const struct pathspec *pathspec) size_t i = 0, len = 0, item_len; if (pathspec->items[n].magic & PATHSPEC_EXCLUDE) continue; + if (first < 0) + first = n; if (pathspec->items[n].magic & PATHSPEC_ICASE) item_len = pathspec->items[n].prefix; else item_len = pathspec->items[n].nowildcard_len; - while (i < item_len && (n == 0 || i < max)) { + while (i < item_len && (n == first || i < max)) { char c = pathspec->items[n].match[i]; - if (c != pathspec->items[0].match[i]) + if (c != pathspec->items[first].match[i]) break; if (c == '/') len = i + 1; i++; } - if (n == 0 || len < max) { + if (n == first || len < max) { max = len; if (!max) break; } } + *matched_prefix = first < 0 ? NULL : pathspec->items[first].match; return max; } /* - * Returns a copy of the longest leading path common among all - * pathspecs. + * Returns a copy of the longest leading path common among all pathspec + * items that are not excluded. */ char *common_prefix(const struct pathspec *pathspec) { - unsigned long len = common_prefix_len(pathspec); + const char *matched_prefix; + size_t len = common_prefix_len(pathspec, &matched_prefix); - return len ? xmemdupz(pathspec->items[0].match, len) : NULL; + return len ? xmemdupz(matched_prefix, len) : NULL; } int fill_directory(struct dir_struct *dir, struct index_state *istate, const struct pathspec *pathspec) { - const char *prefix; + const char *matched_prefix; size_t prefix_len; unsigned exclusive_flags = DIR_SHOW_IGNORED | DIR_SHOW_IGNORED_TOO; @@ -284,11 +289,11 @@ int fill_directory(struct dir_struct *dir, * Calculate common prefix for the pathspec, and * use that to optimize the directory walk */ - prefix_len = common_prefix_len(pathspec); - prefix = prefix_len ? pathspec->items[0].match : ""; + prefix_len = common_prefix_len(pathspec, &matched_prefix); /* Read the directory and prune it */ - read_directory(dir, istate, prefix, prefix_len, pathspec); + read_directory(dir, istate, prefix_len ? matched_prefix : "", + prefix_len, pathspec); return prefix_len; } @@ -394,7 +399,7 @@ static int match_pathspec_item(struct index_state *istate, /* * The normal call pattern is: - * 1. prefix = common_prefix_len(ps); + * 1. prefix = common_prefix_len(ps, &matched_prefix); * 2. prune something, or fill_directory * 3. match_pathspec() * @@ -414,8 +419,8 @@ static int match_pathspec_item(struct index_state *istate, * Normally the caller (common_prefix_len() in fact) does * _exact_ matching on name[-prefix+1..-1] and we do not need * to check that part. Be defensive and check it anyway, in - * case common_prefix_len is changed, or a new caller is - * introduced that does not use common_prefix_len. + * case common_prefix_len() is changed, or a new caller is + * introduced that does not use common_prefix_len(). * * If the penalty turns out too high when prefix is really * long, maybe change it to diff --git a/t/unit-tests/u-dir.c b/t/unit-tests/u-dir.c index 2d0adaa39ed3d2..a3442c3d3c4121 100644 --- a/t/unit-tests/u-dir.c +++ b/t/unit-tests/u-dir.c @@ -45,3 +45,31 @@ void test_dir__within_depth(void) } + +void test_dir__common_prefix_skips_excluded_pathspec_items(void) +{ + struct pathspec_item items[] = { + { + .match = "unrelated/path", + .magic = PATHSPEC_EXCLUDE, + .nowildcard_len = 14, + }, + { + .match = "foo/bar", + .nowildcard_len = 7, + }, + { + .match = "foo/baz", + .nowildcard_len = 7, + }, + }; + struct pathspec pathspec = { + .nr = ARRAY_SIZE(items), + .magic = PATHSPEC_EXCLUDE, + .items = items, + }; + char *prefix = common_prefix(&pathspec); + + cl_assert_equal_s(prefix, "foo/"); + free(prefix); +} From db06ca011e12b8156f327c59f6d643ca7799d453 Mon Sep 17 00:00:00 2001 From: Todd Zullinger Date: Tue, 15 Sep 2026 09:10:31 -0400 Subject: [PATCH 3/6] doc/pack-refs: convert synopsis and options to new style Replace [verse] with [synopsis] in the SYNOPSIS block and remove single-quote formatting from the command name. Backtick-quote all option terms in the OPTIONS section via the included pack-refs-options.adoc and convert the standalone placeholder __ in prose. Signed-off-by: Todd Zullinger Signed-off-by: Junio C Hamano --- Documentation/git-pack-refs.adoc | 8 ++++---- Documentation/pack-refs-options.adoc | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Documentation/git-pack-refs.adoc b/Documentation/git-pack-refs.adoc index fde9f2f294e815..69e018d07e8010 100644 --- a/Documentation/git-pack-refs.adoc +++ b/Documentation/git-pack-refs.adoc @@ -7,8 +7,8 @@ git-pack-refs - Pack heads and tags for efficient repository access SYNOPSIS -------- -[verse] -'git pack-refs' [--all] [--no-prune] [--auto] [--include ] [--exclude ] +[synopsis] +git pack-refs [--all] [--no-prune] [--auto] [--include ] [--exclude ] DESCRIPTION ----------- @@ -52,8 +52,8 @@ BUGS ---- Older documentation written before the packed-refs mechanism was -introduced may still say things like ".git/refs/heads/ file -exists" when it means "branch exists". +introduced may still say things like ".git/refs/heads/__ file +exists" when it means "branch __ exists". GIT diff --git a/Documentation/pack-refs-options.adoc b/Documentation/pack-refs-options.adoc index 0b11282941bb02..2263648b39d921 100644 --- a/Documentation/pack-refs-options.adoc +++ b/Documentation/pack-refs-options.adoc @@ -1,4 +1,4 @@ ---all:: +`--all`:: The command by default packs all tags and refs that are already packed, and leaves other refs @@ -8,12 +8,12 @@ This option causes all refs to be packed as well, with the exception of hidden refs, broken refs, and symbolic refs. Useful for a repository with many branches of historical interests. ---no-prune:: +`--no-prune`:: The command usually removes loose refs under `$GIT_DIR/refs` hierarchy after packing them. This option tells it not to. ---auto:: +`--auto`:: Pack refs as needed depending on the current state of the ref database. The behavior depends on the ref format used by the repository and may change in the @@ -29,7 +29,7 @@ future. maintains the property that N is at least twice as big as N+1. Only tables that violate this property are compacted. ---include :: +`--include `:: Pack refs based on a `glob(7)` pattern. Repetitions of this option accumulate inclusion patterns. If a ref is both included in `--include` and @@ -38,7 +38,7 @@ tags from being included by default. Symbolic refs and broken refs will never be packed. When used with `--all`, it will be a noop. Use `--no-include` to clear and reset the list of patterns. ---exclude :: +`--exclude `:: Do not pack refs matching the given `glob(7)` pattern. Repetitions of this option accumulate exclusion patterns. Use `--no-exclude` to clear and reset the list of From 995251109fa3e631e6fc7204e8cad128e8da2217 Mon Sep 17 00:00:00 2001 From: Todd Zullinger Date: Tue, 15 Sep 2026 09:10:32 -0400 Subject: [PATCH 4/6] doc/refs: backtick-quote commands and options consistently The git-refs doc was converted to the synopsis style in 89be7d2774 (builtin/refs: add '--no-reflog' flag to drop reflogs, 2025-02-21). The commands and options were not backtick-quoted at that time. 84f3d6e11e (doc lint: check that synopsis manpages have synopsis inlines, 2025-08-11) applied backtick-quotes to the existing commands and options. Subsequently, a number of commands and options were added without such quoting, leaving the documentation rendered inconsistently. Apply backtick-quotes to all entries. Signed-off-by: Todd Zullinger Signed-off-by: Junio C Hamano --- Documentation/git-refs.adoc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc index 9063892651e478..9dc08cbca9dbd3 100644 --- a/Documentation/git-refs.adoc +++ b/Documentation/git-refs.adoc @@ -54,40 +54,40 @@ These limitations may eventually be lifted. `verify`:: Verify reference database consistency. -list:: +`list`:: List references in the repository with support for filtering, formatting, and sorting. This subcommand is an alias for linkgit:git-for-each-ref[1] and offers identical functionality. -exists:: +`exists`:: Check whether the given reference exists. Returns an exit code of 0 if it does, 2 if it is missing, and 1 in case looking up the reference failed with an error other than the reference being missing. This does not verify whether the reference resolves to an actual object. -optimize:: +`optimize`:: Optimizes references to improve repository performance and reduce disk usage. This subcommand is an alias for linkgit:git-pack-refs[1] and offers identical functionality. -create:: +`create`:: Create the given reference, which must not already exist, pointing at ``. -delete:: +`delete`:: Delete the given reference. This subcommand mirrors `git update-ref -d` (see linkgit:git-update-ref[1]). When `` is given, the reference is only deleted after verifying that it currently contains ``. -update:: +`update`:: Update the given reference to point at ``. If `` is given, the reference is only updated after verifying that it currently contains ``. As a special case, an all-zeroes `` deletes the branch, whereas an all-zeroes `` ensures that the branch does not yet exist. -rename:: +`rename`:: Rename the reference `` to ``. The old reference must exist and the new reference must not yet exist, and both must have a well-formed name (see linkgit:git-check-ref-format[1]). From 7b58375ab3b5865cdc44f11e72a6bcee779b63f0 Mon Sep 17 00:00:00 2001 From: Yoichi NAKAYAMA Date: Thu, 17 Sep 2026 13:39:53 +0000 Subject: [PATCH 5/6] mailmap: normalize name for Yoichi NAKAYAMA Normalize name formatting and map older formats to the canonical one. Signed-off-by: Yoichi NAKAYAMA Signed-off-by: Junio C Hamano --- .mailmap | 1 + 1 file changed, 1 insertion(+) diff --git a/.mailmap b/.mailmap index e3fab1df9dbfa8..29b48905327aa4 100644 --- a/.mailmap +++ b/.mailmap @@ -312,6 +312,7 @@ YONETANI Tomokazu YONETANI Tomokazu YOSHIFUJI Hideaki Yi-Jyun Pan +Yoichi NAKAYAMA # the two anonymous contributors are different persons: anonymous anonymous From d38352cd43ab9745686d697872408bc3249a153f Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 17 Sep 2026 09:48:07 -0700 Subject: [PATCH 6/6] A few more fixes before -rc2 Signed-off-by: Junio C Hamano --- Documentation/RelNotes/2.56.0.adoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Documentation/RelNotes/2.56.0.adoc b/Documentation/RelNotes/2.56.0.adoc index 83d2f90daab35c..7056e990fa49ea 100644 --- a/Documentation/RelNotes/2.56.0.adoc +++ b/Documentation/RelNotes/2.56.0.adoc @@ -906,6 +906,10 @@ Fixes since v2.55 repo_logmsg_reencode() during the rewording operation in 'git history' has been plugged. + * The pathspec matching logic has been updated to avoid out-of-bounds + memory accesses when a negative pathspec is shorter than the common + prefix of positive pathspecs. + * Other code cleanup, docfix, build fix, etc. (merge 026636128f ss/submittingpatches-typofix later to maint). (merge d2af22cc21 jc/rerere-doc-typofix later to maint).