Files
ruvnet--RuView/docs/adr/ADR-177-nvsim-degenerate-input-hardening.md
T
rUv 1df6d1e1ee security(nvsim): guard degenerate input — config panic + NaN silent-corruption + ADR-177 (#1098)
* fix(nvsim): guard degenerate input — config-induced panic + NaN-state poisoning

Beyond-SOTA security review of the ADR-089 NV-diamond simulator (milestone #9,
crate 2 of 4). Two real degenerate-input findings, each pinned fails-on-old:

NVSIM-DT-01 (config panic/DoS, pipeline.rs): an external f_s_hz == 0 made
dt == +Inf, dt_us saturated to u64::MAX, and `sample * dt_us` panicked with
"attempt to multiply with overflow" at sample >= 2 (debug/WASM panic=abort;
garbage t_us in release). Fix: sanitise dt (non-finite/non-positive -> 1 µs
fallback), cap the u64 cast, and saturating_mul the timestamp.

NVSIM-NAN-01 (NaN-state poisoning, digitiser.rs): a non-finite scene parameter
(NaN dipole position / Inf moment / NaN loop radius) bypasses the near-field
clamp (NaN < R_MIN_M is false) and yields a NaN field; at the ADC `NaN as i32`
== 0 silently emitted b_pt=[0,0,0] with ADC_SATURATED CLEAR — indistinguishable
from a legit zero-field reading. Fix at the funnel: adc_quantise treats any
non-finite input as out-of-range -> clamps to code 0 AND raises the saturation
flag, so the corruption is visible downstream.

Determinism integrity, panic-free MagFrame deserialisation, and RNG seeding
confirmed clean with evidence. The published cross-machine witness
(cc8de9b0…93b4) is unchanged — guards only affect degenerate inputs.

cargo test -p nvsim --no-default-features: 50 -> 53 passed, 0 failed.
Workspace green; Python deterministic proof unchanged (f8e76f21…46f7a,
nvsim off the signal proof path). Needs ADR slot 177.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr): ADR-177 — nvsim degenerate-input hardening

Records the 2 MEASURED MEDIUM fixes in 37764be55 (NVSIM-DT-01 config-induced
overflow panic / WASM-abort DoS; NVSIM-NAN-01 non-finite scene param →
silent fake zero-field reading with saturation flag clear) + 3 pins, and the
clean-with-evidence determinism/deser/div-by-zero verdict. Cross-machine
witness cc8de9b0…93b4 reproduces unchanged.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-15 10:55:04 -04:00

5.6 KiB
Raw Blame History

ADR-177: nvsim Degenerate-Input Hardening (NV-Diamond Simulator)

Field Value
Status Accepted — 2 real MEDIUM bugs fixed + pinned; determinism preserved
Date 2026-06-15
Deciders ruv
Codename NVSIM-FAILCLOSED
Reviews ADR-089 (nvsim NV-diamond magnetometer pipeline simulator)
Milestone #9 (ungated-crate security sweep) — crate 2 of 4

Context

nvsim (ADR-089) is a standalone, WASM-ready deterministic NV-diamond magnetometer pipeline simulator — a forward-only leaf: scene → source → propagation → NV ensemble → digitiser → MagFrame + SHA-256 witness. It has no network surface, so the real attack surface is degenerate physical-parameter input crossing the external boundary — specifically the WASM config_json / scene_json entry points.

Two properties matter for this crate that don't for others: it is billed deterministic (a published cross-machine witness must reproduce bit-exactly), and under panic=abort WASM any panic aborts the whole module. So a config-induced panic is a denial-of-service, and a silent numeric corruption defeats the simulator's entire purpose.

Decision

Fix the two reachable degenerate-input bugs at their funnel points, each pinned by a fails-on-old test, without perturbing the deterministic happy path (the guards fire only on non-finite / degenerate input; the published witness is unchanged).

Findings fixed (both MEASURED-reproduced)

