Skip to content

fix(githttp): refuse a branch tip that is not a commit before the ref store takes it - #860

Merged
ThomasK33 merged 11 commits into
mainfrom
fix-branch-tip-must-be-commit
Nov 14, 2026
Merged

ThomasK33 merged 11 commits into
mainfrom
fix-branch-tip-must-be-commit

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Nov 13, 2026 •

Copy link
Copy Markdown
Member

Why

A push command can set a refs/heads/* value to a blob, a tree or an annotated tag. The update hook's CAS stores that value in the ref store, and git then refuses the same write (trying to write non-commit object ... to branch). From then on the store and git disagree. Every read of the repository answers 500, because hydration cannot write the branch, and every later update of the branch loses its CAS. A git:write token is enough, and no ref policy has to be set.

This is a robustness fix: client input leaves the server in a state that it cannot read.

What changed

  • Pre-receive and the update hook read the object types of the new branch values with one git child, and refuse a branch tip that is not a commit with a per-ref ng (update: <ref> must point at a commit, not a blob). The refusal comes before the policy check, so a refused tip also stores no pack, and an --atomic push that carries one lands nothing.
  • Tags and other namespaces still take any object, as in git. A missing object has no kind to refuse: receive-pack refuses a command whose new value it lacks before the update hook runs.
  • The git-protocol and wire-compatibility docs name the rule and its push rules: it takes the same --atomic and whole-push-decline rules as a policy denial.

Scope

  • The REST write routes cannot set such a tip. branches/create resolves its base through ResolveRef, which peels a tag to its commit and answers 404 for a blob or a tree. Merge reads its source and target as branch names. commit-pack, diff-commit, restore-commit and the merge commit write a new commit. One test pins the one route that takes a caller's revision (branches/create).
  • A repository that this bug has already wedged stays wedged. repair-refs removes refs by name and reads no object types, so repairing such a ref is a separate change.

Verification

  • New raw receive-pack e2e TestBranchTipThatIsNoCommitIsRefused (blob, tree and annotated tag as the tip of a created or updated branch, under no-force-push, --atomic, and a mixed push; no pack stored for a refused tip; tags and a fast-forward still land). It fails on origin/main in every row, and passes with the fix on the local, PostgreSQL and S3-WAL lanes.
  • Dogfood on a local osv.sh node with a raw receive-pack client. On origin/main each of four pushes gets ng ... invalid new value provided, ls-remote and clone then answer 500, and serve logs 8 hydrate failures. With the fix each gets ng ... must point at a commit, ls-remote and a fresh clone work, and serve logs none.
  • Fake-git unit tests pin that a failing or malformed type read refuses every branch tip and gives no verdict to tags and deletes, and that the type read keeps GIT_NO_REPLACE_OBJECTS=1. Each fails when its guard is removed.
  • The mixed-push row runs under no-force-push and moves main to a blob, so an update hook that asks the policy before the type fails it.
  • Lint, docs gate, both fingerprint gates and the race lanes of internal/githttp and its importers pass.

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

…re takes it

A command could set a refs/heads/* value to a blob, a tree or an annotated
tag. The update hook's CAS stored it, and git then refused the same write
("trying to write non-commit object ... to branch"). The ref store and git
disagreed from then on: every read of the repository answered 500, and
every later update of the branch lost its CAS.

Pre-receive and the update hook now read the object types of the new values
with one git child and refuse a branch tip that is not a commit, with a
per-ref ng. The refusal comes before the policy check, so a refused tip
also stores no pack. The REST write routes cannot set such a tip, because
each takes its value from a peeled commit or a new commit. Tests pin the
two routes that take a revision from the caller.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_

Change-Id: Ied5fdb6a1554de5dd027fe6997fcca03168e6b35
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…e-commit

Change-Id: I22d9d7c829bb9cf79a078ead469add156207aa4d
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…s nothing

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_

Change-Id: I6700e2f23ee3d0d330fd348c3a45af239950e249
Signed-off-by: Thomas Kosiewski <tk@coder.com>
receive-pack refuses a command whose new value it lacks before the update
hook runs, so the tip check has no kind to refuse for a missing object. The
in-process update hook tests name objects that no repository holds.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_

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

preReceive and updateHook call the new check and it adds four declarations.
None of them changes a push-hold or schedule step that the model describes.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_

Change-Id: Ib8e639b85241f97157ff16ed8a3617e8d6dbdab0
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…ntee for branch tips

Drop the merge row that checked nothing (a merge source is a branch name, so any
40-hex id answers 404). Add a fake-git unit test for the fail-closed type read and
put objectKinds in the no-replace-objects test. Name the push rules of the refusal
in the docs.

Signed-off-by: Thomas Kosiewski <tk@coder.com>

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_

Change-Id: I154ffc8eab7c86163ec50915a511622c5e64dc3f
…e-commit

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

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_

Change-Id: I2e9ea0f1a0aae59bf212e4c0de7ff857293d86ba
@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 13, 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-14T01:16:49.994958Z ac25558 Manual request
🔒 Security Review ✅ Completed 2026-11-14T01:18:15.706917Z ac25558 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.

…he policy

The mixed push now runs under no-force-push and moves main to a blob. A hook
that asked the policy first would walk the ancestry of a blob.

Signed-off-by: Thomas Kosiewski <tk@coder.com>

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_

Change-Id: I47c86578bd237bb852d97514157ee627cf0fa44b
@chatgpt-codex-connector

Copy link
Copy Markdown

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

Reviewed commit: ff3d152750

ℹ️ 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: ff3d152750

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.

@ThomasK33
ThomasK33 added this pull request to the merge queue Nov 14, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Nov 14, 2026
…e-commit

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

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_

Change-Id: Ica3d5fee2a5e0c5f3c4c854f3524dfbd136c0af3
@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: ac25558e07

ℹ️ 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: ac25558e07

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.

@ThomasK33
ThomasK33 added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit c76fedf Nov 14, 2026
11 checks passed
@ThomasK33
ThomasK33 deleted the fix-branch-tip-must-be-commit branch November 14, 2026 01:58
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