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.c reads/writes /EFI/playos/boot.json and selects the active slot.
  • Boot-count increment + 3-strike rollback logic exists (ShellReady OR 60-second timer β†’ mark_good).
  • .playosb bundle 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 from boot.json) exist.
  • Open question (resolve first): does the playos-refdistro build 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: the ally defconfig already builds a complete bootable rootfs.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 β†’ exec the system init.
  • Read-only guarantee: squashfs is inherently read-only; no MS_RDONLY remount trickery needed. dm-verity remains post-MVP.
  • Slot metadata: stays in boot.json on the ESP (no migration to misc this 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_root into the active slot squashfs; wire boot_slot.c's active-slot selection into the mount path; remount data/misc inside 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

RepoRequired work
playos-refdistroFull bootable squashfs rootfs build (if needed) + minimal initramfs shim
playos-initpivot_root/switch_root into the active slot squashfs; slot-selection wiring; real rollback
playos-specThis 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 IDTaskPrimary repoStatusNotes / evidence
S11.5-T1Confirm/produce a complete bootable squashfs rootfs + minimal initramfs shimplayos-refdistrodoneFull userspace confirmed in output/ally/images/rootfs.squashfs; added /EFI mountpoint to rootfs-overlay.
S11.5-T2pivot_root/switch_root into the active slot squashfsplayos-initdoneplayos_pivot_to_active_slot() added in src/mount.c; switch_root idiom (MS_MOVE + chroot + exec /init).
S11.5-T3Wire boot_slot.c active-slot selection into the mount pathplayos-initdoneboot_slot_read() selects playos-a/playos-b; called in main.c after ESP/boot-slot block.
S11.5-T4Make 3-strike rollback real end-to-endplayos-initdoneHost 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-T5A/B update + rollback validation matrix (was S11-T9)playos-refdistroin progressAutomated 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:

  1. 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).
  2. If complete: proceed to T2 β€” only the initramfs pivot path needs wiring.
  3. 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:

  1. In playos-init/src/main.c, after the early mount of the ESP and boot.json read, mount the active slot partition (by GPT label playos-a/playos-b) read-only at a staging path.
  2. Mount /data (label playos-data) read-write and /misc under the staged new root.
  3. pivot_root (or switch_root if a minimal initramfs shim is used) into the squashfs, then exec the real init.
  4. 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:

  1. Reuse the existing boot_slot.c slot-selection result to choose playos-a vs playos-b at mount time.
  2. Confirm a missing/corrupt boot.json falls back to slot A and recreates the file (behavior already specified in Sprint 11).
  3. Keep boot_slot.c as the single source of truth β€” do not duplicate slot-selection logic in main.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:

  1. Verify the existing rollback path fires when the new root fails to boot: boot_count >= 3 && health != "good" β†’ mark bad, switch active_slot, reboot.
  2. 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.
  3. 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):

  1. Fresh install β†’ 5-partition layout β†’ boot.json shows slot A good.
  2. Apply valid bundle β†’ boot.json shows slot B pending β†’ reboot β†’ slot B active β†’ 60s β†’ slot B good.
  3. Corrupt slot B β†’ reboot 3Γ— β†’ rollback to slot A.
  4. /data content (game files, saves) survives update and rollback unchanged.
  5. Invalid-signature bundle β†’ rejected before any partition write.
  6. 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

EvidenceHow it is producedCurrent state
Read-only rootcat /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 selectionchanging 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"
Rollback3-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/verifyhost test_boot_slot valid / bad-signature / bad-magic casesβœ… ctest 2/2 pass
Data survivalfile hashes before/after update match⚠️ Pending ROG Ally β€” not covered by the QEMU harness
Signature rejectionlog showing bundle rejected before any writeβœ… host test_boot_slot bad-signature case
Version APIplayos_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/test fails with EROFS β€” squashfs ro is by construction, but the current QEMU harness does not assert this
  • Active-slot selection in boot.json drives which slot is mounted β€” QEMU Scenario A reads active_slot: "a"
  • A valid bundle applied to the inactive slot does not affect the running system β€” host test_boot_slot apply 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
  • /data content survives update and rollback unchanged β€” not yet exercised on QEMU or hardware
  • Invalid-signature bundle is rejected before any partition write β€” host test_boot_slot bad-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 /data and 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).

Previous: Sprint 11 | Next: Sprint 12