# Severity Location Issue Fix
NVSIM-DT-01 MEDIUM (DoS) pipeline.rs:58,95 dt = config.dt_s.unwrap_or(1.0 / f_s_hz); an external f_s_hz == 0.0dt = +Inf(dt*1e6) as u64 saturates to u64::MAX(sample as u64) * dt_us panics attempt to multiply with overflow at sample ≥ 2 (debug/WASM-abort; garbage t_us in release). MEASURED: panic at pipeline.rs:95:30. Sanitise dt (non-finite/non-positive → 1 µs fallback), cap the u64 cast at u64::MAX, saturating_mul the timestamp — no config can overflow it.
NVSIM-NAN-01 MEDIUM (silent corruption) funnel digitiser.rs::adc_quantise (root: near-field clamp bypass in source.rs) A non-finite scene param (NaN/Inf dipole position, Inf moment, NaN loop radius) bypasses the near-field clamp (NaN < R_MIN_M == false → the 1/r³ path runs → NaN field), and at the ADC NaN as i32 == 0 (Rust saturating cast) emits a frame b_pt=[0,0,0] with ADC_SATURATED CLEAR — indistinguishable from a legitimate zero-field reading. MEASURED: b=[NaN,NaN,NaN] sat=falseb_pt=[0,0,0] flags=0b0000. adc_quantise: any non-finite input → code 0 with the saturation flag raised; the pipeline's existing adc_sat OR-reduction propagates ADC_SATURATED onto the frame, making the corruption visible downstream.

This is the same NaN-fail-open / NaN-poisoning family seen across calibration/vitals/geo and ruview-swarm — non-finite input defeating a guard — but bounded here to a single frame (no cross-timestep accumulator).

Dimensions confirmed clean (with evidence)

  1. Determinism integrity — clean. One RNG only: ChaCha20Rng::seed_from_u64(seed), fully caller-seeded (grep: one seed_from_u64, zero thread_rng/getrandom/ SystemTime/Instant/HashMap); Cargo.toml pins rand/rand_chacha default-features=false (no OS entropy). BoxMuller draws gen_range(f64::EPSILON..=1.0) (avoids ln(0)=-Inf by construction). Frame bytes fixed LE; source summation order fixed by Vec order. The published cross-machine witness cc8de9b0…93b4 (proof_witness_publishes_a_known_value) passes UNCHANGED after both fixes — the happy path is byte-identical; guards touch only degenerate inputs. Attested caveat (not a finding): libm cos/ln/sqrt could differ x86↔wasm; the witness is documented as x86_64-captured.
  2. Panic-free deserialisation — clean. MagFrame::from_bytes validates len/magic/version, then per-field buf[a..b].try_into().expect(...) are over fixed sub-ranges of an already-length-checked 60-byte buffer (provably infallible). No unsafe, no panic!/unreachable! in production; every other unwrap/expect is #[cfg(test)].
  3. Div-by-zero / numerical landmines — clean. dipole_field/current_loop_field clamp r_norm < R_MIN_M before 1/r³,1/r² (finite inputs); shot_noise_floor guards denom <= 0; vec3_normalise guards n < 1e-20. The only hole was the NaN bypass of the clamp — closed at the ADC funnel (NVSIM-NAN-01).

Validation

  • cargo test -p nvsim --no-default-features50 → 53 passed, 0 failed (+3 pins: degenerate_zero_sample_rate_does_not_panic, non_finite_scene_input_flags_frame_instead_of_silently_zeroing, adc_quantise_flags_non_finite_as_saturated).
  • cargo test --workspace --no-default-featuresexit 0, 0 failed.
  • python archive/v1/data/proof/verify.pyVERDICT: PASS, hash f8e76f21…46f7a unchanged (nvsim off the signal proof path).
  • nvsim's own cross-machine witness cc8de9b0…93b4 reproduces unchanged.

Consequences

Positive

  • A config-induced WASM-abort DoS and a silent NaN→fake-zero-field corruption are closed at their funnel points, each regression-pinned, with the deterministic witness proven intact.

Negative / Neutral

  • None. Guards affect only degenerate inputs; happy-path output is byte-identical.
  • ADR-089 — nvsim NV-diamond magnetometer simulator
  • ADR-176 — ruview-swarm (sibling NaN-fail-open review)
  • ADR-172 — core/cli (where the NaN-bug-class root was settled NO)