Skip to content

v0.43.0 review fixes: varve env injection, realmless dispatch, and four requirement gaps - #278

Merged
avrabe merged 9 commits into
mainfrom
fix/v043-review-gaps
Oct 21, 2026
Merged

avrabe merged 9 commits into
mainfrom
fix/v043-review-gaps

Conversation

@avrabe

@avrabe avrabe commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Why

The independent clean-room review of the v0.43.0 scope dissented on all six requirements. This PR fixes what it found. Every fix has a test that I watched fail on the reported behaviour first.

Security

  • The varve env injection (REQ-ENVQUOTE-001), reproduced.
    • Attack: a cloned repository's varve.toml with [[export]] out = "sdk\ntouch PWNED\n#" made varve env print touch PWNED on a line of its own, and eval "$(varve env)" created the file. The store root and script path were quoted, but the comment line naming the export was not.
    • Fix: a destination containing a control character is now refused at parse time, and the comment escapes the value anyway.
    • Test: the new end-to-end test evals the output in sh. Before the fix it created the canary file.
  • A realmless pin with no trust root now dispatches nothing (REQ-NOPINNOTOOL-001), reproduced.

Requirement gaps

  • REQ-COMPOSEWINDOW-001
    • Clauses 1–3, lost from the description at merge, are restored.
    • Eight end-to-end support-horizon tests run against a loopback registry serving a signed include. With &windows replaced by &[], five of them fail; before this PR every test passed.
  • REQ-ADVISORYWATCH-001
    • Entries the watcher cannot check (no version or repo, included layers, non-tool kinds) are reported as unchecked with a reason, not silently dropped.
    • A line made only of such entries no longer reads "nothing new".
  • REQ-SBOMFIELDS-001
    • Supplier: stays the signed repository's owner. The proof signer is recorded as evidence, and when its GitHub owner differs from the repo's, a property says so. A reusable workflow such as slsa-github-generator signs releases it did not supply.
    • Author: named by the fingerprint of the root that verified the layer, not by the local realm alias. That makes the same signed manifest produce byte-identical SBOMs everywhere (clause 7).
  • REQ-MUTANTDEBT-001
    • A shard whose tested count differs from its planned count is INCOMPLETE.
    • The stated rate is corrected to the measured ~4.75 mutants/min.
    • VER-MUTANTDEBT-001 records the dispatch run.

Evidence (local, combined branch)

  • Tests: 1488 workspace tests, 0 failed.
  • Gates: fmt, clippy -D warnings, rivet validate, verification-evidence, trace-gate, claim-check, tracepack-check, proof-check, the tool unit tests, cargo deny, no-key-on-disk.
  • Mutation testing, unfiltered, 0 missed:
    • pin.rs: 77 tested, 68 caught, 9 unviable.
    • advisorywatch.rs, verify.rs and sbom.rs: 3 survivors on PinnedKeyVerifier::public_key were killed by a new test. verify.rs was re-run: 27 tested, 26 caught.
  • Shard sizes: advisories goes from 382 to 390 mutants and trust from 231 to 234, both within the 90-minute cap.

🤖 Generated with Claude Code

https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu

avrabe and others added 9 commits October 21, 2026 11:15
…asure the cap end to end

The description lost clauses (1)-(3) when it merged; only clause 4 remained.
Restored verbatim.

The two CLI tests reached support_horizon before any include was read, so
replacing &windows with &[] at the cap_by_includes call passed every test.
Add tests that run the binary against a loopback OCI registry serving a signed
include and a signed line-status: the cap (clause 1), an include with no
line-status / a status stating no window / an unverifiable realm root refusing
(clause 2), and --past-includes exceeding it with the reason on stderr and a
blank reason refused (clause 3). With &[] in place, 5 of them fail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…eck is reported, not dropped

payload_from_entry silently dropped entries with no tool version or no source
repository, and included layers and non-tool kinds never reached the report,
so a line made of them read 'nothing new' over bytes nobody had looked at.

