Skip to content

fix(gitread): answer 500, not 404, for a ref whose commit git cannot read - #782

Open
ThomasK33 wants to merge 16 commits into
mainfrom
resolve-ref-missing-commit
Open

ThomasK33 wants to merge 16 commits into
mainfrom
resolve-ref-missing-commit

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

Why

An existing branch or tag whose commit git cannot read must answer 500, not 404. On main before this PR, ResolveRef peeled each candidate with rev-parse --verify --quiet <candidate>^{commit} and read exit 1 as "no such ref". git also exits 1 for a ref that exists but whose commit is missing, or whose tag chain git cannot read.

Round 3 tried to save git spawns with for-each-ref. The root verifier found a blocker: it reads loose-ref directories for any pattern. At 180k loose refs a read took 5–7 seconds, and at 540k it exceeded the production 32 MiB memory cap. There is no ref cap. Round 4 removes for-each-ref from resolution. Every candidate lookup is direct, with no ref-directory listing.

Round 4 read a hex abbreviation with a plain rev-parse --verify --quiet <abbrev>. When a commit shares the abbreviation with a blob or a tree, git calls that name ambiguous and exits 1, so GET /repos/commit?ref=<abbrev> answered 404 where main answers 200. Round 5 reads an abbreviation as <abbrev>^{commit} first, as main does.

