fix(proof): refuse empty-artefact stubs on the topic-VM path; real artefact in the wire check (Architecte follow-up 3/4) - #250
Conversation
Staging's happy path went green on the digest of an empty tree: the RLM VM could not reach the artefact host (ufw/docker FORWARD drop, server in a container netns) and its guest agent substituted an empty artefact. Prod must never rely on that. KVM host: check_request now walks artifact_tar - an uncompressed tar with at least one byte of file content - and refuses gzip / non-tar / empty archives / trees of empty files by name before any sister jail (proof_fc_host::artefact). CP: an artifact_digest that is the sha256 of nothing (zero bytes, empty tar) is a 400 at submit, no row. Agent: at VM boot, name foreign forward chains with policy drop (ufw, docker) that block guest egress regardless of the allowlist. Harness: a live submit-probe needs --artifact-file / --artifact-digest of the real bytes, refuses loopback / .invalid URIs, fetches and re-hashes the URI before posting, and checks the row echoes that digest; matrix gains the digest-of-nothing row. Runbook: real-artefact happy path, Egress section (ufw / DOCKER-USER pfc+ accepts), the fetch-fallback stub documented as staging-only history and forbidden in the production image.
… crate The tar shape check is the guest contract for SisterRequest.artifact_tar, so it lives beside it (proof_vm_proto::tar, with test-fixtures for sibling crates); proof-fc-host calls it and stays under the LOC cap. The submit e2e probe for a missing topic id now sends a real artefact digest (the digest of nothing is its own 400).
|
@greptileai please review empty-artefact stub refusal (Architecte follow-up 3/4) after rebase onto main post-#252. |
|
@greptileai review again — previous Greptile check still pending after CI green. |
Greptile SummaryThe follow-up retains the archive identity contract: the guest and host verify the exact uncompressed archive bytes served at the artifact URI against the submitted digest, rejecting substitutions and re-encoded archives. The networking advisory now recognizes TAP ingress accept rules in the affected nftables table, allowing the documented remediation to clear the advisory. Confidence Score: 5/5Safe to merge; no outstanding blocking issues remain. There are no accepted new findings and no outstanding previous findings. The archive-bytes finding is fixed: the host verifies verbatim archive bytes against the paid digest before it creates a sister jail. The forwarding-advisory finding is fixed: the check recognizes a TAP ingress accept rule in the table that contains the dropping forward chain. Reviews (3): Last reviewed commit: "test(proof-fc-host): a failed fetch is a..." | Re-trigger Greptile |
…act-no-empty-stub Co-authored-by: Mathis <echobt@users.noreply.github.com>
Greptile P1 on #250: the guest contract let the RLM re-tar the fetched tree while the host hashed the raw bytes against the paid digest, so a valid artefact could be refused (tar metadata / member order change under re-encoding). One identity now, end to end: artifact_digest is the sha256 of the file served at artifact_uri; the guest verifies the bytes as received and forwards them verbatim; the host runs the same function (proof_vm_proto::tar::verify_artifact - shape, then digest) before any sister jail, with a message that names the re-tar cause. Documented in the guest contract, the runner request, PROOF.md, the miner docs and the runbook. Greptile P2: the forward-policy advisory now reads nft -j list ruleset and stays quiet for a dropping table that already accepts iifname pfc* (the documented ufw / DOCKER-USER fix leaves the policy at drop), so it clears once the fix is in; wording and runbook check updated, connectivity proof stays with the happy path.
…ct lock Architecte lock on #250: the guest forwards the exact archive bytes (never an empty or substitute tree, never a re-tar) and a fetch that fails or does not verify is RlmToHost::Failed. Pin the host half with a stand-in guest that answers its Evaluate job Failed: run_job yields HvError::Guest with the guest's reason, no output, no sister jail, the sister listener is released, and the topic VM stays alive (agent 502 backend -> CP 503, no row). State the lock in AGENTS.md item 7, PROOF.md's isolation boundary and the runbook's locked-by-design table.
|
@greptileai please re-review after archive-bytes P1 fix (42c62d6). |
* deploy: staging pins for 4252130 images.yml built GHCR digests on main then could not push the pin commit (GH013: PRs required + Greptile). Land the same promote via PR. Co-authored-by: Mathis <echobt@users.noreply.github.com> * fix(deploy): keep full prior staging release in previous Sequential promote.sh calls were snapshotting previous per service, so rollback of 4252130 would restore a mixed pin set. Capture the 1dd07f7 release once and write it back after the promote loop. Co-authored-by: Mathis <echobt@users.noreply.github.com> * deploy: staging pins for f013abb Retarget #245: main moved to #246. Digests from images run 34281980177. previous stays the full 1dd07f7 staging release. Co-authored-by: Mathis <echobt@users.noreply.github.com> * deploy: staging pins for 890ce5b Retarget #245: main moved to #247. Digests from images run 34286333214. previous stays the full 1dd07f7 staging release. Co-authored-by: Mathis <echobt@users.noreply.github.com> * deploy: staging pins for c0ce094 Retarget #245: main moved through #248/#252. Digests from images run 34297422117. previous stays the full 1dd07f7 staging release. Co-authored-by: Mathis <echobt@users.noreply.github.com> * deploy: staging pins for 85ab2b7 Retarget #245: main moved to #251. Digests from images run 34299348722. previous stays the full 1dd07f7 staging release. Co-authored-by: Mathis <echobt@users.noreply.github.com> * deploy: staging pins for e30d51d Retarget #245: main moved to #250. Digests from images run 34302039784. previous stays the full 1dd07f7 staging release. Co-authored-by: Mathis <echobt@users.noreply.github.com> * deploy: staging pins for a8b9018 Retarget #245: main moved to #253. Digests from images run 34373475123. previous stays the full 1dd07f7 staging release. Co-authored-by: Mathis <echobt@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Mathis <echobt@users.noreply.github.com>
Summary
Architecte / Owner follow-up 3 of 4 (empty-digest stub out). Based on
main(merged with #248 / #252 / #251).What happened on staging. The happy path went green on the digest of an empty tree: the RLM VM could not reach the artefact host (the host's ufw / Docker
FORWARDchain drops by default whatever the per-VM allowlist says; the server sat in a container netns / on loopback the VM never sees), and the guest image substituted an empty artefact whose digest matched. Nothing in the repo could tell. Prod must never rely on that stub, so this PR makes it unable to pass at every layer and documents it as staging-only history.Architecte lock (added in
42c62d65, on top of the P1 fix): the guest forwards the exact archive bytes it fetched — never an empty or substitute tree that still hashes, never a re-tar (exact bytes preferred over inventing a tree-digest scheme; exact bytes are possible here, so that is the contract); a fetch that fails or does not verify isRlmToHost::Failed(hostHvError::Guest→ agent 502backend→ CP 503, no row); the empty-digest stub stays refused (CP 400 on the digest of nothing, host refusal of content-less tars). Written intoAGENTS.mditem 7,docs/PROOF.md§ Isolation boundary and the runbook's locked-by-design table; the host half is pinned bya_guest_whose_fetch_failed_answers_failed_and_nothing_is_scored(stand-in guest answers itsEvaluatejobFailed: no output, no sister jail, sister listener released, topic VM stays alive).KVM host (
proof-fc-host+proof-vm-proto)9e99e381): the served file's bytes, verbatim.artifact_digest= sha256 of the exact file served atartifact_uri(an uncompressed tar of the recipe tree). The RLM guest fetches it, runsproof_vm_proto::tar::verify_artifacton the bytes as received against the job's digest, inspects a copy, and forwards those same bytes asSisterRequest.artifact_tar— it never re-tars (tar metadata / member order change under re-encoding, so a rebuilt tar hashes differently). The KVM host runs the same function insister::check_requestbefore any jail: shape (uncompressed ustar / GNU / pax with ≥ 1 byte of file content; ~110 LOC walker: checksum-verified headers, GNU base-256 sizes,L/K/x/gmeta entries skipped), then digest. gzip, non-tar bytes, an empty archive, a tree of empty files, or bytes that do not hash to the paid digest are refused by name (artifact_tar hashes to …: the guest must forward the bytes it fetched from artifact_uri verbatim (a re-tarred tree never matches) …), logged assister request refused: …, and the run comes back without an attestation → CP 503 forfirecracker_required, no row. A digest that matches an empty tree is a guest whose fetch failed, not a miner's work. Test: the same files re-tarred (other order / other mtime) keep identical content and are refused; verbatim bytes verify.SisterRequest,artifact_tar,HostToMiner::Run,tarmodule,CustomRunRequest.artifact_uri): a failed or non-verifying fetch isRlmToHost::Failed, never a substitute, never a re-tar.nft -j list rulesetand logs an advisory when a foreign forward chain haspolicy drop(ufw's / Docker'sip filter FORWARD, never ourproof_vm_*tables) and its table has no rule acceptingiifname pfc*— the documented ufw /DOCKER-USERfixes leave the policy atdropand add exactly that accept, so the line clears on the next VM boot once the fix is in. Never fails the boot; the runbook says connectivity is proven by § 5, not by the advisory.Control plane (
proof-http) —POST /v1/submissionsanswers 400 (no row) to anartifact_digestthat is the sha256 of nothing (zero bytes, or the 10240-zero-byte empty tar):artifact_digest is the sha256 of empty input (or of an empty tar archive): hash the recipe bytes you ship at artifact_uri. Miner docs table +AGENTS.mditem 5 +docs/PROOF.mdupdated.Harness (
proof-vm-wire-check.sh submit-probe) — a live run (--expect 2xx) can no longer start on a random digest. New--artifact-file F(sha256 computed here; must be an uncompressed tar with content — pythontarfileinr:mode),--artifact-digest HEX,--no-fetch-check. Before posting it refuses the digest of nothing, empty / gzip files, loopback /.invalidURIs (the VM never sees the host's loopback), fetches--artifact-urifrom the CP host and re-hashes it (mismatch or fetch failure = FAIL, no POST), and after a 2xx checks the row echoes the real digest. Fail-closed probes keep the random digest;matrixgains the digest-of-nothing 400 row.Runbook — § 5 rewritten around a real artefact (
tar -cf recipe.tar …,sha256sum, serve it on an allowlisted address,--artifact-file); new § Egress with the ufw (ufw-before-forward -i pfc+ -j ACCEPTinbefore.rules) and Docker (DOCKER-USER) accepts and how to verify; the fetch-fallback stub documented as staging-only and forbidden in the production RLM image; sign-off gains the real-digest + no-forward-advisory checks; env examples note the forward-policy trap.No digest is invented anywhere (the two "digest of nothing" values are computed at runtime from zero bytes). No change to what a correct guest sends.
How to verify
Verified here: harness refusals (no digest / digest of nothing / empty tar / gzip / loopback URI /
.invalid) exit 2 before any request; a local http server serving the wrong bytes → FAIL before the POST, the right bytes → PASS then POST; the proof-http test proves 400 + no row and that a real digest still scores on the same host.Greptile
9e99e381; Architecte lock →42c62d65; threads answered and resolved)@greptileai reviewTest plan
cargo test --workspace(isolated target dir)cargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warnings,cargo deny check, xtaskloc-cap(proof-fc-host 1467 / proof-vm-proto 593) /consensus-lint/spec-check/design-check/external-docs-checkRisk
docs/external-miner/proof.md; no real recipe is affected.artifact_taris now refused (503, no row) instead of being run. The host always hashed the raw bytes against the paid digest, so a re-tarring guest was already broken silently; the contract now says so (forward the fetched file verbatim) and the refusal names the reason in the agent journal and theSisterAnswer::Refusederror. Guest images built againstproof-vm-protoshould calltar::verify_artifacton the fetched bytes.Naming
I did not rename
BASE_*environment variables, deployed host paths(
/opt/base,/run/base, …), GHCRbaseintelligence/basepackage names, orbase-*-v1cryptographic domain tags, unless this PR’s purpose is a coordinatedcutover documented in
docs/NAMING.md.