git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: [PATCH] fsck: snapshot default refs before object walk

From
Elijah Newren <newren@gmail.com>
Date
Jan 6, 2026, 23:36 UTC
Message-ID
<CABPp-BGqiM8fmirgdqumRNfzWediC5v_uZ9qHjntTqPqABDhnA@mail.gmail.com>
In-Reply-To
<20260102054922.GA2580212@coredump.intra.peff.net>
On Thu, Jan 1, 2026 at 9:49 PM Jeff King <peff@peff.net> wrote:
Show 25 quoted lines
>
> On Mon, Dec 29, 2025 at 07:12:29PM +0000, Elijah Newren via GitGitGadget wrote:
>
> > Fsck has a race when operating on live repositories; consider the
> > following simple script that writes new commits as fsck runs:
> >
> >     #!/bin/bash
> >     git fsck &
> >     PID=$!
> >
> >     while ps -p $PID >/dev/null; do
> >         sleep 3
> >         git commit -q --allow-empty -m "Another commit"
> >     done
> >
> > Since fsck reads refs at the beginning, walks those for connectivity,
> > and then reads the refs again at the end to check, this can cause fsck
> > to get confused and think that the new refs refer to missing commits and
> > that new reflog entries are invalid.
>
> I'm not sure if this is entirely accurate. Does fsck read refs at the
> beginning? I think it walks over everything in the object database
> without regard to connectivity, marking each with the HAS_OBJ flag. And
> then we use the refs to find the reachable objects, making sure any that
> are reachable also have HAS_OBJ set (otherwise they are missing).
Oh, indeed; I got that wrong.
Show 9 quoted lines
> So the race is really:
>
>   1. We look at all of the packs and loose objects to set HAS_OBJ.
>
>   2. Somebody else simultaneously adds a new object, which is missed by
>      our step 1, and updates a ref to point to it.
>
>   3. We look at the refs for reachability, see the new object, but
>      think it is missing because we never saw it in step 1.
Yep.
Show 24 quoted lines
> And your fix is to snapshot the refs as a "step 0", and use that
> snapshot in step 3. So any new objects that are introduced after step 1
> will never be referenced, since we are using the snapshot values.
>
> Which makes sense as long as we assume objects are only added to the
> repository. I think we'd now have the opposite direction race:
>
>   0. We snapshot the refs.
>
>   1. Somebody else deletes a ref, and then does a pruning git-gc which
>      deletes the object it pointed to.
>
>   2. We look at all of the objects and mark them as HAS_OBJ. We do not
>      include the now-deleted object.
>
>   3. We do a connectivity check with the snapshot, and are dismayed to
>      find that the deleted object (which we believe is still referenced)
>      has gone away.
>
> I think you could argue that this is a much more preferable race,
> though. A busy server will see lots of new objects introduced and refs
> updated, and you do not want to have a stop-the-world lock that prevents
> pushes. But it is much less common to do a pruning gc, and it is
> probably OK to have a mutually exclusive lock between fsck and gc.
Indeed.  I'll add some comments to that effect.
Show 25 quoted lines
> > This problem doesn't occur when refs are specified on the command line
> > for us to check, since we use those specified refs for both walking and
> > checking.  Using the same refs for walking and checking seems to just
> > make sense, so modify the existing code to do the same when refs aren't
> > specified.
>
> So I don't think this part is quite right either, then. We're not using
> the command-line arguments collect the set of objects in the repo. That
> still happens by walking over the odb itself. So if I do:
>
>   git fsck --no-dangling HEAD
>
> in git.git, and while it is running, do this in another terminal:
>
>   git commit --allow-empty -m foo
>
> then I get:
>
>   error: 49a90c19d0cd010ed00fbb1e4256cbefaa8b83e2: object missing
>
> even with your patch. So I think that names given on the command-line
> could benefit from this type of snapshot, because they suffer from the
> same race. You want to lock in the ref resolution (whether from
> iterating or from names on the command-line) before you start walking
> over the odb.
Good point.  I added this snapshotting in v2.
>   Side note: I do not think I have ever run fsck with refs on the
>   command-line. It is not like it saves you any time! Most of the
>   expense comes from opening up and verifying the objects in the first
>   step, not from looking at ref reachability.

Not to mention it produces spurious "dangling" object warnings, because while the objects might be reachable, they aren't necessarily reachable from the particular subset you specified on the command line. I wonder if no one ever noticed that because it's such a useless mode; I only noticed it because you pointed out how Matthew and I overlooked races with command-line arguments.

