Orchestrator spec and reference impl - #357
Conversation
|
Do you plan to realize your state machine implementation based on https://github.com/typeconstructor/rot-reducer? |
|
@rusty1968 Should we also add multi-level cascades, concurrent faults, and double-fault recovery tests? Or is this not necessary? AnswerAll three are already covered: multi-level cascade (cascading_runtime_corruption_cascades_transitively), concurrent fault (corruption_while_recovering_retargets_to_new_component, corruption_during_update_discards_staged), and double fault (retry_cap_self_latches_via_emit). Happy to add a specific scenario these miss. |
|
Are we trying to restore the golden image several times or just once? AnswerThe state machine isn't aware of the source — note the effect is named RecoverComponent, not "restore golden image." Golden vs A/B vs last-known-good is the platform driver's call. The reducer only decides how many attempts: up to max_retry per component (set 1 for once). |
|
Even if we only ever update one device at a time, §5.3.3 background polling can report corruption of device B while device A is mid-update or mid-recovery. What does the machine do then? |
|
Maybe each device should have their own state machine? |
Excellent question - The scenario is covered by tests:
|
Your intuition is directionally correct — but what genuinely matters here is per-device state , and the design already carries it (ComponentStatus, retry_budget, isolation markers,etc). What stays singular is the ordering: we only fix one device at a time, and we only update one device at a time. The machine keeps a single "who am I fixing right now" slot (State::Recovering(id)), so it can't be pointed at two devices at once. That one-at-a-time behavior is on purpose, not a limitation we ran into. |
This comment was marked as resolved.
This comment was marked as resolved.
What happens if we get another update firmware request for device B while we are fixing the firmware if device A? |
TL;DRThe That drop is deliberate as a policy (recovery outranks update — "fix first, then update"), but the silence is the part worth a second look: the requester cannot tell "refused, busy recovering" apart from "lost." CSA AlignmentThe CSA mandates that failures be reported and models updates as answered requests, which is exactly the gap I am fixing with 57fedb0 This is a sketch of how we use the report effect. impl Platform for PlatformAst {
fn execute(&mut self, effect: Effect) -> Result<(), EffectError> {
match effect {
// ... device I/O effects (ReadFirmware, ReleaseReset, ...) ...
Effect::ReportIsolated(id) => self.mgmt.report_isolated(id),
Effect::ReportRecoveryFailed(id) => self.mgmt.report_recovery_failed(id),
// Pure protocol reply — no device I/O. Answer the pending PLDM
// FW-update request so the requester gets a definite "busy, retry".
Effect::ReportUpdateDeferred =>
self.pldm.respond_update(PldmCompletion::RetryLater),
// ...
}
}
}The CSA does not mandate update-vs-recovery arbitration. Recovery is described as autonomous and separate from update; the interaction of an in-flight recovery with an incoming update is left to the implementation. Our single-flight, recovery-priority choice is therefore a legitimate implementation-defined policy, neither required nor forbidden by the CSA. |
Add two new documents — a narrative walkthrough of the orchestrator state machine (orchestrator-sm-walkthru.md) and a reference transitions table (orchestrator-sm-transitions.md) — and expand the existing state machine document with superstate handler descriptions. Register both new pages in SUMMARY.md.
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.
Replace hardcoded EFFECT_CAP=8 with a per-machine capacity E on Rot/ Orchestrator/Sink, floored at compile time to E >= N+2 (worst case: a full cascade of N AssertResets plus the PreSupervision entry's two effects land in one Sink). emit now panics on overflow instead of silently dropping security-critical effects.
Replace the single global retry_count with a per-component retries map so interleaved recoveries don't share a budget. Clear a component's streak on successful verification and on gating; clear all on Ready.
Extract the unit test module into src/tests.rs via mod tests; and add it to the library srcs. No behavior change; lib.rs drops to ~800 lines.
Introduce Chain<N> with a TryFrom that validates the chain of trust at the boundary (non-empty, unique ids, depends_on exists and is strictly earlier, length fits u8). Orchestrator/Rot::new take Chain<N> instead of a raw heapless::Vec, so the storage representation is no longer part of the public constructor. Add ChainError tests.
Boot-progress watchdog timeouts now enter recovery for the awaited component; stale/spurious timeouts are dropped (INV9). Adds three tests.
…Restored A Required corruption that preempts an in-flight update now emits DiscardStaged so the staged image is not orphaned. Recovering ignores a Restored whose id is not the current recovery target, preventing a displaced episode from mis-crediting the wrong component. Adds two tests.
Replace the gated set and retries map with a single statuses array parallel to chain, each entry a ComponentLifecycle (Nominal/Isolated) plus a retry count. Add status_index/gate_one helpers; rewrite is_gated, bump_retry, clear_retry, gate_by_policy, cascade_hold, and the Ready entry action to index into statuses. State enum and behavior unchanged.
…ress supervision-contract essay
Add BootConfirmed event + CommitSvnFloor effect; Ready emits the commit only on a proven-healthy boot, decoupling the anti-rollback floor advance from ActivateUpdate. Conforms to CSA resiliency: the floor advances after a higher-SVN image successfully boots and is deemed stable.
Track boot-progress per component instead of watching only Active devices. Passive components released from reset are now watched for liveness (new Booted event) under the same watchdog as Active ComponentReady; a released component that misses its window is recovered from any state. Timeout is handled uniformly in the supervisor and PreSupervision; the AwaitingReady-only Timeout arm is removed.
57fedb0 to
50989bd
Compare
|
@rusty1968 Should we add those test?: /// INV9: the iRoT gate must hold even when the eRoT walk finishes first.
/// C0's `ComponentReady` is still outstanding when the last verdict
/// arrives; completing the walk must not bypass the gate.
#[test]
fn chain_exhaustion_does_not_bypass_irot_gate() {
let (_effects, state) = drive(
chain(&[
(C0, ComponentAttrs::active_required()),
(C1, ComponentAttrs::passive_required()),
]),
&[
BOOT,
Event::VerificationPassed(C0), // C0 released; its iRoT still owes ComponentReady
Event::VerificationPassed(C1), // walk done — but C0 never reported
],
);
assert_eq!(state, State::AwaitingReady(Some(C0)));
}/// Trust invariant: a verdict only means something for the component under
/// verification. A replayed `VerificationPassed` for a component that has
/// since been isolated must not re-release it.
#[test]
fn replayed_verdict_does_not_release_isolated_component() {
let (effects, state) = drive(
chain(&[
(C0, ComponentAttrs::passive_isolable()),
(C1, ComponentAttrs::passive_required()),
]),
&[
BOOT,
Event::VerificationPassed(C0), // released; walk moves to C1
Event::CorruptionDetected(C0), // Isolable → AssertReset + Isolated
Event::VerificationPassed(C0), // replayed / spurious verdict
],
);
let releases_c0 = effects
.iter()
.filter(|&&e| e == Effect::ReleaseReset(C0))
.count();
assert_eq!(releases_c0, 1, "isolated component was re-released");
assert_ne!(state, State::Ready, "C1's verdict is still outstanding");
}/// A healthy Active component must not be dragged into recovery by a stale
/// boot-progress watchdog. After a re-walk, C0's `ComponentReady` arrives
/// while the machine is back in `PreSupervision`; the report must clear its
/// watchdog (as `Booted` does there), making the later `Timeout` stale.
#[test]
fn component_ready_during_rewalk_clears_watchdog() {
let (effects, state) = drive(
chain(&[
(C0, ComponentAttrs::active_required()),
(C1, ComponentAttrs::passive_required()),
]),
&[
BOOT,
Event::VerificationPassed(C0), // C0 released, watchdog armed
Event::VerificationFailed(C1), // C1 fails → Recovering(C1)
Event::Restored(C1), // restore ok → re-walk from top
Event::ComponentReady(C0), // C0's iRoT reports in — discarded
Event::Timeout(C0), // stale watchdog fires
],
);
assert!(
!effects.contains(&Effect::RestoreGoldenImage(C0)),
"healthy, ready C0 was sent to recovery by a stale watchdog"
);
assert_ne!(state, State::Recovering(C0));
}Answers1.
|
Add architecture diagram and narrative to the Platform Boundary section describing the Event/Effect boundary and mechanism-neutral effects.
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].
- model: add §5 (recovery re-boot / at-rest re-verify) and §6 reset-must-hold note - machine: fill the INV8 catalogue gap with verify-before-release (property test) - model/overview: rename the 'platform shell' concept to 'platform' throughout
…rdicts - quiesce_all: assert reset on every live component before a recovery re-walk, so recovery is a genuine platform re-boot and no live component is re-verified while running (closes the TOCTOU on already-released siblings) - VerificationPassed: release only the component under verification (chain[cursor]); drop stale/out-of-turn verdicts, mirroring the INV9 guard on ComponentReady - tests: quiesce coverage (multi-sibling, at-rest re-verify), plus a property test (INV8) asserting verify-before-release across random event sequences
Cover the downstream firmware-update lifecycle with host tests that assert only on the device's externally visible signals — reset transitions, what was flashed while the device was held, which slot it booted, what got committed: - update success: the device is held in reset the whole time firmware is written (programs_while_running == 0), rebooted on the new slot, and committed only after the booted state was observed (commit_preceded_by_ready). - failed firmware write: the device's broken write path surfaces through the flash seam; nothing is armed or committed and the device comes back up on its old slot, target slot untouched. - corrupt staged image: rejected during staging, staged bytes discarded, the running device never disturbed. - trial-boot timeout: no boot evidence within the window rolls the trial back and reboots the still-committed old slot. - negative control: a deliberately careless driver that flashes a live device and commits without boot evidence is caught by the instrumentation — proof the positive assertions aren't vacuous while the drivers are still test-local. stage_update/activate_update are the test-local reference rendering of the trial-boot protocol from the BootControl docs (hold, flash, arm trial, release, observe, commit/rollback). They are the executable spec for the orchestrator from PR OpenPRoT#357: when it merges, the real state machine replaces these drivers and the assertions stay. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast <christina.quast@9elements.com>
Cover the downstream firmware-update lifecycle with host tests that assert only on the device's externally visible signals — reset transitions, what was flashed while the device was held, which slot it booted, what got committed: - update success: the device is held in reset the whole time firmware is written (programs_while_running == 0), rebooted on the new slot, and committed only after the booted state was observed (commit_preceded_by_ready). - failed firmware write: the device's broken write path surfaces through the flash seam; nothing is armed or committed and the device comes back up on its old slot, target slot untouched. - corrupt staged image: rejected during staging, staged bytes discarded, the running device never disturbed. - trial-boot timeout: no boot evidence within the window rolls the trial back and reboots the still-committed old slot. - negative control: a deliberately careless driver that flashes a live device and commits without boot evidence is caught by the instrumentation — proof the positive assertions aren't vacuous while the drivers are still test-local. stage_update/activate_update are the test-local reference rendering of the trial-boot protocol from the BootControl docs (hold, flash, arm trial, release, observe, commit/rollback). They are the executable spec for the orchestrator from PR OpenPRoT#357: when it merges, the real state machine replaces these drivers and the assertions stay. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast <christina.quast@9elements.com>
Host tests asserting only on externally visible signals: successful update (device held in reset while flashed, committed only after observed boot), failed firmware write, corrupt staged image, trial timeout rollback, and a negative control proving the instrumentation catches a careless driver. stage_update/activate_update are test-local stand-ins for the PR OpenPRoT#357 orchestrator: when it merges, the drivers go and the assertions stay. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast <christina.quast@9elements.com>
No description provided.