Sprint 11.5 β Pivot-to-Squashfs Boot and A/B Validation
Goal: Complete Sprint 11's remaining critical path β make the running system actually boot from the read-only squashfs active slot (instead of the embedded initramfs), and validate the full A/B update/rollback cycle end-to-end.
Primary Outcome: On boot, the initramfs mounts the active slot's squashfs image read-only and pivots into it; touch /usr/test fails with EROFS; a bad slot automatically rolls back after 3 failed boots; the full A/B test matrix passes on QEMU and the ROG Ally.
Status: π‘ Partially validated β T1βT4 landed; host tests + QEMU (pivot + forced rollback) pass. The full 6-case A/B matrix still needs a ROG Ally hardware run.
ROG Ally hardware gate: execute the full 6-case matrix on real hardware only after Sprint 11.6 is complete and the ROG Ally is reachable over the network via SSH (USB-C Ethernet + Dropbear). Sprint 11.6 provides the remote access needed to run and capture the matrix directly, instead of reading logs off a USB stick.
Prerequisites: Sprint 11 host-side stack landed and committed (boot.json read/write + slot selection, boot-count/rollback logic, .playosb bundle format + sign/verify, ApplyUpdate IPC, shell update UI, playos_system_os_version()).
Why This Sprint Exists
Sprint 11 delivered the A/B update machinery, but the runtime still boots entirely from the embedded initramfs. The squashfs image written to the inactive slot is never mounted as /, so "immutable root" and automatic rollback are not real end-to-end yet. This sprint closes that gap with the actual boot-path change, then validates it.
Start Condition Checklist
-
src/boot_slot.creads/writes/EFI/playos/boot.jsonand selects the active slot. -
Boot-count increment + 3-strike rollback logic exists (ShellReady OR 60-second timer β
mark_good). -
.playosbbundle creation (scripts/create-update-bundle.sh) and verification (src/sha256.c+src/update.c) exist. -
Shell software-update UI and
playos_system_os_version()(reads active slot fromboot.json) exist. -
Open question (resolve first): does the
playos-refdistrobuild produce a complete bootable squashfs rootfs (full userspace: shell, compositor, runtime, platform-api, samples, libs, config), or only a partial artifact that the updater writes? This decides whether T1 is "wire the pivot" (small) or "build a full rootfs + minimal initramfs shim" (large). Resolved: theallydefconfig already builds a complete bootablerootfs.squashfs(full userspace present), so this sprint is the small "wire the pivot" path.
Decisions Locked for This Sprint
- Initramfs role: becomes a minimal early-boot shim; the real root is the active slot's squashfs image.
- Boot sequence: mount ESP read-write β read
boot.jsonβ select active slot β mount its squashfs read-only β mount/data(rw) and/miscβpivot_root/switch_rootβexecthe system init. - Read-only guarantee: squashfs is inherently read-only; no
MS_RDONLYremount trickery needed. dm-verity remains post-MVP. - Slot metadata: stays in
boot.jsonon the ESP (no migration tomiscthis sprint). - No repartitioning: reuse the Sprint 10 5-partition layout unchanged.
Scope
In Scope
playos-refdistroβ full bootable squashfs rootfs (if the open question shows it is partial) + a minimal initramfs shim.playos-initβpivot_root/switch_rootinto the active slot squashfs; wireboot_slot.c's active-slot selection into the mount path; remountdata/miscinside the new root.playos-initβ make 3-strike rollback real end-to-end (a slot that fails to boot actually triggers a switch + reboot).- Validation matrix (was Sprint 11 S11-T9) on QEMU first, then the ROG Ally.
Explicitly Out of Scope
- dm-verity (post-MVP)
- Network update download (post-MVP)
- Production HSM update key (post-MVP)
- Delta updates (post-MVP)
- Migrating slot metadata from the ESP to
misc
Required Repository Changes
| Repo | Required work |
|---|---|
playos-refdistro | Full bootable squashfs rootfs build (if needed) + minimal initramfs shim |
playos-init | pivot_root/switch_root into the active slot squashfs; slot-selection wiring; real rollback |
playos-spec | This document |
Expected Files and Directories
playos-refdistro
br2-external/board/ally/ # rootfs-as-squashfs build changes (if the open question shows a partial rootfs)
br2-external/board/ally/initramfs/ # minimal early-boot shim (mount squashfs -> pivot_root)
playos-init
src/boot_slot.c # already implements selection; wire into the mount path
src/main.c # replace embedded-rootfs boot with pivot into the active slot
src/mount.c # mount active slot squashfs + remount data/misc in the new root
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S11.5-T1 | Confirm/produce a complete bootable squashfs rootfs + minimal initramfs shim | playos-refdistro | done | Full userspace confirmed in output/ally/images/rootfs.squashfs; added /EFI mountpoint to rootfs-overlay. |
| S11.5-T2 | pivot_root/switch_root into the active slot squashfs | playos-init | done | playos_pivot_to_active_slot() added in src/mount.c; switch_root idiom (MS_MOVE + chroot + exec /init). |
| S11.5-T3 | Wire boot_slot.c active-slot selection into the mount path | playos-init | done | boot_slot_read() selects playos-a/playos-b; called in main.c after ESP/boot-slot block. |
| S11.5-T4 | Make 3-strike rollback real end-to-end | playos-init | done | Host test_boot_slot covers boot-count/3-strike rollback; QEMU Scenario B proves forced rollback (boot.json flipped to slot b, slot a marked bad). |
| S11.5-T5 | A/B update + rollback validation matrix (was S11-T9) | playos-refdistro | in progress | Automated subset passes (host test_boot_slot 2/2 + QEMU Scenario A/B); fresh install β 5-partition layout β NVMe boot validated 2026-08-19 (S10-T8 re-install fix). Full 6-case matrix on ROG Ally still pending (real apply/reboot/mark-good timing, /data survival on real NVMe, live version API per slot) β blocked on Sprint 11.6 SSH/network reachability for remote execution. |
S11.5-T1 β Confirm/produce a complete bootable squashfs rootfs + minimal initramfs shim
Finding (to confirm before any code): the system currently boots from the embedded initramfs. It is unknown whether the refdistro build already produces a complete squashfs rootfs or only a partial artifact for the updater.
Steps:
- Inspect the refdistro build output: locate the squashfs artifact, list its top-level entries, and confirm whether it contains a complete userspace (shell, compositor, runtime, platform-api, samples,
/lib,/etc). - If complete: proceed to T2 β only the initramfs pivot path needs wiring.
- If partial: build the full rootfs as squashfs and reduce the initramfs to a minimal shim (mount ESP β read
boot.jsonβ mount active squashfs βpivot_rootβexec /sbin/init).
Done when: the build produces a squashfs that contains the full system userspace, and the initramfs no longer carries the whole system.
S11.5-T2 β pivot_root/switch_root into the active slot squashfs
Steps:
- In
playos-init/src/main.c, after the early mount of the ESP andboot.jsonread, mount the active slot partition (by GPT labelplayos-a/playos-b) read-only at a staging path. - Mount
/data(labelplayos-data) read-write and/miscunder the staged new root. pivot_root(orswitch_rootif a minimal initramfs shim is used) into the squashfs, thenexecthe real init.- Log the transition:
playos-init: pivoted to active slot <a|b> (squashfs, read-only).
Done when: QEMU boots and cat /proc/mounts shows / on squashfs with ro; touch /usr/test fails with EROFS.
S11.5-T3 β Wire boot_slot.c active-slot selection into the mount path
Steps:
- Reuse the existing
boot_slot.cslot-selection result to chooseplayos-avsplayos-bat mount time. - Confirm a missing/corrupt
boot.jsonfalls back to slot A and recreates the file (behavior already specified in Sprint 11). - Keep
boot_slot.cas the single source of truth β do not duplicate slot-selection logic inmain.c.
Done when: changing active_slot in boot.json causes the next boot to mount the other slot.
S11.5-T4 β Make 3-strike rollback real end-to-end
Steps:
- Verify the existing rollback path fires when the new root fails to boot:
boot_count >= 3 && health != "good"β markbad, switchactive_slot, reboot. - Ensure the boot-count increment and
mark_good(ShellReady/60s) survive the pivot β they must operate on the ESP, which stays mounted across the pivot. - Test that a deliberately broken slot (bad initramfs or missing squashfs) rolls back to the previous slot after 3 attempts.
Done when: corrupting the active slot causes 3 failed boots and an automatic switch to the previous slot.
S11.5-T5 β A/B update + rollback validation matrix
Full test matrix (QEMU first, then ROG Ally):
- Fresh install β 5-partition layout β
boot.jsonshows slot A good. - Apply valid bundle β
boot.jsonshows slot B pending β reboot β slot B active β 60s β slot B good. - Corrupt slot B β reboot 3Γ β rollback to slot A.
/datacontent (game files, saves) survives update and rollback unchanged.- Invalid-signature bundle β rejected before any partition write.
playos_system_os_version()returns the correct version for each slot.
Done when: all 6 cases pass with log evidence.
Implementation Guidance
Resolve T1 before coding T2βT4
T1 determines whether this sprint is a small boot-path wiring change or a larger rootfs-build effort. Do the refdistro build inspection first and report the finding before writing any pivot_root code.
Keep boot_slot.c the single source of truth
Slot selection, boot counting, and rollback all live in boot_slot.c. main.c should consume its result, not re-derive it.
Atomic commits
S11.5-T1: build full bootable squashfs rootfs + minimal initramfs shim
S11.5-T2: pivot into active slot squashfs
S11.5-T3: wire active-slot selection into boot mount path
S11.5-T4: make 3-strike rollback real end-to-end
S11.5-T5: A/B validation matrix + evidence
Verification and Evidence
| Evidence | How it is produced | Current state |
|---|---|---|
| Read-only root | cat /proc/mounts shows / on squashfs ro; touch /usr/test β EROFS | β οΈ Not yet asserted by the QEMU harness (pivot is asserted; ro/EROFS check not captured) |
| Slot selection | changing active_slot in boot.json changes the mounted slot | β
QEMU Scenario A pivots to slot a read from boot.json; Scenario B writes active_slot: "b" |
| Rollback | 3-strike test log showing automatic slot switch | β
QEMU Scenario B flips to b and marks a bad; host test_boot_slot covers boot-count logic |
| Update apply/verify | host test_boot_slot valid / bad-signature / bad-magic cases | β
ctest 2/2 pass |
| Data survival | file hashes before/after update match | β οΈ Pending ROG Ally β not covered by the QEMU harness |
| Signature rejection | log showing bundle rejected before any write | β
host test_boot_slot bad-signature case |
| Version API | playos_system_os_version() per slot | β οΈ Code inspection only; hardware per-slot run pending |
Acceptance Criteria
- System boots from the read-only squashfs active slot (not the embedded initramfs) β QEMU Scenario A
-
touch /usr/testfails withEROFSβ squashfs ro is by construction, but the current QEMU harness does not assert this -
Active-slot selection in
boot.jsondrives which slot is mounted β QEMU Scenario A readsactive_slot: "a" -
A valid bundle applied to the inactive slot does not affect the running system β host
test_boot_slotapply path (by design, apply writes only to the inactive slot) - After reboot, the system boots from the newly updated slot β full applyβrebootβactive cycle not yet run on QEMU or hardware
-
Successful boot marks the new slot
health = "good"after 60 seconds β logic covered by host test; real 60s timer not yet exercised -
3 consecutive failed boots trigger rollback to the previous slot β QEMU Scenario B + host
test_boot_slot -
/datacontent survives update and rollback unchanged β not yet exercised on QEMU or hardware -
Invalid-signature bundle is rejected before any partition write β host
test_boot_slotbad-signature case - QEMU validation passes for the bootable subset β Scenario A (pivot) + Scenario B (forced rollback)
- ROG Ally passes the full 6-case matrix β pending Sprint 11.6 SSH/network reachability
Handoff to Sprint 12
Sprint 12 (Security Hardening) may assume:
- The system image is immutable at runtime (read-only squashfs root).
- Signed A/B updates with automatic rollback are functional on the automated/QEMU subset; the full hardware matrix is pending Sprint 11.5 closure, executed over SSH after Sprint 11.6 is complete.
- Games and user data live on
/dataand are unaffected by updates and rollback.
Exit Gate
The system boots from the read-only squashfs active slot, A/B updates can be applied and automatically rolled back on failure, and the full validation matrix passes on QEMU and the ROG Ally (the Ally portion executed over SSH once Sprint 11.6 is complete).