Merge remote-tracking branch 'origin/main' into docs-reposition

This commit is contained in:
BFE Engineering
2026-08-31 08:00:33 -06:00
16 changed files with 586 additions and 9 deletions
+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="116" height="20" role="img" aria-label="lines of code: 9048"><title>lines of code: 9048</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="79" height="20" fill="#555"/><rect x="79" width="37" height="20" fill="#007ec6"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="405" y="150" fill-opacity=".8" filter="url(#blur)" textLength="690">lines of code</text><text x="405" y="150" fill-opacity=".3" textLength="690">lines of code</text></g><text x="405" y="140" textLength="690">lines of code</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="965" y="150" fill-opacity=".8" filter="url(#blur)" textLength="270">9048</text><text x="965" y="150" fill-opacity=".3" textLength="270">9048</text></g><text x="965" y="140" textLength="270">9048</text></g></g></svg> <svg xmlns="http://www.w3.org/2000/svg" width="124" height="20" role="img" aria-label="lines of code: 10180"><title>lines of code: 10180</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="124" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="79" height="20" fill="#555"/><rect x="79" width="45" height="20" fill="#007ec6"/><rect width="124" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="405" y="150" fill-opacity=".8" filter="url(#blur)" textLength="690">lines of code</text><text x="405" y="150" fill-opacity=".3" textLength="690">lines of code</text></g><text x="405" y="140" textLength="690">lines of code</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="1005" y="150" fill-opacity=".8" filter="url(#blur)" textLength="350">10180</text><text x="1005" y="150" fill-opacity=".3" textLength="350">10180</text></g><text x="1005" y="140" textLength="350">10180</text></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="tests: 46 passing"><title>tests: 46 passing</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="106" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="69" height="20" fill="#4b0"/><rect width="106" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="195" y="150" fill-opacity=".8" filter="url(#blur)" textLength="270">tests</text><text x="195" y="150" fill-opacity=".3" textLength="270">tests</text></g><text x="195" y="140" textLength="270">tests</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="705" y="150" fill-opacity=".8" filter="url(#blur)" textLength="590">46 passing</text><text x="705" y="150" fill-opacity=".3" textLength="590">46 passing</text></g><text x="705" y="140" textLength="590">46 passing</text></g></g></svg> <svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="tests: 53 passing"><title>tests: 53 passing</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="106" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="69" height="20" fill="#4b0"/><rect width="106" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="195" y="150" fill-opacity=".8" filter="url(#blur)" textLength="270">tests</text><text x="195" y="150" fill-opacity=".3" textLength="270">tests</text></g><text x="195" y="140" textLength="270">tests</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="705" y="150" fill-opacity=".8" filter="url(#blur)" textLength="590">53 passing</text><text x="705" y="150" fill-opacity=".3" textLength="590">53 passing</text></g><text x="705" y="140" textLength="590">53 passing</text></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

