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
86 changes: 86 additions & 0 deletions services/orchestrator/driver/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,92 @@ impl<S: core::error::Error + 'static, F: core::error::Error + 'static> core::err
}
}

/// Advances the eRoT's own anti-rollback floor to the SVN of its confirmed
/// self-update, then closes the session.
///
/// Call this when the update agent sends UpdateSecurityRevision, which the FD
/// reports as `SvnCommitPending`. The caller answers the agent with what comes
/// back: done on `Ok`, refused on `Err`. Activating an image never moves the
/// floor. It moves only here, when the update agent asks after a trial boot
/// was confirmed.
///
/// Refuses unless the session says confirmed and is still open. With no
/// confirmed session, nothing has run the image this SVN belongs to, so the
/// floor stays where it is.
///
/// Asking twice is safe. If the eRoT crashes after the floor moved but before
/// the session closed, the next UpdateSecurityRevision advances the floor to
/// the same value again, which changes nothing, and closes the session. Once
/// the session is closed, any further one gets
/// [`CommitFloorError::NotConfirmed`]: an answer lost on the way back means
/// the agent's retry is refused even though the floor is already at the SVN
/// it asked for.
///
/// If the floor cannot be written, the session stays open, so the next
/// UpdateSecurityRevision runs the whole thing again and the advance is not
/// lost. The update agent is the only thing that retries; nothing here does.
///
/// Takes the session and the floor as arguments, as [`settle_self_update`]
/// does. Neither is wired into the board yet.
pub fn commit_self_svn_floor<S: SelfUpdate, F: SvnFloor>(
session: &mut S,
floor: &mut F,
) -> Result<(), CommitFloorError<S::Error, F::Error>> {
let state = session.state().map_err(CommitFloorError::Session)?;
let running = session.running().map_err(CommitFloorError::Session)?;
let TrialOutcome::ConfirmedUncommitted { svn } =
orchestrator_capabilities::trial_outcome(state, running)
else {
return Err(CommitFloorError::NotConfirmed(state));
};
floor.advance(svn).map_err(CommitFloorError::Floor)?;
session.complete().map_err(CommitFloorError::Session)
}

/// Why [`commit_self_svn_floor`] could not advance the floor.
///
/// Keeps the session's or the floor's own error inside instead of flattening
/// both into one code, the same as [`SettleError`] does.
#[derive(Debug, PartialEq, Eq)]
pub enum CommitFloorError<S, F> {
/// Reading or writing the session failed.
Session(S),
/// Writing the eRoT's own anti-rollback floor failed.
Floor(F),
/// No confirmed self-update is waiting for the floor, so there is no SVN
/// an image has proven itself at. Carries what the session says: `Idle`
/// when no self-update is in flight or the request arrived a second time,
/// `TrialPending` when it arrived before the trial boot was judged.
NotConfirmed(SelfUpdateState),
}

impl<S: core::fmt::Display, F: core::fmt::Display> core::fmt::Display for CommitFloorError<S, F> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
CommitFloorError::Session(err) => write!(f, "self-update session: {err}"),
CommitFloorError::Floor(err) => write!(f, "self-update floor: {err}"),
CommitFloorError::NotConfirmed(state) => {
write!(
f,
"no confirmed self-update to commit the floor to: session reads {state:?}"
)
}
}
}
}

impl<S: core::error::Error + 'static, F: core::error::Error + 'static> core::error::Error
for CommitFloorError<S, F>
{
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
CommitFloorError::Session(err) => Some(err),
CommitFloorError::Floor(err) => Some(err),
CommitFloorError::NotConfirmed(_) => None,
}
}
}

/// The connection between an update frontend and the SM: called (by the
/// event loop, on the frontend's behalf) once a complete candidate for
/// `target` sits in the staging region. Records the job first, then injects
Expand Down
4 changes: 2 additions & 2 deletions services/orchestrator/driver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ pub use board::{
Board, BoardCapabilities, ImageSource, Report, ReportSink, SvnFloorBinding, Verdict, Verifier,
};
pub use driver::{
bring_up, request_update, settle_self_update, BootWalkPoll, DriverError, PlatformDriver,
SelfUpdateSettlement, SettleError, UpdatePoll,
bring_up, commit_self_svn_floor, request_update, settle_self_update, BootWalkPoll,
CommitFloorError, DriverError, PlatformDriver, SelfUpdateSettlement, SettleError, UpdatePoll,
};
109 changes: 109 additions & 0 deletions services/orchestrator/driver/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2220,3 +2220,112 @@ fn settle_reports_an_unclaimed_image_and_reverts_the_session() {

assert_eq!(session.state, SelfUpdateState::Idle);
}

