From 4ee4dfcf88f95f46cd90c548920aed013df0d694 Mon Sep 17 00:00:00 2001 From: BFE Engineering Date: Tue, 25 Aug 2026 15:48:44 -0600 Subject: [PATCH] review: fix review findings (correctness, hardening, doc-accuracy) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First recursive code-review-harness pass over the authored SDK code (sim/, config-lint, flowgen, drivers/{relays,freshness}, build/, ci.yml). Four parallel dimension reviewers; all findings at every severity corrected per workspace rule. Correctness / reliability: - freshness.c min_budget_ms: use a `seen` flag, not `best==0`, as the empty sentinel — a zero-tolerance (max_stale_ms==0) binding was silently widened to a looser neighbour's budget. Regression test added; still 66/66 MC/DC. - config-lint parse_reserved_ranges: match `reg` as a whole property token (ident boundary before, `=` after) so `reg-names` / a `region-*` label no longer mis-parses into a bogus reserved range. - config-lint loader check: fail closed — flag any loader with a LOAD_ADDR that is not a known-safe boot component, instead of only known MCU names, so a future coprocessor ("Rtos"/"Bl32") can't slip past the 0x40000-brick gate. - build-kernel.sh: sha256 verification is now mandatory (refuse to build if the pin is missing) and the mktemp scratch tree is removed on exit (trap), while a caller-provided WORK is left intact for CI artifact upload. Test quality: - freshness: added the age==max_stale boundary case and a clock-wraparound (now < last_ok) fail-safe-to-UNKNOWN test. - relays: unsetenv(WARDEN_GPIO_ROOT) at main() so the NULL-env arm is hermetic. Security / CI: - ci.yml: top-level `permissions: contents: read` (badges overrides to write); pin taiki-e/install-action to commit SHA (v2.86.7). Maintainability / docs: - drivers/enforce-mcdc.sh: one shared, name-derived gate replaces the two copy-pasted per-driver scripts; Makefiles call ../../enforce-mcdc.sh. - docs/architecture.md: §3/§4/§6/§7 rewritten to match reality — NPU/RGA models, config-lint, and the relays+freshness MC/DC harnesses are done; kernel §6 now reflects the 5.10->6.18.46 forward-port (ADR-0001), not the superseded plan44/6.6. - README: status blurb + layout table corrected (kernel/, .github/; stale ci/ and patches/ descriptions fixed). hpmcu "8 tests" -> 7 in docs. - freshness.{c,h}: ADR reference points at flare-edge ADR-0004 (warden-sdk's ADR-0004 is the CI runner — number collision). - normalize rustfmt drift across sim/ + config-lint. All green: sim 37 tests, config-lint 8 tests, both drivers 100% MC/DC (relays 40/40, freshness 66/66), clippy clean under -D warnings, gitleaks clean. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf --- .github/workflows/ci.yml | 7 +- .gitignore | 5 + README.md | 28 +++-- build/build-kernel.sh | 30 +++-- docs/architecture.md | 74 +++++++----- drivers/README.md | 9 +- drivers/{relays/test => }/enforce-mcdc.sh | 22 ++-- drivers/freshness/freshness.c | 9 +- drivers/freshness/freshness.h | 2 +- drivers/freshness/test/Makefile | 2 +- drivers/freshness/test/enforce-mcdc.sh | 48 -------- drivers/freshness/test/test_freshness.c | 30 ++++- drivers/relays/test/Makefile | 2 +- drivers/relays/test/test_relays.c | 5 + sim/benches/sim_bench.rs | 13 +- sim/src/cru.rs | 6 +- sim/src/hpmcu.rs | 24 +++- sim/src/modbus.rs | 20 ++- sim/src/npu.rs | 15 ++- sim/src/rga.rs | 31 ++++- tools/config-lint/src/lib.rs | 141 ++++++++++++++++++++-- 21 files changed, 375 insertions(+), 148 deletions(-) rename drivers/{relays/test => }/enforce-mcdc.sh (56%) delete mode 100755 drivers/freshness/test/enforce-mcdc.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b663fed..a663ee1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,11 @@ concurrency: group: ci-${{ github.ref }} cancel-in-progress: true +# Least privilege by default: every job gets a read-only token; only `badges` +# (which commits rendered SVGs) overrides this with contents: write below. +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest @@ -34,7 +39,7 @@ jobs: done - name: coverage (cargo-llvm-cov on sim) run: rustup component add llvm-tools-preview - - uses: taiki-e/install-action@v2 + - uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc # v2.86.7 with: tool: cargo-llvm-cov - name: run coverage diff --git a/.gitignore b/.gitignore index dd0a7de..862a5b0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,11 @@ target/ *.swp *~ .DS_Store +__pycache__/ +*.pyc + +# local tooling state (code-review harness cross-session memory, etc.) +.claude/ # driver MC/DC harness build dirs **/test/build/ diff --git a/README.md b/README.md index 46abc15..142162b 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,13 @@ RV1106 HMI). A from-scratch replacement for the twice-ported vendor stack the rest of the firmware: tested, benchmarked, reproducible, and honest about what runs on real silicon versus what we simulate. -> Status: **bootstrapping.** This repo is being stood up incrementally; today it -> hosts the hardware **simulator** and its tests. The kernel forward-port and the -> hermetic image build move in as each is proven. Until then, flare-edge still -> builds firmware from the vendored SDK + `sdk-patches/`; nothing here is on the -> production build path yet. +> Status: **bringup.** The hardware **simulator** and its tests, the RV1106 kernel +> forward-port as a reviewable `patches/` series, the hermetic kernel build, and two +> Tier-1 drivers at 100% MC/DC are all in and CI-green on the `bringup` branch. What +> remains before this is on the production build path: installing the self-hosted +> kernel-build runner (see `docs/ci-cd.md`) and having flare-edge consume warden-sdk +> as a dependency — both [maintainer]-gated. Until then, flare-edge still builds firmware from +> the vendored SDK + `sdk-patches/`. ## Why a new SDK @@ -87,13 +89,15 @@ than duplicate each other. ## Layout ``` -sim/ the hardware simulator (Rust): mailbox/devmem model, HPMCU, RGA, NPU. -drivers/ our own hardened drivers + their seams (as they migrate in). -patches/ the vendor-SDK delta (mirrors flare-edge/sdk-patches until it moves here). -build/ the hermetic image-build wrapper (kernel → rootfs → image), incremental. -ci/ CI: patches-still-apply, host tests, coverage, benchmarks. -docs/ architecture + ADRs (decisions/). -tools/ dev tooling. config-lint: static target-config gates (MCU-load-vs-reserved-memory — the 0x40000 brick class). +sim/ the hardware simulator (Rust): membus/devmem, HPMCU, CRU, Modbus, RGA, NPU. +drivers/ our own hardened drivers + their seams (relays, freshness; more migrate in). +patches/ the RV1106 kernel forward-port delta onto pristine linux-6.18.46 (subsystem-split). +kernel/ forward-port docs + provenance (rv1106-enablement/, PROVENANCE.md). +build/ the hermetic kernel build (fetch pristine → apply patches → zImage + dtb). +docs/ architecture + ADRs (decisions/) + ci-cd + generated workflow flowcharts. +tools/ dev tooling. config-lint: static target-config gates (MCU-load-vs-reserved-memory, + the 0x40000 brick class); flowgen: the workflow-flowchart generator. +.github/ CI (workflows/ci.yml): patches-apply, host tests, coverage, MC/DC, benchmarks, badges. ``` ## Principles diff --git a/build/build-kernel.sh b/build/build-kernel.sh index 1eae7ba..919f65d 100755 --- a/build/build-kernel.sh +++ b/build/build-kernel.sh @@ -23,9 +23,20 @@ REPO="$(cd "$HERE/.." && pwd)" PATCHES="$REPO/patches" SHA_FILE="$HERE/linux-$KVER.tar.xz.sha256" JOBS="${JOBS:-$(nproc)}" -WORK="${WORK:-$(mktemp -d "${TMPDIR:-/tmp}/warden-kbuild.XXXXXX")}" URL="https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-$KVER.tar.xz" +# A caller-provided WORK (e.g. CI's ${{ github.workspace }}/kbuild-out, from which +# artifacts are uploaded) is left intact; a scratch dir we mktemp'd here is our own +# (>1GB of extracted source + build output) and is removed on exit so repeated runs +# on the shared SDK box don't fill the disk. +if [ -n "${WORK:-}" ]; then + WORK_OWNED=0 +else + WORK="$(mktemp -d "${TMPDIR:-/tmp}/warden-kbuild.XXXXXX")" + WORK_OWNED=1 +fi +trap '[ "${WORK_OWNED:-0}" = 1 ] && rm -rf "$WORK"' EXIT + log() { printf '\033[36m== %s\033[0m\n' "$*"; } command -v python >/dev/null || { echo "need 'python' (not python3) on PATH — SDK quirk" >&2; exit 1; } @@ -37,12 +48,17 @@ if [ ! -f "$TB" ]; then log "downloading $URL" curl -fSL "$URL" -o "$TB" fi -if [ -f "$SHA_FILE" ]; then - want="$(cat "$SHA_FILE")" - got="$(sha256sum "$TB" | awk '{print $1}')" - [ "$want" = "$got" ] || { echo "tarball sha256 mismatch: want $want got $got" >&2; exit 1; } - log "tarball sha256 verified" -fi +# Fail closed: a missing pin (e.g. forgotten on a KVER bump) or a KERNEL_TARBALL +# pointed at an arbitrary file must refuse to build, never silently skip the check +# — the pristine tarball is the ONLY external input and integrity is the whole point. +[ -f "$SHA_FILE" ] || { + echo "FATAL: no pinned sha256 for linux-$KVER (expected $SHA_FILE) — refusing to build from an unverified tarball" >&2 + exit 1 +} +want="$(cat "$SHA_FILE")" +got="$(sha256sum "$TB" | awk '{print $1}')" +[ "$want" = "$got" ] || { echo "tarball sha256 mismatch: want $want got $got" >&2; exit 1; } +log "tarball sha256 verified" # 2. extract pristine SRC="$WORK/linux-$KVER" diff --git a/docs/architecture.md b/docs/architecture.md index abe067d..0089902 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -55,7 +55,7 @@ supervisor logic runs in CI with no panel. - **`membus` — register/SRAM bus.** Done. `MemBus` trait + `SimBus`. - **`hpmcu` — the RISC-V watchdog coprocessor.** Done. Faithful port of `hpmcu/watchdog/main.c`'s state machine (boot-grace, heartbeat-timeout, disarm, - fire) against a `SimBus` mailbox, virtual clock, 8 tests including the + fire) against a `SimBus` mailbox, virtual clock, 7 tests including the arm-within-grace no-boot-loop safety property. This is the model that would have let the boot-loaded-watchdog logic be validated before the flash that bricked a bench unit (though the *layout* fault — a load address in unreserved kernel RAM — @@ -70,9 +70,16 @@ supervisor logic runs in CI with no panel. and fault injection — silent-drop and forced-NAK) so `warden-modbus`'s master can be hardened to MC/DC against realistic device behaviour with no serial hardware. MEI (0x2B/0x0E) identification is the documented follow-up. -- **Next:** an **NPU** load model behind the path seam (deferred — no NPU feature - ships soon); a **GPIO/relay** sysfs model (largely covered by the `WARDEN_GPIO_ROOT` - seam in flare-edge's `tests/relays-mcdc/`); an **RGA** recording fake. +- **`npu` — NPU load model.** Done. `NpuSim` models `/proc/rknpu/load` (the exact + "NPU load: N%" text the sysmon reads) behind the path seam, so the load-readout UI + is host-testable. NPU *compute* is explicitly out of scope — no inference runs here. +- **`rga` — 2D blitter offload.** Done. `RgaSim`, a recording `improcess` fake with a + programmable `IM_STATUS`, so the RGA offload-dispatch and CPU-fallback logic is + exercised behind the `#if WARDEN_USE_RGA` seam without librga; wired into the + `rga_improcess` benchmark. +- **Next:** MEI (0x2B/0x0E) Modbus identification; the Tier-2 driver *sources* + (`modbus_engine.c`, `warden_rga.c`) migrate in with the flare-edge unification + (ADR-0005) — their hardware ends are already modelled and tested above. Integration with flare-edge: flared implements `MemBus` for `/dev/mem` and gains `#[cfg(test)]` tests driving its real arm/beat logic against `HpmcuSim`. This needs @@ -86,14 +93,15 @@ once the dependency exists. No duplication of *logic* — only the tiny trait. "100% MC/DC on 100% of drivers" is infeasible literally: ~97% of driver LOC is vendor blobs (AIC8800 wifi = 88.5K lines). Tiered target: -- **Tier 1 — our own hardware code → real MC/DC.** modbus master (`modbus_engine.c`), - relays (`relays.c`), the RGA wrapper's dispatch, the HPMCU supervisor - (`hpmcu.rs`), the devmem reset ladder. Method: the proven `tests/uboot-ab` - pattern — extract the unit, mock its world, build `-fcondition-coverage`, enforce - with `enforce-mcdc.sh` (gcc-14 `gcov --conditions`). **Gap the survey found: there - is no C-side coverage in CI at all today** — only flared line-coverage and the one - uboot-ab MC/DC file. Standing up an MC/DC harness for the first C driver - (`relays.c` — small, safety-relevant) is the first driver-hardening deliverable. +- **Tier 1 — our own hardware code → real MC/DC.** Method: the proven `tests/uboot-ab` + pattern — extract the unit behind a small injectable seam, mock its world, build + `-fcondition-coverage`, enforce with the shared `drivers/enforce-mcdc.sh` (gcc-14 + `gcov --conditions`) in the CI `mcdc` job. **Done here now:** `relays.c` (40/40 + conditions) and `freshness.c` (66/66), both at 100% MC/DC and CI-enforced. **Migrate + in next:** the modbus master (`modbus_engine.c`) and the RGA wrapper's dispatch — + their hardware ends are already modelled and tested in `sim/` (`modbus`, `rga`); the + driver sources move in with the flare-edge unification (ADR-0005). The HPMCU + supervisor and devmem reset ladder are covered Rust-side (`sim/hpmcu`, `sim/cru`). - **Tier 2 — near-mainline small drivers → branch coverage + fault injection.** - **Tier 3 — vendor blobs (AIC8800, MPP/ISP/RGA libs) → fault-injection hardening behind the seam,** not MC/DC. The AIC8800 SDIO-wedge Tier-1 fix + the designed @@ -124,24 +132,32 @@ fails against a DT with no `rtos@40000` node and passes once the reservation is added. **Next** target-config checks: partition-table-vs-image-size and vermagic-vs-kernel. -## 6. Kernel forward-port (separate, bounded phase) +## 6. Kernel forward-port (done — see ADR-0001) -Move to **plan44's OpenWrt RV1106 fork — Linux 6.6** (152 RV1106 patches + our -exact board DT), not mainline (no DT/clk/display/RGA/NPU/flash-boot merged). It is -a diff-and-borrow forward-port onto our Buildroot/uClibc base, not a swap-in -(plan44 drops Buildroot for OpenWrt/musl and ships no AIC8800 kmod). The dominant -risk is the struct-ABI break (the VLAN saga) — mitigated by shipping any kernel -move as one matched boot+oem image, never a partial reflash. This phase starts -after the sim + driver-hardening foundation, since those are how we'll regression -the port. +A self-built **Linux 6.18.46**, forward-ported directly from the vendor 5.10.160 tree +onto our Buildroot LTS/uClibc base — **not** the plan44/OpenWrt 6.6 fork this section +originally reached for. **ADR-0001 records why that was superseded:** plan44 drops +Buildroot for OpenWrt/musl and ships no AIC8800 kmod, so it was a swap-out, not a +forward-port. Mainline alone was not viable either (no DT/clk/display/RGA/NPU/ +flash-boot upstream for RV1106); the port reuses the already-in-mainline rv1126 +register data where it matches and carries our deltas as the reviewable `patches/` +series. This is **done and hardware-verified on `warden-c8a3`** — clk, pinctrl, eMMC, +GMAC, TRNG, OTP, SARADC/TSADC, RTC, USB host, PWM/backlight, VOP display, GT911 touch, +AIC8800 wifi, RGA, I2S audio, HPMCU mailbox, the open NPU driver, and PVTM all boot. +The dominant risk was the struct-ABI break (the VLAN saga), mitigated by shipping the +kernel move as one matched boot+oem image, never a partial reflash. +`build/build-kernel.sh` (the hermetic build) and the `patches-apply` CI gate keep the +series honest against pristine 6.18.46; provenance is in `patches/README.md` and +`kernel/rv1106-enablement/`. ## 7. Order of work -1. **Simulator core** — `membus` + `hpmcu` (done); reset-ladder + path-seam - scaffolding next. -2. **First C-driver MC/DC harness** — `relays.c`, establishing the C coverage gate. -3. **flared devmem/hpmcu seam + tests** (firmware-side trait; unify with `sim/` - when the repo has a remote). -4. **Config-lint CI gates** (§5) — the brick-class of bug. -5. **Hermetic image build** wrapper moves in. -6. **Kernel 6.6 forward-port** (§6). +1. **Simulator core** — `membus`, `hpmcu`, the `cru` reset ladder, `modbus`, plus the + `rga`/`npu` models. **Done.** +2. **C-driver MC/DC harnesses** — `relays.c` and `freshness.c` at 100% MC/DC, CI-gated + via the shared `drivers/enforce-mcdc.sh`. **Done** (the first C coverage gate). +3. **flared devmem/hpmcu seam + tests** — firmware-side trait, unified with `sim/` + once flare-edge consumes warden-sdk (a separate, [maintainer]-gated step). **Pending.** +4. **Config-lint CI gates** (§5) — the brick-class of bug. **Done.** +5. **Hermetic kernel build** (`build/build-kernel.sh` + the `patches-apply` gate). **Done.** +6. **Kernel 5.10→6.18.46 forward-port** (§6, ADR-0001). **Done** (hardware-verified). diff --git a/drivers/README.md b/drivers/README.md index dba9780..011d193 100644 --- a/drivers/README.md +++ b/drivers/README.md @@ -17,8 +17,11 @@ Self-contained logic with a clean seam, measured to **100% MC/DC** (gcc-14 **Adding a Tier-1 driver:** copy `.{c,h}` here, put the hardware/OS calls behind a small injectable seam, then mirror `relays/test/` (a fake backend for the logic -branches + a real backend over a scratch tree for the plumbing) and -`enforce-mcdc.sh`. The CI job picks up any `drivers/*/test/Makefile` automatically. +branches + a real backend over a scratch tree for the plumbing). Reuse the shared +gate — the Makefile calls `bash ../../enforce-mcdc.sh build/.c.gcov +build/test.rc` (it derives the driver name from the `.gcov` file, so there is no +per-driver copy to keep in sync). The CI `mcdc` job picks up any +`drivers/*/test/Makefile` automatically. ## Tier 2 — serious testing + fault-injection + benchmarks @@ -30,7 +33,7 @@ is already modelled and tested here** in `../sim/`: |---|---|---| | `modbus_engine.c` (RS485 master) | 11 pty scenarios + fault-injection + a compiled corpus walk (flare-edge `tools/modbus-sim/`, green) | `sim::modbus` RTU slave (11 tests, silent-drop/forced-NAK faults) + `modbus_read_holding` benchmark | | `warden_rga.c` (RGA offload) | offload-dispatch + CPU-fallback logic | `sim::rga` recording `improcess` fake (programmable IM_STATUS) + `rga_improcess` benchmark | -| HPMCU supervisor (`hpmcu.rs`) | arm/beat/fire + boot-grace safety property | `sim::hpmcu` (8 tests) + `hpmcu_tick` benchmark | +| HPMCU supervisor (`hpmcu.rs`) | arm/beat/fire + boot-grace safety property | `sim::hpmcu` (7 tests) + `hpmcu_tick` benchmark | **Why the Tier-2 *source* isn't vendored here yet:** `modbus_engine.c` and `warden_rga.c` pull in shared UI headers (`platform.h`, `settings.h`, `lv_*`) and diff --git a/drivers/relays/test/enforce-mcdc.sh b/drivers/enforce-mcdc.sh similarity index 56% rename from drivers/relays/test/enforce-mcdc.sh rename to drivers/enforce-mcdc.sh index 4fdb4ed..9cb2d4c 100755 --- a/drivers/relays/test/enforce-mcdc.sh +++ b/drivers/enforce-mcdc.sh @@ -1,9 +1,16 @@ #!/usr/bin/env bash -# Gate: fail unless every unit check passed AND relays.c reached 100% MC/DC -# (condition) coverage with no uncovered executable lines. -# $1 = gcov stdout log $2 = relays.c.gcov $3 = test exit-code file +# Shared Tier-1 MC/DC gate for every driver harness. Each driver's test/Makefile +# invokes it as: +# bash ../../enforce-mcdc.sh /.c.gcov +# Fails unless every unit check passed AND .c reached 100% MC/DC (condition) +# coverage with no uncovered executable lines. The driver name is derived from the +# .gcov filename so this one script serves all drivers (no per-driver copies to keep +# in sync). +# $1 = gcov stdout log $2 = .c.gcov $3 = test exit-code file set -uo pipefail LOG="$1"; GCOV="$2"; RCFILE="$3" +base="$(basename "$GCOV")"; base="${base%.gcov}" # e.g. freshness.c +name="${base%.c}" # e.g. freshness rc=0 testrc="$(cat "$RCFILE" 2>/dev/null || echo 1)" @@ -20,9 +27,10 @@ fi notcov="$(grep -nE "condition[s]? .*not covered" "$GCOV" || true)" uncov_lines="$(grep -nE "^ +#####:" "$GCOV" || true)" -# Summary lines for relays.c from gcov stdout. Match the exact file so -# 'test_relays.c' (which also contains "relays.c") is NOT picked up. -FMATCH="File '([^']*/)?relays[.]c'" +# Summary lines for .c from gcov stdout. Match the exact file so the test +# harness translation unit (test_.c, which also contains ".c") is NOT +# picked up. +FMATCH="File '([^']*/)?${name}[.]c'" cond_line="$(awk -v patt="$FMATCH" '$0 ~ patt {f=1} f&&/Condition outcomes covered:/{print; f=0}' "$LOG")" line_line="$(awk -v patt="$FMATCH" '$0 ~ patt {f=1} f&&/Lines executed:/{print; f=0}' "$LOG")" echo " ${line_line:-Lines executed: (n/a)}" @@ -34,7 +42,7 @@ if [ -n "$notcov" ]; then rc=1 fi if [ -n "$uncov_lines" ]; then - echo "RESULT: uncovered executable lines in relays.c:" + echo "RESULT: uncovered executable lines in ${name}.c:" echo "$uncov_lines" | sed 's/^/ /' rc=1 fi diff --git a/drivers/freshness/freshness.c b/drivers/freshness/freshness.c index b13b855..b5eee56 100644 --- a/drivers/freshness/freshness.c +++ b/drivers/freshness/freshness.c @@ -1,5 +1,5 @@ /* - * The UI Freshness Contract engine (ADR 0004) — core, LVGL-free. + * The UI Freshness Contract engine (flare-edge ADR-0004) — core, LVGL-free. * See freshness.h for the contract. LVGL binding lives in freshness_lv.c. */ #include "freshness.h" @@ -175,11 +175,16 @@ uint32_t warden_fresh_count(void) uint32_t warden_fresh_min_budget_ms(void) { + /* `seen` — not `best == 0` — marks "nothing scanned yet", so a legitimate + * zero-tolerance binding (max_stale_ms == 0, "must be fresh every tick") wins + * the minimum instead of being mistaken for the empty sentinel and widened. */ uint32_t best = 0; + bool seen = false; for(uint32_t i = 0; i < FRESH_MAX; i++) { struct warden_fresh *v = &s_vals[i]; - if(v->used && v->visible && (best == 0 || v->max_stale_ms < best)) { + if(v->used && v->visible && (!seen || v->max_stale_ms < best)) { best = v->max_stale_ms; + seen = true; } } return best; diff --git a/drivers/freshness/freshness.h b/drivers/freshness/freshness.h index d914de0..cb0d07f 100644 --- a/drivers/freshness/freshness.h +++ b/drivers/freshness/freshness.h @@ -1,5 +1,5 @@ /* - * The UI Freshness Contract engine (ADR 0004) — core, LVGL-free. + * The UI Freshness Contract engine (flare-edge ADR-0004) — core, LVGL-free. * * The panel is read on-site to judge whether hardware is healthy, so a silently * *stale* number is worse than a missing one: a stale IP or hashrate reads as diff --git a/drivers/freshness/test/Makefile b/drivers/freshness/test/Makefile index f7138ca..f41e32a 100644 --- a/drivers/freshness/test/Makefile +++ b/drivers/freshness/test/Makefile @@ -31,7 +31,7 @@ check: $(BUILD)/test @echo "== MC/DC (condition) coverage of freshness.c ==" @$(GCOV) --conditions --branch-probabilities -o $(BUILD) ../freshness.c >$(BUILD)/gcov.log 2>&1 || true @mv -f *.gcov $(BUILD)/ 2>/dev/null || true - @bash enforce-mcdc.sh $(BUILD)/gcov.log $(BUILD)/freshness.c.gcov $(BUILD)/test.rc + @bash ../../enforce-mcdc.sh $(BUILD)/gcov.log $(BUILD)/freshness.c.gcov $(BUILD)/test.rc report: check @grep -nE "condition.*not covered|conditions covered" $(BUILD)/freshness.c.gcov || true diff --git a/drivers/freshness/test/enforce-mcdc.sh b/drivers/freshness/test/enforce-mcdc.sh deleted file mode 100755 index 13eb3c8..0000000 --- a/drivers/freshness/test/enforce-mcdc.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash -# Gate: fail unless every unit check passed AND freshness.c reached 100% MC/DC -# (condition) coverage with no uncovered executable lines. -# $1 = gcov stdout log $2 = freshness.c.gcov $3 = test exit-code file -set -uo pipefail -LOG="$1"; GCOV="$2"; RCFILE="$3" -rc=0 - -testrc="$(cat "$RCFILE" 2>/dev/null || echo 1)" -if [ "$testrc" != "0" ]; then - echo "RESULT: unit checks FAILED (test exit $testrc)"; rc=1 -else - echo "RESULT: all unit checks passed" -fi - -if [ ! -f "$GCOV" ]; then - echo "RESULT: no coverage file ($GCOV) produced"; exit 1 -fi - -notcov="$(grep -nE "condition[s]? .*not covered" "$GCOV" || true)" -uncov_lines="$(grep -nE "^ +#####:" "$GCOV" || true)" - -# Summary lines for freshness.c from gcov stdout. Match the exact file so -# 'test_freshness.c' (which also contains "freshness.c") is NOT picked up. -FMATCH="File '([^']*/)?freshness[.]c'" -cond_line="$(awk -v patt="$FMATCH" '$0 ~ patt {f=1} f&&/Condition outcomes covered:/{print; f=0}' "$LOG")" -line_line="$(awk -v patt="$FMATCH" '$0 ~ patt {f=1} f&&/Lines executed:/{print; f=0}' "$LOG")" -echo " ${line_line:-Lines executed: (n/a)}" -echo " ${cond_line:-Condition outcomes covered: (n/a)}" - -if [ -n "$notcov" ]; then - echo "RESULT: MC/DC gaps (conditions not covered):" - echo "$notcov" | sed 's/^/ /' - rc=1 -fi -if [ -n "$uncov_lines" ]; then - echo "RESULT: uncovered executable lines in freshness.c:" - echo "$uncov_lines" | sed 's/^/ /' - rc=1 -fi - -if ! echo "$cond_line" | grep -q "100.00%"; then - echo "RESULT: condition coverage is below 100%" - rc=1 -fi - -[ "$rc" = "0" ] && echo "RESULT: 100% MC/DC + all checks green ✓" -exit "$rc" diff --git a/drivers/freshness/test/test_freshness.c b/drivers/freshness/test/test_freshness.c index c46fce1..f0185a5 100644 --- a/drivers/freshness/test/test_freshness.c +++ b/drivers/freshness/test/test_freshness.c @@ -44,6 +44,9 @@ static void test_decide(void) { EXPECT(warden_fresh_decide(FRESH_UNKNOWN, false, false, 0, 100) == FRESH_RENDER_UNKNOWN); EXPECT(warden_fresh_decide(FRESH_UNKNOWN, true, false, 200, 100) == FRESH_RENDER_UNKNOWN); EXPECT(warden_fresh_decide(FRESH_UNKNOWN, true, false, 50, 100) == FRESH_RENDER_NOCHANGE); + /* boundary: age == max_stale is NOT stale (guards a `>`->`>=` regression that + * MC/DC alone would not catch — both outcomes are already covered above). */ + EXPECT(warden_fresh_decide(FRESH_UNKNOWN, true, false, 100, 100) == FRESH_RENDER_NOCHANGE); } /* --- 2. bind: !produce, !render, valid, and table-full (FRESH_MAX=2) --- */ @@ -121,10 +124,18 @@ static void test_invalidate(void) { EXPECT(g_render_calls == 1); } -/* --- 6. min_budget: best==0 first, then max_stale300 */ warden_fresh_bind((void*)1, fake_produce, NULL, 100, "s", fake_render, (void*)9); /* 100<300 -> 100 */ EXPECT(warden_fresh_min_budget_ms() == 100); @@ -160,6 +171,22 @@ static void test_false_arms(void) { EXPECT(g_render_calls == 0); } +/* --- 8. clock wraparound: a uint32 ms counter wraps every ~49.7 days, so a tick + * can arrive with now_ms < last_ok_ms. The unsigned `now - last_ok_ms` then + * underflows to a huge age; the contract must fail SAFE to UNKNOWN, never + * assert the last value as if still fresh. --- */ +static void test_wraparound(void) { + warden_fresh_reset(); + warden_fresh_bind((void*)1, fake_produce, NULL, 100, "s", fake_render, (void*)9); + set_produce(FRESH_OK, "9"); reset_render(); + warden_fresh_tick(1000); /* last_ok_ms = 1000, ever_ok */ + EXPECT(g_render_calls == 1 && g_render_what == FRESH_RENDER_VALUE); + /* now wraps behind last_ok_ms: age underflows -> treated as stale -> UNKNOWN */ + set_produce(FRESH_UNKNOWN, NULL); reset_render(); + warden_fresh_tick(10); + EXPECT(g_render_calls == 1 && g_render_what == FRESH_RENDER_UNKNOWN); +} + int main(void) { test_decide(); test_bind(); @@ -168,6 +195,7 @@ int main(void) { test_invalidate(); test_min_budget(); test_false_arms(); + test_wraparound(); fprintf(stderr, "%d checks, %d failures\n", g_checks, g_fail); return g_fail ? 1 : 0; } diff --git a/drivers/relays/test/Makefile b/drivers/relays/test/Makefile index 58ed59e..5d04d9f 100644 --- a/drivers/relays/test/Makefile +++ b/drivers/relays/test/Makefile @@ -32,7 +32,7 @@ check: $(BUILD)/test @echo "== MC/DC (condition) coverage of relays.c ==" @$(GCOV) --conditions --branch-probabilities -o $(BUILD) ../relays.c >$(BUILD)/gcov.log 2>&1 || true @mv -f *.gcov $(BUILD)/ 2>/dev/null || true - @bash enforce-mcdc.sh $(BUILD)/gcov.log $(BUILD)/relays.c.gcov $(BUILD)/test.rc + @bash ../../enforce-mcdc.sh $(BUILD)/gcov.log $(BUILD)/relays.c.gcov $(BUILD)/test.rc report: check @grep -nE "condition.*not covered|conditions covered" $(BUILD)/relays.c.gcov || true diff --git a/drivers/relays/test/test_relays.c b/drivers/relays/test/test_relays.c index 1ef90dd..c399df4 100644 --- a/drivers/relays/test/test_relays.c +++ b/drivers/relays/test/test_relays.c @@ -211,6 +211,11 @@ static void integration_tests(void) int main(void) { + /* Hermeticity: layer 1's fake-io tests exercise gpio_root()'s `r && *r` NULL + * arm (unset env). Clear any inherited WARDEN_GPIO_ROOT so that outcome is + * deterministic instead of depending on the ambient shell (an exported value + * would otherwise drop condition coverage and spuriously fail the gate). */ + unsetenv("WARDEN_GPIO_ROOT"); unit_tests(); integration_tests(); fprintf(stderr, "%d checks, %d failures\n", g_checks, g_fail); diff --git a/sim/benches/sim_bench.rs b/sim/benches/sim_bench.rs index 1175086..c4bf66a 100644 --- a/sim/benches/sim_bench.rs +++ b/sim/benches/sim_bench.rs @@ -59,8 +59,17 @@ fn main() { // RGA: one improcess dispatch record (+ clear, so the log stays bounded). { let mut r = RgaSim::new(); - let sf = Surface { width: 720, height: 720, format: 0 }; - let rc = Rect { x: 0, y: 0, w: 720, h: 720 }; + let sf = Surface { + width: 720, + height: 720, + format: 0, + }; + let rc = Rect { + x: 0, + y: 0, + w: 720, + h: 720, + }; bench("rga_improcess", N, || { let _ = r.improcess(sf, sf, rc, rc); r.clear(); diff --git a/sim/src/cru.rs b/sim/src/cru.rs index 5251e78..38f2760 100644 --- a/sim/src/cru.rs +++ b/sim/src/cru.rs @@ -203,7 +203,11 @@ mod tests { cru.poll(0); bus.poke32(DW_WDT_CRR, DW_WDT_PET); // pet at t=5 -> new deadline 13 assert_eq!(cru.poll(5), None); - assert_eq!(cru.poll(8), None, "petted: original deadline no longer applies"); + assert_eq!( + cru.poll(8), + None, + "petted: original deadline no longer applies" + ); assert_eq!(cru.poll(13), Some(ResetCause::Watchdog)); } diff --git a/sim/src/hpmcu.rs b/sim/src/hpmcu.rs index 7c8c933..2146000 100644 --- a/sim/src/hpmcu.rs +++ b/sim/src/hpmcu.rs @@ -92,7 +92,8 @@ impl HpmcuSim { return; } self.polls = self.polls.wrapping_add(1); - self.bus.poke32(self.base + OFF_CYC, (now & 0xffff_ffff) as u32); + self.bus + .poke32(self.base + OFF_CYC, (now & 0xffff_ffff) as u32); let magic = self.bus.peek32(self.base + OFF_MAGIC); let counter = self.bus.peek32(self.base + OFF_COUNTER); let poll_lo = self.polls & 0xffff; @@ -100,7 +101,8 @@ impl HpmcuSim { if magic == MAGIC_DISARM { // Deliberate stand-down: resume the moment Linux re-arms; the grace // clock restarts so a disarm-then-silence never fires. - self.bus.poke32(self.base + OFF_STATE, STATE_DISARMED | poll_lo); + self.bus + .poke32(self.base + OFF_STATE, STATE_DISARMED | poll_lo); self.t0 = now; self.last_change = now; } else if magic == MAGIC_ARMED { @@ -110,7 +112,8 @@ impl HpmcuSim { self.bus.poke32(self.base + OFF_SEEN, counter); self.last_change = now; } - self.bus.poke32(self.base + OFF_STATE, STATE_ARMED | poll_lo); + self.bus + .poke32(self.base + OFF_STATE, STATE_ARMED | poll_lo); if now.saturating_sub(self.last_change) > HEARTBEAT_TIMEOUT_S { self.fire(); } @@ -186,7 +189,10 @@ mod tests { // Heartbeat stops (counter frozen). Fires just past 90s. for now in (5..=90).step_by(5) { m.tick(now); - assert!(!m.fired(), "must not fire within the heartbeat window (@{now}s)"); + assert!( + !m.fired(), + "must not fire within the heartbeat window (@{now}s)" + ); } m.tick(91); assert!(m.fired(), "must fire just past the 90s heartbeat timeout"); @@ -225,7 +231,10 @@ mod tests { counter += 1; arm_beat(&bus, counter); m.tick(now); - assert!(!m.fired(), "flared armed before boot-grace: must never fire (@{now}s)"); + assert!( + !m.fired(), + "flared armed before boot-grace: must never fire (@{now}s)" + ); } assert_eq!(m.state(), STATE_ARMED); } @@ -252,6 +261,9 @@ mod tests { assert!(!m.fired(), "within the heartbeat window (@{now}s)"); } m.tick(695); // 695 - 600 = 95 > 90 - assert!(m.fired(), "flared dead > heartbeat timeout: MCU fires the reset"); + assert!( + m.fired(), + "flared dead > heartbeat timeout: MCU fires the reset" + ); } } diff --git a/sim/src/modbus.rs b/sim/src/modbus.rs index 7112c44..dcd7750 100644 --- a/sim/src/modbus.rs +++ b/sim/src/modbus.rs @@ -28,7 +28,11 @@ pub fn crc16(bytes: &[u8]) -> u16 { for &b in bytes { crc ^= b as u16; for _ in 0..8 { - crc = if crc & 1 != 0 { (crc >> 1) ^ 0xA001 } else { crc >> 1 }; + crc = if crc & 1 != 0 { + (crc >> 1) ^ 0xA001 + } else { + crc >> 1 + }; } } crc @@ -202,7 +206,11 @@ impl ModbusSlave { if count == 0 || count > 2000 { return Err(exc::ILLEGAL_DATA_VALUE); } - let bank = if discrete { &self.discrete } else { &self.coils }; + let bank = if discrete { + &self.discrete + } else { + &self.coils + }; if start + count > bank.len() { return Err(exc::ILLEGAL_DATA_ADDRESS); } @@ -272,7 +280,10 @@ impl ModbusSlave { let start = u16::from_be_bytes([pdu[0], pdu[1]]) as usize; let count = u16::from_be_bytes([pdu[2], pdu[3]]) as usize; let bytecount = pdu[4] as usize; - if count == 0 || count > 1968 || bytecount != count.div_ceil(8) || pdu.len() != 5 + bytecount + if count == 0 + || count > 1968 + || bytecount != count.div_ceil(8) + || pdu.len() != 5 + bytecount { return Err(exc::ILLEGAL_DATA_VALUE); } @@ -361,7 +372,8 @@ mod tests { fn coils_write_then_read() { let mut s = ModbusSlave::new(1, 0, 16); // FC05 write single coil 3 = ON - s.handle_frame(&request(1, &[0x05, 0, 3, 0xFF, 0x00])).unwrap(); + s.handle_frame(&request(1, &[0x05, 0, 3, 0xFF, 0x00])) + .unwrap(); assert!(s.coil(3)); // FC01 read coils 0..8 -> bit 3 set => byte 0x08 let resp = s.handle_frame(&request(1, &[0x01, 0, 0, 0, 8])).unwrap(); diff --git a/sim/src/npu.rs b/sim/src/npu.rs index be235f5..e5bb8dd 100644 --- a/sim/src/npu.rs +++ b/sim/src/npu.rs @@ -20,12 +20,18 @@ pub struct NpuSim { impl NpuSim { /// A present NPU reporting 0% load. pub fn new() -> Self { - Self { present: true, load: 0 } + Self { + present: true, + load: 0, + } } /// An absent NPU (rknpu.ko not loaded): `/proc/rknpu/load` does not exist. pub fn absent() -> Self { - Self { present: false, load: 0 } + Self { + present: false, + load: 0, + } } /// Set the reported load, clamped to 0..=100. @@ -105,6 +111,9 @@ mod tests { #[test] fn default_is_present_idle() { - assert_eq!(NpuSim::default().proc_load(), Some("NPU load: 0%\n".to_string())); + assert_eq!( + NpuSim::default().proc_load(), + Some("NPU load: 0%\n".to_string()) + ); } } diff --git a/sim/src/rga.rs b/sim/src/rga.rs index 76f211d..83b9452 100644 --- a/sim/src/rga.rs +++ b/sim/src/rga.rs @@ -52,7 +52,10 @@ pub struct RgaSim { impl RgaSim { /// A working RGA whose `improcess` succeeds. pub fn new() -> Self { - Self { status: ImStatus::Success, blits: Vec::new() } + Self { + status: ImStatus::Success, + blits: Vec::new(), + } } /// Program the status `improcess` returns (set `Failed` to drive the driver's @@ -63,7 +66,12 @@ impl RgaSim { /// Model one `improcess()` call: record it and return the programmed status. pub fn improcess(&mut self, src: Surface, dst: Surface, srect: Rect, drect: Rect) -> ImStatus { - self.blits.push(Blit { src, dst, srect, drect }); + self.blits.push(Blit { + src, + dst, + srect, + drect, + }); self.status } @@ -99,7 +107,11 @@ mod tests { use super::*; fn surf(w: i32, h: i32) -> Surface { - Surface { width: w, height: h, format: 0 } + Surface { + width: w, + height: h, + format: 0, + } } fn rect(w: i32, h: i32) -> Rect { Rect { x: 0, y: 0, w, h } @@ -108,7 +120,12 @@ mod tests { #[test] fn records_a_successful_blit() { let mut r = RgaSim::new(); - let st = r.improcess(surf(720, 720), surf(360, 360), rect(720, 720), rect(360, 360)); + let st = r.improcess( + surf(720, 720), + surf(360, 360), + rect(720, 720), + rect(360, 360), + ); assert_eq!(st, ImStatus::Success); assert_eq!(r.count(), 1); let b = r.last().unwrap(); @@ -120,8 +137,10 @@ mod tests { fn failed_status_drives_fallback() { let mut r = RgaSim::new(); r.set_status(ImStatus::Failed); - assert_eq!(r.improcess(surf(10, 10), surf(10, 10), rect(10, 10), rect(10, 10)), - ImStatus::Failed); + assert_eq!( + r.improcess(surf(10, 10), surf(10, 10), rect(10, 10), rect(10, 10)), + ImStatus::Failed + ); // the op is still recorded — the driver dispatched it, then fell back. assert_eq!(r.count(), 1); } diff --git a/tools/config-lint/src/lib.rs b/tools/config-lint/src/lib.rs index 799d197..fadefe1 100644 --- a/tools/config-lint/src/lib.rs +++ b/tools/config-lint/src/lib.rs @@ -38,6 +38,13 @@ pub struct Finding { pub msg: String, } +/// A byte that can appear inside a devicetree node/property identifier — used to +/// require a token boundary before a `reg` property, so substrings like `reg-names` +/// or a `region-*` node label are not matched as the `reg` property itself. +fn is_ident_byte(b: u8) -> bool { + b.is_ascii_alphanumeric() || b == b'_' || b == b'-' +} + fn parse_addr(s: &str) -> Option { let s = s.trim(); if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) { @@ -47,10 +54,33 @@ fn parse_addr(s: &str) -> Option { } } -/// Does a loader entry name a coprocessor/MCU firmware (whose LOAD_ADDR matters)? -fn is_mcu_loader(name: &str) -> bool { +/// Loaders that are part of the normal RK boot chain — DDR init, SPL/loader, the +/// secure monitor, U-Boot — and are NOT boot-loaded coprocessor firmware, so their +/// LOAD_ADDR (if any) is the vendor boot flow, not a DRAM carve-out that must be +/// reserved. Anything NOT on this allowlist that declares a LOAD_ADDR is treated as +/// a coprocessor load and checked (**fail closed**): a future MCU named +/// "Rtos"/"Bl32"/"M0" must not slip through an MCU-*name* allowlist the way the +/// original `hpmcu`/`mcu`/`amp` substring test would have — that is exactly the +/// c8a3 0x40000-brick class this tool exists to catch. +fn is_known_safe_loader(name: &str) -> bool { let n = name.to_ascii_lowercase(); - n.contains("hpmcu") || n.contains("mcu") || n.contains("amp") + const SAFE: &[&str] = &[ + "flashdata", + "flashboot", + "ddr", + "usbplug", + "spl", + "uboot", + "u-boot", + "loader", + "trust", + "tee", + "atf", + "bl31", + "fsbl", + "idblock", + ]; + SAFE.iter().any(|s| n.contains(s)) } /// Extract MCU firmware loads from an rkbin loader `.ini`: for each `LOADERn=` @@ -72,7 +102,9 @@ pub fn parse_ini_mcu_loads(ini: &str) -> Vec { section = sec.trim().to_string(); continue; } - let Some((k, v)) = line.split_once('=') else { continue }; + let Some((k, v)) = line.split_once('=') else { + continue; + }; let (k, v) = (k.trim(), v.trim()); if section == "LOADER_OPTION" && k.to_ascii_uppercase().starts_with("LOADER") { loaders.push((k.to_ascii_uppercase(), v.to_string())); @@ -85,12 +117,19 @@ pub fn parse_ini_mcu_loads(ini: &str) -> Vec { let mut out = Vec::new(); for (loader, name) in loaders { - if !is_mcu_loader(&name) { + // Fail closed: check every loader that isn't a known-safe boot component. + // A benign component without a LOAD_ADDR produces nothing (the `get` below + // misses), so this only ever surfaces loads that actually target DRAM. + if is_known_safe_loader(&name) { continue; } // LOADER2 -> [LOADER2_PARAM] if let Some(&addr) = load_addrs.get(&format!("{loader}_PARAM")) { - out.push(McuLoad { loader, name, load_addr: addr }); + out.push(McuLoad { + loader, + name, + load_addr: addr, + }); } } out @@ -129,15 +168,40 @@ pub fn parse_reserved_ranges(dt: &str) -> Vec { } j += 1; } - // scan reg = < a b > inside [start, j) + // scan `reg = ` PROPERTY tokens inside [start, j). Match `reg` as + // a whole token — identifier boundary before it, `=` after optional space — + // so a `reg-names` property or a `region-*@…` node label (both contain the + // substring "reg") is not misparsed into a bogus range. let block = &dt[start..j.min(dt.len())]; - for reg in block.split("reg").skip(1) { - let Some(lt) = reg.find('<') else { continue }; - let Some(gt) = reg[lt..].find('>') else { continue }; - let nums: Vec<&str> = reg[lt + 1..lt + gt].split_whitespace().collect(); + let bb = block.as_bytes(); + let mut k = 0; + while let Some(rel) = block[k..].find("reg") { + let p = k + rel; + let end = p + 3; + k = end; // forward progress regardless of whether this "reg" matches + let before_ok = p == 0 || !is_ident_byte(bb[p - 1]); + let mut a = end; // first non-space byte after "reg" must be '=' + while a < bb.len() && matches!(bb[a], b' ' | b'\t' | b'\r' | b'\n') { + a += 1; + } + if !(before_ok && a < bb.len() && bb[a] == b'=') { + continue; + } + let Some(lt_rel) = block[a..].find('<') else { + continue; + }; + let lt = a + lt_rel; + let Some(gt_rel) = block[lt..].find('>') else { + continue; + }; + let gt = lt + gt_rel; + let nums: Vec<&str> = block[lt + 1..gt].split_whitespace().collect(); if nums.len() >= 2 { - if let (Some(a), Some(s)) = (parse_addr(nums[0]), parse_addr(nums[1])) { - out.push(Range { start: a, size: s }); + if let (Some(addr), Some(sz)) = (parse_addr(nums[0]), parse_addr(nums[1])) { + out.push(Range { + start: addr, + size: sz, + }); } } } @@ -245,4 +309,55 @@ FLAG=0x10007 let loads = parse_ini_mcu_loads(NONTB_INI); assert!(check(&loads, &parse_reserved_ranges(DT_NO_RTOS)).is_empty()); } + + // A reserving node whose LABEL and a sibling property both contain "reg", with + // an unrelated cell property (`interrupts`) right before the real `reg`. + const DT_REG_SUBSTRING: &str = r#" + reserved-memory { + #address-cells = <1>; + #size-cells = <1>; + ranges; + region-a@40000 { interrupts = <0 43 4>; reg-names = "ctrl"; reg = <0x40000 0x3c000>; no-map; }; + }; + "#; + + /// Regression (correctness review): `reg` must match as a whole property token, + /// not a bare substring — the `region-*` label, `reg-names`, and the + /// `interrupts` cells must NOT be misparsed into extra ranges. + #[test] + fn reg_token_not_substring() { + let r = parse_reserved_ranges(DT_REG_SUBSTRING); + assert_eq!(r.len(), 1, "only the real `reg` property is a range: {r:?}"); + assert_eq!(r[0].start, 0x40000); + assert_eq!(r[0].size, 0x3c000); + } + + // A future coprocessor loader named nothing like hpmcu/mcu/amp, at an + // unreserved DRAM address — the exact case an MCU-name allowlist would miss. + const UNKNOWN_MCU_INI: &str = r#" +[LOADER_OPTION] +NUM=2 +LOADER1=FlashData +LOADER2=Rtos +FlashData=bin/rv11/rv1106_ddr.bin +Rtos=bin/rv11/rv1106_rtos.bin +[LOADER2_PARAM] +LOAD_ADDR=0x40000 +"#; + + /// Regression (correctness review): fail closed. A loader that isn't a known-safe + /// boot component and declares a LOAD_ADDR is treated as a coprocessor load and + /// checked, even though its name matches no MCU keyword. + #[test] + fn unknown_coprocessor_name_is_flagged() { + let loads = parse_ini_mcu_loads(UNKNOWN_MCU_INI); + assert_eq!( + loads.len(), + 1, + "the unknown loader must be checked: {loads:?}" + ); + assert_eq!(loads[0].load_addr, 0x40000); + // and it's caught when no reserved-memory node covers it: + assert_eq!(check(&loads, &parse_reserved_ranges(DT_NO_RTOS)).len(), 1); + } }