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 }}
|
group: ci-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
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:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -34,7 +39,7 @@ jobs:
|
|||||||
done
|
done
|
||||||
- name: coverage (cargo-llvm-cov on sim)
|
- name: coverage (cargo-llvm-cov on sim)
|
||||||
run: rustup component add llvm-tools-preview
|
run: rustup component add llvm-tools-preview
|
||||||
- uses: taiki-e/install-action@v2
|
- uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc # v2.86.7
|
||||||
with:
|
with:
|
||||||
tool: cargo-llvm-cov
|
tool: cargo-llvm-cov
|
||||||
- name: run coverage
|
- name: run coverage
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ target/
|
|||||||
*.swp
|
*.swp
|
||||||
*~
|
*~
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# local tooling state (code-review harness cross-session memory, etc.)
|
||||||
|
.claude/
|
||||||
|
|
||||||
# driver MC/DC harness build dirs
|
# driver MC/DC harness build dirs
|
||||||
**/test/build/
|
**/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
|
the rest of the firmware: tested, benchmarked, reproducible, and honest about
|
||||||
what runs on real silicon versus what we simulate.
|
what runs on real silicon versus what we simulate.
|
||||||
|
|
||||||
> Status: **bootstrapping.** This repo is being stood up incrementally; today it
|
> Status: **bringup.** The hardware **simulator** and its tests, the RV1106 kernel
|
||||||
> hosts the hardware **simulator** and its tests. The kernel forward-port and the
|
> forward-port as a reviewable `patches/` series, the hermetic kernel build, and two
|
||||||
> hermetic image build move in as each is proven. Until then, flare-edge still
|
> Tier-1 drivers at 100% MC/DC are all in and CI-green on the `bringup` branch. What
|
||||||
> builds firmware from the vendored SDK + `sdk-patches/`; nothing here is on the
|
> remains before this is on the production build path: installing the self-hosted
|
||||||
> production build path yet.
|
> 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
|
## Why a new SDK
|
||||||
|
|
||||||
@@ -87,13 +89,15 @@ than duplicate each other.
|
|||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
sim/ the hardware simulator (Rust): mailbox/devmem model, HPMCU, RGA, NPU.
|
sim/ the hardware simulator (Rust): membus/devmem, HPMCU, CRU, Modbus, RGA, NPU.
|
||||||
drivers/ our own hardened drivers + their seams (as they migrate in).
|
drivers/ our own hardened drivers + their seams (relays, freshness; more migrate in).
|
||||||
patches/ the vendor-SDK delta (mirrors flare-edge/sdk-patches until it moves here).
|
patches/ the RV1106 kernel forward-port delta onto pristine linux-6.18.46 (subsystem-split).
|
||||||
build/ the hermetic image-build wrapper (kernel → rootfs → image), incremental.
|
kernel/ forward-port docs + provenance (rv1106-enablement/, PROVENANCE.md).
|
||||||
ci/ CI: patches-still-apply, host tests, coverage, benchmarks.
|
build/ the hermetic kernel build (fetch pristine → apply patches → zImage + dtb).
|
||||||
docs/ architecture + ADRs (decisions/).
|
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).
|
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
|
## Principles
|
||||||
|
|||||||
+19
-3
@@ -23,9 +23,20 @@ REPO="$(cd "$HERE/.." && pwd)"
|
|||||||
PATCHES="$REPO/patches"
|
PATCHES="$REPO/patches"
|
||||||
SHA_FILE="$HERE/linux-$KVER.tar.xz.sha256"
|
SHA_FILE="$HERE/linux-$KVER.tar.xz.sha256"
|
||||||
JOBS="${JOBS:-$(nproc)}"
|
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"
|
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' "$*"; }
|
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; }
|
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"
|
log "downloading $URL"
|
||||||
curl -fSL "$URL" -o "$TB"
|
curl -fSL "$URL" -o "$TB"
|
||||||
fi
|
fi
|
||||||
if [ -f "$SHA_FILE" ]; then
|
# 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")"
|
want="$(cat "$SHA_FILE")"
|
||||||
got="$(sha256sum "$TB" | awk '{print $1}')"
|
got="$(sha256sum "$TB" | awk '{print $1}')"
|
||||||
[ "$want" = "$got" ] || { echo "tarball sha256 mismatch: want $want got $got" >&2; exit 1; }
|
[ "$want" = "$got" ] || { echo "tarball sha256 mismatch: want $want got $got" >&2; exit 1; }
|
||||||
log "tarball sha256 verified"
|
log "tarball sha256 verified"
|
||||||
fi
|
|
||||||
|
|
||||||
# 2. extract pristine
|
# 2. extract pristine
|
||||||
SRC="$WORK/linux-$KVER"
|
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`.
|
- **`membus` — register/SRAM bus.** Done. `MemBus` trait + `SimBus`.
|
||||||
- **`hpmcu` — the RISC-V watchdog coprocessor.** Done. Faithful port of
|
- **`hpmcu` — the RISC-V watchdog coprocessor.** Done. Faithful port of
|
||||||
`hpmcu/watchdog/main.c`'s state machine (boot-grace, heartbeat-timeout, disarm,
|
`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
|
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
|
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 —
|
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
|
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
|
hardened to MC/DC against realistic device behaviour with no serial hardware. MEI
|
||||||
(0x2B/0x0E) identification is the documented follow-up.
|
(0x2B/0x0E) identification is the documented follow-up.
|
||||||
- **Next:** an **NPU** load model behind the path seam (deferred — no NPU feature
|
- **`npu` — NPU load model.** Done. `NpuSim` models `/proc/rknpu/load` (the exact
|
||||||
ships soon); a **GPIO/relay** sysfs model (largely covered by the `WARDEN_GPIO_ROOT`
|
"NPU load: N%" text the sysmon reads) behind the path seam, so the load-readout UI
|
||||||
seam in flare-edge's `tests/relays-mcdc/`); an **RGA** recording fake.
|
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
|
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
|
`#[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
|
"100% MC/DC on 100% of drivers" is infeasible literally: ~97% of driver LOC is
|
||||||
vendor blobs (AIC8800 wifi = 88.5K lines). Tiered target:
|
vendor blobs (AIC8800 wifi = 88.5K lines). Tiered target:
|
||||||
|
|
||||||
- **Tier 1 — our own hardware code → real MC/DC.** modbus master (`modbus_engine.c`),
|
- **Tier 1 — our own hardware code → real MC/DC.** Method: the proven `tests/uboot-ab`
|
||||||
relays (`relays.c`), the RGA wrapper's dispatch, the HPMCU supervisor
|
pattern — extract the unit behind a small injectable seam, mock its world, build
|
||||||
(`hpmcu.rs`), the devmem reset ladder. Method: the proven `tests/uboot-ab`
|
`-fcondition-coverage`, enforce with the shared `drivers/enforce-mcdc.sh` (gcc-14
|
||||||
pattern — extract the unit, mock its world, build `-fcondition-coverage`, enforce
|
`gcov --conditions`) in the CI `mcdc` job. **Done here now:** `relays.c` (40/40
|
||||||
with `enforce-mcdc.sh` (gcc-14 `gcov --conditions`). **Gap the survey found: there
|
conditions) and `freshness.c` (66/66), both at 100% MC/DC and CI-enforced. **Migrate
|
||||||
is no C-side coverage in CI at all today** — only flared line-coverage and the one
|
in next:** the modbus master (`modbus_engine.c`) and the RGA wrapper's dispatch —
|
||||||
uboot-ab MC/DC file. Standing up an MC/DC harness for the first C driver
|
their hardware ends are already modelled and tested in `sim/` (`modbus`, `rga`); the
|
||||||
(`relays.c` — small, safety-relevant) is the first driver-hardening deliverable.
|
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 2 — near-mainline small drivers → branch coverage + fault injection.**
|
||||||
- **Tier 3 — vendor blobs (AIC8800, MPP/ISP/RGA libs) → fault-injection hardening
|
- **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
|
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
|
added. **Next** target-config checks: partition-table-vs-image-size and
|
||||||
vermagic-vs-kernel.
|
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
|
A self-built **Linux 6.18.46**, forward-ported directly from the vendor 5.10.160 tree
|
||||||
exact board DT), not mainline (no DT/clk/display/RGA/NPU/flash-boot merged). It is
|
onto our Buildroot LTS/uClibc base — **not** the plan44/OpenWrt 6.6 fork this section
|
||||||
a diff-and-borrow forward-port onto our Buildroot/uClibc base, not a swap-in
|
originally reached for. **ADR-0001 records why that was superseded:** plan44 drops
|
||||||
(plan44 drops Buildroot for OpenWrt/musl and ships no AIC8800 kmod). The dominant
|
Buildroot for OpenWrt/musl and ships no AIC8800 kmod, so it was a swap-out, not a
|
||||||
risk is the struct-ABI break (the VLAN saga) — mitigated by shipping any kernel
|
forward-port. Mainline alone was not viable either (no DT/clk/display/RGA/NPU/
|
||||||
move as one matched boot+oem image, never a partial reflash. This phase starts
|
flash-boot upstream for RV1106); the port reuses the already-in-mainline rv1126
|
||||||
after the sim + driver-hardening foundation, since those are how we'll regression
|
register data where it matches and carries our deltas as the reviewable `patches/`
|
||||||
the port.
|
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
|
## 7. Order of work
|
||||||
|
|
||||||
1. **Simulator core** — `membus` + `hpmcu` (done); reset-ladder + path-seam
|
1. **Simulator core** — `membus`, `hpmcu`, the `cru` reset ladder, `modbus`, plus the
|
||||||
scaffolding next.
|
`rga`/`npu` models. **Done.**
|
||||||
2. **First C-driver MC/DC harness** — `relays.c`, establishing the C coverage gate.
|
2. **C-driver MC/DC harnesses** — `relays.c` and `freshness.c` at 100% MC/DC, CI-gated
|
||||||
3. **flared devmem/hpmcu seam + tests** (firmware-side trait; unify with `sim/`
|
via the shared `drivers/enforce-mcdc.sh`. **Done** (the first C coverage gate).
|
||||||
when the repo has a remote).
|
3. **flared devmem/hpmcu seam + tests** — firmware-side trait, unified with `sim/`
|
||||||
4. **Config-lint CI gates** (§5) — the brick-class of bug.
|
once flare-edge consumes warden-sdk (a separate, [maintainer]-gated step). **Pending.**
|
||||||
5. **Hermetic image build** wrapper moves in.
|
4. **Config-lint CI gates** (§5) — the brick-class of bug. **Done.**
|
||||||
6. **Kernel 6.6 forward-port** (§6).
|
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
|
**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
|
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
|
branches + a real backend over a scratch tree for the plumbing). Reuse the shared
|
||||||
`enforce-mcdc.sh`. The CI job picks up any `drivers/*/test/Makefile` automatically.
|
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
|
## 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 |
|
| `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 |
|
| `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
|
**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
|
`warden_rga.c` pull in shared UI headers (`platform.h`, `settings.h`, `lv_*`) and
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Gate: fail unless every unit check passed AND relays.c reached 100% MC/DC
|
# Shared Tier-1 MC/DC gate for every driver harness. Each driver's test/Makefile
|
||||||
# (condition) coverage with no uncovered executable lines.
|
# invokes it as:
|
||||||
# $1 = gcov stdout log $2 = relays.c.gcov $3 = test exit-code file
|
# 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
|
set -uo pipefail
|
||||||
LOG="$1"; GCOV="$2"; RCFILE="$3"
|
LOG="$1"; GCOV="$2"; RCFILE="$3"
|
||||||
|
base="$(basename "$GCOV")"; base="${base%.gcov}" # e.g. freshness.c
|
||||||
|
name="${base%.c}" # e.g. freshness
|
||||||
rc=0
|
rc=0
|
||||||
|
|
||||||
testrc="$(cat "$RCFILE" 2>/dev/null || echo 1)"
|
testrc="$(cat "$RCFILE" 2>/dev/null || echo 1)"
|
||||||
@@ -20,9 +27,10 @@ fi
|
|||||||
notcov="$(grep -nE "condition[s]? .*not covered" "$GCOV" || true)"
|
notcov="$(grep -nE "condition[s]? .*not covered" "$GCOV" || true)"
|
||||||
uncov_lines="$(grep -nE "^ +#####:" "$GCOV" || true)"
|
uncov_lines="$(grep -nE "^ +#####:" "$GCOV" || true)"
|
||||||
|
|
||||||
# Summary lines for relays.c from gcov stdout. Match the exact file so
|
# Summary lines for <name>.c from gcov stdout. Match the exact file so the test
|
||||||
# 'test_relays.c' (which also contains "relays.c") is NOT picked up.
|
# harness translation unit (test_<name>.c, which also contains "<name>.c") is NOT
|
||||||
FMATCH="File '([^']*/)?relays[.]c'"
|
# picked up.
|
||||||
|
FMATCH="File '([^']*/)?${name}[.]c'"
|
||||||
cond_line="$(awk -v patt="$FMATCH" '$0 ~ patt {f=1} f&&/Condition outcomes covered:/{print; f=0}' "$LOG")"
|
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")"
|
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 " ${line_line:-Lines executed: (n/a)}"
|
||||||
@@ -34,7 +42,7 @@ if [ -n "$notcov" ]; then
|
|||||||
rc=1
|
rc=1
|
||||||
fi
|
fi
|
||||||
if [ -n "$uncov_lines" ]; then
|
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/^/ /'
|
echo "$uncov_lines" | sed 's/^/ /'
|
||||||
rc=1
|
rc=1
|
||||||
fi
|
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.
|
* See freshness.h for the contract. LVGL binding lives in freshness_lv.c.
|
||||||
*/
|
*/
|
||||||
#include "freshness.h"
|
#include "freshness.h"
|
||||||
@@ -175,11 +175,16 @@ uint32_t warden_fresh_count(void)
|
|||||||
|
|
||||||
uint32_t warden_fresh_min_budget_ms(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;
|
uint32_t best = 0;
|
||||||
|
bool seen = false;
|
||||||
for(uint32_t i = 0; i < FRESH_MAX; i++) {
|
for(uint32_t i = 0; i < FRESH_MAX; i++) {
|
||||||
struct warden_fresh *v = &s_vals[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;
|
best = v->max_stale_ms;
|
||||||
|
seen = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return best;
|
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
|
* 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
|
* *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 =="
|
@echo "== MC/DC (condition) coverage of freshness.c =="
|
||||||
@$(GCOV) --conditions --branch-probabilities -o $(BUILD) ../freshness.c >$(BUILD)/gcov.log 2>&1 || true
|
@$(GCOV) --conditions --branch-probabilities -o $(BUILD) ../freshness.c >$(BUILD)/gcov.log 2>&1 || true
|
||||||
@mv -f *.gcov $(BUILD)/ 2>/dev/null || 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
|
report: check
|
||||||
@grep -nE "condition.*not covered|conditions covered" $(BUILD)/freshness.c.gcov || true
|
@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, 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, 200, 100) == FRESH_RENDER_UNKNOWN);
|
||||||
EXPECT(warden_fresh_decide(FRESH_UNKNOWN, true, false, 50, 100) == FRESH_RENDER_NOCHANGE);
|
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) --- */
|
/* --- 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);
|
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) {
|
static void test_min_budget(void) {
|
||||||
warden_fresh_reset();
|
warden_fresh_reset();
|
||||||
EXPECT(warden_fresh_min_budget_ms() == 0); /* nothing bound */
|
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, 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 */
|
warden_fresh_bind((void*)1, fake_produce, NULL, 100, "s", fake_render, (void*)9); /* 100<300 -> 100 */
|
||||||
EXPECT(warden_fresh_min_budget_ms() == 100);
|
EXPECT(warden_fresh_min_budget_ms() == 100);
|
||||||
@@ -160,6 +171,22 @@ static void test_false_arms(void) {
|
|||||||
EXPECT(g_render_calls == 0);
|
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) {
|
int main(void) {
|
||||||
test_decide();
|
test_decide();
|
||||||
test_bind();
|
test_bind();
|
||||||
@@ -168,6 +195,7 @@ int main(void) {
|
|||||||
test_invalidate();
|
test_invalidate();
|
||||||
test_min_budget();
|
test_min_budget();
|
||||||
test_false_arms();
|
test_false_arms();
|
||||||
|
test_wraparound();
|
||||||
fprintf(stderr, "%d checks, %d failures\n", g_checks, g_fail);
|
fprintf(stderr, "%d checks, %d failures\n", g_checks, g_fail);
|
||||||
return g_fail ? 1 : 0;
|
return g_fail ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ check: $(BUILD)/test
|
|||||||
@echo "== MC/DC (condition) coverage of relays.c =="
|
@echo "== MC/DC (condition) coverage of relays.c =="
|
||||||
@$(GCOV) --conditions --branch-probabilities -o $(BUILD) ../relays.c >$(BUILD)/gcov.log 2>&1 || true
|
@$(GCOV) --conditions --branch-probabilities -o $(BUILD) ../relays.c >$(BUILD)/gcov.log 2>&1 || true
|
||||||
@mv -f *.gcov $(BUILD)/ 2>/dev/null || 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
|
report: check
|
||||||
@grep -nE "condition.*not covered|conditions covered" $(BUILD)/relays.c.gcov || true
|
@grep -nE "condition.*not covered|conditions covered" $(BUILD)/relays.c.gcov || true
|
||||||
|
|||||||
@@ -211,6 +211,11 @@ static void integration_tests(void)
|
|||||||
|
|
||||||
int main(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();
|
unit_tests();
|
||||||
integration_tests();
|
integration_tests();
|
||||||
fprintf(stderr, "%d checks, %d failures\n", g_checks, g_fail);
|
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).
|
// RGA: one improcess dispatch record (+ clear, so the log stays bounded).
|
||||||
{
|
{
|
||||||
let mut r = RgaSim::new();
|
let mut r = RgaSim::new();
|
||||||
let sf = Surface { width: 720, height: 720, format: 0 };
|
let sf = Surface {
|
||||||
let rc = Rect { x: 0, y: 0, w: 720, h: 720 };
|
width: 720,
|
||||||
|
height: 720,
|
||||||
|
format: 0,
|
||||||
|
};
|
||||||
|
let rc = Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: 720,
|
||||||
|
h: 720,
|
||||||
|
};
|
||||||
bench("rga_improcess", N, || {
|
bench("rga_improcess", N, || {
|
||||||
let _ = r.improcess(sf, sf, rc, rc);
|
let _ = r.improcess(sf, sf, rc, rc);
|
||||||
r.clear();
|
r.clear();
|
||||||
|
|||||||
+5
-1
@@ -203,7 +203,11 @@ mod tests {
|
|||||||
cru.poll(0);
|
cru.poll(0);
|
||||||
bus.poke32(DW_WDT_CRR, DW_WDT_PET); // pet at t=5 -> new deadline 13
|
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(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));
|
assert_eq!(cru.poll(13), Some(ResetCause::Watchdog));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-6
@@ -92,7 +92,8 @@ impl<B: MemBus> HpmcuSim<B> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.polls = self.polls.wrapping_add(1);
|
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 magic = self.bus.peek32(self.base + OFF_MAGIC);
|
||||||
let counter = self.bus.peek32(self.base + OFF_COUNTER);
|
let counter = self.bus.peek32(self.base + OFF_COUNTER);
|
||||||
let poll_lo = self.polls & 0xffff;
|
let poll_lo = self.polls & 0xffff;
|
||||||
@@ -100,7 +101,8 @@ impl<B: MemBus> HpmcuSim<B> {
|
|||||||
if magic == MAGIC_DISARM {
|
if magic == MAGIC_DISARM {
|
||||||
// Deliberate stand-down: resume the moment Linux re-arms; the grace
|
// Deliberate stand-down: resume the moment Linux re-arms; the grace
|
||||||
// clock restarts so a disarm-then-silence never fires.
|
// 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.t0 = now;
|
||||||
self.last_change = now;
|
self.last_change = now;
|
||||||
} else if magic == MAGIC_ARMED {
|
} else if magic == MAGIC_ARMED {
|
||||||
@@ -110,7 +112,8 @@ impl<B: MemBus> HpmcuSim<B> {
|
|||||||
self.bus.poke32(self.base + OFF_SEEN, counter);
|
self.bus.poke32(self.base + OFF_SEEN, counter);
|
||||||
self.last_change = now;
|
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 {
|
if now.saturating_sub(self.last_change) > HEARTBEAT_TIMEOUT_S {
|
||||||
self.fire();
|
self.fire();
|
||||||
}
|
}
|
||||||
@@ -186,7 +189,10 @@ mod tests {
|
|||||||
// Heartbeat stops (counter frozen). Fires just past 90s.
|
// Heartbeat stops (counter frozen). Fires just past 90s.
|
||||||
for now in (5..=90).step_by(5) {
|
for now in (5..=90).step_by(5) {
|
||||||
m.tick(now);
|
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);
|
m.tick(91);
|
||||||
assert!(m.fired(), "must fire just past the 90s heartbeat timeout");
|
assert!(m.fired(), "must fire just past the 90s heartbeat timeout");
|
||||||
@@ -225,7 +231,10 @@ mod tests {
|
|||||||
counter += 1;
|
counter += 1;
|
||||||
arm_beat(&bus, counter);
|
arm_beat(&bus, counter);
|
||||||
m.tick(now);
|
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);
|
assert_eq!(m.state(), STATE_ARMED);
|
||||||
}
|
}
|
||||||
@@ -252,6 +261,9 @@ mod tests {
|
|||||||
assert!(!m.fired(), "within the heartbeat window (@{now}s)");
|
assert!(!m.fired(), "within the heartbeat window (@{now}s)");
|
||||||
}
|
}
|
||||||
m.tick(695); // 695 - 600 = 95 > 90
|
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 {
|
for &b in bytes {
|
||||||
crc ^= b as u16;
|
crc ^= b as u16;
|
||||||
for _ in 0..8 {
|
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
|
crc
|
||||||
@@ -202,7 +206,11 @@ impl ModbusSlave {
|
|||||||
if count == 0 || count > 2000 {
|
if count == 0 || count > 2000 {
|
||||||
return Err(exc::ILLEGAL_DATA_VALUE);
|
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() {
|
if start + count > bank.len() {
|
||||||
return Err(exc::ILLEGAL_DATA_ADDRESS);
|
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 start = u16::from_be_bytes([pdu[0], pdu[1]]) as usize;
|
||||||
let count = u16::from_be_bytes([pdu[2], pdu[3]]) as usize;
|
let count = u16::from_be_bytes([pdu[2], pdu[3]]) as usize;
|
||||||
let bytecount = pdu[4] 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);
|
return Err(exc::ILLEGAL_DATA_VALUE);
|
||||||
}
|
}
|
||||||
@@ -361,7 +372,8 @@ mod tests {
|
|||||||
fn coils_write_then_read() {
|
fn coils_write_then_read() {
|
||||||
let mut s = ModbusSlave::new(1, 0, 16);
|
let mut s = ModbusSlave::new(1, 0, 16);
|
||||||
// FC05 write single coil 3 = ON
|
// 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));
|
assert!(s.coil(3));
|
||||||
// FC01 read coils 0..8 -> bit 3 set => byte 0x08
|
// FC01 read coils 0..8 -> bit 3 set => byte 0x08
|
||||||
let resp = s.handle_frame(&request(1, &[0x01, 0, 0, 0, 8])).unwrap();
|
let resp = s.handle_frame(&request(1, &[0x01, 0, 0, 0, 8])).unwrap();
|
||||||
|
|||||||
+12
-3
@@ -20,12 +20,18 @@ pub struct NpuSim {
|
|||||||
impl NpuSim {
|
impl NpuSim {
|
||||||
/// A present NPU reporting 0% load.
|
/// A present NPU reporting 0% load.
|
||||||
pub fn new() -> Self {
|
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.
|
/// An absent NPU (rknpu.ko not loaded): `/proc/rknpu/load` does not exist.
|
||||||
pub fn absent() -> Self {
|
pub fn absent() -> Self {
|
||||||
Self { present: false, load: 0 }
|
Self {
|
||||||
|
present: false,
|
||||||
|
load: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the reported load, clamped to 0..=100.
|
/// Set the reported load, clamped to 0..=100.
|
||||||
@@ -105,6 +111,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_is_present_idle() {
|
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 {
|
impl RgaSim {
|
||||||
/// A working RGA whose `improcess` succeeds.
|
/// A working RGA whose `improcess` succeeds.
|
||||||
pub fn new() -> Self {
|
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
|
/// 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.
|
/// 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 {
|
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
|
self.status
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +107,11 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn surf(w: i32, h: i32) -> Surface {
|
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 {
|
fn rect(w: i32, h: i32) -> Rect {
|
||||||
Rect { x: 0, y: 0, w, h }
|
Rect { x: 0, y: 0, w, h }
|
||||||
@@ -108,7 +120,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn records_a_successful_blit() {
|
fn records_a_successful_blit() {
|
||||||
let mut r = RgaSim::new();
|
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!(st, ImStatus::Success);
|
||||||
assert_eq!(r.count(), 1);
|
assert_eq!(r.count(), 1);
|
||||||
let b = r.last().unwrap();
|
let b = r.last().unwrap();
|
||||||
@@ -120,8 +137,10 @@ mod tests {
|
|||||||
fn failed_status_drives_fallback() {
|
fn failed_status_drives_fallback() {
|
||||||
let mut r = RgaSim::new();
|
let mut r = RgaSim::new();
|
||||||
r.set_status(ImStatus::Failed);
|
r.set_status(ImStatus::Failed);
|
||||||
assert_eq!(r.improcess(surf(10, 10), surf(10, 10), rect(10, 10), rect(10, 10)),
|
assert_eq!(
|
||||||
ImStatus::Failed);
|
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.
|
// the op is still recorded — the driver dispatched it, then fell back.
|
||||||
assert_eq!(r.count(), 1);
|
assert_eq!(r.count(), 1);
|
||||||
}
|
}
|
||||||
|
|||||||
+128
-13
@@ -38,6 +38,13 @@ pub struct Finding {
|
|||||||
pub msg: String,
|
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> {
|
fn parse_addr(s: &str) -> Option<u64> {
|
||||||
let s = s.trim();
|
let s = s.trim();
|
||||||
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
|
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)?
|
/// Loaders that are part of the normal RK boot chain — DDR init, SPL/loader, the
|
||||||
fn is_mcu_loader(name: &str) -> bool {
|
/// 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();
|
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>`
|
/// 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();
|
section = sec.trim().to_string();
|
||||||
continue;
|
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());
|
let (k, v) = (k.trim(), v.trim());
|
||||||
if section == "LOADER_OPTION" && k.to_ascii_uppercase().starts_with("LOADER") {
|
if section == "LOADER_OPTION" && k.to_ascii_uppercase().starts_with("LOADER") {
|
||||||
loaders.push((k.to_ascii_uppercase(), v.to_string()));
|
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();
|
let mut out = Vec::new();
|
||||||
for (loader, name) in loaders {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
// LOADER2 -> [LOADER2_PARAM]
|
// LOADER2 -> [LOADER2_PARAM]
|
||||||
if let Some(&addr) = load_addrs.get(&format!("{loader}_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
|
out
|
||||||
@@ -129,15 +168,40 @@ pub fn parse_reserved_ranges(dt: &str) -> Vec<Range> {
|
|||||||
}
|
}
|
||||||
j += 1;
|
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())];
|
let block = &dt[start..j.min(dt.len())];
|
||||||
for reg in block.split("reg").skip(1) {
|
let bb = block.as_bytes();
|
||||||
let Some(lt) = reg.find('<') else { continue };
|
let mut k = 0;
|
||||||
let Some(gt) = reg[lt..].find('>') else { continue };
|
while let Some(rel) = block[k..].find("reg") {
|
||||||
let nums: Vec<&str> = reg[lt + 1..lt + gt].split_whitespace().collect();
|
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 nums.len() >= 2 {
|
||||||
if let (Some(a), Some(s)) = (parse_addr(nums[0]), parse_addr(nums[1])) {
|
if let (Some(addr), Some(sz)) = (parse_addr(nums[0]), parse_addr(nums[1])) {
|
||||||
out.push(Range { start: a, size: s });
|
out.push(Range {
|
||||||
|
start: addr,
|
||||||
|
size: sz,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,4 +309,55 @@ FLAG=0x10007
|
|||||||
let loads = parse_ini_mcu_loads(NONTB_INI);
|
let loads = parse_ini_mcu_loads(NONTB_INI);
|
||||||
assert!(check(&loads, &parse_reserved_ranges(DT_NO_RTOS)).is_empty());
|
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