review: fix review findings (correctness, hardening, doc-accuracy)
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
75ff711e43
commit
4ee4dfcf88
@@ -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
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
+23
-7
@@ -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"
|
||||
|
||||
+45
-29
@@ -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).
|
||||
|
||||
+6
-3
@@ -17,8 +17,11 @@ Self-contained logic with a clean seam, measured to **100% MC/DC** (gcc-14
|
||||
|
||||
**Adding a Tier-1 driver:** copy `<name>.{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 <gcov.log> build/<name>.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
|
||||
|
||||
@@ -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 <gcov.log> <build>/<name>.c.gcov <test.rc>
|
||||
# Fails unless every unit check passed AND <name>.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 = <name>.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 <name>.c from gcov stdout. Match the exact file so the test
|
||||
# harness translation unit (test_<name>.c, which also contains "<name>.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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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_stale<best true and false --- */
|
||||
/* --- 6. min_budget: unseen first, then max_stale<best true and false --- */
|
||||
static void test_min_budget(void) {
|
||||
warden_fresh_reset();
|
||||
EXPECT(warden_fresh_min_budget_ms() == 0); /* nothing bound */
|
||||
/* regression (correctness review): a zero-tolerance binding (max_stale=0) must
|
||||
* win the minimum, not be mistaken for the "nothing scanned" sentinel and
|
||||
* widened to a looser neighbour's budget. */
|
||||
warden_fresh_bind((void*)1, fake_produce, NULL, 0, "s", fake_render, (void*)9);
|
||||
warden_fresh_bind((void*)1, fake_produce, NULL, 300, "s", fake_render, (void*)9);
|
||||
EXPECT(warden_fresh_min_budget_ms() == 0);
|
||||
warden_fresh_reset();
|
||||
EXPECT(warden_fresh_min_budget_ms() == 0); /* nothing bound (post-reset) */
|
||||
warden_fresh_bind((void*)1, fake_produce, NULL, 300, "s", fake_render, (void*)9); /* best=0->300 */
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
+5
-1
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
+18
-6
@@ -92,7 +92,8 @@ impl<B: MemBus> HpmcuSim<B> {
|
||||
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<B: MemBus> HpmcuSim<B> {
|
||||
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<B: MemBus> HpmcuSim<B> {
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+16
-4
@@ -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();
|
||||
|
||||
+12
-3
@@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+25
-6
@@ -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);
|
||||
}
|
||||
|
||||
+128
-13
@@ -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<u64> {
|
||||
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<u64> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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=<name>`
|
||||
@@ -72,7 +102,9 @@ pub fn parse_ini_mcu_loads(ini: &str) -> Vec<McuLoad> {
|
||||
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<McuLoad> {
|
||||
|
||||
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<Range> {
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
// scan reg = < a b > inside [start, j)
|
||||
// scan `reg = <addr size>` 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user