Show 9 quoted lines
> And one final note on the overall direction of the patch. We are
> assuming that if we look at the refs first and then the odb second, that
> we will be getting a "fresh" view of the odb in that second step. But
> that isn't necessarily so, as we might have loaded the set of packs
> earlier in the process. I don't know if it is possible to trigger that
> during fsck or not, but certainly it is relying on a subtle assumption.
> It probably is worth calling odb_reprepare() after taking the snapshot
> to ensure we are not getting any results cached from before the snapshot
> was taken.
Will add.
Show 29 quoted lines
> > +struct ref_snapshot {
> > +     size_t nr;
> > +     size_t name_alloc;
> > +     size_t oid_alloc;
> > +     char **refname;
> > +     struct object_id *oid;
> > +};
>
> Minor nit, but: why keep two arrays and not a single struct with both?
> After all, you even end up sticking them back in a struct at the only
> point of use:
>
> > +     if (the_refs)
> > +             for (size_t i = 0; i < the_refs->nr; i++) {
> > +                     struct reference ref = {
> > +                             .name = the_refs->refname[i],
> > +                             .oid = &the_refs->oid[i],
> > +                     };
> > +                     fsck_handle_ref(&ref, NULL);
> > +             }
>
> So this could really just be an array of "struct reference". You can't
> just hold onto the "struct reference" passed in to the snapshot_refs()
> callback (because it gets reused as we iterate), but you could do a deep
> copy.
>
> That did make me wonder a bit about the other fields in "struct
> reference" (which your snapshot just throws away). But it looks like
> fsck_handle_ref() only cares about the name and oid, so it is OK.

Junio suggested the same thing, although he also suggested we might want to snapshot some reflog information at the same time, which then wouldn't make sense to be using a struct reference. Even though I'm not implementing per-reflog snapshotting, I left a comment in the code about it so I think it made sense to just create my own data structure with just the name and oid.

Show 34 quoted lines
> > @@ -999,6 +1050,19 @@ int cmd_fsck(int argc,
> >       if (check_references)
> >               fsck_refs(the_repository);
> >
> > +     /*
> > +      * Take a snapshot of the refs before walking objects to avoid looking
> > +      * at a set of refs that may be changed by the user while we are walking
> > +      * objects. We can still walk over new objects that are added during the
> > +      * execution of fsck but won't miss any objects that were reachable.
> > +      */
> > +     use_snapshot = !argc;
> > +     if (use_snapshot) {
> > +             now = time(NULL);
> > +             refs_for_each_rawref(get_main_ref_store(the_repository),
> > +                                  snapshot_refs, &default_refs_snapshot);
> > +     }
>
> BTW, one of the reasons I started looking at this is that Coverity
> complained about this segment of code. We set use_snapshot if and only
> if we don't have any argc arguments. And then later...
>
> >       if (!argc) {
> > -             get_default_heads();
> > +             get_default_heads(use_snapshot ? &default_refs_snapshot : NULL);
> >               keep_cache_objects = 1;
> >       }
>
> ...we enter this block only if argc is zero. So we know that
> use_snapshot will be true here, and the NULL path (and thus the fallback
> code in get_default_heads()) will never be used.
>
> That's not wrong exactly, as it's "just" dead code. But it was what led
> me to thinking about whether the case of non-zero argc would benefit
> from the snapshot, too.

Yeah, I needed to restructure this anyway to handle snapshotting command line arguments, so I just cleaned it all up. Thanks for reading carefully.

> There are a few other related interesting cases, too:
>
>   - We may use the index file for connectivity, as well. It suffers from
>     the same race, and would benefit from a snapshot.

I left a couple TODO comments about this, so that those who are interested/motivated can extend the snapshotting further.

>   - In get_default_heads() we also look at worktree HEADs. Those have
>     the same race (their normal refs we don't consider here, because
>     they were already handled by the overall ref iteration).

I handled these in my newer version, since handling them is pretty similar to handling command line arguments.

> I know that neither of those is of particular interest to you as a bare
> server repo would have neither. And it may be OK not to handle them, if
> the complexity doesn't merit it. But it might be worth documenting the
> short-coming.
Yep, absolutely.  Thanks for reading so carefully.
Show 6 quoted lines
> -Peff
>
> PS The other reason I looked at your patch is that I got deja vu from
>    all of this. I thought we had discussed ref snapshotting for fsck
>    before, but I couldn't find anything on the list. It may have been
>    internal GitHub discussions.

It likely was. This was based on a patch that's in GitHub's fork of git, which tripped up Michael Haggerty recently -- in particular, he thought this fsck race had been "fixed", but was tripped up both by the fact that it was a non-default option and that it was only in our fork. I volunteered to try to fix both issues, and heavily overhauled the patch in v1, and will have completely rewritten the original by v2.

Previous: Jeff KingNext: Jeff King
Message 6 of 11 in “fsck: snapshot default refs before object walk”
  1. fsck: snapshot default refs before object walkElijah Newren via GitGitGadget, Dec 29, 2025
  2. Junio C HamanoDec 30, 2025
  3. Elijah NewrenJan 6, 2026
  4. Matthew John CheethamJan 9, 2026
  5. Jeff KingJan 2, 2026
  6. Elijah NewrenJan 6, 2026
  7. Jeff KingJan 14, 2026
  8. fsck: snapshot default refs before object walkElijah Newren via GitGitGadget, Jan 7, 2026
  9. fsck: snapshot default refs before object walkElijah Newren via GitGitGadget, Jan 9, 2026
  10. Junio C HamanoJan 11, 2026
  11. Jeff KingJan 14, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.