Merge remote-tracking branch 'origin/main' into docs-reposition
This commit is contained in:
@@ -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 @@
|
||||
<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 |
@@ -35,7 +35,7 @@ jobs:
|
||||
run: |
|
||||
set -o pipefail
|
||||
: > /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
|
||||
( cd "$d" && cargo test --locked ) 2>&1 | tee -a /tmp/test.log
|
||||
done
|
||||
@@ -220,6 +220,11 @@ jobs:
|
||||
CROSS_COMPILE: arm-linux-gnueabihf-
|
||||
WARDEN_CCACHE: 1
|
||||
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
|
||||
# `~` in a YAML env: value is never tilde-expanded and broke every
|
||||
# dispatch until caught in review.
|
||||
@@ -246,6 +251,7 @@ jobs:
|
||||
path: |
|
||||
${{ 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/modules-out/*.ko
|
||||
# Short so kernel images self-expire instead of piling into the
|
||||
# account-wide storage quota; the prune-artifacts job above is the
|
||||
# active bound, this is the backstop.
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
# (qemu/configs/virt.fragment builds the QEMU -M virt variant);
|
||||
# every fragment option is verified to have taken effect
|
||||
# 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
|
||||
# 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" \
|
||||
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"
|
||||
D="$SRC/arch/arm/boot/dts/rockchip/rv1106-warden.dtb"
|
||||
log "build OK"
|
||||
|
||||
@@ -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.
|
||||
@@ -76,6 +76,21 @@ stage-2 init when present.
|
||||
(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
|
||||
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 —
|
||||
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.
|
||||
|
||||
+66
-5
@@ -10,6 +10,10 @@
|
||||
# sudo), then dd'd into a sparse raw image.
|
||||
#
|
||||
# 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:
|
||||
# BUSYBOX path to a local busybox binary (skips the download; still verified)
|
||||
# OUT output dir (default: qemu/out); image at $OUT/disk.img
|
||||
@@ -27,6 +31,8 @@ PATH="$PATH:/usr/sbin:/sbin"
|
||||
PORTAL_URL=""
|
||||
STATE_KV=()
|
||||
FW_VERSION="0.0.1"
|
||||
ROOTFS_IMAGE=""
|
||||
OEM_IMAGE=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--portal-url) PORTAL_URL="${2:?--portal-url needs a value}"; shift 2 ;;
|
||||
@@ -45,6 +51,8 @@ while [ $# -gt 0 ]; do
|
||||
esac
|
||||
STATE_KV+=("$2"); 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 ;;
|
||||
esac
|
||||
done
|
||||
@@ -90,28 +98,79 @@ mkfs_part() {
|
||||
DISK="$OUT/disk.img"
|
||||
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() {
|
||||
local name="$1" off="$2" size="$3" stage=""
|
||||
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" ;;
|
||||
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,
|
||||
# left zeroed. Enumerated (not a wildcard) so a typo'd name in
|
||||
# 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 ;;
|
||||
esac
|
||||
DISK_END_TRACK "$off" "$size"
|
||||
# dd in 4K blocks — every offset in the canonical layout is 4K-aligned;
|
||||
# assert rather than assume, a misaligned write would corrupt a neighbor.
|
||||
if [ $((off % 4096)) -ne 0 ] || [ $((size % 4096)) -ne 0 ]; then
|
||||
echo "FATAL: partition $name not 4K-aligned (off=$off size=$size)" >&2
|
||||
exit 1
|
||||
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
|
||||
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"
|
||||
dd if="$img" of="$DISK" bs=4096 seek=$((off / 4096)) \
|
||||
conv=notrunc,sparse status=none
|
||||
@@ -119,6 +178,8 @@ place_partition() {
|
||||
}
|
||||
|
||||
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)"
|
||||
truncate -s 0 "$DISK"
|
||||
qemu_each_partition place_partition
|
||||
|
||||
@@ -39,5 +39,12 @@ if ! command -v switch_root >/dev/null; then
|
||||
return 0
|
||||
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)"
|
||||
exec switch_root /mnt /sbin/init
|
||||
|
||||
@@ -70,6 +70,16 @@ export WARDEN_FLARE_INSECURE=1
|
||||
# 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.
|
||||
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
|
||||
if [ -x "$d" ]; then
|
||||
name="$(basename "$d")"
|
||||
|
||||
+5
-1
@@ -22,6 +22,8 @@
|
||||
# --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)
|
||||
# --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
|
||||
|
||||
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}"
|
||||
|
||||
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
|
||||
EXTRA=()
|
||||
|
||||
@@ -50,6 +52,7 @@ while [ $# -gt 0 ]; do
|
||||
--http-port) HTTP_PORT="${2:?}"; shift 2 ;;
|
||||
--api-port) API_PORT="${2:?}"; shift 2 ;;
|
||||
--shell) SHELL_FLAG=1; shift ;;
|
||||
--allow-apply) ALLOW_APPLY=1; shift ;;
|
||||
--) shift; EXTRA=("$@"); break ;;
|
||||
*) echo "FATAL: unknown argument '$1' (see header of $0)" >&2; exit 1 ;;
|
||||
esac
|
||||
@@ -99,6 +102,7 @@ if [ -n "$DISK" ] && [ "$NO_DISK" -eq 0 ]; then
|
||||
-device "virtio-blk-device,drive=vd0" )
|
||||
fi
|
||||
[ "$SHELL_FLAG" -eq 1 ] && APPEND="$APPEND warden.shell"
|
||||
[ "$ALLOW_APPLY" -eq 1 ] && APPEND="$APPEND warden.fwapply"
|
||||
[ -n "$RTC" ] && ARGS+=( -rtc "base=$RTC" )
|
||||
[ "$WATCHDOG" -eq 1 ] && ARGS+=( -device i6300esb -action watchdog=reset )
|
||||
[ -n "$RS485" ] && ARGS+=( -chardev "socket,id=rs485,path=$RS485,server=on,wait=off"
|
||||
|
||||
Executable
+92
@@ -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"
|
||||
Generated
+16
@@ -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"
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
Executable
+165
@@ -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"
|
||||
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