Skip to content
Merged
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
1 change: 1 addition & 0 deletions services/orchestrator/adapters/walk/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ rust_library(
deps = [
"//services/orchestrator/capabilities:orchestrator_capabilities",
"//services/orchestrator/config:orchestrator_config",
"//services/orchestrator/sm:orchestrator_sm",
],
)

Expand Down
9 changes: 7 additions & 2 deletions services/orchestrator/adapters/walk/src/walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,19 @@ impl<R: EvidenceReader<P>, P> BootWatch for CheckpointWalk<R, P> {
mod tests {
use super::*;
use core::time::Duration;
use openprot_orchestrator_sm::ComponentAttrs;
use orchestrator_config::DeviceConfig;

const ATTRS: ComponentAttrs = ComponentAttrs::passive_required();

const BL1: BootCheckpoint<u8> = BootCheckpoint::new("bl1", 1, Duration::from_millis(100));
const KERNEL: BootCheckpoint<u8> = BootCheckpoint::new("kernel", 2, Duration::from_millis(200));
const CHECKPOINTS: &[BootCheckpoint<u8>] = &[BL1, KERNEL];

static DEVICE: DeviceConfig<u8, u8> = DeviceConfig::new("test-dev", 0, CHECKPOINTS, None);
static ONE_CP_DEVICE: DeviceConfig<u8, u8> = DeviceConfig::new("one-cp-dev", 0, &[BL1], None);
static DEVICE: DeviceConfig<u8, u8> =
DeviceConfig::new("test-dev", 0, CHECKPOINTS, None, ATTRS);
static ONE_CP_DEVICE: DeviceConfig<u8, u8> =
DeviceConfig::new("one-cp-dev", 0, &[BL1], None, ATTRS);

// A progress-register reader: probe N is Booted once progress >= N.
// Mirrors the SocReader archetype in the evidence tests.
Expand Down
7 changes: 7 additions & 0 deletions services/orchestrator/config/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
rust_library(
name = "orchestrator_config",
srcs = [
"src/chain.rs",
"src/checkpoint.rs",
"src/device.rs",
"src/layout.rs",
Expand All @@ -14,10 +15,16 @@ rust_library(
],
edition = "2024",
visibility = ["//visibility:public"],
deps = [
"//services/orchestrator/sm:orchestrator_sm",
],
)

# Host tests: build on the host platform, no kernel/QEMU.
rust_test(
name = "orchestrator_config_test",
crate = ":orchestrator_config",
deps = [
"@rust_crates//:heapless",
],
)
138 changes: 138 additions & 0 deletions services/orchestrator/config/src/chain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0

//! The views the orchestrator and the platform driver take of the device
//! table. Both are derived here, so the table is the only place a board
//! states what its components are.

use crate::device::DeviceConfig;
use openprot_orchestrator_sm::{ComponentAttrs, ComponentId};

/// The entries `chain_of` validated. The field is private, so the only
/// constructor is the const-validated path and holders can trust the
/// invariants without rechecking.
pub struct ChainEntries<const N: usize>([(ComponentId, ComponentAttrs); N]);

impl<const N: usize> ChainEntries<N> {
/// The validated entries, one per device in table order.
pub const fn entries(&self) -> &[(ComponentId, ComponentAttrs); N] {
&self.0
}
}

/// Table order assigns the ids: `devices[i]` is `ComponentId::new(i)`. The
/// board's per-component arrays are indexed the same way, so the table is
/// also what keeps those arrays lined up with the chain.
///
/// # Panics
///
/// Panics, a build error in const context, if the table is empty or holds
/// more than `u8::MAX` devices (`Chain` takes neither), or if a `depends_on`
/// names anything other than an earlier device in the table.
#[must_use]
pub const fn chain_of<R, P, const N: usize>(devices: &[DeviceConfig<R, P>; N]) -> ChainEntries<N> {
assert!(N > 0, "a device table needs at least one device");
assert!(
N <= u8::MAX as usize,
"a device table holds at most u8::MAX devices"
);
let mut i = 0;
while i < N {
if let Some(on) = devices[i].attrs().depends_on {
let on = on.get() as usize;
assert!(
on < i,
"depends_on must name an earlier device in the table"
);
}
i += 1;
}

// Const arrays need a default; the loop below overwrites every element.
let mut chain = [(ComponentId::new(0), ComponentAttrs::passive_required()); N];
let mut i = 0;
while i < N {
chain[i] = (ComponentId::new(i as u8), devices[i].attrs());
i += 1;
}
ChainEntries(chain)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::checkpoint::BootCheckpoint;
use core::time::Duration;
use openprot_orchestrator_sm::{Chain, FailurePolicy};

const CP: BootCheckpoint<u8> = BootCheckpoint::new("up", 1, Duration::from_millis(10));

const fn dev(attrs: ComponentAttrs) -> DeviceConfig<u8, u8> {
DeviceConfig::new("dev", 0, &[CP], None, attrs)
}

#[test]
fn ids_come_from_table_order() {
let table = [
dev(ComponentAttrs::passive_required()),
dev(ComponentAttrs::active_isolable()),
];
let validated = chain_of(&table);
let entries = validated.entries();

assert_eq!(entries[0].0, ComponentId::new(0));
assert_eq!(entries[1].0, ComponentId::new(1));
assert_eq!(entries[1].1.failure_policy, FailurePolicy::Isolable);
}

#[test]
fn the_derived_chain_is_one_the_machine_accepts() {
let table = [
dev(ComponentAttrs::passive_required()),
dev(ComponentAttrs::active_isolable()),
];
let validated = chain_of(&table);
let entries: heapless::Vec<_, 2> =
heapless::Vec::from_slice(validated.entries()).expect("same length as the table");

assert!(Chain::<2>::try_from(entries).is_ok());
}

#[test]
#[should_panic(expected = "at least one device")]
fn rejects_an_empty_table() {
let table: [DeviceConfig<u8, u8>; 0] = [];
let _ = chain_of(&table);
}

#[test]
#[should_panic(expected = "at most u8::MAX")]
fn rejects_a_table_longer_than_the_chain_takes() {
let table = [dev(ComponentAttrs::passive_required()); 256];
let _ = chain_of(&table);
}

#[test]
#[should_panic(expected = "depends_on must name an earlier device")]
fn rejects_a_dependency_outside_the_table() {
let mut attrs = ComponentAttrs::passive_required();
attrs.depends_on = Some(ComponentId::new(4));
let _ = chain_of(&[dev(attrs)]);
}

#[test]
#[should_panic(expected = "depends_on must name an earlier device")]
fn rejects_a_self_dependency() {
let mut attrs = ComponentAttrs::passive_required();
attrs.depends_on = Some(ComponentId::new(0));
let _ = chain_of(&[dev(attrs)]);
}

#[test]
#[should_panic(expected = "depends_on must name an earlier device")]
fn rejects_a_forward_dependency() {
let mut first = ComponentAttrs::passive_required();
first.depends_on = Some(ComponentId::new(1));
let _ = chain_of(&[dev(first), dev(ComponentAttrs::passive_required())]);
}
}
47 changes: 34 additions & 13 deletions services/orchestrator/config/src/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use crate::checkpoint::BootCheckpoint;
use crate::layout::ImageLayout;
use openprot_orchestrator_sm::ComponentAttrs;

/// Fails the build if `max_retry` is too small for a device to boot every
/// image. Recovery restores one image per attempt, and the orchestrator
Expand Down Expand Up @@ -48,10 +49,9 @@ pub const fn assert_retry_reaches_every_image<R, P>(max_retry: u8, devices: &[De
/// implementation) and its boot-probe vocabulary `P`, for the same
/// reason: probes are board-specific.
///
/// Deliberately says nothing about attestation or commit requirements:
/// those follow from what kind of device this is (iRoT-backed or
/// symbiont, the orchestrator's `ComponentKind`), not from a table
/// setting — a second knob would only let the two disagree.
/// Says nothing about attestation or commit requirements: those follow from
/// what kind of device this is, which `attrs` carries, not from a second
/// table setting that could disagree with it.
///
/// Fields are private so a device entry that violates the schema is
/// unrepresentable: [`new`](Self::new) is the only way in, and it checks.
Expand All @@ -61,6 +61,7 @@ pub struct DeviceConfig<R, P: 'static> {
reset_signal: R,
checkpoints: &'static [BootCheckpoint<P>],
layout: Option<ImageLayout>,
attrs: ComponentAttrs,
}

impl<R, P> DeviceConfig<R, P> {
Expand All @@ -79,6 +80,7 @@ impl<R, P> DeviceConfig<R, P> {
reset_signal: R,
checkpoints: &'static [BootCheckpoint<P>],
layout: Option<ImageLayout>,
attrs: ComponentAttrs,
) -> Self {
assert!(!name.is_empty(), "device name must not be empty");
assert!(
Expand All @@ -102,9 +104,16 @@ impl<R, P> DeviceConfig<R, P> {
reset_signal,
checkpoints,
layout,
attrs,
}
}

/// The orchestrator's per-component policy for this device.
#[must_use]
pub const fn attrs(&self) -> ComponentAttrs {
self.attrs
}

/// The device's name in reports and logs.
#[must_use]
pub const fn name(&self) -> &'static str {
Expand Down Expand Up @@ -182,6 +191,7 @@ mod tests {
/// The golden image, above every slot `slot` can place.
const GOLDEN: Golden = Golden::new(Region::new(0xF000_0000, SLOT_LEN));

const ATTRS: ComponentAttrs = ComponentAttrs::passive_required();
const LAYOUT: ImageLayout = ImageLayout::new(const { &[slot(0), slot(1)] }, Some(GOLDEN));

#[test]
Expand All @@ -192,12 +202,13 @@ mod tests {
0u8,
&[BOOT_COMPLETE, BOOT_COMPLETE_DUPLICATE_NAME],
None,
ATTRS,
);
}

#[test]
fn accepts_a_valid_table() {
let device = DeviceConfig::new("dev", 0u8, &[BOOT_COMPLETE], Some(LAYOUT));
let device = DeviceConfig::new("dev", 0u8, &[BOOT_COMPLETE], Some(LAYOUT), ATTRS);
assert_eq!(device.name(), "dev");
assert_eq!(*device.reset_signal(), 0);
assert_eq!(device.checkpoints().len(), 1);
Expand All @@ -219,28 +230,33 @@ mod tests {
/// included: the eRoT never addresses a byte range for it.
#[test]
fn accepts_a_device_without_a_layout() {
let device = DeviceConfig::new("dev", 0u8, &[BOOT_COMPLETE], None);
let device = DeviceConfig::new("dev", 0u8, &[BOOT_COMPLETE], None, ATTRS);
assert!(device.layout().is_none());
}

#[test]
#[should_panic(expected = "device name must not be empty")]
fn rejects_an_empty_device_name() {
let _ = DeviceConfig::new("", 0u8, &[BOOT_COMPLETE], None);
let _ = DeviceConfig::new("", 0u8, &[BOOT_COMPLETE], None, ATTRS);
}

#[test]
#[should_panic(expected = "at least one boot checkpoint")]
fn rejects_an_empty_checkpoint_list() {
let _ = DeviceConfig::new("dev", 0u8, &[] as &[BootCheckpoint<u8>], None);
let _ = DeviceConfig::new("dev", 0u8, &[] as &[BootCheckpoint<u8>], None, ATTRS);
}

/// Two slots and a golden image need four attempts: three restores
/// plus the one the last restore would otherwise never get.
#[test]
fn accepts_a_retry_budget_that_boots_the_golden_image() {
const DEVICES: &[DeviceConfig<u8, u8>] =
&[DeviceConfig::new("dev", 0, &[BOOT_COMPLETE], Some(LAYOUT))];
const DEVICES: &[DeviceConfig<u8, u8>] = &[DeviceConfig::new(
"dev",
0,
&[BOOT_COMPLETE],
Some(LAYOUT),
ATTRS,
)];
assert_retry_reaches_every_image(4, DEVICES);
}

Expand All @@ -249,16 +265,21 @@ mod tests {
#[test]
fn accepts_any_retry_budget_for_a_device_without_a_layout() {
const DEVICES: &[DeviceConfig<u8, u8>] =
&[DeviceConfig::new("dev", 0, &[BOOT_COMPLETE], None)];
&[DeviceConfig::new("dev", 0, &[BOOT_COMPLETE], None, ATTRS)];
assert_retry_reaches_every_image(0, DEVICES);
}

#[test]
/// Three attempts restore the golden image and stop before booting it.
#[should_panic(expected = "max_retry is too small")]
fn rejects_a_retry_budget_that_never_boots_the_golden_image() {
const DEVICES: &[DeviceConfig<u8, u8>] =
&[DeviceConfig::new("dev", 0, &[BOOT_COMPLETE], Some(LAYOUT))];
const DEVICES: &[DeviceConfig<u8, u8>] = &[DeviceConfig::new(
"dev",
0,
&[BOOT_COMPLETE],
Some(LAYOUT),
ATTRS,
)];
assert_retry_reaches_every_image(3, DEVICES);
}
}
3 changes: 3 additions & 0 deletions services/orchestrator/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@

#![cfg_attr(not(test), no_std)]

pub mod chain;
pub mod checkpoint;
pub mod device;
pub mod layout;
pub mod record;

#[doc(inline)]
pub use chain::{chain_of, ChainEntries};
#[doc(inline)]
pub use checkpoint::BootCheckpoint;
#[doc(inline)]
Expand Down
1 change: 1 addition & 0 deletions services/orchestrator/driver/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ rust_library(
visibility = ["//visibility:public"],
deps = [
"//services/orchestrator/capabilities:orchestrator_capabilities",
"//services/orchestrator/config:orchestrator_config",
"//services/orchestrator/sm:orchestrator_sm",
"//util/io",
"@rust_crates//:heapless",
Expand Down
Loading
Loading