classify_entry now returns the reason. Each such entry lands in the line's
unchecked list: tool entries and included layers one by one with the missing
annotation named, non-tool kinds once per kind with a distinct count. The
summary wording is 'N item(s) NOT checked' since the list no longer holds only
embedded-component gaps. What the watcher fetches is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…s a function of the signed manifest alone

Clause 1: sbom.rs never read eu.pulseengine.source.proof-signer. A component's
supplier is now the organisation of the signer where it is a GitHub identity,
else the repository owner; the signer is also recorded verbatim as a component
property so a reader sees what the supplier came from.

Clause 7: the author came from the caller's local realm alias (varve-realms.toml
label, or a placeholder outside a project), so one signed manifest emitted
different bytes in different projects. emit() now takes the root public key that
verified the envelope (PinnedKeyVerifier::public_key) and names the author by
its fingerprint, the one fact about 'the realm that signed this' the signed data
states. The alias is no longer in the document.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…an gave it

report took a cargo-mutants summary line as proof a shard was measured, so a log
reading '12 mutants tested' for a shard planned at 314 was MEASURED with exit 0.
N mutants tested counts every mutant run (unviable and timed out included) and
the planned count is cargo mutants --list on the same commit, so they must be
equal: fewer or more is INCOMPLETE, with both numbers shown, and adds nothing to
the measured totals.

The plan's stated rate is corrected from ~7 to ~4.75 mutants/minute, measured by
workflow_dispatch run 39541389293 (939 mutants; shards of 314/300/298/27 took
66/61/59/7 minutes). The 300 target stays: 63 minutes expected, 66 measured at
314, against the 90-minute cap. VER-MUTANTDEBT-001 now records that run, that it
predates this comparison, and that no scheduled run has completed yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…pplier

A Sigstore identity is the job_workflow_ref; for a reusable workflow it names a
different organisation from the one that supplied the bytes
(slsa-framework/slsa-github-generator signs for every caller). Letting it decide
the supplier would misattribute it. The supplier is the signed repository's
owner again. The signer stays on the record as a property, and where it is a
GitHub identity of another owner a supplier-signer-differs property says so.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
Found by the v0.43.0 clean-room review and reproduced: a cloned repo's
varve.toml with `out = "sdk\ntouch PWNED\n#"` made `varve env` print
`touch PWNED` on its own line, and `eval "$(varve env)"` ran it. The
root and script path were quoted (REQ-ENVQUOTE-001); the comment naming
the export was not, and a newline needs no quote to end a comment.

A destination with a control character is now refused at parse, and the
comment escapes the value anyway. The end-to-end test evals the output
in sh and failed on the live attack before the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
Found by the v0.43.0 clean-room review and reproduced: with a pin naming
no realm and VARVE_TRUST_ROOT unset, status refused while which printed a
store path and run executed it, #198's two answers to one question.
which and run_tool (so the shims too) now go through dispatch_ctx and
refuse with status's reason.

REQ-NOPINNOTOOL-001 clause 2 is reworded to that property and the change
is recorded on the artifact: as written it forbade the store every
realmless pin installs into. list's note now names VARVE_TRUST_ROOT as
what vouches, and its test checks the layer is still on disk. Tampering
after install affects every pin and is tracked in #277.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
… a root

Second v0.43.0 clean-room review, both reproduced:
- dispatch_ctx only checked VARVE_TRUST_ROOT parsed, so any key let
  which/run dispatch a layer verify refused. A realmless pin now checks
  the retained envelope against that root (steps 1-2 of verify_installed,
  shared as verify_retained_envelope: one signature, one small read).
  shim install goes through the same check.
- shim install printed `. "<root>/env"` for the login config, where
  $(...) in VARVE_ROOT ran; it is now quoted literally.

The realmless CLI fixture signs what it lays down, as install would.
REQ-NOPINNOTOOL-001 clauses 2-3 and REQ-SBOMFIELDS-001 clauses 1-2 are
amended to what the code does, with the reasons recorded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
@avrabe
avrabe merged commit 35592ae into main Oct 21, 2026
36 checks passed
@avrabe
avrabe deleted the fix/v043-review-gaps branch October 21, 2026 16:50
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