Skip to content

security: remove uncalibrated automatic modem cuts from threat policy #874

Description

@forkwright

Finding

SecurityModeManager::evaluate_threat accepts a caller-supplied score and, at the critical threshold, enters Panic firewall policy and invokes the modem power-cut path. The function is still unreferenced outside tests but is compiled and tested. It contradicts the active Phase-10 contract that uncalibrated detector output is advisory and cannot autonomously cut radios or enter Panic.

What #885 changed is the consequence, not the authorization gap. The cut path now records a request instead of performing MMIO, so the same uncalibrated score can no longer reach hardware — but it still autonomously drives Panic firewall mode and a sticky requested state, and it will reach hardware the moment #862 lands a working seam behind it.

Evidence

Current tree, main at a1154631cd03a212636e36407ba73d21d675fff0:

  • crates/thumos/src/security_mode.rs:808-826evaluate_threat compares a raw score to a caller-supplied threshold and, above it, calls firewall.apply_mode(FirewallMode::Panic) and power_manager.request_modem_power_cut(). Two FIXME(#874) markers sit on those exact lines.
  • crates/thumos/src/security_mode.rs:1546,1577,1604,1630 — every call site of evaluate_threat is inside #[cfg(test)]. It remains dead-but-public policy.
  • crates/thumos/src/power.rs:459-471request_modem_power_cut sets PowerState::PowerCutRequested and performs no hardware operation.
  • crates/thumos/src/security_mode.rs:791 — the response renders as modem cut requested (unapplied).
  • Phase-10 REQ-05/08 require versioned corpus calibration, uncertainty, and no automatic destructive or radio-cut action.
  • fix(m7): replace invalid direct PMIC MMIO with a source-grounded PWRAP seam #862 owns the PWRAP transaction mechanics; it does not own detector authorization, policy calibration, confirmation, or destructive-action safety.

The earlier Evidence text said evaluate_threat "directly calls modem_power_cut", describing a physical kill. That symbol no longer exists; #885 replaced it with the request-only path above.

The requested/observed fix on the UI side has no regression coverage

crates/thumos/src/kardia.rs:538-543 is the corrected threat indicator: threat_high now derives from self.threat.detector_online() && matches!(self.threat.threat_level(), High | Critical) rather than from CCCI boot-path absence. That fix is correct and is exactly the requested-versus-observed separation this issue asks for.

Nothing would catch its reversion:

  • crates/thumos/src/kardia.rs has no #[cfg(test)] module at all.
  • scripts/witness/boot.sh:92 asserts modem_path_available=false in the threat-monitor log line, and never asserts threat_high.

So a revert to the old !self.boot.modem_ok derivation would pass the full suite and the boot witness. Per VERIFICATION.md, a claim without a failing fixture cannot be passed — this one landed without one, which makes it fixed-until-the-next-refactor rather than durably fixed.

Why this matters

Dead-but-public policy invites a future caller to treat a tested API as accepted security behaviour, and this one now sits directly upstream of a seam that is being built. The ordering is the hazard: if #862 lands a working PWRAP transaction before this authorization policy exists, an arbitrary or false-positive score becomes a real loss of connectivity with no operator in the loop, and nothing in #862's own acceptance criteria would catch that — it is a correctness contract for the transport, not for who is allowed to call it.

The untested UI fix compounds the same problem from the other direction. The whole point of separating requested from observed state is that an operator can trust the badge; a derivation nothing guards can silently drift back to reporting a boot-path artefact as a threat.

Done when

  • Remove or redesign score-to-action coupling so detector outputs carry source, policy version, uncertainty, calibration status, freshness, and evidence; uncalibrated output is advisory only.
  • Any requested radio transition crosses an explicit operator-accepted authorization/confirmation policy with bounded emergency behaviour, and cannot call fix(m7): replace invalid direct PMIC MMIO with a source-grounded PWRAP seam #862's seam without it.
  • Separate requested, software-applied, independently observed, failed, and unknown state in the API, logs, UI, docs, and tests.
  • The kardia.rs threat-indicator derivation is covered by a regression test over the detector_online × threat_level cross product, or the boot witness asserts threat_high directly. Extracting the expression to a pure function is the cheap route.
  • Cover spoofed/stale/replayed/missing evidence, threshold boundaries, false positives, unavailable UI, repeated requests, partial failure, and rollback.
  • Keep physical kill claims impossible until a source-grounded driver/PMIC seam and an independent receipt exist.

This issue gates activation of #862's callers; #862 may land its transport without waiting on this, but nothing may call it on hardware until this is resolved.

Metadata

Metadata

Assignees

No one assigned

    Labels

    securitySecurity vulnerability or hardening

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions