Skip to content

trace.py accepts a gate verdict from ANY commit as run-status evidence — the third member of its own stated triple is unchecked #1413

Description

@dc0sk

Found by relying on it: I added an enforced requirement with a fresh // VERIFIES: binding, ran scripts/trace.sh check, and got TRACE: PASS on evidence from a gate verdict taken at a different commit — one that predated the binding.

The defect

scripts/lib/trace.py:406-433 selects the evidence log. Its own comment states the rule:

A verdict is attributable to (tree, HEAD, TOOLCHAIN).

It then checks two of the three: result == "INVALID" (tree/HEAD moved during the run) and a toolchain mismatch. It never compares the verdict's recorded commit against HEAD.

Sabotage-verified, with a control that the surrounding machinery does fire:

control  : verdict commit 0e42ea2f == HEAD 0e42ea2f      -> TRACE: PASS   rc=0
sabotage : verdict commit := deadbeefdeadbeef…           -> TRACE: PASS   rc=0   <-- unchecked
control 2: verdict toolchain := "rustc 0.0.0 (not-real)" -> NOTE "…a verdict does not survive a
                                                             toolchain change; run a full gate"

Scope — narrower than it first looks, and worth stating precisely

path behaviour
inside scripts/gate.sh exports GATE_LOG="$LOG" (gate.sh:203), so it reads the live log — attributable by construction. Not affected.
CI's traceability job has no target/, so: NOTE: no gate log in target/ — run-status of enforced bindings unverified. Honest, but it means CI never verifies run-status. Not a regression; pre-existing and declared.
standalone scripts/trace.sh check — what a developer runs falls back to target/gate-verdict.json and accepts it regardless of commit

So this is a local hole. The failure it permits: add a // VERIFIES: binding, do not run the gate, and the checker confirms the cited test "ran and passed" from a verdict taken before the binding existed. Sharper variant — rename a test and bind to the old name: the stale log still records the old name passing, so the binding validates against a test that no longer exists.

Why it matters despite being local

The standalone path is the one used while authoring a binding, which is exactly when the evidence claim is being made for the first time. It is also the same class the file already guards against for toolchain — feedback_gate_verdict_bound_to_toolchain records that a distro upgrade re-derives a verdict "with no act by anyone". A commit change is an act by someone, which arguably makes it the easier of the two to check.

Proposed fix

Mirror the existing toolchain branch: compare v["commit"] against git rev-parse HEAD and, on mismatch, return the same (None, reason) degradation — "the last gate verdict was taken at , and HEAD is ; run a full gate" — so it becomes the familiar NOTE plus unverified, not a silent pass and not a hard failure.

Deliberately not proposing a hard failure: the toolchain case degrades rather than fails, and matching that keeps one behaviour for "the evidence is not attributable". A stricter choice would also make every ordinary edit-then-check cycle noisy.

The sabotage above is the self-test: a bogus commit must produce the NOTE, and the restored verdict must pass.

What I did about it meanwhile

Ran a full gate on the exact commit (GATE: PASS 0e42ea2f… clean) so #1412's new enforced binding rests on attributable evidence rather than on this gap.

Related: #1151 (the INVALID verdict check), #1240 (evidence-self-test), #1412.

🤖 Generated with Claude Code

https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions