qemu: device-sim bringup — boot smoke, A/B disk harness, virt kernel variant
The third simulator (deliberately not named "sim"): a QEMU -M virt VM that boots the real 6.18.46 kernel and enters at -kernel zImage — everything below (BootROM/idblock/U-Boot/real BCB A/B selection) is closed blobs + mask ROM and is explicitly out of scope. - qemu/mkinitramfs.sh: pinned static busybox (sha256 fail-closed) + rootfs/ - qemu/mkimage.sh: unprivileged sparse disk image with the device's canonical 12-partition blkdevparts A/B layout (vda == mmcblk0 mapping) - qemu/rootfs/: stage-1 init (by-name symlinks from PARTNAME uevents, whole-token warden.slot= parse, switch_root) + stage-2 init (userdata/oem mounts, slirp networking, payload daemon start) - qemu/run.sh: runner with --slot/--rtc/--watchdog/--rs485/--qmp/--display - qemu/configs/virt.fragment + WARDEN_KCONFIG_FRAGMENT hook in build/build-kernel.sh (canonical RV1106 build untouched when unset): adds PCI, pci-serial, i6300esb watchdog, WireGuard, virtio-gpu/input - qemu/tests/boot-smoke.sh: sentinel-asserting boot test Verified on QEMU 10.0.11: canonical zImage boots -M virt unmodified (the feared DEBUG_UNCOMPRESS decompressor hang does not exist in 6.18); full stack boots both slots; 12 by-name symlinks; userdata persists across reboot; -rtc base=2021-01-01 reproduces the no-RTC wrong-clock class. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HUayid7W5w7jBdb9Rrj1K
This commit is contained in:
co-authored by
Claude Fable 5
parent
44cdf2bf38
commit
bf3c93cf85
@@ -19,3 +19,9 @@ __pycache__/
|
||||
|
||||
# driver MC/DC harness build dirs
|
||||
**/test/build/
|
||||
|
||||
# qemu device sim: build outputs (initramfs, disk images, cached busybox) and
|
||||
# payload binaries (dropped in from flare-edge builds, never committed)
|
||||
qemu/out/
|
||||
qemu/payload/*
|
||||
!qemu/payload/README.md
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
# SDK_TC dir holding the arm-rockchip830 uclibc cross toolchain bin/
|
||||
# WORK build scratch dir (default: a mktemp under $TMPDIR)
|
||||
# JOBS parallel make jobs (default: nproc)
|
||||
# WARDEN_KCONFIG_FRAGMENT
|
||||
# optional kconfig fragment merged onto warden_defconfig
|
||||
# (qemu/configs/virt.fragment builds the QEMU -M virt variant)
|
||||
#
|
||||
# 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.
|
||||
@@ -97,6 +100,19 @@ log "patch series applied ($(basename "$SENTINEL") present)"
|
||||
# 4. configure
|
||||
log "configuring (warden_defconfig)"
|
||||
cp "$HERE/warden_defconfig" "$SRC/.config"
|
||||
# Optional kconfig fragment overlay (e.g. qemu/configs/virt.fragment for the
|
||||
# QEMU -M virt device-sim variant). Fail closed if set but unreadable — never
|
||||
# silently build the wrong kernel. Unset => the canonical RV1106 build,
|
||||
# byte-identical to a build without this hook.
|
||||
if [ -n "${WARDEN_KCONFIG_FRAGMENT:-}" ]; then
|
||||
[ -f "$WARDEN_KCONFIG_FRAGMENT" ] || {
|
||||
echo "FATAL: WARDEN_KCONFIG_FRAGMENT set but not a file: $WARDEN_KCONFIG_FRAGMENT" >&2
|
||||
exit 1
|
||||
}
|
||||
FRAG="$(cd "$(dirname "$WARDEN_KCONFIG_FRAGMENT")" && pwd)/$(basename "$WARDEN_KCONFIG_FRAGMENT")"
|
||||
log "merging kconfig fragment $(basename "$FRAG")"
|
||||
( cd "$SRC" && ARCH=arm ./scripts/kconfig/merge_config.sh -m .config "$FRAG" )
|
||||
fi
|
||||
# CROSS_COMPILE defaults to the Luckfox SDK uclibc prefix (set SDK_TC to its bin/),
|
||||
# but the kernel is freestanding, so a caller may override with a generic arm cross
|
||||
# toolchain instead — e.g. CROSS_COMPILE=arm-linux-gnueabihf- (in Debian's
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# qemu/ — the WardenOS device simulator
|
||||
|
||||
A QEMU virtual machine that boots the real forward-ported kernel (`build/` +
|
||||
`patches/`) and real userspace, so the *device* — init, daemons, networking,
|
||||
OTA, watchdog — can be tested off-hardware. The third simulator in the stack,
|
||||
deliberately not named "sim":
|
||||
|
||||
- `lvglsim` (flare-edge) — SDL desktop build of the UI. Rendering only.
|
||||
- `sim/` (this repo) — register-level Rust models of RV1106 blocks behind
|
||||
driver seams.
|
||||
- `qemu/` (this) — the whole machine above the kernel entry point.
|
||||
|
||||
## The boundary (read this before trusting a green run)
|
||||
|
||||
There is no RV1106 machine model in QEMU and everything below the kernel is
|
||||
closed rkbin blobs plus mask ROM, so the VM **enters at `-kernel zImage`** on
|
||||
`-M virt` (generic ARMv7 machine, virtio peripherals). That means:
|
||||
|
||||
- **Not emulated, not tested here:** BootROM, idblock/DDR-init, SPL, U-Boot,
|
||||
the real BCB-driven A/B slot selection, bootcount auto-revert. The untested
|
||||
A/B rollback chain stays untested by this tool.
|
||||
- **Substituted, not modeled:** display (virtio-gpu, not VOP), input
|
||||
(virtio-tablet, not GT911), network (virtio-net, not GMAC), storage
|
||||
(virtio-blk, not eMMC). NPU/RGA/HPMCU behavior stays `sim/` territory.
|
||||
- "Boots under emulation" is **not** "works on silicon". On-device claims
|
||||
still need on-device evidence; the VM narrows which claims need a panel.
|
||||
|
||||
## Quick start
|
||||
|
||||
```sh
|
||||
# 1. build the QEMU kernel variant (canonical RV1106 build + virt fragment)
|
||||
WORK=$HOME/kbuild-out CROSS_COMPILE=arm-linux-gnueabihf- \
|
||||
WARDEN_KCONFIG_FRAGMENT=qemu/configs/virt.fragment bash build/build-kernel.sh
|
||||
|
||||
# 2. build the initramfs (sha256-pinned static busybox + qemu/rootfs/)
|
||||
bash qemu/mkinitramfs.sh
|
||||
|
||||
# 3. smoke it
|
||||
bash qemu/tests/boot-smoke.sh $HOME/kbuild-out/linux-6.18.46/arch/arm/boot/zImage
|
||||
```
|
||||
|
||||
Host requirements: `qemu-system-arm` (Debian 13 ships QEMU 10), `curl`, `cpio`,
|
||||
`gcc-arm-linux-gnueabihf` for the kernel build.
|
||||
|
||||
Status: bringup — boot smoke only. Disk/network harness, RS485 bridge to
|
||||
`sim/`, portal scenarios, and display/touch land in later phases (see
|
||||
`docs/decisions/0006-qemu-device-sim.md` once written).
|
||||
@@ -0,0 +1,10 @@
|
||||
# The device's canonical 12-partition A/B layout, expressed for the VM's
|
||||
# virtio disk (vda). On hardware the same string names mmcblk0 and is baked
|
||||
# into the U-Boot env — source: flare-edge docs/decisions/0003-partition-layout.md.
|
||||
# There is no MBR/GPT anywhere: U-Boot and Linux both parse this string, which
|
||||
# is why handing it to the VM kernel on the cmdline reproduces the exact
|
||||
# partition map (vda9 = rootfs_a = hardware mmcblk0p9).
|
||||
#
|
||||
# Sourced by qemu/mkimage.sh (computes byte offsets from it) and qemu/run.sh
|
||||
# (passes it verbatim in -append). Single source of truth — edit only here.
|
||||
WARDEN_BLKDEVPARTS='vda:32K(env),512K@32K(idblock),512K(uboot),512K(misc),32M(boot_a),32M(boot_b),128M(oem_a),128M(oem_b),1G(rootfs_a),1G(rootfs_b),32M(recovery),1G(userdata)'
|
||||
@@ -0,0 +1 @@
|
||||
cd04052b8b6885f75f50b2a280bfcbf849d8710c8e61d369c533acf307eda064
|
||||
@@ -0,0 +1,36 @@
|
||||
# QEMU -M virt kernel variant — merged onto build/warden_defconfig via
|
||||
# WARDEN_KCONFIG_FRAGMENT (see build/build-kernel.sh). The RV1106 zImage stays
|
||||
# canonical and byte-identical when the variable is unset.
|
||||
#
|
||||
# The canonical zImage already BOOTS on -M virt as-is (verified 2026-08-29:
|
||||
# the multi_v7 heritage supplies ARCH_VIRT + the virtio set, and DEBUG_LL's
|
||||
# hardcoded RV1106 UART is inert as long as `earlyprintk` is never passed on
|
||||
# the cmdline). This fragment therefore only ADDS what the device-sim
|
||||
# scenarios need beyond the canonical config.
|
||||
|
||||
# -M virt exposes exactly one PL011 (probed: QEMU 10 dtb has a single
|
||||
# pl011@9000000); every additional device below rides the machine's PCIe
|
||||
# (ECAM "host generic") root.
|
||||
CONFIG_PCI=y
|
||||
CONFIG_PCI_HOST_GENERIC=y
|
||||
|
||||
# Second UART for the RS485/Modbus bridge: -device pci-serial (16550-class,
|
||||
# shows up as ttyS0; stage-2 init aliases it to the device's /dev/ttyS4).
|
||||
CONFIG_SERIAL_8250_PCI=y
|
||||
|
||||
# /dev/watchdog for flared's watchdog_loop() — untestable on both existing
|
||||
# sims. i6300esb is the watchdog QEMU offers on arm virt (PCI device):
|
||||
# -device i6300esb -action watchdog=reset.
|
||||
CONFIG_WATCHDOG=y
|
||||
CONFIG_I6300ESB_WDT=y
|
||||
|
||||
# wg0 mesh scenarios (flared owns identity, the panel shells out `wg`).
|
||||
CONFIG_WIREGUARD=y
|
||||
|
||||
# Display + touch: virtio-gpu scanout with fbdev emulation (the VM UI build
|
||||
# uses LVGL's fbdev backend — no libdrm needed in the guest), virtio-tablet
|
||||
# for absolute-coordinate touch injection via QMP.
|
||||
CONFIG_FB=y
|
||||
CONFIG_DRM_VIRTIO_GPU=y
|
||||
CONFIG_DRM_FBDEV_EMULATION=y
|
||||
CONFIG_VIRTIO_INPUT=y
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
# Shared helpers for the qemu/ device-sim build scripts. Sourced, not executed.
|
||||
# Callers must run under `set -euo pipefail` and define QEMU_DIR (the qemu/ dir).
|
||||
|
||||
BB_VER=1.31.0
|
||||
BB_URL="https://busybox.net/downloads/binaries/${BB_VER}-defconfig-multiarch-musl/busybox-armv7l"
|
||||
|
||||
qemu_log() { printf '\033[36m== %s\033[0m\n' "$*"; }
|
||||
|
||||
# Fetch (or accept via $BUSYBOX) the pinned static armv7 busybox and verify it
|
||||
# against qemu/busybox.sha256. FAILS CLOSED: a missing pin refuses to build,
|
||||
# never silently skips verification — mirroring build/build-kernel.sh's
|
||||
# tarball handling. Sets $BB to the verified binary's path.
|
||||
qemu_get_busybox() {
|
||||
local sha_file="$QEMU_DIR/busybox.sha256"
|
||||
local out="${OUT:-$QEMU_DIR/out}"
|
||||
mkdir -p "$out"
|
||||
BB="${BUSYBOX:-$out/busybox-armv7l}"
|
||||
if [ ! -f "$BB" ]; then
|
||||
qemu_log "downloading $BB_URL"
|
||||
curl -fSL "$BB_URL" -o "$BB"
|
||||
fi
|
||||
[ -f "$sha_file" ] || {
|
||||
echo "FATAL: no pinned sha256 for busybox (expected $sha_file) — refusing to build from an unverified binary" >&2
|
||||
exit 1
|
||||
}
|
||||
local want got
|
||||
want="$(cat "$sha_file")"
|
||||
got="$(sha256sum "$BB" | awk '{print $1}')"
|
||||
[ "$want" = "$got" ] || { echo "busybox sha256 mismatch: want $want got $got" >&2; exit 1; }
|
||||
qemu_log "busybox sha256 verified"
|
||||
}
|
||||
|
||||
# Stage the shared rootfs skeleton (qemu/rootfs/ + busybox) into $1.
|
||||
# Requires qemu_get_busybox to have run (uses $BB).
|
||||
qemu_stage_rootfs() {
|
||||
local root="$1"
|
||||
mkdir -p "$root/bin" "$root/sbin" "$root/dev" "$root/proc" "$root/sys" \
|
||||
"$root/etc" "$root/tmp" "$root/mnt" "$root/userdata" "$root/oem" \
|
||||
"$root/usr/bin" "$root/usr/share/udhcpc"
|
||||
install -m 0755 "$BB" "$root/bin/busybox"
|
||||
cp -a "$QEMU_DIR/rootfs/." "$root/"
|
||||
chmod 0755 "$root/init" "$root/sbin/init" "$root/etc/rc" \
|
||||
"$root/usr/share/udhcpc/default.script"
|
||||
}
|
||||
|
||||
# Parse a "SIZE[@OFFSET](NAME)" blkdevparts entry list (without the "vda:"
|
||||
# prefix) and invoke a callback `$1 name offset_bytes size_bytes` per entry.
|
||||
qemu_each_partition() {
|
||||
local cb="$1" parts entry size_s off_s name size off
|
||||
parts="${WARDEN_BLKDEVPARTS#*:}"
|
||||
off=0
|
||||
local IFS=','
|
||||
for entry in $parts; do
|
||||
name="${entry##*(}"; name="${name%)}"
|
||||
size_s="${entry%%(*}"
|
||||
if [ "${size_s#*@}" != "$size_s" ]; then
|
||||
off_s="${size_s#*@}"; size_s="${size_s%@*}"
|
||||
off="$(qemu_to_bytes "$off_s")"
|
||||
fi
|
||||
size="$(qemu_to_bytes "$size_s")"
|
||||
"$cb" "$name" "$off" "$size"
|
||||
off=$((off + size))
|
||||
done
|
||||
}
|
||||
|
||||
qemu_to_bytes() {
|
||||
local v="$1" n mult=1
|
||||
case "$v" in
|
||||
*K) n="${v%K}"; mult=1024 ;;
|
||||
*M) n="${v%M}"; mult=1048576 ;;
|
||||
*G) n="${v%G}"; mult=1073741824 ;;
|
||||
*) n="$v" ;;
|
||||
esac
|
||||
case "$n" in
|
||||
''|*[!0-9]*) echo "FATAL: bad blkdevparts size/offset token: '$v'" >&2; exit 1 ;;
|
||||
esac
|
||||
echo $((n * mult))
|
||||
}
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the VM's virtio disk image carrying the device's canonical 12-partition
|
||||
# A/B layout (qemu/blkdevparts.conf — the same string U-Boot and Linux parse on
|
||||
# hardware; there is no MBR/GPT). Every partition is placed at the exact offset
|
||||
# the cmdline string declares; rootfs_a/rootfs_b/oem_a/oem_b/userdata get ext4,
|
||||
# the boot-chain partitions (env/idblock/uboot/misc/boot_a/boot_b/recovery)
|
||||
# stay zeroed — the VM enters at -kernel and never reads them.
|
||||
#
|
||||
# Built entirely UNPRIVILEGED: per-partition mkfs.ext4 -d (no loop mounts, no
|
||||
# sudo), then dd'd into a sparse raw image.
|
||||
#
|
||||
# Usage: mkimage.sh [--portal-url URL] [--state KEY=VALUE]...
|
||||
# Env:
|
||||
# BUSYBOX path to a local busybox binary (skips the download; still verified)
|
||||
# OUT output dir (default: qemu/out); image at $OUT/disk.img
|
||||
set -euo pipefail
|
||||
|
||||
QEMU_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
. "$QEMU_DIR/lib.sh"
|
||||
. "$QEMU_DIR/blkdevparts.conf"
|
||||
OUT="${OUT:-$QEMU_DIR/out}"
|
||||
# mkfs.ext4 lives in sbin, which user shells on Debian don't have on PATH.
|
||||
PATH="$PATH:/usr/sbin:/sbin"
|
||||
|
||||
PORTAL_URL=""
|
||||
STATE_KV=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--portal-url) PORTAL_URL="${2:?--portal-url needs a value}"; shift 2 ;;
|
||||
--state) STATE_KV+=("${2:?--state needs KEY=VALUE}"); shift 2 ;;
|
||||
*) echo "FATAL: unknown argument '$1' (usage: mkimage.sh [--portal-url URL] [--state KEY=VALUE]...)" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
qemu_get_busybox
|
||||
|
||||
SCRATCH="$(mktemp -d "${TMPDIR:-/tmp}/warden-qemu-image.XXXXXX")"
|
||||
trap 'rm -rf "$SCRATCH"' EXIT
|
||||
|
||||
# Stage the rootfs tree once (skeleton + payload into /usr/bin), reused for
|
||||
# both slots so A and B start byte-identical, like a factory flash.
|
||||
ROOT="$SCRATCH/root"
|
||||
qemu_stage_rootfs "$ROOT"
|
||||
for p in "$QEMU_DIR"/payload/*; do
|
||||
[ -f "$p" ] || continue
|
||||
case "$(basename "$p")" in README.md) continue ;; esac
|
||||
install -m 0755 "$p" "$ROOT/usr/bin/$(basename "$p")"
|
||||
done
|
||||
|
||||
# Seed persistent state (flared: one file per key under /userdata/warden).
|
||||
UDATA="$SCRATCH/userdata"
|
||||
mkdir -p "$UDATA/warden"
|
||||
[ -n "$PORTAL_URL" ] && printf '%s' "$PORTAL_URL" > "$UDATA/warden/flare.url"
|
||||
for kv in ${STATE_KV[@]+"${STATE_KV[@]}"}; do
|
||||
printf '%s' "${kv#*=}" > "$UDATA/warden/${kv%%=*}"
|
||||
done
|
||||
|
||||
mkdir -p "$SCRATCH/empty"
|
||||
|
||||
# mkfs an ext4 partition image of exactly $2 bytes from staged dir $1.
|
||||
mkfs_part() {
|
||||
local stage="$1" bytes="$2" img="$3"
|
||||
rm -f "$img"
|
||||
truncate -s "$bytes" "$img"
|
||||
mkfs.ext4 -F -q -d "$stage" "$img"
|
||||
}
|
||||
|
||||
DISK="$OUT/disk.img"
|
||||
rm -f "$DISK"
|
||||
|
||||
place_partition() {
|
||||
local name="$1" off="$2" size="$3" stage=""
|
||||
case "$name" in
|
||||
rootfs_a|rootfs_b) stage="$ROOT" ;;
|
||||
userdata) stage="$UDATA" ;;
|
||||
oem_a|oem_b) stage="$SCRATCH/empty" ;;
|
||||
*) stage="" ;; # boot-chain partition: left zeroed
|
||||
esac
|
||||
# dd in 4K blocks — every offset in the canonical layout is 4K-aligned;
|
||||
# assert rather than assume, a misaligned write would corrupt a neighbor.
|
||||
[ $((off % 4096)) -eq 0 ] && [ $((size % 4096)) -eq 0 ] || {
|
||||
echo "FATAL: partition $name not 4K-aligned (off=$off size=$size)" >&2
|
||||
exit 1
|
||||
}
|
||||
DISK_END=$((off + size))
|
||||
[ -z "$stage" ] && return 0
|
||||
local img="$SCRATCH/$name.img"
|
||||
mkfs_part "$stage" "$size" "$img"
|
||||
dd if="$img" of="$DISK" bs=4096 seek=$((off / 4096)) \
|
||||
conv=notrunc,sparse status=none
|
||||
qemu_log " $name: ext4, $((size / 1048576))M @ $off"
|
||||
}
|
||||
|
||||
DISK_END=0
|
||||
qemu_log "building $DISK ($WARDEN_BLKDEVPARTS)"
|
||||
truncate -s 0 "$DISK"
|
||||
qemu_each_partition place_partition
|
||||
truncate -s "$DISK_END" "$DISK"
|
||||
qemu_log "disk image: $DISK ($(du -h "$DISK" | cut -f1) used, $((DISK_END / 1048576))M apparent)"
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the QEMU device-sim initramfs: the pinned static busybox + qemu/rootfs/.
|
||||
# The busybox binary is the ONLY external input (sha256-pinned, fail-closed —
|
||||
# see qemu/lib.sh).
|
||||
#
|
||||
# Env:
|
||||
# BUSYBOX path to a local busybox binary (skips the download; still verified)
|
||||
# OUT output dir (default: qemu/out); initramfs at $OUT/initramfs.cpio.gz
|
||||
set -euo pipefail
|
||||
|
||||
QEMU_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
. "$QEMU_DIR/lib.sh"
|
||||
OUT="${OUT:-$QEMU_DIR/out}"
|
||||
|
||||
qemu_get_busybox
|
||||
|
||||
# Assemble in a scratch dir, removed on exit (scratch-dir leaks were a past
|
||||
# review finding in this repo).
|
||||
SCRATCH="$(mktemp -d "${TMPDIR:-/tmp}/warden-qemu-initramfs.XXXXXX")"
|
||||
trap 'rm -rf "$SCRATCH"' EXIT
|
||||
qemu_stage_rootfs "$SCRATCH/root"
|
||||
|
||||
# Pack (newc cpio, gzip). No device nodes required: /init mounts devtmpfs and
|
||||
# reopens the console itself, so the archive builds unprivileged.
|
||||
( cd "$SCRATCH/root" && find . -print0 | cpio -0 -o -H newc -R +0:+0 2>/dev/null ) \
|
||||
| gzip -9 > "$OUT/initramfs.cpio.gz"
|
||||
qemu_log "initramfs: $OUT/initramfs.cpio.gz ($(du -h "$OUT/initramfs.cpio.gz" | cut -f1))"
|
||||
@@ -0,0 +1,24 @@
|
||||
# qemu/payload/ — guest binaries (never committed)
|
||||
|
||||
Drop **static musl armv7** binaries here; `qemu/mkimage.sh` copies everything
|
||||
in this directory (except this README) into `/usr/bin/` of both rootfs slots.
|
||||
Static musl is the same target the device uses for its Rust daemons, so the
|
||||
exact production binaries run unmodified in the VM.
|
||||
|
||||
Typical payload, built in a flare-edge checkout:
|
||||
|
||||
```sh
|
||||
# flared (static musl armv7)
|
||||
tools/build-flared.sh --local
|
||||
# warden-modbus and friends: see tools/build-firmware.sh for the recipes
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```sh
|
||||
cp <flare-edge>/target/armv7-unknown-linux-musleabihf/release/warden-flared qemu/payload/
|
||||
```
|
||||
|
||||
Stage-2 init starts `warden-flared` and `warden-modbus` automatically when
|
||||
present (logs land in `/tmp/<name>.log` inside the guest). An empty payload is
|
||||
valid — the image boots to a busybox-only userspace.
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/bin/busybox sh
|
||||
# Stage-1 rc: sourced by /init (still PID 1, initramfs root) when a virtio
|
||||
# disk is present. Emulates U-Boot's slot choice — parse warden.slot= from the
|
||||
# cmdline, mount that rootfs, switch_root into it. This is an EMULATION of the
|
||||
# A/B selection outcome, not the BCB/bootcount mechanism itself.
|
||||
|
||||
# /dev/block/by-name/<PARTNAME> symlinks: the contract flare-edge slotctl.rs
|
||||
# relies on. blkdevparts= gives every vda partition a PARTNAME in sysfs.
|
||||
mkdir -p /dev/block/by-name
|
||||
for uev in /sys/class/block/vda*/uevent; do
|
||||
[ -f "$uev" ] || continue
|
||||
partname=""
|
||||
devname=""
|
||||
while IFS='=' read -r k v; do
|
||||
case "$k" in
|
||||
PARTNAME) partname="$v" ;;
|
||||
DEVNAME) devname="$v" ;;
|
||||
esac
|
||||
done < "$uev"
|
||||
[ -n "$partname" ] && [ -n "$devname" ] \
|
||||
&& ln -sf "/dev/$devname" "/dev/block/by-name/$partname"
|
||||
done
|
||||
|
||||
# Slot select: whole-token parse of warden.slot= (never a substring match).
|
||||
slot="_a"
|
||||
for tok in $(cat /proc/cmdline); do
|
||||
case "$tok" in
|
||||
warden.slot=*) slot="${tok#warden.slot=}" ;;
|
||||
esac
|
||||
done
|
||||
case "$slot" in
|
||||
_a|_b) ;;
|
||||
*) echo "rc: bad warden.slot='$slot', falling back to _a"; slot="_a" ;;
|
||||
esac
|
||||
|
||||
root="/dev/block/by-name/rootfs${slot}"
|
||||
if [ ! -e "$root" ]; then
|
||||
echo "rc: $root missing — staying in initramfs"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p /mnt
|
||||
if ! mount -t ext4 "$root" /mnt; then
|
||||
echo "rc: mount of $root failed — staying in initramfs"
|
||||
return 0
|
||||
fi
|
||||
if [ ! -x /mnt/sbin/init ]; then
|
||||
echo "rc: $root has no /sbin/init — staying in initramfs"
|
||||
umount /mnt
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "rc: switching root to rootfs${slot} ($root)"
|
||||
exec switch_root /mnt /sbin/init
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/bin/busybox sh
|
||||
# WardenOS QEMU device sim: initramfs /init (PID 1).
|
||||
#
|
||||
# Phase-1 duty: prove the kernel booted on -M virt — print the sentinel the
|
||||
# smoke test greps for, then power off (PSCI SYSTEM_OFF, so qemu exits).
|
||||
# `warden.shell` on the kernel cmdline drops to an interactive shell instead.
|
||||
|
||||
/bin/busybox mount -t devtmpfs devtmpfs /dev 2>/dev/null
|
||||
|
||||
# The cpio archive carries no device nodes (it is built unprivileged, no
|
||||
# mknod); reopen stdio on the real console now that devtmpfs is mounted.
|
||||
exec </dev/console >/dev/console 2>&1
|
||||
|
||||
/bin/busybox --install -s /bin
|
||||
mount -t proc proc /proc
|
||||
mount -t sysfs sysfs /sys
|
||||
|
||||
echo "WARDEN-QEMU-BOOT-OK"
|
||||
|
||||
# With a virtio disk attached, hand over to the stage-1 rc (by-name symlinks,
|
||||
# slot select, switch_root). It only returns on failure — then fall through to
|
||||
# the diskless shell/poweroff behavior below.
|
||||
if [ -b /dev/vda ]; then
|
||||
. /etc/rc
|
||||
fi
|
||||
|
||||
if grep -qw warden.shell /proc/cmdline; then
|
||||
echo "warden.shell: interactive shell (exit to power off)"
|
||||
setsid cttyhack sh
|
||||
fi
|
||||
|
||||
poweroff -f
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/bin/busybox sh
|
||||
# Stage-2 init: PID 1 on the disk rootfs (rootfs_a or rootfs_b), reached via
|
||||
# switch_root from the initramfs. Brings up the minimum a WardenOS userspace
|
||||
# needs — mounts, by-name symlinks, network, serial alias — then starts any
|
||||
# payload daemons and holds. This stands in for the device's BusyBox SysV
|
||||
# /etc/init.d/S* sequence; it is deliberately tiny, not a model of it.
|
||||
|
||||
/bin/busybox mount -t devtmpfs devtmpfs /dev 2>/dev/null
|
||||
exec </dev/console >/dev/console 2>&1
|
||||
/bin/busybox --install -s /bin
|
||||
|
||||
mount -t proc proc /proc
|
||||
mount -t sysfs sysfs /sys
|
||||
mount -t tmpfs tmpfs /tmp
|
||||
|
||||
# Fresh devtmpfs — repopulate the by-name contract (slotctl.rs depends on it).
|
||||
mkdir -p /dev/block/by-name
|
||||
for uev in /sys/class/block/vda*/uevent; do
|
||||
[ -f "$uev" ] || continue
|
||||
partname=""
|
||||
devname=""
|
||||
while IFS='=' read -r k v; do
|
||||
case "$k" in
|
||||
PARTNAME) partname="$v" ;;
|
||||
DEVNAME) devname="$v" ;;
|
||||
esac
|
||||
done < "$uev"
|
||||
[ -n "$partname" ] && [ -n "$devname" ] \
|
||||
&& ln -sf "/dev/$devname" "/dev/block/by-name/$partname"
|
||||
done
|
||||
|
||||
# Slot (whole-token parse, same rule as stage 1).
|
||||
slot="_a"
|
||||
for tok in $(cat /proc/cmdline); do
|
||||
case "$tok" in
|
||||
warden.slot=*) slot="${tok#warden.slot=}" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# The device's matched mounts: persistent state and the slot's oem partition.
|
||||
mount -t ext4 /dev/block/by-name/userdata /userdata || echo "init: userdata mount failed"
|
||||
mount -t ext4 "/dev/block/by-name/oem${slot}" /oem || echo "init: oem${slot} mount failed"
|
||||
mkdir -p /userdata/warden
|
||||
|
||||
# RS485: warden-modbus hardcodes /dev/ttyS4 at compile time; alias it to the
|
||||
# VM's pci-serial UART when one is present (needs the virt.fragment kernel).
|
||||
[ -c /dev/ttyS0 ] && ln -sf /dev/ttyS0 /dev/ttyS4
|
||||
|
||||
# Network: slirp user-mode net on eth0 (DHCP, fallback to QEMU's static map).
|
||||
ip link set lo up
|
||||
if [ -e /sys/class/net/eth0 ]; then
|
||||
ip link set eth0 up
|
||||
if ! udhcpc -i eth0 -n -q -t 5 -T 2 >/dev/null 2>&1; then
|
||||
ip addr add 10.0.2.15/24 dev eth0 2>/dev/null
|
||||
ip route replace default via 10.0.2.2 dev eth0
|
||||
echo "nameserver 10.0.2.3" > /etc/resolv.conf
|
||||
fi
|
||||
fi
|
||||
|
||||
hostname warden-qemu
|
||||
|
||||
echo "WARDEN-QEMU-ROOTFS-OK slot=${slot}"
|
||||
|
||||
# Payload daemons (dropped into /usr/bin by qemu/mkimage.sh from qemu/payload/).
|
||||
for d in /usr/bin/warden-flared /usr/bin/warden-modbus; do
|
||||
if [ -x "$d" ]; then
|
||||
name="$(basename "$d")"
|
||||
echo "init: starting $name"
|
||||
"$d" > "/tmp/${name}.log" 2>&1 &
|
||||
fi
|
||||
done
|
||||
|
||||
if grep -qw warden.shell /proc/cmdline; then
|
||||
echo "warden.shell: interactive shell (exit powers off)"
|
||||
setsid cttyhack sh
|
||||
poweroff -f
|
||||
fi
|
||||
|
||||
# Hold: daemons run, console idles, scenarios drive the VM from outside.
|
||||
while :; do sleep 3600; done
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/busybox sh
|
||||
# Minimal udhcpc hook for the VM (busybox looks here by default).
|
||||
[ -n "$1" ] || exit 1
|
||||
case "$1" in
|
||||
deconfig)
|
||||
ip addr flush dev "$interface"
|
||||
ip link set "$interface" up
|
||||
;;
|
||||
bound|renew)
|
||||
ip addr replace "$ip/${mask:-24}" dev "$interface"
|
||||
[ -n "${router:-}" ] && ip route replace default via "${router%% *}" dev "$interface"
|
||||
if [ -n "${dns:-}" ]; then
|
||||
: > /etc/resolv.conf
|
||||
for d in $dns; do echo "nameserver $d" >> /etc/resolv.conf; done
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
# Launch the WardenOS device VM (qemu-system-arm -M virt, single Cortex-A7,
|
||||
# 256M — the RV1106G3's shape). See qemu/README.md for what this does and does
|
||||
# not emulate.
|
||||
#
|
||||
# Usage: run.sh --kernel <zImage> [options] [-- <extra qemu args>]
|
||||
# --kernel PATH zImage (canonical or virt.fragment variant)
|
||||
# --initrd PATH initramfs (default: qemu/out/initramfs.cpio.gz)
|
||||
# --disk PATH virtio disk image from mkimage.sh (default: qemu/out/disk.img
|
||||
# when present; pass --no-disk for a diskless initramfs boot)
|
||||
# --no-disk boot without a disk (initramfs shell/smoke behavior)
|
||||
# --slot _a|_b rootfs slot to boot (default _a)
|
||||
# --rtc DATE guest RTC base, e.g. 2021-01-01 — reproduces the no-RTC
|
||||
# "device boots believing 2021" incident class
|
||||
# --rs485 SOCK unix socket chardev for the RS485/Modbus bridge
|
||||
# (pci-serial: needs the virt.fragment kernel)
|
||||
# --watchdog add i6300esb watchdog, reset on expiry (fragment kernel)
|
||||
# --qmp SOCK QMP unix socket (screendump, input-send-event, quit)
|
||||
# --display MODE off (default, -nographic) | on (gtk window) | headless
|
||||
# (virtio-gpu without a window; screendump via --qmp)
|
||||
# --ssh-port N hostfwd 127.0.0.1:N -> guest :22 (default 2222)
|
||||
# --http-port N hostfwd 127.0.0.1:N -> guest :80 (default 8080)
|
||||
# --api-port N hostfwd 127.0.0.1:N -> guest :28443 (default 28443)
|
||||
# --shell interactive shell in the guest instead of daemon hold
|
||||
set -euo pipefail
|
||||
|
||||
QEMU_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
. "$QEMU_DIR/blkdevparts.conf"
|
||||
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
|
||||
SSH_PORT=2222 HTTP_PORT=8080 API_PORT=28443
|
||||
EXTRA=()
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--kernel) KERNEL="${2:?}"; shift 2 ;;
|
||||
--initrd) INITRD="${2:?}"; shift 2 ;;
|
||||
--disk) DISK="${2:?}"; shift 2 ;;
|
||||
--no-disk) NO_DISK=1; shift ;;
|
||||
--slot) SLOT="${2:?}"; shift 2 ;;
|
||||
--rtc) RTC="${2:?}"; shift 2 ;;
|
||||
--rs485) RS485="${2:?}"; shift 2 ;;
|
||||
--watchdog) WATCHDOG=1; shift ;;
|
||||
--qmp) QMP="${2:?}"; shift 2 ;;
|
||||
--display) DISPLAY_MODE="${2:?}"; shift 2 ;;
|
||||
--ssh-port) SSH_PORT="${2:?}"; shift 2 ;;
|
||||
--http-port) HTTP_PORT="${2:?}"; shift 2 ;;
|
||||
--api-port) API_PORT="${2:?}"; shift 2 ;;
|
||||
--shell) SHELL_FLAG=1; shift ;;
|
||||
--) shift; EXTRA=("$@"); break ;;
|
||||
*) echo "FATAL: unknown argument '$1' (see header of $0)" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -n "$KERNEL" ] && [ -f "$KERNEL" ] || {
|
||||
echo "FATAL: --kernel <zImage> required and must exist (got '${KERNEL:-}')" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -f "$INITRD" ] || {
|
||||
echo "FATAL: initramfs not found at $INITRD — run qemu/mkinitramfs.sh" >&2
|
||||
exit 1
|
||||
}
|
||||
case "$SLOT" in _a|_b) ;; *) echo "FATAL: --slot must be _a or _b" >&2; exit 1 ;; esac
|
||||
|
||||
if [ "$NO_DISK" -eq 0 ] && [ -z "$DISK" ] && [ -f "$OUT/disk.img" ]; then
|
||||
DISK="$OUT/disk.img"
|
||||
fi
|
||||
if [ -n "$DISK" ] && [ ! -f "$DISK" ]; then
|
||||
echo "FATAL: disk image $DISK not found — run qemu/mkimage.sh (or pass --no-disk)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# NOTE: never add `earlyprintk` — the config's DEBUG_UART_PHYS is the RV1106's
|
||||
# 0xff4c0000, which does not exist on -M virt.
|
||||
APPEND="console=ttyAMA0 rdinit=/init"
|
||||
ARGS=(
|
||||
-M virt -cpu cortex-a7 -smp 1 -m 256M
|
||||
-kernel "$KERNEL" -initrd "$INITRD"
|
||||
-netdev "user,id=n0,hostfwd=tcp:127.0.0.1:${SSH_PORT}-:22,hostfwd=tcp:127.0.0.1:${HTTP_PORT}-:80,hostfwd=tcp:127.0.0.1:${API_PORT}-:28443"
|
||||
-device virtio-net-device,netdev=n0
|
||||
-no-reboot
|
||||
)
|
||||
|
||||
if [ -n "$DISK" ] && [ "$NO_DISK" -eq 0 ]; then
|
||||
APPEND="$APPEND blkdevparts=$WARDEN_BLKDEVPARTS warden.slot=$SLOT"
|
||||
ARGS+=( -drive "if=none,file=$DISK,format=raw,id=vd0"
|
||||
-device virtio-blk-device,drive=vd0 )
|
||||
fi
|
||||
[ "$SHELL_FLAG" -eq 1 ] && APPEND="$APPEND warden.shell"
|
||||
[ -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"
|
||||
-device pci-serial,chardev=rs485 )
|
||||
[ -n "$QMP" ] && ARGS+=( -qmp "unix:$QMP,server=on,wait=off" )
|
||||
|
||||
case "$DISPLAY_MODE" in
|
||||
off) ARGS+=( -nographic ) ;;
|
||||
on) ARGS+=( -device virtio-gpu-device,xres=720,yres=720
|
||||
-device virtio-tablet-device -serial mon:stdio ) ;;
|
||||
headless) ARGS+=( -device virtio-gpu-device,xres=720,yres=720
|
||||
-device virtio-tablet-device -display none -serial mon:stdio ) ;;
|
||||
*) echo "FATAL: --display must be off|on|headless" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
exec qemu-system-arm "${ARGS[@]}" -append "$APPEND" ${EXTRA[@]+"${EXTRA[@]}"}
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
# Boot the warden kernel under qemu-system-arm -M virt and assert the initramfs
|
||||
# sentinel appears on the console. FAILS CLOSED: a missing zImage, initramfs,
|
||||
# or qemu binary is an error, never a skip.
|
||||
#
|
||||
# Usage: boot-smoke.sh <zImage> [initramfs.cpio.gz]
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # qemu/tests/
|
||||
QDIR="$(cd "$HERE/.." && pwd)" # qemu/
|
||||
|
||||
ZIMAGE="${1:-}"
|
||||
[ -n "$ZIMAGE" ] && [ -f "$ZIMAGE" ] || {
|
||||
echo "FATAL: usage: $0 <zImage> [initramfs] — zImage missing or not a file: '${ZIMAGE:-}'" >&2
|
||||
exit 1
|
||||
}
|
||||
INITRD="${2:-$QDIR/out/initramfs.cpio.gz}"
|
||||
[ -f "$INITRD" ] || {
|
||||
echo "FATAL: initramfs not found at $INITRD — run qemu/mkinitramfs.sh first" >&2
|
||||
exit 1
|
||||
}
|
||||
command -v qemu-system-arm >/dev/null || {
|
||||
echo "FATAL: qemu-system-arm not on PATH (apt-get install qemu-system-arm) — see qemu/README.md" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
LOG="$(mktemp "${TMPDIR:-/tmp}/warden-qemu-smoke.XXXXXX")"
|
||||
trap 'rm -f "$LOG"' EXIT
|
||||
|
||||
# NOTE: never pass `earlyprintk` — the config's DEBUG_UART_PHYS is the RV1106's
|
||||
# 0xff4c0000, which does not exist on -M virt.
|
||||
timeout 180 qemu-system-arm \
|
||||
-M virt -cpu cortex-a7 -smp 1 -m 256M \
|
||||
-kernel "$ZIMAGE" -initrd "$INITRD" \
|
||||
-append "console=ttyAMA0 rdinit=/init" \
|
||||
-nographic -no-reboot </dev/null | tee "$LOG" || {
|
||||
echo "FATAL: qemu exited non-zero (or hung until the 180s timeout)" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
grep -q "WARDEN-QEMU-BOOT-OK" "$LOG" || {
|
||||
echo "FATAL: boot sentinel WARDEN-QEMU-BOOT-OK not found in console log" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "boot smoke OK"
|
||||
Reference in New Issue
Block a user