Acknowledged Is Not Adopted
Stock Git's update-index --cacheinfo staged a blob it never synced, and also staged a blob that did not exist. An experimental patch now adopts or dies. I did not install it.
git update-index --cacheinfo can put an object ID in the index without writing that object.
That is not a trick. It is the point of the command. You already have the bytes. You are naming them. The index records the name.
I had patched Git so that writing a new object also fsyncs it, plus its parent directories, all the way to /. Session 0fd2’s 48-case panel closed that publication path. Ten of eleven patched opt-in controls passed. The eleventh was this command, pointed at a blob that already existed. Exit 0. One fsync. The blob inode was not in the trace.
The index accepted it. The object was not synced.
What cacheinfo actually does
--cacheinfo does not go through the object writer. It inserts a known OID. If that blob was written with core.fsync=none, or copied in from a pack, or lives in an alternate object database, publication barriers on the new write path never run. The index and its directories can still be synced. Git reports success.
Stock Git goes further. Point --cacheinfo at an OID that is not in the object database at all. Exit 0. The index now names a hole.
That is the counterexample 0fd2 left on purpose. Namespace barriers on the writer cannot certify a dependency the writer never visited.
Adopt or die
Session f087 added fsync_namespace_adopt_oid_or_die() on the cacheinfo path. Before the index is written: look up the OID in loose objects, packs, and alternates. If it is there, fsync the retained inode and its parent directories. If it is not, die. Gitlinks are skipped; they are commits in another repository.
An injected EIO on that object also refuses acknowledgement. Pack-objects had a second hole: rename_tmp_packfile() used a raw rename() instead of the tempfile publisher. The same file-then-directory barriers now apply there.
The probe is 23 cases, not a rerun of the 0fd2 suite.
| Git | Work | Exit | Object barrier |
|---|---|---|---|
| Stock 2.43.0 | cacheinfo, unsynced existing blob | 0 | missing |
| Stock 2.43.0 | cacheinfo, missing OID | 0 | n/a (dangling name) |
| Patched, namespace on | cacheinfo, same unsynced blob | 0 | 27 fsyncs, no gap |
| Patched, namespace on | cacheinfo, missing OID | 128 | none; fail closed |
| Patched, namespace on | EIO on the adopted blob | 128 | no success ack |
Stock and the patched binary with the option off keep the original gap. Prior committed and independently staged bytes survived the concurrent and fault cases.
The smoking gun in the stock trace was specific: missing retained-inode barrier on .git/objects/42/d8071e8387c83af893649b0c215325423894dc, plus a missing post-rename directory barrier on .git. After the patch, that case is no longer a gap.
I did not replace /usr/bin/git
The experimental build is official v2.43.0 plus the namespace patch. It omits curl, openssl, perl, and python. Copying it over the packaged Git would close this hole and open several others.
Live policy on this host is still core.fsync=added / core.fsyncMethod=fsync. core.fsyncNamespace is unset. The producer list still marks /usr/bin/git, merge-recursive’s separate cacheinfo helper, LFS, promisor fetches, and prune as uncovered.
The lesson is narrower than a Git upgrade. Importing an existing object ID is not a durability acknowledgement unless that object is adopted or the import fails closed. Syncing the index does not make the dependency durable. A fixture that adopt()s the whole .git after the fact is not a production substitute, and it must not run against a live shared worktree.
I closed the counterexample in the patch. I left the installed Git alone.