What changes

  • ResolveRef reads each candidate name with rev-parse --verify --quiet --end-of-options <candidate>, without a peel. A ref whose object is missing still resolves to its full id. A name that does not resolve exits 1. This keeps git's short-name DWIM rules, including refs/r resolving refs/heads/refs/r.
  • A hex abbreviation is read as <abbrev>^{commit} first, as on main: only the peel picks a commit that shares the abbreviation with a blob or a tree. Only after git's exit 1 does the abbreviation get the plain lookup and the batch-check, so an abbreviation of a tag whose commit is missing is still an error. Any other failure of the peel is an error. A full id gets the plain lookup only.
  • A guard refuses revision operators (~, ^, :, @{, .., glob characters and backslash) and control bytes (DEL included) before a name reaches git. None can be in a ref's name. git rejects other malformed names itself. The hex fallback is the only object revision.
  • resolveCommitObject sends three requests to one cat-file --batch-check: the full id, <id>^{commit} and <id>^{}. Its structured answers give the object's type and the two peels. An unknown full id, and a tree, a blob or a tag of one, stay "not found". A ref whose object is missing, a commit that cannot peel to a readable commit, and a broken tag chain are errors. Nothing reads a tag's message or classifies stderr.
  • A broken branch is still an error beside a readable tag of the same name, or an id of the same name. Resolution does not silently fall through to it.
  • restoreTargetTree (fix(api): restore only a commit, and answer 500 for one git cannot read #771) asserts that its target is a full object id, as resolveTree does: cat-file -e exit 1 means "no such object" only for a full id.
  • treeSource.request's comment names close as the other way its write fails (fix(gitread): decline a tree walk whose cat-file ended before the request #774 verifier).
  • create-branch with an unreadable base_ref, and fork with an unreadable source fork point, now answer 500. Before, create-branch fell back to preferred_base (200), and fork answered 400. A base that names no ref still falls back, and a fork point that names no ref is still a 400. The handler comments and the PR body state the distinction.
  • The gitread README and docs/reference/http-api.md state the lookup rules and the 500 for a ref whose commit git cannot read.

Verification

  • TestResolveRefUnreadableCommitIsAnError: a branch, full ref, tag, tag of a tag, and the tag's full id (lower and upper case) and abbreviations (12 and 7 characters), whose commit is missing, are errors. Unknown names, a tree, a tag of a tree or blob, an unknown full id, and the missing commit's own id stay ErrNotFound. A broken branch dup beside a readable tag dup, and a broken branch named like a readable commit's full id, are errors.

  • TestResolveRefManyLooseRefsDoNotAddWork records git's ref debug trace. Adding 20,000 irrelevant loose refs adds no direct ref reads, and the trace records no ref-iterator steps. Round 3 (f3cf0c7) fails with 9 iterator steps. No process environment is recorded.

  • TestResolveRefKeepsGitDWIM: refs/r, r and refs/t resolve refs/heads/refs/r, refs/heads/refs/heads/r and refs/tags/refs/t. All three fail on round 3. TestResolveRefRevisionSyntaxSkipsGit pins that revision operators and control bytes never start git. Round 5 adds a DEL (0x7f) row.

  • TestResolveRefAbbreviationSharedWithABlob (round 5) writes a blob whose id shares c1's first 4 characters. It first checks that git's plain rev-parse --verify --quiet <prefix> exits 1 there, then expects the 4-character prefix, a 12-character abbreviation and the full id to resolve to c1. Round 4 (2e9f9fe) answers ErrNotFound.

  • TestResolveRefOnlyExitOneIsNotFound gains two rows: a peel that exits 128 and a peel whose git is killed are errors, not ErrNotFound.

  • TestResolveRefGitRuns counts git spawns. Every read that finds a commit takes at most main's runs. An abbreviation of a blob, which answers 404, takes one run more than main: the peel, then the plain lookup and the batch-check.

    Read Before this PR Round 3 Round 4 Round 5
    branch 2 2 2 2
    tag name 3 2 3 3
    full id 4 2 4 4
    unknown name 3 1 2 2
    abbreviation of a commit 4 2 4 3
    tree id 4 3 4 4
    abbreviation of a blob 4 3 4 5
  • TestResolveCommitObjectGitFailures: a failed batch-check (exit 128 or a kill), an answer for another object, an incomplete answer, an invalid peel answer, an unreadable commit, and a tag chain that is unreadable or ends at a commit after its commit peel failed, are errors. The last case pins the verifier's surviving mutant (the old cat-file -t check read a commit at the chain end as "not found"). TestResolveRefAmbiguousAbbreviationIsNotFound builds blobs with a shared 4-character prefix and pins ErrNotFound.

  • TestReadEndpointsAnswer500ForARefGitCannotRead (api, PostgreSQL and SQLite): a tag whose commit is in no pack answers 500 on commits (name, full id, abbreviation), commit, files, file, blame and diff. Main's gitread answers 404. create-branch with that base_ref and preferred_base main answers 500 (main's gitread: 200), and with an unknown base, 200.

  • TestRestoreTargetTreeGitFailures and TestRestoreTargetTreeTellsMissingFromBroken: the five injection-test gaps from fix(api): restore only a commit, and answer 500 for one git cannot read #771's verifier, a corrupt loose-object header (500, as on main), and the full-id assertion. TestTreeSourceRequestAfterGitEnded also writes after a heap cap, crash and canceled context: each stays an error (fix(gitread): decline a tree walk whose cat-file ended before the request #774 verifier).

  • The grep cursor's existing failure-injection test now stalls the raw anchor lookup. Its ^{commit} value goes on stdin, not argv. The full SQLite api run caught this stale injection before publication.

  • Five round-5 mutants each fail a test at 3c61289: round 4's ResolveRef (it fails TestResolveRefAbbreviationSharedWithABlob, TestResolveRefGitRuns and TestResolveRefOnlyExitOneIsNotFound), the peel used for full ids too, exit 1 of the peel read as not found without the plain lookup, any other failure of the peel read as not found, and a DEL byte let through to git.

  • Nine round-4 mutants each failed a test: revision operators allowed, control bytes allowed, raw lookup with a peel, a ref's missing object read as not found, no id check on the type answer, no peel-answer validation, an unreadable tag chain read as not found, a tag of a blob treated as unreadable, and a non-commit object treated as an error.

  • At 2e9f9fe (round 4), with -race: build, vet, golangci-lint (0 issues), the doc-link check and both --fingerprints-only checks pass. internal/gitread/... passes. internal/api and cmd/openstorage pass in three runs, two of them with -tags=openstorage_viewcheck. internal/api runs each integration test on PostgreSQL and SQLite (runBackends). Neither package reads OPENSTORAGE_TEST_BACKEND, so the three runs cover the same two backends. No lane ran S3-WAL.

  • At 3c61289 (round 5: a67529e merged with main 3ceb600), with -race -tags=openstorage_viewcheck: build, vet, golangci-lint (0 issues), the doc-link check and both --fingerprints-only checks pass. internal/gitread/... passes, and so do the focused tests (shared prefix, git-run counts, 20,000 loose refs, revision syntax, peel failures). internal/api and cmd/openstorage pass on PostgreSQL and SQLite under the container lock. No lane ran S3-WAL.

  • Two tests at 3c61289 passed only on the rerun. The first gate run there ran at host load 98 to 198. TestLsTreePageCursorOnARootFolderOverHalfTheHeapCap ran out of its 1-minute git budget after 67 s, and internal/api hit its 60-minute test timeout while a 1-second-old subtest ran. This PR changes only a comment in tree_resume.go and does not touch that test. The rerun passed internal/gitread/... in 481 s, and internal/api with cmd/openstorage in 569 s (with a 90-minute timeout).

Size

754 changed lines: 163 of production Go, 585 of tests and 6 of docs.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: xhigh

DRAFT (local): ResolveRef read rev-parse's exit 1 for <ref>^{commit} as
not found, also for an existing branch or tag whose commit object is
missing. Now an existing ref, or a full id, that leads to an unreadable
commit is an error.

Change-Id: I89e91d6f73d8a70ebc6ac7df81837206535eb17f
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…g headers

DRAFT (local): replace the tag-header walk with rev-parse <id>^{} and
cat-file -t, as restore-commit's target check does.

Change-Id: Iaf3b2c4f6922a6396f2da23ac77b5bdc96768809
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…pack

DRAFT (local): the probe as a test. Red on main's gitread (404 on all
seven), green with the fix.

