qemu: real-image milestone — boot an actual flare-edge build in the VM
mkimage gains --rootfs-image/--oem-image (raw ext4 matched pair placed into slot A, size-checked fail-closed; slot B keeps the skeleton as a known-good fallback), and stage-1 rc now hands the live devtmpfs to the new root across switch_root — busybox switch_root moves nothing, our skeleton init remounted defensively, but a REAL image's getty opens /dev/console immediately and looped on ENOENT without it. Verified with the 2026-08-24 flare-edge build (rootfs 192M + oem 26M): the image's own rcS chain runs on the 6.18 virt kernel, real daemons start (warden-modbus confirmed running via root shell; the shipped 0.2.4 flared reproduces flare-edge#106 exactly, and S99hciinit reports the issue-#4 btlpm symptom — the VM doubles as a faithful reproducer of known field bugs), and a getty answers on the console. qemu/tests/real-image-boot.sh asserts switch_root + daemon starts + login prompt, credential-free. REAL-IMAGE-BOOT-PASS. Regression sweep after the shared-path changes: OTA-APPLY-PASS, PORTAL-SCENARIO-PASS, UI-SHOT-PASS, boot smoke OK. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HUayid7W5w7jBdb9Rrj1K
This commit is contained in:
co-authored by
Claude Fable 5
parent
d63c2117c1
commit
7ab07a35d3
@@ -88,6 +88,12 @@ stage-2 init when present.
|
|||||||
and the physical reset stay emulated by the harness (ADR-0006 boundary);
|
and the physical reset stay emulated by the harness (ADR-0006 boundary);
|
||||||
the VM exports `WARDEN_HARD_RESET=0` so flared's post-apply reset surfaces
|
the VM exports `WARDEN_HARD_RESET=0` so flared's post-apply reset surfaces
|
||||||
as a clean reported error instead of a /dev/mem fault.
|
as a clean reported error instead of a /dev/mem fault.
|
||||||
|
- `real-image-boot.sh <zImage> <rootfs.img> <oem.img>` — the real-image
|
||||||
|
milestone: an ACTUAL flare-edge build (matched pair, placed by
|
||||||
|
`mkimage.sh --rootfs-image/--oem-image`) boots through its own init chain
|
||||||
|
to a getty on the VM console; real daemons start. RV1106-only init steps
|
||||||
|
degrade as documented, and binaries predating known fixes reproduce their
|
||||||
|
bugs faithfully (a feature: the VM is a time machine for field issues).
|
||||||
- Watchdog: `run.sh --watchdog`, arm `/dev/watchdog` in the guest, don't pet —
|
- Watchdog: `run.sh --watchdog`, arm `/dev/watchdog` in the guest, don't pet —
|
||||||
the VM resets ~30 s later (verified). Do NOT combine with a flared payload
|
the VM resets ~30 s later (verified). Do NOT combine with a flared payload
|
||||||
expecting survival: flared pets only while the UI heartbeat is fresh.
|
expecting survival: flared pets only while the UI heartbeat is fresh.
|
||||||
|
|||||||
+41
-4
@@ -10,6 +10,10 @@
|
|||||||
# sudo), then dd'd into a sparse raw image.
|
# sudo), then dd'd into a sparse raw image.
|
||||||
#
|
#
|
||||||
# Usage: mkimage.sh [--portal-url URL] [--state KEY=VALUE]... [--fw-version V]
|
# Usage: mkimage.sh [--portal-url URL] [--state KEY=VALUE]... [--fw-version V]
|
||||||
|
# [--rootfs-image PATH] [--oem-image PATH]
|
||||||
|
# --rootfs-image/--oem-image place REAL device images (raw ext4, e.g. a
|
||||||
|
# flare-edge build's rootfs.img/oem.img matched pair) into slot A instead of
|
||||||
|
# the busybox skeleton; slot B keeps the skeleton as a known-good fallback.
|
||||||
# Env:
|
# Env:
|
||||||
# BUSYBOX path to a local busybox binary (skips the download; still verified)
|
# BUSYBOX path to a local busybox binary (skips the download; still verified)
|
||||||
# OUT output dir (default: qemu/out); image at $OUT/disk.img
|
# OUT output dir (default: qemu/out); image at $OUT/disk.img
|
||||||
@@ -27,6 +31,8 @@ PATH="$PATH:/usr/sbin:/sbin"
|
|||||||
PORTAL_URL=""
|
PORTAL_URL=""
|
||||||
STATE_KV=()
|
STATE_KV=()
|
||||||
FW_VERSION="0.0.1"
|
FW_VERSION="0.0.1"
|
||||||
|
ROOTFS_IMAGE=""
|
||||||
|
OEM_IMAGE=""
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--portal-url) PORTAL_URL="${2:?--portal-url needs a value}"; shift 2 ;;
|
--portal-url) PORTAL_URL="${2:?--portal-url needs a value}"; shift 2 ;;
|
||||||
@@ -45,6 +51,8 @@ while [ $# -gt 0 ]; do
|
|||||||
esac
|
esac
|
||||||
STATE_KV+=("$2"); shift 2 ;;
|
STATE_KV+=("$2"); shift 2 ;;
|
||||||
--fw-version) FW_VERSION="${2:?--fw-version needs a value}"; shift 2 ;;
|
--fw-version) FW_VERSION="${2:?--fw-version needs a value}"; shift 2 ;;
|
||||||
|
--rootfs-image) ROOTFS_IMAGE="${2:?--rootfs-image needs a path}"; shift 2 ;;
|
||||||
|
--oem-image) OEM_IMAGE="${2:?--oem-image needs a path}"; shift 2 ;;
|
||||||
*) echo "FATAL: unknown argument '$1' (usage: mkimage.sh [--portal-url URL] [--state KEY=VALUE]... [--fw-version V])" >&2; exit 1 ;;
|
*) echo "FATAL: unknown argument '$1' (usage: mkimage.sh [--portal-url URL] [--state KEY=VALUE]... [--fw-version V])" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@@ -90,12 +98,40 @@ mkfs_part() {
|
|||||||
DISK="$OUT/disk.img"
|
DISK="$OUT/disk.img"
|
||||||
rm -f "$DISK"
|
rm -f "$DISK"
|
||||||
|
|
||||||
|
# dd a REAL raw image into a partition window, fail-closed on overflow.
|
||||||
|
place_real_image() { # $1 src image, $2 offset, $3 partition size, $4 name
|
||||||
|
local srcsz
|
||||||
|
srcsz=$(stat -c %s "$1")
|
||||||
|
[ "$srcsz" -le "$3" ] || {
|
||||||
|
echo "FATAL: $4 image $1 ($srcsz bytes) exceeds the $3-byte partition" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
dd if="$1" of="$DISK" bs=4096 seek=$(($2 / 4096)) conv=notrunc,sparse status=none
|
||||||
|
qemu_log " $4: REAL image $(basename "$1") ($((srcsz / 1048576))M) @ $2"
|
||||||
|
}
|
||||||
|
|
||||||
place_partition() {
|
place_partition() {
|
||||||
local name="$1" off="$2" size="$3" stage=""
|
local name="$1" off="$2" size="$3" stage=""
|
||||||
case "$name" in
|
case "$name" in
|
||||||
rootfs_a|rootfs_b) stage="$ROOT" ;;
|
rootfs_a)
|
||||||
|
if [ -n "$ROOTFS_IMAGE" ]; then
|
||||||
|
[ -f "$ROOTFS_IMAGE" ] || { echo "FATAL: --rootfs-image $ROOTFS_IMAGE not found" >&2; exit 1; }
|
||||||
|
DISK_END_TRACK "$off" "$size"
|
||||||
|
place_real_image "$ROOTFS_IMAGE" "$off" "$size" "$name"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
stage="$ROOT" ;;
|
||||||
|
oem_a)
|
||||||
|
if [ -n "$OEM_IMAGE" ]; then
|
||||||
|
[ -f "$OEM_IMAGE" ] || { echo "FATAL: --oem-image $OEM_IMAGE not found" >&2; exit 1; }
|
||||||
|
DISK_END_TRACK "$off" "$size"
|
||||||
|
place_real_image "$OEM_IMAGE" "$off" "$size" "$name"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
stage="$SCRATCH/empty" ;;
|
||||||
|
rootfs_b) stage="$ROOT" ;;
|
||||||
userdata) stage="$UDATA" ;;
|
userdata) stage="$UDATA" ;;
|
||||||
oem_a|oem_b) stage="$SCRATCH/empty" ;;
|
oem_b) stage="$SCRATCH/empty" ;;
|
||||||
# misc carries REAL AvbABData (byte 2048): flared's slotctl fail-closes
|
# misc carries REAL AvbABData (byte 2048): flared's slotctl fail-closes
|
||||||
# on a bad magic before any OTA write, so a zeroed misc blocks apply
|
# on a bad magic before any OTA write, so a zeroed misc blocks apply
|
||||||
# scenarios. Bytes mirror flare-edge tools/mk-misc.py provisioning
|
# scenarios. Bytes mirror flare-edge tools/mk-misc.py provisioning
|
||||||
@@ -107,14 +143,13 @@ place_partition() {
|
|||||||
env|idblock|uboot|boot_a|boot_b|recovery) stage="" ;;
|
env|idblock|uboot|boot_a|boot_b|recovery) stage="" ;;
|
||||||
*) echo "FATAL: unknown partition name '$name' in blkdevparts.conf" >&2; exit 1 ;;
|
*) echo "FATAL: unknown partition name '$name' in blkdevparts.conf" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
DISK_END_TRACK "$off" "$size"
|
||||||
# dd in 4K blocks — every offset in the canonical layout is 4K-aligned;
|
# dd in 4K blocks — every offset in the canonical layout is 4K-aligned;
|
||||||
# assert rather than assume, a misaligned write would corrupt a neighbor.
|
# assert rather than assume, a misaligned write would corrupt a neighbor.
|
||||||
if [ $((off % 4096)) -ne 0 ] || [ $((size % 4096)) -ne 0 ]; then
|
if [ $((off % 4096)) -ne 0 ] || [ $((size % 4096)) -ne 0 ]; then
|
||||||
echo "FATAL: partition $name not 4K-aligned (off=$off size=$size)" >&2
|
echo "FATAL: partition $name not 4K-aligned (off=$off size=$size)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
# Max, not last: blkdevparts grammar permits explicit @offsets out of order.
|
|
||||||
[ $((off + size)) -gt "$DISK_END" ] && DISK_END=$((off + size))
|
|
||||||
[ -z "$stage" ] && return 0
|
[ -z "$stage" ] && return 0
|
||||||
local img="$SCRATCH/$name.img"
|
local img="$SCRATCH/$name.img"
|
||||||
if [ "$stage" = "__misc__" ]; then
|
if [ "$stage" = "__misc__" ]; then
|
||||||
@@ -143,6 +178,8 @@ PYMISC
|
|||||||
}
|
}
|
||||||
|
|
||||||
DISK_END=0
|
DISK_END=0
|
||||||
|
# Max, not last: blkdevparts grammar permits explicit @offsets out of order.
|
||||||
|
DISK_END_TRACK() { [ $(($1 + $2)) -gt "$DISK_END" ] && DISK_END=$(($1 + $2)); return 0; }
|
||||||
qemu_log "building $DISK ($WARDEN_BLKDEVPARTS)"
|
qemu_log "building $DISK ($WARDEN_BLKDEVPARTS)"
|
||||||
truncate -s 0 "$DISK"
|
truncate -s 0 "$DISK"
|
||||||
qemu_each_partition place_partition
|
qemu_each_partition place_partition
|
||||||
|
|||||||
@@ -39,5 +39,12 @@ if ! command -v switch_root >/dev/null; then
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Hand the live devtmpfs to the new root: busybox switch_root moves nothing,
|
||||||
|
# and a REAL device rootfs's init expects /dev to already be there (its getty
|
||||||
|
# opens /dev/console immediately). Our own skeleton init remounts devtmpfs
|
||||||
|
# defensively either way.
|
||||||
|
mkdir -p /mnt/dev
|
||||||
|
mount -o move /dev /mnt/dev 2>/dev/null || mount --move /dev /mnt/dev
|
||||||
|
|
||||||
echo "rc: switching root to rootfs${slot} ($root)"
|
echo "rc: switching root to rootfs${slot} ($root)"
|
||||||
exec switch_root /mnt /sbin/init
|
exec switch_root /mnt /sbin/init
|
||||||
|
|||||||
Executable
+82
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Real-image milestone: boot an ACTUAL flare-edge build (rootfs.img + oem.img
|
||||||
|
# matched pair) in the VM on the 6.18 kernel and assert its own init chain
|
||||||
|
# reaches multi-user: the vendor rcS runs, real warden daemons start, and a
|
||||||
|
# getty answers on the console.
|
||||||
|
#
|
||||||
|
# Documented caveats (this is a fidelity milestone, not full parity): the
|
||||||
|
# RV1106-only init steps degrade on virt (backlight, goodix, npu, the 5.10
|
||||||
|
# /oem modules fail vermagic), and binaries older than the flare-edge #106
|
||||||
|
# fix reproduce that crash faithfully. Interactive login uses the image's own
|
||||||
|
# credentials — deliberately not recorded here.
|
||||||
|
#
|
||||||
|
# FAILS CLOSED on missing prerequisites.
|
||||||
|
#
|
||||||
|
# Usage: real-image-boot.sh <zImage> <rootfs.img> <oem.img>
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # qemu/tests/
|
||||||
|
QDIR="$(cd "$HERE/.." && pwd)" # qemu/
|
||||||
|
|
||||||
|
ZIMAGE="${1:-}"; ROOTFS="${2:-}"; OEM="${3:-}"
|
||||||
|
for f in "$ZIMAGE" "$ROOTFS" "$OEM"; do
|
||||||
|
if [ -z "$f" ] || [ ! -f "$f" ]; then
|
||||||
|
echo "FATAL: usage: $0 <zImage> <rootfs.img> <oem.img> — '$f' missing" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
command -v qemu-system-arm >/dev/null || {
|
||||||
|
echo "FATAL: qemu-system-arm not on PATH — see qemu/README.md" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
WORK="$(mktemp -d /tmp/wqr.XXXXXX)"
|
||||||
|
QEMU_PID=""
|
||||||
|
cleanup() {
|
||||||
|
if [ -n "$QEMU_PID" ]; then kill "$QEMU_PID" 2>/dev/null || true; fi
|
||||||
|
rm -rf "$WORK"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
bash "$QDIR/mkinitramfs.sh"
|
||||||
|
bash "$QDIR/mkimage.sh" --rootfs-image "$ROOTFS" --oem-image "$OEM"
|
||||||
|
|
||||||
|
for _attempt in 1 2 3; do
|
||||||
|
PORT=$((23000 + RANDOM % 20000))
|
||||||
|
: > "$WORK/console.log"
|
||||||
|
bash "$QDIR/run.sh" --kernel "$ZIMAGE" \
|
||||||
|
--ssh-port "$PORT" --http-port $((PORT + 1)) --api-port $((PORT + 2)) \
|
||||||
|
> "$WORK/console.log" 2>&1 &
|
||||||
|
QEMU_PID=$!
|
||||||
|
sleep 3
|
||||||
|
kill -0 "$QEMU_PID" 2>/dev/null && break
|
||||||
|
if grep -aq 'Could not set up host forwarding' "$WORK/console.log"; then
|
||||||
|
echo "== hostfwd port collision on base $PORT — retrying"
|
||||||
|
QEMU_PID=""
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "FATAL: VM died at launch:" >&2; tail -20 "$WORK/console.log" >&2; exit 1
|
||||||
|
done
|
||||||
|
if [ -z "$QEMU_PID" ] || ! kill -0 "$QEMU_PID" 2>/dev/null; then
|
||||||
|
echo "FATAL: could not launch the VM after 3 port attempts" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
deadline=$((SECONDS + 180))
|
||||||
|
ok_switch=0 ok_daemons=0 ok_getty=0
|
||||||
|
while [ $SECONDS -lt $deadline ]; do
|
||||||
|
grep -aq 'rc: switching root to rootfs_a' "$WORK/console.log" && ok_switch=1
|
||||||
|
[ "$(grep -ac 'Starting warden-' "$WORK/console.log")" -ge 2 ] && ok_daemons=1
|
||||||
|
grep -aq 'login:' "$WORK/console.log" && ok_getty=1
|
||||||
|
[ $ok_switch -eq 1 ] && [ $ok_daemons -eq 1 ] && [ $ok_getty -eq 1 ] && break
|
||||||
|
kill -0 "$QEMU_PID" 2>/dev/null || {
|
||||||
|
echo "FATAL: VM exited early" >&2; tail -30 "$WORK/console.log" >&2; exit 1; }
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
[ $ok_switch -eq 1 ] || { echo "FAIL: never switch_rooted into the real image"; fail=1; }
|
||||||
|
[ $ok_daemons -eq 1 ] || { echo "FAIL: the image's own init never started warden daemons"; fail=1; }
|
||||||
|
[ $ok_getty -eq 1 ] || { echo "FAIL: no getty login prompt on the console"; fail=1; }
|
||||||
|
[ $fail -eq 0 ] || { tail -25 "$WORK/console.log" >&2; exit 1; }
|
||||||
|
echo "REAL-IMAGE-BOOT-PASS: the flare-edge image reached multi-user on the VM"
|
||||||
Reference in New Issue
Block a user