Git development
 help / color / mirror / Atom feed
From: "Elijah Newren via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Patrick Steinhardt <ps@pks.im>, Elijah Newren <newren@gmail.com>,
	Jeff King <peff@peff.net>, Derrick Stolee <stolee@gmail.com>,
	Elijah Newren <newren@gmail.com>
Subject: [PATCH v3 0/4] Objects treated as missing despite being present, due to race with geometric repacking
Date: Sat, 29 Aug 2026 07:00:27 +0000	[thread overview]
Message-ID: <pull.2207.v3.git.1787986831.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2207.git.1787092446.gitgitgadget@gmail.com>

Changes since v2:

 * Ripped out the old 3/4 dealing with QUICK readers; QUICK readers are left
   alone
 * Insert a new 3/4 fixing git mktree --batch to stop passing QUICK (with
   new testcase in t1010)
 * undo bad paragraph comment change
 * renamed fill_midx_entry() -> midx_fill_entry(), so that we catch any
   other new callers and appropriately check their return value (caught one
   in test-read-midx.c)

Changes since v1:

 * Rebased on top of ps/odb-generic-corrupt-objects, and conflicts with it
   resolved
 * Removed useless test_grep line spotted by Junio in PATCH 1
 * Switched fill_midx_entry() to a tri-state to avoid duplicate
   bsearch_midx(), as suggested by Peff
 * Only do the re-read on SECOND_READ, as suggested by Peff
 * Handle multiple objects shared across multiple packs correctly (issue
   caught & corrected & new testcase by deeper AI review)
 * Inserted two new patches:
   * 2/4: Fix a leak in git mktree --batch since I use it in new testcases
     and don't want the *-leaks jobs failing
   * 3/4: Demonstrate and fix QUICK reader problems, while keeping expected
     QUICK performance for normal cases (we've already been discussing this
     patch in this thread a bunch anyway, and it's logically related)

Cover letter addendum/update:

We also fix git mktree --batch to no longer erroneously pass QUICK.

Note here that Stolee's suggestion to defer pack deletion via git
multi-pack-index expire seems like a good complementary mitigation; it would
reduce how often we fall into recovery, while this series tries to fix
recovery to work more robustly.

Original cover letter (focused on the final patch):

When an object is found in multiple packs that are in a multi-pack-index,
and a subsequent geometric repacking creates a new multi-pack-index and
removes the pack that was considered the owner of the object in the old
multi-pack-index, then an already-running process that had opened the old
multi-pack-index and hadn't yet opened the removed packfile will not be able
to access the object -- lookups will return it as missing. Additionally,
replay has a separate bug where a missing object causes a SIGSEGV rather
than an error message.

This appears to affect a very small percentage of git operations in
production since it is a tiny window, but I've found evidence of it
occurring in at least eight distinct server-side operations, covering seven
different git commands:

git operation                        symptom
-----------------------------------  -----------------------------
git replay (server-side rebase)      SIGSEGV (this series, 1/2)
git merge-tree                       spurious read-miss failure
git diff (raw and tree-vs-tree)      spurious read-miss failure
git rev-list --count                 spurious read-miss failure
git merge-base                       spurious read-miss failure
object/rev resolution (rev-parse,    spurious read-miss failure
  cat-file)
repository repair (fsck/repack)      spurious read-miss failure


There are also commands that could be changing behavior without throwing an
error -- e.g. object negotiation thinking an object doesn't exist and
instead negotiating based on an older common commit, or cat-file --batch
reporting that some objects don't exist.

This series fixes the replay bug first, since it's simpler; investigating
it, together with my other recent repacking work, is what led me to the
underlying multi-pack-index issue that 2/2 addresses.

Elijah Newren (4):
  replay: fail gracefully when a merge input is unreadable
  mktree: plug per-tree leak in --batch mode
  mktree: do not use OBJECT_INFO_QUICK when checking objects
  packfile: recover when a multi-pack-index names a removed pack

 builtin/mktree.c            |  4 +++-
 builtin/pack-objects.c      |  2 +-
 midx.c                      | 20 ++++++++--------
 midx.h                      | 21 ++++++++++++++--
 odb/source-packed.c         | 42 ++++++++++++++++++++++++++++----
 replay.c                    |  7 ++++++
 t/helper/test-read-midx.c   |  2 +-
 t/t1010-mktree.sh           | 48 +++++++++++++++++++++++++++++++++++++
 t/t3650-replay-basics.sh    | 34 ++++++++++++++++++++++++++
 t/t5319-multi-pack-index.sh | 40 +++++++++++++++++++++++++++++++
 10 files changed, 200 insertions(+), 20 deletions(-)


base-commit: 2135b14863642bbcec02996e7f5e54ac1f77b03a
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2207%2Fnewren%2Fmidx-removed-pack-recovery-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2207/newren/midx-removed-pack-recovery-v3
Pull-Request: https://github.com/gitgitgadget/git/pull/2207

Range-diff vs v2:

 1:  36bf2ce17b = 1:  36bf2ce17b replay: fail gracefully when a merge input is unreadable
 2:  3f3b75690e = 2:  3f3b75690e mktree: plug per-tree leak in --batch mode
 3:  fc98f48ddb < -:  ---------- packfile: recover object lookups racing a concurrent repack
 -:  ---------- > 3:  79ce753c68 mktree: do not use OBJECT_INFO_QUICK when checking objects
 4:  eacf6ba4b1 ! 4:  9b0966df9a packfile: recover when a multi-pack-index names a removed pack
     @@ Commit message
          previous MIDX keeps seeing a removed pack listed as the owner of some
          objects.  Since a MIDX attributes each object to exactly one pack, such
          an object is served only through its recorded owner; if that owner was
     -    just removed, find_pack_entry() cannot serve it -- fill_midx_entry()
     -    routes to the missing pack, and the regular pack fallback deliberately
     -    skips every MIDX-covered pack, so a surviving copy in another covered
     -    pack (e.g. a kept base pack) is never consulted.
     +    just removed, find_pack_entry() cannot serve it -- the MIDX lookup routes
     +    to the missing pack, and the regular pack fallback deliberately skips
     +    every MIDX-covered pack, so a surviving copy in another covered pack
     +    (e.g. a kept base pack) is never consulted.
      
          Unlike the ordinary "a pack's .idx is mapped but its .pack is gone"
     -    race, the second read does not rescue us -- and not only for
     -    OBJECT_INFO_QUICK callers.  Reloading the on-disk pack set does not
     -    reload the borrowed, cached MIDX (freeing it under the code that caches
     -    the "struct multi_pack_index *" would be a use-after-free), so the stale
     -    MIDX keeps routing to the removed pack and the surviving copy stays
     +    race, the second read does not rescue us.  Reloading the on-disk pack set
     +    does not reload the borrowed, cached MIDX (freeing it under the code that
     +    caches the "struct multi_pack_index *" would be a use-after-free), so the
     +    stale MIDX keeps routing to the removed pack and the surviving copy stays
          hidden behind the covered-pack skip.  cat-file, rev-list and pack-objects
          can thus all spuriously fail with "unable to read object".
      
     -    Teach find_pack_entry() to recover.  fill_midx_entry() now returns a
     -    tri-state, distinguishing "absent from the MIDX" from "present but the
     -    owning pack is unavailable"; in the latter case, once the regular
     -    fallback has also missed, scan the MIDX's packs directly for a surviving
     -    copy.
     +    Teach find_pack_entry() to recover.  The MIDX lookup now returns a
     +    tri-state, distinguishing an object absent from the MIDX from one it owns
     +    via a pack that can no longer be opened; in the latter case, once the
     +    regular fallback has also missed, scan the MIDX's packs directly for a
     +    surviving copy.  Because the return value is no longer a boolean, rename
     +    fill_midx_entry() to midx_fill_entry() so callers must reckon with the
     +    new enum rather than silently treat MIDX_FILL_OWNER_UNAVAILABLE as a hit.
      
          Do the scan only on the second read (OBJECT_INFO_SECOND_READ): by then
          the cheaper on-disk reload has run, so an object merely relocated into a
     -    new (non-covered) pack has already been found by the regular fallback,
     -    and only a genuine hidden duplicate reaches the rescan.  QUICK callers
     -    that would skip the second read are steered into it by the preceding
     -    commit's stale_packs_detected flag, which prepare_midx_pack() sets when
     -    it cannot open the owning pack.
     +    new (uncovered) pack has already been found by the regular fallback, and
     +    only a genuine hidden duplicate reaches the rescan.  A QUICK caller that
     +    skips the second read simply accepts the false negative, as QUICK is
     +    designed to.
      
          Reloading the stale MIDX would be a more complete fix but is much more
          involved (the borrowers above need proper invalidation), so leave that
     @@ builtin/pack-objects.c: static int want_object_in_pack_mtime(const struct object
       		struct pack_entry e;
       
      -		if (m && fill_midx_entry(m, oid, &e, NULL)) {
     -+		if (m && fill_midx_entry(m, oid, &e, NULL) == MIDX_FILL_HIT) {
     ++		if (m && midx_fill_entry(m, oid, &e, NULL) == MIDX_FILL_HIT) {
       			want = want_object_in_pack_one(e.p, oid, exclude, found_pack, found_offset, found_mtime);
       			if (want != -1)
       				return want;
     @@ midx.c: uint32_t nth_midxed_pack_int_id(struct multi_pack_index *m, uint32_t pos
      -		    const struct object_id *oid,
      -		    struct pack_entry *e,
      -		    struct packed_git **bad_pack)
     -+enum midx_fill_result fill_midx_entry(struct multi_pack_index *m,
     ++enum midx_fill_result midx_fill_entry(struct multi_pack_index *m,
      +				      const struct object_id *oid,
      +				      struct pack_entry *e,
      +				      struct packed_git **bad_pack)
     @@ midx.c: uint32_t nth_midxed_pack_int_id(struct multi_pack_index *m, uint32_t pos
       
       	if (prepare_midx_pack(m, pack_int_id))
      -		return 0;
     -+		goto owner_unavailable;
     ++		return MIDX_FILL_OWNER_UNAVAILABLE;
       	p = m->packs[pack_int_id - m->num_packs_in_base];
       
     --	/*
     --	* We are about to tell the caller where they can locate the
     --	* requested object.  We better make sure the packfile is
     --	* still here and can be accessed before supplying that
     --	* answer, as it may have been deleted since the MIDX was
     --	* loaded!
     --	*/
     -+	/* Make sure the pack is still present before pointing at it. */
     + 	/*
     +@@ midx.c: int fill_midx_entry(struct multi_pack_index *m,
     + 	* loaded!
     + 	*/
       	if (!is_pack_valid(p))
      -		return 0;
     -+		goto owner_unavailable;
     ++		return MIDX_FILL_OWNER_UNAVAILABLE;
       
       	if (oidset_size(&p->bad_objects) &&
       	    oidset_contains(&p->bad_objects, oid)) {
     @@ midx.c: uint32_t nth_midxed_pack_int_id(struct multi_pack_index *m, uint32_t pos
       
      -	return 1;
      +	return MIDX_FILL_HIT;
     -+
     -+owner_unavailable:
     -+	/*
     -+	 * Re-arm stale_packs_detected on every such lookup, not just the
     -+	 * first: prepare_midx_pack() caches the failure, so without this a
     -+	 * later lookup of the same vanished pack would leave the flag clear
     -+	 * and a QUICK reader would skip its recovering second read.
     -+	 */
     -+	m->source->base.odb->stale_packs_detected = 1;
     -+	return MIDX_FILL_OWNER_UNAVAILABLE;
       }
       
       /* Match "foo.idx" against either "foo.pack" _or_ "foo.idx". */
     @@ midx.c: int verify_midx_file(struct odb_source_packed *source, unsigned flags)
       		nth_midxed_object_oid(&oid, m, pairs[i].pos);
       
      -		if (!fill_midx_entry(m, &oid, &e, NULL)) {
     -+		if (fill_midx_entry(m, &oid, &e, NULL) != MIDX_FILL_HIT) {
     ++		if (midx_fill_entry(m, &oid, &e, NULL) != MIDX_FILL_HIT) {
       			midx_report(_("failed to load pack entry for oid[%d] = %s"),
       				    pairs[i].pos, oid_to_hex(&oid));
       			continue;
     @@ midx.h: uint32_t nth_midxed_pack_int_id(struct multi_pack_index *m, uint32_t pos
      +	MIDX_FILL_OWNER_UNAVAILABLE,
      +};
      +
     -+enum midx_fill_result fill_midx_entry(struct multi_pack_index *m,
     ++enum midx_fill_result midx_fill_entry(struct multi_pack_index *m,
      +				      const struct object_id *oid,
      +				      struct pack_entry *e,
      +				      struct packed_git **bad_pack);
     @@ odb/source-packed.c
      -	if (store->midx && fill_midx_entry(store->midx, oid, e, bad_pack))
      -		return 1;
      +	if (store->midx) {
     -+		midx_result = fill_midx_entry(store->midx, oid, e, bad_pack);
     ++		midx_result = midx_fill_entry(store->midx, oid, e, bad_pack);
      +		if (midx_result == MIDX_FILL_HIT)
      +			return 1;
      +	}
     @@ odb/source-packed.c: static int odb_source_packed_freshen_object(struct odb_sour
       	if (e.p->is_cruft)
       		return 0;
      
     + ## t/helper/test-read-midx.c ##
     +@@ t/helper/test-read-midx.c: static int read_midx_file(const char *object_dir, const char *checksum,
     + 		for (i = 0; i < m->num_objects; i++) {
     + 			nth_midxed_object_oid(&oid, m,
     + 					      i + m->num_objects_in_base);
     +-			fill_midx_entry(m, &oid, &e, NULL);
     ++			midx_fill_entry(m, &oid, &e, NULL);
     + 
     + 			printf("%s %"PRIu64"\t%s\n",
     + 			       oid_to_hex(&oid), e.offset, e.p->pack_name);
     +
       ## t/t5319-multi-pack-index.sh ##
      @@ t/t5319-multi-pack-index.sh: test_expect_success 'pack.preferBitmapTips interprets patterns as hierarchy' '
       	)
     @@ t/t5319-multi-pack-index.sh: test_expect_success 'pack.preferBitmapTips interpre
      +		test_cmp expect actual
      +	)
      +'
     -+
     -+test_expect_success 'repeated QUICK lookups recover after owning pack removed' '
     -+	test_when_finished "rm -fr repo" &&
     -+	git init repo &&
     -+	(
     -+		cd repo &&
     -+
     -+		# Two blobs, each duplicated across packs so the midx must pick
     -+		# an owning pack, and each attributed to the same moderate pack.
     -+		echo one >f1 &&
     -+		echo two >f2 &&
     -+		git add f1 f2 &&
     -+		git commit -m dups &&
     -+		d1=$(git rev-parse HEAD:f1) &&
     -+		d2=$(git rev-parse HEAD:f2) &&
     -+
     -+		# Roll every object, including d1 and d2, into one big pack,
     -+		# then build a moderate pack that also holds both blobs.
     -+		git repack -adq &&
     -+		moderate=$(printf "%s\n%s\n" "$d1" "$d2" |
     -+			git pack-objects --quiet $objdir/pack/pack) &&
     -+
     -+		git multi-pack-index write \
     -+			--preferred-pack="pack-$moderate.idx" &&
     -+
     -+		# Retire the moderate pack; the stale midx still names it as the
     -+		# owner of both blobs, each of which survives in the big pack.
     -+		rm -f $objdir/pack/pack-$moderate.* &&
     -+
     -+		# One resident QUICK reader ("git mktree --batch") resolves both
     -+		# blobs.  The first lookup recovers d1 and caches the owning
     -+		# packs failure; unless that failure keeps re-arming the second
     -+		# read, the lookup of d2 skips its recovering read and the reader
     -+		# dies reporting d2 as missing.
     -+		printf "100644 blob %s\tf1\n\n100644 blob %s\tf2\n\n" \
     -+			"$d1" "$d2" |
     -+			git mktree --batch >trees &&
     -+		test_line_count = 2 trees
     -+	)
     -+'
      +
       test_done

-- 
gitgitgadget

  parent reply	other threads:[~2026-08-29  7:00 UTC|newest]

Thread overview: 49+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-18 22:34 [PATCH 0/2] Objects treated as missing despite being present, due to race with geometric repacking Elijah Newren via GitGitGadget
2026-08-18 22:34 ` [PATCH 1/2] replay: fail gracefully when a merge input is unreadable Elijah Newren via GitGitGadget
2026-08-19 18:09   ` Junio C Hamano
2026-08-21  1:44     ` Elijah Newren
2026-08-21  3:37       ` Junio C Hamano
2026-08-18 22:34 ` [PATCH 2/2] packfile: recover when a multi-pack-index names a removed pack Elijah Newren via GitGitGadget
2026-08-19 18:21   ` Junio C Hamano
2026-08-20  7:54   ` Patrick Steinhardt
2026-08-21  1:36     ` Elijah Newren
2026-08-24  4:48       ` Jeff King
2026-08-24  5:13         ` Patrick Steinhardt
2026-08-24  6:55           ` Jeff King
2026-08-24  7:06             ` Jeff King
2026-08-24  7:23               ` Jeff King
2026-08-25  7:38               ` Elijah Newren
2026-08-24  4:55   ` Jeff King
2026-08-24  5:40     ` Patrick Steinhardt
2026-08-24  7:03       ` Jeff King
2026-08-25  7:19     ` Elijah Newren
2026-08-24 14:45   ` Derrick Stolee
2026-08-25  7:38     ` Elijah Newren
2026-08-24 14:46   ` Derrick Stolee
2026-08-25 19:00 ` [PATCH v2 0/4] Objects treated as missing despite being present, due to race with geometric repacking Elijah Newren via GitGitGadget
2026-08-25 19:00   ` [PATCH v2 1/4] replay: fail gracefully when a merge input is unreadable Elijah Newren via GitGitGadget
2026-08-25 19:00   ` [PATCH v2 2/4] mktree: plug per-tree leak in --batch mode Elijah Newren via GitGitGadget
2026-08-27  5:36     ` Jeff King
2026-08-25 19:00   ` [PATCH v2 3/4] packfile: recover object lookups racing a concurrent repack Elijah Newren via GitGitGadget
2026-08-27  5:57     ` Jeff King
2026-08-27 22:23       ` Elijah Newren
2026-08-29 11:32         ` Jeff King
2026-08-25 19:00   ` [PATCH v2 4/4] packfile: recover when a multi-pack-index names a removed pack Elijah Newren via GitGitGadget
2026-08-27  6:06     ` Jeff King
2026-08-28  7:29       ` Elijah Newren
2026-08-29 11:34         ` Jeff King
2026-08-29  7:00 ` Elijah Newren via GitGitGadget [this message]
2026-08-29  7:00   ` [PATCH v3 1/4] replay: fail gracefully when a merge input is unreadable Elijah Newren via GitGitGadget
2026-08-29  7:00   ` [PATCH v3 2/4] mktree: plug per-tree leak in --batch mode Elijah Newren via GitGitGadget
2026-08-29  7:00   ` [PATCH v3 3/4] mktree: do not use OBJECT_INFO_QUICK when checking objects Elijah Newren via GitGitGadget
2026-08-29 11:46     ` Jeff King
2026-08-29  7:00   ` [PATCH v3 4/4] packfile: recover when a multi-pack-index names a removed pack Elijah Newren via GitGitGadget
2026-08-29 12:07     ` Jeff King
2026-08-30 20:53       ` Junio C Hamano
2026-08-31 10:43         ` Patrick Steinhardt
2026-08-31 23:10         ` Jeff King
2026-09-01 15:27         ` Derrick Stolee
2026-09-01 16:04           ` Junio C Hamano
2026-09-01 15:26     ` Derrick Stolee
2026-09-01 16:47       ` Elijah Newren
2026-09-01 17:12         ` Derrick Stolee

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=pull.2207.v3.git.1787986831.gitgitgadget@gmail.com \
    --to=gitgitgadget@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=newren@gmail.com \
    --cc=peff@peff.net \
    --cc=ps@pks.im \
    --cc=stolee@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox