Skip to content

Run Mac image lifecycle scenarios in VM acceptance - #554

Merged
maralcbr merged 6 commits into
quattro-upstreamfrom
mac/28-vm-acceptance-images
Sep 25, 2026
Merged

maralcbr merged 6 commits into
quattro-upstreamfrom
mac/28-vm-acceptance-images

Conversation

@maralcbr

Copy link
Copy Markdown
Collaborator

Ticket 28: VM acceptance accepts quattro-upstream images.

What changed

  • tools/acceptance/mac-image/run now takes the image producer's output directory (--image DIR, from omacom/omarchy-mac-installer image-builder/), not mx-mac's GRUB image releases.
    • It holds IMAGE (format 2), PROVENANCE, a passed INSPECTION, installer_data.json, the inputs record and every payload member to each other.
    • IMAGE.sig is verified when present, and run.txt records whether the image was signed.
  • Named, independently runnable scenarios (--scenario NAME, --list; fresh-install is the default):
    • first-boot: plain install, owner setup, console login.
    • conversion: LUKS2 in the initrd, then owner setup re-keys to the owner password and a recovery key.
    • second-boot: Limine's command line prompts, refuses a wrong password, unlocks with the owner's.
    • password-change: omarchy-drive-password at the console, then a boot with the new password.
    • update, snapshot-restore and factory-reset skip while the image lacks update-verify (Post-update boot verification blocks update completion and the reboot #543), the snapshot check (ticket 36) or reset-prepare (ticket 34). Once an image ships one, its case fails until it is automated.
    • A case that needs a re-keyed disk runs conversion first.
  • first-boot also checks what the first M2 install got wrong: the Plymouth theme is omarchy (in the configuration and in the UKI's initramfs), and /.snapshots is a btrfs subvolume. The default sink cannot be checked in a VM and is recorded as a skip.
  • How the guest stands in for a Mac:
    • It boots through UEFI (Arch's edk2-aarch64, pinned by digest), since the image builds its UKI only under UEFI.
    • QEMU's device tree carries an Apple root compatible, so the image's Apple paths run.
    • Each boot uses the command line of the Limine entry on the disk's ESP.
    • Two credential drop-ins on the command line move owner setup to the serial console, where the harness answers it as an owner would.
  • Fixes found while running it:
    • The chroot's rbind mounts leaked into service mount namespaces and pinned the payload's loop device until a reboot (the old harness did this too). The chroot now runs in a private mount namespace.
    • Loop devices are hidden from automounters while a run lasts.
    • QEMU drops serial input past the UART FIFO when the writer disconnects, so the connection now stays open until the guest has read it.
    • Disks are written at partition offsets, where they used to take ~10 minutes each through a loop device.
  • Evidence gets SHA256SUMS and the guest's first-boot and setup logs. Passwords and the recovery key are random per run and redacted.

Testing

These were VM runs on the M1 Pro against the first converged image, apple-test-073e489b5b85-20260925 (payload sha256 5e2a293a…21af, source 073e489b5, which predates #528/#532/#540/#541):

  • fresh-install (run t28-f, harness fcc00c5): every lifecycle check passes.

    • First boot runs the deferred Limine step, which rebuilds the menu and UKI for this Mac. Owner setup and the console login complete.
    • The LUKS2 conversion takes 59 s. Only the owner password and the recovery key open the disk, each in its own slot, so no slot is left for the temporary key.
    • encrypt.state is finished and Limine's command line carries no key file. Second boot refuses a wrong password and unlocks with the owner's.
  • Expected failures on this older image: Plymouth theme bgrt, and /.snapshots is a plain directory. These are the M2 findings, which installer Consider adding error handling for the pacman command. If this fails, the function will continue and likely fail later with less clear error messages. #9 fixes in the builder.

  • password-change (run t28-g, harness adc2070):

    • The disk takes the new password, the old one is refused and the recovery key still opens it.
    • Fails on this image: the login password does not follow the disk's (the image predates Change the root LUKS key before syncing login and root passwords #532). An image from current quattro-upstream should pass.
    • Also fails: omarchy-drive-password lists no drive until root has run blkid in that boot, because its unprivileged blkid reads only /run/blkid. The current version still does this.
  • update, snapshot-restore and factory-reset skip with their reasons.

  • Evidence is on the M1 Pro in ~/vm-evidence/mac-image-t28-{f,g}/, and SHA256SUMS verifies. The later commits only record the run identity earlier and change no check.

  • Also seen: the image's omarchy-mac-encrypt.service sets StandardOutput=journal+kmsg, which systemd rejects and ignores.

  • tools/acceptance/test/mac-image-run-test.sh (new: scenario selection and refusals, no VM) passes. shellcheck was not available on the hosts used.

  • Second review: two rounds. The first found a missed refusal on a cryptsetup error, the recovery-key acknowledgement never matching gum's placeholder, a relative --payload, and README gaps. The second found the error path still swallowed inside a command substitution. All fixed.

mac-image takes the image-builder's output directory, holds IMAGE, PROVENANCE
and INSPECTION to the payload, and boots each scenario through UEFI with the
Limine entry's command line and an Apple root compatible: first boot,
conversion and re-key, second boot and password change, with update,
snapshot restore and factory reset skipped until an image ships their
entrypoints. First boot also checks the Plymouth theme and /.snapshots.
…ew findings

The chroot's rbind mounts propagated into every service's mount namespace,
where the host's unmount never reached them, so the payload's loop device
stayed busy until a reboot: the chroot now mounts in a private namespace.
A cryptsetup error no longer reads as a refused passphrase, the recovery
acknowledgement waits for gum's placeholder as it is drawn, a relative
--payload unpacks, and first boot proves the deferred Limine step rebuilt
the menu for this machine.
QEMU takes serial input only as fast as the guest drains the UART's 16-byte
FIFO and drops the rest when the writer disconnects, which cut every typed
line after 16 bytes.
… past it

Its drive list is an unprivileged blkid, which finds no drive until root has
run blkid this boot. The scenario records that, warms the cache with sudo as
an owner could, and goes on to test the change itself. luks_slot returns its
slot in a variable, so a cryptsetup error fails the run instead of reading as
a refused passphrase.
After a drive password change, a refused login is a finding about the image,
not a harness failure: the case says which password still logs in, and the
cases after it still run.
@maralcbr
maralcbr merged commit f68dc82 into quattro-upstream Sep 25, 2026
5 checks passed
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.

1 participant