threads / patch / 65051

patch, 4 partspack-check: fix verification of large objects

Subject: [PATCH 0/4] pack-check: fix verification of large objects

## tl;dr

26 messages between Feb 23, 2026 and Feb 24, 2026. Diffs are folded; open one to read it.

replies: 25people: 4as markdown or json

Patrick Steinhardt· Feb 23, 2026, 09:50 UTC · lore
Hi,

this small patch series addresses the bug reported by brian in [1]. Thanks!

Patrick
[1]: <20260222183710.2963424-1-sandals@crustytoothpaste.net>
---
Patrick Steinhardt (4):
      t/helper: improve "genrandom" test helper
      object-file: adapt `stream_object_signature()` to take a stream
      packfile: expose function to read object stream for an offset
      pack-check: fix verification of large objects
 object-file.c                         | 10 +++-------
 object-file.h                         |  4 +++-
 object.c                              | 15 ++++++++++++---
 pack-check.c                          | 12 +++++++++---
 packfile.c                            | 36 +++++++++++++++++++----------------
 packfile.h                            |  4 ++++
 t/helper/test-genrandom.c             |  5 ++++-
 t/t1006-cat-file.sh                   |  2 +-
 t/t1050-large.sh                      |  6 +++---
 t/t1450-fsck.sh                       | 17 ++++++++++++++++-
 t/t5301-sliding-window.sh             |  2 +-
 t/t5310-pack-bitmaps.sh               |  2 +-
 t/t5710-promisor-remote-capability.sh |  4 ++--
 t/t7700-repack.sh                     |  6 +++---
 14 files changed, 82 insertions(+), 43 deletions(-)

--- base-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4 change-id: 20260223-pks-fsck-fix-aa8a18a223c8

Patrick Steinhardt· Feb 23, 2026, 09:50 UTC · re: Patrick Steinhardt · lore

[PATCH 1/4] t/helper: improve "genrandom" test helper

The `test-tool genrandom` test helper can be used to generate random data, either as an infinite stream or with a specified number of bytes. The way we handle parsing the number of bytes is lacking though:

  - We don't have good error handling, so if the caller for example uses
    `test-tool genrandom 200xyz` then we'll end up generating 200 bytes
    of random data successfully.
  - Many callers want to generate e.g. 1 kilobyte or megabyte of data,
    but they have to either use unwieldy numbers like 1048576, or they
    have to precompute them.

Fix both of these issues by using `git_parse_ulong()` to parse the argumemnt. This function has better error handling, and it knows to handle unit suffixes.

Adapt a couple of our tests to use suffixes instead of manual computations.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 t/helper/test-genrandom.c             | 5 ++++-
 t/t1006-cat-file.sh                   | 2 +-
 t/t1050-large.sh                      | 6 +++---
 t/t1450-fsck.sh                       | 2 +-
 t/t5301-sliding-window.sh             | 2 +-
 t/t5310-pack-bitmaps.sh               | 2 +-
 t/t5710-promisor-remote-capability.sh | 4 ++--
 t/t7700-repack.sh                     | 6 +++---
 8 files changed, 16 insertions(+), 13 deletions(-)
Show changes to 8 files +16 −13

t/helper/test-genrandom.c, t/t1006-cat-file.sh, t/t1050-large.sh, t/t1450-fsck.sh, t/t5301-sliding-window.sh, t/t5310-pack-bitmaps.sh, t/t5710-promisor-remote-capability.sh, t/t7700-repack.sh

diff --git a/t/helper/test-genrandom.c b/t/helper/test-genrandom.c
index 51b67f2f87..77dc31a315 100644
--- a/t/helper/test-genrandom.c
+++ b/t/helper/test-genrandom.c
@@ -6,6 +6,7 @@
 
 #include "test-tool.h"
 #include "git-compat-util.h"
+#include "parse.h"
 
 int cmd__genrandom(int argc, const char **argv)
 {
@@ -22,7 +23,9 @@ int cmd__genrandom(int argc, const char **argv)
 		next = next * 11 + *c;
 	} while (*c++);
 
-	count = (argc == 3) ? strtoul(argv[2], NULL, 0) : ULONG_MAX;
+	count = ULONG_MAX;
+	if (argc == 3 && git_parse_ulong(argv[2], &count) < 0)
+		return error_errno("cannot parse argument '%s'", argv[2]);
 
 	while (count--) {
 		next = next * 1103515245 + 12345;
diff --git a/t/t1006-cat-file.sh b/t/t1006-cat-file.sh
index 0eee3bb878..5499be8dc9 100755
--- a/t/t1006-cat-file.sh
+++ b/t/t1006-cat-file.sh
@@ -643,7 +643,7 @@ test_expect_success 'object reference via commit text search' '
 '
 
 test_expect_success 'setup blobs which are likely to delta' '
-	test-tool genrandom foo 10240 >foo &&
+	test-tool genrandom foo 10k >foo &&
 	{ cat foo && echo plus; } >foo-plus &&
 	git add foo foo-plus &&
 	git commit -m foo &&
diff --git a/t/t1050-large.sh b/t/t1050-large.sh
index 5be273611a..7d40d08521 100755
--- a/t/t1050-large.sh
+++ b/t/t1050-large.sh
@@ -104,9 +104,9 @@ test_expect_success 'packsize limit' '
 		# mid1 and mid2 will fit within 256k limit but
 		# appending mid3 will bust the limit and will
 		# result in a separate packfile.
-		test-tool genrandom "a" $(( 66 * 1024 )) >mid1 &&
-		test-tool genrandom "b" $(( 80 * 1024 )) >mid2 &&
-		test-tool genrandom "c" $(( 128 * 1024 )) >mid3 &&
+		test-tool genrandom "a" 66k >mid1 &&
+		test-tool genrandom "b" 80k >mid2 &&
+		test-tool genrandom "c" 128k >mid3 &&
 		git add mid1 mid2 mid3 &&
 
 		count=0 &&
diff --git a/t/t1450-fsck.sh b/t/t1450-fsck.sh
index 3fae05f9d9..8fb79b3e5d 100755
--- a/t/t1450-fsck.sh
+++ b/t/t1450-fsck.sh
@@ -918,7 +918,7 @@ test_expect_success 'fsck detects trailing loose garbage (large blob)' '
 test_expect_success 'fsck detects truncated loose object' '
 	# make it big enough that we know we will truncate in the data
 	# portion, not the header
-	test-tool genrandom truncate 4096 >file &&
+	test-tool genrandom truncate 4k >file &&
 	blob=$(git hash-object -w file) &&
 	file=$(sha1_file $blob) &&
 	test_when_finished "remove_object $blob" &&
diff --git a/t/t5301-sliding-window.sh b/t/t5301-sliding-window.sh
index ff6b5159a3..3c3666b278 100755
--- a/t/t5301-sliding-window.sh
+++ b/t/t5301-sliding-window.sh
@@ -12,7 +12,7 @@ test_expect_success 'setup' '
 	for i in a b c
 	do
 	echo $i >$i &&
-	test-tool genrandom "$i" 32768 >>$i &&
+	test-tool genrandom "$i" 32k >>$i &&
 	git update-index --add $i || return 1
 	done &&
 	echo d >d && cat c >>d && git update-index --add d &&
diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh
index 6718fb98c0..3e3366f57d 100755
--- a/t/t5310-pack-bitmaps.sh
+++ b/t/t5310-pack-bitmaps.sh
@@ -242,7 +242,7 @@ test_bitmap_cases () {
 	'
 
 	test_expect_success 'splitting packs does not generate bogus bitmaps' '
-		test-tool genrandom foo $((1024 * 1024)) >rand &&
+		test-tool genrandom foo 1m >rand &&
 		git add rand &&
 		git commit -m "commit with big file" &&
 		git -c pack.packSizeLimit=500k repack -adb &&
diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh
index 023735d6a8..66af84cd56 100755
--- a/t/t5710-promisor-remote-capability.sh
+++ b/t/t5710-promisor-remote-capability.sh
@@ -20,7 +20,7 @@ test_expect_success 'setup: create "template" repository' '
 	test_commit -C template 1 &&
 	test_commit -C template 2 &&
 	test_commit -C template 3 &&
-	test-tool genrandom foo 10240 >template/foo &&
+	test-tool genrandom foo 10k >template/foo &&
 	git -C template add foo &&
 	git -C template commit -m foo
 '
@@ -376,7 +376,7 @@ test_expect_success "clone with promisor.advertise set to 'true' but don't delet
 
 test_expect_success "setup for subsequent fetches" '
 	# Generate new commit with large blob
-	test-tool genrandom bar 10240 >template/bar &&
+	test-tool genrandom bar 10k >template/bar &&
 	git -C template add bar &&
 	git -C template commit -m bar &&
 
diff --git a/t/t7700-repack.sh b/t/t7700-repack.sh
index 73b78bdd88..439ab24d23 100755
--- a/t/t7700-repack.sh
+++ b/t/t7700-repack.sh
@@ -319,7 +319,7 @@ test_expect_success 'no bitmaps created if .keep files present' '
 
 test_expect_success 'auto-bitmaps do not complain if unavailable' '
 	test_config -C bare.git pack.packSizeLimit 1M &&
-	blob=$(test-tool genrandom big $((1024*1024)) |
+	blob=$(test-tool genrandom big 1m |
 	       git -C bare.git hash-object -w --stdin) &&
 	git -C bare.git update-ref refs/tags/big $blob &&
 
@@ -495,9 +495,9 @@ test_expect_success '--filter works with --max-pack-size' '
 		cd max-pack-size &&
 		test_commit base &&
 		# two blobs which exceed the maximum pack size
-		test-tool genrandom foo 1048576 >foo &&
+		test-tool genrandom foo 1m >foo &&
 		git hash-object -w foo &&
-		test-tool genrandom bar 1048576 >bar &&
+		test-tool genrandom bar 1m >bar &&
 		git hash-object -w bar &&
 		git add foo bar &&
 		git commit -m "adding foo and bar"
-- 
2.53.0.414.gf7e9f6c205.dirty
Jeff King· Feb 23, 2026, 11:13 UTC · re: Patrick Steinhardt · lore

Re: [PATCH 1/4] t/helper: improve "genrandom" test helper

On Mon, Feb 23, 2026 at 10:50:40AM +0100, Patrick Steinhardt wrote:
> Fix both of these issues by using `git_parse_ulong()` to parse the
> argumemnt. This function has better error handling, and it knows to
> handle unit suffixes.
Makes sense, but...
Show 8 quoted lines
> @@ -22,7 +23,9 @@ int cmd__genrandom(int argc, const char **argv)
>  		next = next * 11 + *c;
>  	} while (*c++);
>  
> -	count = (argc == 3) ? strtoul(argv[2], NULL, 0) : ULONG_MAX;
> +	count = ULONG_MAX;
> +	if (argc == 3 && git_parse_ulong(argv[2], &count) < 0)
> +		return error_errno("cannot parse argument '%s'", argv[2]);

...I think the return value of git_parse_ulong() is boolean 0/1, not 0/negative.

-Peff
Patrick Steinhardt· Feb 23, 2026, 12:20 UTC · re: Jeff King · lore

Re: [PATCH 1/4] t/helper: improve "genrandom" test helper

On Mon, Feb 23, 2026 at 06:13:46AM -0500, Jeff King wrote:
Show 12 quoted lines
> On Mon, Feb 23, 2026 at 10:50:40AM +0100, Patrick Steinhardt wrote:
> > @@ -22,7 +23,9 @@ int cmd__genrandom(int argc, const char **argv)
> >  		next = next * 11 + *c;
> >  	} while (*c++);
> >  
> > -	count = (argc == 3) ? strtoul(argv[2], NULL, 0) : ULONG_MAX;
> > +	count = ULONG_MAX;
> > +	if (argc == 3 && git_parse_ulong(argv[2], &count) < 0)
> > +		return error_errno("cannot parse argument '%s'", argv[2]);
> 
> ...I think the return value of git_parse_ulong() is boolean 0/1, not
> 0/negative.
Ugh, right. Will fix.
Patrick
Eric Sunshine· Feb 23, 2026, 14:01 UTC · re: Patrick Steinhardt · lore

Re: [PATCH 1/4] t/helper: improve "genrandom" test helper

On Mon, Feb 23, 2026 at 4:51 AM Patrick Steinhardt <ps@pks.im> wrote:
Show 15 quoted lines
> The `test-tool genrandom` test helper can be used to generate random
> data, either as an infinite stream or with a specified number of bytes.
> The way we handle parsing the number of bytes is lacking though:
>
>   - We don't have good error handling, so if the caller for example uses
>     `test-tool genrandom 200xyz` then we'll end up generating 200 bytes
>     of random data successfully.
>
>   - Many callers want to generate e.g. 1 kilobyte or megabyte of data,
>     but they have to either use unwieldy numbers like 1048576, or they
>     have to precompute them.
>
> Fix both of these issues by using `git_parse_ulong()` to parse the
> argumemnt. This function has better error handling, and it knows to
> handle unit suffixes.
s/argumemnt/argument/
> Adapt a couple of our tests to use suffixes instead of manual
> computations.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
Patrick Steinhardt· Feb 23, 2026, 09:50 UTC · re: Patrick Steinhardt · lore

[PATCH 2/4] object-file: adapt `stream_object_signature()` to take a stream

The function `stream_object_signature()` is responsible for verifying whether the given object ID matches the actual hash of the object's contents. In contrast to `check_object_signature()` it does so in a streaming fashion so that we don't have to load the full object into memory.

In a subsequent commit we'll want to adapt one of its callsites to pass a preconstructed stream. Prepare for this by accepting a stream as input that the caller needs to assemble.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 object-file.c | 10 +++-------
 object-file.h |  4 +++-
 object.c      | 15 ++++++++++++---
 pack-check.c  | 12 +++++++++---
 4 files changed, 27 insertions(+), 14 deletions(-)
Show changes to 4 files +27 −14

object-file.c, object-file.h, object.c, pack-check.c

diff --git a/object-file.c b/object-file.c
index 1b62996ef0..ca2c4dddf3 100644
--- a/object-file.c
+++ b/object-file.c
@@ -129,18 +129,15 @@ int check_object_signature(struct repository *r, const struct object_id *oid,
 	return !oideq(oid, &real_oid) ? -1 : 0;
 }
 
-int stream_object_signature(struct repository *r, const struct object_id *oid)
+int stream_object_signature(struct repository *r,
+			    struct odb_read_stream *st,
+			    const struct object_id *oid)
 {
 	struct object_id real_oid;
-	struct odb_read_stream *st;
 	struct git_hash_ctx c;
 	char hdr[MAX_HEADER_LEN];
 	int hdrlen;
 
-	st = odb_read_stream_open(r->objects, oid, NULL);
-	if (!st)
-		return -1;
-
 	/* Generate the header */
 	hdrlen = format_object_header(hdr, sizeof(hdr), st->type, st->size);
 
@@ -160,7 +157,6 @@ int stream_object_signature(struct repository *r, const struct object_id *oid)
 		git_hash_update(&c, buf, readlen);
 	}
 	git_hash_final_oid(&real_oid, &c);
-	odb_read_stream_close(st);
 	return !oideq(oid, &real_oid) ? -1 : 0;
 }
 
