Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions services/orchestrator/driver/src/board.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ pub trait BoardCapabilities {
/// survives reset and power loss, otherwise a power cycle would
/// re-admit images below the floor.
type SvnFloor: SvnFloor;
/// Where reports go. `()` for a board with no management side to tell.
type ReportSink: ReportSink;
// Later seams: Recovery, Staging.
}

Expand Down Expand Up @@ -193,6 +195,7 @@ pub enum SvnFloorBinding<F: SvnFloor> {
/// type BootControl = ExtrstGpio; // per-component reset line
/// type BootWatch = CheckpointWalk; // GPIO checkpoint walk over the boot window
/// type SvnFloor = OtpSvnFloor; // fuse-backed anti-rollback floor
/// type ReportSink = MctpReports; // reports out over the management transport
/// }
/// let board = Board::<Ast1060Board, 2> {
/// images: [bmc_image, cpld_image],
Expand All @@ -201,6 +204,7 @@ pub enum SvnFloorBinding<F: SvnFloor> {
/// boot_watches: [bmc_walk, cpld_walk],
/// component_kinds: [ComponentKind::Active, ComponentKind::Passive],
/// svn_floors: [SvnFloorBinding::Erot(bmc_floor), SvnFloorBinding::SelfManaged],
/// report_sink,
/// };
/// ```
pub struct Board<B: BoardCapabilities, const N: usize> {
Expand All @@ -222,5 +226,8 @@ pub struct Board<B: BoardCapabilities, const N: usize> {
/// `svn_floors[i]` says who keeps `ComponentId(i)`'s anti-rollback
/// floor, same indexing as `images`.
pub svn_floors: [SvnFloorBinding<B::SvnFloor>; N],
/// Where the driver hands the SM's reports. One per platform, not one
/// per component: two of the four reports name no component.
pub report_sink: B::ReportSink,
// Later seams add fields, e.g. recovery: [B::Recovery; N].
}
36 changes: 29 additions & 7 deletions services/orchestrator/driver/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

use openprot_orchestrator_sm::{ComponentId, ComponentKind, Effect, EffectError, Event, Platform};

use crate::board::{Board, BoardCapabilities, ImageSource, SvnFloorBinding, Verdict, Verifier};
use crate::board::{
Board, BoardCapabilities, ImageSource, Report, ReportSink, SvnFloorBinding, Verdict, Verifier,
};
use orchestrator_capabilities::{BootControl, BootWatch, Svn, SvnFloor, WalkVerdict};

/// Why the driver could not carry out an effect.
Expand Down Expand Up @@ -232,6 +234,13 @@ impl<B: BoardCapabilities, const N: usize> PlatformDriver<B, N> {
next_deadline_millis,
}
}

/// Hands one report to the board's sink. Cannot fail, so reporting stays
/// off the fail-closed path; reports arrive in the order the SM emitted
/// them.
pub fn report(&mut self, report: Report) {
self.board.report_sink.report(report);
}
}

/// One [`PlatformDriver::poll_boot_walks`] round.
Expand Down Expand Up @@ -259,24 +268,37 @@ impl<B: BoardCapabilities, const N: usize> Platform for PlatformDriver<B, N> {
Effect::ReleaseReset(id) => self.release_reset(id).map(|_| None),
Effect::AssertReset(id) => self.assert_reset(id).map(|_| None),
Effect::CommitSvnFloor(id) => self.commit_svn_floor(id).map(|_| None),
// Reports carry no error, so they never reach the fail-closed
// group below.
Effect::ReportIsolated(id) => {
self.report(Report::Isolated(id));
Ok(None)
}
Effect::ReportRecoveryFailed(id) => {
self.report(Report::RecoveryFailed(id));
Ok(None)
}
Effect::ReportUpdateDeferred => {
self.report(Report::UpdateDeferred);
Ok(None)
}
Effect::ReportUpdateAborted => {
self.report(Report::UpdateAborted);
Ok(None)
}
// No board capability is composed for these seams yet, so they
// fail closed here instead of behind stub methods. Each group
// gains an executor when its capability joins
// [`BoardCapabilities`], as BootControl did above: recovery
// sourcing for RecoverComponent; update staging, authentication
// and trial activation for the update quartet; evidence signing
// for SignAttestation; the management reporting path for the
// Report effects; the terminal latch for LatchLockdown.
// for SignAttestation; the terminal latch for LatchLockdown.
Effect::RecoverComponent { .. }
| Effect::AuthenticateUpdate
| Effect::StageUpdate
| Effect::ActivateUpdate
| Effect::DiscardStaged
| Effect::SignAttestation
| Effect::ReportIsolated(_)
| Effect::ReportRecoveryFailed(_)
| Effect::ReportUpdateDeferred
| Effect::ReportUpdateAborted
| Effect::LatchLockdown => return Err(EffectError),
// Emit is consumed by the orchestrator; receiving one is a
// driver bug.
Expand Down
97 changes: 92 additions & 5 deletions services/orchestrator/driver/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ impl BoardCapabilities for MockBoard {
type BootControl = MockReset;
type BootWatch = MockWalk;
type SvnFloor = MockFloor;
type ReportSink = RecordingSink;
}

fn driver(images: [MemImage; 1]) -> PlatformDriver<MockBoard, 1> {
Expand All @@ -314,6 +315,7 @@ fn driver(images: [MemImage; 1]) -> PlatformDriver<MockBoard, 1> {
boot_watches: [MockWalk::idle()],
component_kinds: [ComponentKind::Passive],
svn_floors: [SvnFloorBinding::Erot(MockFloor::new())],
report_sink: RecordingSink::new(),
})
}

Expand Down Expand Up @@ -401,6 +403,7 @@ fn verifier_fault_fails_closed() {
boot_watches: [MockWalk::idle()],
component_kinds: [ComponentKind::Passive],
svn_floors: [SvnFloorBinding::Erot(MockFloor::new())],
report_sink: RecordingSink::new(),
});

orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned));
Expand Down Expand Up @@ -428,6 +431,7 @@ fn verify_for_a_different_component_is_refused() {
SvnFloorBinding::Erot(MockFloor::new()),
SvnFloorBinding::Erot(MockFloor::new()),
],
report_sink: RecordingSink::new(),
});

driver.stage_firmware(C0).unwrap();
Expand Down Expand Up @@ -468,6 +472,7 @@ fn reset_release_and_assert_reach_the_boot_control() {
boot_watches: [MockWalk::idle()],
component_kinds: [ComponentKind::Passive],
svn_floors: [SvnFloorBinding::Erot(MockFloor::new())],
report_sink: RecordingSink::new(),
});

driver.release_reset(C0).unwrap();
Expand Down Expand Up @@ -505,6 +510,7 @@ fn reset_line_fault_is_reported() {
boot_watches: [MockWalk::idle()],
component_kinds: [ComponentKind::Passive],
svn_floors: [SvnFloorBinding::Erot(MockFloor::new())],
report_sink: RecordingSink::new(),
});

assert_eq!(driver.release_reset(C0), Err(DriverError::BootControlFault));
Expand Down Expand Up @@ -568,6 +574,8 @@ impl BoardCapabilities for WatchBoard {
type BootControl = MockReset;
type BootWatch = MockWalk;
type SvnFloor = MockFloor;
// A board with nothing to tell: exercises the no-op sink.
type ReportSink = ();
}

// The at-rest guarantee end to end: the component is still held while its
Expand All @@ -591,6 +599,7 @@ fn release_follows_verification() {
boot_watches: [MockWalk::idle()],
component_kinds: [ComponentKind::Passive],
svn_floors: [SvnFloorBinding::Erot(MockFloor::new())],
report_sink: (),
});
let mut orch = orchestrator();

Expand Down Expand Up @@ -621,6 +630,7 @@ fn failed_release_fails_closed() {
boot_watches: [MockWalk::idle()],
component_kinds: [ComponentKind::Passive],
svn_floors: [SvnFloorBinding::Erot(MockFloor::new())],
report_sink: RecordingSink::new(),
});
let mut orch = orchestrator();

Expand Down Expand Up @@ -656,6 +666,7 @@ fn walk_driver(
SvnFloorBinding::Erot(MockFloor::new()),
SvnFloorBinding::Erot(MockFloor::new()),
],
report_sink: RecordingSink::new(),
})
}

Expand Down Expand Up @@ -843,6 +854,7 @@ fn booted_walk_settles_in_ready() {
boot_watches: [MockWalk::scripted(std::vec![WalkVerdict::Complete])],
component_kinds: [ComponentKind::Passive],
svn_floors: [SvnFloorBinding::Erot(MockFloor::new())],
report_sink: RecordingSink::new(),
});

orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned));
Expand Down Expand Up @@ -875,6 +887,7 @@ fn boot_timeout_fails_closed_without_recovery() {
}])],
component_kinds: [ComponentKind::Passive],
svn_floors: [SvnFloorBinding::Erot(MockFloor::new())],
report_sink: RecordingSink::new(),
});

orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned));
Expand All @@ -890,18 +903,16 @@ fn boot_timeout_fails_closed_without_recovery() {
// ---------------------------------------------------------------------------
// Report sink.
// ---------------------------------------------------------------------------

/// Records what it is handed: the seam satisfied without a management
/// transport.
/// transport. Tests read `seen` back through `PlatformDriver::board`.
#[derive(Default)]
struct RecordingSink {
seen: std::vec::Vec<Report>,
}

impl RecordingSink {
fn new() -> Self {
Self {
seen: std::vec::Vec::new(),
}
Self::default()
}
}

Expand Down Expand Up @@ -956,6 +967,7 @@ fn commit_advances_the_floor_to_the_verified_svn() {
boot_watches: [MockWalk::idle()],
component_kinds: [ComponentKind::Passive],
svn_floors: [SvnFloorBinding::Erot(MockFloor::new())],
report_sink: RecordingSink::new(),
});

driver
Expand Down Expand Up @@ -992,6 +1004,7 @@ fn commit_without_an_erot_floor_is_a_no_op() {
boot_watches: [MockWalk::idle()],
component_kinds: [ComponentKind::Passive],
svn_floors: [SvnFloorBinding::SelfManaged],
report_sink: RecordingSink::new(),
});

assert_eq!(driver.execute(Effect::CommitSvnFloor(C0)), Ok(None));
Expand Down Expand Up @@ -1023,6 +1036,7 @@ fn rejected_image_clears_the_verified_svn() {
boot_watches: [MockWalk::idle()],
component_kinds: [ComponentKind::Passive],
svn_floors: [SvnFloorBinding::Erot(MockFloor::new())],
report_sink: RecordingSink::new(),
});

driver.stage_firmware(C0).expect("stage failed");
Expand Down Expand Up @@ -1059,10 +1073,83 @@ fn floor_fault_is_reported() {
boot_watches: [MockWalk::idle()],
component_kinds: [ComponentKind::Passive],
svn_floors: [SvnFloorBinding::Erot(mock)],
report_sink: RecordingSink::new(),
});

driver.stage_firmware(C0).expect("stage failed");
driver.verify_firmware(C0).expect("verify failed");

assert_eq!(driver.commit_svn_floor(C0), Err(DriverError::SvnFloorFault));
}

// Every report effect reaches the board's sink, in emission order, and none
// hands back an error for the SM to fail closed on.
#[test]
fn reports_reach_the_board_sink() {
use openprot_orchestrator_sm::{Effect, Platform};

let mut driver = PlatformDriver::<MockBoard, 1>::new(Board {
images: [MemImage::holding(valid_image())],
verifier: XorVerifier {
fault: false,
svn: 0,
},
boot_controls: [MockReset::new()],
boot_watches: [MockWalk::idle()],
component_kinds: [ComponentKind::Passive],
svn_floors: [SvnFloorBinding::Erot(MockFloor::new())],
report_sink: RecordingSink::new(),
});

for effect in [
Effect::ReportIsolated(C0),
Effect::ReportRecoveryFailed(C0),
Effect::ReportUpdateDeferred,
Effect::ReportUpdateAborted,
] {
assert_eq!(driver.execute(effect), Ok(None));
}

assert_eq!(driver.board().report_sink.seen, every_report());
}

// An Isolable component is contained and reported, and the platform keeps
// running: executing a report returns no error, so it never reaches the
// fail-closed path.
#[test]
fn reporting_an_isolated_component_does_not_lock_the_platform() {
let mut driver = PlatformDriver::<MockBoard, 2>::new(Board {
images: [
MemImage::holding(valid_image()),
MemImage::holding(valid_image()),
],
verifier: XorVerifier {
fault: false,
svn: 0,
},
boot_controls: [MockReset::new(), MockReset::new()],
boot_watches: [MockWalk::idle(), MockWalk::idle()],
component_kinds: [ComponentKind::Passive, ComponentKind::Passive],
svn_floors: [
SvnFloorBinding::Erot(MockFloor::new()),
SvnFloorBinding::Erot(MockFloor::new()),
],
report_sink: RecordingSink::new(),
});
let mut chain = heapless::Vec::<_, 2>::new();
chain
.push((C0, ComponentAttrs::passive_required()))
.unwrap();
chain
.push((C1, ComponentAttrs::passive_isolable()))
.unwrap();
let mut orch = Orchestrator::<2, 6>::new(chain.try_into().unwrap(), 3);

orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned));
assert_eq!(orch.state(), State::Ready, "both components verified");

orch.dispatch(&mut driver, Event::CorruptionDetected(C1));

assert_eq!(orch.state(), State::Ready, "contained, not locked");
assert_eq!(driver.board().report_sink.seen, [Report::Isolated(C1)]);
}