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: |
|
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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
(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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
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