# 47 — Validation-predicate probe (`valprobe`) — in development > **Status: in development (2026-10-06, wall-directed wave).** Documents > `harness/valprobe.py`: run one firmware **validation branch** of the real > image with crafted register/memory state (plus, where needed, one recorded > inline stub for a dependency outside the predicate) and record the branch > the firmware took. Born from the live GR wall's question class: *which > control/entry shapes does 570.144 GSP-RM accept or refuse, and why?* > > Development-only: executes firmware on the simulator. Never touches a GPU, > kernel module, mdev, guest or network. No timing/cycle claim (L0/L1). > > **See also:** `46-SURGICAL-REHOST.md` (whole-function rehost — the parent > tool), `44-GMMU-ORACLE-AND-WORKFLOW.md` (evidence discipline), > `39-RE-WORKFLOW-REVIEW.md` (time-to-value ranking), `06-ACCURACY.md` > (fidelity levels). ## 1. Why it exists The live GR campaign submits `GPU_PROMOTE_CTX` shapes to the real GSP-RM and reads accept/refuse per supervised cycle. The decisive questions are firmware-side predicates, but a whole-function rehost (`docs/46`) early-outs on missing RM object state (`0x40`) before the predicate executes. This harness executes the predicate branch itself: * **entry**: the validation branch point (not the full handler); * **state**: exactly the registers/memory the branch reads, crafted in RAM; * **stub**: at most one `lui/addi/ret` stub for a dependency that is *not* the predicate (e.g. a lock/table walk), recorded in the report; * **outcome**: execution stops on labelled outcome addresses; the verdict is the branch taken, with the firmware's own arithmetic/indexing. ## 2. Invocation ```bash # Bundled matrix: the 570.144 PROMOTE_CTX per-buffer validation (FECS etc.) python3 harness/valprobe.py sweep # crafted-state, ~2 s python3 harness/valprobe.py sweep --boot-to B3 # boot the image first python3 harness/valprobe.py demo # matrix twice (determinism) python3 harness/valprobe.py run --spec my-spec.json # generic spec ``` A spec is `greeneye-valprobe-spec/1` with a `target`, an `outcomes` map (address → label), optional `boot_to`, and `variants` carrying `registers`, `memory` (`hex|u64|u32|u16|u8`), `stubs` (`addr` + `return`), `reads` (post-run) and `expect`. Report schemas: `greeneye-valprobe/1` per variant, `greeneye-valprobe-matrix/1` for the run (`valprobe-matrix.json`), with one `greeneye-session/1` per variant when recording is on. Outcome statuses: `outcome` (a labelled address was reached — the verdict), `return` (`ra` sentinel), `fault` (first missing state), `ecall`, `timeout`, `budget`. `pass` compares `outcome` to `expect`; `sweep` exits 3 on any fail. ## 3. The current wall, answered offline (570.144, GR PROMOTE_CTX) Live facts this reproduces (V6 KMD-managed channel, supervised cycles): `PM`/`PATCH` init accepted, **FECS_EVENT init refused `0x1f`**, FECS VA-only accepted; engine dispatch still absent. The handoff's suggested next hardware experiment was "find a FECS init form V6 accepts (e.g. a sysmem attr 0x6 / different size / different bufferId)". The probe enters the per-buffer validation branch `0x5290dd4` (`FUN_ram_05290a48`) and executes the firmware's own logic. Firmware facts (disassembly, numeric addresses only): | # | Fact | |---|---| | 1 | `bInitialize=1`, `bufferId==9` (FECS) takes the branch at `0x5290e3c` | | 2 | that branch reads the **global GR-context memDesc slot**: `instance = pGpu[(idx+0x37c)]`, `table = *(u64*)(instance+0x190)`, slot index from the firmware's own `0x531b944` jump table (**FECS → 6**), and returns `NV_ERR_INVALID_ARGUMENT` (`0x1f`) **iff `slot != 0`** | | 3 | `physAttr`, `size`, `gpuPhysAddr`, `gpuVirtAddr` are **not read** in that branch | | 4 | `bInitialize=0`: `bNonmapped && !bInitialize` → `0x1f`; otherwise the entry passes | | 5 | init is allowed for ids 0..2, 8..11; refused for 3..7 and 12; ids > 12 refused a block earlier | So **if the server has already populated the FECS slot, no init form exists** — the aperture/size variants cannot change the verdict, and the only accepted FECS shape is `bInitialize=0` (a VA booking, matching the live VA-only accept). The campaign can redirect from "find a FECS init form" to "prevent/clear the server's FECS slot" or "accept VA-only and look at dispatch elsewhere" without spending a cycle on the aperture test. Matrix (15/15, deterministic; run twice with identical verdicts; 1.9 s crafted-state, 13 s with `--boot-to B3`): | variant | verdict | |---|---| | FECS init vidmem, slot non-NULL | `refuse-fecs-slot-exists-0x1f` | | FECS init vidmem, slot NULL | accept (commit path) | | FECS init **sysmem attr 0x6**, slot non-NULL | `refuse-fecs-slot-exists-0x1f` | | FECS init **sysmem attr 0x6**, slot NULL | accept | | FECS init size 0x1000, slot non-NULL | `refuse-fecs-slot-exists-0x1f` | | FECS no-init mapped, slot non-NULL | accept (skip path) | | FECS no-init nonmapped | `refuse-invalid-argument-0x1f` | | PM init / PATCH init | accept | | GFXP_CTRL_BLK (8) init | accept (skip path) | | PRIV (10) / UNR (11) init | accept (skip path) | | BUNDLE_CB (3) / GLOBAL_PRIV (12) init | `refuse-invalid-argument-0x1f` | | unknown id 13 | `refuse-invalid-argument-0x1f` | Each variant's report also carries the firmware-mapped slot index read back from the run (`6` for FECS) and the applied stub, so the data flow is in the evidence, not in prose. ### 3.1 The FECS buffer is global and one-shot (live-confirmed 2026-10-06) The probe's refusal condition was then joined to a live backend run and the semantics are now settled: * The FECS memDesc lives in a **global GR context-buffer table shared across channels** (per GPU/client, per gfid); the table's only writer is the init-promote path, and a successful init leaves the slot set so that **every later `bInitialize=1` FECS promote is refused `0x1f`** — independent of aperture, size, address or channel. * A live run showed the decisive ordering: the backend's own **golden-channel 9-entry atomic promote** (including `FECS_EVENT bInitialize=1`) was accepted, and the per-entry FECS promote on a *different channel of the same client* was refused `0x1f` **~0.1–0.3 s later**. The refusal was the shared one-shot rule, not a channel deficiency. * The per-channel requirement for FECS is therefore only the **VA booking** (`bInitialize=0`), which the same live channel already accepted. No client "FECS init form" exists once any earlier promote initialized the shared buffer; an atomic multi-entry set containing FECS init fails as a whole after that point. This is what the probe class is for: it turns a supervised-cycle guessing loop into a seconds-long offline verdict plus one log join. ## 4. Honesty rules * A probe is **not a boot** and **not a model**: only the seeded state exists; the report lists entry, registers, memory, stubs, outcomes and limits. * A stub replaces an explicitly named dependency outside the predicate; the predicate, its id→slot mapping call and its memory reads are the firmware's own code. Prefer entering after the dependency when that is safe. * A fault is the **first missing state observed**, not a product defect; a timeout/budget is not a verdict. * `--boot-to ` runs the real image first so predicates that need initialized globals can be probed with real state; the report records the observed rung and whether the request was reached. * No timing/cycle claims; sessions and matrix JSON are the evidence. ## 5. Tests `tests/test_valprobe.py` — stub encoding decodes as `lui/addi/jalr`, memory spec widths, spec shape, scratch layout bounds, a live reduced matrix with determinism, the firmware slot-index read-back (asserts `6`), boot-to-B3, and CLI exit codes. Live tests skip without the shipped image. ```bash python3 -m unittest tests.test_valprobe -v python3 harness/valprobe.py demo ``` ## 6. Problem classes and time-to-value Use this tool when the question can be stated as *"what will this firmware branch do with input/state X?"* — control-entry validation, accept/refuse rules, error-path predicates, field-dependency questions. It answers offline, in seconds, with labelled outcomes and a session; the alternative is static reading (rule known, cause not), or a supervised hardware cycle per variant (minutes to an hour plus teardown risk). The reference case (2026-10-06): a wall that had consumed five-plus supervised cycles and was still misdiagnosed as a channel limitation was closed by a 15-variant probe (≈2 s, run twice, identical) plus one log join. The rule to copy: phrase the question as a predicate → probe it offline → join the verdict to live logs → spend the supervised cycle only on what the probe cannot see (the engine/silicon side). Companion tools: `harness/rehost.py` (docs/46) for whole-function calls with crafted state and snapshots; `sim.gsp_boot` for boot-state evidence (rung and state); `harness/triage.py` / `version_diff.py` for release-to-release questions.