orchestrator/sm: drop out-of-chain ids at the dispatch boundary Enforce component-id membership once in step() via Event::component_id(), so no handler acts on an id outside the configured chain. Also: rename RestoreGoldenImage -> RecoverComponent, add the commit-or-lock watchdog (pending_commit/CommitTimeout), and drop #[non_exhaustive].
diff --git a/services/orchestrator/sm/src/lib.rs b/services/orchestrator/sm/src/lib.rs index 7d31b67..8dd5f35 100644 --- a/services/orchestrator/sm/src/lib.rs +++ b/services/orchestrator/sm/src/lib.rs
@@ -182,6 +182,21 @@ /// resets it. statuses: heapless::Vec<ComponentStatus, N>, max_retry: u8, + /// Set when an update has been activated ([`Effect::ActivateUpdate`]) but + /// its anti-rollback floor has not yet been committed, i.e. the machine is + /// in the *activated-but-not-committed* window inside [`State::Ready`]. A + /// [`Event::BootConfirmed`] commits the floor and clears this; a + /// [`Event::CommitTimeout`] fired while this is set latches + /// [`State::Locked`] (commit-or-lock: the floor is never advanced for an + /// image that has not proven healthy, and the downgrade window is never left + /// open indefinitely). Cleared on [`Event::BootConfirmed`] (the window + /// closes normally) and on entry to the two states that end the window by + /// leaving `Ready` while still running — [`State::Updating`] (a superseding + /// update) and [`State::Recovering`] (corruption/timeout) — so any path that + /// leaves and later re-enters `Ready` resets the window without per-branch + /// bookkeeping. (It is *not* cleared on `Ready` entry, because activation + /// sets it while transitioning *into* `Ready`.) + pending_commit: bool, /// Ties the effect-buffer size `E` to this type (zero-sized). _effect_cap: PhantomData<[u8; E]>, } @@ -209,6 +224,7 @@ cursor: 0, statuses, max_retry, + pending_commit: false, _effect_cap: PhantomData, } } @@ -228,6 +244,14 @@ self.chain.iter().position(|(cid, _)| *cid == id) } + /// Whether `id` names a component in the configured chain. The core only + /// supervises chain components, so an event that names an id outside the + /// chain is dropped rather than acted on — it describes something the core + /// has no model of and never released. + fn in_chain(&self, id: ComponentId) -> bool { + self.status_index(id).is_some() + } + fn is_gated(&self, id: ComponentId) -> bool { self.status_index(id) .is_some_and(|i| self.statuses[i].lifecycle == ComponentLifecycle::Isolated) @@ -365,8 +389,8 @@ /// so this path and the recovery-exhaustion path can never diverge: /// `Isolable`/`Cascading` → gate the component (single or cascade) and stay /// put, so a later re-walk skips it instead of silently re-releasing one we - /// already found corrupt; `Required`/unknown → recover first (the - /// halt-on-exhaustion decision happens later in `Recovering`). + /// already found corrupt; `Required` → recover first (the halt-on-exhaustion + /// decision happens later in `Recovering`). fn handle_corruption(&mut self, id: ComponentId, ctx: &mut Sink<E>) -> Outcome { match self.gate_by_policy(ctx, id) { Gating::Gated => Outcome::Handled, @@ -552,18 +576,37 @@ Event::UpdateRequest => Outcome::Transition(State::Updating), // Proven-boot checkpoint: the image authenticated at // `ActivateUpdate`, but the SVN floor only advances now, once - // the shell reports it healthy. Handled in place — confirming a - // running image is not a state change. + // the driver reports it healthy. Handled in place — confirming a + // running image is not a state change. Closing the window clears + // `pending_commit` so a later `CommitTimeout` cannot lock a + // device that has already committed. Event::BootConfirmed(id) => { ctx.emit(Effect::CommitSvnFloor(*id)); + self.pending_commit = false; Outcome::Handled } + // Commit watchdog. If the activated-but-not-committed window is + // still open, fail closed: never commit an unproven image, and + // never leave the downgrade window open indefinitely. Outside + // the window this is a stale watchdog fire and is dropped. + Event::CommitTimeout => { + if self.pending_commit { + Outcome::Transition(State::Locked) + } else { + Outcome::Handled + } + } _ => Outcome::Super, }, State::Updating => match event { Event::UpdateVerified => { ctx.emit(Effect::ActivateUpdate); + // Open the activated-but-not-committed window: the floor is + // NOT advanced here; it waits for `BootConfirmed`. The + // driver arms its commit watchdog on `ActivateUpdate`, and + // `CommitTimeout` bounds this window (commit-or-lock). + self.pending_commit = true; Outcome::Transition(State::Ready) } Event::UpdateRejected => { @@ -715,15 +758,22 @@ let _ = self.advance_to_next_ungated(ctx, 0); } State::Updating => { + // A new update supersedes any activated-but-not-committed image; + // the prior commit window is void. + self.pending_commit = false; ctx.emit(Effect::AuthenticateUpdate); ctx.emit(Effect::StageUpdate); } State::Recovering(failed) => { + // Recovery voids any activated-but-not-committed image: the + // running image is now under suspicion, so its commit window + // ends here. + self.pending_commit = false; // The component under recovery is being restored, not booting; // drop any pending boot-progress watchdog so a late `Timeout` // can't re-enter recovery for it. self.clear_awaiting_boot(failed); - ctx.emit(Effect::RestoreGoldenImage(failed)); + ctx.emit(Effect::RecoverComponent(failed)); } State::Locked => { ctx.emit(Effect::LatchLockdown); @@ -818,6 +868,18 @@ /// isolation reports, plus the destination `PreSupervision` entry's two /// effects share one `Sink`). fn step(&mut self, event: &Event, ctx: &mut Sink<E>) { + // 0. Single point of id-membership enforcement. The core supervises + // only the components in the configured chain, so an event that names + // an id the chain does not contain is dropped here, before any + // handler runs — no handler needs its own membership check, and none + // can act on a component the core never modeled. Events that name no + // component (`component_id() == None`) always pass through. + if let Some(id) = event.component_id() + && !self.rot.in_chain(id) + { + return; + } + // 1. Dispatch to the current (leaf) state. let mut outcome = self.rot.handle(self.state, event, ctx);
diff --git a/services/orchestrator/sm/src/model.rs b/services/orchestrator/sm/src/model.rs index f9537e9..ae9cfd8 100644 --- a/services/orchestrator/sm/src/model.rs +++ b/services/orchestrator/sm/src/model.rs
@@ -26,7 +26,6 @@ /// Corresponds directly to the two-tier model in the CSA architecture document: /// `Active` = eRoT gate + iRoT gate; `Passive` = eRoT gate only. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -#[non_exhaustive] pub enum ComponentKind { /// Has an integrated iRoT (e.g. Caliptra). Both eRoT-side (signature + SVN) /// and iRoT-side (local self-verification) checks apply. The machine waits in @@ -54,7 +53,6 @@ /// (The narrative design docs sometimes call the `Required` outcome "platform /// halt" — same behavior, this is the type-level name.) #[derive(Clone, Copy, PartialEq, Eq, Debug)] -#[non_exhaustive] pub enum FailurePolicy { /// Stop the boot sequence entirely: self-emits [`Event::RecoveryFailed`], /// which drives the machine to [`State::Locked`]. @@ -172,7 +170,6 @@ /// The result of the board's power-on checks, delivered inside [`Event::PowerGood`]. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -#[non_exhaustive] pub enum PowerOnResult { /// Self-verified and provisioned. Provisioned, @@ -184,7 +181,6 @@ /// Everything the outside world can tell the state machine. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -#[non_exhaustive] pub enum Event { /// Power-on, carrying the shell's self-verification and provisioning result. PowerGood(PowerOnResult), @@ -215,7 +211,7 @@ BootConfirmed(ComponentId), /// This component was found corrupt at runtime. CorruptionDetected(ComponentId), - /// This component's golden image has been restored. + /// This component has been restored from its configured recovery source. Restored(ComponentId), /// A required component's recovery was exhausted. RecoveryFailed, @@ -228,6 +224,18 @@ /// is stale/spurious and dropped. The watchdog is per component and /// device-agnostic, matching CSA boot-progress checkpointing. Timeout(ComponentId), + /// The driver's *commit* watchdog fired: an update was activated + /// ([`Effect::ActivateUpdate`]) but did not report [`Event::BootConfirmed`] + /// within the policy window. Distinct from [`Event::Timeout`], which is the + /// per-component boot-progress watchdog; this one bounds the single + /// activated-but-not-committed window in [`State::Ready`]. Handled only while + /// that window is open: it latches [`State::Locked`] (commit-or-lock — the + /// anti-rollback floor is never advanced for an unproven image, and the + /// downgrade window may not stay open indefinitely). Outside the window + /// (nothing pending) it is stale and dropped. The driver arms this watchdog + /// when it executes [`Effect::ActivateUpdate`] and cancels it on + /// [`Effect::CommitSvnFloor`]. + CommitTimeout, /// The shell could not carry out an emitted [`Effect`]; fail-closed, it /// latches to [`State::Locked`] from any state. Injected by the driver when /// a [`Platform::execute`](crate::Platform::execute) call fails; never @@ -235,13 +243,43 @@ EffectFailed, } +impl Event { + /// The component this event is about, or `None` for events that name no + /// component (`PowerGood`, `UpdateRequest`, `CommitTimeout`, …). + /// + /// This is the single enumeration of the id-carrying events, consulted once + /// at the dispatch boundary ([`Orchestrator::step`](crate::Orchestrator::step)) + /// to drop any event that names a component outside the configured chain + /// before a handler ever sees it. A new id-carrying variant must be listed + /// here, or it will bypass that membership check. + pub(crate) fn component_id(&self) -> Option<ComponentId> { + match self { + Event::VerificationPassed(id) + | Event::VerificationFailed(id) + | Event::ComponentReady(id) + | Event::Booted(id) + | Event::BootConfirmed(id) + | Event::CorruptionDetected(id) + | Event::Restored(id) + | Event::Timeout(id) => Some(*id), + Event::PowerGood(_) + | Event::AttestationChallenge + | Event::UpdateRequest + | Event::UpdateVerified + | Event::UpdateRejected + | Event::RecoveryFailed + | Event::CommitTimeout + | Event::EffectFailed => None, + } + } +} + /// Everything the state machine can ask the outside world to do. /// /// [`Effect::Emit`] is the sole internal effect: the orchestrator catches it and /// queues the carried event for immediate handling, making follow-up events /// visible in the effect trace instead of hidden state changes. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -#[non_exhaustive] pub enum Effect { ReadFirmware(ComponentId), VerifyFirmware(ComponentId), @@ -263,7 +301,12 @@ /// floor earlier would burn anti-rollback on an image that authenticated /// but has not yet demonstrated it can boot and run. CommitSvnFloor(ComponentId), - RestoreGoldenImage(ComponentId), + /// Recover `id` from its configured recovery source. The mechanism — + /// golden image, A/B slot, streamed image, or vendor-specific scheme — is + /// deferred to the [`Platform`](crate::Platform) driver, which resolves it + /// per configuration policy. The reducer only names the component to + /// recover; it does not encode how recovery is performed. + RecoverComponent(ComponentId), /// Report that a component has been isolated (held in reset and removed /// from the trust chain) so management software is aware the platform is /// running degraded. Emitted once per component, at the moment it is @@ -307,7 +350,6 @@ /// cursor, the gate set, retry counts) lives in [`Rot`](crate::Rot) shared /// storage. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -#[non_exhaustive] pub enum State { PowerOnReset, PreSupervision, @@ -326,7 +368,8 @@ Ready, Updating, /// A component failed verification (or was found corrupt under a - /// non-gating policy) and its golden image is being restored. The payload is + /// non-gating policy) and is being restored from its configured recovery + /// source. The payload is /// that component — always present, since the machine only enters this state /// with a recovery target in hand. Recovering(ComponentId), @@ -360,7 +403,6 @@ /// Why a `heapless::Vec` of components is not a valid [`Chain`]. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -#[non_exhaustive] pub enum ChainError { /// The chain has no components. Empty,
diff --git a/services/orchestrator/sm/src/tests.rs b/services/orchestrator/sm/src/tests.rs index bc4636d..b479509 100644 --- a/services/orchestrator/sm/src/tests.rs +++ b/services/orchestrator/sm/src/tests.rs
@@ -211,7 +211,7 @@ ], ); assert_eq!(state, State::Recovering(C0)); - assert!(effects.contains(&Effect::RestoreGoldenImage(C0))); + assert!(effects.contains(&Effect::RecoverComponent(C0))); } /// INV7 (feedback-as-data): after MAX_RETRY restores the core self-emits @@ -437,7 +437,7 @@ &[BOOT, Event::VerificationPassed(C0), Event::Timeout(C0)], ); assert_eq!(state, State::Recovering(C0)); - assert!(effects.contains(&Effect::RestoreGoldenImage(C0))); + assert!(effects.contains(&Effect::RecoverComponent(C0))); } /// D2: a timeout for a component that is not awaiting boot-progress is @@ -458,7 +458,39 @@ ], ); assert_eq!(state, State::AwaitingReady(Some(C0))); - assert!(!effects.contains(&Effect::RestoreGoldenImage(C1))); + assert!(!effects.contains(&Effect::RecoverComponent(C1))); +} + +/// An out-of-chain id in a `VerificationFailed` report is dropped: the core +/// supervises only chain components, so a verdict for an id the chain does not +/// contain neither enters `Recovering` nor emits `RecoverComponent`. +#[test] +fn verification_failed_out_of_chain_id_is_dropped() { + let (effects, state) = drive( + passive_required(&[C0, C1]), + &[BOOT, Event::VerificationFailed(C3)], + ); + assert!(!effects.contains(&Effect::RecoverComponent(C3))); + // Untouched: still walking the chain from the top with C0 under verification. + assert_eq!(state, State::PreSupervision); +} + +/// An out-of-chain id in a `CorruptionDetected` report is likewise dropped: a +/// malformed report from the platform cannot drive a spurious recovery or move +/// the machine out of `Ready`. +#[test] +fn corruption_out_of_chain_id_is_dropped() { + let (effects, state) = drive( + passive_required(&[C0, C1]), + &[ + BOOT, + Event::VerificationPassed(C0), + Event::VerificationPassed(C1), + Event::CorruptionDetected(C3), + ], + ); + assert!(!effects.contains(&Effect::RecoverComponent(C3))); + assert_eq!(state, State::Ready); } /// Device-agnostic boot-progress: a *passive* component that is released but @@ -476,7 +508,7 @@ ); assert_eq!(state, State::Recovering(C0)); assert!(effects.contains(&Effect::ReleaseReset(C0))); - assert!(effects.contains(&Effect::RestoreGoldenImage(C0))); + assert!(effects.contains(&Effect::RecoverComponent(C0))); } /// A passive boot timeout is caught even after the chain walk has completed and @@ -492,7 +524,7 @@ &[BOOT, Event::VerificationPassed(C0), Event::Timeout(C0)], ); assert_eq!(state, State::Recovering(C0)); - assert!(effects.contains(&Effect::RestoreGoldenImage(C0))); + assert!(effects.contains(&Effect::RecoverComponent(C0))); } /// A passive component that reports [`Event::Booted`] retires its watchdog, so a @@ -511,7 +543,7 @@ ], ); assert_eq!(state, State::Ready); - assert!(!effects.contains(&Effect::RestoreGoldenImage(C0))); + assert!(!effects.contains(&Effect::RecoverComponent(C0))); } /// D2: full path — timeout drives recovery, restore rewalks from the top, and @@ -534,7 +566,7 @@ ], ); assert_eq!(state, State::Ready); - assert!(effects.contains(&Effect::RestoreGoldenImage(C0))); + assert!(effects.contains(&Effect::RecoverComponent(C0))); assert!(effects.contains(&Effect::ReleaseReset(C1))); } @@ -560,7 +592,7 @@ // C1 must never be released. assert!(!effects.contains(&Effect::ReleaseReset(C1))); // Recovery IS attempted before C1 is classified and held. - assert!(effects.contains(&Effect::RestoreGoldenImage(C1))); + assert!(effects.contains(&Effect::RecoverComponent(C1))); assert!(effects.contains(&Effect::AssertReset(C1))); assert!(!effects.contains(&Effect::LatchLockdown)); } @@ -586,7 +618,7 @@ ); assert_eq!(state, State::Ready); assert!(!effects.contains(&Effect::ReleaseReset(C1))); - assert!(effects.contains(&Effect::RestoreGoldenImage(C1))); + assert!(effects.contains(&Effect::RecoverComponent(C1))); assert!(effects.contains(&Effect::AssertReset(C1))); } @@ -608,7 +640,7 @@ ); assert_eq!(state, State::Ready); assert!(effects.contains(&Effect::AssertReset(C1))); - assert!(!effects.contains(&Effect::RestoreGoldenImage(C1))); + assert!(!effects.contains(&Effect::RecoverComponent(C1))); assert!(!effects.contains(&Effect::LatchLockdown)); } @@ -708,7 +740,7 @@ ], ); assert_eq!(state, State::Recovering(C0)); - assert!(effects.contains(&Effect::RestoreGoldenImage(C0))); + assert!(effects.contains(&Effect::RecoverComponent(C0))); } /// Runtime corruption of a `Cascading` component must gate the whole @@ -737,7 +769,7 @@ assert!(effects.contains(&Effect::AssertReset(C1))); assert!(effects.contains(&Effect::AssertReset(C2))); // No recovery is started for a non-required corruption. - assert!(!effects.contains(&Effect::RestoreGoldenImage(C1))); + assert!(!effects.contains(&Effect::RecoverComponent(C1))); assert!(!effects.contains(&Effect::LatchLockdown)); } @@ -829,7 +861,7 @@ assert!(effects.contains(&Effect::ReportIsolated(C2))); assert!(effects.contains(&Effect::ReportIsolated(C3))); // A non-required cascade never enters recovery or lockdown. - assert!(!effects.contains(&Effect::RestoreGoldenImage(C1))); + assert!(!effects.contains(&Effect::RecoverComponent(C1))); assert!(!effects.contains(&Effect::LatchLockdown)); } @@ -852,7 +884,7 @@ ); assert_eq!(state, State::Ready); assert!(effects.contains(&Effect::ReportIsolated(C1))); - assert!(!effects.contains(&Effect::RestoreGoldenImage(C1))); + assert!(!effects.contains(&Effect::RecoverComponent(C1))); } /// A `Required` component whose recovery is exhausted is named in a @@ -900,7 +932,7 @@ ], ); assert_eq!(state, State::Ready); - assert!(effects.contains(&Effect::RestoreGoldenImage(C0))); + assert!(effects.contains(&Effect::RecoverComponent(C0))); assert!( !effects.iter().any(|e| matches!( e, @@ -955,7 +987,7 @@ &[BOOT, Event::VerificationFailed(C0)], ); assert_eq!(state, State::Recovering(C0)); - assert!(effects.contains(&Effect::RestoreGoldenImage(C0))); + assert!(effects.contains(&Effect::RecoverComponent(C0))); // Component must never be released when its eRoT check failed. assert!(!effects.contains(&Effect::ReleaseReset(C0))); } @@ -974,7 +1006,7 @@ ], ); assert_eq!(state, State::Ready); - assert!(effects.contains(&Effect::RestoreGoldenImage(C0))); + assert!(effects.contains(&Effect::RecoverComponent(C0))); // ReleaseReset only after the recovery re-walk passes. assert!(effects.contains(&Effect::ReleaseReset(C0))); } @@ -995,7 +1027,7 @@ ], ); assert_eq!(state, State::Recovering(C1)); - assert!(effects.contains(&Effect::RestoreGoldenImage(C1))); + assert!(effects.contains(&Effect::RecoverComponent(C1))); assert!(!effects.contains(&Effect::ReleaseReset(C1))); } @@ -1015,7 +1047,7 @@ ], ); assert_eq!(state, State::Recovering(C0)); - assert!(effects.contains(&Effect::RestoreGoldenImage(C0))); + assert!(effects.contains(&Effect::RecoverComponent(C0))); } /// CorruptionDetected while in Updating (required component) → Recovering @@ -1032,7 +1064,7 @@ ], ); assert_eq!(state, State::Recovering(C0)); - assert!(effects.contains(&Effect::RestoreGoldenImage(C0))); + assert!(effects.contains(&Effect::RecoverComponent(C0))); } /// Concurrent faults: corruption of a *different* component arriving while the @@ -1055,9 +1087,9 @@ ], ); assert_eq!(state, State::Recovering(C2)); - // Both recovery episodes kicked off a golden-image restore. - assert!(effects.contains(&Effect::RestoreGoldenImage(C1))); - assert!(effects.contains(&Effect::RestoreGoldenImage(C2))); + // Both recovery episodes kicked off a recovery. + assert!(effects.contains(&Effect::RecoverComponent(C1))); + assert!(effects.contains(&Effect::RecoverComponent(C2))); assert!(!effects.contains(&Effect::LatchLockdown)); } @@ -1145,7 +1177,7 @@ assert_eq!(state, State::Ready); assert!(effects.contains(&Effect::ActivateUpdate)); assert!(!effects.contains(&Effect::DiscardStaged)); - assert!(!effects.contains(&Effect::RestoreGoldenImage(C0))); + assert!(!effects.contains(&Effect::RecoverComponent(C0))); } /// The anti-rollback floor is committed only on a proven-healthy boot, never @@ -1184,6 +1216,91 @@ assert!(confirmed.contains(&Effect::CommitSvnFloor(C0))); } +/// Commit-or-lock watchdog: while the activated-but-not-committed window is +/// open (update activated, `BootConfirmed` not yet seen), a `CommitTimeout` +/// fails closed — the machine latches `Locked` rather than leaving the +/// downgrade window open indefinitely, and never commits the unproven image. +#[test] +fn commit_timeout_while_pending_latches_locked() { + let (effects, state) = drive( + passive_required(&[C0]), + &[ + BOOT, + Event::VerificationPassed(C0), + Event::UpdateRequest, + Event::UpdateVerified, + // Window open: activated, awaiting BootConfirmed. Watchdog fires. + Event::CommitTimeout, + ], + ); + assert_eq!(state, State::Locked); + assert!(effects.contains(&Effect::ActivateUpdate)); + assert!(effects.contains(&Effect::LatchLockdown)); + // The floor was never advanced for the unproven image. + assert!(!effects.contains(&Effect::CommitSvnFloor(C0))); +} + +/// Once `BootConfirmed` has committed the floor the window is closed, so a +/// later (stale) `CommitTimeout` is a no-op: the machine stays `Ready` and does +/// not lock. +#[test] +fn commit_timeout_after_confirm_is_stale_noop() { + let (effects, state) = drive( + passive_required(&[C0]), + &[ + BOOT, + Event::VerificationPassed(C0), + Event::UpdateRequest, + Event::UpdateVerified, + Event::BootConfirmed(C0), + // Window already closed by the commit above. + Event::CommitTimeout, + ], + ); + assert_eq!(state, State::Ready); + assert!(effects.contains(&Effect::CommitSvnFloor(C0))); + assert!(!effects.contains(&Effect::LatchLockdown)); +} + +/// A `CommitTimeout` in steady-state `Ready` with no update in flight (no +/// window open) is stale and ignored — a spurious watchdog fire must not lock a +/// healthy device. +#[test] +fn commit_timeout_without_pending_is_ignored() { + let (effects, state) = drive( + passive_required(&[C0]), + &[BOOT, Event::VerificationPassed(C0), Event::CommitTimeout], + ); + assert_eq!(state, State::Ready); + assert!(!effects.contains(&Effect::LatchLockdown)); +} + +/// A recovery that intervenes during the commit window voids it: after the +/// machine recovers and walks back to `Ready`, the window is closed, so a +/// `CommitTimeout` no longer locks. This pins that entering `Recovering` clears +/// `pending_commit`, so the flag cannot go stale across a recovery round-trip. +#[test] +fn recovery_clears_commit_window() { + let (effects, state) = drive( + passive_required(&[C0]), + &[ + BOOT, + Event::VerificationPassed(C0), + Event::UpdateRequest, + Event::UpdateVerified, + // Window open, then a Required corruption preempts to recovery. + Event::CorruptionDetected(C0), + // Restore succeeds (retry < MAX_RETRY) and re-walk re-verifies. + Event::Restored(C0), + Event::VerificationPassed(C0), + // Back in Ready with the window voided; the watchdog is now stale. + Event::CommitTimeout, + ], + ); + assert_eq!(state, State::Ready); + assert!(!effects.contains(&Effect::LatchLockdown)); +} + /// Locked is a terminal state: no effects are produced in response to any /// event after the machine latches. #[test] @@ -1244,7 +1361,7 @@ assert!(!effects.contains(&Effect::ReleaseReset(C0))); assert!(effects.contains(&Effect::ReleaseReset(C1))); // Recovery IS attempted before C0 is classified and held. - assert!(effects.contains(&Effect::RestoreGoldenImage(C0))); + assert!(effects.contains(&Effect::RecoverComponent(C0))); assert!(effects.contains(&Effect::AssertReset(C0))); } @@ -1452,7 +1569,7 @@ } /// A failed recovery actuation is fail-closed too: if the shell cannot even -/// restore a required component's golden image, the platform latches rather +/// recover a required component, the platform latches rather /// than continuing with an unrecovered component. #[test] fn failed_restore_actuation_latches_lockdown() { @@ -1460,10 +1577,10 @@ c.push((C0, ComponentAttrs::passive_required())).unwrap(); let mut orch = Orchestrator::<CAPACITY, ECAP>::new(c.try_into().expect("valid chain"), MAX_RETRY); - let mut plat = FailOn::new(Effect::RestoreGoldenImage(C0)); + let mut plat = FailOn::new(Effect::RecoverComponent(C0)); orch.dispatch(&mut plat, BOOT); - orch.dispatch(&mut plat, Event::VerificationFailed(C0)); // → Recovering → RestoreGoldenImage(C0) fails + orch.dispatch(&mut plat, Event::VerificationFailed(C0)); // → Recovering → RecoverComponent(C0) fails assert!(plat.failed); assert_eq!(orch.state(), State::Locked);