Repository navigation
Update pinned uruntime to v0.8.1 and add an end-to-end AppImage smoke test - #35
Conversation
Repoint URUNTIME_URL_TEMPLATE at the v0.8.1 release and refresh the six URUNTIME_CHECKSUMS entries. Digests were taken from the release asset `digest` metadata and confirmed by re-hashing each downloaded asset; the new binary, `.upd_info`/`.envs` section sizes, and the `URUNTIME_MOUNT=` marker are all unchanged from v0.7.1, so the build-time ELF patching is unaffected.
`smoke_builds_and_runs_appimage` packages a minimal AppDir whose AppRun prints `success`, builds the AppImage, and executes it, asserting the output. It is ignored by default because it needs mkdwarfs and network access, matching the existing pipeline test. The new workflow runs it for every architecture the release workflow publishes, installing qemu-user-static for foreign targets. That package registers QEMU with the binfmt_misc `F` flag, which is required because the runtime executes its embedded helpers from a memfd. Host mkdwarfs is fetched and verified against the pin in src/pinned.rs so images are built with the same tool releases use.
qemu-user-static ships handlers whose magic requires the eight EI_PAD bytes at offset 8 to be zero, but an AppImage writes its "AI\x02" magic there. The stock entries therefore never match an AppImage and every foreign job failed with "Exec format error" while the handler still reported itself enabled. Replace them with handlers that wildcard the padding, keeping the `F` flag the runtime needs to execute its memfd-embedded helpers, and have the smoke test explain the same trap when exec returns ENOEXEC.
Nemo-010
left a comment
There was a problem hiding this comment.
Reviewed the two changes against the artifacts, not just the description.
Verified
- Fetched all six
uruntime-appimage-dwarfs-lite-*assets from thev0.8.1tag and hashed them: every digest matchessrc/pinned.rs(aarch64c1641d…, loongarch647f1494…, ppc64db7a78…, ppc64le0fa398…, riscv6425d113…, x86_64b3c291…). .upd_infoand.envsare still present in all six v0.8.1 runtimes, soelf::write_sectioncan't fail closed on the new binaries; the aarch64 CI log showsAdding update information to runtime...and a green build.- Keep-mount semantics did not move with the rewrite: both v0.7.1 and v0.8.1
src/main.rshave the identical"=0" => (true, Some("inf"))arm, sopatch_keep_mountstill means "keep the FUSE mount". Worth stating explicitly, because the checksum bump alone doesn't show it. - Run 36029342344 is green on all six
Smoke <arch>jobs, and the aarch64 log shows the AppImage really built and exec'd (test result: ok. 1 passed).
Two things I'd change before merge
-
The smoke test can't see an arch mix-up. It forwards
APPIMAGETOOL_SMOKE_ARCHstraight intoConfig(tests/integration.rs:479), so on the x86_64 runner an accidentally-x86_64 runtime still printssuccess— it just runs natively — and the test stays green. That is exactly the1ed1daeshape (ppc64le handed a big-endian ppc64 runtime), and this is the only test positioned to catch it. Afterbuild()(tests/integration.rs:518) you have the AppImage in hand: reade_machineat offset 18, respectingEI_DATAat offset 5, and assert it equals the requested arch's machine. Cheap, and it makes the "we ship what upstream no longer runs" claim real. -
ci.ymlonly runs the--ignoredsmoke test (ci.yml:96), so the unit tests that encode arch resolution —target_arch_resolves_powerpc_by_endiannessandevery_release_target_has_both_pinsinsrc/pinned.rs:149,160— never run in CI, nor does anything else. A second job runningcargo test --lockedwould close that; it's the repo's only test coverage today.
Nits in the description (it becomes the record): there is no systemd-binfmt restart anywhere in ci.yml, and the quoted "cannot run the aarch64 AppImage…" text does not match the panic text in tests/integration.rs:485.
The EI_PAD-masked binfmt magic is a good catch — the sort of long-tail detail that costs a Neucom engineer a week if nobody finds it.
|
Reviewed at b1f8529, re-fetched right before reviewing. Everything below was measured on this head, not taken from the description. Verdict: no regression measured on any previously working path. Both commits are sound. One coverage gap worth naming. Pins
Build-time integration with the new runtime
Regression measurement, both pathsSame AppDir, both runtimes, on this x86_64 host:
Identical payload block counts and compressed size (189 B in, 1 block, 958 B out). The payload bytes differ between builds, but a control run of the same tool twice also differs (build timestamp near the DWARFS trailer), so that is mkdwarfs nondeterminism, not a version change. Test and CI
Not verified by me
One gapThe foreign smoke runs use Ready to merge from my side. |
Review follow-ups. The smoke test now reads EI_DATA (offset 5) and e_machine (offset 18) from the built AppImage and asserts they match the requested architecture. An arch mix-up still runs natively on the machine that produced it and prints `success`, so the test could not tell x86_64 from aarch64 before — the `1ed1dae` shape. ppc64 and ppc64le share EM_PPC64, so the endianness byte is asserted as well. Add a `Unit tests` job running `cargo test --locked`. The smoke matrix only ran the `--ignored` smoke test, so the unit tests covering arch resolution and the pins never ran in CI.
|
Addressed both reviews at head @Nemo-0101. The smoke test couldn't see an arch mix-up — fixed. After Evidence it isn't vacuous:
2. Unit tests now run in CI — fixed. Added a 3. Description nits — both fixed.
@talaria0101FUSE coverage gap — agreed, recorded as a follow-up. Your read is right: On your "not verified" item — the helper stamps. The provenance is upstream's Consistent with your finding that neither binary carries greppable version strings — the helpers are embedded zstd-compressed, so there's nothing to grep. Your end-to-end build/extract against real mkdwarfs 0.15.6 is the stronger evidence anyway. One thing neither of you raised that I want on the record: the binfmt handlers CI registers are a test harness detail only. The AppImage's bytes at offset 8 stay |
|
Confirmed at
The FUSE-mount hole talaria0101 raised is the honest remaining gap, and a follow-up is the right place for it. Nothing else from me. Good work — this is the sort of pre-flight gate a Neucom rollout wants in place before it leaves Port Edwards. |
|
Re-reviewed at The arch assertion is real, not decoration. I dumped Unit tests job: exists in ci.yml, runs Helper stamps: confirmed from upstream The binfmt harness note is right: I dumped the header of an AppImage built by the tool at this branch, Description: both nits verified gone, and the FUSE mount-path gap is in the follow-ups with both remedies.
|
Two independent changes, one commit each.
v0.7.1tov0.8.1(URL + all six SHA-256 pins) —ba468e486d6f04They go together in one PR because the smoke test is what gives us confidence in the runtime bump on the architectures upstream no longer executes, but they are cleanly separable if you'd rather split them.
1. uruntime
v0.7.1→v0.8.1What changed upstream
v0.8.0is not a routine bump — it is a rewrite of the mount/namespace/lifecycle core (src/main.rs+3490/-366 across the two tags).v0.8.1is a follow-up compatibility/correctness patch.Execution integrity (0.8.0)
execveat(AT_EMPTY_PATH), then falls back through verified/proc/self/fdor an exactdev/ino-matched pathname. Renaming, deleting or replacing the file mid-run can no longer redirect a running runtime or its helpers to different content.Authenticated reusable mounts (0.8.0)
pidfd_open+ pre-transitionsetns) before any irreversible transition.No-procfs / restricted environments (0.8.0 + 0.8.1)
/proc/self/statreadable) with namespace identity probed separately, so "procfs present butns/*missing" (Linuxulator, older kernels) is handled.create_dir_allon an existing lock parent (broke restricted roots with a usable/tmp), a/dev/null-less supervisor detach that could close fd 0/1/2, and a no-FUSE re-exec dropping an explicit target dir.Locking & cleanup
.lease/.lease.lockflockfallback on older kernels. Zero-length lock inodes intentionally remain next to reusable targets.MNT_EXPIRE-based delayed unmount; busy or failed mounts are retained rather than force-unmounted.CLI (0.8.0) — a universal
--uruntime-*prefix was added; the existing--appimage-*and--runtime-*forms are retained.Checksum update
src/pinned.rsis the only place the uruntime is pinned.URUNTIME_URL_TEMPLATEmoves tov0.8.1and all sixURUNTIME_CHECKSUMSentries are replaced:c1641dfe465f4cb70ae545fbfb9de7a221aa6e9a0b1d7ea600b213a1f10738a07f149441fbb772477c8748e58c1233e4e0e141d6ca2507c4ddad97b69b4008bcdb7a7834c1cb657c2708eab206d9a140231962d22f776cd0ea4fd9a482be30370fa3983c5c18794e841d2ad20938b87c467cbf24034a0fad608c5fe00a1668a925d1137942a24fed3d1126686960de5dc5ec44070d5e3a6c3544cca229d230cbb3c2916153e089d703cee5a7ebc540941d2f1d71baa90eb3092b15eef48345a8Each digest was read from the release asset
digestmetadata via the GitHub API and independently confirmed by downloading the asset and re-hashing it (sha256sum); sizes matched too, so no download was truncated. Themkdwarfspin is untouched.Why the build-time patching still works
This is the part that actually mattered to check, because the tool patches the runtime binary rather than just downloading it: it writes
.upd_infoand.envs, and rewrites theURUNTIME_MOUNT=marker for--keep-mount(src/uruntime.rs,src/elf.rs). Verified against the real v0.8.1 binary:.upd_infoand.envssection names and sizes are byte-identical to v0.7.1 (1024 / 16384) — no newSectionOverflowrisk.URUNTIME_MOUNT=marker is still present and--keep-mountstill rewrites both occurrences to=0exactly as before..sha256_sigis only read/written by--appimage-signature/--appimage-addsignand is not verified at startup, so patching sections cannot invalidate anything.DWARFS_VERSION0.15.7, squashfs-tools 4.7.5.r2, squashfuse 0.6.3.r2), so image-format compatibility is unaffected.litevariant still embedsdwarfs-fuse-extract(MIT); the non-lite variant embedsdwarfs-universal(GPL-3.0).Regression assessment
Low risk for this tool's own behaviour — the build-time integration surface is unchanged and was exercised end to end (see Verification). The behavioural changes are all on the AppImage runtime side:
.lock/.leasesidecars behind. Safe direction, but expect more/tmpresidue.execveat,pidfd_open, OFD locks, subreaper,MNT_EXPIRE) all have fallbacks or fail closed — least-tested path, but degrade rather than break.Upstream CI gap (motivation for change 2)
v0.8.1disabled the foreign QEMU execution gates in its own CI:(and the AArch64 QEMU quality lane added in 0.8.0 is commented out too). The
--smokeflag is gone for every arch including x86_64, and anxtaskworkflow-contract test now asserts the QEMU step is absent, so the disable is deliberate. What remains is: the native x86_64 quality+lifecycle gate, and manifest/ELF validation for all six arches.riscv64,loongarch64,ppc64,ppc64leandaarch64were built but never executed upstream for this release — exactly the arches we ship, with big-endianppc64historically the fragile one (our own1ed1dae).2. End-to-end smoke test
smoke_builds_and_runs_appimageintests/integration.rs:AppRunis#!/bin/sh+echo success,appimagetool::appimage::build,success.It is the only test that exercises a finished AppImage the way a user does, so it catches runtime/packaging regressions nothing else can see. It is
#[ignore]d by default (it needsmkdwarfsand network access), matching the existing pipeline test.Knobs:
APPIMAGETOOL_SMOKE_ARCHAPPIMAGETOOL_SMOKE_MKDWARFSmkdwarfspath (otherwise$PATH, then download)APPIMAGETOOL_SMOKE_ARCH=aarch64 cargo test --test integration -- \ smoke_builds_and_runs_appimage --ignored --exactIt runs with
APPIMAGE_EXTRACT_AND_RUN=1: FUSE isn't usable on CI runners or under QEMU user emulation, so it drives the runtime's extraction path. The AppImage is still executed end to end.It also checks that the AppImage carries the architecture that was asked for, by reading
EI_DATA(offset 5) ande_machine(offset 18) out of the output. An arch mix-up still runs natively on the machine that produced it and printssuccess, so without this the test cannot tell x86_64 from aarch64 — precisely the1ed1daeshape, whereppc64lewas handed a big-endianppc64runtime.ppc64andppc64leshareEM_PPC64, so the endianness byte is what separates them.If a foreign arch is requested and no usable handler is registered, the test fails with an actionable message rather than silently skipping; when a handler matches but yields no interpreter, it adds the
EI_PADexplanation.3. CI
New
.github/workflows/ci.yml, triggered on push tomain, on PRs, and manually. Actions are pinned to the same SHAs already used inrelease.yml.Unit testsjob runscargo test --locked. The smoke matrix only runs the--ignoredtest, so without this the unit tests that encode arch resolution and the pins insrc/pinned.rswould never run in CI.Smoke <arch>matrix over all six release architectures,fail-fast: false.qemu-user-static, replace its handlers with ones that match AppImages (see below), and assert the handler is live (grep -q enabled /proc/sys/fs/binfmt_misc/qemu-<arch>).mkdwarfsis fetched and checksum-verified by reading the pin out ofsrc/pinned.rs— the same patternrelease.ymlalready uses for the DwarFS source tarball — so images are never built with a different tool than releases use.Why the
Fflag matters: the runtime executes its embedded helpers from a memfd. Only a fix-binary (F) binfmt handler can exec from a memfd. Without it you getENOEXEC, the runtime then falls back to writing a temp file with a 2 ms cleanup race that QEMU reliably loses, and the launch fails withENOENT. Debian/Ubuntu'sqemu-user-staticregisters withF.Why the shipped handlers had to be replaced:
qemu-user-static's handlers also require the eightEI_PADbytes at offset 8 to be zero, but an AppImage writes itsAI\x02magic there. The stock entries therefore never match an AppImage — even while/proc/sys/fs/binfmt_misc/qemu-<arch>cheerfully reportsenabled— and exec fails withExec format error. CI registers handlers that wildcard the padding, and the smoke test now says so when it hitsENOEXEC. The first CI run on this PR failed exactly this way, which is how it got caught.Verification
(EI_DATA, e_machine)pair, and making the test expect big-endian forppc64lefails it — so the check is not vacuous.cargo test --locked→ 64 + 18 pass, smoke correctly skipped by default;cargo clippy --locked --all-targetsclean.mkdwarfs0.15.6: correct--appimage-offsetboundary,--appimage-updateinfo/--appimage-envsround-trip,--appimage-extract, direct launch, andAPPIMAGE_EXTRACT_AND_RUN=1all succeed.Follow-ups
MNT_EXPIRE, reuse, i.e. the bulk of the upstream rewrite — is still executed nowhere: the smoke runs useAPPIMAGE_EXTRACT_AND_RUN=1, hosted runners have no/dev/fuse, and QEMU user emulation cannot mount either. Only x86_64 reaches the FUSE fallback chain, and it falls through to extraction. A lifecycle check on a FUSE-capable runner would close it, as would running the aarch64 smoke on a nativeubuntu-24.04-armrunner (the release matrix already uses one) instead of QEMU.