review: iteration-2 fixes (fragment assertion, evidence paths, hardening)
- build-kernel.sh fragment assertion: survives a missing trailing newline (the read footgun, reproduced) and treats an absent symbol on a disable line as FATAL, symmetric with the enable arm. - fetch-kernel-tarball.sh checks the pin before downloading; both fetchers add --retry-connrefused. - mkimage rejects '.'/'..' state keys. - ui-shot: VM liveness checked before every QMP call, console.log preserved as evidence on every failure path, repaint deadline widened to 90s with the contended-runner rationale documented. - rs485-bridge: overflow discards back off one gap and rate-limit their log line, mirroring the accept-loop fix; clippy nit fixed. - .gitignore ignores *.elf/*.map so the untracked artifacts cannot silently return; CI shellcheck glob now covers build/ and the rootfs boot scripts (directives added for the deliberate in-guest source paths). - Docs: NPU parity row matches its sibling verification docs; line-pinned audit cross-references unpinned; CROSS_COMPILE documented in the build header; payload README lists warden-ui; ci-cd tense settled. Verified: guards negative-tested (bad state keys, no-newline fragment); boot smoke, portal scenario, ui-shot all PASS; 53 tests green; shellcheck clean across the widened glob; clippy zero. 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
2756de0b46
commit
973a414f07
+1
-1
@@ -18,7 +18,7 @@ qemu_get_busybox() {
|
||||
BB="${BUSYBOX:-$out/busybox-armv7l}"
|
||||
if [ ! -f "$BB" ]; then
|
||||
qemu_log "downloading $BB_URL"
|
||||
curl --retry 3 --retry-delay 5 -fSL "$BB_URL" -o "$BB"
|
||||
curl --retry 3 --retry-delay 5 --retry-connrefused -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
|
||||
|
||||
@@ -36,6 +36,9 @@ while [ $# -gt 0 ]; do
|
||||
*) echo "FATAL: --state needs KEY=VALUE, got '$2'" >&2; exit 1 ;;
|
||||
esac
|
||||
case "${2%%=*}" in
|
||||
.|..)
|
||||
echo "FATAL: --state key cannot be '.' or '..'" >&2
|
||||
exit 1 ;;
|
||||
*[!A-Za-z0-9_.]*|'')
|
||||
echo "FATAL: --state key '${2%%=*}' must match [A-Za-z0-9_.]+ (it becomes a filename)" >&2
|
||||
exit 1 ;;
|
||||
|
||||
@@ -19,6 +19,7 @@ Then:
|
||||
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.
|
||||
Stage-2 init starts `warden-flared`, `warden-modbus`, and `warden-ui` (the
|
||||
UI additionally needs `--display on|headless` + the virt.fragment kernel for
|
||||
/dev/fb0) automatically when present (logs land in `/tmp/<name>.log` inside
|
||||
the guest). An empty payload is valid — the image boots busybox-only.
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
# exits and the kernel panics; the applet-existence check below catches the
|
||||
# only preventable variant of that.
|
||||
|
||||
# shellcheck source=qemu/rootfs/etc/warden-lib.sh disable=SC1091
|
||||
. /etc/warden-lib.sh
|
||||
|
||||
warden_populate_by_name
|
||||
|
||||
@@ -29,6 +29,7 @@ warden_populate_by_name() {
|
||||
# VALIDATE it — echoes "_a" or "_b", falling back to _a with a warning.
|
||||
warden_slot() {
|
||||
slot="_a"
|
||||
# shellcheck disable=SC2013 # cmdline TOKENS are the unit here, not lines
|
||||
for tok in $(cat /proc/cmdline); do
|
||||
case "$tok" in
|
||||
warden.slot=*) slot="${tok#warden.slot=}" ;;
|
||||
|
||||
@@ -21,6 +21,7 @@ echo "WARDEN-QEMU-BOOT-OK"
|
||||
# slot select, switch_root). It only returns on failure — then fall through to
|
||||
# the diskless shell/poweroff behavior below.
|
||||
if [ -b /dev/vda ]; then
|
||||
# shellcheck source=qemu/rootfs/etc/rc disable=SC1091
|
||||
. /etc/rc
|
||||
fi
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ mount -t proc proc /proc
|
||||
mount -t sysfs sysfs /sys
|
||||
mount -t tmpfs tmpfs /tmp
|
||||
|
||||
# shellcheck source=qemu/rootfs/etc/warden-lib.sh disable=SC1091
|
||||
. /etc/warden-lib.sh
|
||||
|
||||
# Fresh devtmpfs — repopulate the by-name contract; same VALIDATED slot rule
|
||||
|
||||
@@ -61,6 +61,7 @@ pub fn pump_serial(
|
||||
stream.set_read_timeout(Some(gap))?;
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
let mut chunk = [0u8; 256];
|
||||
let mut discards: u64 = 0;
|
||||
loop {
|
||||
match (&*stream).read(&mut chunk) {
|
||||
Ok(0) => {
|
||||
@@ -72,12 +73,20 @@ pub fn pump_serial(
|
||||
Ok(n) => {
|
||||
buf.extend_from_slice(&chunk[..n]);
|
||||
if buf.len() > MAX_PENDING {
|
||||
eprintln!(
|
||||
"rs485: {} bytes buffered with no inter-frame gap — discarding \
|
||||
(misbehaving master streaming continuously?)",
|
||||
buf.len()
|
||||
);
|
||||
// Rate-limit the log and back off for one gap so a master
|
||||
// streaming continuously cannot peg a core and flood
|
||||
// stderr — mirroring the accept-loop backoff.
|
||||
discards += 1;
|
||||
if discards == 1 || discards.is_multiple_of(256) {
|
||||
eprintln!(
|
||||
"rs485: {} bytes buffered with no inter-frame gap — \
|
||||
discarding (misbehaving master? {} discards so far)",
|
||||
buf.len(),
|
||||
discards
|
||||
);
|
||||
}
|
||||
buf.clear();
|
||||
std::thread::sleep(gap);
|
||||
}
|
||||
}
|
||||
Err(e)
|
||||
|
||||
+20
-2
@@ -94,16 +94,29 @@ sys.exit(0 if distinct > 16 else 1)
|
||||
EOF
|
||||
}
|
||||
|
||||
# The VM can die mid-poll (OOM, crash): check liveness before every QMP
|
||||
# call so the failure is OUR message + console evidence, not a python
|
||||
# traceback — and preserve the console log before the trap removes $WORK.
|
||||
vm_alive_or_die() {
|
||||
kill -0 "$QEMU_PID" 2>/dev/null && return 0
|
||||
echo "FATAL: VM exited during the screendump poll" >&2
|
||||
tail -25 "$WORK/console.log" >&2
|
||||
mkdir -p "$OUTDIR"; cp "$WORK/console.log" "$OUTDIR/ui-shot-console.log" || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Poll for the first rendered frame (bounded, no guessed sleep).
|
||||
rendered=0
|
||||
deadline=$((SECONDS + 90))
|
||||
while [ $SECONDS -lt $deadline ]; do
|
||||
vm_alive_or_die
|
||||
qmp screendump "$WORK/shot1.ppm"
|
||||
if frame_rendered "$WORK/shot1.ppm"; then rendered=1; break; fi
|
||||
sleep 3
|
||||
done
|
||||
[ "$rendered" = 1 ] || {
|
||||
echo "FATAL: UI never rendered a non-blank frame within 90s" >&2
|
||||
mkdir -p "$OUTDIR"; cp "$WORK/console.log" "$OUTDIR/ui-shot-console.log" || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -112,9 +125,13 @@ done
|
||||
# repaint rather than guessing a delay.
|
||||
qmp tap 16975 1820
|
||||
changed=0
|
||||
deadline=$((SECONDS + 30))
|
||||
# 90s, matching the first-frame budget: TCG repaints are CPU-bound and a
|
||||
# contended CI runner can be arbitrarily slower than this dev box (same
|
||||
# margin reasoning as the rs485 test-gap widening).
|
||||
deadline=$((SECONDS + 90))
|
||||
while [ $SECONDS -lt $deadline ]; do
|
||||
sleep 2
|
||||
vm_alive_or_die
|
||||
qmp screendump "$WORK/shot2.ppm"
|
||||
if ! cmp -s "$WORK/shot1.ppm" "$WORK/shot2.ppm"; then changed=1; break; fi
|
||||
done
|
||||
@@ -124,7 +141,8 @@ cp "$WORK/shot1.ppm" "$OUTDIR/ui-shot1.ppm"
|
||||
cp "$WORK/shot2.ppm" "$OUTDIR/ui-shot2.ppm" 2>/dev/null || true
|
||||
|
||||
[ "$changed" = 1 ] || {
|
||||
echo "FATAL: tapping the Metrics tab did not change the frame within 30s — touch is not reaching the UI" >&2
|
||||
echo "FATAL: tapping the Metrics tab did not change the frame within 90s — touch is not reaching the UI" >&2
|
||||
cp "$WORK/console.log" "$OUTDIR/ui-shot-console.log" || true
|
||||
exit 1
|
||||
}
|
||||
echo "tap on the Metrics tab repainted the frame (touch reached the UI)"
|
||||
|
||||
Reference in New Issue
Block a user