diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4181a82..8f3f633 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,7 +108,9 @@ jobs: - name: shellcheck qemu scripts run: | sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck - shellcheck -x qemu/*.sh qemu/tests/*.sh + shellcheck -x qemu/*.sh qemu/tests/*.sh build/*.sh \ + qemu/rootfs/etc/warden-lib.sh qemu/rootfs/etc/rc \ + qemu/rootfs/sbin/init qemu/rootfs/init - name: cache pinned busybox uses: actions/cache@v4 with: diff --git a/.gitignore b/.gitignore index 8493652..471c91e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ target/ # build artifacts (firmware/kernel objects are rebuilt from source) *.o *.a +*.elf +*.map # scratch / editor *.swp diff --git a/build/build-kernel.sh b/build/build-kernel.sh index f5db1e9..114f68b 100755 --- a/build/build-kernel.sh +++ b/build/build-kernel.sh @@ -10,6 +10,8 @@ # Env: # KERNEL_TARBALL path to a local linux-6.18.46.tar.xz (skips the download) # SDK_TC dir holding the arm-rockchip830 uclibc cross toolchain bin/ +# CROSS_COMPILE cross-compiler prefix (default arm-rockchip830-linux-uclibcgnueabihf-; +# CI overrides with the generic arm-linux-gnueabihf-) # WORK build scratch dir (default: a mktemp under $TMPDIR) # JOBS parallel make jobs (default: nproc) # WARDEN_KCONFIG_FRAGMENT @@ -119,7 +121,7 @@ make -C "$SRC" ARCH=arm CROSS_COMPILE="$CROSS_COMPILE" olddefconfig >/dev/null # request in the fragment survived into the final .config; fail loud if not. if [ -n "${WARDEN_KCONFIG_FRAGMENT:-}" ]; then frag_fail=0 - while IFS= read -r line; do + while IFS= read -r line || [ -n "$line" ]; do case "$line" in CONFIG_*=*) grep -qxF "$line" "$SRC/.config" || { @@ -127,9 +129,11 @@ if [ -n "${WARDEN_KCONFIG_FRAGMENT:-}" ]; then frag_fail=1 } ;; "# CONFIG_"*" is not set") - opt="${line#\# }"; opt="${opt% is not set}" - grep -qE "^$opt=" "$SRC/.config" && { - echo "FATAL: fragment disabled '$opt' but it is set in the final .config" >&2 + # Symmetric with the enable arm: the exact disable line must be + # present. A symbol absent entirely means a typo'd/renamed option, + # not a successful disable. + grep -qxF "$line" "$SRC/.config" || { + echo "FATAL: fragment line '$line' not reflected in the final .config" >&2 frag_fail=1 } ;; esac diff --git a/build/fetch-kernel-tarball.sh b/build/fetch-kernel-tarball.sh index ffc996a..6d16cbe 100644 --- a/build/fetch-kernel-tarball.sh +++ b/build/fetch-kernel-tarball.sh @@ -14,14 +14,16 @@ URL="https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-$KVER.tar.xz" TB="${1:?usage: fetch-kernel-tarball.sh }" -if [ ! -f "$TB" ]; then - echo "== downloading $URL" - curl --retry 3 --retry-delay 5 -fSL "$URL" -o "$TB" -fi +# Pin first: a forgotten pin on a KVER bump should refuse BEFORE burning a +# 140MB download it will then reject anyway. [ -f "$SHA_FILE" ] || { echo "FATAL: no pinned sha256 for linux-$KVER (expected $SHA_FILE) — refusing an unverified tarball" >&2 exit 1 } +if [ ! -f "$TB" ]; then + echo "== downloading $URL" + curl --retry 3 --retry-delay 5 --retry-connrefused -fSL "$URL" -o "$TB" +fi want="$(cat "$SHA_FILE")" got="$(sha256sum "$TB" | awk '{print $1}')" [ "$want" = "$got" ] || { echo "FATAL: tarball sha256 mismatch: want $want got $got" >&2; exit 1; } diff --git a/docs/ci-cd.md b/docs/ci-cd.md index 5e7794e..4e13a52 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -2,7 +2,7 @@ `.github/workflows/ci.yml` — every job runs on GitHub-hosted `ubuntu-latest`. No self-hosted runner is (or may be) reachable from this repo's workflows: -the repo is going public, and a fork PR that gets one approved run could +the repo is public, and a fork PR that gets one approved run could otherwise execute code on private infrastructure (ADR-0007). ## Jobs diff --git a/kernel/rv1106-enablement/DRIVER-PARITY.md b/kernel/rv1106-enablement/DRIVER-PARITY.md index 6fad692..133c49f 100644 --- a/kernel/rv1106-enablement/DRIVER-PARITY.md +++ b/kernel/rv1106-enablement/DRIVER-PARITY.md @@ -38,7 +38,7 @@ c8a3, not just compiled. | GPIO_SYSFS / crypto / CFG80211 | — | mainline (config) | [x] =y (batch2) | | AIC8800 wifi (bsp/fdrv) | aic8800_* | **out-of-tree** | [x] M5 — wlan0 up, scanned the site AP at −43dBm (modules, `wifi/VERIFIED-on-c8a3.md`) | | AIC8800 BT (btlpm) | aic8800_btlpm | **out-of-tree** | [wip] module built (6.18 vermagic); HCI bring-up not yet exercised | -| NPU (rknpu, ff660000) | rknpu, ff660000.npu | **out-of-tree** | [wip] M6 built, 0 errors/0 warnings, 99 `rknpu`-prefixed symbols in `System.map`, `&npu {status="okay"}` in the dtb — **not yet flashed/probed on hardware** (build-only session; see `npu/PORT-PROGRESS.md`) | +| NPU (rknpu, ff660000) | rknpu, ff660000.npu | **out-of-tree** | [x] open GPL driver VERIFIED on hardware — `/dev/dri/card1`, `rknpu_version_test` PASS (power/clock/reset path exercised); open *compute* (regcmd) remains a from-scratch RE project (`npu/VERIFIED.md`, `npu/OPEN-NPU-PLAN.md`) | | RGA 2D (rga2) | rga2 | ported (vendor char-dev) | [x] /dev/rga, hw 3.3.87975 | | I2S audio (i2s-tdm) | i2s | rv1126 fallback (=y) | [x] cpu DAI registers (part of the card below) | | Audio codec (acodec) | rockchip,rv1106-codec | ported (rv1106_codec.c) | [x] card `rv1106-acodec`, pcmC0D0p/c (`audio/`); audible test @ bench | diff --git a/kernel/rv1106-enablement/mailbox/PLAN.md b/kernel/rv1106-enablement/mailbox/PLAN.md index 74b421a..2d03d84 100644 --- a/kernel/rv1106-enablement/mailbox/PLAN.md +++ b/kernel/rv1106-enablement/mailbox/PLAN.md @@ -25,7 +25,7 @@ watchdog exactly as-is (different threat model, different job). - **Controller (Linux side): non-issue.** `drivers/mailbox/rockchip-mailbox.c` is upstream in mainline 6.18 and **already binds on our exact kernel** via the generic `rockchip,rk3368-mailbox` **fallback compatible** with **zero patching** - — recorded in `CAPABILITIES-AUDIT.md:30`, confirmed by source read. RV1106's DT + — recorded in `CAPABILITIES-AUDIT.md`'s Remaining-blocks table (mailbox row), confirmed by source read. RV1106's DT declares both instances with that fallback string. Gated today only by `status="disabled"` + `CONFIG_ROCKCHIP_MBOX` being absent from the defconfig. - **HPMCU firmware (MCU side): we already do the hard part.** WardenOS has a @@ -241,7 +241,7 @@ adopt rpmsg/virtio unless the payload complexity genuinely demands it. moot, since our `hpmcu.rs` is an independent hardware-validated reimplementation. --- -_Cross-refs: `../CAPABILITIES-AUDIT.md:30`, `../REMAINING-PORTS.md §7`, +_Cross-refs: `../CAPABILITIES-AUDIT.md`, `../REMAINING-PORTS.md §7`, `../../luckfox-pico-86-panel/riscv-mcu.md`, `.../raw/followup-riscv-mcu.md`, `flare-edge/major-app-additions/docs/decisions/0002-hpmcu-watchdog.md`, diff --git a/kernel/rv1106-enablement/npu/OPEN-NPU-PLAN.md b/kernel/rv1106-enablement/npu/OPEN-NPU-PLAN.md index ec4a2eb..7f2e372 100644 --- a/kernel/rv1106-enablement/npu/OPEN-NPU-PLAN.md +++ b/kernel/rv1106-enablement/npu/OPEN-NPU-PLAN.md @@ -197,7 +197,8 @@ the whole problem. `PROVENANCE.md`: the kernel driver is portable GPL; the closed piece is the userspace RKNN runtime + regcmd format (a blob). **Per directive we do not ship -that blob.** `CAPABILITIES-AUDIT.md:32` rates NPU "not worth shipping" until an +that blob.** `CAPABILITIES-AUDIT.md`'s Remaining-blocks table rates the NPU +"open driver VERIFIED; compute deferred" until an open encoder exists. ### URLs @@ -299,5 +300,5 @@ ever begun; otherwise this is the documented reason open NPU compute is deferred --- _Cross-refs: `PORT-PLAN.md` (authoritative file-by-file kernel port), -`../../docs/npu-graphics-feasibility.md`, `../CAPABILITIES-AUDIT.md:32`, +`../../docs/npu-graphics-feasibility.md`, `../CAPABILITIES-AUDIT.md`, `../PROVENANCE.md`, `../DRIVER-PARITY.md:41`, `../REMAINING-PORTS.md §6`._ diff --git a/qemu/lib.sh b/qemu/lib.sh index 4249709..2470945 100644 --- a/qemu/lib.sh +++ b/qemu/lib.sh @@ -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 diff --git a/qemu/mkimage.sh b/qemu/mkimage.sh index 7d38e98..7b85626 100755 --- a/qemu/mkimage.sh +++ b/qemu/mkimage.sh @@ -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 ;; diff --git a/qemu/payload/README.md b/qemu/payload/README.md index cba4483..f5554a8 100644 --- a/qemu/payload/README.md +++ b/qemu/payload/README.md @@ -19,6 +19,7 @@ Then: cp /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/.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/.log` inside +the guest). An empty payload is valid — the image boots busybox-only. diff --git a/qemu/rootfs/etc/rc b/qemu/rootfs/etc/rc index fa2c335..66d9f94 100755 --- a/qemu/rootfs/etc/rc +++ b/qemu/rootfs/etc/rc @@ -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 diff --git a/qemu/rootfs/etc/warden-lib.sh b/qemu/rootfs/etc/warden-lib.sh index 72801dc..6e24d10 100644 --- a/qemu/rootfs/etc/warden-lib.sh +++ b/qemu/rootfs/etc/warden-lib.sh @@ -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=}" ;; diff --git a/qemu/rootfs/init b/qemu/rootfs/init index ad759fe..c03c699 100755 --- a/qemu/rootfs/init +++ b/qemu/rootfs/init @@ -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 diff --git a/qemu/rootfs/sbin/init b/qemu/rootfs/sbin/init index c13b15c..efdd651 100755 --- a/qemu/rootfs/sbin/init +++ b/qemu/rootfs/sbin/init @@ -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 diff --git a/qemu/rs485-bridge/src/lib.rs b/qemu/rs485-bridge/src/lib.rs index bb2fae2..22f0b22 100644 --- a/qemu/rs485-bridge/src/lib.rs +++ b/qemu/rs485-bridge/src/lib.rs @@ -61,6 +61,7 @@ pub fn pump_serial( stream.set_read_timeout(Some(gap))?; let mut buf: Vec = 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) diff --git a/qemu/tests/ui-shot.sh b/qemu/tests/ui-shot.sh index c58ee83..11a3a8a 100755 --- a/qemu/tests/ui-shot.sh +++ b/qemu/tests/ui-shot.sh @@ -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)"