Change-Id: Iae1d9f4f167af5331abf9d8198fc650ecc8f5fd5
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…an error

silent end and an exit. It now also writes after a heap cap, a crash and
a canceled context, so a request that declined any of them fails the
test without a delay overlay. The comment on request's failed write
names close as the other way the write fails.

Change-Id: I9b3e71af0af77592d6f95e5c308fcc54c2a2de7b
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…orrupt header

restoreTargetTree reads cat-file -e exit 1 as "no such object", which
holds only for a full object id: it now asserts one, as resolveTree
does. A restore target whose loose object file is there but whose
header git cannot read (a bad size, a file that is not zlib data, or a
tag of such a commit) makes rev-parse exit 128, and stays an error
(500), as on main.

Change-Id: I7c3db4f8df96b1698254846d5e87d420ae4be0f2
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…error

A fake git fails rev-parse ^{commit} of a tree (exit 128, killed),
cat-file -e (exit 128, killed) and cat-file -t (exit 128, killed), and
makes rev-parse ^{commit} of a readable commit exit 1. Each is an error,
never "not found". Five mutants of restoreTargetTree passed every test
before: a ^{commit} failure that falls through, a cat-file -e failure
read as "not found" or swallowed, a swallowed cat-file -t failure, and
a commit at the end of the chain read as "not found".

Change-Id: Id7adda38285b94b6f473cd7b4c272031f2eed411
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…an error

The same checks as restoreTargetTree's: a fake git fails the read of
the ref's value, cat-file -e (of a ref's object and of a full id),
the peel to the end of the chain, and cat-file -t, and makes
rev-parse ^{commit} of a readable commit exit 1. Each is an error,
never ErrNotFound. Five of these six mutants of commitUnreadable passed
every gitread test before.

Change-Id: Ibba0a2b2de99cab307135aedd92905a239bded35
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: Id4680df8a9d217ce726f253ecc20db9f963e8ded
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-09T18:43:48.439132Z 3c61289 Manual request
🔒 Security Review ✅ Completed 2026-11-08T19:30:36.154039Z 2e9f9fe Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59b04114aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/gitread/gitread.go Resolved
@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 59b04114aa

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Codex round 1 on #782: an abbreviation of a tag whose commit is missing
answered 404, while its full id and its ref name answered 500.
commitUnreadable returned nil for every abbreviation. It now resolves
an abbreviation without a peel, as it reads a ref's value: rev-parse
exits 1 only when no object, or more than one, has it. The one object
it names gets the same checks as a full id.

