News

Core's commit verifier accepted unrelated history

Two paths through the script that checks Bitcoin Core's signed commit history reached the success exit without proving anything: a failed git command, and a history that had diverged from the trusted root. The fix makes the script prove the link or refuse.

4 min readBitcoin Core
Core's commit verifier accepted unrelated history

What happened

On 19 August 2026 Bitcoin Core merged pull request 35980, a two-commit change to contrib/verify-commits/verify-commits.py. That script walks the repository's commit history and checks that every commit back to a configured trusted root was signed by a key on a trusted list. Two paths through it reached the same successful exit without establishing anything. The pull request describes the problem in its own words: "a commit that is an ancestor of a configured root is intentionally accepted without checking earlier history. The script also takes this success path after Git errors or for divergent commits, even though neither establishes that relationship." It adds that the issue "was also found and disclosed responsibly by the Red Team".

What it changes

The script answers one question before a developer builds anything: is the history sitting in this clone the history the maintainers signed? Per-commit signatures answer it commit by commit, and an ancestry test decides where to stop, because commits older than the trusted root are deliberately out of scope.

The stopping rule was the hole. The old code asked git whether the trusted root was an ancestor of the commit under test, and read any non-zero exit as "this commit predates the root, stop here", printing a message and exiting successfully. Git returns 1 for "not an ancestor", which covers a commit that genuinely predates the root and equally covers a commit on a history that shares no ancestry with the root at all. Other exit codes mean git could not answer: a missing object, a broken repository. Those took the same branch.

The change routes both questions through two small helpers in the current script. is_ancestor now treats any git exit code other than 0 or 1 as fatal and stops, naming the two commits it could not compare. predates asks the question in both directions and returns true only when the commit is provably an ancestor of the root, as its docstring says: "Return whether commit is provably older than root, rejecting divergent history." When neither direction holds, it reports that the commit "diverges from the trusted Git root" and exits with an error. A verification tool that exits successfully when it could not perform the verification is worse than no tool, because it hands a person a result to rely on. Both cases ship with reproducers.

What it does not change

This is a check on a clone, run by whoever chooses to run it. The tooling's own README is specific about the order: fetch, verify with a trusted copy of the script, then check out, because "you can't use an untrusted script to verify itself". None of that changed, and a person who never runs the script gains nothing from a better one. It is also not what someone downloading a release checks. That is a different artefact with its own signature.

Nothing here says the flaw was used. The pull request supplies reproducers, which demonstrate the script accepting what it should have rejected. Showing that a lock can be picked is not evidence that a house was entered.

And a passing run still means only what it always meant: these commits carry signatures from keys on this repository's trusted list. Whether those keys belong to the people you think they belong to is the supply chain question underneath, and it sits outside the script, as it always did.

Context

Core has spent years making its own failure modes legible rather than quiet. It began publishing fixed vulnerabilities in July 2024, on the reasoning that nobody can be forced to upgrade, so silence just leaves node operators guessing. The BIPs repository added a route for reporting a flaw in a specification earlier this month. This fix points the same instinct at the tooling: what it removes is not a wrong answer but a confident one.

The Red Team credit in the description is the only trace of how the report arrived. The pull request does not say who they are or what else they looked at, and the repository carries no further record of it.

Newsletter

Bitcoin, without the noise

What happened in Bitcoin, what it actually changes, and the sources so you can check us. One issue at a time, straight to your inbox.

  • One email per issue, never a drip campaign
  • No tracking pixels and no shared addresses
  • Unsubscribe from any issue in one click

Get the next issue

One email per issue, no tracking pixels, and unsubscribe from any of them. We do not share your address. Privacy policy