diff --git a/Cargo.lock b/Cargo.lock index 2a69b4c6..86442e47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -485,7 +485,7 @@ dependencies = [ [[package]] name = "faux-mgs" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "async-trait", diff --git a/faux-mgs/Cargo.toml b/faux-mgs/Cargo.toml index a714ae7d..d45bd430 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.2.0" edition = "2021" license = "MPL-2.0" diff --git a/faux-mgs/src/main.rs b/faux-mgs/src/main.rs index dfb5c4f4..6ff6f7a9 100644 --- a/faux-mgs/src/main.rs +++ b/faux-mgs/src/main.rs @@ -131,6 +131,14 @@ enum Command { /// Ask SP for its current state. State, + /// Ask RoT via SP for its current boot-time information represented by + /// a particular struct. + RotBootInfo { + /// RotStateV2 is the default verion + #[clap(long, short, default_value = "3")] + version: u8, + }, + /// Get the ignition state for a single target port (only valid if the SP is /// an ignition controller). Ignition { @@ -178,6 +186,15 @@ enum Command { }, /// Get or set the active slot of a component (e.g., `host-boot-flash`). + /// + /// In the case of the bootloader flash banks, there is no atomic switching + /// from stage0next (bank 1), to stage0 (bank 0). Instead, contents are copied. + /// + /// The copy is only performed if the signature on stage0next was valid at boot time + /// and the current contents still match the boot-time contents. + /// + /// 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, @@ -821,8 +838,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) => { @@ -850,12 +870,51 @@ 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!("{}", &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!("{}", state)); + Ok(Output::Lines(lines)) + } Command::Ignition { target } => { let mut by_target = BTreeMap::new(); if let Some(target) = target.0 { diff --git a/gateway-messages/src/lib.rs b/gateway-messages/src/lib.rs index 2eadf2ef..81d0dc84 100644 --- a/gateway-messages/src/lib.rs +++ b/gateway-messages/src/lib.rs @@ -126,7 +126,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, )] @@ -135,6 +135,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 a773bec7..31ffeebc 100644 --- a/gateway-messages/src/mgs_to_sp.rs +++ b/gateway-messages/src/mgs_to_sp.rs @@ -173,6 +173,11 @@ pub enum MgsRequest { /// Dump information about the lock state of the VPD (Vital Product Data) /// The values are serialized in the trailer of the packet VpdLockState, + + /// 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 62e2f285..b0bd8f7b 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; @@ -394,6 +395,13 @@ pub trait SpHandler { fn vpd_lock_status_all(&mut self, buf: &mut [u8]) -> Result; + + fn versioned_rot_boot_info( + &mut self, + sender: SocketAddrV6, + port: SpPort, + version: u8, + ) -> Result; } /// Handle a single incoming message. @@ -961,6 +969,10 @@ fn handle_mgs_request( } r.map(|_| SpResponse::VpdLockState) } + MgsRequest::VersionedRotBootInfo { version } => { + let r = handler.versioned_rot_boot_info(sender, port, version); + r.map(SpResponse::RotBootInfo) + } }; let response = match result { @@ -1365,6 +1377,15 @@ mod tests { ) -> Result { 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 a72abaa4..8dc8da34 100644 --- a/gateway-messages/src/sp_to_mgs.rs +++ b/gateway-messages/src/sp_to_mgs.rs @@ -128,6 +128,8 @@ pub enum SpResponse { ReadRot(RotResponse), /// The packet contains trailing lock information VpdLockState, + SpStateV3(SpStateV3), + RotBootInfo(RotBootInfo), } /// Identifier for one of of an SP's KSZ8463 management-network-facing ports. @@ -164,6 +166,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( @@ -199,6 +233,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, )] @@ -255,6 +304,222 @@ pub struct RotStateV2 { pub slot_b_sha3_256_digest: Option<[u8; 32]>, } +#[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 is 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>, +} + +impl fmt::Display for RotState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "RotStateV2 {{")?; + write!(f, "active: {:?}", self.rot_updates.boot_state.active)?; + match self.rot_updates.boot_state.slot_a { + Some(details) => { + write!(f, "slot_a: Some({{digest: Some(")?; + for b in details.digest.iter() { + write!(f, "{:02x}", b)?; + } + write!( + f, + ", epoch: {}, version{}}})", + details.version.epoch, details.version.version + )?; + } + + None => write!(f, "slot_a: None")?, + }; + match self.rot_updates.boot_state.slot_b { + Some(details) => { + write!(f, "slot_b: Some({{digest: Some(")?; + for b in details.digest.iter() { + write!(f, "{:02x}", b)?; + } + write!( + f, + ", epoch: {}, version{}}})", + details.version.epoch, details.version.version + )?; + } + None => write!(f, "slot_b: None")?, + }; + write!(f, "}}")?; + Ok(()) + } +} + +impl fmt::Display for RotStateV2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "RotStateV2 {{")?; + writeln!(f, " active: {:?},", self.active)?; + writeln!( + f, + " persistent_boot_preference: {:?},", + self.persistent_boot_preference + )?; + writeln!( + f, + " pending_persistent_boot_preference: {:?},", + self.pending_persistent_boot_preference + )?; + writeln!( + f, + " transient_boot_preference: {:?}", + self.transient_boot_preference + )?; + match self.slot_a_sha3_256_digest { + Some(digest) => { + write!(f, " slot_a_sha3_256_digest: ")?; + for b in digest.iter() { + write!(f, "{:02x}", b)?; + } + writeln!(f, ",")?; + } + None => writeln!(f, "slot_a_sha3_256_digest: None")?, + }; + match self.slot_b_sha3_256_digest { + Some(digest) => { + write!(f, " slot_b_sha3_256_digest: ")?; + for b in digest.iter() { + write!(f, "{:02x}", b)?; + } + writeln!(f, ",")?; + } + None => writeln!(f, " slot_a_sha3_256_digest: None,")?, + }; + write!(f, "}}")?; + Ok(()) + } +} + +impl fmt::Display for RotStateV3 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "RotStateV3 {{")?; + writeln!(f, " active: {:?},", self.active)?; + writeln!( + f, + " persistent_boot_preference: {:?},", + self.persistent_boot_preference + )?; + writeln!( + f, + " pending_persistent_boot_preference: {:?},", + self.pending_persistent_boot_preference + )?; + writeln!( + f, + " transient_boot_preference: {:?}", + self.transient_boot_preference + )?; + + // TODO: Add Display for Fwid + + let Fwid::Sha3_256(digest) = self.slot_a_fwid; + write!(f, " slot_a_fwid: Sha3_256( ")?; + for b in digest.iter() { + write!(f, "{:02x}", b)?; + } + writeln!(f, " ),")?; + + let Fwid::Sha3_256(digest) = self.slot_b_fwid; + write!(f, " slot_b_fwid: Sha3_256( ")?; + for b in digest.iter() { + write!(f, "{:02x}", b)?; + } + writeln!(f, " ),")?; + + let Fwid::Sha3_256(digest) = self.stage0_fwid; + write!(f, " stage0_fwid: Sha3_256( ")?; + for b in digest.iter() { + write!(f, "{:02x}", b)?; + } + writeln!(f, " ),")?; + + let Fwid::Sha3_256(digest) = self.stage0next_fwid; + write!(f, " stage0next_fwid: Sha3_256( ")?; + for b in digest.iter() { + write!(f, "{:02x}", b)?; + } + writeln!(f, " ),")?; + writeln!(f, "slot_a_status: {:?},", self.slot_a_status)?; + writeln!(f, "slot_b_status: {:?},", self.slot_b_status)?; + writeln!(f, "stage0_status: {:?},", self.stage0_status)?; + writeln!(f, "stage0next_status: {:?},", self.stage0next_status)?; + + write!(f, "}}")?; + Ok(()) + } +} + +/// While the `rot_state` API is deprecated, the RotState structures remain. +/// They are fetched with `rot_boot_info` API for `RotState`, and +/// `versioned_rot_boot_info` to get the latest or a particular version. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, SerializedSize, Serialize, Deserialize, +)] +pub enum RotBootInfo { + V1(RotState), + V2(RotStateV2), + V3(RotStateV3), +} + +impl fmt::Display for RotBootInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "RotBootInfo {{")?; + match self { + RotBootInfo::V1(rotstate) => { + write!(f, "V1({})", &rotstate)?; + } + RotBootInfo::V2(rotstate) => { + write!(f, "V2({})", &rotstate)?; + } + RotBootInfo::V3(rotstate) => { + write!(f, "V3({})", &rotstate)?; + } + } + writeln!(f, "}}")?; + Ok(()) + } +} + /// Metadata describing a single page (out of a larger list) of TLV-encoded /// structures returned by the SP. /// @@ -570,6 +835,9 @@ pub enum SpError { Update(UpdateError), Sensor(SensorError), Vpd(VpdError), + BlockOutOfOrder, + InvalidSlotIdForOperation, + InvalidComponent, } impl fmt::Display for SpError { @@ -681,6 +949,15 @@ impl fmt::Display for SpError { Self::Update(e) => write!(f, "update: {}", e), Self::Sensor(e) => write!(f, "sensor: {}", e), Self::Vpd(e) => write!(f, "vpd: {}", 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") + } } } } @@ -750,6 +1027,13 @@ pub enum UpdateError { Unknown(u32), MissingHandoffData, + BlockOutOfOrder, + InvalidComponent, + InvalidSlotIdForOperation, + InvalidArchive, + ImageMismatch, + SignatureNotValidated, + VersionNotSupported, } impl fmt::Display for UpdateError { @@ -788,6 +1072,27 @@ impl fmt::Display for UpdateError { Self::MissingHandoffData => { write!(f, "boot data not handed off to hubris kernel") } + Self::BlockOutOfOrder => { + write!(f, "bootloader 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/mod.rs b/gateway-messages/tests/versioning/mod.rs index 272a5de3..9204b935 100644 --- a/gateway-messages/tests/versioning/mod.rs +++ b/gateway-messages/tests/versioning/mod.rs @@ -16,6 +16,7 @@ mod v08; mod v09; mod v10; mod v11; +mod v12; pub fn assert_serialized( out: &mut [u8], diff --git a/gateway-messages/tests/versioning/v12.rs b/gateway-messages/tests/versioning/v12.rs new file mode 100644 index 00000000..22b57e11 --- /dev/null +++ b/gateway-messages/tests/versioning/v12.rs @@ -0,0 +1,224 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! The tests in this module check that the serialized form of messages from MGS +//! protocol version 12 have not changed. +//! +//! If a test in this module fails, _do not change the test_! This means you +//! have changed, deleted, or reordered an existing message type or enum +//! variant, and you should revert that change. This will remain true until we +//! bump the `version::MIN` to a value higher than 11, at which point these +//! tests can be removed as we will stop supporting v11. + +use super::assert_serialized; +use gateway_messages::MgsRequest; +use gateway_messages::Fwid; +use gateway_messages::ImageError; +use gateway_messages::RotBootInfo; +use gateway_messages::RotSlotId; +use gateway_messages::RotStateV3; +use gateway_messages::SerializedSize; +use gateway_messages::SpResponse; +use gateway_messages::SpStateV3; + +#[test] +fn sp_request() { + let mut out = [0; MgsRequest::MAX_SIZE]; + let request = MgsRequest::VersionedRotBootInfo { + version: 3, + }; + + #[rustfmt::skip] + let expected = vec![ + 42, // VersionedRotBootInfo + 3, // version + ]; + + assert_serialized(&mut out, &expected, &request); +} + +#[test] +fn sp_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![ + 42, // 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] +fn rot_boot_info_v3() { + let mut out = [0; SpResponse::MAX_SIZE]; + + // let rbiv2 = RotStateV3 { + // active: RotSlotId::A, + // persistent_boot_preference: RotSlotId::A, + // pending_persistent_boot_preference: RotSlotId: Some(A), + // transient_persistent_boot_preference: RotSlotId: Some(B), + // slot_a_fwid: Fwid::Sha3_256([0u8; 32]), + // } + 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), + + + })); + + + // V1(RotState), + // V2(RotStateV2), + // V3(RotStateV3), + + #[rustfmt::skip] + let expected = vec![ + 43, 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); +} + +/* +pub enum RotBootInfo { + V1(RotState), + V2(RotStateV2), + V3(RotStateV3), +} +pub struct RotState { + pub rot_updates: RotUpdateDetails, +} +pub struct RotUpdateDetails { + pub boot_state: RotBootState, +} +pub struct RotBootState { + pub active: RotSlotId, + pub slot_a: Option, + pub slot_b: Option, +} +pub struct RotImageDetails { + pub digest: [u8; 32], + pub version: ImageVersion, +} +pub struct ImageVersion { + pub epoch: u32, + pub version: u32, +} +--- V2 +pub struct RotStateV2 { + /// 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 is 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_sha3_256_digest: Option<[u8; 32]>, + /// Sha3-256 Digest of Slot B in Flash + pub slot_b_sha3_256_digest: Option<[u8; 32]>, +} +--- V3 +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 is 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>, +} + +*/ diff --git a/gateway-sp-comms/src/error.rs b/gateway-sp-comms/src/error.rs index 6e3d1386..fbd08b89 100644 --- a/gateway-sp-comms/src/error.rs +++ b/gateway-sp-comms/src/error.rs @@ -104,4 +104,10 @@ 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, } 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 040ca7c9..49893c9b 100644 --- a/gateway-sp-comms/src/single_sp.rs +++ b/gateway-sp-comms/src/single_sp.rs @@ -34,6 +34,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; @@ -380,6 +381,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?; @@ -581,9 +589,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..9c5026ba 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; @@ -257,57 +258,137 @@ fn read_auxi_check_from_tlvc(data: &[u8]) -> Result<[u8; 32], UpdateError> { }) } +/// Isolate extraction of bootleby from old-format archives. +/// TODO: Purge old-format archives and delete this code. +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) + })?; + + let mut rot_image = vec![]; + for i in 0..archive.len() { + let mut file = match archive.by_index(i) { + Ok(file) => file, + Err(e) => { + error!( + log, + "did not find bootleby.bin in old-style archive: {e}" + ); + return Err(UpdateError::InvalidArchive); + } + }; + if file.name() == "bootleby.bin" { + if file.read_to_end(&mut rot_image).is_err() { + error!(log, "invalid archive"); + return Err(UpdateError::InvalidArchive); + } + debug!(log, "using bootleby.bin from old-style archive"); + break; + } + } + Ok(rot_image) +} + /// 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. diff --git a/gateway-sp-comms/src/sp_response_expect.rs b/gateway-sp-comms/src/sp_response_expect.rs index 229dfbbe..94bb865d 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; @@ -116,6 +117,7 @@ expect_fn!(SwitchDefaultImageAck); expect_fn!(ComponentActionAck); expect_fn!(ReadSensor(resp) -> SensorResponse); expect_fn!(CurrentTime(time) -> u64); +expect_fn!(RotBootInfo(rot_state) -> RotBootInfo); // Data-bearing responses expect_data_fn!(BulkIgnitionState(page) -> TlvPage); @@ -180,6 +182,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 @@ -197,7 +200,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 { @@ -348,6 +351,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]