Skip to content

ship the Pixhawk 6X-RT Renode model as a signed, reproducible release asset (jess#299) - #302

Merged
avrabe merged 1 commit into
mainfrom
feat/renode-model-asset
Oct 27, 2026
Merged

avrabe merged 1 commit into
mainfrom
feat/renode-model-asset

Conversation

@avrabe

@avrabe avrabe commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

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.

  1. $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.

  2. 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.

  3. 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).

    • PYTHON, NOT tar. Ship the Pixhawk 6X-RT Renode model as a signed release asset (licence, $ORIGIN paths, MODEL.files) #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 Ship the Pixhawk 6X-RT Renode model as a signed release asset (licence, $ORIGIN paths, MODEL.files) #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.

  4. 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.

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

… 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
avrabe force-pushed the feat/renode-model-asset branch from 284a5c4 to 84d0d94 Compare October 27, 2026 23:03
@avrabe
avrabe merged commit c33da03 into main Oct 27, 2026
9 checks passed
@avrabe
avrabe deleted the feat/renode-model-asset branch October 27, 2026 23:48
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.
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.

Ship the Pixhawk 6X-RT Renode model as a signed release asset (licence, $ORIGIN paths, MODEL.files)

1 participant