diff --git a/object-file.h b/object-file.h
index a62d0de394..733d232309 100644
--- a/object-file.h
+++ b/object-file.h
@@ -164,7 +164,9 @@ int check_object_signature(struct repository *r, const struct object_id *oid,
  * Try reading the object named with "oid" using
  * the streaming interface and rehash it to do the same.
  */
-int stream_object_signature(struct repository *r, const struct object_id *oid);
+int stream_object_signature(struct repository *r,
+			    struct odb_read_stream *stream,
+			    const struct object_id *oid);
 
 enum finalize_object_file_flags {
 	FOF_SKIP_COLLISION_CHECK = 1,
diff --git a/object.c b/object.c
index 4669b8d65e..56d79d77b4 100644
--- a/object.c
+++ b/object.c
@@ -6,6 +6,7 @@
 #include "object.h"
 #include "replace-object.h"
 #include "object-file.h"
+#include "odb/streaming.h"
 #include "blob.h"
 #include "statinfo.h"
 #include "tree.h"
@@ -330,9 +331,17 @@ struct object *parse_object_with_flags(struct repository *r,
 
 	if ((!obj || obj->type == OBJ_NONE || obj->type == OBJ_BLOB) &&
 	    odb_read_object_info(r->objects, oid, NULL) == OBJ_BLOB) {
-		if (!skip_hash && stream_object_signature(r, repl) < 0) {
-			error(_("hash mismatch %s"), oid_to_hex(oid));
-			return NULL;
+		if (!skip_hash) {
+			struct odb_read_stream *stream = odb_read_stream_open(r->objects, oid, NULL);
+			if (!stream || stream_object_signature(r, stream, repl) < 0) {
+				error(_("hash mismatch %s"), oid_to_hex(oid));
+				if (stream)
+					odb_read_stream_close(stream);
+				return NULL;
+			}
+
+			if (stream)
+				odb_read_stream_close(stream);
 		}
 		parse_blob_buffer(lookup_blob(r, oid));
 		return lookup_object(r, oid);
diff --git a/pack-check.c b/pack-check.c
index 67cb2cf72f..46782a29d5 100644
--- a/pack-check.c
+++ b/pack-check.c
@@ -9,6 +9,7 @@
 #include "packfile.h"
 #include "object-file.h"
 #include "odb.h"
+#include "odb/streaming.h"
 
 struct idx_entry {
 	off_t                offset;
@@ -104,6 +105,7 @@ static int verify_packfile(struct repository *r,
 	QSORT(entries, nr_objects, compare_entries);
 
 	for (i = 0; i < nr_objects; i++) {
+		struct odb_read_stream *stream = NULL;
 		void *data;
 		struct object_id oid;
 		enum object_type type;
@@ -152,7 +154,9 @@ static int verify_packfile(struct repository *r,
 							type) < 0)
 			err = error("packed %s from %s is corrupt",
 				    oid_to_hex(&oid), p->pack_name);
-		else if (!data && stream_object_signature(r, &oid) < 0)
+		else if (!data &&
+			 (!(stream = odb_read_stream_open(r->objects, &oid, NULL)) ||
+			  stream_object_signature(r, stream, &oid) < 0))
 			err = error("packed %s from %s is corrupt",
 				    oid_to_hex(&oid), p->pack_name);
 		else if (fn) {
@@ -163,12 +167,14 @@ static int verify_packfile(struct repository *r,
 		}
 		if (((base_count + i) & 1023) == 0)
 			display_progress(progress, base_count + i);
-		free(data);
 
+		if (stream)
+			odb_read_stream_close(stream);
+		free(data);
 	}
+
 	display_progress(progress, base_count + i);
 	free(entries);
-
 	return err;
 }
 
-- 
2.53.0.414.gf7e9f6c205.dirty
Jeff King· Feb 23, 2026, 10:49 UTC · re: Patrick Steinhardt · lore

Re: [PATCH 2/4] object-file: adapt `stream_object_signature()` to take a stream

On Mon, Feb 23, 2026 at 10:50:41AM +0100, Patrick Steinhardt wrote:
Show 5 quoted lines
> The function `stream_object_signature()` is responsible for verifying
> whether the given object ID matches the actual hash of the object's
> contents. In contrast to `check_object_signature()` it does so in a
> streaming fashion so that we don't have to load the full object into
> memory.
Makes sense, and is the first step I expected to see.
The code looks OK, though a few small comments below.
Show 25 quoted lines
> -int stream_object_signature(struct repository *r, const struct object_id *oid)
> +int stream_object_signature(struct repository *r,
> +			    struct odb_read_stream *st,
> +			    const struct object_id *oid)
>  {
>  	struct object_id real_oid;
> -	struct odb_read_stream *st;
>  	struct git_hash_ctx c;
>  	char hdr[MAX_HEADER_LEN];
>  	int hdrlen;
>  
> -	st = odb_read_stream_open(r->objects, oid, NULL);
> -	if (!st)
> -		return -1;
> -
>  	/* Generate the header */
>  	hdrlen = format_object_header(hdr, sizeof(hdr), st->type, st->size);
>  
> @@ -160,7 +157,6 @@ int stream_object_signature(struct repository *r, const struct object_id *oid)
>  		git_hash_update(&c, buf, readlen);
>  	}
>  	git_hash_final_oid(&real_oid, &c);
> -	odb_read_stream_close(st);
>  	return !oideq(oid, &real_oid) ? -1 : 0;
>  }

The minimal change for callers would be to give a wrapper like stream_object_signature_from_oid() or similar, that included the open/close. But it's not too many lines, and of the two callers, one is the caller we are trying to split apart anyway. So inlining these bits in the callers makes sense.

Show 19 quoted lines
> @@ -330,9 +331,17 @@ struct object *parse_object_with_flags(struct repository *r,
>  
>  	if ((!obj || obj->type == OBJ_NONE || obj->type == OBJ_BLOB) &&
>  	    odb_read_object_info(r->objects, oid, NULL) == OBJ_BLOB) {
> -		if (!skip_hash && stream_object_signature(r, repl) < 0) {
> -			error(_("hash mismatch %s"), oid_to_hex(oid));
> -			return NULL;
> +		if (!skip_hash) {
> +			struct odb_read_stream *stream = odb_read_stream_open(r->objects, oid, NULL);
> +			if (!stream || stream_object_signature(r, stream, repl) < 0) {
> +				error(_("hash mismatch %s"), oid_to_hex(oid));
> +				if (stream)
> +					odb_read_stream_close(stream);
> +				return NULL;
> +			}
> +
> +			if (stream)
> +				odb_read_stream_close(stream);
>  		}

This final "if (stream)" is a noop; we'd have exited the function already if "!stream".

In the earlier conditional:
  if (!stream || stream_object_signature(r, stream, repl) < 0)

we're combining two error checks: opening the stream and actually hashing it. That matches the existing code (since it all happened in a single function), but should we take this opportunity to give more accurate error messages? I.e., to do:

  if (!stream) {
	error(_("unable to open object stream for %s"), oid_to_hex(oid));
	return NULL;
  }
  if (stream_object_signature(r, stream, repl) < 0) {
	error(_("hash mismatch %s"), oid_to_hex(oid));
	odb_read_stream_close(stream);
	return NULL;
  }
  odb_read_stream_close(stream);

I dunno. It should be quite uncommon to see either of these messages, but that is sometimes the moment when details are most important.

Also, as an aside, I found it curious that we still need to pass the repository struct to stream_object_signature(). That's because it needs to know the correct hash_algo. I wondered if the stream struct itself might know about that, but it doesn't seem to (it doesn't know anything about where it came from). So it's unavoidable that we'd need to retain it.

Show 29 quoted lines
> @@ -104,6 +105,7 @@ static int verify_packfile(struct repository *r,
>  	QSORT(entries, nr_objects, compare_entries);
>  
>  	for (i = 0; i < nr_objects; i++) {
> +		struct odb_read_stream *stream = NULL;
>  		void *data;
>  		struct object_id oid;
>  		enum object_type type;
> @@ -152,7 +154,9 @@ static int verify_packfile(struct repository *r,
>  							type) < 0)
>  			err = error("packed %s from %s is corrupt",
>  				    oid_to_hex(&oid), p->pack_name);
> -		else if (!data && stream_object_signature(r, &oid) < 0)
> +		else if (!data &&
> +			 (!(stream = odb_read_stream_open(r->objects, &oid, NULL)) ||
> +			  stream_object_signature(r, stream, &oid) < 0))
>  			err = error("packed %s from %s is corrupt",
>  				    oid_to_hex(&oid), p->pack_name);
>  		else if (fn) {
> @@ -163,12 +167,14 @@ static int verify_packfile(struct repository *r,
>  		}
>  		if (((base_count + i) & 1023) == 0)
>  			display_progress(progress, base_count + i);
> -		free(data);
>  
> +		if (stream)
> +			odb_read_stream_close(stream);
> +		free(data);
>  	}

OK, and in this case the final "if" is important, because we just set "err" and continue through the function. This would be much simpler with a wrapper, but of course this is the very caller we're planning to fix.

-Peff
Patrick Steinhardt· Feb 23, 2026, 12:21 UTC · re: Jeff King · lore

Re: [PATCH 2/4] object-file: adapt `stream_object_signature()` to take a stream

On Mon, Feb 23, 2026 at 05:49:15AM -0500, Jeff King wrote:
Show 43 quoted lines
> On Mon, Feb 23, 2026 at 10:50:41AM +0100, Patrick Steinhardt wrote:
> 
> > The function `stream_object_signature()` is responsible for verifying
> > whether the given object ID matches the actual hash of the object's
> > contents. In contrast to `check_object_signature()` it does so in a
> > streaming fashion so that we don't have to load the full object into
> > memory.
> 
> Makes sense, and is the first step I expected to see.
> 
> The code looks OK, though a few small comments below.
> 
> > -int stream_object_signature(struct repository *r, const struct object_id *oid)
> > +int stream_object_signature(struct repository *r,
> > +			    struct odb_read_stream *st,
> > +			    const struct object_id *oid)
> >  {
> >  	struct object_id real_oid;
> > -	struct odb_read_stream *st;
> >  	struct git_hash_ctx c;
> >  	char hdr[MAX_HEADER_LEN];
> >  	int hdrlen;
> >  
> > -	st = odb_read_stream_open(r->objects, oid, NULL);
> > -	if (!st)
> > -		return -1;
> > -
> >  	/* Generate the header */
> >  	hdrlen = format_object_header(hdr, sizeof(hdr), st->type, st->size);
> >  
> > @@ -160,7 +157,6 @@ int stream_object_signature(struct repository *r, const struct object_id *oid)
> >  		git_hash_update(&c, buf, readlen);
> >  	}
> >  	git_hash_final_oid(&real_oid, &c);
> > -	odb_read_stream_close(st);
> >  	return !oideq(oid, &real_oid) ? -1 : 0;
> >  }
> 
> The minimal change for callers would be to give a wrapper like
> stream_object_signature_from_oid() or similar, that included the
> open/close. But it's not too many lines, and of the two callers, one is
> the caller we are trying to split apart anyway. So inlining these bits
> in the callers makes sense.

Yeah. Had there been more callers then I'd have done that, but as you noted there's only two, and one of them will need to be open coded anyway.

Show 29 quoted lines
> > @@ -330,9 +331,17 @@ struct object *parse_object_with_flags(struct repository *r,
> >  
> >  	if ((!obj || obj->type == OBJ_NONE || obj->type == OBJ_BLOB) &&
> >  	    odb_read_object_info(r->objects, oid, NULL) == OBJ_BLOB) {
> > -		if (!skip_hash && stream_object_signature(r, repl) < 0) {
> > -			error(_("hash mismatch %s"), oid_to_hex(oid));
> > -			return NULL;
> > +		if (!skip_hash) {
> > +			struct odb_read_stream *stream = odb_read_stream_open(r->objects, oid, NULL);
> > +			if (!stream || stream_object_signature(r, stream, repl) < 0) {
> > +				error(_("hash mismatch %s"), oid_to_hex(oid));
> > +				if (stream)
> > +					odb_read_stream_close(stream);
> > +				return NULL;
> > +			}
> > +
> > +			if (stream)
> > +				odb_read_stream_close(stream);
> >  		}
> 
> This final "if (stream)" is a noop; we'd have exited the function
> already if "!stream".
> 
> In the earlier conditional:
> 
>   if (!stream || stream_object_signature(r, stream, repl) < 0)
> 
> we're combining two error checks: opening the stream and actually
> hashing it.
Ah, right.
Show 17 quoted lines
> That matches the existing code (since it all happened in a
> single function), but should we take this opportunity to give more
> accurate error messages? I.e., to do:
> 
>   if (!stream) {
> 	error(_("unable to open object stream for %s"), oid_to_hex(oid));
> 	return NULL;
>   }
>   if (stream_object_signature(r, stream, repl) < 0) {
> 	error(_("hash mismatch %s"), oid_to_hex(oid));
> 	odb_read_stream_close(stream);
> 	return NULL;
>   }
>   odb_read_stream_close(stream);
> 
> I dunno. It should be quite uncommon to see either of these messages,
> but that is sometimes the moment when details are most important.

Agreed, that feels like a sensible change indeed. Also makes the code flow easier to follow in my opinion.

Show 6 quoted lines
> Also, as an aside, I found it curious that we still need to pass the
> repository struct to stream_object_signature(). That's because it needs
> to know the correct hash_algo. I wondered if the stream struct itself
> might know about that, but it doesn't seem to (it doesn't know anything
> about where it came from). So it's unavoidable that we'd need to retain
> it.

Yeah, agreed. I wondered whether we should eventually extend `struct odb_read_stream` to have a pointer to the owning object source, and in that case we could've avoided the extra repository parameter. But I decided it was out of scope for this patch series, also because I don't want to cause conflicts with other stuff I'm working on in this vicinity :)

Patrick
Jeff King· Feb 23, 2026, 12:59 UTC · re: Patrick Steinhardt · lore

Re: [PATCH 2/4] object-file: adapt `stream_object_signature()` to take a stream

On Mon, Feb 23, 2026 at 01:21:00PM +0100, Patrick Steinhardt wrote:
Show 20 quoted lines
> > That matches the existing code (since it all happened in a
> > single function), but should we take this opportunity to give more
> > accurate error messages? I.e., to do:
> > 
> >   if (!stream) {
> > 	error(_("unable to open object stream for %s"), oid_to_hex(oid));
> > 	return NULL;
> >   }
> >   if (stream_object_signature(r, stream, repl) < 0) {
> > 	error(_("hash mismatch %s"), oid_to_hex(oid));
> > 	odb_read_stream_close(stream);
> > 	return NULL;
> >   }
> >   odb_read_stream_close(stream);
> > 
> > I dunno. It should be quite uncommon to see either of these messages,
> > but that is sometimes the moment when details are most important.
> 
> Agreed, that feels like a sensible change indeed. Also makes the code
> flow easier to follow in my opinion.

Yeah, the readability was actually what got me thinking on it in the first place.

Show 13 quoted lines
> > Also, as an aside, I found it curious that we still need to pass the
> > repository struct to stream_object_signature(). That's because it needs
> > to know the correct hash_algo. I wondered if the stream struct itself
> > might know about that, but it doesn't seem to (it doesn't know anything
> > about where it came from). So it's unavoidable that we'd need to retain
> > it.
> 
> Yeah, agreed. I wondered whether we should eventually extend `struct
> odb_read_stream` to have a pointer to the owning object source, and in
> that case we could've avoided the extra repository parameter. But I
> decided it was out of scope for this patch series, also because I don't
> want to cause conflicts with other stuff I'm working on in this vicinity
> :)
Yes, definitely out of scope for this series.
-Peff
Patrick Steinhardt· Feb 23, 2026, 09:50 UTC · re: Patrick Steinhardt · lore

[PATCH 3/4] packfile: expose function to read object stream for an offset

The function `packfile_store_read_object_stream()` takes as input an object ID and then constructs a `struct odb_read_stream` from it. In a subsequent commit we'll want to create an object stream for a given combination of packfile and offset though, which is not something that can currently be done.

Extract a new function `packfile_read_object_stream()` that makes this functionality available.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 packfile.c | 36 ++++++++++++++++++++----------------
 packfile.h |  4 ++++
 2 files changed, 24 insertions(+), 16 deletions(-)
Show changes to 2 files +24 −16

packfile.c, packfile.h

diff --git a/packfile.c b/packfile.c
index 402c3b5dc7..9d795a671f 100644
--- a/packfile.c
+++ b/packfile.c
@@ -2553,29 +2553,21 @@ static int close_istream_pack_non_delta(struct odb_read_stream *_st)
 	return 0;
 }
 
-int packfile_store_read_object_stream(struct odb_read_stream **out,
-				      struct packfile_store *store,
-				      const struct object_id *oid)
+int packfile_read_object_stream(struct odb_read_stream **out,
+				struct packed_git *pack,
+				off_t offset)
 {
 	struct odb_packed_read_stream *stream;
 	struct pack_window *window = NULL;
-	struct object_info oi = OBJECT_INFO_INIT;
 	enum object_type in_pack_type;
 	unsigned long size;
 
-	oi.sizep = &size;
+	in_pack_type = unpack_object_header(pack, &window, &offset, &size);
+	unuse_pack(&window);
 
-	if (packfile_store_read_object_info(store, oid, &oi, 0) ||
-	    oi.u.packed.type == PACKED_OBJECT_TYPE_REF_DELTA ||
-	    oi.u.packed.type == PACKED_OBJECT_TYPE_OFS_DELTA ||
-	    repo_settings_get_big_file_threshold(store->source->odb->repo) >= size)
+	if (repo_settings_get_big_file_threshold(pack->repo) >= size)
 		return -1;
 
-	in_pack_type = unpack_object_header(oi.u.packed.pack,
-					    &window,
-					    &oi.u.packed.offset,
-					    &size);
-	unuse_pack(&window);
 	switch (in_pack_type) {
 	default:
 		return -1; /* we do not do deltas for now */
@@ -2592,10 +2584,22 @@ int packfile_store_read_object_stream(struct odb_read_stream **out,
 	stream->base.type = in_pack_type;
 	stream->base.size = size;
 	stream->z_state = ODB_PACKED_READ_STREAM_UNINITIALIZED;
-	stream->pack = oi.u.packed.pack;
-	stream->pos = oi.u.packed.offset;
+	stream->pack = pack;
+	stream->pos = offset;
 
 	*out = &stream->base;
 
 	return 0;
 }
+
+int packfile_store_read_object_stream(struct odb_read_stream **out,
+				      struct packfile_store *store,
+				      const struct object_id *oid)
+{
+	struct pack_entry e;
+
+	if (!find_pack_entry(store, oid, &e))
+		return -1;
+
+	return packfile_read_object_stream(out, e.p, e.offset);
+}
diff --git a/packfile.h b/packfile.h
index acc5c55ad5..67d5750140 100644
--- a/packfile.h
+++ b/packfile.h
@@ -436,6 +436,10 @@ off_t get_delta_base(struct packed_git *p, struct pack_window **w_curs,
 		     off_t *curpos, enum object_type type,
 		     off_t delta_obj_offset);
 
+int packfile_read_object_stream(struct odb_read_stream **out,
+				struct packed_git *pack,
+				off_t offset);
+
 void release_pack_memory(size_t);
 
 /* global flag to enable extra checks when accessing packed objects */
-- 
2.53.0.414.gf7e9f6c205.dirty
Jeff King· Feb 23, 2026, 11:07 UTC · re: Patrick Steinhardt · lore

Re: [PATCH 3/4] packfile: expose function to read object stream for an offset

On Mon, Feb 23, 2026 at 10:50:42AM +0100, Patrick Steinhardt wrote:
Show 8 quoted lines
> The function `packfile_store_read_object_stream()` takes as input an
> object ID and then constructs a `struct odb_read_stream` from it. In a
> subsequent commit we'll want to create an object stream for a given
> combination of packfile and offset though, which is not something that
> can currently be done.
> 
> Extract a new function `packfile_read_object_stream()` that makes this
> functionality available.

Yup, makes sense. There's one part that puzzled me at first (but I figured out), and one part I'm not quite sure of.

Show 29 quoted lines
> -int packfile_store_read_object_stream(struct odb_read_stream **out,
> -				      struct packfile_store *store,
> -				      const struct object_id *oid)
> +int packfile_read_object_stream(struct odb_read_stream **out,
> +				struct packed_git *pack,
> +				off_t offset)
>  {
>  	struct odb_packed_read_stream *stream;
>  	struct pack_window *window = NULL;
> -	struct object_info oi = OBJECT_INFO_INIT;
>  	enum object_type in_pack_type;
>  	unsigned long size;
>  
> -	oi.sizep = &size;
> +	in_pack_type = unpack_object_header(pack, &window, &offset, &size);
> +	unuse_pack(&window);
>  
> -	if (packfile_store_read_object_info(store, oid, &oi, 0) ||
> -	    oi.u.packed.type == PACKED_OBJECT_TYPE_REF_DELTA ||
> -	    oi.u.packed.type == PACKED_OBJECT_TYPE_OFS_DELTA ||
> -	    repo_settings_get_big_file_threshold(store->source->odb->repo) >= size)
> +	if (repo_settings_get_big_file_threshold(pack->repo) >= size)
>  		return -1;
>  
> -	in_pack_type = unpack_object_header(oi.u.packed.pack,
> -					    &window,
> -					    &oi.u.packed.offset,
> -					    &size);
> -	unuse_pack(&window);

Before we were checking big_file_threshold up front, and now we must call unpack_object_header() first. But that's because we got the size for "free" as part of the object_info call that found our pack entry.

Now our caller is responsible for finding the entry. Our wrapper _could_ continue to provide us with the size, but I don't think there is any efficiency to be gained. Once we have the pack/offset pair, both code paths will call unpack_object_header() to find it cheaply. And the new code is even a little more efficient.

But what about the checks for deltas? We've dropped them completely. I think that's OK, though, because later we have:

>  	switch (in_pack_type) {
>  	default:
>  		return -1; /* we do not do deltas for now */

So they were somewhat redundant in the first place, and just avoided calling unpack_object_header() for cases where we knew we could not use the result (which again, was already filled by packed_object_info() in the same way).

Good.
Show 11 quoted lines
> +int packfile_store_read_object_stream(struct odb_read_stream **out,
> +				      struct packfile_store *store,
> +				      const struct object_id *oid)
> +{
> +	struct pack_entry e;
> +
> +	if (!find_pack_entry(store, oid, &e))
> +		return -1;
> +
> +	return packfile_read_object_stream(out, e.p, e.offset);
> +}

OK. The original read via packfile_store_read_object_info(), which does a bit more work. It called packed_object_info() and if necessary would trigger mark_bad_packed_object(). But now that we are leaving it to packfile_read_object_stream() to look at the header, we don't need to load any object info, and we have no error code to check.

It does make me wonder, though, if we are missing out on marking bad objects here. The idea is that we'd usually do something like:

  1. some code wants to access $OID
  2. we find $OID in pack $P
  3. that turns out to be broken for some reason, so we mark it as bad
  4. we try again, skipping $P and finding it in some other pack

But now I wonder if code that tries to stream will skip step 3, and then in step 4 we'll find the same broken $P over and over.

But I suspect if that is possible, it was already true. We were only asking for the type and size, so any content-level corruption wouldn't be caught here and we'd have the same issue. I think the right thing is probably for the streaming code to know about the pack/oid pair it's trying to read, and to mark it as bad if it hits an error.

So your patch here might be making the problem a tiny bit worse, but not in a material way. I think we can ignore it for now.

-Peff
Patrick Steinhardt· Feb 23, 2026, 12:21 UTC · re: Jeff King · lore

Re: [PATCH 3/4] packfile: expose function to read object stream for an offset

On Mon, Feb 23, 2026 at 06:07:22AM -0500, Jeff King wrote:
Show 41 quoted lines
> On Mon, Feb 23, 2026 at 10:50:42AM +0100, Patrick Steinhardt wrote:
> > +int packfile_store_read_object_stream(struct odb_read_stream **out,
> > +				      struct packfile_store *store,
> > +				      const struct object_id *oid)
> > +{
> > +	struct pack_entry e;
> > +
> > +	if (!find_pack_entry(store, oid, &e))
> > +		return -1;
> > +
> > +	return packfile_read_object_stream(out, e.p, e.offset);
> > +}
> 
> OK. The original read via packfile_store_read_object_info(), which does
> a bit more work. It called packed_object_info() and if necessary would
> trigger mark_bad_packed_object(). But now that we are leaving it to
> packfile_read_object_stream() to look at the header, we don't need to
> load any object info, and we have no error code to check.
> 
> It does make me wonder, though, if we are missing out on marking bad
> objects here. The idea is that we'd usually do something like:
> 
>   1. some code wants to access $OID
> 
>   2. we find $OID in pack $P
> 
>   3. that turns out to be broken for some reason, so we mark it as bad
> 
>   4. we try again, skipping $P and finding it in some other pack
> 
> But now I wonder if code that tries to stream will skip step 3, and then
> in step 4 we'll find the same broken $P over and over.
> 
> But I suspect if that is possible, it was already true. We were only
> asking for the type and size, so any content-level corruption wouldn't
> be caught here and we'd have the same issue. I think the right thing is
> probably for the streaming code to know about the pack/oid pair it's
> trying to read, and to mark it as bad if it hits an error.
> 
> So your patch here might be making the problem a tiny bit worse, but not
> in a material way. I think we can ignore it for now.

I guess the "tiny bit worse" part is that we don't handle the case anymore where `unpack_object_header()` returns `OBJ_BAD`. As you say, we previously didn't fully parse the object anyway, so we couldn't have detected all kinds of corruptions. But we definitely handled the case where `unpack_object_header()` failed.

So maybe we should do something like the below patch?
Patrick
Show changes to 2 files +6 −1

packfile.c, packfile.h

diff --git a/packfile.c b/packfile.c
index 9d795a671f..3e61176128 100644
--- a/packfile.c
+++ b/packfile.c
@@ -2554,6 +2554,7 @@ static int close_istream_pack_non_delta(struct odb_read_stream *_st)
 }
 
 int packfile_read_object_stream(struct odb_read_stream **out,
+				const struct object_id *oid,
 				struct packed_git *pack,
 				off_t offset)
 {
@@ -2571,6 +2572,9 @@ int packfile_read_object_stream(struct odb_read_stream **out,
 	switch (in_pack_type) {
 	default:
 		return -1; /* we do not do deltas for now */
+	case OBJ_BAD:
+		mark_bad_packed_object(pack, oid);
+		return -1;
 	case OBJ_COMMIT:
 	case OBJ_TREE:
 	case OBJ_BLOB:
@@ -2601,5 +2605,5 @@ int packfile_store_read_object_stream(struct odb_read_stream **out,
 	if (!find_pack_entry(store, oid, &e))
 		return -1;
 
-	return packfile_read_object_stream(out, e.p, e.offset);
+	return packfile_read_object_stream(out, oid, e.p, e.offset);
 }
diff --git a/packfile.h b/packfile.h
index 67d5750140..b9f5f1c18c 100644
--- a/packfile.h
+++ b/packfile.h
@@ -437,6 +437,7 @@ off_t get_delta_base(struct packed_git *p, struct pack_window **w_curs,
 		     off_t delta_obj_offset);
 
 int packfile_read_object_stream(struct odb_read_stream **out,
+				const struct object_id *oid,
 				struct packed_git *pack,
 				off_t offset);
 
Jeff King· Feb 23, 2026, 13:12 UTC · re: Patrick Steinhardt · lore

Re: [PATCH 3/4] packfile: expose function to read object stream for an offset

On Mon, Feb 23, 2026 at 01:21:06PM +0100, Patrick Steinhardt wrote:
Show 8 quoted lines
> > So your patch here might be making the problem a tiny bit worse, but not
> > in a material way. I think we can ignore it for now.
> 
> I guess the "tiny bit worse" part is that we don't handle the case
> anymore where `unpack_object_header()` returns `OBJ_BAD`. As you say, we
> previously didn't fully parse the object anyway, so we couldn't have
> detected all kinds of corruptions. But we definitely handled the case
> where `unpack_object_header()` failed.

Yeah, I think that would cover it. Technically packed_object_info() could error on more cases (e.g., errors chasing delta bases for type/size info). But we would bail on trying to stream those anyway, so presumably any errors would be found via the non-streaming code paths in those cases.

Show 12 quoted lines
> So maybe we should do something like the below patch?
> [...]
> @@ -2571,6 +2572,9 @@ int packfile_read_object_stream(struct odb_read_stream **out,
>  	switch (in_pack_type) {
>  	default:
>  		return -1; /* we do not do deltas for now */
> +	case OBJ_BAD:
> +		mark_bad_packed_object(pack, oid);
> +		return -1;
>  	case OBJ_COMMIT:
>  	case OBJ_TREE:
>  	case OBJ_BLOB:

I think that restores the original behavior. But I'm not sure it's even worth it. We are still missing the much more likely case of a bit error in the actual zlib stream, which would not be caught until much later.

So yeah, if you want to feel better about making sure your patch keeps the behavior as identical as possible, I don't mind adding this. But it feels like the tip of the iceberg, and I'd be OK leaving it for later (or never).

My biggest objection is not the two lines above (which I actually think clarify what is going on) but rather this interface change:

>  int packfile_read_object_stream(struct odb_read_stream **out,
> +				const struct object_id *oid,
>  				struct packed_git *pack,
>  				off_t offset);

Now we are back to taking an oid, except we don't ever use it to look up the object! So it's a little misleading that it's there at all. It may be the best we can do, though.

The only other way I could think of is for packfile_read_object_stream() to return a more detailed error: one of "success", "chose not to stream", or "broken object". And then the caller can call mark_bad_packed_object() as appropriate. In this case, I think packfile_store_read_object_stream() would do so, but verify_pack() probably would not choose to (it is not interested in fallbacks at all but is going through an individual pack).

-Peff
Patrick Steinhardt· Feb 23, 2026, 15:59 UTC · re: Jeff King · lore

Re: [PATCH 3/4] packfile: expose function to read object stream for an offset

On Mon, Feb 23, 2026 at 08:12:01AM -0500, Jeff King wrote:
Show 50 quoted lines
> On Mon, Feb 23, 2026 at 01:21:06PM +0100, Patrick Steinhardt wrote:
> 
> > > So your patch here might be making the problem a tiny bit worse, but not
> > > in a material way. I think we can ignore it for now.
> > 
> > I guess the "tiny bit worse" part is that we don't handle the case
> > anymore where `unpack_object_header()` returns `OBJ_BAD`. As you say, we
> > previously didn't fully parse the object anyway, so we couldn't have
> > detected all kinds of corruptions. But we definitely handled the case
> > where `unpack_object_header()` failed.
> 
> Yeah, I think that would cover it. Technically packed_object_info()
> could error on more cases (e.g., errors chasing delta bases for
> type/size info). But we would bail on trying to stream those anyway, so
> presumably any errors would be found via the non-streaming code paths in
> those cases.
> 
> > So maybe we should do something like the below patch?
> > [...]
> > @@ -2571,6 +2572,9 @@ int packfile_read_object_stream(struct odb_read_stream **out,
> >  	switch (in_pack_type) {
> >  	default:
> >  		return -1; /* we do not do deltas for now */
> > +	case OBJ_BAD:
> > +		mark_bad_packed_object(pack, oid);
> > +		return -1;
> >  	case OBJ_COMMIT:
> >  	case OBJ_TREE:
> >  	case OBJ_BLOB:
> 
> I think that restores the original behavior. But I'm not sure it's even
> worth it. We are still missing the much more likely case of a bit error
> in the actual zlib stream, which would not be caught until much later.
> 
> So yeah, if you want to feel better about making sure your patch keeps
> the behavior as identical as possible, I don't mind adding this. But it
> feels like the tip of the iceberg, and I'd be OK leaving it for later
> (or never).
> 
> My biggest objection is not the two lines above (which I actually think
> clarify what is going on) but rather this interface change:
> 
> >  int packfile_read_object_stream(struct odb_read_stream **out,
> > +				const struct object_id *oid,
> >  				struct packed_git *pack,
> >  				off_t offset);
> 
> Now we are back to taking an oid, except we don't ever use it to look up
> the object! So it's a little misleading that it's there at all. It may
> be the best we can do, though.

Yeah, I agree it's a bit ugly. But the function is not likely to gain a lot of additional callers anyway, as it is an implementation detail of the packfile store. So overall I think it's okayish.

Show 7 quoted lines
> The only other way I could think of is for packfile_read_object_stream()
> to return a more detailed error: one of "success", "chose not to
> stream", or "broken object". And then the caller can call
> mark_bad_packed_object() as appropriate. In this case, I think
> packfile_store_read_object_stream() would do so, but verify_pack()
> probably would not choose to (it is not interested in fallbacks at all
> but is going through an individual pack).

We could of course have it return the `enum object_type` directly, which gives us enough context to do this. But on the other hand it'd mean that we might now miss adding calls to `mark_bad_packed_object()` in the future, and it causes a bit of repetition across callsites.

So I think I lean towards my proposed patch.
Thanks!
Patrick
Patrick Steinhardt· Feb 23, 2026, 09:50 UTC · re: Patrick Steinhardt · lore

[PATCH 4/4] pack-check: fix verification of large objects

It was reported [1] that git-fsck(1) may sometimes run into an infinite loop when processing packfiles. This bug was bisected to c31bad4f7d (packfile: track packs via the MRU list exclusively, 2025-10-30), which refactored our lsit of packfiles to only be tracked via an MRU list, exclusively. This isn't entirely surprising: any caller that iterates through the list of packfiles and then hits `find_pack_entry()`, for example because they read an object from it, may cause the MRU list to be updated. And if the caller is unlucky, this may cause the mentioned infinite loop.

While this mechanism is somewhat fragile, it is still surprising that we encounter it when verifying the packfile. We iterate through objects in a given pack one by one and then read them via their offset, and doing this shouldn't ever end up in `find_pack_entry()`.

But there is an edge case here: when the object in question is a blob bigger than "core.largeFileThreshold", then we will be careful to not read it into memory. Instead, we read it via an object stream by calling `odb_read_object_stream()`, and that function will perform an object lookup via `odb_read_object_info()`. So in the case where there are at least two blobs in two different packfiles, and both of these blobs exceed "core.largeFileThreshold", then we'll run into an infinite loop because we'll always update the MRU.

We could fix this by improving `repo_for_each_pack()` to not update the MRU, and this would address the issue. But the fun part is that using `odb_read_object_stream()` is the wrong thing to do in the first place: it may open _any_ instance of this object, so we ultimately cannot be sure that we even verified the object in our given packfile.

Fix this bug by creating the object stream for the packed object directly via `packfile_read_object_stream()`. Add a test that would have caused the infinite loop.

[1]: <20260222183710.2963424-1-sandals@crustytoothpaste.net>
Reported-by: brian m. carlson <sandals@crustytoothpaste.net>
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 pack-check.c    |  2 +-
 t/t1450-fsck.sh | 15 +++++++++++++++
 2 files changed, 16 insertions(+), 1 deletion(-)
Show changes to 2 files +16 −1

pack-check.c, t/t1450-fsck.sh

diff --git a/pack-check.c b/pack-check.c
index 46782a29d5..6149567060 100644
--- a/pack-check.c
+++ b/pack-check.c
@@ -155,7 +155,7 @@ static int verify_packfile(struct repository *r,
 			err = error("packed %s from %s is corrupt",
 				    oid_to_hex(&oid), p->pack_name);
 		else if (!data &&
-			 (!(stream = odb_read_stream_open(r->objects, &oid, NULL)) ||
+			 (packfile_read_object_stream(&stream, p, entries[i].offset) < 0 ||
 			  stream_object_signature(r, stream, &oid) < 0))
 			err = error("packed %s from %s is corrupt",
 				    oid_to_hex(&oid), p->pack_name);
diff --git a/t/t1450-fsck.sh b/t/t1450-fsck.sh
index 8fb79b3e5d..ec68397ea3 100755
--- a/t/t1450-fsck.sh
+++ b/t/t1450-fsck.sh
@@ -852,6 +852,21 @@ test_expect_success 'fsck errors in packed objects' '
 	! grep corrupt out
 '
 
+test_expect_success 'fsck handles multiple packfiles with big blobs' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		blob_one=$(test-tool genrandom one 200k | git hash-object -t blob -w --stdin) &&
+		blob_two=$(test-tool genrandom two 200k | git hash-object -t blob -w --stdin) &&
+		printf "%s\n" "$blob_one" | git pack-objects .git/objects/pack/pack &&
+		printf "%s\n" "$blob_two" | git pack-objects .git/objects/pack/pack &&
+		remove_object "$blob_one" &&
+		remove_object "$blob_two" &&
+		git -c core.bigFileThreshold=100k fsck
+	)
+'
+
 test_expect_success 'fsck fails on corrupt packfile' '
 	hsh=$(git commit-tree -m mycommit HEAD^{tree}) &&
 	pack=$(echo $hsh | git pack-objects .git/objects/pack/pack) &&
-- 
2.53.0.414.gf7e9f6c205.dirty
Jeff King· Feb 23, 2026, 11:11 UTC · re: Patrick Steinhardt · lore

Re: [PATCH 4/4] pack-check: fix verification of large objects

On Mon, Feb 23, 2026 at 10:50:43AM +0100, Patrick Steinhardt wrote:
Show 10 quoted lines
> diff --git a/pack-check.c b/pack-check.c
> index 46782a29d5..6149567060 100644
> --- a/pack-check.c
> +++ b/pack-check.c
> @@ -155,7 +155,7 @@ static int verify_packfile(struct repository *r,
>  			err = error("packed %s from %s is corrupt",
>  				    oid_to_hex(&oid), p->pack_name);
>  		else if (!data &&
> -			 (!(stream = odb_read_stream_open(r->objects, &oid, NULL)) ||
> +			 (packfile_read_object_stream(&stream, p, entries[i].offset) < 0 ||
And now this change is delightfully simple.
Show 14 quoted lines
> +test_expect_success 'fsck handles multiple packfiles with big blobs' '
> +	test_when_finished "rm -rf repo" &&
> +	git init repo &&
> +	(
> +		cd repo &&
> +		blob_one=$(test-tool genrandom one 200k | git hash-object -t blob -w --stdin) &&
> +		blob_two=$(test-tool genrandom two 200k | git hash-object -t blob -w --stdin) &&
> +		printf "%s\n" "$blob_one" | git pack-objects .git/objects/pack/pack &&
> +		printf "%s\n" "$blob_two" | git pack-objects .git/objects/pack/pack &&
> +		remove_object "$blob_one" &&
> +		remove_object "$blob_two" &&
> +		git -c core.bigFileThreshold=100k fsck
> +	)
> +'

I like seeing this much-more-specific test case. It does sort of become a noop if we fix the iteration problem, though.

A more concrete test would probably be something like:
   1. Two packs, $X and $Y, both contain the same object.
   2. The object is corrupt in $X but not in $Y.
   3. Running fsck detects that one copy is corrupt but the other is
      not.

Right now it may or may not fail depending on the ordering of the packs in the MRU list (which we might be able to tweak via mtimes). But hopefully in the "after" state it should deterministically complain about $X.

-Peff
Patrick Steinhardt· Feb 23, 2026, 11:30 UTC · re: Jeff King · lore

Re: [PATCH 4/4] pack-check: fix verification of large objects

On Mon, Feb 23, 2026 at 06:11:20AM -0500, Jeff King wrote:
Show 46 quoted lines
> On Mon, Feb 23, 2026 at 10:50:43AM +0100, Patrick Steinhardt wrote:
> 
> > diff --git a/pack-check.c b/pack-check.c
> > index 46782a29d5..6149567060 100644
> > --- a/pack-check.c
> > +++ b/pack-check.c
> > @@ -155,7 +155,7 @@ static int verify_packfile(struct repository *r,
> >  			err = error("packed %s from %s is corrupt",
> >  				    oid_to_hex(&oid), p->pack_name);
> >  		else if (!data &&
> > -			 (!(stream = odb_read_stream_open(r->objects, &oid, NULL)) ||
> > +			 (packfile_read_object_stream(&stream, p, entries[i].offset) < 0 ||
> 
> And now this change is delightfully simple.
> 
> > +test_expect_success 'fsck handles multiple packfiles with big blobs' '
> > +	test_when_finished "rm -rf repo" &&
> > +	git init repo &&
> > +	(
> > +		cd repo &&
> > +		blob_one=$(test-tool genrandom one 200k | git hash-object -t blob -w --stdin) &&
> > +		blob_two=$(test-tool genrandom two 200k | git hash-object -t blob -w --stdin) &&
> > +		printf "%s\n" "$blob_one" | git pack-objects .git/objects/pack/pack &&
> > +		printf "%s\n" "$blob_two" | git pack-objects .git/objects/pack/pack &&
> > +		remove_object "$blob_one" &&
> > +		remove_object "$blob_two" &&
> > +		git -c core.bigFileThreshold=100k fsck
> > +	)
> > +'
> 
> I like seeing this much-more-specific test case. It does sort of become
> a noop if we fix the iteration problem, though.
> 
> A more concrete test would probably be something like:
> 
>    1. Two packs, $X and $Y, both contain the same object.
> 
>    2. The object is corrupt in $X but not in $Y.
> 
>    3. Running fsck detects that one copy is corrupt but the other is
>       not.
> 
> Right now it may or may not fail depending on the ordering of the packs
> in the MRU list (which we might be able to tweak via mtimes). But
> hopefully in the "after" state it should deterministically complain
> about $X.

Yeah. The problem I had here is that I'm not sure whether we have any tools to reliably create a corrupted object, e.g. with a hash mismatch. I'll have a look for v2.

Thanks!
Patrick
Jeff King· Feb 23, 2026, 12:58 UTC · re: Patrick Steinhardt · lore

Re: [PATCH 4/4] pack-check: fix verification of large objects

On Mon, Feb 23, 2026 at 12:30:35PM +0100, Patrick Steinhardt wrote:
Show 17 quoted lines
> > A more concrete test would probably be something like:
> > 
> >    1. Two packs, $X and $Y, both contain the same object.
> > 
> >    2. The object is corrupt in $X but not in $Y.
> > 
> >    3. Running fsck detects that one copy is corrupt but the other is
> >       not.
> > 
> > Right now it may or may not fail depending on the ordering of the packs
> > in the MRU list (which we might be able to tweak via mtimes). But
> > hopefully in the "after" state it should deterministically complain
> > about $X.
> 
> Yeah. The problem I had here is that I'm not sure whether we have any
> tools to reliably create a corrupted object, e.g. with a hash mismatch.
> I'll have a look for v2.

You can see how do_corrupt_object() in t5303 does it. It's basically finding an offset via show-index and then writing a zero over it with dd.

-Peff
Patrick Steinhardt· Feb 23, 2026, 15:48 UTC · re: Jeff King · lore

Re: [PATCH 4/4] pack-check: fix verification of large objects

On Mon, Feb 23, 2026 at 07:58:43AM -0500, Jeff King wrote:
Show 23 quoted lines
> On Mon, Feb 23, 2026 at 12:30:35PM +0100, Patrick Steinhardt wrote:
> 
> > > A more concrete test would probably be something like:
> > > 
> > >    1. Two packs, $X and $Y, both contain the same object.
> > > 
> > >    2. The object is corrupt in $X but not in $Y.
> > > 
> > >    3. Running fsck detects that one copy is corrupt but the other is
> > >       not.
> > > 
> > > Right now it may or may not fail depending on the ordering of the packs
> > > in the MRU list (which we might be able to tweak via mtimes). But
> > > hopefully in the "after" state it should deterministically complain
> > > about $X.
> > 
> > Yeah. The problem I had here is that I'm not sure whether we have any
> > tools to reliably create a corrupted object, e.g. with a hash mismatch.
> > I'll have a look for v2.
> 
> You can see how do_corrupt_object() in t5303 does it. It's basically
> finding an offset via show-index and then writing a zero over it with
> dd.
Yeah, that's what I ended up doing indeed. I spotted such a test in t1450.
Patrick
Junio C Hamano· Feb 23, 2026, 20:35 UTC · re: Patrick Steinhardt · lore

Re: [PATCH 4/4] pack-check: fix verification of large objects

Patrick Steinhardt <ps@pks.im> writes:
> It was reported [1] that git-fsck(1) may sometimes run into an infinite
> loop when processing packfiles. This bug was bisected to c31bad4f7d
> (packfile: track packs via the MRU list exclusively, 2025-10-30), which
> refactored our lsit of packfiles to only be tracked via an MRU list,
"lsit of" -> "list of"
Show 19 quoted lines
> exclusively. This isn't entirely surprising: any caller that iterates
> through the list of packfiles and then hits `find_pack_entry()`, for
> example because they read an object from it, may cause the MRU list to
> be updated. And if the caller is unlucky, this may cause the mentioned
> infinite loop.
>
> While this mechanism is somewhat fragile, it is still surprising that we
> encounter it when verifying the packfile. We iterate through objects in
> a given pack one by one and then read them via their offset, and doing
> this shouldn't ever end up in `find_pack_entry()`.
>
> But there is an edge case here: when the object in question is a blob
> bigger than "core.largeFileThreshold", then we will be careful to not
> read it into memory. Instead, we read it via an object stream by calling
> `odb_read_object_stream()`, and that function will perform an object
> lookup via `odb_read_object_info()`. So in the case where there are at
> least two blobs in two different packfiles, and both of these blobs
> exceed "core.largeFileThreshold", then we'll run into an infinite loop
> because we'll always update the MRU.

Good find, and it is not surprising. What is surprising is that we do not see this kind of breakage more often. The mechanism does sound fragile, not just "somewhat" X-<.

Show 5 quoted lines
> We could fix this by improving `repo_for_each_pack()` to not update the
> MRU, and this would address the issue. But the fun part is that using
> `odb_read_object_stream()` is the wrong thing to do in the first place:
> it may open _any_ instance of this object, so we ultimately cannot be
> sure that we even verified the object in our given packfile.
Again, very good reasoning.
> Fix this bug by creating the object stream for the packed object
> directly via `packfile_read_object_stream()`. Add a test that would have
> caused the infinite loop.

Curious that we have a completely different test. I've locally applied (without committing or amending) t1050 update from brian's patch and with this series, fsck there does not seem to get stuck. Of course, the new test added here doesn't either ;-).

Show 49 quoted lines
>
> [1]: <20260222183710.2963424-1-sandals@crustytoothpaste.net>
>
> Reported-by: brian m. carlson <sandals@crustytoothpaste.net>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
>  pack-check.c    |  2 +-
>  t/t1450-fsck.sh | 15 +++++++++++++++
>  2 files changed, 16 insertions(+), 1 deletion(-)
>
> diff --git a/pack-check.c b/pack-check.c
> index 46782a29d5..6149567060 100644
> --- a/pack-check.c
> +++ b/pack-check.c
> @@ -155,7 +155,7 @@ static int verify_packfile(struct repository *r,
>  			err = error("packed %s from %s is corrupt",
>  				    oid_to_hex(&oid), p->pack_name);
>  		else if (!data &&
> -			 (!(stream = odb_read_stream_open(r->objects, &oid, NULL)) ||
> +			 (packfile_read_object_stream(&stream, p, entries[i].offset) < 0 ||
>  			  stream_object_signature(r, stream, &oid) < 0))
>  			err = error("packed %s from %s is corrupt",
>  				    oid_to_hex(&oid), p->pack_name);
> diff --git a/t/t1450-fsck.sh b/t/t1450-fsck.sh
> index 8fb79b3e5d..ec68397ea3 100755
> --- a/t/t1450-fsck.sh
> +++ b/t/t1450-fsck.sh
> @@ -852,6 +852,21 @@ test_expect_success 'fsck errors in packed objects' '
>  	! grep corrupt out
>  '
>  
> +test_expect_success 'fsck handles multiple packfiles with big blobs' '
> +	test_when_finished "rm -rf repo" &&
> +	git init repo &&
> +	(
> +		cd repo &&
> +		blob_one=$(test-tool genrandom one 200k | git hash-object -t blob -w --stdin) &&
> +		blob_two=$(test-tool genrandom two 200k | git hash-object -t blob -w --stdin) &&
> +		printf "%s\n" "$blob_one" | git pack-objects .git/objects/pack/pack &&
> +		printf "%s\n" "$blob_two" | git pack-objects .git/objects/pack/pack &&
> +		remove_object "$blob_one" &&
> +		remove_object "$blob_two" &&
> +		git -c core.bigFileThreshold=100k fsck
> +	)
> +'
> +
>  test_expect_success 'fsck fails on corrupt packfile' '
>  	hsh=$(git commit-tree -m mycommit HEAD^{tree}) &&
>  	pack=$(echo $hsh | git pack-objects .git/objects/pack/pack) &&
Patrick Steinhardt· Feb 24, 2026, 06:26 UTC · re: Junio C Hamano · lore

Re: [PATCH 4/4] pack-check: fix verification of large objects

On Mon, Feb 23, 2026 at 12:35:48PM -0800, Junio C Hamano wrote:
Show 9 quoted lines
> Patrick Steinhardt <ps@pks.im> writes:
> > Fix this bug by creating the object stream for the packed object
> > directly via `packfile_read_object_stream()`. Add a test that would have
> > caused the infinite loop.
> 
> Curious that we have a completely different test.  I've locally
> applied (without committing or amending) t1050 update from brian's
> patch and with this series, fsck there does not seem to get stuck.
> Of course, the new test added here doesn't either ;-).

Yeah, the fact that the test in t1050 hit the bug was pure coincidence, and I wanted to have something a bit more reliable that also allowed myself to prove what exact circumstances cause this to fail.

Patrick
Patrick Steinhardt· Feb 23, 2026, 16:00 UTC · re: Patrick Steinhardt · lore

[PATCH v2 0/4] pack-check: fix verification of large objects

Hi,

this small patch series addresses the bug reported by brian in [1]. Thanks!

Changes in v2:
  - Extend the test to verify that we actually find corrupted objects in
    both packs, even in the case where a non-corrupt version exists in
    another pack.
  - Reinstate `mark_packed_object_bad()`.
  - Fix error checking for `git_parse_ulong()`.
  - Disambiguate error conditions in `parse_object_with_flags()`.
  - Link to v1: https://lore.kernel.org/r/20260223-pks-fsck-fix-v1-0-c29036832b6e@pks.im
Patrick
[1]: <20260222183710.2963424-1-sandals@crustytoothpaste.net>
---
Patrick Steinhardt (4):
      t/helper: improve "genrandom" test helper
      object-file: adapt `stream_object_signature()` to take a stream
      packfile: expose function to read object stream for an offset
      pack-check: fix verification of large objects
 object-file.c                         | 10 +++------
 object-file.h                         |  4 +++-
 object.c                              | 19 ++++++++++++++---
 pack-check.c                          | 12 ++++++++---
 packfile.c                            | 40 +++++++++++++++++++++--------------
 packfile.h                            |  5 +++++
 t/helper/test-genrandom.c             |  5 ++++-
 t/t1006-cat-file.sh                   |  2 +-
 t/t1050-large.sh                      |  6 +++---
 t/t1450-fsck.sh                       | 40 ++++++++++++++++++++++++++++++++++-
 t/t5301-sliding-window.sh             |  2 +-
 t/t5310-pack-bitmaps.sh               |  2 +-
 t/t5710-promisor-remote-capability.sh |  4 ++--
 t/t7700-repack.sh                     |  6 +++---
 14 files changed, 114 insertions(+), 43 deletions(-)
Range-diff versus v1:
1:  1b1283e837 ! 1:  daf895aef6 t/helper: improve "genrandom" test helper
    @@ Commit message
             have to precompute them.
     
         Fix both of these issues by using `git_parse_ulong()` to parse the
    -    argumemnt. This function has better error handling, and it knows to
    +    argument. This function has better error handling, and it knows to
         handle unit suffixes.
     
         Adapt a couple of our tests to use suffixes instead of manual
    @@ t/helper/test-genrandom.c: int cmd__genrandom(int argc, const char **argv)
      
     -	count = (argc == 3) ? strtoul(argv[2], NULL, 0) : ULONG_MAX;
     +	count = ULONG_MAX;
    -+	if (argc == 3 && git_parse_ulong(argv[2], &count) < 0)
    ++	if (argc == 3 && !git_parse_ulong(argv[2], &count))
     +		return error_errno("cannot parse argument '%s'", argv[2]);
      
      	while (count--) {
2:  9f25ed1a4b ! 2:  ebca9efaec object-file: adapt `stream_object_signature()` to take a stream
    @@ Commit message
         a preconstructed stream. Prepare for this by accepting a stream as input
         that the caller needs to assemble.
     
    +    While at it, improve the error reporting in `parse_object_with_flags()`
    +    to tell apart the two failure modes.
    +
    +    Helped-by: Jeff King <peff@peff.net>
         Signed-off-by: Patrick Steinhardt <ps@pks.im>
     
      ## object-file.c ##
    @@ object.c: struct object *parse_object_with_flags(struct repository *r,
     -			return NULL;
     +		if (!skip_hash) {
     +			struct odb_read_stream *stream = odb_read_stream_open(r->objects, oid, NULL);
    -+			if (!stream || stream_object_signature(r, stream, repl) < 0) {
    -+				error(_("hash mismatch %s"), oid_to_hex(oid));
    -+				if (stream)
    -+					odb_read_stream_close(stream);
    ++
    ++			if (!stream) {
    ++				error(_("unable to open object stream for %s"), oid_to_hex(oid));
     +				return NULL;
     +			}
     +
    -+			if (stream)
    ++			if (stream_object_signature(r, stream, repl) < 0) {
    ++				error(_("hash mismatch %s"), oid_to_hex(oid));
     +				odb_read_stream_close(stream);
    ++				return NULL;
    ++			}
    ++
    ++			odb_read_stream_close(stream);
      		}
      		parse_blob_buffer(lookup_blob(r, oid));
      		return lookup_object(r, oid);
3:  9a28867564 ! 3:  bead96797e packfile: expose function to read object stream for an offset
    @@ packfile.c: static int close_istream_pack_non_delta(struct odb_read_stream *_st)
     -				      struct packfile_store *store,
     -				      const struct object_id *oid)
     +int packfile_read_object_stream(struct odb_read_stream **out,
    ++				const struct object_id *oid,
     +				struct packed_git *pack,
     +				off_t offset)
      {
    @@ packfile.c: static int close_istream_pack_non_delta(struct odb_read_stream *_st)
      	switch (in_pack_type) {
      	default:
      		return -1; /* we do not do deltas for now */
    ++	case OBJ_BAD:
    ++		mark_bad_packed_object(pack, oid);
    ++		return -1;
    + 	case OBJ_COMMIT:
    + 	case OBJ_TREE:
    + 	case OBJ_BLOB:
     @@ packfile.c: int packfile_store_read_object_stream(struct odb_read_stream **out,
      	stream->base.type = in_pack_type;
      	stream->base.size = size;
    @@ packfile.c: int packfile_store_read_object_stream(struct odb_read_stream **out,
     +	if (!find_pack_entry(store, oid, &e))
     +		return -1;
     +
    -+	return packfile_read_object_stream(out, e.p, e.offset);
    ++	return packfile_read_object_stream(out, oid, e.p, e.offset);
     +}
     
      ## packfile.h ##
    @@ packfile.h: off_t get_delta_base(struct packed_git *p, struct pack_window **w_cu
      		     off_t delta_obj_offset);
      
     +int packfile_read_object_stream(struct odb_read_stream **out,
    ++				const struct object_id *oid,
     +				struct packed_git *pack,
     +				off_t offset);
     +
4:  4eaf958e57 ! 4:  6b69624d81 pack-check: fix verification of large objects
    @@ pack-check.c: static int verify_packfile(struct repository *r,
      				    oid_to_hex(&oid), p->pack_name);
      		else if (!data &&
     -			 (!(stream = odb_read_stream_open(r->objects, &oid, NULL)) ||
    -+			 (packfile_read_object_stream(&stream, p, entries[i].offset) < 0 ||
    ++			 (packfile_read_object_stream(&stream, &oid, p, entries[i].offset) < 0 ||
      			  stream_object_signature(r, stream, &oid) < 0))
      			err = error("packed %s from %s is corrupt",
      				    oid_to_hex(&oid), p->pack_name);
    @@ t/t1450-fsck.sh: test_expect_success 'fsck errors in packed objects' '
     +	git init repo &&
     +	(
     +		cd repo &&
    ++
    ++		# We construct two packfiles with two objects in common and one
    ++		# object not in common. The objects in common can then be
    ++		# corrupted in one of the packfiles, respectively. The other
    ++		# objects that are unique to the packs are merely used to not
    ++		# have both packs contain the same data.
     +		blob_one=$(test-tool genrandom one 200k | git hash-object -t blob -w --stdin) &&
     +		blob_two=$(test-tool genrandom two 200k | git hash-object -t blob -w --stdin) &&
    -+		printf "%s\n" "$blob_one" | git pack-objects .git/objects/pack/pack &&
    -+		printf "%s\n" "$blob_two" | git pack-objects .git/objects/pack/pack &&
    -+		remove_object "$blob_one" &&
    -+		remove_object "$blob_two" &&
    -+		git -c core.bigFileThreshold=100k fsck
    ++		blob_three=$(test-tool genrandom three 200k | git hash-object -t blob -w --stdin) &&
    ++		blob_four=$(test-tool genrandom four 200k | git hash-object -t blob -w --stdin) &&
    ++		pack_one=$(printf "%s\n" "$blob_one" "$blob_two" "$blob_three" | git pack-objects .git/objects/pack/pack) &&
    ++		pack_two=$(printf "%s\n" "$blob_two" "$blob_three" "$blob_four" | git pack-objects .git/objects/pack/pack) &&
    ++		chmod a+w .git/objects/pack/pack-*.pack &&
    ++
    ++		# Corrupt blob two in the first pack.
    ++		git verify-pack -v .git/objects/pack/pack-$pack_one >objects &&
    ++		offset_one=$(sed <objects -n "s/^$blob_two .* \(.*\)$/\1/p") &&
    ++		printf "\0" | dd of=.git/objects/pack/pack-$pack_one.pack bs=1 conv=notrunc seek=$offset_one &&
    ++
    ++		# Corrupt blob three in the second pack.
    ++		git verify-pack -v .git/objects/pack/pack-$pack_two >objects &&
    ++		offset_two=$(sed <objects -n "s/^$blob_three .* \(.*\)$/\1/p") &&
    ++		printf "\0" | dd of=.git/objects/pack/pack-$pack_two.pack bs=1 conv=notrunc seek=$offset_two &&
    ++
    ++		# We now expect to see two failures for the corrupted objects,
    ++		# even though they exist in a non-corrupted form in the
    ++		# respective other pack.
    ++		test_must_fail git -c core.bigFileThreshold=100k fsck 2>err &&
    ++		test_grep "unknown object type 0 at offset $offset_one in .git/objects/pack/pack-$pack_one.pack" err &&
    ++		test_grep "unknown object type 0 at offset $offset_two in .git/objects/pack/pack-$pack_two.pack" err
     +	)
     +'
     +

--- base-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4 change-id: 20260223-pks-fsck-fix-aa8a18a223c8

Patrick Steinhardt· Feb 23, 2026, 16:00 UTC · re: Patrick Steinhardt · lore

[PATCH v2 1/4] t/helper: improve "genrandom" test helper

The `test-tool genrandom` test helper can be used to generate random data, either as an infinite stream or with a specified number of bytes. The way we handle parsing the number of bytes is lacking though:

  - We don't have good error handling, so if the caller for example uses
    `test-tool genrandom 200xyz` then we'll end up generating 200 bytes
    of random data successfully.
  - Many callers want to generate e.g. 1 kilobyte or megabyte of data,
    but they have to either use unwieldy numbers like 1048576, or they
    have to precompute them.

Fix both of these issues by using `git_parse_ulong()` to parse the argument. This function has better error handling, and it knows to handle unit suffixes.

Adapt a couple of our tests to use suffixes instead of manual computations.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 t/helper/test-genrandom.c             | 5 ++++-
 t/t1006-cat-file.sh                   | 2 +-
 t/t1050-large.sh                      | 6 +++---
 t/t1450-fsck.sh                       | 2 +-
 t/t5301-sliding-window.sh             | 2 +-
 t/t5310-pack-bitmaps.sh               | 2 +-
 t/t5710-promisor-remote-capability.sh | 4 ++--
 t/t7700-repack.sh                     | 6 +++---
 8 files changed, 16 insertions(+), 13 deletions(-)
Show changes to 8 files +16 −13

t/helper/test-genrandom.c, t/t1006-cat-file.sh, t/t1050-large.sh, t/t1450-fsck.sh, t/t5301-sliding-window.sh, t/t5310-pack-bitmaps.sh, t/t5710-promisor-remote-capability.sh, t/t7700-repack.sh

diff --git a/t/helper/test-genrandom.c b/t/helper/test-genrandom.c
index 51b67f2f87..d681961abb 100644
--- a/t/helper/test-genrandom.c
+++ b/t/helper/test-genrandom.c
@@ -6,6 +6,7 @@
 
 #include "test-tool.h"
 #include "git-compat-util.h"
+#include "parse.h"
 
 int cmd__genrandom(int argc, const char **argv)
 {
@@ -22,7 +23,9 @@ int cmd__genrandom(int argc, const char **argv)
 		next = next * 11 + *c;
 	} while (*c++);
 
-	count = (argc == 3) ? strtoul(argv[2], NULL, 0) : ULONG_MAX;
+	count = ULONG_MAX;
+	if (argc == 3 && !git_parse_ulong(argv[2], &count))
+		return error_errno("cannot parse argument '%s'", argv[2]);
 
 	while (count--) {
 		next = next * 1103515245 + 12345;
diff --git a/t/t1006-cat-file.sh b/t/t1006-cat-file.sh
index 0eee3bb878..5499be8dc9 100755
--- a/t/t1006-cat-file.sh
+++ b/t/t1006-cat-file.sh
@@ -643,7 +643,7 @@ test_expect_success 'object reference via commit text search' '
 '
 
 test_expect_success 'setup blobs which are likely to delta' '
-	test-tool genrandom foo 10240 >foo &&
+	test-tool genrandom foo 10k >foo &&
 	{ cat foo && echo plus; } >foo-plus &&
 	git add foo foo-plus &&
 	git commit -m foo &&
diff --git a/t/t1050-large.sh b/t/t1050-large.sh
index 5be273611a..7d40d08521 100755
--- a/t/t1050-large.sh
+++ b/t/t1050-large.sh
@@ -104,9 +104,9 @@ test_expect_success 'packsize limit' '
 		# mid1 and mid2 will fit within 256k limit but
 		# appending mid3 will bust the limit and will
 		# result in a separate packfile.
-		test-tool genrandom "a" $(( 66 * 1024 )) >mid1 &&
-		test-tool genrandom "b" $(( 80 * 1024 )) >mid2 &&
-		test-tool genrandom "c" $(( 128 * 1024 )) >mid3 &&
+		test-tool genrandom "a" 66k >mid1 &&
+		test-tool genrandom "b" 80k >mid2 &&
+		test-tool genrandom "c" 128k >mid3 &&
 		git add mid1 mid2 mid3 &&
 
 		count=0 &&
diff --git a/t/t1450-fsck.sh b/t/t1450-fsck.sh
index 3fae05f9d9..8fb79b3e5d 100755
--- a/t/t1450-fsck.sh
+++ b/t/t1450-fsck.sh
@@ -918,7 +918,7 @@ test_expect_success 'fsck detects trailing loose garbage (large blob)' '
 test_expect_success 'fsck detects truncated loose object' '
 	# make it big enough that we know we will truncate in the data
 	# portion, not the header
-	test-tool genrandom truncate 4096 >file &&
+	test-tool genrandom truncate 4k >file &&
 	blob=$(git hash-object -w file) &&
 	file=$(sha1_file $blob) &&
 	test_when_finished "remove_object $blob" &&
diff --git a/t/t5301-sliding-window.sh b/t/t5301-sliding-window.sh
index ff6b5159a3..3c3666b278 100755
--- a/t/t5301-sliding-window.sh
+++ b/t/t5301-sliding-window.sh
@@ -12,7 +12,7 @@ test_expect_success 'setup' '
 	for i in a b c
 	do
 	echo $i >$i &&
-	test-tool genrandom "$i" 32768 >>$i &&
+	test-tool genrandom "$i" 32k >>$i &&
 	git update-index --add $i || return 1
 	done &&
 	echo d >d && cat c >>d && git update-index --add d &&
diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh
index 6718fb98c0..3e3366f57d 100755
--- a/t/t5310-pack-bitmaps.sh
+++ b/t/t5310-pack-bitmaps.sh
@@ -242,7 +242,7 @@ test_bitmap_cases () {
 	'
 
 	test_expect_success 'splitting packs does not generate bogus bitmaps' '
-		test-tool genrandom foo $((1024 * 1024)) >rand &&
+		test-tool genrandom foo 1m >rand &&
 		git add rand &&
 		git commit -m "commit with big file" &&
 		git -c pack.packSizeLimit=500k repack -adb &&
diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh
index 023735d6a8..66af84cd56 100755
--- a/t/t5710-promisor-remote-capability.sh
+++ b/t/t5710-promisor-remote-capability.sh
@@ -20,7 +20,7 @@ test_expect_success 'setup: create "template" repository' '
 	test_commit -C template 1 &&
 	test_commit -C template 2 &&
 	test_commit -C template 3 &&
-	test-tool genrandom foo 10240 >template/foo &&
+	test-tool genrandom foo 10k >template/foo &&
 	git -C template add foo &&
 	git -C template commit -m foo
 '
@@ -376,7 +376,7 @@ test_expect_success "clone with promisor.advertise set to 'true' but don't delet
 
 test_expect_success "setup for subsequent fetches" '
 	# Generate new commit with large blob
-	test-tool genrandom bar 10240 >template/bar &&
+	test-tool genrandom bar 10k >template/bar &&
 	git -C template add bar &&
 	git -C template commit -m bar &&
 
diff --git a/t/t7700-repack.sh b/t/t7700-repack.sh
index 73b78bdd88..439ab24d23 100755
--- a/t/t7700-repack.sh
+++ b/t/t7700-repack.sh
@@ -319,7 +319,7 @@ test_expect_success 'no bitmaps created if .keep files present' '
 
 test_expect_success 'auto-bitmaps do not complain if unavailable' '
 	test_config -C bare.git pack.packSizeLimit 1M &&
-	blob=$(test-tool genrandom big $((1024*1024)) |
+	blob=$(test-tool genrandom big 1m |
 	       git -C bare.git hash-object -w --stdin) &&
 	git -C bare.git update-ref refs/tags/big $blob &&
 
@@ -495,9 +495,9 @@ test_expect_success '--filter works with --max-pack-size' '
 		cd max-pack-size &&
 		test_commit base &&
 		# two blobs which exceed the maximum pack size
-		test-tool genrandom foo 1048576 >foo &&
+		test-tool genrandom foo 1m >foo &&
 		git hash-object -w foo &&
-		test-tool genrandom bar 1048576 >bar &&
+		test-tool genrandom bar 1m >bar &&
 		git hash-object -w bar &&
 		git add foo bar &&
 		git commit -m "adding foo and bar"
-- 
2.53.0.536.g309c995771.dirty
Patrick Steinhardt· Feb 23, 2026, 16:00 UTC · re: Patrick Steinhardt · lore

[PATCH v2 2/4] object-file: adapt `stream_object_signature()` to take a stream

The function `stream_object_signature()` is responsible for verifying whether the given object ID matches the actual hash of the object's contents. In contrast to `check_object_signature()` it does so in a streaming fashion so that we don't have to load the full object into memory.

In a subsequent commit we'll want to adapt one of its callsites to pass a preconstructed stream. Prepare for this by accepting a stream as input that the caller needs to assemble.

While at it, improve the error reporting in `parse_object_with_flags()` to tell apart the two failure modes.

Helped-by: Jeff King <peff@peff.net>
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 object-file.c | 10 +++-------
 object-file.h |  4 +++-
 object.c      | 19 ++++++++++++++++---
 pack-check.c  | 12 +++++++++---
 4 files changed, 31 insertions(+), 14 deletions(-)
Show changes to 4 files +31 −14

object-file.c, object-file.h, object.c, pack-check.c

diff --git a/object-file.c b/object-file.c
index 1b62996ef0..ca2c4dddf3 100644
--- a/object-file.c
+++ b/object-file.c
@@ -129,18 +129,15 @@ int check_object_signature(struct repository *r, const struct object_id *oid,
 	return !oideq(oid, &real_oid) ? -1 : 0;
 }
 
-int stream_object_signature(struct repository *r, const struct object_id *oid)
+int stream_object_signature(struct repository *r,
+			    struct odb_read_stream *st,
+			    const struct object_id *oid)
 {
 	struct object_id real_oid;
-	struct odb_read_stream *st;
 	struct git_hash_ctx c;
 	char hdr[MAX_HEADER_LEN];
 	int hdrlen;
 
-	st = odb_read_stream_open(r->objects, oid, NULL);
-	if (!st)
-		return -1;
-
 	/* Generate the header */
 	hdrlen = format_object_header(hdr, sizeof(hdr), st->type, st->size);
 
@@ -160,7 +157,6 @@ int stream_object_signature(struct repository *r, const struct object_id *oid)
 		git_hash_update(&c, buf, readlen);
 	}
 	git_hash_final_oid(&real_oid, &c);
-	odb_read_stream_close(st);
 	return !oideq(oid, &real_oid) ? -1 : 0;
 }
 
diff --git a/object-file.h b/object-file.h
index a62d0de394..733d232309 100644
--- a/object-file.h
+++ b/object-file.h
@@ -164,7 +164,9 @@ int check_object_signature(struct repository *r, const struct object_id *oid,
  * Try reading the object named with "oid" using
  * the streaming interface and rehash it to do the same.
  */
-int stream_object_signature(struct repository *r, const struct object_id *oid);
+int stream_object_signature(struct repository *r,
+			    struct odb_read_stream *stream,
+			    const struct object_id *oid);
 
 enum finalize_object_file_flags {
 	FOF_SKIP_COLLISION_CHECK = 1,
diff --git a/object.c b/object.c
index 4669b8d65e..9d2c676b16 100644
--- a/object.c
+++ b/object.c
@@ -6,6 +6,7 @@
 #include "object.h"
 #include "replace-object.h"
 #include "object-file.h"
+#include "odb/streaming.h"
 #include "blob.h"
 #include "statinfo.h"
 #include "tree.h"
@@ -330,9 +331,21 @@ struct object *parse_object_with_flags(struct repository *r,
 
 	if ((!obj || obj->type == OBJ_NONE || obj->type == OBJ_BLOB) &&
 	    odb_read_object_info(r->objects, oid, NULL) == OBJ_BLOB) {
-		if (!skip_hash && stream_object_signature(r, repl) < 0) {
-			error(_("hash mismatch %s"), oid_to_hex(oid));
-			return NULL;
+		if (!skip_hash) {
+			struct odb_read_stream *stream = odb_read_stream_open(r->objects, oid, NULL);
+
+			if (!stream) {
+				error(_("unable to open object stream for %s"), oid_to_hex(oid));
+				return NULL;
+			}
+
+			if (stream_object_signature(r, stream, repl) < 0) {
+				error(_("hash mismatch %s"), oid_to_hex(oid));
+				odb_read_stream_close(stream);
+				return NULL;
+			}
+
+			odb_read_stream_close(stream);
 		}
 		parse_blob_buffer(lookup_blob(r, oid));
 		return lookup_object(r, oid);
diff --git a/pack-check.c b/pack-check.c
index 67cb2cf72f..46782a29d5 100644
--- a/pack-check.c
+++ b/pack-check.c
@@ -9,6 +9,7 @@
 #include "packfile.h"
 #include "object-file.h"
 #include "odb.h"
+#include "odb/streaming.h"
 
 struct idx_entry {
 	off_t                offset;
@@ -104,6 +105,7 @@ static int verify_packfile(struct repository *r,
 	QSORT(entries, nr_objects, compare_entries);
 
 	for (i = 0; i < nr_objects; i++) {
+		struct odb_read_stream *stream = NULL;
 		void *data;
 		struct object_id oid;
 		enum object_type type;
@@ -152,7 +154,9 @@ static int verify_packfile(struct repository *r,
 							type) < 0)
 			err = error("packed %s from %s is corrupt",
 				    oid_to_hex(&oid), p->pack_name);
-		else if (!data && stream_object_signature(r, &oid) < 0)
+		else if (!data &&
+			 (!(stream = odb_read_stream_open(r->objects, &oid, NULL)) ||
+			  stream_object_signature(r, stream, &oid) < 0))
 			err = error("packed %s from %s is corrupt",
 				    oid_to_hex(&oid), p->pack_name);
 		else if (fn) {
@@ -163,12 +167,14 @@ static int verify_packfile(struct repository *r,
 		}
 		if (((base_count + i) & 1023) == 0)
 			display_progress(progress, base_count + i);
-		free(data);
 
+		if (stream)
+			odb_read_stream_close(stream);
+		free(data);
 	}
+
 	display_progress(progress, base_count + i);
 	free(entries);
-
 	return err;
 }
 
-- 
2.53.0.536.g309c995771.dirty
Patrick Steinhardt· Feb 23, 2026, 16:00 UTC · re: Patrick Steinhardt · lore

[PATCH v2 3/4] packfile: expose function to read object stream for an offset

The function `packfile_store_read_object_stream()` takes as input an object ID and then constructs a `struct odb_read_stream` from it. In a subsequent commit we'll want to create an object stream for a given combination of packfile and offset though, which is not something that can currently be done.

Extract a new function `packfile_read_object_stream()` that makes this functionality available.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 packfile.c | 40 ++++++++++++++++++++++++----------------
 packfile.h |  5 +++++
 2 files changed, 29 insertions(+), 16 deletions(-)
Show changes to 2 files +29 −16

packfile.c, packfile.h

diff --git a/packfile.c b/packfile.c
index 402c3b5dc7..3e61176128 100644
--- a/packfile.c
+++ b/packfile.c
@@ -2553,32 +2553,28 @@ static int close_istream_pack_non_delta(struct odb_read_stream *_st)
 	return 0;
 }
 
-int packfile_store_read_object_stream(struct odb_read_stream **out,
-				      struct packfile_store *store,
-				      const struct object_id *oid)
+int packfile_read_object_stream(struct odb_read_stream **out,
+				const struct object_id *oid,
+				struct packed_git *pack,
+				off_t offset)
 {
 	struct odb_packed_read_stream *stream;
 	struct pack_window *window = NULL;
-	struct object_info oi = OBJECT_INFO_INIT;
 	enum object_type in_pack_type;
 	unsigned long size;
 
-	oi.sizep = &size;
+	in_pack_type = unpack_object_header(pack, &window, &offset, &size);
+	unuse_pack(&window);
 
-	if (packfile_store_read_object_info(store, oid, &oi, 0) ||
-	    oi.u.packed.type == PACKED_OBJECT_TYPE_REF_DELTA ||
-	    oi.u.packed.type == PACKED_OBJECT_TYPE_OFS_DELTA ||
-	    repo_settings_get_big_file_threshold(store->source->odb->repo) >= size)
+	if (repo_settings_get_big_file_threshold(pack->repo) >= size)
 		return -1;
 
-	in_pack_type = unpack_object_header(oi.u.packed.pack,
-					    &window,
-					    &oi.u.packed.offset,
-					    &size);
-	unuse_pack(&window);
 	switch (in_pack_type) {
 	default:
 		return -1; /* we do not do deltas for now */
+	case OBJ_BAD:
+		mark_bad_packed_object(pack, oid);
+		return -1;
 	case OBJ_COMMIT:
 	case OBJ_TREE:
 	case OBJ_BLOB:
@@ -2592,10 +2588,22 @@ int packfile_store_read_object_stream(struct odb_read_stream **out,
 	stream->base.type = in_pack_type;
 	stream->base.size = size;
 	stream->z_state = ODB_PACKED_READ_STREAM_UNINITIALIZED;
-	stream->pack = oi.u.packed.pack;
-	stream->pos = oi.u.packed.offset;
+	stream->pack = pack;
+	stream->pos = offset;
 
 	*out = &stream->base;
 
 	return 0;
 }
+
+int packfile_store_read_object_stream(struct odb_read_stream **out,
+				      struct packfile_store *store,
+				      const struct object_id *oid)
+{
+	struct pack_entry e;
+
+	if (!find_pack_entry(store, oid, &e))
+		return -1;
+
+	return packfile_read_object_stream(out, oid, e.p, e.offset);
+}
diff --git a/packfile.h b/packfile.h
index acc5c55ad5..b9f5f1c18c 100644
--- a/packfile.h
+++ b/packfile.h
@@ -436,6 +436,11 @@ off_t get_delta_base(struct packed_git *p, struct pack_window **w_curs,
 		     off_t *curpos, enum object_type type,
 		     off_t delta_obj_offset);
 
+int packfile_read_object_stream(struct odb_read_stream **out,
+				const struct object_id *oid,
+				struct packed_git *pack,
+				off_t offset);
+
 void release_pack_memory(size_t);
 
 /* global flag to enable extra checks when accessing packed objects */
-- 
2.53.0.536.g309c995771.dirty
Patrick Steinhardt· Feb 23, 2026, 16:00 UTC · re: Patrick Steinhardt · lore

[PATCH v2 4/4] pack-check: fix verification of large objects

It was reported [1] that git-fsck(1) may sometimes run into an infinite loop when processing packfiles. This bug was bisected to c31bad4f7d (packfile: track packs via the MRU list exclusively, 2025-10-30), which refactored our lsit of packfiles to only be tracked via an MRU list, exclusively. This isn't entirely surprising: any caller that iterates through the list of packfiles and then hits `find_pack_entry()`, for example because they read an object from it, may cause the MRU list to be updated. And if the caller is unlucky, this may cause the mentioned infinite loop.

While this mechanism is somewhat fragile, it is still surprising that we encounter it when verifying the packfile. We iterate through objects in a given pack one by one and then read them via their offset, and doing this shouldn't ever end up in `find_pack_entry()`.

But there is an edge case here: when the object in question is a blob bigger than "core.largeFileThreshold", then we will be careful to not read it into memory. Instead, we read it via an object stream by calling `odb_read_object_stream()`, and that function will perform an object lookup via `odb_read_object_info()`. So in the case where there are at least two blobs in two different packfiles, and both of these blobs exceed "core.largeFileThreshold", then we'll run into an infinite loop because we'll always update the MRU.

We could fix this by improving `repo_for_each_pack()` to not update the MRU, and this would address the issue. But the fun part is that using `odb_read_object_stream()` is the wrong thing to do in the first place: it may open _any_ instance of this object, so we ultimately cannot be sure that we even verified the object in our given packfile.

Fix this bug by creating the object stream for the packed object directly via `packfile_read_object_stream()`. Add a test that would have caused the infinite loop.

[1]: <20260222183710.2963424-1-sandals@crustytoothpaste.net>
Reported-by: brian m. carlson <sandals@crustytoothpaste.net>
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 pack-check.c    |  2 +-
 t/t1450-fsck.sh | 38 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 39 insertions(+), 1 deletion(-)
Show changes to 2 files +39 −1

pack-check.c, t/t1450-fsck.sh

diff --git a/pack-check.c b/pack-check.c
index 46782a29d5..7378c80730 100644
--- a/pack-check.c
+++ b/pack-check.c
@@ -155,7 +155,7 @@ static int verify_packfile(struct repository *r,
 			err = error("packed %s from %s is corrupt",
 				    oid_to_hex(&oid), p->pack_name);
 		else if (!data &&
-			 (!(stream = odb_read_stream_open(r->objects, &oid, NULL)) ||
+			 (packfile_read_object_stream(&stream, &oid, p, entries[i].offset) < 0 ||
 			  stream_object_signature(r, stream, &oid) < 0))
 			err = error("packed %s from %s is corrupt",
 				    oid_to_hex(&oid), p->pack_name);
diff --git a/t/t1450-fsck.sh b/t/t1450-fsck.sh
index 8fb79b3e5d..54e81c2636 100755
--- a/t/t1450-fsck.sh
+++ b/t/t1450-fsck.sh
@@ -852,6 +852,44 @@ test_expect_success 'fsck errors in packed objects' '
 	! grep corrupt out
 '
 
+test_expect_success 'fsck handles multiple packfiles with big blobs' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+
+		# We construct two packfiles with two objects in common and one
+		# object not in common. The objects in common can then be
+		# corrupted in one of the packfiles, respectively. The other
+		# objects that are unique to the packs are merely used to not
+		# have both packs contain the same data.
+		blob_one=$(test-tool genrandom one 200k | git hash-object -t blob -w --stdin) &&
+		blob_two=$(test-tool genrandom two 200k | git hash-object -t blob -w --stdin) &&
+		blob_three=$(test-tool genrandom three 200k | git hash-object -t blob -w --stdin) &&
+		blob_four=$(test-tool genrandom four 200k | git hash-object -t blob -w --stdin) &&
+		pack_one=$(printf "%s\n" "$blob_one" "$blob_two" "$blob_three" | git pack-objects .git/objects/pack/pack) &&
+		pack_two=$(printf "%s\n" "$blob_two" "$blob_three" "$blob_four" | git pack-objects .git/objects/pack/pack) &&
+		chmod a+w .git/objects/pack/pack-*.pack &&
+
+		# Corrupt blob two in the first pack.
+		git verify-pack -v .git/objects/pack/pack-$pack_one >objects &&
+		offset_one=$(sed <objects -n "s/^$blob_two .* \(.*\)$/\1/p") &&
+		printf "\0" | dd of=.git/objects/pack/pack-$pack_one.pack bs=1 conv=notrunc seek=$offset_one &&
+
+		# Corrupt blob three in the second pack.
+		git verify-pack -v .git/objects/pack/pack-$pack_two >objects &&
+		offset_two=$(sed <objects -n "s/^$blob_three .* \(.*\)$/\1/p") &&
+		printf "\0" | dd of=.git/objects/pack/pack-$pack_two.pack bs=1 conv=notrunc seek=$offset_two &&
+
+		# We now expect to see two failures for the corrupted objects,
+		# even though they exist in a non-corrupted form in the
+		# respective other pack.
+		test_must_fail git -c core.bigFileThreshold=100k fsck 2>err &&
+		test_grep "unknown object type 0 at offset $offset_one in .git/objects/pack/pack-$pack_one.pack" err &&
+		test_grep "unknown object type 0 at offset $offset_two in .git/objects/pack/pack-$pack_two.pack" err
+	)
+'
+
 test_expect_success 'fsck fails on corrupt packfile' '
 	hsh=$(git commit-tree -m mycommit HEAD^{tree}) &&
 	pack=$(echo $hsh | git pack-objects .git/objects/pack/pack) &&
-- 
2.53.0.536.g309c995771.dirty

← back to recent threads