diff --git a/Cargo.lock b/Cargo.lock index 10b8f3e1..251a2012 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -485,7 +485,7 @@ dependencies = [ [[package]] name = "faux-mgs" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "async-trait", diff --git a/faux-mgs/Cargo.toml b/faux-mgs/Cargo.toml index a714ae7d..9e608048 100644 --- a/faux-mgs/Cargo.toml +++ b/faux-mgs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "faux-mgs" -version = "0.1.0" +version = "0.1.1" edition = "2021" license = "MPL-2.0" diff --git a/faux-mgs/src/main.rs b/faux-mgs/src/main.rs index 44ceda4a..ac678d62 100644 --- a/faux-mgs/src/main.rs +++ b/faux-mgs/src/main.rs @@ -19,6 +19,8 @@ use gateway_messages::ComponentAction; use gateway_messages::IgnitionCommand; use gateway_messages::LedComponentAction; use gateway_messages::PowerState; +use gateway_messages::RotBootInfoDisplay; +use gateway_messages::RotStateV2Display; use gateway_messages::SpComponent; use gateway_messages::StartupOptions; use gateway_messages::UpdateId; @@ -178,6 +180,16 @@ enum Command { }, /// Get or set the active slot of a component (e.g., `host-boot-flash`). + /// + /// Except for component "stage0", setting the active slot can be + /// viewed as an atomic operation. + /// + /// Setting "stage0" slot 1 as the active slot initiates a copy from + /// slot 1 to slot 0 if the contents of slot 1 still match those seen + /// at last RoT reset and the contents are properly signed. + /// + /// Power failures during the copy can disable the RoT. Only one stage0 + /// update should be in process in a rack at any time. ComponentActiveSlot { #[clap(value_parser = parse_sp_component)] component: SpComponent, @@ -373,6 +385,13 @@ enum Command { /// Reads the lock status of any VPD in the system VpdLockStatus, + + /// Read the RoT's boot-time information. + RotBootInfo { + /// Return highest version of RotBootInfo less then or equal to given + #[clap(long, short, default_value = "3")] + version: u8, + }, } #[derive(Subcommand, Debug, Clone)] @@ -828,8 +847,11 @@ async fn run_command( lines.push(format!("hubris version: {:?}", state.version)); lines.push(format!("power state: {:?}", state.power_state)); - // TODO: pretty print RoT state? - lines.push(format!("RoT state: {:?}", state.rot)); + match state.rot { + Ok(rotstate) => lines.push(format!("{:?}", rotstate)), + Err(err) => lines.push(format!("RoT state: {:?}", err)), + } + Ok(Output::Lines(lines)) } VersionedSpState::V2(state) => { @@ -857,11 +879,55 @@ async fn run_command( .join(":") )); lines.push(format!("power state: {:?}", state.power_state)); - // TODO: pretty print RoT state? - lines.push(format!("RoT state: {:?}", state.rot)); + match state.rot { + Ok(rotstate) => { + lines.push(format!( + "{}", + &RotStateV2Display(&rotstate) + )); + } + Err(err) => lines.push(format!("RoT state: {:?}", err)), + } Ok(Output::Lines(lines)) } + VersionedSpState::V3(state) => { + lines.push(format!( + "hubris archive: {}", + hex::encode(state.hubris_archive_id) + )); + + lines.push(format!( + "serial number: {}", + zero_padded_to_str(state.serial_number) + )); + lines.push(format!( + "model: {}", + zero_padded_to_str(state.model) + )); + lines.push(format!("revision: {}", state.revision)); + lines.push(format!( + "base MAC address: {}", + state + .base_mac_address + .iter() + .map(|b| format!("{b:02x}")) + .collect::>() + .join(":") + )); + lines.push(format!("power state: {:?}", state.power_state)); + Ok(Output::Lines(lines)) + } + } + } + Command::RotBootInfo { version } => { + let state = sp.rot_state(version).await?; + info!(log, "{state:x?}"); + if json { + return Ok(Output::Json(serde_json::to_value(state).unwrap())); } + let mut lines = Vec::new(); + lines.push(format!("{}", &RotBootInfoDisplay(&state))); + Ok(Output::Lines(lines)) } Command::Ignition { target } => { let mut by_target = BTreeMap::new(); diff --git a/gateway-messages/src/lib.rs b/gateway-messages/src/lib.rs index 664d9380..21ba002e 100644 --- a/gateway-messages/src/lib.rs +++ b/gateway-messages/src/lib.rs @@ -129,7 +129,7 @@ pub enum BadRequestReason { DeserializationError, } -/// Image slot name for SwitchDefaultImage +/// Image slot name for SwitchDefaultImage on component ROT #[derive( Debug, Clone, Copy, PartialEq, Eq, SerializedSize, Serialize, Deserialize, )] @@ -138,6 +138,25 @@ pub enum RotSlotId { B, } +/// Image slot name for SwitchDefaultImage on component STAGE0 +#[derive( + Debug, Clone, Copy, PartialEq, Eq, SerializedSize, Serialize, Deserialize, +)] +pub enum Stage0SlotId { + Stage0, + Stage0Next, +} + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, SerializedSize, Serialize, Deserialize, +)] +pub enum ComponentSlot { + /// Hubris flash slot + Rot(RotSlotId), + /// Bootloader flash slot + Stage0(Stage0SlotId), +} + /// Duration for SwitchDefaultImage #[derive( Debug, Clone, Copy, PartialEq, Eq, SerializedSize, Serialize, Deserialize, diff --git a/gateway-messages/src/mgs_to_sp.rs b/gateway-messages/src/mgs_to_sp.rs index 826b524c..9ce2feb0 100644 --- a/gateway-messages/src/mgs_to_sp.rs +++ b/gateway-messages/src/mgs_to_sp.rs @@ -204,6 +204,11 @@ pub enum MgsRequest { ComponentWatchdogSupported { component: SpComponent, }, + + /// Read RoT boot state at the highest version not to exceed specified version. + VersionedRotBootInfo { + version: u8, + }, } #[derive( diff --git a/gateway-messages/src/sp_impl.rs b/gateway-messages/src/sp_impl.rs index fbffe1e0..3d48ceb0 100644 --- a/gateway-messages/src/sp_impl.rs +++ b/gateway-messages/src/sp_impl.rs @@ -25,6 +25,7 @@ use crate::MgsError; use crate::MgsRequest; use crate::MgsResponse; use crate::PowerState; +use crate::RotBootInfo; use crate::RotRequest; use crate::RotResponse; use crate::RotSlotId; @@ -412,6 +413,13 @@ pub trait SpHandler { &mut self, component: SpComponent, ) -> Result<(), SpError>; + + fn versioned_rot_boot_info( + &mut self, + sender: SocketAddrV6, + port: SpPort, + version: u8, + ) -> Result; } /// Handle a single incoming message. @@ -991,6 +999,10 @@ fn handle_mgs_request( MgsRequest::ComponentWatchdogSupported { component } => handler .component_watchdog_supported(component) .map(|()| SpResponse::ComponentWatchdogSupportedAck), + MgsRequest::VersionedRotBootInfo { version } => { + let r = handler.versioned_rot_boot_info(sender, port, version); + r.map(SpResponse::RotBootInfo) + } }; let response = match result { @@ -1416,6 +1428,15 @@ mod tests { ) -> Result<(), SpError> { unimplemented!() } + + fn versioned_rot_boot_info( + &mut self, + _sender: SocketAddrV6, + _port: SpPort, + _version: u8, + ) -> Result { + unimplemented!() + } } #[cfg(feature = "std")] diff --git a/gateway-messages/src/sp_to_mgs.rs b/gateway-messages/src/sp_to_mgs.rs index 3fbc5bbe..b1bf6735 100644 --- a/gateway-messages/src/sp_to_mgs.rs +++ b/gateway-messages/src/sp_to_mgs.rs @@ -131,6 +131,9 @@ pub enum SpResponse { DisableComponentWatchdogAck, ComponentWatchdogSupportedAck, + + SpStateV3(SpStateV3), + RotBootInfo(RotBootInfo), } /// Identifier for one of of an SP's KSZ8463 management-network-facing ports. @@ -167,6 +170,38 @@ pub struct ImageVersion { pub version: u32, } +#[derive( + Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, SerializedSize, +)] +pub enum ImageError { + /// Image has not been sanity checked (internal use) + Unchecked = 1, + /// First page of image is erased. + FirstPageErased, + /// Some pages in the image are erased. + PartiallyProgrammed, + /// The NXP image offset + length caused a wrapping add. + InvalidLength, + /// The header flash page is erased. + HeaderNotProgrammed, + /// A bootloader image is too short. + BootloaderTooSmall, + /// A required ImageHeader is missing. + BadMagic, + /// The image size in ImageHeader is unreasonable. + HeaderImageSize, + /// total_image_length in ImageHeader is not properly aligned. + UnalignedLength, + /// Some NXP image types are not supported. + UnsupportedType, + /// Wrong format reset vector. + ResetVectorNotThumb2, + /// Reset vector points outside of image execution range. + ResetVector, + /// Signature check on image failed. + Signature, +} + /// This is quasi-deprecated in that it will only be returned by SPs with images /// older than the introduction of `SpStateV2`. #[derive( @@ -202,6 +237,21 @@ pub struct SpStateV2 { pub rot: Result, } +#[derive( + Debug, Clone, Copy, PartialEq, Eq, SerializedSize, Serialize, Deserialize, +)] +pub struct SpStateV3 { + pub hubris_archive_id: [u8; 8], + // Serial and revision are only 11 bytes in practice; we have plenty of room + // so we'll leave the fields wider in case we grow it in the future. The + // values are 0-padded. + pub serial_number: [u8; 32], + pub model: [u8; 32], + pub revision: u32, + pub base_mac_address: [u8; 6], + pub power_state: PowerState, +} + #[derive( Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, SerializedSize, )] @@ -210,6 +260,20 @@ pub struct RotImageDetails { pub version: ImageVersion, } +/// This class exists for faux-mgs to nicely display the Firmware ID (FWID). +#[derive(Clone, Debug)] +#[must_use = "this struct does nothing unless displayed"] +pub struct RotImageDetailsDisplay<'a>(pub &'a RotImageDetails); + +impl<'a> fmt::Display for RotImageDetailsDisplay<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = self.0; + write!(f, "digest: {}, ", &Digest256Display(&s.digest))?; + write!(f, "version: {:?}, ", s.version)?; + Ok(()) + } +} + /// The boot time details dumped by Stage0 into Hubris on the RoT #[derive( Debug, Clone, Copy, PartialEq, Eq, SerializedSize, Serialize, Deserialize, @@ -220,6 +284,39 @@ pub struct RotBootState { pub slot_b: Option, } +/// This class exists for faux-mgs to nicely display the Firmware ID (FWID). +#[derive(Clone, Debug)] +#[must_use = "this struct does nothing unless displayed"] +pub struct RotBootStateDisplay<'a>(pub &'a RotBootState); + +impl<'a> fmt::Display for RotBootStateDisplay<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = self.0; + write!(f, "active: {:?}, ", s.active)?; + match s.slot_a { + Some(details) => { + write!( + f, + "slot_a: Some(RotImageDetails{{ {} }}), ", + &RotImageDetailsDisplay(&details) + )?; + } + None => write!(f, "None, ")?, + }; + match s.slot_b { + Some(details) => { + write!( + f, + "slot_b: Some(RotImageDetails{{ {} }}), ", + &RotImageDetailsDisplay(&details) + )?; + } + None => write!(f, "None, ")?, + }; + Ok(()) + } +} + #[derive( Debug, Clone, Copy, PartialEq, Eq, SerializedSize, Serialize, Deserialize, )] @@ -234,6 +331,23 @@ pub struct RotState { pub rot_updates: RotUpdateDetails, } +/// This class exists for faux-mgs to nicely display the Firmware ID (FWID). +#[derive(Clone, Debug)] +#[must_use = "this struct does nothing unless displayed"] +pub struct RotStateDisplay<'a>(pub &'a RotState); + +impl<'a> fmt::Display for RotStateDisplay<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = self.0.rot_updates.boot_state; + writeln!( + f, + "RotState {{ rot_updates: RotUpdateDetails {{ boot_state: RotBootState {{ {} }} }} }}", + &RotBootStateDisplay(&s) + )?; + Ok(()) + } +} + #[derive( Debug, Clone, Copy, PartialEq, Eq, SerializedSize, Serialize, Deserialize, )] @@ -258,6 +372,219 @@ pub struct RotStateV2 { pub slot_b_sha3_256_digest: Option<[u8; 32]>, } +// Helper for displaying FWIDs from faux-mgs +struct Digest256Display<'a>(&'a [u8; 32]); + +impl<'a> fmt::Display for Digest256Display<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "\"")?; + for b in self.0.iter() { + write!(f, "{:02x}", b)?; + } + write!(f, "\"")?; + Ok(()) + } +} + +/// This class exists for faux-mgs to nicely display the Firmware ID (FWID). +#[derive(Clone, Debug)] +#[must_use = "this struct does nothing unless displayed"] +pub struct RotStateV2Display<'a>(pub &'a RotStateV2); + +impl<'a> fmt::Display for RotStateV2Display<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = self.0; + writeln!(f, "RotStateV2 {{")?; + write!(f, "active: {:?}, ", s.active)?; + write!( + f, + "persistent_boot_preference: {:?}, ", + s.persistent_boot_preference + )?; + write!( + f, + "pending_persistent_boot_preference: {:?}, ", + s.pending_persistent_boot_preference + )?; + write!( + f, + "transient_boot_preference: {:?}, ", + s.transient_boot_preference + )?; + write!(f, "slot_a_sha3_256_digest: ")?; + match s.slot_a_sha3_256_digest { + Some(digest) => { + write!(f, "Some({}), ", &Digest256Display(&digest))?; + } + None => write!(f, "None, ")?, + }; + write!(f, "slot_b_sha3_256_digest: ")?; + match s.slot_b_sha3_256_digest { + Some(digest) => { + write!(f, "Some({}), ", &Digest256Display(&digest))?; + } + None => write!(f, "None, ")?, + }; + write!(f, "}}")?; + Ok(()) + } +} + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, SerializedSize, Serialize, Deserialize, +)] +pub enum Fwid { + Sha3_256([u8; 32]), +} + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, SerializedSize, Serialize, Deserialize, +)] +pub struct RotStateV3 { + /// The slot of the currently running image + pub active: RotSlotId, + /// The persistent boot preference written into the current authoritative + /// CFPA page (ping or pong). + pub persistent_boot_preference: RotSlotId, + /// The persistent boot preference written into the CFPA scratch page that + /// will become the persistent boot preference in the authoritative CFPA + /// page upon reboot, unless CFPA update of the authoritative page fails for + /// some reason. + pub pending_persistent_boot_preference: Option, + /// Override persistent preference selection for a single boot + /// + /// This corresponds to a magic ram value that is cleared by bootleby + pub transient_boot_preference: Option, + /// Sha3-256 Digest of Slot A in Flash + pub slot_a_fwid: Fwid, + /// Sha3-256 Digest of Slot B in Flash + pub slot_b_fwid: Fwid, + /// Sha3-256 Digest of Bootloader in Flash at boot time + pub stage0_fwid: Fwid, + /// Sha3-256 Digest of Staged Bootloader in Flash at boot time + pub stage0next_fwid: Fwid, + + /// Flash Slot A status at last RoT reset + pub slot_a_status: Result<(), ImageError>, + /// Slot B status at last RoT reset + pub slot_b_status: Result<(), ImageError>, + /// Stage0 (bootloader) status at last RoT reset + pub stage0_status: Result<(), ImageError>, + /// Stage0Next status at last RoT reset + pub stage0next_status: Result<(), ImageError>, +} + +#[derive(Clone, Debug)] +#[must_use = "this struct does nothing unless displayed"] +pub struct RotStateV3Display<'a>(pub &'a RotStateV3); + +impl<'a> fmt::Display for RotStateV3Display<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = self.0; + writeln!(f, "RotStateV3 {{")?; + write!(f, "active: {:?}, ", s.active)?; + write!( + f, + "persistent_boot_preference: {:?}, ", + s.persistent_boot_preference + )?; + write!( + f, + "pending_persistent_boot_preference: {:?}, ", + s.pending_persistent_boot_preference + )?; + write!( + f, + "transient_boot_preference: {:?}, ", + s.transient_boot_preference + )?; + match s.slot_a_fwid { + Fwid::Sha3_256(digest) => write!( + f, + "slot_a_fwid: Fwid::Sha3_256({}), ", + &Digest256Display(&digest) + )?, + } + match s.slot_b_fwid { + Fwid::Sha3_256(digest) => write!( + f, + "slot_b_fwid: Fwid::Sha3_256({}), ", + &Digest256Display(&digest) + )?, + } + match s.stage0_fwid { + Fwid::Sha3_256(digest) => write!( + f, + "stage0_fwid: Fwid::Sha3_256({}), ", + &Digest256Display(&digest) + )?, + } + match s.stage0next_fwid { + Fwid::Sha3_256(digest) => write!( + f, + "stage0next_fwid: Fwid::Sha3_256({}), ", + &Digest256Display(&digest) + )?, + } + write!(f, "slot_a_status: {:?}, ", s.slot_a_status)?; + write!(f, "slot_b_status: {:?}, ", s.slot_b_status)?; + write!(f, "stage0_status: {:?}, ", s.stage0_status)?; + write!(f, "stage0next_status: {:?} ", s.stage0next_status)?; + write!(f, "}}")?; + Ok(()) + } +} + +/// `rot_boot_info` and versioned_rot_boot_info` are used to +/// implement backward/forward compatible Hubris update flows. +/// +/// The end goal is to flush out old images from the customer base +/// and spares so that the older APIs can be deprecated and removed. +/// +/// A to-be-implemented rollback-protection feature will keep old +/// versions from being reintroduced. +/// [Issue 222](https://github.com/oxidecomputer/management-gateway-service/issues/222) +/// +/// Until then, the management-gateway-service needs to continue to +/// handle old versions of SP and RoT firmware update flows. +/// +/// MGS will always need to handle SP and RoT version skew during update as +/// well as being exposed to spares loaded with SP and RoT images that are +/// newer than the running MGS version. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, SerializedSize, Serialize, Deserialize, +)] +pub enum RotBootInfo { + V1(RotState), + V2(RotStateV2), + V3(RotStateV3), +} + +#[derive(Clone, Debug)] +#[must_use = "this struct does nothing unless displayed"] +pub struct RotBootInfoDisplay<'a>(pub &'a RotBootInfo); + +impl<'a> fmt::Display for RotBootInfoDisplay<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, " RotBootInfo {{")?; + match self.0 { + RotBootInfo::V1(rotstate) => { + write!(f, " V1({})", &RotStateDisplay(rotstate))?; + } + // Use a helper on V2 to display a human readable FWID + RotBootInfo::V2(rotstate) => { + write!(f, " V2({})", &RotStateV2Display(rotstate))?; + } + // Use helper on V3 to display a human readable FWID + RotBootInfo::V3(rotstate) => { + write!(f, " V3({})", &RotStateV3Display(rotstate))?; + } + } + writeln!(f, "}}")?; + Ok(()) + } +} + /// Metadata describing a single page (out of a larger list) of TLV-encoded /// structures returned by the SP. /// @@ -574,6 +901,9 @@ pub enum SpError { Sensor(SensorError), Vpd(VpdError), Watchdog(WatchdogError), + BlockOutOfOrder, + InvalidSlotIdForOperation, + InvalidComponent, } impl fmt::Display for SpError { @@ -686,6 +1016,15 @@ impl fmt::Display for SpError { Self::Sensor(e) => write!(f, "sensor: {}", e), Self::Vpd(e) => write!(f, "vpd: {}", e), Self::Watchdog(e) => write!(f, "watchdog: {}", e), + Self::BlockOutOfOrder => { + write!(f, "block written out of order") + } + Self::InvalidSlotIdForOperation => { + write!(f, "SlotId parameter is not valid for request") + } + Self::InvalidComponent => { + write!(f, "component is not supported on device") + } } } } @@ -755,6 +1094,13 @@ pub enum UpdateError { Unknown(u32), MissingHandoffData, + BlockOutOfOrder, + InvalidComponent, + InvalidSlotIdForOperation, + InvalidArchive, + ImageMismatch, + SignatureNotValidated, + VersionNotSupported, } impl fmt::Display for UpdateError { @@ -793,6 +1139,27 @@ impl fmt::Display for UpdateError { Self::MissingHandoffData => { write!(f, "boot data not handed off to hubris kernel") } + Self::BlockOutOfOrder => { + write!(f, "update blocks delivered out of order") + } + Self::InvalidSlotIdForOperation => { + write!(f, "specified SlotId is not supported for operation") + } + Self::InvalidArchive => { + write!(f, "invalid archive") + } + Self::ImageMismatch => { + write!(f, "image does not match") + } + Self::SignatureNotValidated => { + write!(f, "image not present or signature not valid") + } + Self::VersionNotSupported => { + write!(f, "RoT boot info version is not supported") + } + Self::InvalidComponent => { + write!(f, "invalid component for operation") + } } } } diff --git a/gateway-messages/tests/versioning/v12.rs b/gateway-messages/tests/versioning/v12.rs index f0abe2ff..90e11604 100644 --- a/gateway-messages/tests/versioning/v12.rs +++ b/gateway-messages/tests/versioning/v12.rs @@ -12,7 +12,12 @@ //! tests can be removed as we will stop supporting v11. use super::assert_serialized; +use gateway_messages::Fwid; +use gateway_messages::ImageError; use gateway_messages::MgsRequest; +use gateway_messages::RotBootInfo; +use gateway_messages::RotSlotId; +use gateway_messages::RotStateV3; use gateway_messages::RotWatchdogError; use gateway_messages::SerializedSize; use gateway_messages::SpComponent; @@ -20,6 +25,8 @@ use gateway_messages::SpError; use gateway_messages::SpResponse; use gateway_messages::WatchdogError; +use gateway_messages::SpStateV3; + #[test] fn sp_response() { let mut out = [0; SpResponse::MAX_SIZE]; @@ -31,6 +38,42 @@ fn sp_response() { let response = SpResponse::ComponentWatchdogSupportedAck; let expected = [43]; assert_serialized(&mut out, &expected, &response); + + let mut out = [0; SpResponse::MAX_SIZE]; + let response = SpResponse::SpStateV3(SpStateV3 { + hubris_archive_id: [1, 2, 3, 4, 5, 6, 7, 8], + serial_number: [ + 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + ], + model: [ + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, + 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, + ], + revision: 0xf0f1f2f3, + base_mac_address: [73, 74, 75, 76, 77, 78], + power_state: gateway_messages::PowerState::A0, + }); + + #[rustfmt::skip] + let expected = vec![ + 44, // SpStateV3 + 1, 2, 3, 4, 5, 6, 7, 8, // hubris_archive_id + + 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, // serial_number + + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, // model + + 0xf3, 0xf2, 0xf1, 0xf0, // revision + 73, 74, 75, 76, 77, 78, // base_mac_address + 0, // power_state + ]; + + assert_serialized(&mut out, &expected, &response); } #[test] @@ -64,6 +107,17 @@ fn host_request() { b's', b'p', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]; assert_serialized(&mut out, &expected, &request); + + let mut out = [0; MgsRequest::MAX_SIZE]; + let request = MgsRequest::VersionedRotBootInfo { version: 3 }; + + #[rustfmt::skip] + let expected = vec![ + 45, // VersionedRotBootInfo + 3, // version + ]; + + assert_serialized(&mut out, &expected, &request); } #[test] @@ -89,3 +143,50 @@ fn watchdog_error() { assert_serialized(&mut out, serialized, &response); } } + +#[test] +fn rot_boot_info_v3() { + let mut out = [0; SpResponse::MAX_SIZE]; + + let response = SpResponse::RotBootInfo(RotBootInfo::V3(RotStateV3 { + active: RotSlotId::A, + persistent_boot_preference: RotSlotId::A, + pending_persistent_boot_preference: Some(RotSlotId::B), + transient_boot_preference: None, + slot_a_fwid: Fwid::Sha3_256([11u8; 32]), + slot_b_fwid: Fwid::Sha3_256([22u8; 32]), + stage0_fwid: Fwid::Sha3_256([33u8; 32]), + stage0next_fwid: Fwid::Sha3_256([44u8; 32]), + slot_a_status: Ok(()), + slot_b_status: Err(ImageError::Signature), + stage0_status: Ok(()), + stage0next_status: Err(ImageError::FirstPageErased), + })); + + #[rustfmt::skip] + let expected = vec![ + 45, 2, // SpResponse::RotBootInfo(RotBootInfo::V3(RotStateV3 { + 0, // active: RotSlotId::A + 0, // persistent_boot_preference: RotSlotId::A + 1, 1, // pending_persistent_boot_preference: Some(RotSlotId::B) + 0, // transient_boot_preference: None + 0, // slot_a_fwid: Fwid::Sha3_256([11u8;32]) + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 0, // slot_b_fwid: Fwid::Sha3_256([22u8;32]) + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 0, // stage0_fwid: Fwid::Sha3_256([33u8;32]) + 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, + 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, + 0, // stage0next_fwid: Fwid::Sha3_256([44u8;32]) + 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, + 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, + 0, // slot_a_status: Ok(()) + 1, 12, // slot_b_status: Err(ImageError::Signature) + 0, // stage0_status: Ok(()) + 1, 1 // stage0next_status: Err(ImageError::FirstPageErased) + ]; + + assert_serialized(&mut out, &expected, &response); +} diff --git a/gateway-sp-comms/src/error.rs b/gateway-sp-comms/src/error.rs index 6e3d1386..420283b6 100644 --- a/gateway-sp-comms/src/error.rs +++ b/gateway-sp-comms/src/error.rs @@ -104,4 +104,12 @@ pub enum UpdateError { CorruptTlvc(String), #[error("failed to send update message to SP")] Communication(#[from] CommunicationError), + #[error("invalid zip archive")] + InvalidArchive, + #[error("invalid slot ID for operation")] + InvalidSlotIdForOperation, + #[error("invalid component for device")] + InvalidComponent, + #[error("an image was not found")] + ImageNotFound, } diff --git a/gateway-sp-comms/src/lib.rs b/gateway-sp-comms/src/lib.rs index f51e7396..440a8d05 100644 --- a/gateway-sp-comms/src/lib.rs +++ b/gateway-sp-comms/src/lib.rs @@ -28,6 +28,7 @@ pub use gateway_messages; pub use gateway_messages::SpComponent; pub use gateway_messages::SpStateV1; pub use gateway_messages::SpStateV2; +pub use gateway_messages::SpStateV3; pub use host_phase2::HostPhase2ImageError; pub use host_phase2::HostPhase2Provider; pub use host_phase2::InMemoryHostPhase2Provider; @@ -70,4 +71,5 @@ pub struct SwitchPortConfig { pub enum VersionedSpState { V1(SpStateV1), V2(SpStateV2), + V3(SpStateV3), } diff --git a/gateway-sp-comms/src/shared_socket.rs b/gateway-sp-comms/src/shared_socket.rs index d6e5d691..c8fb8a9c 100644 --- a/gateway-sp-comms/src/shared_socket.rs +++ b/gateway-sp-comms/src/shared_socket.rs @@ -430,6 +430,7 @@ enum RecvError { // we look up the `SingleSp` instance by the scope ID of the source of the // packet then send it an instance of this enum to handle. #[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] pub(crate) enum SingleSpMessage { HostPhase2Request(HostPhase2Request), SerialConsole { diff --git a/gateway-sp-comms/src/single_sp.rs b/gateway-sp-comms/src/single_sp.rs index b1b3439b..aa3aac6b 100644 --- a/gateway-sp-comms/src/single_sp.rs +++ b/gateway-sp-comms/src/single_sp.rs @@ -36,6 +36,7 @@ use gateway_messages::Message; use gateway_messages::MessageKind; use gateway_messages::MgsRequest; use gateway_messages::PowerState; +use gateway_messages::RotBootInfo; use gateway_messages::RotRequest; use gateway_messages::SensorReading; use gateway_messages::SensorRequest; @@ -391,6 +392,13 @@ impl SingleSp { self.rpc(MgsRequest::SpState).await.and_then(expect_sp_state) } + /// Request the state of the RoT. + pub async fn rot_state(&self, version: u8) -> Result { + self.rpc(MgsRequest::VersionedRotBootInfo { version }) + .await + .and_then(expect_rot_boot_info) + } + /// Request the inventory of the SP. pub async fn inventory(&self) -> Result { let devices = self.get_paginated_tlv_data(InventoryTlvRpc).await?; @@ -592,9 +600,16 @@ impl SingleSp { )); } start_sp_update(&self.cmds_tx, update_id, image, self.log()).await - } else if component == SpComponent::ROT { - start_rot_update(&self.cmds_tx, update_id, slot, image, self.log()) - .await + } else if matches!(component, SpComponent::ROT | SpComponent::STAGE0) { + start_rot_update( + &self.cmds_tx, + update_id, + component, + slot, + image, + self.log(), + ) + .await } else { start_component_update( &self.cmds_tx, diff --git a/gateway-sp-comms/src/single_sp/update.rs b/gateway-sp-comms/src/single_sp/update.rs index 662435c1..5b126132 100644 --- a/gateway-sp-comms/src/single_sp/update.rs +++ b/gateway-sp-comms/src/single_sp/update.rs @@ -25,6 +25,7 @@ use slog::warn; use slog::Logger; use std::convert::TryInto; use std::io::Cursor; +use std::io::Read; use std::time::Duration; use tlvc::TlvcReader; use tokio::sync::mpsc; @@ -102,7 +103,7 @@ pub(super) async fn start_sp_update( }; info!( - log, "starting SP update"; + log, "starting SP update"; "id" => %update_id, "aux_flash_chck" => ?aux_flash_chck, "aux_flash_size" => aux_flash_size, @@ -156,14 +157,14 @@ async fn drive_sp_update( { Ok(sp_matched_chck) => { info!( - log, "update preparation complete"; + log, "update preparation complete"; "update_id" => %update_id, ); sp_matched_chck } Err(message) => { error!( - log, "update preparation failed"; + log, "update preparation failed"; "err" => message, "update_id" => %update_id, ); @@ -191,7 +192,7 @@ async fn drive_sp_update( } Err(err) => { error!( - log, "aux flash update failed"; + log, "aux flash update failed"; "id" => %update_id, err, ); @@ -215,7 +216,7 @@ async fn drive_sp_update( } Err(err) => { error!( - log, "update failed"; + log, "update failed"; "id" => %update_id, err, ); @@ -257,57 +258,142 @@ fn read_auxi_check_from_tlvc(data: &[u8]) -> Result<[u8; 32], UpdateError> { }) } +/// Isolate extraction of bootleby from old-format archives. +// TODO: When old-format archives are eliminated from customer +// racks and spares inventory, then this code can be removed. +fn bootleby_from_old_style_archive( + image: Vec, + log: &Logger, +) -> Result, UpdateError> { + // Try the pre-v1.2.0 Bootleby archive format. + let cursor = Cursor::new(image.as_slice()); + let mut archive = zip::ZipArchive::new(cursor).map_err(|zip_error| { + // Return the original Hubris Archive error instead + // of our attempted zip extraction error. + HubtoolsError::ZipError(zip_error) + })?; + + for i in 0..archive.len() { + match archive.by_index(i) { + Ok(mut file) => { + if file.name() == "bootleby.bin" { + let mut rot_image = vec![]; + match file.read_to_end(&mut rot_image) { + Ok(_) => { + debug!( + log, + "using bootleby.bin from old-style archive" + ); + return Ok(rot_image); + } + Err(err) => { + error!(log, "cannot access bootleby.bin from zip file index {i}: {err}"); + break; + } + } + } + } + Err(err) => { + error!(log, "cannot access zip archive at index {i}: {err}") + } + } + } + Err(UpdateError::ImageNotFound) +} + /// Start an update to the RoT. pub(super) async fn start_rot_update( cmds_tx: &mpsc::Sender, update_id: Uuid, + component: SpComponent, slot: u16, image: Vec, log: &Logger, ) -> Result<(), UpdateError> { - let archive = RawHubrisArchive::from_vec(image)?; - let rot_image = archive.image.to_binary()?; - - // Sanity check on `hubtools`: Prior to using hubtools, we would manually - // extract `img/final.bin` from the archive (which is a zip file); we're now - // using `archive.image.to_binary()` which _should_ be the same thing. Check - // here and log a warning if it is not. We should never see this, but if we - // do it's likely something is about to go wrong, and it'd be nice to have a - // breadcrumb. - if let Ok(final_bin) = archive.extract_file("img/final.bin") { - if rot_image != final_bin { - warn!( - log, - "hubtools `image.to_binary()` DOES NOT MATCH `img/final.bin`", - ); + let rot_image = match component { + SpComponent::ROT => { + match slot { + // Hubris images + 0 | 1 => { + let archive = RawHubrisArchive::from_vec(image)?; + let rot_image = archive.image.to_binary()?; + + // Sanity check on `hubtools`: Prior to using hubtools, we would manually + // extract `img/final.bin` from the archive (which is a zip file); we're now + // using `archive.image.to_binary()` which _should_ be the same thing. Check + // here and log a warning if it is not. We should never see this, but if we + // do it's likely something is about to go wrong, and it'd be nice to have a + // breadcrumb. + if let Ok(final_bin) = archive.extract_file("img/final.bin") + { + if rot_image != final_bin { + warn!( + log, + "hubtools `image.to_binary()` DOES NOT MATCH `img/final.bin`", + ); + } + } + + // Preflight check 1: Does the image name of this archive match the target + // slot? + match archive.image_name() { + Ok(image_name) => match (image_name.as_str(), slot) { + ("a", 0) | ("b", 1) => (), // OK! + _ => { + return Err(UpdateError::RotSlotMismatch { + slot, + image_name, + }) + } + }, + // At the time of this writing `image-name` is a recent addition to + // hubris archives, so skip this check if we don't have one. + Err(HubtoolsError::MissingFile(..)) => (), + Err(err) => return Err(err.into()), + } + + // TODO: Add a caboose BORD preflight check just like the SP has, once the + // RoT has a caboose and we have RPC calls to read its values. + rot_image + } + _ => return Err(UpdateError::InvalidSlotIdForOperation), + } } - } - - // Preflight check 1: Does the image name of this archive match the target - // slot? - match archive.image_name() { - Ok(image_name) => match (image_name.as_str(), slot) { - ("a", 0) | ("b", 1) => (), // OK! - _ => return Err(UpdateError::RotSlotMismatch { slot, image_name }), - }, - // At the time of this writing `image-name` is a recent addition to - // hubris archives, so skip this check if we don't have one. - Err(HubtoolsError::MissingFile(..)) => (), - Err(err) => return Err(err.into()), - } + SpComponent::STAGE0 => { + // Staging area for a Bootloader image: + // stage0next can be updated directly, stage0 cannot. + // The RoT will reject updates to slot !=1 but don't + // waste its time. + if slot != 1 { + return Err(UpdateError::InvalidSlotIdForOperation); + } - // TODO: Add a caboose BORD preflight check just like the SP has, once the - // RoT has a caboose and we have RPC calls to read its values. + RawHubrisArchive::from_vec(image.clone()) + .and_then(|archive| archive.image.to_binary()) + .or_else(|hubtool_error| + // Prior to v1.2.0, Bootleby was packaged as a simple + // zip archive containing a "bootleby.bin" file. + // + // TODO: Remove support for the old image format when + // those bootleby versions are no longer used in + // manufacturing and rollback protection can be used to + // prevent their re-introduction. Until then, we need to + // be able to test update and rollback using the oldest + // releases that may be in customers' racks or spares pool. + bootleby_from_old_style_archive(image, log) + // Report the original Hubtools error if + // this second chance did not work. + .map_err(|_| hubtool_error))? + + // TODO: Even though the RoT will protect itself, put + // pre-flash checks here for BORD, Bootloader vs Hubris, + // and signature validity. + } + _ => return Err(UpdateError::InvalidComponent), + }; - start_component_update( - cmds_tx, - SpComponent::ROT, - update_id, - slot, - rot_image, - log, - ) - .await + start_component_update(cmds_tx, component, update_id, slot, rot_image, log) + .await } /// Start an update to a component of the SP. @@ -326,7 +412,7 @@ pub(super) async fn start_component_update( image.len().try_into().map_err(|_err| UpdateError::ImageTooLarge)?; info!( - log, "starting update"; + log, "starting update"; "component" => component.as_str(), "id" => %update_id, "total_size" => total_size, @@ -373,13 +459,13 @@ async fn drive_component_update( { Ok(_) => { info!( - log, "update preparation complete"; + log, "update preparation complete"; "update_id" => %update_id, ); } Err(message) => { error!( - log, "update preparation failed"; + log, "update preparation failed"; "err" => message, "update_id" => %update_id, ); @@ -396,7 +482,7 @@ async fn drive_component_update( } Err(err) => { error!( - log, "update failed"; + log, "update failed"; "id" => %update_id, err, ); @@ -518,7 +604,7 @@ async fn send_update_in_chunks( while !CursorExt::is_empty(&image) { let prior_pos = image.position(); debug!( - log, "sending update chunk"; + log, "sending update chunk"; "id" => %update_id, "offset" => offset, ); diff --git a/gateway-sp-comms/src/sp_response_expect.rs b/gateway-sp-comms/src/sp_response_expect.rs index 684782c7..f9ff126c 100644 --- a/gateway-sp-comms/src/sp_response_expect.rs +++ b/gateway-sp-comms/src/sp_response_expect.rs @@ -8,6 +8,7 @@ use gateway_messages::ignition::LinkEvents; use gateway_messages::DiscoverResponse; use gateway_messages::IgnitionState; use gateway_messages::PowerState; +use gateway_messages::RotBootInfo; use gateway_messages::RotResponse; use gateway_messages::SensorResponse; use gateway_messages::SpResponse; @@ -118,6 +119,7 @@ expect_fn!(ReadSensor(resp) -> SensorResponse); expect_fn!(CurrentTime(time) -> u64); expect_fn!(DisableComponentWatchdogAck); expect_fn!(ComponentWatchdogSupportedAck); +expect_fn!(RotBootInfo(rot_state) -> RotBootInfo); // Data-bearing responses expect_data_fn!(BulkIgnitionState(page) -> TlvPage); @@ -182,6 +184,7 @@ pub(crate) fn expect_sp_state( let out = match response { SpResponse::SpState(state) => Ok(VersionedSpState::V1(state)), SpResponse::SpStateV2(state) => Ok(VersionedSpState::V2(state)), + SpResponse::SpStateV3(state) => Ok(VersionedSpState::V3(state)), SpResponse::Error(err) => Err(CommunicationError::SpError(err)), other => Err(CommunicationError::BadResponseType { expected: "versioned_sp_state", // hard-coded special string @@ -199,7 +202,7 @@ pub(crate) fn expect_sp_state( #[cfg(test)] mod tests { use super::*; - use crate::{SpStateV1, SpStateV2, VersionedSpState}; + use crate::{SpStateV1, SpStateV2, SpStateV3, VersionedSpState}; use std::net::Ipv6Addr; fn dummy_addr() -> SocketAddrV6 { @@ -350,6 +353,24 @@ mod tests { panic!("mismatched value {v:?}"); }; assert_eq!(r, VersionedSpState::V2(state)); + + let state = SpStateV3 { + hubris_archive_id: [1, 2, 3, 4, 5, 6, 7, 8], + serial_number: [0; 32], + model: [0; 32], + revision: 123, + base_mac_address: [0; 6], + power_state: gateway_messages::PowerState::A0, + }; + let v = expect_sp_state(( + dummy_addr(), + SpResponse::SpStateV3(state), + vec![], + )); + let Ok(r) = v else { + panic!("mismatched value {v:?}"); + }; + assert_eq!(r, VersionedSpState::V3(state)); } #[test]