orchestrator: document PreSupervision/CorruptionDetected gap as accepted PreSupervision isn't linked into SupervisingPlatform, so corruption on an already-released component is dropped during an all-Passive chain walk. CSA defines no mechanism for detecting corruption of a live, executing component, so this isn't a confirmed requirement to fix. Reframes the Superstate doc comment, renames the expected-failing test (...triggers_recovery -> ...is_dropped) to match actual behavior, and softens the unconfirmed CSA-continuity claim in the walkthrough doc.
diff --git a/docs/src/design/orchestrator/orchestrator-machine.md b/docs/src/design/orchestrator/orchestrator-machine.md index d6c8103..62b2a2a 100644 --- a/docs/src/design/orchestrator/orchestrator-machine.md +++ b/docs/src/design/orchestrator/orchestrator-machine.md
@@ -115,6 +115,17 @@ **not** consulted here; it is applied later, in `Recovering`, only if the restore attempts are exhausted. +> **Deliberate exclusion, not an oversight.** Because `PreSupervision` has no +> `superstate()` link, a `CorruptionDetected` on an already-released component +> is discarded (the `anything else` row above) for as long as the machine +> keeps self-looping here — which, for an all-`Passive` chain, can span the +> entire walk. This is a current, intended decision: CSA defines no mechanism +> for detecting corruption of an already-released component's *live, +> executing* state, so there is no confirmed CSA requirement forcing +> continuous coverage from a component's own release. See +> `corruption_during_presupervision_selfloop_is_dropped` in `lib.rs` and the +> "Decision" note in [orchestrator-sm-walkthru.md](./orchestrator-sm-walkthru.md). + --- ### `AwaitingReady` @@ -272,8 +283,8 @@ | Event | Guard | Effects | Next state | |---|---|---|---| | `AttestationChallenge` | — | `SignAttestation` | `Handled` (no transition — INV6) | -| `CorruptionDetected(id)` | `attrs.required == true` | — | `Recovering` (failed = Some(id) — INV5) | -| `CorruptionDetected(id)` | `attrs.required == false` | `AssertReset(id)` | `Handled` (component gated; machine stays in current state) | +| `CorruptionDetected(id)` | `attrs.failure_policy == Required` | — | `Recovering` (failed = Some(id) — INV5) | +| `CorruptionDetected(id)` | `attrs.failure_policy != Required` | `AssertReset(id)` | `Handled` (component gated; machine stays in current state) | | anything else | — | — | `Outcome::Super` (discarded) | --- @@ -361,10 +372,10 @@ ## Invariant Verification -The invariants for the `SupervisingPlatform` regime describe the whole regime, not one -state at a time. Because the hierarchy stores each rule at that same whole-regime -level, checking that the code matches the spec stays simple instead of turning -into a state-by-state comparison. +The invariants for the `SupervisingPlatform` superstate describe the whole +superstate, not one state at a time. Because the hierarchy stores each rule at +that same superstate level, checking that the code matches the spec stays +simple instead of turning into a state-by-state comparison. Take INV6: *"an attestation challenge is answered in any `SupervisingPlatform` state without changing state."* In this design the rule itself is one line of code —
diff --git a/docs/src/design/orchestrator/orchestrator-sm-walkthru.md b/docs/src/design/orchestrator/orchestrator-sm-walkthru.md index 2b9387b..6acad0d 100644 --- a/docs/src/design/orchestrator/orchestrator-sm-walkthru.md +++ b/docs/src/design/orchestrator/orchestrator-sm-walkthru.md
@@ -23,7 +23,7 @@ ## The shape of the journey -At the highest level the machine has two operating regimes, a provisioning gate, +At the highest level the machine has two operating modes, a provisioning gate, and a terminal exit: ```mermaid @@ -58,14 +58,35 @@ contract switches on and stays on: attestation is always answered and corruption is always acted on, regardless of whether the chain walk is still in progress. -The threshold is one firmware verification result — passed or failed — not all -components having passed verification. CSA does not define a safe window before -supervision begins. A remote verifier may issue an attestation challenge as soon -as the first component's measurements exist. A corruption can be detected the -moment a component is running. The supervision contract — attestation always -answered, corruption always acted on — must therefore hold continuously from the -first result onward, including during recovery before anything has been released. -Deferring supervision until `Ready` would leave a gap that CSA does not permit. +The threshold *for an `Active` component* is one firmware verification result — +passed or failed — not all components having passed verification: the next +`VerificationPassed` moves the machine into `AwaitingReady`, which is linked to +`SupervisingPlatform`, so supervision switches on at that point. + +For an all-`Passive` chain, no such threshold exists before `Ready`: the +machine self-loops inside `PreSupervision`, which is *not* linked to +`SupervisingPlatform`, for the entire walk. A corruption on an +already-released `Passive` component reported during that self-loop is +currently discarded (see +`corruption_during_presupervision_selfloop_is_dropped` in `lib.rs`). + +**Decision: `PreSupervision` stays out of `SupervisingPlatform` for now.** It +is tempting to argue that CSA implies supervision must hold continuously from +each component's own release — a remote verifier could plausibly challenge as +soon as the first component's measurements exist, and corruption could +plausibly be detected the moment a component is running — but CSA does not +actually say this anywhere. CSA's Resiliency chapter defines only two +corruption-detection points (at-boot and at-rest/NVM-polling); neither is a +monitor of an already-released component's *live, executing* state. NVM +polling in particular re-checks the stored flash image, not what a component +is currently executing, so it cannot stand in for that missing mechanism. +Since CSA never defines a runtime-execution integrity monitor, or assigns +responsibility for one, there is no confirmed CSA requirement forcing +continuous coverage during the `PreSupervision` self-loop. Until CSA states +otherwise, `PreSupervision` remains architecturally isolated from +`SupervisingPlatform` and this gap is accepted as current, documented +behavior rather than something to patch around. Revisit only if CSA is +amended to define such a mechanism and assign it to the eRoT/PRoT. During recovery the machine temporarily exits supervision (`SupervisingPlatform::Recovering` → `PreSupervision`), gating all components @@ -74,10 +95,10 @@ - **`PowerOnReset`** is the provisioning gate: the eRoT checks its own integrity before it vouches for anything else. -- **`PreSupervision`** is regime one — supervision contract off: the eRoT walks +- **`PreSupervision`** is mode one — supervision contract off: the eRoT walks the trust chain, verifying and releasing components without yet answering attestation challenges or acting on corruption. -- **`SupervisingPlatform`** is regime two — supervision contract on: attestation, +- **`SupervisingPlatform`** is mode two — supervision contract on: attestation, firmware updates, iRoT gating (`AwaitingReady`), and active recovery (`SupervisingPlatform::Recovering`) all run under one shared superstate. `PreSupervision` drives into it; `SupervisingPlatform::Recovering` drives
diff --git a/services/orchestrator/sm/src/lib.rs b/services/orchestrator/sm/src/lib.rs index 628df58..1b9fc44 100644 --- a/services/orchestrator/sm/src/lib.rs +++ b/services/orchestrator/sm/src/lib.rs
@@ -266,13 +266,25 @@ Locked, } -/// Superstate entered on the eRoT's first component release and held until -/// [`State::Locked`]. Provides two platform-wide guarantees that must hold -/// across all four sub-states ([`State::AwaitingReady`], [`State::Ready`], -/// [`State::Updating`], [`State::Recovering`]): +/// Superstate entered once the eRoT exits [`State::PreSupervision`] — i.e. on +/// release of the first `Active` component, or once the whole chain has +/// finished if it is all-`Passive`. Provides two platform-wide guarantees +/// that must hold across all four sub-states ([`State::AwaitingReady`], +/// [`State::Ready`], [`State::Updating`], [`State::Recovering`]): /// /// - Attestation challenges are always answered. /// - Corruption of a required component always triggers recovery. +/// +/// Note: [`State::PreSupervision`] itself is *not* linked to this superstate +/// (its `superstate()` returns `None`), so neither guarantee holds while a +/// component is still being walked there. This is a deliberate, current +/// decision, not an oversight to be silently patched: CSA defines no +/// mechanism for detecting corruption of an already-released component's +/// *live, executing* state (NVM polling only re-checks stored flash images), +/// so there is no confirmed CSA requirement forcing continuous coverage +/// during this self-loop. Keep `PreSupervision` excluded unless/until CSA is +/// amended to define such a mechanism. See +/// `corruption_during_presupervision_selfloop_is_dropped` below. #[derive(Debug)] pub enum Superstate<'sub> { SupervisingPlatform(PhantomData<&'sub ()>), @@ -852,6 +864,37 @@ assert_eq!(state, State::PreSupervision); } + /// ACCEPTED GAP, KEPT DELIBERATELY: once a component has been released, + /// `PreSupervision`'s `superstate()` returns `None`, so `CorruptionDetected` + /// on it falls to `Outcome::Super` with nowhere to go and is silently + /// discarded while the machine is still self-looping through the rest of + /// a multi-component chain (all-`Passive` chains only — an `Active` + /// component in the chain incidentally closes this gap via `AwaitingReady`, + /// which *is* linked to `SupervisingPlatform`). + /// + /// This test documents current, intended behavior. CSA defines no + /// mechanism for detecting corruption of an already-released component's + /// *live, executing* state — only at-boot and at-rest/NVM-polling + /// detection, and NVM polling only re-checks stored flash images, not + /// what a component is currently executing. With no confirmed CSA + /// requirement for continuous coverage from a component's own release, + /// `PreSupervision` stays excluded from `SupervisingPlatform`. Do not + /// "fix" this by linking `PreSupervision` into `SupervisingPlatform` + /// unless CSA is amended to define such a mechanism. + #[test] + fn corruption_during_presupervision_selfloop_is_dropped() { + let (effects, state) = drive( + passive_required(&[C0, C1, C2]), + &[ + BOOT, + Event::VerificationPassed(C0), // released; walk continues (still PreSupervision) + Event::CorruptionDetected(C0), // C0 already released; currently a no-op + ], + ); + assert_eq!(state, State::PreSupervision); + assert!(!effects.contains(&Effect::RestoreGoldenImage(C0))); + } + /// INV7 (feedback-as-data): after MAX_RETRY restores the core self-emits /// RecoveryFailed and latches to Locked without any external RecoveryFailed /// in the script.