From 6abbdd61a5e9b360659a2a015e35d49a496357e9 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 2 Jul 2026 13:04:30 +0200 Subject: [PATCH 1/8] fixup! object-file.c: use size_t for header lengths These gaps were identified while upstreaming the patches. Signed-off-by: Johannes Schindelin --- odb/source-files.c | 2 +- odb/source-inmemory.c | 2 +- odb/source-loose.c | 2 +- odb/source.h | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/odb/source-files.c b/odb/source-files.c index 5bdd0429225397..3b1261eba9700e 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -159,7 +159,7 @@ static int odb_source_files_freshen_object(struct odb_source *source, } static int odb_source_files_write_object(struct odb_source *source, - const void *buf, unsigned long len, + const void *buf, size_t len, enum object_type type, struct object_id *oid, struct object_id *compat_oid, diff --git a/odb/source-inmemory.c b/odb/source-inmemory.c index e004566d768b01..7f1b6f46363f2e 100644 --- a/odb/source-inmemory.c +++ b/odb/source-inmemory.c @@ -227,7 +227,7 @@ static int odb_source_inmemory_count_objects(struct odb_source *source, } static int odb_source_inmemory_write_object(struct odb_source *source, - const void *buf, unsigned long len, + const void *buf, size_t len, enum object_type type, struct object_id *oid, struct object_id *compat_oid UNUSED, diff --git a/odb/source-loose.c b/odb/source-loose.c index 2bf89626c620c7..6211348a4d3561 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -583,7 +583,7 @@ static int odb_source_loose_freshen_object(struct odb_source *source, } static int odb_source_loose_write_object(struct odb_source *source, - const void *buf, unsigned long len, + const void *buf, size_t len, enum object_type type, struct object_id *oid, struct object_id *compat_oid_in, enum odb_write_object_flags flags) diff --git a/odb/source.h b/odb/source.h index 2192a101b8ab08..1c65a05e2c4c6b 100644 --- a/odb/source.h +++ b/odb/source.h @@ -199,7 +199,7 @@ struct odb_source { * return 0 on success, a negative error code otherwise. */ int (*write_object)(struct odb_source *source, - const void *buf, unsigned long len, + const void *buf, size_t len, enum object_type type, struct object_id *oid, struct object_id *compat_oid, From 3ae86cd84ca54629e3bcebee64ed76ed0445d239 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 2 Jul 2026 13:05:26 +0200 Subject: [PATCH 2/8] fixup! hash algorithms: use size_t for section lengths While upstreaming the patch, it was pointed out that we should now drop that prereq. Signed-off-by: Johannes Schindelin --- t/t1007-hash-object.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/t1007-hash-object.sh b/t/t1007-hash-object.sh index 841a6671d1a3c1..f0f0a7bbc503e4 100755 --- a/t/t1007-hash-object.sh +++ b/t/t1007-hash-object.sh @@ -261,7 +261,7 @@ test_expect_success '--stdin outside of repository (uses default hash)' ' test_cmp expect actual ' -test_expect_success EXPENSIVE,SIZE_T_IS_64BIT,!LONG_IS_64BIT \ +test_expect_success EXPENSIVE,SIZE_T_IS_64BIT \ 'files over 4GB hash literally' ' test-tool genzeros $((5*1024*1024*1024)) >big && test_oid large5GB >expect && From 6bcaa3b8c6cba387a83b9c3f94b91bf2706c67d8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 2 Jul 2026 13:06:07 +0200 Subject: [PATCH 3/8] fixup! hash-object --stdin: verify that it works with >4GB/LLP64 While upstreaming the patch, it was pointed out that we should now drop that prereq. Signed-off-by: Johannes Schindelin --- t/t1007-hash-object.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/t1007-hash-object.sh b/t/t1007-hash-object.sh index f0f0a7bbc503e4..cce0c56619d171 100755 --- a/t/t1007-hash-object.sh +++ b/t/t1007-hash-object.sh @@ -269,7 +269,7 @@ test_expect_success EXPENSIVE,SIZE_T_IS_64BIT \ test_cmp expect actual ' -test_expect_success EXPENSIVE,SIZE_T_IS_64BIT,!LONG_IS_64BIT \ +test_expect_success EXPENSIVE,SIZE_T_IS_64BIT \ 'files over 4GB hash correctly via --stdin' ' { test -f big || test-tool genzeros $((5*1024*1024*1024)) >big; } && test_oid large5GB >expect && From ede99ee512eda5a6459b15b7bae5b2d7ff2e01d1 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 2 Jul 2026 13:06:24 +0200 Subject: [PATCH 4/8] fixup! hash-object: add another >4GB/LLP64 test case While upstreaming the patch, it was pointed out that we should now drop that prereq. Signed-off-by: Johannes Schindelin --- t/t1007-hash-object.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/t1007-hash-object.sh b/t/t1007-hash-object.sh index cce0c56619d171..c0b84b6c522d76 100755 --- a/t/t1007-hash-object.sh +++ b/t/t1007-hash-object.sh @@ -277,7 +277,7 @@ test_expect_success EXPENSIVE,SIZE_T_IS_64BIT \ test_cmp expect actual ' -test_expect_success EXPENSIVE,SIZE_T_IS_64BIT,!LONG_IS_64BIT \ +test_expect_success EXPENSIVE,SIZE_T_IS_64BIT \ 'files over 4GB hash correctly' ' { test -f big || test-tool genzeros $((5*1024*1024*1024)) >big; } && test_oid large5GB >expect && From c8dff95af823bd04181a583c03f3fbb954551c30 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 2 Jul 2026 13:06:33 +0200 Subject: [PATCH 5/8] fixup! hash-object: add a >4GB/LLP64 test case using filtered input While upstreaming the patch, it was pointed out that we should now drop that prereq. Signed-off-by: Johannes Schindelin --- t/t1007-hash-object.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/t1007-hash-object.sh b/t/t1007-hash-object.sh index c0b84b6c522d76..4bc82dd9682954 100755 --- a/t/t1007-hash-object.sh +++ b/t/t1007-hash-object.sh @@ -287,7 +287,7 @@ test_expect_success EXPENSIVE,SIZE_T_IS_64BIT \ # This clean filter does nothing, other than excercising the interface. # We ensure that cleaning doesn't mangle large files on 64-bit Windows. -test_expect_success EXPENSIVE,SIZE_T_IS_64BIT,!LONG_IS_64BIT \ +test_expect_success EXPENSIVE,SIZE_T_IS_64BIT \ 'hash filtered files over 4GB correctly' ' { test -f big || test-tool genzeros $((5*1024*1024*1024)) >big; } && test_oid large5GB >expect && From b4d1fb62c7e2fdcab5a018e25e51acb196eb1756 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 29 Jun 2026 13:07:18 +0200 Subject: [PATCH 6/8] wincred: avoid memory corruption when erasing a credential The earlier d22a488482 (wincred: avoid memory corruption, 2025-11-17) repaired only get_credential(); match_cred_password() has the same defect and is reached on `git credential reject`. When Git asks the helper to erase a stored credential whose password was supplied by the caller, the helper copies the candidate's password into a freshly allocated buffer for comparison. That copy overruns the allocation by one WCHAR of NUL, which on uninstrumented Windows manifests as process termination with status 0xC0000374. Because the helper can die before reaching CredDeleteW(), `git credential reject` masks the failure and the rejected credential remains stored. CredentialBlobSize is documented as a byte count, so for an N-WCHAR blob it equals N * sizeof(WCHAR). The pre-fix code allocated that many bytes and asked wcsncpy_s to copy N wide characters, but wcsncpy_s always appends a terminating NUL WCHAR, writing one WCHAR past the allocation. The destination-capacity argument was also passed in bytes rather than in WCHAR elements as the API requires, so the safe-CRT runtime never rejected the copy. See GHSA-rxqw-wxqg-g7hw. Assisted-by: Opus 4.7 Signed-off-by: Johannes Schindelin --- contrib/credential/wincred/git-credential-wincred.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contrib/credential/wincred/git-credential-wincred.c b/contrib/credential/wincred/git-credential-wincred.c index 73c2b9b72ab53e..190bbccdf9e84c 100644 --- a/contrib/credential/wincred/git-credential-wincred.c +++ b/contrib/credential/wincred/git-credential-wincred.c @@ -121,10 +121,10 @@ static int match_part_last(LPCWSTR *ptarget, LPCWSTR want, LPCWSTR delim) static int match_cred_password(const CREDENTIALW *cred) { int ret; - WCHAR *cred_password = xmalloc(cred->CredentialBlobSize); - wcsncpy_s(cred_password, cred->CredentialBlobSize, - (LPCWSTR)cred->CredentialBlob, - cred->CredentialBlobSize / sizeof(WCHAR)); + size_t wlen = cred->CredentialBlobSize / sizeof(WCHAR); + WCHAR *cred_password = xmalloc((wlen + 1) * sizeof(WCHAR)); + wcsncpy_s(cred_password, wlen + 1, + (LPCWSTR)cred->CredentialBlob, wlen); ret = !wcscmp(cred_password, password); free(cred_password); return ret; From 52ca1113d651127f89477a8763f86ab20f645e1d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 29 Jun 2026 13:13:06 +0200 Subject: [PATCH 7/8] wincred: prevent silent credential loss when storing OAuth tokens When `git credential approve` hands the wincred helper a password together with an `oauth_refresh_token`, the OAuth branch of `store_credential()` writes one WCHAR past the allocation while formatting both fields into a single `CredentialBlob`. On Windows this trips heap verification and tears the helper down with status `0xC0000374`; `approve` masks the failure, so the credential the user meant to save never reaches `CredWriteW()` and the next session prompts for it again. The bug has the same shape as the one fixed in the previous commit: the allocation leaves no room for the terminating NUL, and the `sizeOfBuffer` argument to `_snwprintf_s()` is a byte count where the API expects a WCHAR count, which lets the safe-CRT runtime write the terminator out of bounds. Apply the same remedy d22a488482 (wincred: avoid memory corruption, 2025-11-17) applied in `get_credential()`: allocate `(wlen + 1) * sizeof(WCHAR)` bytes and pass `wlen + 1` as the destination capacity in WCHARs. This closes the second of the two heap writes tracked under GHSA-rxqw-wxqg-g7hw. Assisted-by: Opus 4.7 Signed-off-by: Johannes Schindelin --- contrib/credential/wincred/git-credential-wincred.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/credential/wincred/git-credential-wincred.c b/contrib/credential/wincred/git-credential-wincred.c index 190bbccdf9e84c..22eb27ca31dea0 100644 --- a/contrib/credential/wincred/git-credential-wincred.c +++ b/contrib/credential/wincred/git-credential-wincred.c @@ -208,8 +208,8 @@ static void store_credential(void) if (oauth_refresh_token) { wlen = _scwprintf(L"%s\r\noauth_refresh_token=%s", password, oauth_refresh_token); - secret = xmalloc(sizeof(WCHAR) * wlen); - _snwprintf_s(secret, sizeof(WCHAR) * wlen, wlen, L"%s\r\noauth_refresh_token=%s", password, oauth_refresh_token); + secret = xmalloc((wlen + 1) * sizeof(WCHAR)); + _snwprintf_s(secret, wlen + 1, wlen, L"%s\r\noauth_refresh_token=%s", password, oauth_refresh_token); } else { secret = _wcsdup(password); } From a93524749d7806870fd2b4b00a3812da1d6e5f4a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 26 Jun 2026 12:15:27 +0200 Subject: [PATCH 8/8] bundle-uri: refuse advertised URIs by protocol Servers may advertise bundle URIs that are not HTTP(S); copy_uri_to_file() then opens it as a local path. On Windows that can be a UNC path like `//attacker/share/x`, i.e. a clone can be manipulated into making an outbound SMB connection that leaks NTLM credentials (CVE-2026-62960). Subject advertised URIs to the usual protocol allow-list (`protocol.*.allow`), which drops "file" (and bare/UNC paths) by default but keeps http/https/git/ssh. Do it in fetch_bundle_list(), the clone/fetch consume path, so ls-remote still lists everything; each skipped URI is reported. A user-supplied `--bundle-uri` is unaffected, and `protocol.file.allow=always` re-enables an advertised file URI. Assisted-by: Opus 4.7 Signed-off-by: Johannes Schindelin --- bundle-uri.c | 50 ++++++++++++++++++++++++++++++++ t/lib-bundle-uri-protocol.sh | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/bundle-uri.c b/bundle-uri.c index 3b2e347288c3b7..dfb5a193fff521 100644 --- a/bundle-uri.c +++ b/bundle-uri.c @@ -15,6 +15,8 @@ #include "remote.h" #include "trace2.h" #include "odb.h" +#include "transport.h" +#include "url.h" static struct { enum bundle_list_heuristic heuristic; @@ -890,11 +892,59 @@ int fetch_bundle_uri(struct repository *r, const char *uri, return result; } +/* protocol of 'uri', or "file" if it has none (bare/UNC/relative path) */ +static void bundle_uri_protocol(const char *uri, struct strbuf *out) +{ + const char *p = uri; + + while (is_urlschemechar(p == uri, *p)) + p++; + strbuf_reset(out); + if (p > uri && starts_with(p, "://")) + strbuf_add(out, uri, p - uri); + else + strbuf_addstr(out, "file"); +} + +/* Drop advertised URIs whose protocol is not allowed (see protocol.*.allow). */ +static void sanitize_bundle_list(struct bundle_list *list) +{ + struct remote_bundle_info **skipped; + size_t nr = 0, i; + struct remote_bundle_info *info; + struct hashmap_iter iter; + struct strbuf proto = STRBUF_INIT; + + ALLOC_ARRAY(skipped, hashmap_get_size(&list->bundles)); + hashmap_for_each_entry(&list->bundles, &iter, info, ent) { + if (!info->uri) + continue; + bundle_uri_protocol(info->uri, &proto); + /* advertised URIs are not user-provided */ + if (!is_transport_allowed(proto.buf, 0)) { + warning(_("skipping bundle URI '%s': protocol '%s' " + "is not allowed"), info->uri, proto.buf); + skipped[nr++] = info; + } + } + strbuf_release(&proto); + + for (i = 0; i < nr; i++) { + hashmap_remove(&list->bundles, &skipped[i]->ent, NULL); + clear_remote_bundle_info(skipped[i], NULL); + free(skipped[i]); + } + + free(skipped); +} + int fetch_bundle_list(struct repository *r, struct bundle_list *list) { int result; struct bundle_list global_list; + sanitize_bundle_list(list); + /* * If the creationToken heuristic is used, then the URIs * advertised by 'list' are not nested lists and instead diff --git a/t/lib-bundle-uri-protocol.sh b/t/lib-bundle-uri-protocol.sh index de09b6b02e2485..b1bf940cbd8b9b 100644 --- a/t/lib-bundle-uri-protocol.sh +++ b/t/lib-bundle-uri-protocol.sh @@ -214,3 +214,59 @@ test_expect_success "test bundle-uri with $BUNDLE_URI_PROTOCOL:// using protocol >actual && test_cmp_config_output expect actual ' + +# Advertised bundle URIs are subject to protocol.*.allow; "file" (and bare or +# UNC paths) is denied by default, so such a URI must be skipped, not fetched. +advertise_uri () { + test_config -C "$BUNDLE_URI_PARENT" bundle.version 1 && + test_config -C "$BUNDLE_URI_PARENT" bundle.mode all && + test_config -C "$BUNDLE_URI_PARENT" bundle.payload.uri "$1" +} + +ignores_advertised_uri () { + rm -rf victim && + advertise_uri "$1" && + git -c transfer.bundleURI=true -c protocol.version=2 \ + clone "$BUNDLE_URI_REPO_URI" victim && + git -C victim for-each-ref refs/bundles/ >refs && + test_must_be_empty refs +} + +test_expect_success "create bundle to advertise" ' + git -C "$BUNDLE_URI_PARENT" bundle create "$PWD/payload.bundle" main +' + +test_expect_success "ignore non-HTTP(S) bundle URI with $BUNDLE_URI_PROTOCOL://" ' + ignores_advertised_uri "$PWD/payload.bundle" && + ignores_advertised_uri "file://$PWD/payload.bundle" +' + +test_expect_success "protocol.file.allow=always honors file bundle URI with $BUNDLE_URI_PROTOCOL://" ' + rm -rf victim && + advertise_uri "$PWD/payload.bundle" && + git -c transfer.bundleURI=true -c protocol.version=2 \ + -c protocol.file.allow=always \ + clone "$BUNDLE_URI_REPO_URI" victim && + git -C victim rev-parse --verify refs/bundles/heads/main +' + +# same path via a UNC administrative share (cf. t5580-unc-paths.sh) +if test_have_prereq CYGWIN +then + UNCPATH="$(cygpath -aw .)" +elif test_have_prereq MINGW +then + UNCPATH="$(pwd)" +fi +case "$UNCPATH" in +[A-Za-z]:*) + WITHOUTDRIVE="${UNCPATH#?:}" + UNCPATH="//localhost/${UNCPATH%%:*}\$$WITHOUTDRIVE" + test -d "$UNCPATH" && test_set_prereq ADMIN_UNC + ;; +esac + +test_expect_success ADMIN_UNC "ignore UNC bundle URI with $BUNDLE_URI_PROTOCOL://" ' + ignores_advertised_uri "$UNCPATH/payload.bundle" && + ignores_advertised_uri "file://$UNCPATH/payload.bundle" +'