+7 -1
View File
@@ -35,7 +35,7 @@ jobs:
run: | run: |
set -o pipefail set -o pipefail
: > /tmp/test.log : > /tmp/test.log
for d in sim tools/config-lint qemu/rs485-bridge; do for d in sim tools/config-lint qemu/rs485-bridge qemu/tests/clockprobe; do
echo "== cargo test in $d ==" | tee -a /tmp/test.log echo "== cargo test in $d ==" | tee -a /tmp/test.log
( cd "$d" && cargo test --locked ) 2>&1 | tee -a /tmp/test.log ( cd "$d" && cargo test --locked ) 2>&1 | tee -a /tmp/test.log
done done
@@ -220,6 +220,11 @@ jobs:
CROSS_COMPILE: arm-linux-gnueabihf- CROSS_COMPILE: arm-linux-gnueabihf-
WARDEN_CCACHE: 1 WARDEN_CCACHE: 1
CCACHE_DIR: /home/runner/.ccache CCACHE_DIR: /home/runner/.ccache
# Matched module set for the out-of-tree wifi/BT drivers (issue #4:
# a stale 5.10 .ko was all the panel had). Full `make modules` runs
# (zImage alone emits no Module.symvers); only these dirs' .ko files
# are collected/uploaded.
WARDEN_MODULES_COLLECT: drivers/net/wireless/aic8800
# KERNEL_TARBALL is exported from the SHELL so $HOME expands — a literal # KERNEL_TARBALL is exported from the SHELL so $HOME expands — a literal
# `~` in a YAML env: value is never tilde-expanded and broke every # `~` in a YAML env: value is never tilde-expanded and broke every
# dispatch until caught in review. # dispatch until caught in review.
@@ -246,6 +251,7 @@ jobs:
path: | path: |
${{ runner.temp }}/kbuild-out/linux-6.18.46/arch/arm/boot/zImage ${{ runner.temp }}/kbuild-out/linux-6.18.46/arch/arm/boot/zImage
${{ runner.temp }}/kbuild-out/linux-6.18.46/arch/arm/boot/dts/rockchip/rv1106-warden.dtb ${{ runner.temp }}/kbuild-out/linux-6.18.46/arch/arm/boot/dts/rockchip/rv1106-warden.dtb
${{ runner.temp }}/kbuild-out/modules-out/*.ko
# Short so kernel images self-expire instead of piling into the # Short so kernel images self-expire instead of piling into the
# account-wide storage quota; the prune-artifacts job above is the # account-wide storage quota; the prune-artifacts job above is the
# active bound, this is the backstop. # active bound, this is the backstop.
+28
View File
@@ -19,6 +19,12 @@
# (qemu/configs/virt.fragment builds the QEMU -M virt variant); # (qemu/configs/virt.fragment builds the QEMU -M virt variant);
# every fragment option is verified to have taken effect # every fragment option is verified to have taken effect
# WARDEN_CCACHE=1 compile through ccache (CI caches ~/.ccache) # WARDEN_CCACHE=1 compile through ccache (CI caches ~/.ccache)
# WARDEN_MODULES_COLLECT
# space-separated in-tree dirs whose .ko files are wanted
# (e.g. "drivers/net/wireless/aic8800"). Runs a full
# `make modules` (the zImage target alone generates no
# Module.symvers, so per-dir M= builds cannot link) and
# collects the listed dirs' modules into $WORK/modules-out.
# #
# Requires: `python` (not python3) on PATH — the SDK quirk; the CI runner provides # Requires: `python` (not python3) on PATH — the SDK quirk; the CI runner provides
# a project-local venv. Builds are SERIAL on the shared SDK box — never run two. # a project-local venv. Builds are SERIAL on the shared SDK box — never run two.
@@ -157,6 +163,28 @@ log "building zImage + rv1106-warden.dtb (-j$JOBS)"
make -C "$SRC" ARCH=arm CROSS_COMPILE="$CROSS_COMPILE" CC="$KCC" -j"$JOBS" \ make -C "$SRC" ARCH=arm CROSS_COMPILE="$CROSS_COMPILE" CC="$KCC" -j"$JOBS" \
zImage rockchip/rv1106-warden.dtb zImage rockchip/rv1106-warden.dtb
# Optional module set (issue #4: the panel loaded a stale 5.10 .ko because
# this build never produced matched 6.18 modules). Full `make modules` is
# required — zImage alone emits no Module.symvers, so a per-directory M=
# build cannot resolve even core symbols. FAILS CLOSED if a listed dir
# yields no modules.
if [ -n "${WARDEN_MODULES_COLLECT:-}" ]; then
MODOUT="$WORK/modules-out"
rm -rf "$MODOUT"; mkdir -p "$MODOUT"
log "building modules (full set — needed for Module.symvers)"
make -C "$SRC" ARCH=arm CROSS_COMPILE="$CROSS_COMPILE" CC="$KCC" -j"$JOBS" modules
for d in $WARDEN_MODULES_COLLECT; do
[ -d "$SRC/$d" ] || { echo "FATAL: WARDEN_MODULES_COLLECT dir '$d' not in tree" >&2; exit 1; }
n=0
while IFS= read -r ko; do
cp "$ko" "$MODOUT/"; n=$((n + 1))
done < <(find "$SRC/$d" -name '*.ko')
[ "$n" -gt 0 ] || { echo "FATAL: no .ko produced under '$d'" >&2; exit 1; }
log "collected $n module(s) from $d"
done
ls -la "$MODOUT"
fi
Z="$SRC/arch/arm/boot/zImage" Z="$SRC/arch/arm/boot/zImage"
D="$SRC/arch/arm/boot/dts/rockchip/rv1106-warden.dtb" D="$SRC/arch/arm/boot/dts/rockchip/rv1106-warden.dtb"
log "build OK" log "build OK"
+54
View File
@@ -0,0 +1,54 @@
# arch-timer / vDSO clock fix — plan (issue #3)
Status: DIAGNOSED off-board, fix gated on two bench measurements.
## Established (2026-08-30, qemu/ device sim)
The same kernel family under `qemu-system-arm -M virt` gives musl
`CLOCK_MONOTONIC`/`_RAW` rate ratio 0.99963 vs `/proc/uptime` (interval
measurement, `qemu/tests/clockprobe`). The generic 6.18 armv7 vDSO is
therefore CORRECT; the board symptom (musl reads ~12% high, kernel time
right) is RV1106-specific. The boot chain runs in the secure world and is
closed rkbin — the NS view of the CPU timer registers (CNTFRQ, CNTVOFF) is
whatever it left behind, and only the arch-counter path (vDSO,
`arch_sys_counter`) trusts them.
## What the bench must answer (single boot of the `_b` slot)
Run `clockprobe` (interval mode) on the 6.18 slot:
1. `ratio_mono` far from 1.0 -> RATE error: CNTFRQ wrong. The true rate =
claimed rate (dmesg `arch_timer: cp15 timer running at X MHz`) times the
measured ratio.
2. `ratio_mono` ~= 1.0 but `abs_ratio` far from 1.0 -> OFFSET error: CNTVOFF
left nonzero; rate fine.
(The original issue measured only one absolute sample, which cannot
distinguish these.)
## The fix (both cases, one DT override)
Append to the BOARD dts (`rv1106-warden.dts` — never the vendor dtsi) an
override on the armv7-timer node:
arm,cpu-registers-not-fw-configured;
clock-frequency = <MEASURED_HZ>;
The property makes the driver use the physical counter, ignore CNTVOFF, and
take the frequency from DT — the documented remedy for firmware that does
not configure the CPU timer registers. `MEASURED_HZ` comes from bench
answer 1 (do NOT guess; a wrong value makes every clock wrong instead of
one path). Ship as an update to the arch/dts patch in `patches/`.
## Regression guards
- Off-board: `qemu/tests/clock-sanity.sh` asserts the vDSO rate in the VM
(guards the generic path; cannot see board registers).
- On-board: re-run `clockprobe` on the patched `_b` slot; both ratios and
the absolute ratio must be ~1.0. Record the numbers here and in issue #3.
## Bench access note
2026-08-30: c8a3 is physically dark (CP2102 console silent through two
remote power cycles; both network paths down) — needs hands at the bench
before the measurements can run.
+15
View File
@@ -76,6 +76,21 @@ stage-2 init when present.
(a 200 ms hold — an instantaneous press+release lands inside one LVGL poll (a 200 ms hold — an instantaneous press+release lands inside one LVGL poll
and never clicks), and asserts the frame changed. `qmp.py` is the tiny QMP and never clicks), and asserts the frame changed. `qmp.py` is the tiny QMP
client. client.
- `ota-apply.sh <zImage>` (needs `FLARE_EDGE`) — the FULL apply loop the
portal scenario stops short of: a real signed tier-1 `.wfw` whose payload
is a bootable rootfs is pulled, verified, WRITTEN to rootfs_b
(`run.sh --allow-apply` gates it per boot), the AvbABData in `misc` is
flipped (mkimage provisions real A/B metadata), and the harness reboots
slot `_b` and asserts the applied version is running. The BCB slot CHOICE
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
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.
+66 -5
View File
@@ -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,28 +98,79 @@ 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
# on a bad magic before any OTA write, so a zeroed misc blocks apply
# scenarios. Bytes mirror flare-edge tools/mk-misc.py provisioning
# defaults (A: prio 15 successful, B: prio 14 successful, CRC32-BE).
misc) stage="__misc__" ;;
# Boot-chain partitions the VM never reads: present at the right offsets, # Boot-chain partitions the VM never reads: present at the right offsets,
# left zeroed. Enumerated (not a wildcard) so a typo'd name in # left zeroed. Enumerated (not a wildcard) so a typo'd name in
# blkdevparts.conf fails HERE, not as a confusing mount error at boot. # blkdevparts.conf fails HERE, not as a confusing mount error at boot.
env|idblock|uboot|misc|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
python3 - "$img" "$size" <<'PYMISC'
import struct, sys, zlib
img, size = sys.argv[1], int(sys.argv[2])
s = bytearray(28)
s[0:4] = b"\0AB0" # AB_MAGIC
s[4] = 1 # major
s[8:12] = bytes([15, 0, 1, 0]) # slot A: priority, tries, successful
s[12:16] = bytes([14, 0, 1, 0]) # slot B
meta = bytes(s) + struct.pack(">I", zlib.crc32(bytes(s)) & 0xFFFFFFFF)
buf = bytearray(size)
buf[2048:2048 + len(meta)] = meta
open(img, "wb").write(buf)
PYMISC
dd if="$img" of="$DISK" bs=4096 seek=$((off / 4096)) \
conv=notrunc,sparse status=none
qemu_log " $name: AvbABData provisioned @ +2048"
return 0
fi
mkfs_part "$stage" "$size" "$img" mkfs_part "$stage" "$size" "$img"
dd if="$img" of="$DISK" bs=4096 seek=$((off / 4096)) \ dd if="$img" of="$DISK" bs=4096 seek=$((off / 4096)) \
conv=notrunc,sparse status=none conv=notrunc,sparse status=none
@@ -119,6 +178,8 @@ place_partition() {
} }
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
+7
View File
@@ -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
+10
View File
@@ -70,6 +70,16 @@ export WARDEN_FLARE_INSECURE=1
# here, and flared's /dev/mem poke dies with an external abort (SIGBUS). The # here, and flared's /dev/mem poke dies with an external abort (SIGBUS). The
# SCR1 supervisor state machine is modeled in sim/src/hpmcu.rs instead. # SCR1 supervisor state machine is modeled in sim/src/hpmcu.rs instead.
export WARDEN_HPMCU=0 export WARDEN_HPMCU=0
# Same class: the CRU reset ladder's /dev/mem poke is fatal on virt. A
# post-apply "reboot" surfaces as a clean flared error; the scenario harness
# performs the actual reboot into the applied slot.
export WARDEN_HARD_RESET=0
# OTA apply is opt-in per boot (run.sh --allow-apply): writing rootfs_b is
# safe inside disk.img but must never be the default posture.
if grep -qw warden.fwapply /proc/cmdline; then
export WARDEN_FW_ALLOW_APPLY=1
echo "init: OTA APPLY ENABLED (warden.fwapply)"
fi
for d in /usr/bin/warden-flared /usr/bin/warden-modbus; do for d in /usr/bin/warden-flared /usr/bin/warden-modbus; do
if [ -x "$d" ]; then if [ -x "$d" ]; then
name="$(basename "$d")" name="$(basename "$d")"
+5 -1
View File
@@ -22,6 +22,8 @@
# --http-port N hostfwd 127.0.0.1:N -> guest :80 (default 8080; 0 disables) # --http-port N hostfwd 127.0.0.1:N -> guest :80 (default 8080; 0 disables)
# --api-port N hostfwd 127.0.0.1:N -> guest :28443 (default 28443; 0 disables) # --api-port N hostfwd 127.0.0.1:N -> guest :28443 (default 28443; 0 disables)
# --shell interactive shell in the guest instead of daemon hold # --shell interactive shell in the guest instead of daemon hold
# --allow-apply let flared ACTUALLY apply OTA firmware (writes rootfs_b
# inside disk.img — safe in the VM, never the default)
set -euo pipefail set -euo pipefail
QEMU_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" QEMU_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -30,7 +32,7 @@ QEMU_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUT="${OUT:-$QEMU_DIR/out}" OUT="${OUT:-$QEMU_DIR/out}"
KERNEL="" INITRD="$OUT/initramfs.cpio.gz" DISK="" NO_DISK=0 SLOT="_a" KERNEL="" INITRD="$OUT/initramfs.cpio.gz" DISK="" NO_DISK=0 SLOT="_a"
RTC="" RS485="" WATCHDOG=0 QMP="" DISPLAY_MODE="off" SHELL_FLAG=0 RTC="" RS485="" WATCHDOG=0 QMP="" DISPLAY_MODE="off" SHELL_FLAG=0 ALLOW_APPLY=0
SSH_PORT=2222 HTTP_PORT=8080 API_PORT=28443 SSH_PORT=2222 HTTP_PORT=8080 API_PORT=28443
EXTRA=() EXTRA=()
@@ -50,6 +52,7 @@ while [ $# -gt 0 ]; do
--http-port) HTTP_PORT="${2:?}"; shift 2 ;; --http-port) HTTP_PORT="${2:?}"; shift 2 ;;
--api-port) API_PORT="${2:?}"; shift 2 ;; --api-port) API_PORT="${2:?}"; shift 2 ;;
--shell) SHELL_FLAG=1; shift ;; --shell) SHELL_FLAG=1; shift ;;
--allow-apply) ALLOW_APPLY=1; shift ;;
--) shift; EXTRA=("$@"); break ;; --) shift; EXTRA=("$@"); break ;;
*) echo "FATAL: unknown argument '$1' (see header of $0)" >&2; exit 1 ;; *) echo "FATAL: unknown argument '$1' (see header of $0)" >&2; exit 1 ;;
esac esac
@@ -99,6 +102,7 @@ if [ -n "$DISK" ] && [ "$NO_DISK" -eq 0 ]; then
-device "virtio-blk-device,drive=vd0" ) -device "virtio-blk-device,drive=vd0" )
fi fi
[ "$SHELL_FLAG" -eq 1 ] && APPEND="$APPEND warden.shell" [ "$SHELL_FLAG" -eq 1 ] && APPEND="$APPEND warden.shell"
[ "$ALLOW_APPLY" -eq 1 ] && APPEND="$APPEND warden.fwapply"
[ -n "$RTC" ] && ARGS+=( -rtc "base=$RTC" ) [ -n "$RTC" ] && ARGS+=( -rtc "base=$RTC" )
[ "$WATCHDOG" -eq 1 ] && ARGS+=( -device i6300esb -action watchdog=reset ) [ "$WATCHDOG" -eq 1 ] && ARGS+=( -device i6300esb -action watchdog=reset )
[ -n "$RS485" ] && ARGS+=( -chardev "socket,id=rs485,path=$RS485,server=on,wait=off" [ -n "$RS485" ] && ARGS+=( -chardev "socket,id=rs485,path=$RS485,server=on,wait=off"
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# Clock-sanity scenario (issue #3 regression guard): boot the VM, run the
# musl-static clockprobe in the guest, and assert the vDSO monotonic RATE
# matches the kernel's /proc/uptime within 1%. Under QEMU -M virt this passes
# on the current kernel (measured 0.99963) — a regression here means the
# generic vDSO path broke. The RV1106 *board* leg of issue #3 is a separate,
# bench-only measurement; this scenario cannot see board-specific CNTFRQ or
# CNTVOFF misprogramming.
#
# FAILS CLOSED on missing prerequisites.
#
# Usage: clock-sanity.sh <zImage>
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # qemu/tests/
QDIR="$(cd "$HERE/.." && pwd)" # qemu/
ZIMAGE="${1:-}"
if [ -z "$ZIMAGE" ] || [ ! -f "$ZIMAGE" ]; then
echo "FATAL: usage: $0 <zImage>" >&2
exit 1
fi
command -v qemu-system-arm >/dev/null || {
echo "FATAL: qemu-system-arm not on PATH — see qemu/README.md" >&2
exit 1
}
command -v arm-linux-gnueabihf-gcc >/dev/null || {
echo "FATAL: arm-linux-gnueabihf-gcc needed to cross-build the probe" >&2
exit 1
}
# Build the probe for the guest (static musl armv7) and stage it as payload.
( cd "$HERE/clockprobe" && \
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER=arm-linux-gnueabihf-gcc \
cargo build -q --release --target armv7-unknown-linux-musleabihf )
install -m 0755 "$HERE/clockprobe/target/armv7-unknown-linux-musleabihf/release/clockprobe" \
"$QDIR/payload/clockprobe"
WORK="$(mktemp -d /tmp/wqc.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"
for _attempt in 1 2 3; do
PORT=$((22000 + RANDOM % 20000))
: > "$WORK/console.log"
{ sleep 40; printf '/usr/bin/clockprobe\n'; sleep 16; printf 'poweroff -f\n'; sleep 8; } | \
bash "$QDIR/run.sh" --kernel "$ZIMAGE" --shell \
--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
wait "$QEMU_PID" || true
grep -a 'CLOCKPROBE' "$WORK/console.log" || {
echo "FATAL: probe never ran; console tail:" >&2
tail -25 "$WORK/console.log" >&2
exit 1
}
python3 - "$WORK/console.log" <<'EOF'
import re, sys
text = open(sys.argv[1], errors="replace").read()
m = re.search(r"ratio_mono=([0-9.]+) ratio_raw=([0-9.]+)", text)
if not m:
sys.exit("FATAL: no ratio line in console output")
mono, raw = float(m.group(1)), float(m.group(2))
ok = abs(mono - 1.0) < 0.01 and abs(raw - 1.0) < 0.01
print(f"clock-sanity: ratio_mono={mono} ratio_raw={raw} -> {'PASS' if ok else 'FAIL'}")
sys.exit(0 if ok else 1)
EOF
echo "CLOCK-SANITY-PASS"
+16
View File
@@ -0,0 +1,16 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "clockprobe"
version = "0.1.0"
dependencies = [
"libc",
]
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "clockprobe"
version = "0.1.0"
edition = "2021"
description = "Interval-based monotonic-clock rate probe: compares musl/vDSO CLOCK_MONOTONIC(+_RAW) against /proc/uptime so rate errors and boot-time offsets (CNTVOFF) separate cleanly. Diagnostic for issue #3."
license = "GPL-2.0-only"
[dependencies]
libc = "0.2"
[profile.release]
strip = true
+25
View File
@@ -0,0 +1,25 @@
// Differential clock probe for warden-sdk issue #3: compares the musl/vDSO
// CLOCK_MONOTONIC rate against the kernel's own /proc/uptime over a fixed
// interval, so boot-time offsets cancel and only the RATE ratio remains.
use std::{fs, thread, time::Duration};
fn mono(clock: libc::clockid_t) -> f64 {
let mut ts = libc::timespec { tv_sec: 0, tv_nsec: 0 };
unsafe { libc::clock_gettime(clock, &mut ts) };
ts.tv_sec as f64 + ts.tv_nsec as f64 / 1e9
}
fn uptime() -> f64 {
fs::read_to_string("/proc/uptime").unwrap()
.split_whitespace().next().unwrap().parse().unwrap()
}
fn main() {
let interval = 10.0;
let (m0, r0, u0) = (mono(libc::CLOCK_MONOTONIC), mono(libc::CLOCK_MONOTONIC_RAW), uptime());
thread::sleep(Duration::from_secs_f64(interval));
let (m1, r1, u1) = (mono(libc::CLOCK_MONOTONIC), mono(libc::CLOCK_MONOTONIC_RAW), uptime());
let (dm, dr, du) = (m1 - m0, r1 - r0, u1 - u0);
println!("CLOCKPROBE monotonic={dm:.4} raw={dr:.4} uptime={du:.4} ratio_mono={:.5} ratio_raw={:.5}", dm / du, dr / du);
println!("CLOCKPROBE abs monotonic={m1:.2} uptime={u1:.2} abs_ratio={:.5}", m1 / u1);
}
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env bash
# FULL OTA apply scenario — the loop the desk e2e stops short of: the real
# flared inside the VM downloads a real signed tier-1 .wfw whose payload is a
# bootable rootfs, verifies it, and ACTUALLY WRITES rootfs_b (safe: it is a
# region inside disk.img); the harness then reboots into slot _b and asserts
# the applied firmware version is running.
#
# Documented emulation gaps (ADR-0006): the BCB slot CHOICE and the physical
# reset are performed by the harness (cmdline slot + a fresh qemu boot), not
# by U-Boot/CRU — those stay bench territory.
#
# FAILS CLOSED on missing prerequisites.
#
# Usage: ota-apply.sh <zImage-virt>
# Env: FLARE_EDGE path to a flare-edge checkout (mock portal, mk-wfw, dev key)
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # qemu/tests/
QDIR="$(cd "$HERE/.." && pwd)" # qemu/
# mkfs.ext4 lives in sbin (not on user PATH on Debian).
PATH="$PATH:/usr/sbin:/sbin"
ZIMAGE="${1:-}"
if [ -z "$ZIMAGE" ] || [ ! -f "$ZIMAGE" ]; then
echo "FATAL: usage: $0 <zImage> — the virt.fragment kernel variant" >&2
exit 1
fi
if [ -z "${FLARE_EDGE:-}" ] || [ ! -f "$FLARE_EDGE/tools/mock-flare-portal.py" ]; then
echo "FATAL: FLARE_EDGE must point at a flare-edge checkout" >&2
exit 1
fi
[ -x "$QDIR/payload/warden-flared" ] || {
echo "FATAL: no qemu/payload/warden-flared (needs the WARDEN_HARD_RESET-gated build)" >&2
exit 1
}
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/wqo.XXXXXX)"
QEMU_PID="" MOCK_PID=""
cleanup() {
if [ -n "$QEMU_PID" ]; then kill "$QEMU_PID" 2>/dev/null || true; fi
if [ -n "$MOCK_PID" ]; then kill "$MOCK_PID" 2>/dev/null || true; fi
rm -rf "$WORK"
}
trap cleanup EXIT
DEVICE_ID="$(python3 -c 'import uuid; print(uuid.uuid4())')"
API_KEY="$(python3 -c 'import secrets; print(secrets.token_hex(24))')"
PORT=$((20000 + RANDOM % 20000))
# 0. The offer's payload is a REAL bootable rootfs: the same skeleton the
# disk uses, stamped with the NEW version — booting it is the proof.
QEMU_DIR="$QDIR"
OUT="$QDIR/out"
# shellcheck source=qemu/lib.sh disable=SC1091
. "$QDIR/lib.sh"
qemu_get_busybox
qemu_stage_rootfs "$WORK/newroot"
printf '0.0.2\n' > "$WORK/newroot/etc/warden-firmware-version"
printf 'applied-via-ota\n' > "$WORK/newroot/etc/ota-marker"
truncate -s 64M "$WORK/rootfs-payload.img"
mkfs.ext4 -F -q -d "$WORK/newroot" "$WORK/rootfs-payload.img"
FW_SIGNING_KEY_FILE="$FLARE_EDGE/tools/testdata/fw-dev-key.seed" \
WARDEN_KERNEL_VERSION=6.18.46 WARDEN_BUILDROOT_VERSION=2025.02 \
WARDEN_UBOOT_VERSION=2017.09 \
bash "$FLARE_EDGE/tools/mk-wfw.sh" "$WORK/rootfs-payload.img" 1 0.0.2 "$WORK/offer.wfw"
# 1. mock portal offering it.
python3 "$FLARE_EDGE/tools/mock-flare-portal.py" \
--port "$PORT" --device "$DEVICE_ID:$API_KEY" \
--wfw "$WORK/offer.wfw" > "$WORK/mock.log" 2>&1 &
MOCK_PID=$!
mock_ready=0
for _ in $(seq 1 50); do
curl -so /dev/null "http://127.0.0.1:$PORT/" && { mock_ready=1; break; }
kill -0 "$MOCK_PID" 2>/dev/null || { echo "FATAL: mock portal died:" >&2; cat "$WORK/mock.log" >&2; exit 1; }
sleep 0.2
done
[ "$mock_ready" = 1 ] || { echo "FATAL: mock never answered on :$PORT" >&2; exit 1; }
echo "== mock portal on :$PORT offering 0.0.2 (payload: bootable rootfs, 64M)"
# 2. image at version 0.0.1, enrolment seeded.
bash "$QDIR/mkinitramfs.sh"
bash "$QDIR/mkimage.sh" \
--portal-url "http://10.0.2.2:$PORT" \
--state "flare.device_id=$DEVICE_ID" \
--state "flare.api_key=$API_KEY" \
--state "flare.site=qemu-devsim" \
--fw-version 0.0.1
# 3. boot slot _a WITH APPLY ENABLED; flared should pull, verify, write
# rootfs_b, and surface the (gated) reboot attempt.
QEMU_PID=""
for _attempt in 1 2 3; do
VMBASE=$((20000 + RANDOM % 20000))
: > "$WORK/console.log"
bash "$QDIR/run.sh" --kernel "$ZIMAGE" --allow-apply \
--ssh-port "$VMBASE" --http-port $((VMBASE + 1)) --api-port $((VMBASE + 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 $VMBASE — 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
# 4. wait for the apply to conclude. flared logs to its in-guest file, not
# the console — but the next check-in REPORTS the outcome to the portal:
# detail "hard reset failed after slot flip" is the exact post-apply state
# under the gated reset (write done, AvbABData flipped, reboot refused).
deadline=$((SECONDS + 420))
staged=0
while [ $SECONDS -lt $deadline ]; do
grep -aq 'hard reset failed after slot flip' "$WORK/mock.log" && { staged=1; break; }
grep -aq 'WARDEN-QEMU-MOUNT-FAILED' "$WORK/console.log" && {
echo "FATAL: guest mount failed" >&2; exit 1; }
kill -0 "$QEMU_PID" 2>/dev/null || {
echo "FATAL: VM exited early" >&2; tail -30 "$WORK/console.log" >&2; exit 1; }
sleep 3
done
[ "$staged" = 1 ] || {
echo "FATAL: apply never reached the post-flip state within 420s; tails:" >&2
tail -20 "$WORK/console.log" >&2
tail -10 "$WORK/mock.log" >&2
exit 1
}
grep -aq "GET /api/v1/devices/$DEVICE_ID/firmware/assets/.* -> 200" "$WORK/mock.log" || {
echo "FATAL: staged without a portal asset download?!" >&2
exit 1
}
echo "== apply staged (asset downloaded, rootfs_b written, reset gated) — rebooting into _b"
kill "$QEMU_PID" 2>/dev/null || true
wait "$QEMU_PID" 2>/dev/null || true
QEMU_PID=""
# 5. the harness performs the "reboot": boot slot _b, assert the OTA'd rootfs
# is what runs.
{ sleep 40; printf 'cat /etc/warden-firmware-version /etc/ota-marker\n'; sleep 3; printf 'poweroff -f\n'; sleep 8; } | \
timeout 180 bash "$QDIR/run.sh" --kernel "$ZIMAGE" --slot _b --shell \
--ssh-port 0 --http-port 0 --api-port 0 \
> "$WORK/boot-b.log" 2>&1 || true
grep -aq 'WARDEN-QEMU-ROOTFS-OK slot=_b' "$WORK/boot-b.log" || {
echo "FATAL: slot _b did not boot; tail:" >&2; tail -25 "$WORK/boot-b.log" >&2; exit 1
}
grep -aq '^0.0.2' "$WORK/boot-b.log" || {
echo "FATAL: _b is not running the applied 0.0.2 firmware" >&2
grep -a 'warden-firmware-version' -A2 "$WORK/boot-b.log" >&2 || true
exit 1
}
grep -aq 'applied-via-ota' "$WORK/boot-b.log" || {
echo "FATAL: OTA marker missing on _b" >&2; exit 1
}
echo "OTA-APPLY-PASS: 0.0.1 -> 0.0.2 applied over the air and booted from slot _b"
+82
View File
@@ -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"