Repository navigation
ship the Pixhawk 6X-RT Renode model as a signed, reproducible release asset (jess#299) - #302
Merged
Merged
Conversation
… asset (jess#299) So varve layers can distribute jess's RT1176 model. Items 2-5 of #299; item 1 (the licence) lands in #300, as Apache-2.0 only per the maintainer decision recorded on the issue. 2. $ORIGIN-RELATIVE PATHS — measured, not taken from the docs alone. Renode 1.16.1, the version CI pins: .resc form cwd result @hardware/renode/... (old) / NO-BOOT "Could not find file" @$ORIGIN/... / NO-BOOT read as a literal path $ORIGIN/... / BOOT-OK $ORIGIN/... repo root BOOT-OK The second row is the trap: Renode's docs say "Do not use @ at the beginning of an $ORIGIN-based path", and keeping the @ breaks it. 3. hardware/renode/MODEL.files — the committed allow-list, exactly #299's list. The only README in scope is vehicle/README.md; there is no top-level model README. LICENSE and a generated renode-model.toml are added by the packer, which refuses any listed file that is absent or untracked. 4. RELEASE: jess-renode-rt1176-<ver>.tar.gz lands in release-assets before the SBOM and SHA256SUMS steps, so SLSA (release-assets/*.tar.gz), checksums and cosign cover it unchanged. The release does not proceed unless the model, unpacked fresh and run from cwd /, booted on that exact commit (see below for where that runs, and why not here). * PYTHON, NOT tar. #299's flags are GNU tar; macOS ships bsdtar with different flags, so a tar packer would be deterministic only where it could not be tested. tarfile+gzip give the same properties everywhere. Determinism tested BOTH ways: identical despite changed checkout mtimes, a chmod 755, and cwd /; different for SOURCE_DATE_EPOCH+1s and a changed source commit. * THE CHECK RUNS THE .resc AS WELL AS THE ROBOT. The robot suite uses ${CURDIR}, so it passes even with the old repo-root .resc paths — measured: self-test NC1 (old paths) fails at the .resc check, which means it PASSED the robot check. The robot alone could not see the defect #299 fixes. * renode-test WRITES ITS RESULTS INTO THE CWD. A literal `cd / && renode-test` dies with "Read-only file system" — on macOS, and for the non-root `runner` user on GitHub. That would fail the release for a reason unrelated to the model. cwd stays / (so the MODEL cannot resolve against a checkout) and results go to a temp dir via -r. * "COULD NOT RUN" IS NOT "FAILED". Twice the first version of verify.sh reported an environment fault (missing robotframework; the unwritable results file) as the model failing, exit 1. Both now exit 2. * Exact member set, not membership: an extra unlisted file fails. * Controls: 1 positive, 4 negative (old .resc paths, a listed file missing, an extra file, LICENSE missing). The same pack + verify now runs on EVERY PR in the renode-smoke job, packed twice and required byte-identical — a release-only step's first run would otherwise be a real tag. NOTHING IN THE RELEASE RUN EXECUTES THIRD-PARTY CODE. Booting the model needs Renode and renode-test's pip dependencies. Two security reviews of drafts of this commit (neither was pushed) found why that cannot live in release.yml: 1. inside `publish`, it would run beside the OIDC token that mints the cosign signature and SLSA attestations; 2. even in a separate job with `contents: read`, a run's ARTIFACT STORE is shared by every job in it regardless of `permissions:` — a compromised dependency could upload an artifact `publish` would sweep into release-assets and sign. So release.yml runs no Renode, no pip, no renode-test at all. Instead: - ci.yml already packs and boots the model on every commit to main, in its OWN run. A read-only `model-gate` job (contents+actions: read) asks GitHub whether that step succeeded on EXACTLY the tagged commit (tools/renode-model/ci-gate.sh): it requires the named step, not just a green run, so a commit whose ci.yml predates the step cannot pass. Exit 2 if CI has not finished on that commit yet. Controls against REAL runs: current main (ci.yml predates the step) -> FAIL "job ran WITHOUT the step"; a sha with no runs -> CANNOT DETERMINE. (The first version aborted on `$STEP_PREFIX…` — bash read the ellipsis into the variable name — and that abort also exited 1, i.e. looked like a verdict. Braced, and re-run.) - `publish` builds the archive itself with pack.py, Python stdlib + git only. Reproducibility (tested every PR by packing twice) makes these the bytes ci.yml booted, without importing anything from that run. - Flatten is an EXACT SET: exactly the four with-device packages, or the release fails before copying anything. Controls on the rendered step: exact -> 4 assets; an injected pkg-evil tarball -> rc=1, 0 copied; one missing -> rc=1, 0 copied. - The Renode tarball is pinned by sha256 at both ci.yml download sites. Renode publishes NO checksums or signatures, so the pin (1a532d4b…, two agreeing fetches) is TRUST-ON-FIRST-USE: it stops a later substitution, it does not prove the original upload genuine. - pip is not hash-locked: it now runs only in ci.yml, whose artifacts the release never consumes. Its worst case is a false "boots OK". Pre-existing and NOT changed here, same class as finding 1: `publish` runs `cargo install --locked cargo-cyclonedx` with the OIDC token in scope. The post-publish consumer gate covered only with-device-*; it now asserts the model is in the signed manifest and verifies its attestation. Its boot is not repeated there: it was proven on these bytes before upload and `sha256sum -c` proves the published bytes are those bytes. 5. NO --clobber ON A PUBLISHED TAG. A draft may be re-uploaded; a published release now uploads without --clobber, so replacing an asset consumers may have pinned fails the job instead of silently changing a tag's bytes. ALSO: two lines in artifacts.pins said "Renode is not installed on this host". It is, at ~/renode-1.16.1, just not on PATH — `command -v renode` was the wrong test. Corrected in place with the correction visible; the results those lines accompanied stand (QEMU was a validated stand-in, CI confirmed on Renode). All of the above was run locally on Renode 1.16.1 before pushing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017EKh2onUHpWSUiAEqWxfy2
avrabe
force-pushed
the
feat/renode-model-asset
branch
from
October 27, 2026 23:03
284a5c4 to
84d0d94
Compare
avrabe
added a commit
that referenced
this pull request
Oct 28, 2026
…runtimes (jess#299) Finishes jess#299 against the maintainer's "no Python" decision, which #302 had merged without meeting: deploying the model needs no host Python, so the self-test must not rely on renode-test or Robot Framework. hardware/renode/rt1176-smoke-check.resc — run by plain `renode --console --disable-xwt` — asserts `JESS-RT1176 boot OK` on the UART itself. Measured in CI on BOTH Renode runtimes, run 40639170685: boot line line missing load error .NET exit 0 exit 3 exit 4 — the .resc itself exits non-zero Mono exit 0 exit 3 exit 0 WITHOUT the "SMOKE PASS" line — rejected by the pass contract; documented limitation, accepted on #299 No host Python on the verdict path, shown with PATH pointed at an empty directory: still exit 0 and SMOKE PASS. THE PASS CONTRACT IS exit 0 AND "SMOKE PASS: JESS-RT1176 boot OK". Exit 0 alone is not a pass: Renode given a script path that does not exist exits 0 in ~3 s without loading anything. Stated in the .resc, in the archive's renode-model.toml, and enforced by verify.sh. Every line of the .resc is a measured fix — exiting from a UART hook deadlocks; Environment.Exit hangs once a hook has fired; Process.Kill() on itself does not work; an armed watchdog makes quit and Exit hang; a failed command ends the script either by exiting 0 (non-interactive stdin) or by idling forever (a terminal), so it needs both a ProcessExit guard and a 120 s watchdog. On Mono nothing raised from ProcessExit changes the code; the branch that tried was measured ineffective and removed rather than left claiming to work. verify.sh detects the runtime from the files beside the binary (libcoreclr = .NET, libmono = Mono; .NET also ships libMono.Unix.so, so libcoreclr is tested first) and grants the Mono allowance only to a detected Mono, so .NET regressing to the Mono symptom fails. 6 controls per runtime. Also measured: RENODE DELETES /tmp/renode-* ON LINUX. The .NET install vanished after the Mono runs; a canary planted at /tmp/renode-canary-jess was deleted in 2 of 2 runs, with no other actor touching it. Not reproducible on macOS, where .NET's temp path is $TMPDIR under /var/folders. The .NET Renode now installs outside /tmp; /tmp/renode (no dash) is unaffected. Closes #299.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
So varve layers can distribute jess's RT1176 model. Items 2-5 of #299; item 1
(the licence) lands in #300, as Apache-2.0 only per the maintainer decision
recorded on the issue.
$ORIGIN-RELATIVE PATHS — measured, not taken from the docs alone. Renode
1.16.1, the version CI pins:
The second row is the trap: Renode's docs say "Do not use @ at the
beginning of an $ORIGIN-based path", and keeping the @ breaks it.
hardware/renode/MODEL.files — the committed allow-list, exactly Ship the Pixhawk 6X-RT Renode model as a signed release asset (licence, $ORIGIN paths, MODEL.files) #299's
list. The only README in scope is vehicle/README.md; there is no top-level
model README. LICENSE and a generated renode-model.toml are added by the
packer, which refuses any listed file that is absent or untracked.
RELEASE: jess-renode-rt1176-.tar.gz lands in release-assets before
the SBOM and SHA256SUMS steps, so SLSA (release-assets/*.tar.gz),
checksums and cosign cover it unchanged. The release does not proceed
unless the model, unpacked fresh and run from cwd /, booted on that exact
commit (see below for where that runs, and why not here).
different flags, so a tar packer would be deterministic only where it
could not be tested. tarfile+gzip give the same properties everywhere.
Determinism tested BOTH ways: identical despite changed checkout mtimes,
a chmod 755, and cwd /; different for SOURCE_DATE_EPOCH+1s and a changed
source commit.
${CURDIR}, so it passes even with the old repo-root .resc paths —
measured: self-test NC1 (old paths) fails at the .resc check, which means
it PASSED the robot check. The robot alone could not see the defect Ship the Pixhawk 6X-RT Renode model as a signed release asset (licence, $ORIGIN paths, MODEL.files) #299
fixes.
cd / && renode-testdies with "Read-only file system" — on macOS, and for thenon-root
runneruser on GitHub. That would fail the release for areason unrelated to the model. cwd stays / (so the MODEL cannot resolve
against a checkout) and results go to a temp dir via -r.
reported an environment fault (missing robotframework; the unwritable
results file) as the model failing, exit 1. Both now exit 2.
missing, an extra file, LICENSE missing).
The same pack + verify now runs on EVERY PR in the renode-smoke job,
packed twice and required byte-identical — a release-only step's first run
would otherwise be a real tag.
NOTHING IN THE RELEASE RUN EXECUTES THIRD-PARTY CODE. Booting the model
needs Renode and renode-test's pip dependencies. Two security reviews of
drafts of this commit (neither was pushed) found why that cannot live in
release.yml:
publish, it would run beside the OIDC token that mints thecosign signature and SLSA attestations;
contents: read, a run's ARTIFACT STOREis shared by every job in it regardless of
permissions:— acompromised dependency could upload an artifact
publishwould sweepinto release-assets and sign.
So release.yml runs no Renode, no pip, no renode-test at all. Instead:
its OWN run. A read-only
model-gatejob (contents+actions: read)asks GitHub whether that step succeeded on EXACTLY the tagged commit
(tools/renode-model/ci-gate.sh): it requires the named step, not just
a green run, so a commit whose ci.yml predates the step cannot pass.
Exit 2 if CI has not finished on that commit yet.
Controls against REAL runs: current main (ci.yml predates the step) ->
FAIL "job ran WITHOUT the step"; a sha with no runs -> CANNOT
DETERMINE. (The first version aborted on
$STEP_PREFIX…— bash readthe ellipsis into the variable name — and that abort also exited 1,
i.e. looked like a verdict. Braced, and re-run.)
publishbuilds the archive itself with pack.py, Python stdlib + gitonly. Reproducibility (tested every PR by packing twice) makes these
the bytes ci.yml booted, without importing anything from that run.
release fails before copying anything. Controls on the rendered step:
exact -> 4 assets; an injected pkg-evil tarball -> rc=1, 0 copied;
one missing -> rc=1, 0 copied.
Renode publishes NO checksums or signatures, so the pin (1a532d4b…,
two agreeing fetches) is TRUST-ON-FIRST-USE: it stops a later
substitution, it does not prove the original upload genuine.
the release never consumes. Its worst case is a false "boots OK".
Pre-existing and NOT changed here, same class as finding 1:
publishruns
cargo install --locked cargo-cyclonedxwith the OIDC token in scope.The post-publish consumer gate covered only with-device-*; it now asserts
the model is in the signed manifest and verifies its attestation. Its boot
is not repeated there: it was proven on these bytes before upload and
sha256sum -cproves the published bytes are those bytes.NO --clobber ON A PUBLISHED TAG. A draft may be re-uploaded; a published
release now uploads without --clobber, so replacing an asset consumers may
have pinned fails the job instead of silently changing a tag's bytes.
ALSO: two lines in artifacts.pins said "Renode is not installed on this host".
It is, at ~/renode-1.16.1, just not on PATH —
command -v renodewas thewrong test. Corrected in place with the correction visible; the results those
lines accompanied stand (QEMU was a validated stand-in, CI confirmed on
Renode).
All of the above was run locally on Renode 1.16.1 before pushing.
Closes #299 (items 2–5; item 1, the licence, landed in #300 as Apache-2.0 per the decision recorded on the issue).
🤖 Generated with Claude Code
https://claude.ai/code/session_017EKh2onUHpWSUiAEqWxfy2