// The ordinary case. The trial boot was confirmed, so the floor moves up to
// the SVN the session recorded and the session ends.
#[test]
fn commit_advances_the_floor_and_closes_the_session() {
let mut session = MockSelfUpdate::with_session(
SelfUpdateState::Committed { svn: Svn(7) },
RunningImage::Trial,
);
let mut floor = MockFloor::with_floor(Svn(3));

assert_eq!(commit_self_svn_floor(&mut session, &mut floor), Ok(()));

assert_eq!(floor.floor(), Ok(Svn(7)));
assert_eq!(session.state, SelfUpdateState::Idle);
}

// There is no session, so no image has run at any SVN. The floor stays where
// it was and the caller tells the update agent no.
#[test]
fn commit_without_a_session_is_refused() {
let mut session = MockSelfUpdate::idle();
let mut floor = MockFloor::with_floor(Svn(3));

assert_eq!(
commit_self_svn_floor(&mut session, &mut floor),
Err(CommitFloorError::NotConfirmed(SelfUpdateState::Idle))
);

assert_eq!(floor.floor(), Ok(Svn(3)));
}

// The trial is still running, so nothing has judged it yet. The floor waits
// for the boot that does.
#[test]
fn commit_during_a_trial_is_refused() {
let pending = SelfUpdateState::TrialPending { svn: Svn(7) };
let mut session = MockSelfUpdate::with_session(pending, RunningImage::Trial);
let mut floor = MockFloor::with_floor(Svn(3));

assert_eq!(
commit_self_svn_floor(&mut session, &mut floor),
Err(CommitFloorError::NotConfirmed(pending))
);

assert_eq!(floor.floor(), Ok(Svn(3)));
}

// The eRoT crashed after the floor moved to 7 but before the session closed.
// The update agent asks again: advancing to 7 a second time changes nothing,
// and this run closes the session. A third request has no session left to
// commit, so it is refused.
#[test]
fn commit_repeated_after_a_crash_finishes_the_close() {
let mut session = MockSelfUpdate::with_session(
SelfUpdateState::Committed { svn: Svn(7) },
RunningImage::Trial,
);
let mut floor = MockFloor::with_floor(Svn(7));

assert_eq!(commit_self_svn_floor(&mut session, &mut floor), Ok(()));

assert_eq!(floor.floor(), Ok(Svn(7)));
assert_eq!(session.state, SelfUpdateState::Idle);
assert_eq!(
commit_self_svn_floor(&mut session, &mut floor),
Err(CommitFloorError::NotConfirmed(SelfUpdateState::Idle)),
"the session is closed, so the floor cannot move again"
);
assert_eq!(floor.floor(), Ok(Svn(7)));
}

// Writing the floor fails. The session has to stay as it was, so the update
// agent's next request can advance the floor instead of the advance being
// lost.
#[test]
fn commit_with_a_floor_that_cannot_advance_leaves_the_session_open() {
let committed = SelfUpdateState::Committed { svn: Svn(7) };
let mut session = MockSelfUpdate::with_session(committed, RunningImage::Trial);

assert_eq!(
commit_self_svn_floor(&mut session, &mut MockFloor::faulting()),
Err(CommitFloorError::Floor(FloorFaultInjected))
);

assert_eq!(
session.state, committed,
"the floor still owes this SVN, so the agent's next request can retry"
);
}

// The session cannot be read, so nothing says which SVN to advance to.
// Refuse and leave the floor alone rather than guess.
#[test]
fn commit_with_an_unreadable_session_fails_secure() {
let mut session = MockSelfUpdate::with_session(
SelfUpdateState::Committed { svn: Svn(7) },
RunningImage::Trial,
);
session.fail = true;
let mut floor = MockFloor::with_floor(Svn(3));

assert_eq!(
commit_self_svn_floor(&mut session, &mut floor),
Err(CommitFloorError::Session(SelfSessionFault))
);

assert_eq!(floor.floor(), Ok(Svn(3)));
}
Loading