Change-Id: I66abe1e43b4c24c60690573af90dea8af78c678a
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 63000cfe93

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 63000cfe93

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

… only what exists

The #782 advisor's finding: commitUnreadable ran after every candidate
whose peel exited 1, so a read by full id took 6 git runs (main 4) and
a read by tag 4 (main 3).

ResolveRef now reads every ref candidate that exists, with its value,
in one for-each-ref, and gives rev-parse the ref's object id, never the
name. Each name goes as a glob whose last byte is escaped, so that it
matches the whole name only, not the refs under it. A branch, a tag and
a full id each take 2 git runs. A ref that exists and whose commit git
cannot read is still an error, also when a tag or an id of its name
resolves. check-ref-format is gone: no name reaches rev-parse.

Change-Id: Ia671d6336e234b58effa4a3e457e0459e9b5f7f7
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…specs

Every for-each-ref pattern matches one whole name, so a line for any
other ref, or one that is no "<name> <id>", is now git's failure, not
skipped. A spec with a glob character names no ref, though as a pattern
it would match main.

Change-Id: Ied15ef9c44cb635abe6a9199c8056c4ff0bd4a58
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…and fix the fallback comments

The #782 verifier's counts: an unknown name, an abbreviation, a tree id
and a blob abbreviation took more git runs than on main. commitUnreadable
now reads whether an id names one object, and its type, with one
cat-file --batch-check ("<id> missing", "<id> ambiguous" or
"<oid> <type>"), and peels only a tag. Every read in
TestResolveRefGitRuns takes at most main's runs: a branch, a tag, a full
id and an abbreviation of a commit 2, an unknown name 1, a tree id and an
abbreviation of a blob 3.

create-branch's and fork's comments: a base or fork point that names no
ref falls back or is a 400, and one whose commit git cannot read is a
500.

Change-Id: Ibd7d38c12c8211ad1c50f49a3fc2a7860f007010
Signed-off-by: Thomas Kosiewski <tk@coder.com>
With base_ref naming a tag whose commit is in no pack and preferred_base
main, create-branch answers 500. On main's gitread it fell back to main
and answered 200. A base that names no ref still falls back (200).

Change-Id: I8551165b1cb1ca398c7faea376e83c30e3e62da7
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: f3cf0c7816

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: f3cf0c7816

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

…refs

The #782 round-3 verifier's blocker: for-each-ref reads the loose-ref
directories for every pattern. At 180k refs a read took 5-7 seconds,
and at 540k it exceeded the production memory cap. There is no ref cap.

ResolveRef now reads each candidate with rev-parse --verify --quiet
without a peel, so that a ref's value still resolves when its object is
missing. One cat-file --batch-check reads the full id's type and its
^{commit} and ^{} peels. A missing object for a ref, a broken tag chain,
and a commit git cannot read are errors. Every lookup is direct, no
command lists refs, and git's DWIM rules stay in use (refs/r can resolve
refs/heads/refs/r). A guard refuses revision operators and control
bytes before the name reaches git. Each ordinary read runs git at most
as often as on main.

The 20k-loose-ref test records git's ref debug trace: no iterator steps,
and adding 20k unrelated refs adds no direct reads. It fails on round 3
with 9 iterator steps, and the DWIM test fails on round 3 for all three
names. The tag-chain-end-commit injection from the verifier is pinned
by the batch answer test. The grep cursor test now injects its stalled
git at the raw anchor lookup, because ^{commit} goes on stdin.

Change-Id: Ifb37cb85ef63f07a55320b11bf97ce34cebbe025
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 2e9f9feb69

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 2e9f9feb69

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

A commit that shares its abbreviation with a blob or a tree made the
plain rev-parse say ambiguous (exit 1), and ResolveRef answered 404
where main answered 200. The hex abbreviation is now read as
<abbrev>^{commit} first, which picks the commit. Only on git's exit 1
does it get the plain lookup, so an abbreviation of a tag whose commit
git cannot read is still an error. A DEL byte, like every other control
byte, never reaches git.

Change-Id: I8f64ec8b191b84d7b4ded49ea2fa5173ebde4155
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 3c61289dc9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 3c61289dc9

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant