From 0c001d401e875cf9289c0d3e426289de9e2df34b Mon Sep 17 00:00:00 2001 From: Ben Stoltz Date: Tue, 30 Apr 2024 10:41:11 -0700 Subject: [PATCH] Update RoT bootloader, a.k.a. stage0 (#1675) This PR has four commits to simplify reviewing. - The first is Laura's macro to manipulate the HASHCRYPT interrupt vector. - 2nd is pre-main kernel changes to create a staging area for Bootleby update and to validate the (now) four RoT flash banks and record those results for use by update_server. - 3rd are the API changes to support stage0 update including treating the bootloader as a separate component with A (active) and B (stage0next) banks. - last is the code in the RoT update_server to actually update stage0 Before merging this PR, Cargo.toml needs to be edited to reference the gateway-message-service main branch. Merging needs to be coordinated with an MGS merge. Closes #1043, #1404, #1548, #1353, --- Cargo.lock | 5 +- app/demo-stm32g0-nucleo/app-g031.toml | 2 +- app/lpc55xpresso/app-sprot.toml | 4 +- app/lpc55xpresso/app.toml | 4 +- app/oxide-rot-1/app-dev.toml | 7 +- app/oxide-rot-1/app.toml | 5 +- app/rot-carrier/app.toml | 8 +- chips/lpc55/memory.toml | 17 +- drv/lpc55-sprot-server/src/handler.rs | 114 ++- drv/lpc55-update-api/src/lib.rs | 115 ++- drv/lpc55-update-server/Cargo.toml | 3 + drv/lpc55-update-server/src/images.rs | 104 ++ drv/lpc55-update-server/src/main.rs | 981 +++++++++++++------ drv/sprot-api/src/lib.rs | 75 +- drv/stm32h7-sprot-server/src/main.rs | 164 +++- drv/update-api/src/lib.rs | 12 + idl/lpc55-update.idol | 68 ++ idl/sprot.idol | 74 +- lib/lpc55-rot-startup/Cargo.toml | 1 + lib/lpc55-rot-startup/src/images.rs | 422 +++++--- lib/lpc55-rot-startup/src/lib.rs | 82 +- lib/stage0-handoff/{REAME.md => README.md} | 0 lib/stage0-handoff/src/lib.rs | 6 +- lib/stage0-handoff/src/rot_update_details.rs | 138 ++- task/control-plane-agent/src/main.rs | 11 +- task/control-plane-agent/src/mgs_common.rs | 443 +++++++-- task/control-plane-agent/src/mgs_gimlet.rs | 15 +- task/control-plane-agent/src/mgs_psc.rs | 15 +- task/control-plane-agent/src/mgs_sidecar.rs | 15 +- task/control-plane-agent/src/update/rot.rs | 6 +- 30 files changed, 2319 insertions(+), 597 deletions(-) create mode 100644 drv/lpc55-update-server/src/images.rs rename lib/stage0-handoff/{REAME.md => README.md} (100%) diff --git a/Cargo.lock b/Cargo.lock index 46d15c6f3..61df5b4a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2347,7 +2347,7 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "gateway-messages" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/management-gateway-service#c6e6eb667b0ee061f77a0e40ec10e5e15f2f54cf" +source = "git+https://github.com/oxidecomputer/management-gateway-service#c85a4ca043aaa389df12aac5348d8a3feda28762" dependencies = [ "bitflags 2.5.0", "hubpack", @@ -2939,6 +2939,7 @@ dependencies = [ "digest", "drv-lpc55-flash", "hubpack", + "kern", "lib-dice", "lib-lpc55-usart", "lpc55-pac", @@ -2974,9 +2975,11 @@ dependencies = [ "idol", "idol-runtime", "lpc55-pac", + "mutable-statics", "num-traits", "ringbuf", "serde", + "sha3", "stage0-handoff", "static_assertions", "task-jefe-api", diff --git a/app/demo-stm32g0-nucleo/app-g031.toml b/app/demo-stm32g0-nucleo/app-g031.toml index 7bfe0f7ca..bb537f7d3 100644 --- a/app/demo-stm32g0-nucleo/app-g031.toml +++ b/app/demo-stm32g0-nucleo/app-g031.toml @@ -6,7 +6,7 @@ board = "stm32g031-nucleo" [kernel] name = "demo-stm32g0-nucleo" -requires = {flash = 11104, ram = 1296} +requires = {flash = 11344, ram = 1296} features = ["g031"] stacksize = 640 diff --git a/app/lpc55xpresso/app-sprot.toml b/app/lpc55xpresso/app-sprot.toml index 7a98140df..81efe2596 100644 --- a/app/lpc55xpresso/app-sprot.toml +++ b/app/lpc55xpresso/app-sprot.toml @@ -17,7 +17,7 @@ fwid = true [kernel] name = "lpc55xpresso" features = ["dice-self"] -requires = {flash = 53504, ram = 4096} +requires = {flash = 54128, ram = 4096} [caboose] region = "flash" @@ -55,7 +55,6 @@ start = true [tasks.update_server] name = "lpc55-update-server" priority = 3 -max-sizes = {flash = 32768, ram = 4096} stacksize = 3000 start = true sections = {bootstate = "usbsram"} @@ -182,7 +181,6 @@ task-slots = ["swd"] [tasks.sprot] name = "drv-lpc55-sprot-server" priority = 6 -max-sizes = {flash = 48512, ram = 32768} uses = ["flexcomm8", "bootrom"] features = ["spi0"] start = true diff --git a/app/lpc55xpresso/app.toml b/app/lpc55xpresso/app.toml index 41e3f0ff4..377327ce7 100644 --- a/app/lpc55xpresso/app.toml +++ b/app/lpc55xpresso/app.toml @@ -9,7 +9,7 @@ fwid = true [kernel] name = "lpc55xpresso" features = ["dump", "dice-self"] -requires = {flash = 54464, ram = 4096} +requires = {flash = 55040, ram = 4096} [caboose] region = "flash" @@ -48,7 +48,7 @@ start = true [tasks.update_server] name = "lpc55-update-server" priority = 3 -max-sizes = {flash = 16384, ram = 4096} +max-sizes = {flash = 26720, ram = 16384} stacksize = 2048 start = true sections = {bootstate = "usbsram"} diff --git a/app/oxide-rot-1/app-dev.toml b/app/oxide-rot-1/app-dev.toml index 83fd5630a..5a50d25b9 100644 --- a/app/oxide-rot-1/app-dev.toml +++ b/app/oxide-rot-1/app-dev.toml @@ -49,8 +49,9 @@ start = true [tasks.update_server] name = "lpc55-update-server" priority = 3 -max-sizes = {flash = 16384, ram = 4096, usbsram = 4096} -stacksize = 2560 +max-sizes = {flash = 26080, ram = 17000, usbsram = 4096} +# TODO: Size this appropriately +stacksize = 8192 start = true sections = {bootstate = "usbsram"} uses = ["flash_controller", "hash_crypt"] @@ -77,7 +78,7 @@ task-slots = ["syscon_driver"] [tasks.sprot] name = "drv-lpc55-sprot-server" priority = 6 -max-sizes = {flash = 51168, ram = 32768} +max-sizes = {flash = 54016, ram = 32768} uses = ["flexcomm8", "bootrom"] features = ["spi0", "sp-ctrl"] start = true diff --git a/app/oxide-rot-1/app.toml b/app/oxide-rot-1/app.toml index 445e9c031..fae7c6c26 100644 --- a/app/oxide-rot-1/app.toml +++ b/app/oxide-rot-1/app.toml @@ -10,7 +10,7 @@ fwid = true [kernel] name = "oxide-rot-1" -requires = {flash = 61124, ram = 2696} +requires = {flash = 61252, ram = 2720} features = ["dice-mfg"] [caboose] @@ -40,8 +40,7 @@ start = true [tasks.update_server] name = "lpc55-update-server" priority = 3 -max-sizes = {flash = 16384, ram = 4096, usbsram = 4096} -stacksize = 2560 +stacksize = 8192 start = true sections = {bootstate = "usbsram"} uses = ["flash_controller", "hash_crypt"] diff --git a/app/rot-carrier/app.toml b/app/rot-carrier/app.toml index 7082b0c4c..ec85ba8bb 100644 --- a/app/rot-carrier/app.toml +++ b/app/rot-carrier/app.toml @@ -11,7 +11,7 @@ fwid = true [kernel] name = "rot-carrier" features = ["dice-self"] -requires = {flash = 53600, ram = 4096} +requires = {flash = 54176, ram = 3876} [caboose] tasks = ["caboose_reader", "sprot"] @@ -46,8 +46,8 @@ start = true [tasks.update_server] name = "lpc55-update-server" priority = 3 -max-sizes = {flash = 32768, ram = 4096, usbsram = 4096} -stacksize = 2560 +# TODO size this appropriately +stacksize = 8192 start = true sections = {bootstate = "usbsram"} uses = ["flash_controller", "hash_crypt"] @@ -108,7 +108,7 @@ task-slots = ["syscon_driver"] [tasks.sprot] name = "drv-lpc55-sprot-server" priority = 6 -max-sizes = {flash = 49728, ram = 32768} +max-sizes = {flash = 54000, ram = 32768} uses = ["flexcomm8", "bootrom"] features = ["spi0"] start = true diff --git a/chips/lpc55/memory.toml b/chips/lpc55/memory.toml index ee95ebee9..47be0adc4 100644 --- a/chips/lpc55/memory.toml +++ b/chips/lpc55/memory.toml @@ -15,10 +15,25 @@ execute = true [[flash]] name = "stage0" address = 0x00000000 -size = 0x10000 +size = 0x02000 read = true execute = true +# Staging area for "stage0" updates. +# +# The largest stage0 image to date is about 6-ish KiB, +# while the current release is 4-ish KiB. +# That leaves at least 56 KiB of "stage0" unused. +# The Hubris pre-main kernel code checks "stage0next" at boot time +# When it is still possible to call the ROM-based signature verification +# routine. +[[flash]] +name = "stage0next" +address = 0x00002000 +size = 0x02000 +read = true +execute = false + [[flash]] name = "dice-mfg" address = 0x90000 diff --git a/drv/lpc55-sprot-server/src/handler.rs b/drv/lpc55-sprot-server/src/handler.rs index c3963e2a5..0b17a2594 100644 --- a/drv/lpc55-sprot-server/src/handler.rs +++ b/drv/lpc55-sprot-server/src/handler.rs @@ -5,7 +5,7 @@ use crate::Trace; use attest_api::Attest; use crc::{Crc, CRC_32_CKSUM}; -use drv_lpc55_update_api::{RotPage, SlotId, Update}; +use drv_lpc55_update_api::{RotComponent, RotPage, SlotId, Update}; use drv_sprot_api::{ AttestReq, AttestRsp, CabooseReq, CabooseRsp, DumpReq, ReqBody, Request, Response, RotIoStats, RotPageRsp, RotState, RotStatus, RspBody, @@ -48,11 +48,33 @@ pub struct StartupState { /// Marker for data which should be copied after the packet is encoded pub enum TrailingData<'a> { - Caboose { slot: SlotId, start: u32, size: u32 }, - AttestCert { index: u32, offset: u32, size: u32 }, - AttestLog { offset: u32, size: u32 }, - Attest { nonce: &'a [u8], write_size: u32 }, - RotPage { page: RotPage }, + Caboose { + slot: SlotId, + start: u32, + size: u32, + }, + AttestCert { + index: u32, + offset: u32, + size: u32, + }, + AttestLog { + offset: u32, + size: u32, + }, + Attest { + nonce: &'a [u8], + write_size: u32, + }, + RotPage { + page: RotPage, + }, + ComponentCaboose { + component: RotComponent, + slot: SlotId, + start: u32, + size: u32, + }, } pub struct Handler { @@ -117,6 +139,40 @@ impl<'a> Handler { // In certain cases, handling the request has left us with trailing data // that needs to be packed into the remaining packet space. match trailer { + Some(TrailingData::ComponentCaboose { + component, + slot, + start, + size: blob_size, + }) => { + let blob_size: usize = blob_size.try_into().unwrap_lite(); + if blob_size > drv_sprot_api::MAX_BLOB_SIZE { + // If there isn't enough room, then pack an error instead + Response::pack( + &Err(SprotError::Protocol( + SprotProtocolError::BadMessageLength, + )), + tx_buf, + ) + } else { + let pack_result = + Response::pack_with_cb(&rsp_body, tx_buf, |buf| { + self.update + .component_read_raw_caboose( + component, + slot, + start, + &mut buf[..blob_size], + ) + .map_err(|e| RspBody::Caboose(Err(e)))?; + Ok(blob_size) + }); + match pack_result { + Ok(size) => size, + Err(e) => Response::pack(&Ok(e), tx_buf), + } + } + } Some(TrailingData::Caboose { slot, start, @@ -357,6 +413,33 @@ impl<'a> Handler { Some(TrailingData::Caboose { slot, start, size }), )) } + CabooseReq::ComponentSize { component, slot } => { + let rsp = self + .update + .component_caboose_size(component, slot) + .map(CabooseRsp::ComponentSize); + Ok((RspBody::Caboose(rsp), None)) + } + CabooseReq::ComponentRead { + component, + slot, + start, + size, + } => { + // In this case, we're going to be sending back a variable + // amount of data in the trailing section of the packet. We + // don't know exactly where that data will be placed, so + // we'll return a marker here and copy it later. + Ok(( + RspBody::Caboose(Ok(CabooseRsp::ComponentRead)), + Some(TrailingData::ComponentCaboose { + component, + slot, + start, + size, + }), + )) + } }, ReqBody::Update(UpdateReq::BootInfo) => { let boot_info = self.update.rot_boot_info()?; @@ -477,6 +560,25 @@ impl<'a> Handler { #[cfg(not(feature = "sp-ctrl"))] Err(SprotError::Protocol(SprotProtocolError::BadMessageType)) } + ReqBody::Update(UpdateReq::VersionedBootInfo { version }) => { + let versioned_boot_info = + self.update.versioned_rot_boot_info(version)?; + Ok((RspBody::Update(versioned_boot_info.into()), None)) + } + ReqBody::Update(UpdateReq::ComponentPrep { component, slot }) => { + self.update.component_prep_image_update(component, slot)?; + Ok((RspBody::Ok, None)) + } + ReqBody::Update(UpdateReq::ComponentSwitchDefaultImage { + component, + slot, + duration, + }) => { + self.update.component_switch_default_image( + component, slot, duration, + )?; + Ok((RspBody::Ok, None)) + } } } } diff --git a/drv/lpc55-update-api/src/lib.rs b/drv/lpc55-update-api/src/lib.rs index 3b3b4351c..c85366998 100644 --- a/drv/lpc55-update-api/src/lib.rs +++ b/drv/lpc55-update-api/src/lib.rs @@ -48,11 +48,58 @@ impl From for drv_caboose::CabooseError { } } +/// Firmware ID - A measurement of all programmed pages in a flash slot. +/// +/// The FWID is a SHA3-256 digest over all of the programmed pages in the flash +/// slot even if those pages are not part of a valid image. +/// +/// The last partial flash page of an image is filled with 0xff bytes. +/// All subsequent pages in the flash slot are expected to be erased. +/// Erased pages on the LPC55 are not readable and contribute no bytes +/// to the SHA3-256 input. +/// +/// The intent of including non-image flash pages is to detect incomplete +/// update operations where unused pages were not erased or writing was +/// interrupted. It also can detect any attempted exfiltration of data in +/// unused pages. Note that an improperly signed image could have +/// exfiltrated data as a payload. +/// +/// TODO: Test: Try to create a partially programmed page and understand the +/// code behavior in that case. +/// +#[derive( + Copy, Clone, PartialEq, Eq, Deserialize, Serialize, SerializedSize, +)] +pub enum Fwid { + Sha3_256([u8; 32]), +} + +/// Running SHA3-256 over a zero-byte input stream still produces a value. +/// So, any completely erased flash slot will return an FWID equal to +/// `const _FWID_ERASED_SLOT` defined below as: +/// a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a +/// +/// We could switch to using the LPC55 HASHCRYPT block to save some flash +/// space. If we did that, the algorithm would be SHA2-256 and would +/// produce the following digest for an empty slot: +/// e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +/// +/// Note that no page is supposed to be partially programmed/partially +/// erased. The LPC55 should report any page that does not have the +/// final internal ECC syndrome written as being erased. Assuming that +/// is the case, then one would not expect to be able to read any data +/// from that page. +const _FWID_ERASED_SLOT: Fwid = Fwid::Sha3_256([ + 0xa7, 0xff, 0xc6, 0xf8, 0xbf, 0x1e, 0xd7, 0x66, 0x51, 0xc1, 0x47, 0x56, + 0xa0, 0x61, 0xd6, 0x62, 0xf5, 0x80, 0xff, 0x4d, 0xe4, 0x3b, 0x49, 0xfa, + 0x82, 0xd8, 0x0a, 0x4b, 0x80, 0xf8, 0x43, 0x4a, +]); + /// ROT boot state and preferences retrieved from the lpc55-update-server /// /// SW version information is in the caboose and is read from a different /// API -#[derive(Debug, Clone, Serialize, Deserialize, SerializedSize)] +#[derive(Clone, Serialize, Deserialize, SerializedSize)] pub struct RotBootInfo { /// Ths slot of the currently running image pub active: SlotId, @@ -68,12 +115,53 @@ pub struct RotBootInfo { /// /// This is a magic ram value that is cleared by bootleby pub transient_boot_preference: Option, - /// Sha3-256 Digest of Slot A in Flash + /// 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 + /// Sha3-256 Digest of Slot B in Flash pub slot_b_sha3_256_digest: Option<[u8; 32]>, } +/// ROT boot state and preferences retrieved from the lpc55-update-server +/// +/// SW version information is in the caboose and is read from a different +/// API +#[derive(Clone, Serialize, Deserialize, SerializedSize)] +pub struct RotBootInfoV2 { + /// Ths slot of the currently running image + pub active: SlotId, + /// The persistent boot preference written into the current authoritative + /// CFPA page (ping or pong). + pub persistent_boot_preference: SlotId, + /// 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, + /// Digest of Slot A in Flash + pub slot_a_fwid: Fwid, + /// Digest of Slot B in Flash + pub slot_b_fwid: Fwid, + /// Digest of Stage0 in Flash + pub stage0_fwid: Fwid, + /// Digest of Stage0Next in Flash + pub stage0next_fwid: Fwid, + /// If readable, the result of checking an image using the ROM code. + pub slot_a_status: Result<(), ImageError>, + pub slot_b_status: Result<(), ImageError>, + pub stage0_status: Result<(), ImageError>, + pub stage0next_status: Result<(), ImageError>, +} + +#[derive(Clone, Serialize, Deserialize, SerializedSize)] +pub enum VersionedRotBootInfo { + V1(RotBootInfo), + V2(RotBootInfoV2), +} + #[derive(Clone, Copy, Serialize, Deserialize, SerializedSize)] pub enum RotPage { // The manufacturing area that cannot be changed @@ -109,6 +197,22 @@ pub enum UpdateTarget { Bootloader, } +/// Designate a logical sub-component of the RoT +#[derive( + Clone, + Copy, + Eq, + PartialEq, + FromPrimitive, + Serialize, + Deserialize, + SerializedSize, +)] +pub enum RotComponent { + Hubris, + Stage0, +} + /// Designates a firmware image slot in parts that have fixed slots (rather than /// bank remapping). /// @@ -117,7 +221,6 @@ pub enum UpdateTarget { #[derive( Clone, Copy, - Debug, Eq, PartialEq, FromPrimitive, @@ -154,7 +257,6 @@ impl TryFrom for SlotId { #[derive( Clone, Copy, - Debug, Eq, PartialEq, FromPrimitive, @@ -174,7 +276,8 @@ pub enum SwitchDuration { // Re-export pub use stage0_handoff::{ - HandoffDataLoadError, ImageVersion, RotBootState, RotImageDetails, RotSlot, + HandoffDataLoadError, ImageError, ImageVersion, RotBootState, + RotBootStateV2, RotImageDetails, RotSlot, }; // This value is currently set to `lpc55_romapi::FLASH_PAGE_SIZE` diff --git a/drv/lpc55-update-server/Cargo.toml b/drv/lpc55-update-server/Cargo.toml index 5831584b4..b07409922 100644 --- a/drv/lpc55-update-server/Cargo.toml +++ b/drv/lpc55-update-server/Cargo.toml @@ -10,11 +10,14 @@ drv-update-api.path = "../update-api/" drv-lpc55-update-api.path = "../lpc55-update-api/" ringbuf.path = "../../lib/ringbuf" stage0-handoff.path = "../../lib/stage0-handoff" +# userlib = {path = "../../sys/userlib" } userlib = {path = "../../sys/userlib", features = ["panic-messages"]} drv-lpc55-flash.path = "../lpc55-flash" drv-lpc55-sha256.path = "../lpc55-sha256" drv-lpc55-syscon-api.path = "../lpc55-syscon-api" +sha3 = { workspace = true, optional = false } task-jefe-api = { path = "../../task/jefe-api" } +mutable-statics = { path = "../../lib/mutable-statics" } cfg-if.workspace = true hubpack.workspace = true diff --git a/drv/lpc55-update-server/src/images.rs b/drv/lpc55-update-server/src/images.rs new file mode 100644 index 000000000..bd9f085fb --- /dev/null +++ b/drv/lpc55-update-server/src/images.rs @@ -0,0 +1,104 @@ +// 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/. + +use core::ops::Range; +use drv_lpc55_update_api::BLOCK_SIZE_BYTES; +use drv_lpc55_update_api::{RotComponent, SlotId}; +use drv_update_api::UpdateError; +use userlib::UnwrapLite; + +// We shouldn't actually dereference these. The types are not correct. +// They are just here to allow a mechanism for getting the addresses. +extern "C" { + static __IMAGE_A_BASE: [u32; 0]; + static __IMAGE_B_BASE: [u32; 0]; + static __IMAGE_STAGE0_BASE: [u32; 0]; + static __IMAGE_STAGE0NEXT_BASE: [u32; 0]; + static __IMAGE_A_END: [u32; 0]; + static __IMAGE_B_END: [u32; 0]; + static __IMAGE_STAGE0_END: [u32; 0]; + static __IMAGE_STAGE0NEXT_END: [u32; 0]; + + static __this_image: [u32; 0]; +} + +// Location of the NXP header +pub const HEADER_BLOCK: usize = 0; + +// NXP LPC55's mixed header/vector table offsets +const RESET_VECTOR_OFFSET: usize = 0x04; +pub const LENGTH_OFFSET: usize = 0x20; +pub const HEADER_OFFSET: u32 = 0x130; +const MAGIC_OFFSET: usize = HEADER_OFFSET as usize; + +// Perform some sanity checking on the header block. +pub fn validate_header_block( + component: RotComponent, + slot: SlotId, + block: &[u8; BLOCK_SIZE_BYTES], +) -> Result<(), UpdateError> { + let exec = image_range(component, slot).1; + + // This part aliases flash in two positions that differ in bit 28. To allow + // for either position to be used in new images, we clear bit 28 in all of + // the numbers used for comparison below, by ANDing them with this mask: + const ADDRMASK: u32 = !(1 << 28); + + let reset_vector = u32::from_le_bytes( + block[RESET_VECTOR_OFFSET..][..4].try_into().unwrap_lite(), + ) & ADDRMASK; + + // Ensure the image is destined for the right target + if !exec.contains(&reset_vector) { + return Err(UpdateError::InvalidHeaderBlock); + } + + // Ensure the MAGIC is correct. + // Bootloaders have been released without an ImageHeader. Allow those. + let magic = + u32::from_le_bytes(block[MAGIC_OFFSET..][..4].try_into().unwrap_lite()); + if component == RotComponent::Hubris && magic != abi::HEADER_MAGIC { + return Err(UpdateError::InvalidHeaderBlock); + } + + Ok(()) +} + +pub fn same_image(component: RotComponent, slot: SlotId) -> bool { + // Safety: We are trusting the linker. + image_range(component, slot).0.start + == unsafe { &__this_image } as *const _ as u32 +} + +/// Return the flash storage address range and flash execution address range. +/// These are only different for the staged stage0 image. +pub fn image_range( + component: RotComponent, + slot: SlotId, +) -> (Range, Range) { + unsafe { + match (component, slot) { + (RotComponent::Hubris, SlotId::A) => ( + __IMAGE_A_BASE.as_ptr() as u32..__IMAGE_A_END.as_ptr() as u32, + __IMAGE_A_BASE.as_ptr() as u32..__IMAGE_A_END.as_ptr() as u32, + ), + (RotComponent::Hubris, SlotId::B) => ( + __IMAGE_B_BASE.as_ptr() as u32..__IMAGE_B_END.as_ptr() as u32, + __IMAGE_B_BASE.as_ptr() as u32..__IMAGE_B_END.as_ptr() as u32, + ), + (RotComponent::Stage0, SlotId::A) => ( + __IMAGE_STAGE0_BASE.as_ptr() as u32 + ..__IMAGE_STAGE0_END.as_ptr() as u32, + __IMAGE_STAGE0_BASE.as_ptr() as u32 + ..__IMAGE_STAGE0_END.as_ptr() as u32, + ), + (RotComponent::Stage0, SlotId::B) => ( + __IMAGE_STAGE0NEXT_BASE.as_ptr() as u32 + ..__IMAGE_STAGE0NEXT_END.as_ptr() as u32, + __IMAGE_STAGE0_BASE.as_ptr() as u32 + ..__IMAGE_STAGE0_END.as_ptr() as u32, + ), + } + } +} diff --git a/drv/lpc55-update-server/src/main.rs b/drv/lpc55-update-server/src/main.rs index 7a98c7eb3..7e013d63f 100644 --- a/drv/lpc55-update-server/src/main.rs +++ b/drv/lpc55-update-server/src/main.rs @@ -11,65 +11,89 @@ use core::convert::Infallible; use core::mem::MaybeUninit; -use drv_lpc55_flash::BYTES_PER_FLASH_PAGE; +use core::ops::Range; +use drv_lpc55_flash::{BYTES_PER_FLASH_PAGE, BYTES_PER_FLASH_WORD}; use drv_lpc55_update_api::{ - RawCabooseError, RotBootInfo, RotPage, SlotId, SwitchDuration, UpdateTarget, + Fwid, RawCabooseError, RotBootInfo, RotBootInfoV2, RotComponent, RotPage, + SlotId, SwitchDuration, UpdateTarget, VersionedRotBootInfo, }; use drv_update_api::UpdateError; use idol_runtime::{ ClientError, Leased, LenLimit, NotificationHandler, RequestError, R, W, }; +use ringbuf::*; +use sha3::{Digest, Sha3_256}; use stage0_handoff::{ HandoffData, HandoffDataLoadError, ImageVersion, RotBootState, + RotBootStateV2, }; use userlib::*; use zerocopy::{AsBytes, FromBytes}; -// We shouldn't actually dereference these. The types are not correct. -// They are just here to allow a mechanism for getting the addresses. -extern "C" { - static __IMAGE_A_BASE: [u32; 0]; - static __IMAGE_B_BASE: [u32; 0]; - static __IMAGE_STAGE0_BASE: [u32; 0]; - static __IMAGE_A_END: [u32; 0]; - static __IMAGE_B_END: [u32; 0]; - static __IMAGE_STAGE0_END: [u32; 0]; - - static __this_image: [u32; 0]; -} +mod images; +use crate::images::*; + +const U32_SIZE: u32 = core::mem::size_of::() as u32; +const PAGE_SIZE: u32 = BYTES_PER_FLASH_PAGE as u32; #[used] #[link_section = ".bootstate"] static BOOTSTATE: MaybeUninit<[u8; 0x1000]> = MaybeUninit::uninit(); -#[derive(PartialEq)] +#[derive(Copy, Clone, PartialEq)] enum UpdateState { NoUpdate, InProgress, Finished, } -// Note that we could cache the full stage0 image before flashing it. -// That would reduce our time window of having a partially written stage0. +#[derive(Copy, Clone, PartialEq)] +enum Trace { + None, + State(UpdateState), + Prep(RotComponent, SlotId), +} + +ringbuf!(Trace, 16, Trace::None); + +/// FW_CACHE_MAX accomodates the largest production +/// bootloader image while allowing some room for growth. +/// +/// NOTE: The erase/flash of stage0 can be interrupted by a power failure or +/// reset. +/// The LPC55S69 ROM offers no A/B image backup. Therefore the partially +/// updated stage0 contents would fail the boot-time signature check thus +/// rendering the RoT inoperable. +/// +/// The full stage0 image is cached before flashing to reduce that window +/// of vulnerability. +/// +/// While the stage0next flash slot can be updated in parallel, not more than +/// one RoT should have its stage0 updated (persist operation) at any +/// one time. +/// +/// No addition RoT stage0 slots should be updated until a previous failure +/// can be diagnosed. +/// +const FW_CACHE_MAX: usize = 8192_usize; struct ServerImpl<'a> { header_block: Option<[u8; BLOCK_SIZE_BYTES]>, state: UpdateState, - image: Option, + image: Option<(RotComponent, SlotId)>, flash: drv_lpc55_flash::Flash<'a>, hashcrypt: &'a lpc55_pac::hashcrypt::RegisterBlock, syscon: drv_lpc55_syscon_api::Syscon, -} -// TODO: This is the size of the vector table on the LPC55. We should -// probably get it from somewhere else directly. -const MAGIC_OFFSET: usize = 0x130; -const RESET_VECTOR_OFFSET: usize = 4; + // Used to enforce sequential writes from the control plane. + next_block: Option, + // Keep the fw cache 32-bit aligned to make NXP header access easier. + fw_cache: &'a mut [u32; FW_CACHE_MAX / core::mem::size_of::()], +} const BLOCK_SIZE_BYTES: usize = BYTES_PER_FLASH_PAGE; const MAX_LEASE: usize = 1024; -const HEADER_BLOCK: usize = 0; const CMPA_FLASH_WORD: u32 = 0x9E40; const CFPA_PING_FLASH_WORD: u32 = 0x9E00; @@ -87,30 +111,23 @@ enum CfpaPage { impl idl::InOrderUpdateImpl for ServerImpl<'_> { fn prep_image_update( &mut self, - _: &RecvMessage, + msg: &RecvMessage, image_type: UpdateTarget, ) -> Result<(), RequestError> { - // The LPC55 doesn't have an easily accessible mass erase mechanism - // so this is just bookkeeping - match self.state { - UpdateState::InProgress => { - return Err(UpdateError::UpdateInProgress.into()) - } - UpdateState::Finished => { - return Err(UpdateError::UpdateAlreadyFinished.into()) - } - UpdateState::NoUpdate => (), - } - - self.image = Some(image_type); - self.state = UpdateState::InProgress; - Ok(()) + let (component, slot) = match image_type { + UpdateTarget::ImageA => (RotComponent::Hubris, SlotId::A), + UpdateTarget::ImageB => (RotComponent::Hubris, SlotId::B), + UpdateTarget::Bootloader => (RotComponent::Stage0, SlotId::B), + _ => return Err(UpdateError::InvalidSlotIdForOperation.into()), + }; + self.component_prep_image_update(msg, component, slot) } fn abort_update( &mut self, _: &RecvMessage, ) -> Result<(), RequestError> { + ringbuf_entry!(Trace::State(self.state)); match self.state { UpdateState::Finished => { return Err(UpdateError::UpdateAlreadyFinished.into()) @@ -119,6 +136,9 @@ impl idl::InOrderUpdateImpl for ServerImpl<'_> { } self.state = UpdateState::NoUpdate; + ringbuf_entry!(Trace::State(self.state)); + self.next_block = None; + self.fw_cache.fill(0); Ok(()) } @@ -128,6 +148,7 @@ impl idl::InOrderUpdateImpl for ServerImpl<'_> { block_num: usize, block: LenLimit, MAX_LEASE>, ) -> Result<(), RequestError> { + ringbuf_entry!(Trace::State(self.state)); match self.state { UpdateState::NoUpdate => { return Err(UpdateError::UpdateNotStarted.into()) @@ -138,6 +159,13 @@ impl idl::InOrderUpdateImpl for ServerImpl<'_> { UpdateState::InProgress => (), } + // Check that blocks are delivered in order. + let next = self.next_block.get_or_insert(0); + if block_num != *next { + return Err(UpdateError::BlockOutOfOrder.into()); + } + *next += 1; + let len = block.len(); // The max lease length is longer than our block size, double @@ -151,7 +179,7 @@ impl idl::InOrderUpdateImpl for ServerImpl<'_> { // read as 0xff so the image is padded with 0xff const ERASE_BYTE: u8 = 0xff; let mut flash_page = [ERASE_BYTE; BLOCK_SIZE_BYTES]; - let target = self.image.unwrap_lite(); + let (component, slot) = self.image.unwrap_lite(); if block_num == HEADER_BLOCK { let header_block = @@ -160,18 +188,14 @@ impl idl::InOrderUpdateImpl for ServerImpl<'_> { .read_range(0..len, &mut header_block[..]) .map_err(|_| RequestError::Fail(ClientError::WentAway))?; header_block[len..].fill(ERASE_BYTE); - if let Err(e) = validate_header_block(target, header_block) { + if let Err(e) = validate_header_block(component, slot, header_block) + { self.header_block = None; return Err(e.into()); } } else { - // The header block is currently block 0. We should ensure - // we've seen and cached it before proceeding with other - // blocks. Otherwise, we won't be able to complete the update in - // `finish_image_update`. - if self.header_block.is_none() { - return Err(UpdateError::MissingHeaderBlock.into()); - } + // Block order is enforced above. If we're here then we have + // seen block zero already. block .read_range(0..len, &mut flash_page) .map_err(|_| RequestError::Fail(ClientError::WentAway))?; @@ -179,7 +203,13 @@ impl idl::InOrderUpdateImpl for ServerImpl<'_> { flash_page[len..].fill(ERASE_BYTE); } - do_block_write(&mut self.flash, target, block_num, &flash_page)?; + do_block_write( + &mut self.flash, + component, + slot, + block_num, + &flash_page, + )?; Ok(()) } @@ -188,6 +218,7 @@ impl idl::InOrderUpdateImpl for ServerImpl<'_> { &mut self, _: &RecvMessage, ) -> Result<(), RequestError> { + ringbuf_entry!(Trace::State(self.state)); match self.state { UpdateState::NoUpdate => { return Err(UpdateError::UpdateNotStarted.into()) @@ -202,14 +233,31 @@ impl idl::InOrderUpdateImpl for ServerImpl<'_> { return Err(UpdateError::MissingHeaderBlock.into()); } + // Check for nothing written + let endblock = + self.next_block.ok_or(UpdateError::MissingHeaderBlock)?; + if endblock == 0 { + // Nothing to do if no data was received. + return Err(UpdateError::MissingHeaderBlock.into()); + } + + let (component, slot) = self.image.unwrap_lite(); do_block_write( &mut self.flash, - self.image.unwrap_lite(), + component, + slot, HEADER_BLOCK, self.header_block.as_ref().unwrap_lite(), )?; + // Now erase the unused portion of the flash slot so that + // flash slot has predictable contents and the FWID for it + // has some meaning. + let range = image_range(component, slot).0; + let erase_start = range.start + (endblock as u32 * PAGE_SIZE); + self.flash_erase_range(erase_start..range.end)?; self.state = UpdateState::Finished; + ringbuf_entry!(Trace::State(self.state)); self.image = None; Ok(()) } @@ -237,19 +285,15 @@ impl idl::InOrderUpdateImpl for ServerImpl<'_> { &mut self, _: &RecvMessage, ) -> Result> { - // Safety: Data is published by stage0 - let addr = unsafe { BOOTSTATE.assume_init_ref() }; - RotBootState::load_from_addr(addr).map_err(|e| e.into()) + bootstate().map(RotBootState::from).map_err(|e| e.into()) } fn rot_boot_info( &mut self, _: &RecvMessage, ) -> Result> { - // Safety: Data is published by pre-kernel main - let addr = unsafe { BOOTSTATE.assume_init_ref() }; - let boot_state = RotBootState::load_from_addr(addr) - .map_err(|_| UpdateError::MissingHandoffData)?; + let boot_state = + bootstate().map_err(|_| UpdateError::MissingHandoffData)?; let ( persistent_boot_preference, pending_persistent_boot_preference, @@ -261,12 +305,67 @@ impl idl::InOrderUpdateImpl for ServerImpl<'_> { persistent_boot_preference, pending_persistent_boot_preference, transient_boot_preference, - slot_a_sha3_256_digest: boot_state.a.map(|details| details.digest), - slot_b_sha3_256_digest: boot_state.b.map(|details| details.digest), + // There is a change in meaning from the original + // RotBootInfo: + // Previously, None meant that the image did not + // validate, but there was no indication of the + // contents of the flash slot. Now, the FWID (hash + // of all programmed pages in the slot) is always + // available. The "valid image in the slot" semantic + // was not being used and is no longer available in + // this version of RotBootInfo. + slot_a_sha3_256_digest: Some(boot_state.a.digest), + slot_b_sha3_256_digest: Some(boot_state.b.digest), }; Ok(info) } + fn versioned_rot_boot_info( + &mut self, + _: &RecvMessage, + version: u8, + ) -> Result> { + let boot_state = + bootstate().map_err(|_| UpdateError::MissingHandoffData)?; + let ( + persistent_boot_preference, + pending_persistent_boot_preference, + transient_boot_preference, + ) = self.boot_preferences()?; + + match version { + // There are deprecated versions + 0 => Err(UpdateError::VersionNotSupported.into()), + 1 => Ok(VersionedRotBootInfo::V1(RotBootInfo { + active: boot_state.active.into(), + persistent_boot_preference, + pending_persistent_boot_preference, + transient_boot_preference, + slot_a_sha3_256_digest: Some(boot_state.a.digest), + slot_b_sha3_256_digest: Some(boot_state.b.digest), + })), + // Forward compatibility: If our caller wants a higher + // version than we know about, return the highest that we + // do know about. + // Rollback protection and deprecation of older versions + // will allow us to eventually remove old implementations. + _ => Ok(VersionedRotBootInfo::V2(RotBootInfoV2 { + active: boot_state.active.into(), + persistent_boot_preference, + pending_persistent_boot_preference, + transient_boot_preference, + slot_a_fwid: Fwid::Sha3_256(boot_state.a.digest), + slot_b_fwid: Fwid::Sha3_256(boot_state.b.digest), + stage0_fwid: Fwid::Sha3_256(boot_state.stage0.digest), + stage0next_fwid: Fwid::Sha3_256(boot_state.stage0next.digest), + slot_a_status: boot_state.a.status, + slot_b_status: boot_state.b.status, + stage0_status: boot_state.stage0.status, + stage0next_status: boot_state.stage0next.status, + })), + } + } + fn read_raw_caboose( &mut self, _msg: &RecvMessage, @@ -274,7 +373,7 @@ impl idl::InOrderUpdateImpl for ServerImpl<'_> { offset: u32, data: Leased, ) -> Result<(), RequestError> { - let caboose = caboose_slice(&self.flash, slot)?; + let caboose = caboose_slice(&self.flash, RotComponent::Hubris, slot)?; if offset as usize + data.len() > caboose.len() { return Err(RawCabooseError::InvalidRead.into()); } @@ -291,7 +390,7 @@ impl idl::InOrderUpdateImpl for ServerImpl<'_> { _: &RecvMessage, slot: SlotId, ) -> Result> { - let caboose = caboose_slice(&self.flash, slot)?; + let caboose = caboose_slice(&self.flash, RotComponent::Hubris, slot)?; Ok(caboose.end - caboose.start) } @@ -301,126 +400,7 @@ impl idl::InOrderUpdateImpl for ServerImpl<'_> { slot: SlotId, duration: SwitchDuration, ) -> Result<(), RequestError> { - match duration { - SwitchDuration::Once => { - // TODO deposit command token into buffer - return Err(UpdateError::NotImplemented.into()); - } - SwitchDuration::Forever => { - // Locate and return the authoritative CFPA flash word number - // and the CFPA version for that flash number. - // - // There are two "official" copies of the CFPA, referred to as - // ping and pong. One of them will supercede the other, based on - // a monotonic version field at offset 4. We'll take the - // contents of whichever one is most recent, alter them, and - // then write them into the _third_ copy, called the scratch - // page. - // - // At reset, the boot ROM will inspect the scratch page, check - // invariants, and copy it to overwrite the older of the ping - // and pong pages if it approves. - // - // That means you can apply this operation several times before - // resetting without burning many monotonic versions, if you - // want to do that for some reason. - // - // The addresses of these pages are as follows (see Figure 13, - // "Protected Flash Region," in UM11126 rev 2.4, or the NXP - // flash layout spreadsheet): - // - // Page Addr 16-byte word number - // Scratch 0x9_DE00 0x9DE0 - // Ping 0x9_E000 0x9E00 - // Pong 0x9_E200 0x9E20 - let (cfpa_word_number, _) = - self.cfpa_word_number_and_version(CfpaPage::Active)?; - - // Read current CFPA contents. - let mut cfpa = [[0u32; 4]; 512 / 16]; - indirect_flash_read_words( - &self.flash, - cfpa_word_number, - &mut cfpa, - )?; - - // Alter the boot setting, if it needs changing. The boot - // setting (per RFD 374) is in the lowest bit of the 32-bit word - // starting at (byte) offset 0x100. This is flash word offset - // 0x10. - // - // Leave remaining bits undisturbed; they are currently - // reserved. - let offset = BOOT_PREFERENCE_FLASH_WORD_OFFSET as usize; - let bit = cfpa[offset][0] & 1; - let new_bit = u32::from(slot != SlotId::A); - if bit == new_bit { - // No need to write the CFPA if it's unchanged - return Ok(()); - } - cfpa[offset][0] &= !1; - cfpa[offset][0] |= new_bit; - // Increment the monotonic version. The manual doesn't specify - // how the version numbers are compared or what happens if they - // wrap, so, we'll treat wrapping as an error and report it for - // now. (Note that getting this version to wrap _should_ require - // more write cycles than the flash can take.) - let new_version = - cfpa[0][1].checked_add(1).ok_or(UpdateError::SecureErr)?; - cfpa[0][1] = new_version; - // The last two flash words are a SHA256 hash of the preceding - // data. This means we need to compute a SHA256 hash of the - // preceding data -- meaning flash words 0 thru 29 inclusive. - let cfpa_hash = { - // We leave the hashcrypt unit in reset when unused, - // starting in the `main` function, so we only need to bring - // it _out of_ reset here. - self.syscon - .leave_reset(drv_lpc55_syscon_api::Peripheral::HashAes); - let mut h = drv_lpc55_sha256::Hasher::begin( - self.hashcrypt, - notifications::HASHCRYPT_IRQ_MASK, - ); - for chunk in &cfpa[..30] { - h.update(chunk, 0); - } - let hash = h.finish(); - - // Put it back. - self.syscon - .enter_reset(drv_lpc55_syscon_api::Peripheral::HashAes); - - hash - }; - cfpa[30] = cfpa_hash[..4].try_into().unwrap_lite(); - cfpa[31] = cfpa_hash[4..].try_into().unwrap_lite(); - - // Recast that as a page-sized byte array because that's what - // the update side of the machinery wants. The try_into on the - // second line can't fail at runtime, but there's no good - // support for casting between fixed-size arrays in zerocopy - // yet. - let cfpa_bytes: &[u8] = cfpa.as_bytes(); - let cfpa_bytes: &[u8; BLOCK_SIZE_BYTES] = - cfpa_bytes.try_into().unwrap_lite(); - - // Erase and program the scratch page. Note that because the - // scratch page is _not_ the authoritative copy, and because the - // ROM will check its contents before making it authoritative, - // we can fail during this operation without corrupting anything - // permanent. Yay! - // - self.flash - .write_page( - CFPA_SCRATCH_FLASH_ADDR, - cfpa_bytes, - wait_for_flash_interrupt, - ) - .map_err(|_| UpdateError::FlashError)?; - } - } - - Ok(()) + self.switch_default_hubris_image(slot, duration) } /// Reset. @@ -428,11 +408,12 @@ impl idl::InOrderUpdateImpl for ServerImpl<'_> { &mut self, _: &RecvMessage, ) -> Result<(), RequestError> { + ringbuf_entry!(Trace::State(self.state)); if self.state == UpdateState::InProgress { return Err(UpdateError::UpdateInProgress.into()); } self.syscon.chip_reset(); - panic!() + unreachable!(); } fn read_rot_page( @@ -456,8 +437,6 @@ impl idl::InOrderUpdateImpl for ServerImpl<'_> { } }; - const PAGE_SIZE: u32 = BYTES_PER_FLASH_PAGE as u32; - copy_from_flash_range( &self.flash, start_addr..(start_addr + PAGE_SIZE), @@ -466,6 +445,90 @@ impl idl::InOrderUpdateImpl for ServerImpl<'_> { )?; Ok(()) } + + fn component_caboose_size( + &mut self, + _msg: &userlib::RecvMessage, + component: RotComponent, + slot: SlotId, + ) -> Result> { + let caboose = caboose_slice(&self.flash, component, slot)?; + Ok(caboose.end - caboose.start) + } + + fn component_read_raw_caboose( + &mut self, + _msg: &RecvMessage, + component: RotComponent, + slot: SlotId, + offset: u32, + data: Leased, + ) -> Result<(), idol_runtime::RequestError> { + let caboose = caboose_slice(&self.flash, component, slot)?; + if offset as usize + data.len() > caboose.len() { + return Err(RawCabooseError::InvalidRead.into()); + } + copy_from_caboose_chunk( + &self.flash, + caboose, + offset..offset + data.len() as u32, + data, + ) + } + + fn component_prep_image_update( + &mut self, + _msg: &userlib::RecvMessage, + component: RotComponent, + slot: SlotId, + ) -> Result<(), RequestError> { + // The LPC55 doesn't have an easily accessible mass erase mechanism + // so this is just bookkeeping + ringbuf_entry!(Trace::State(self.state)); + ringbuf_entry!(Trace::Prep(component, slot)); + match self.state { + UpdateState::InProgress => { + return Err(UpdateError::UpdateInProgress.into()) + } + UpdateState::Finished | UpdateState::NoUpdate => (), + } + + self.image = match (component, slot) { + (RotComponent::Hubris, SlotId::A) + | (RotComponent::Hubris, SlotId::B) + | (RotComponent::Stage0, SlotId::B) => Some((component, slot)), + _ => return Err(UpdateError::InvalidSlotIdForOperation.into()), + }; + self.state = UpdateState::InProgress; + ringbuf_entry!(Trace::State(self.state)); + self.next_block = None; + self.fw_cache.fill(0); + Ok(()) + } + + fn component_switch_default_image( + &mut self, + _: &userlib::RecvMessage, + component: RotComponent, + slot: SlotId, + duration: SwitchDuration, + ) -> Result<(), RequestError> { + match component { + RotComponent::Hubris => { + self.switch_default_hubris_image(slot, duration) + } + RotComponent::Stage0 => { + match slot { + // Stage0 + SlotId::A => { + Err(UpdateError::InvalidSlotIdForOperation.into()) + } + // Stage0Next + SlotId::B => self.switch_default_boot_image(duration), + } + } + } + } } impl NotificationHandler for ServerImpl<'_> { @@ -573,6 +636,397 @@ impl ServerImpl<'_> { transient_boot_preference, )) } + + fn switch_default_boot_image( + &mut self, + duration: SwitchDuration, + ) -> Result<(), RequestError> { + if duration != SwitchDuration::Forever { + return Err(UpdateError::NotImplemented.into()); + } + + // Any image that passes the NXP image checks can be written to + // stage0next flash slot. + // The image checks ensure that the boot ROM will be able + // to check the image signature without crashing. + // + // Only an image that has a valid signature seen at rot-startup + // time will be copied by update-server to the stage0 partition. + // + // Note that both stage0 and stage0next images can be + // modified multiple times between resets. + // So, although we trust the bootstate(), we do not implicitly trust + // the current contents of stage0 or stage0next unless the image + // hash matches an image seen at rot-startup. + // + // The typical flow is that: + // 1. A new bootloader image is staged by update-server. + // 2. The RoT is reset. + // 3. Signatures are evaluated in rot-startup. + // 4. The validated staged image is copied by update-server + // to the bootloader slot. + // 5. The RoT is rebooted into the new stage0 image. + // + // During step 4, there is a time where the RoT is not bootable. + // Failures during step 4 may require a service call or RMA to fix + // the system. + // + // Only one Gimlet, PSC, or SideCar in a rack should have their RoT + // bootloader updated at a time to minimize the blast-radius of + // failures such as a rack wide power issue. + // + // All of the stage0next slots in the rack can be updated (steps 1 + // through 3) to ensure that RoT image signatures are valid before any + // system continues to step 4. + // + // TBD: While Failures up to step 3 do not adversly affect the RoT, + // resetting the RoT to evaluate signatures may be service affecting + // to the system depending on how the RoT and SP interact with respect + // to their reset handling and the RoT measurement of the SP. + // Appropriate measures should be taken to minimize customer impact. + // + // Note that after copying stage0next to stage0, but before step 5 + // (reset), it is still possible to revert to the old stage0 by + // updating stage0next to the original stage0 contents that were + // validated reset and then copying those to stage0. + // + // It is assumed that a hash collision is not computaionally feasible + // for either the image hash done by rot-startup or used by the ROM + // signature routine. + + // Read stage0next contents into RAM. + let staged = image_range(RotComponent::Stage0, SlotId::B); + let len = self.read_flash_image_to_cache(staged.0)?; + let bootloader = &self.fw_cache[..len / core::mem::size_of::()]; + + let mut hash = Sha3_256::new(); + for page in bootloader.as_bytes().chunks(512) { + hash.update(page); + } + let cache_hash: [u8; 32] = hash.finalize().into(); + let boot_state = + bootstate().map_err(|_| UpdateError::MissingHandoffData)?; + + // The cached image needs to match a properly signed image seen at + // boot time. + // Since stage0 and stage0next can be mutated after boot, we don't + // compare to their current contents. + // We are trusting that the recorded hash and signature status have + // not been altered since boot and that hash collisions are not an issue. + if !(boot_state.stage0next.status.is_ok() + && cache_hash.as_bytes() == boot_state.stage0next.digest + || boot_state.stage0.status.is_ok() + && cache_hash.as_bytes() == boot_state.stage0.digest) + { + return Err(UpdateError::SignatureNotValidated.into()); + }; + + // Don't risk an update if the cache already matches the bootloader. + let stage0 = image_range(RotComponent::Stage0, SlotId::A); + match self.compare_cache_to_flash(&stage0.0) { + Err(UpdateError::ImageMismatch) => { + if let Err(e) = + self.write_cache_to_flash(RotComponent::Stage0, SlotId::A) + { + // N.B. An error here is bad since it means we've likely + // bricked the machine if we reset now. + // We do not want the RoT reset. + // Upper layers should try to write the image again. + // We don't have a valid copy of the code. + // TODO: Think about possible recovery if stage0 + // has been corrupted. + + // Restart update_server + return Err(e.into()); + } + } + Ok(()) => { + // Unerased pages after an image are also hashed and + // therefore contribute to the firmware ID. + // This mechanism helps detect "dirty" flash banks + // and the possible exfiltration of data or incomplete + // update operations. + // It will produce a false negative for image matching + // but this is intended. + } + Err(e) => { + return Err(e.into()); // Non-fatal error. We did not alter stage0. + } + } + + // Finish by erasing the unused portion of flash bank. + // An error here means that the stage0 slot may not be clean but at least + // it has the intended bootloader written. + let erase_start = stage0.0.start.checked_add(len as u32).unwrap_lite(); + self.flash_erase_range(erase_start..stage0.0.end)?; + Ok(()) + } + + fn flash_erase_range( + &mut self, + span: Range, + ) -> Result<(), UpdateError> { + // It's assumed that the caller has done safe math and that + // there is no danger here. + let word_span = (span.start / (BYTES_PER_FLASH_WORD as u32)) + ..=(span.end / (BYTES_PER_FLASH_WORD as u32) - 1); + + self.flash.start_erase_range(word_span); + loop { + match self.flash.poll_erase_or_program_result() { + None => continue, + Some(Ok(())) => return Ok(()), + Some(Err(_)) => return Err(UpdateError::FlashError), + } + } + } + + fn compare_cache_to_flash( + &self, + span: &Range, + ) -> Result<(), UpdateError> { + // Is there a cached image? + // no, return error + + // Lengths are rounded up to a flash page boundary. + let clen = self.cache_image_len()?; + let flen = self.flash_image_len(span)?; + if clen != flen { + return Err(UpdateError::ImageMismatch); + } + // compare flash page to cache + let cached = + self.fw_cache[0..flen / core::mem::size_of::()].as_bytes(); + let mut flash_page = [0u8; BYTES_PER_FLASH_PAGE]; + for addr in (0..flen).step_by(BYTES_PER_FLASH_PAGE) { + let size = if addr + BYTES_PER_FLASH_PAGE > flen { + flen - addr + } else { + BYTES_PER_FLASH_PAGE + }; + + indirect_flash_read( + &self.flash, + addr as u32, + &mut flash_page[..size], + )?; + if flash_page[0..size] != cached[addr..addr + size] { + return Err(UpdateError::ImageMismatch); + } + } + Ok(()) + } + + // Looking at a region of flash, determine if there is a possible NXP + // image programmed. Return the length in bytes of the flash pages + // comprising the image including padding to fill to a page boundary. + fn flash_image_len(&self, span: &Range) -> Result { + let buf = &mut [0u32; 1]; + indirect_flash_read( + &self.flash, + span.start + LENGTH_OFFSET as u32, + buf[..].as_bytes_mut(), + )?; + if let Some(len) = round_up_to_flash_page(buf[0]) { + // The minimum image size should be further constrained + // but this is enough bytes for an NXP header and not + // bigger than the flash slot. + if len as usize <= span.len() && len >= HEADER_OFFSET { + return Ok(len as usize); + } + } + Err(UpdateError::BadLength) + } + + fn cache_image_len(&self) -> Result { + let len = round_up_to_flash_page( + self.fw_cache[LENGTH_OFFSET / core::mem::size_of::()], + ) + .ok_or(UpdateError::BadLength)?; + + if len as usize > self.fw_cache.as_bytes().len() || len < HEADER_OFFSET + { + return Err(UpdateError::BadLength); + } + Ok(len as usize) + } + + fn read_flash_image_to_cache( + &mut self, + span: Range, + ) -> Result { + // Returns error if flash page is erased. + let staged = image_range(RotComponent::Stage0, SlotId::B); + let len = self.flash_image_len(&staged.0)?; + if len as u32 > span.end || len > self.fw_cache.as_bytes().len() { + return Err(UpdateError::BadLength); + } + indirect_flash_read( + &self.flash, + span.start, + self.fw_cache[0..len / core::mem::size_of::()].as_bytes_mut(), + )?; + Ok(len) + } + + fn write_cache_to_flash( + &mut self, + component: RotComponent, + slot: SlotId, + ) -> Result<(), UpdateError> { + let clen = self.cache_image_len()?; + if clen % BYTES_PER_FLASH_PAGE != 0 { + return Err(UpdateError::BadLength); + } + let span = image_range(component, slot).0; + if span.end < span.start + clen as u32 { + return Err(UpdateError::BadLength); + } + // Sanity check could be repeated here. + // erase/write each flash page. + let chunks = self.fw_cache[..] + .as_bytes() + .chunks_exact(BYTES_PER_FLASH_PAGE); + for (block_num, block) in chunks.enumerate() { + let flash_page = block.try_into().unwrap_lite(); + do_block_write( + &mut self.flash, + component, + slot, + block_num, + flash_page, + )?; + } + Ok(()) + } + + fn switch_default_hubris_image( + &mut self, + slot: SlotId, + duration: SwitchDuration, + ) -> Result<(), RequestError> { + match duration { + SwitchDuration::Once => { + // TODO deposit command token into buffer + return Err(UpdateError::NotImplemented.into()); + } + SwitchDuration::Forever => { + // Locate and return the authoritative CFPA flash word number + // and the CFPA version for that flash number. + // + // There are two "official" copies of the CFPA, referred to as + // ping and pong. One of them will supercede the other, based on + // a monotonic version field at offset 4. We'll take the + // contents of whichever one is most recent, alter them, and + // then write them into the _third_ copy, called the scratch + // page. + // + // At reset, the boot ROM will inspect the scratch page, check + // invariants, and copy it to overwrite the older of the ping + // and pong pages if it approves. + // + // That means you can apply this operation several times before + // resetting without burning many monotonic versions, if you + // want to do that for some reason. + // + // The addresses of these pages are as follows (see Figure 13, + // "Protected Flash Region," in UM11126 rev 2.4, or the NXP + // flash layout spreadsheet): + // + // Page Addr 16-byte word number + // Scratch 0x9_DE00 0x9DE0 + // Ping 0x9_E000 0x9E00 + // Pong 0x9_E200 0x9E20 + let (cfpa_word_number, _) = + self.cfpa_word_number_and_version(CfpaPage::Active)?; + + // Read current CFPA contents. + let mut cfpa = [[0u32; 4]; 512 / 16]; + indirect_flash_read_words( + &self.flash, + cfpa_word_number, + &mut cfpa, + )?; + + // Alter the boot setting, if it needs changing. The boot + // setting (per RFD 374) is in the lowest bit of the 32-bit word + // starting at (byte) offset 0x100. This is flash word offset + // 0x10. + // + // Leave remaining bits undisturbed; they are currently + // reserved. + let offset = BOOT_PREFERENCE_FLASH_WORD_OFFSET as usize; + let bit = cfpa[offset][0] & 1; + #[allow(clippy::bool_to_int_with_if)] + let new_bit = if slot == SlotId::A { 0 } else { 1 }; + if bit == new_bit { + // No need to write the CFPA if it's unchanged + return Ok(()); + } + cfpa[offset][0] &= !1; + cfpa[offset][0] |= new_bit; + // Increment the monotonic version. The manual doesn't specify + // how the version numbers are compared or what happens if they + // wrap, so, we'll treat wrapping as an error and report it for + // now. (Note that getting this version to wrap _should_ require + // more write cycles than the flash can take.) + let new_version = + cfpa[0][1].checked_add(1).ok_or(UpdateError::SecureErr)?; + cfpa[0][1] = new_version; + // The last two flash words are a SHA256 hash of the preceding + // data. This means we need to compute a SHA256 hash of the + // preceding data -- meaning flash words 0 thru 29 inclusive. + let cfpa_hash = { + // We leave the hashcrypt unit in reset when unused, + // starting in the `main` function, so we only need to bring + // it _out of_ reset here. + self.syscon + .leave_reset(drv_lpc55_syscon_api::Peripheral::HashAes); + let mut h = drv_lpc55_sha256::Hasher::begin( + self.hashcrypt, + notifications::HASHCRYPT_IRQ_MASK, + ); + for chunk in &cfpa[..30] { + h.update(chunk, 0); + } + let hash = h.finish(); + + // Put it back. + self.syscon + .enter_reset(drv_lpc55_syscon_api::Peripheral::HashAes); + + hash + }; + cfpa[30] = cfpa_hash[..4].try_into().unwrap_lite(); + cfpa[31] = cfpa_hash[4..].try_into().unwrap_lite(); + + // Recast that as a page-sized byte array because that's what + // the update side of the machinery wants. The try_into on the + // second line can't fail at runtime, but there's no good + // support for casting between fixed-size arrays in zerocopy + // yet. + let cfpa_bytes: &[u8] = cfpa.as_bytes(); + let cfpa_bytes: &[u8; BLOCK_SIZE_BYTES] = + cfpa_bytes.try_into().unwrap_lite(); + + // Erase and program the scratch page. Note that because the + // scratch page is _not_ the authoritative copy, and because the + // ROM will check its contents before making it authoritative, + // we can fail during this operation without corrupting anything + // permanent. Yay! + // + self.flash + .write_page( + CFPA_SCRATCH_FLASH_ADDR, + cfpa_bytes, + wait_for_flash_interrupt, + ) + .map_err(|_| UpdateError::FlashError)?; + } + } + + Ok(()) + } } // Return the preferred slot to boot from for a given CFPA boot selection @@ -690,60 +1144,12 @@ fn indirect_flash_read( Ok(()) } -// Perform some sanity checking on the header block. -fn validate_header_block( - target: UpdateTarget, - block: &[u8; BLOCK_SIZE_BYTES], -) -> Result<(), UpdateError> { - // TODO: Do some actual checks for stage0. This will likely change - // with Cliff's bootloader. - if target == UpdateTarget::Bootloader { - return Ok(()); - } - - // This part aliases flash in two positions that differ in bit 28. To allow - // for either position to be used in new images, we clear bit 28 in all of - // the numbers used for comparison below, by ANDing them with this mask: - const ADDRMASK: u32 = !(1 << 28); - - let reset_vector = u32::from_le_bytes( - block[RESET_VECTOR_OFFSET..][..4].try_into().unwrap_lite(), - ) & ADDRMASK; - let a_base = unsafe { __IMAGE_A_BASE.as_ptr() } as u32 & ADDRMASK; - let b_base = unsafe { __IMAGE_B_BASE.as_ptr() } as u32 & ADDRMASK; - let stage0_base = unsafe { __IMAGE_STAGE0_BASE.as_ptr() } as u32 & ADDRMASK; - let a_end = unsafe { __IMAGE_A_END.as_ptr() } as u32 & ADDRMASK; - let b_end = unsafe { __IMAGE_B_END.as_ptr() } as u32 & ADDRMASK; - let stage0_end = unsafe { __IMAGE_STAGE0_END.as_ptr() } as u32 & ADDRMASK; - - // Ensure the image is destined for the right target - let valid = match target { - UpdateTarget::ImageA => (a_base..a_end).contains(&reset_vector), - UpdateTarget::ImageB => (b_base..b_end).contains(&reset_vector), - UpdateTarget::Bootloader => { - (stage0_base..stage0_end).contains(&reset_vector) - } - UpdateTarget::_Reserved => false, - }; - if !valid { - return Err(UpdateError::InvalidHeaderBlock); - } - - // Ensure the MAGIC is correct - let magic = - u32::from_le_bytes(block[MAGIC_OFFSET..][..4].try_into().unwrap_lite()); - if magic != abi::HEADER_MAGIC { - return Err(UpdateError::InvalidHeaderBlock); - } - - Ok(()) -} - /// Performs an erase-write sequence to a single page within a given target /// image. fn do_block_write( flash: &mut drv_lpc55_flash::Flash<'_>, - img: UpdateTarget, + component: RotComponent, + slot: SlotId, block_num: usize, flash_page: &[u8; BLOCK_SIZE_BYTES], ) -> Result<(), UpdateError> { @@ -752,11 +1158,11 @@ fn do_block_write( let page_num = block_num as u32; // Can only update opposite image - if same_image(img) { + if same_image(component, slot) { return Err(UpdateError::RunningImage); } - let write_addr = match target_addr(img, page_num) { + let write_addr = match target_addr(component, slot, page_num) { Some(addr) => addr, None => return Err(UpdateError::OutOfBounds), }; @@ -771,50 +1177,26 @@ fn wait_for_flash_interrupt() { sys_recv_notification(notifications::FLASH_IRQ_MASK); } -fn same_image(which: UpdateTarget) -> bool { - get_base(which) == unsafe { __this_image.as_ptr() } as u32 -} - -/// Returns the byte address of the first byte of the given flash target slot, -/// or panics if you're holding it wrong -fn get_base(which: UpdateTarget) -> u32 { - (match which { - UpdateTarget::ImageA => unsafe { __IMAGE_A_BASE.as_ptr() }, - UpdateTarget::ImageB => unsafe { __IMAGE_B_BASE.as_ptr() }, - UpdateTarget::Bootloader => unsafe { __IMAGE_STAGE0_BASE.as_ptr() }, - UpdateTarget::_Reserved => panic!(), - }) as u32 -} - -fn get_end(which: UpdateTarget) -> u32 { - (match which { - UpdateTarget::ImageA => unsafe { __IMAGE_A_END.as_ptr() }, - UpdateTarget::ImageB => unsafe { __IMAGE_B_END.as_ptr() }, - UpdateTarget::Bootloader => unsafe { __IMAGE_STAGE0_END.as_ptr() }, - UpdateTarget::_Reserved => panic!(), - }) as u32 -} - -/// Computes the byte address of the first byte in a particular (slot, page) -/// combination. -/// -/// `image_target` designates the flash slot and must be `ImageA`, `ImageB`, or -/// `Bootloader`, despite containing other variants. All other choices will -/// panic. (TODO: fix this when time permits.) +/// Computes the byte address of the first byte in a +/// particular (component, slot, page) combination. /// /// `page_num` designates a flash page (called a block elsewhere in this file, a /// 512B unit) within the flash slot. If the page is out range for the target /// slot, returns `None`. -fn target_addr(image_target: UpdateTarget, page_num: u32) -> Option { - let base = get_base(image_target); +fn target_addr( + component: RotComponent, + slot: SlotId, + page_num: u32, +) -> Option { + let range = image_range(component, slot).0; // This is safely calculating addr = base + page_num * PAGE_SIZE let addr = page_num - .checked_mul(BLOCK_SIZE_BYTES as u32)? - .checked_add(base)?; + .checked_mul(BLOCK_SIZE_BYTES as u32) + .and_then(|n| n.checked_add(range.start))?; // check addr + PAGE_SIZE <= end - if addr.checked_add(BLOCK_SIZE_BYTES as u32)? > get_end(image_target) { + if addr.checked_add(BLOCK_SIZE_BYTES as u32)? > range.end { return None; } @@ -827,21 +1209,10 @@ fn target_addr(image_target: UpdateTarget, page_num: u32) -> Option { /// but uses indirect reads instead of mapping the alternate bank into flash. fn caboose_slice( flash: &drv_lpc55_flash::Flash<'_>, + component: RotComponent, slot: SlotId, ) -> Result, RawCabooseError> { - // SAFETY: these symbols are populated by the linker - let (image_start, image_region_end) = unsafe { - match slot { - SlotId::A => ( - __IMAGE_A_BASE.as_ptr() as u32, - __IMAGE_A_END.as_ptr() as u32, - ), - SlotId::B => ( - __IMAGE_B_BASE.as_ptr() as u32, - __IMAGE_B_END.as_ptr() as u32, - ), - } - }; + let flash_range = image_range(component, slot).0; // If all is going according to plan, there will be a valid Hubris image // flashed into the other slot, delimited by `__IMAGE_A/B_BASE` and @@ -855,7 +1226,7 @@ fn caboose_slice( indirect_flash_read( flash, - image_start + HEADER_OFFSET, + flash_range.start + HEADER_OFFSET, header.as_bytes_mut(), ) .map_err(|_| RawCabooseError::ReadFailed)?; @@ -866,34 +1237,38 @@ fn caboose_slice( // Calculate where the image header implies that the image should end // // This is a one-past-the-end value. - let image_end = image_start + header.total_image_len; + let image_end = flash_range.start + header.total_image_len; // Then, check that value against the BANK2 bounds. // - // SAFETY: populated by the linker, so this should be valid - if image_end > image_region_end { + // Safety: populated by the linker, so this should be valid + if image_end > flash_range.end { return Err(RawCabooseError::MissingCaboose); } // By construction, the last word of the caboose is its size as a `u32` let mut caboose_size = 0u32; - indirect_flash_read(flash, image_end - 4, caboose_size.as_bytes_mut()) - .map_err(|_| RawCabooseError::ReadFailed)?; + indirect_flash_read( + flash, + image_end - U32_SIZE, + caboose_size.as_bytes_mut(), + ) + .map_err(|_| RawCabooseError::ReadFailed)?; let caboose_start = image_end.saturating_sub(caboose_size); - let caboose_range = if caboose_start < image_start { + let caboose_range = if caboose_start < flash_range.start { // This branch will be encountered if there's no caboose, because // then the nominal caboose size will be 0xFFFFFFFF, which will send // us out of the bank2 region. return Err(RawCabooseError::MissingCaboose); } else { - // SAFETY: we know this pointer is within the programmed flash region, + // Safety: we know this pointer is within the programmed flash region, // since it's checked above. let mut v = 0u32; indirect_flash_read(flash, caboose_start, v.as_bytes_mut()) .map_err(|_| RawCabooseError::ReadFailed)?; if v == CABOOSE_MAGIC { - caboose_start + 4..image_end - 4 + caboose_start + U32_SIZE..image_end - U32_SIZE } else { return Err(RawCabooseError::MissingCaboose); } @@ -956,6 +1331,16 @@ fn copy_from_flash_range( Ok(()) } +fn bootstate() -> Result { + // Safety: Data is published by stage0 + let addr = unsafe { BOOTSTATE.assume_init_ref() }; + RotBootStateV2::load_from_addr(addr) +} + +fn round_up_to_flash_page(offset: u32) -> Option { + offset.checked_next_multiple_of(BYTES_PER_FLASH_PAGE as u32) +} + task_slot!(SYSCON, syscon); task_slot!(JEFE, jefe); @@ -965,6 +1350,9 @@ fn main() -> ! { // Go ahead and put the HASHCRYPT unit into reset. syscon.enter_reset(drv_lpc55_syscon_api::Peripheral::HashAes); + let fw_cache = mutable_statics::mutable_statics! { + static mut FW_CACHE: [u32; FW_CACHE_MAX / core::mem::size_of::()] = [|| 0; _]; + }; let mut server = ServerImpl { header_block: None, state: UpdateState::NoUpdate, @@ -975,6 +1363,8 @@ fn main() -> ! { }), hashcrypt: unsafe { &*lpc55_pac::HASHCRYPT::ptr() }, syscon, + fw_cache, + next_block: None, }; let mut incoming = [0u8; idl::INCOMING_SIZE]; @@ -988,7 +1378,8 @@ include!(concat!(env!("OUT_DIR"), "/notifications.rs")); mod idl { use super::{ HandoffDataLoadError, ImageVersion, RawCabooseError, RotBootInfo, - RotPage, SlotId, SwitchDuration, UpdateTarget, + RotComponent, RotPage, SlotId, SwitchDuration, UpdateTarget, + VersionedRotBootInfo, }; include!(concat!(env!("OUT_DIR"), "/server_stub.rs")); diff --git a/drv/sprot-api/src/lib.rs b/drv/sprot-api/src/lib.rs index e0cd98aae..949b7e8ed 100644 --- a/drv/sprot-api/src/lib.rs +++ b/drv/sprot-api/src/lib.rs @@ -21,8 +21,10 @@ pub use error::{ use crc::{Crc, CRC_16_XMODEM}; use derive_more::From; pub use drv_lpc55_update_api::{ - HandoffDataLoadError, RawCabooseError, RotBootInfo, RotBootState, RotPage, - RotSlot, SlotId, SwitchDuration, UpdateTarget, + Fwid, HandoffDataLoadError, ImageError, ImageVersion, RawCabooseError, + RotBootInfo, RotBootInfoV2, RotBootState, RotBootStateV2, RotComponent, + RotImageDetails, RotPage, RotSlot, SlotId, SwitchDuration, UpdateTarget, + VersionedRotBootInfo, }; pub use drv_update_api::UpdateError; use hubpack::SerializedSize; @@ -390,12 +392,38 @@ pub enum UpdateReq { Reset, // Added in sprot protocol version 3 BootInfo, + VersionedBootInfo { + version: u8, + }, + ComponentPrep { + component: RotComponent, + slot: SlotId, + }, + ComponentSwitchDefaultImage { + component: RotComponent, + slot: SlotId, + duration: SwitchDuration, + }, } #[derive(Clone, Serialize, Deserialize, SerializedSize)] pub enum CabooseReq { + /// Size of the caboose for Hubris slot A or B Size { slot: SlotId }, + /// Read caboose of Hubris slot A or B Read { slot: SlotId, start: u32, size: u32 }, + /// Size of the caboose of a component's slot A or B + ComponentSize { + component: RotComponent, + slot: SlotId, + }, + /// Read caboose of component's slot A or B + ComponentRead { + component: RotComponent, + slot: SlotId, + start: u32, + size: u32, + }, } #[derive(Clone, Serialize, Deserialize, SerializedSize)] @@ -416,6 +444,7 @@ pub enum UpdateRsp { BlockSize(u32), // Added in sprot protocol version 3 BootInfo(RotBootInfo), + VersionedBootInfo(VersionedRotBootInfo), } /// A response used for caboose requests @@ -425,6 +454,8 @@ pub enum UpdateRsp { pub enum CabooseRsp { Size(u32), Read, + ComponentSize(u32), + ComponentRead, } #[derive(Clone, Serialize, Deserialize, SerializedSize)] @@ -535,6 +566,9 @@ pub enum RotState { /// ROMs. bootrom_crc32: u32, }, + V2 { + state: RotBootStateV2, + }, } /// Stats from the RoT side of sprot @@ -616,11 +650,12 @@ pub struct SprotIoStats { impl SpRot { pub fn read_caboose_value( &self, + component: RotComponent, slot_id: SlotId, key: [u8; 4], buf: &mut [u8], ) -> Result { - let reader = RotCabooseReader::new(slot_id, self)?; + let reader = RotCabooseReader::new(component, slot_id, self)?; let len = reader.get(key, buf)?; Ok(len) } @@ -630,16 +665,28 @@ impl SpRot { struct RotCabooseReader<'a> { sprot: &'a SpRot, size: u32, + component: RotComponent, slot: SlotId, } impl<'a> RotCabooseReader<'a> { fn new( + component: RotComponent, slot: SlotId, sprot: &'a SpRot, ) -> Result { - let size = sprot.caboose_size(slot)?; - Ok(Self { size, slot, sprot }) + let size = match component { + // Use old API for backward compatibility until + // it can be deprecated with anti-rollback/epoch. + RotComponent::Hubris => sprot.caboose_size(slot)?, + _ => sprot.component_caboose_size(component, slot)?, + }; + Ok(Self { + size, + component, + slot, + sprot, + }) } pub fn get( @@ -723,9 +770,21 @@ impl tlvc::TlvcRead for &RotCabooseReader<'_> { let offset = offset .try_into() .map_err(|_| tlvc::TlvcReadError::Truncated)?; - self.sprot - .read_caboose_region(offset, self.slot, dest) - .map_err(tlvc::TlvcReadError::User) + match self.component { + RotComponent::Hubris => self + .sprot + .read_caboose_region(offset, self.slot, dest) + .map_err(tlvc::TlvcReadError::User), + _ => self + .sprot + .component_read_caboose_region( + offset, + self.component, + self.slot, + dest, + ) + .map_err(tlvc::TlvcReadError::User), + } } } diff --git a/drv/stm32h7-sprot-server/src/main.rs b/drv/stm32h7-sprot-server/src/main.rs index 00e9ce3ac..59b9d4b4b 100644 --- a/drv/stm32h7-sprot-server/src/main.rs +++ b/drv/stm32h7-sprot-server/src/main.rs @@ -8,7 +8,7 @@ use attest_api::{AttestError, HashAlgorithm, NONCE_MAX_SIZE, NONCE_MIN_SIZE}; use drv_lpc55_update_api::{ - RotBootInfo, RotPage, SlotId, SwitchDuration, UpdateTarget, + RotBootInfo, RotComponent, RotPage, SlotId, SwitchDuration, UpdateTarget, }; use drv_spi_api::{CsState, SpiDevice, SpiServer}; use drv_sprot_api::*; @@ -77,9 +77,11 @@ const TIMEOUT_QUICK: u32 = 5; const DEFAULT_ATTEMPTS: u16 = 3; /// Slightly longer timeout const TIMEOUT_MEDIUM: u32 = 50; +/// Long timeout +const TIMEOUT_LONG: u32 = 200; // Delay between asserting CSn and sending the portion of a message -// that fits entierly in the RoT's FIFO. +// that fits entirely in the RoT's FIFO. const PART1_DELAY: u64 = 0; // Delay between sending the portion of a message that fits entirely in the @@ -643,6 +645,28 @@ impl idl::InOrderSpRotImpl for ServerImpl { } } + /// Return more useful boot info about the RoT + fn versioned_rot_boot_info( + &mut self, + _msg: &userlib::RecvMessage, + version: u8, + ) -> Result> { + let body = ReqBody::Update(UpdateReq::VersionedBootInfo { version }); + let tx_size = Request::pack(&body, self.tx_buf); + let rsp = self.do_send_recv_retries( + tx_size, + TIMEOUT_QUICK, + DEFAULT_ATTEMPTS, + )?; + if let RspBody::Update(UpdateRsp::VersionedBootInfo(vboot_info)) = + rsp.body? + { + Ok(vboot_info) + } else { + Err(SprotProtocolError::UnexpectedResponse)? + } + } + /// Return the block size of the update server fn block_size( &mut self, @@ -714,9 +738,12 @@ impl idl::InOrderSpRotImpl for ServerImpl { ) -> Result<(), idol_runtime::RequestError> { let body = ReqBody::Update(UpdateReq::Finish); let tx_size = Request::pack(&body, self.tx_buf); + // For stage0next updates, erase and flash doesn't happen + // until the finish operations. Use a long timeout. let rsp = self.do_send_recv_retries( tx_size, - TIMEOUT_QUICK, + // TODO: Tune TIMEOUT_LONG and deal with retried finish_image_update. + TIMEOUT_LONG, DEFAULT_ATTEMPTS, )?; if let RspBody::Ok = rsp.body? { @@ -1190,6 +1217,132 @@ impl idl::InOrderSpRotImpl for ServerImpl { rsp.body?; Ok(()) } + + fn component_caboose_size( + &mut self, + _msg: &userlib::RecvMessage, + component: RotComponent, + slot: SlotId, + ) -> Result> { + let body = + ReqBody::Caboose(CabooseReq::ComponentSize { component, slot }); + let tx_size = Request::pack(&body, self.tx_buf); + let rsp = self + .do_send_recv_retries(tx_size, DUMP_TIMEOUT, 1) + .map_err(RawCabooseOrSprotError::Sprot)?; + match rsp.body { + Ok(RspBody::Caboose(Ok(CabooseRsp::ComponentSize(size)))) => { + Ok(size) + } + Ok(RspBody::Caboose(Err(e))) => { + Err(RawCabooseOrSprotError::Caboose(e).into()) + } + Ok(RspBody::Caboose(_)) | Ok(_) => { + Err(RawCabooseOrSprotError::Sprot(SprotError::Protocol( + SprotProtocolError::UnexpectedResponse, + )) + .into()) + } + Err(e) => Err(RawCabooseOrSprotError::Sprot(e).into()), + } + } + + fn component_read_caboose_region( + &mut self, + _msg: &userlib::RecvMessage, + offset: u32, + component: RotComponent, + slot: SlotId, + data: idol_runtime::Leased, + ) -> Result<(), idol_runtime::RequestError> { + let body = ReqBody::Caboose(CabooseReq::ComponentRead { + component, + slot, + start: offset, + size: data.len() as u32, + }); + let tx_size = Request::pack(&body, self.tx_buf); + let rsp = self + .do_send_recv_retries(tx_size, DUMP_TIMEOUT, 4) + .map_err(RawCabooseOrSprotError::Sprot)?; + + match rsp.body { + Ok(RspBody::Caboose(Ok(CabooseRsp::ComponentRead))) => { + // Copy from the trailing data into the lease + if rsp.blob.len() < data.len() { + return Err(idol_runtime::RequestError::Fail( + idol_runtime::ClientError::BadLease, + )); + } + data.write_range(0..data.len(), &rsp.blob[..data.len()]) + .map_err(|()| { + idol_runtime::RequestError::Fail( + idol_runtime::ClientError::WentAway, + ) + })?; + Ok(()) + } + Ok(RspBody::Caboose(Err(e))) => { + Err(RawCabooseOrSprotError::Caboose(e).into()) + } + Ok(RspBody::Caboose(_)) | Ok(_) => { + Err(RawCabooseOrSprotError::Sprot(SprotError::Protocol( + SprotProtocolError::UnexpectedResponse, + )) + .into()) + } + Err(e) => Err(RawCabooseOrSprotError::Sprot(e).into()), + } + } + + fn component_prep_image_update( + &mut self, + _msg: &userlib::RecvMessage, + component: RotComponent, + slot: SlotId, + ) -> Result<(), idol_runtime::RequestError> + where + SprotError: From, + { + let body = + ReqBody::Update(UpdateReq::ComponentPrep { component, slot }); + let tx_size = Request::pack(&body, self.tx_buf); + let rsp = self.do_send_recv_retries( + tx_size, + TIMEOUT_QUICK, + DEFAULT_ATTEMPTS, + )?; + if let RspBody::Ok = rsp.body? { + Ok(()) + } else { + Err(SprotProtocolError::UnexpectedResponse)? + } + } + + fn component_switch_default_image( + &mut self, + _msg: &userlib::RecvMessage, + component: RotComponent, + slot: SlotId, + duration: SwitchDuration, + ) -> Result<(), idol_runtime::RequestError> { + let body = ReqBody::Update(UpdateReq::ComponentSwitchDefaultImage { + component, + slot, + duration, + }); + let tx_size = Request::pack(&body, self.tx_buf); + let rsp = self.do_send_recv_retries( + tx_size, + TIMEOUT_QUICK, + DEFAULT_ATTEMPTS, + )?; + if let RspBody::Ok = rsp.body? { + Ok(()) + } else { + Err(SprotProtocolError::UnexpectedResponse)? + } + } } impl NotificationHandler for ServerImpl { @@ -1207,8 +1360,9 @@ impl NotificationHandler for ServerImpl { mod idl { use super::{ AttestOrSprotError, DumpOrSprotError, HashAlgorithm, PulseStatus, - RawCabooseOrSprotError, RotBootInfo, RotPage, RotState, SlotId, - SprotError, SprotIoStats, SprotStatus, SwitchDuration, UpdateTarget, + RawCabooseOrSprotError, RotBootInfo, RotComponent, RotPage, RotState, + SlotId, SprotError, SprotIoStats, SprotStatus, SwitchDuration, + UpdateTarget, VersionedRotBootInfo, }; include!(concat!(env!("OUT_DIR"), "/server_stub.rs")); diff --git a/drv/update-api/src/lib.rs b/drv/update-api/src/lib.rs index cd07f5ede..4f77a7333 100644 --- a/drv/update-api/src/lib.rs +++ b/drv/update-api/src/lib.rs @@ -60,6 +60,11 @@ pub enum UpdateError { NotImplemented, MissingHandoffData, + BlockOutOfOrder, + InvalidSlotIdForOperation, + ImageMismatch, + SignatureNotValidated, + VersionNotSupported, } impl From for GwUpdateError { @@ -91,6 +96,13 @@ impl From for GwUpdateError { UpdateError::TaskRestarted => Self::TaskRestarted, UpdateError::NotImplemented => Self::NotImplemented, UpdateError::MissingHandoffData => Self::MissingHandoffData, + UpdateError::BlockOutOfOrder => Self::BlockOutOfOrder, + UpdateError::InvalidSlotIdForOperation => { + Self::InvalidSlotIdForOperation + } + UpdateError::ImageMismatch => Self::ImageMismatch, + UpdateError::SignatureNotValidated => Self::SignatureNotValidated, + UpdateError::VersionNotSupported => Self::VersionNotSupported, } } } diff --git a/idl/lpc55-update.idol b/idl/lpc55-update.idol index 4b2d9d60d..cf2bec9b5 100644 --- a/idl/lpc55-update.idol +++ b/idl/lpc55-update.idol @@ -144,5 +144,73 @@ Interface( encoding: Hubpack, idempotent: true, ), + "component_caboose_size": ( + doc: "Returns the size of the caboose", + args: { + "component": "RotComponent", + "slot": "SlotId", + }, + reply: Result( + ok: "u32", + err: CLike("RawCabooseError"), + ), + idempotent: true, + encoding: Hubpack, + ), + "component_read_raw_caboose": ( + doc: "Reads a raw portion of the caboose", + args: { + "component": "RotComponent", + "slot": "SlotId", + "offset": "u32", + }, + leases: { + "data": (type: "[u8]", write: true), + }, + reply: Result( + ok: "()", + err: CLike("RawCabooseError"), + ), + idempotent: true, + encoding: Hubpack, + ), + "component_prep_image_update": ( + doc: "Do any necessary preparation for writing the image. This may include erasing flash and unlocking registers", + args: { + "component": "RotComponent", + "slot": "SlotId", + }, + reply: Result( + ok: "()", + err: CLike("drv_update_api::UpdateError"), + ), + encoding: Hubpack, + ), + "component_switch_default_image": ( + doc: "Prefer a specific image slot for one or many boots", + args: { + "component": "RotComponent", + "slot": "SlotId", + "duration": "SwitchDuration", + }, + reply: Result( + ok: "()", + err: CLike("drv_update_api::UpdateError"), + ), + encoding: Hubpack, + idempotent: true, + ), + "versioned_rot_boot_info": ( + doc: "RoT Boot selection and preference info", + args: { + "version": "u8", + }, + reply: Result( + ok: "VersionedRotBootInfo", + err: CLike("drv_update_api::UpdateError") + ), + idempotent: true, + encoding: Hubpack + ), }, ) diff --git a/idl/sprot.idol b/idl/sprot.idol index b6fc25ec4..2cdecea11 100644 --- a/idl/sprot.idol +++ b/idl/sprot.idol @@ -6,8 +6,8 @@ Interface( "status": ( doc: "Return status about the sprot protocol", reply: Result( - ok: "SprotStatus", - err: Complex("SprotError"), + ok: "SprotStatus", + err: Complex("SprotError"), ), encoding: Hubpack, idempotent: true, @@ -259,6 +259,7 @@ Interface( ), "attest": ( doc: "Get an attestation", + args: {}, leases: { "nonce": (type: "[u8]", read: true, max_len: Some(128)), "dest": (type: "[u8]", write: true), @@ -305,5 +306,74 @@ Interface( ), encoding: Hubpack, ), + "versioned_rot_boot_info": ( + doc: "Get a specific verions of RoT boot info if availabe.", + args: { + "version": "u8", + }, + reply: Result( + ok: "VersionedRotBootInfo", + err: Complex("SprotError"), + ), + encoding: Hubpack, + idempotent: true, + ), + "component_caboose_size": ( + doc: "Returns the size of the caboose", + args: { + "component": "RotComponent", + "slot": "SlotId", + }, + reply: Result( + ok: "u32", + err: Complex("RawCabooseOrSprotError"), + ), + encoding: Hubpack, + idempotent: true, + ), + "component_read_caboose_region": ( + doc: "Reads a subset of the caboose memory", + args: { + "offset": "u32", + "component": "RotComponent", + "slot": "SlotId", + }, + reply: Result( + ok: "()", + err: Complex("RawCabooseOrSprotError"), + ), + leases: { + "out": (type: "[u8]", write: true), + }, + encoding: Hubpack, + idempotent: true, + ), + "component_prep_image_update": ( + doc: "Do any necessary preparation for writing the image. This may include erasing flash and unlocking registers", + args: { + "component": "RotComponent", + "slot": "SlotId", + }, + reply: Result( + ok: "()", + err: Complex("SprotError"), + ), + encoding: Hubpack, + idempotent: true, + ), + "component_switch_default_image": ( + doc: "Prefer a specific image slot for one or many boots", + args: { + "component": "RotComponent", + "slot": "SlotId", + "duration": "SwitchDuration", + }, + reply: Result( + ok: "()", + err: Complex("SprotError"), + ), + encoding: Hubpack, + idempotent: true, + ), } ) diff --git a/lib/lpc55-rot-startup/Cargo.toml b/lib/lpc55-rot-startup/Cargo.toml index 8c486a5d6..b538702a6 100644 --- a/lib/lpc55-rot-startup/Cargo.toml +++ b/lib/lpc55-rot-startup/Cargo.toml @@ -24,6 +24,7 @@ zerocopy = { workspace = true } zeroize = { workspace = true } abi = { path = "../../sys/abi" } +kern = { path = "../../sys/kern" } armv8-m-mpu = { path = "../armv8-m-mpu" } lpc55-puf = { path = "../lpc55-puf", optional = true } lib-dice = { path = "../dice", optional = true } diff --git a/lib/lpc55-rot-startup/src/images.rs b/lib/lpc55-rot-startup/src/images.rs index 44b4c4806..1164aea43 100644 --- a/lib/lpc55-rot-startup/src/images.rs +++ b/lib/lpc55-rot-startup/src/images.rs @@ -2,45 +2,32 @@ // 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/. -use abi::{ImageHeader, ImageVectors}; +use abi::ImageHeader; use drv_lpc55_flash::{Flash, BYTES_PER_FLASH_PAGE}; +use lpc55_pac::SYSCON; use sha3::{Digest, Sha3_256}; -use stage0_handoff::{ImageVersion, RotImageDetails}; -use unwrap_lite::UnwrapLite; - -pub fn get_image_b(flash: &mut Flash<'_>) -> Option { - let imageb = unsafe { &__IMAGE_B_BASE }; - - let img = Image { - flash: FLASH_B, - vector: imageb, - }; - - if img.validate(flash) { - Some(img) - } else { - None - } -} - -pub fn get_image_a(flash: &mut Flash<'_>) -> Option { - let imagea = unsafe { &__IMAGE_A_BASE }; - - let img = Image { - flash: FLASH_A, - vector: imagea, - }; - - if img.validate(flash) { - Some(img) - } else { - None - } +use stage0_handoff::ImageError; +use zerocopy::AsBytes; + +const U32_SIZE: u32 = core::mem::size_of::() as u32; + +// Corresponds to the ARM vector table on the NXP LPC55S69, +// NXP uses reserved vectors for image information. +// See UM11126 Table 177 for details. +#[repr(C)] +#[derive(Default, AsBytes)] +pub struct ImageVectorsLpc55 { + pub sp: u32, + pub entry: u32, + _unrelated_vectors_0: [u32; 6], + pub nxp_image_length: u32, + pub nxp_image_type: u32, + pub nxp_offset_to_specific_header: u32, + _unrelated_vectors_1: [u32; 2], + pub nxp_image_execution_address: u32, } extern "C" { - static __IMAGE_A_BASE: abi::ImageVectors; - static __IMAGE_B_BASE: abi::ImageVectors; // __vector size is currently defined in the linker script as // // __vector_size = SIZEOF(.vector_table); @@ -55,124 +42,327 @@ extern "C" { // to do the u32 change everywhere const PAGE_SIZE: u32 = BYTES_PER_FLASH_PAGE as u32; -pub struct Image { +pub struct FlashSlot { flash: Range, - vector: &'static ImageVectors, + // The contiguous span of programmed flash pages starting at offset zero. + // Note that any additional programmed pages after the first erased + // page are not interesting for image sanity checks and are not included. + initial_programmed_extent: Range, + // Measurement over all pages including those that follow any erased page. + fwid: [u8; 32], } -pub fn image_details(img: Image, flash: &mut Flash<'_>) -> RotImageDetails { - RotImageDetails { - digest: img.get_fwid(flash), - version: img.get_image_version(), +impl FlashSlot { + pub fn new(flash: &mut Flash<'_>, slot: Range) -> FlashSlot { + // Find the extent of initial programmed pages while + // hashing all programmed pages in the flash slot. + let mut end: Option = None; + let mut hash = Sha3_256::new(); + for page_start in slot.clone().step_by(BYTES_PER_FLASH_PAGE) { + if flash.is_page_range_programmed(page_start, PAGE_SIZE) { + let page = unsafe { + core::slice::from_raw_parts( + page_start as *const u8, + BYTES_PER_FLASH_PAGE, + ) + }; + hash.update(page); + } else if end.is_none() { + end = Some(page_start); + } + } + let fwid = hash.finalize().into(); + let initial_programmed_extent = slot.start..end.unwrap_or(slot.end); + FlashSlot { + flash: slot, + initial_programmed_extent, + fwid, + } } -} -impl Image { - fn get_img_start(&self) -> u32 { - self.vector as *const ImageVectors as u32 + fn is_programmed(&self, addr: &u32) -> bool { + self.initial_programmed_extent.contains(addr) } - fn get_img_size(&self) -> Option { - usize::try_from((unsafe { &*self.get_header() }).total_image_len).ok() + // True if the flash slot's span of contiguous programmed pages + // starting at offset zero includes the given span. + fn is_span_programmed(&self, start: u32, length: u32) -> bool { + if let Some(end) = start.checked_add(length) { + self.is_programmed(&start) + && end <= self.initial_programmed_extent.end + } else { + false + } } - fn get_header(&self) -> *const ImageHeader { - // SAFETY: This generated by the linker script which we trust - // Note that this is generated from _this_ image's linker script - // as opposed to the _image_ linker script but those two _must_ - // be the same value! - let vector_size = unsafe { core::ptr::addr_of!(__vector_size) as u32 }; - (self.get_img_start() + vector_size) as *const ImageHeader + pub fn start(&self) -> u32 { + self.flash.start } - /// Make sure all of the image flash is programmed - fn validate(&self, flash: &mut Flash<'_>) -> bool { - let img_start = self.get_img_start(); + pub fn contains(&self, addr: &u32) -> bool { + self.flash.contains(addr) + } - // Start by making sure we can access the page where the vectors live - let valid = flash.is_page_range_programmed(img_start, PAGE_SIZE); + pub fn fwid(&self) -> [u8; 32] { + self.fwid + } +} - if !valid { - return false; - } +pub struct Image { + // The boundaries of the actual image. + span: Range, + // The runtime address range + run: Range, +} - let header_ptr = self.get_header(); +impl Image { + pub fn get_image_a( + flash: &mut Flash<'_>, + syscon: &SYSCON, + ) -> (FlashSlot, Result) { + let slot = FlashSlot::new(flash, FLASH_A); + let img = Image::new(&slot, FLASH_A, true, syscon); + (slot, img) + } - // Next validate the header location is programmed - let valid = - flash.is_page_range_programmed(header_ptr as u32, PAGE_SIZE); + pub fn get_image_b( + flash: &mut Flash<'_>, + syscon: &SYSCON, + ) -> (FlashSlot, Result) { + let slot = FlashSlot::new(flash, FLASH_B); + let img = Image::new(&slot, FLASH_B, true, syscon); + (slot, img) + } - if !valid { - return false; - } + pub fn get_image_stage0( + flash: &mut Flash<'_>, + syscon: &SYSCON, + ) -> (FlashSlot, Result) { + let slot = FlashSlot::new(flash, FLASH_STAGE0); + let img = Image::new(&slot, FLASH_STAGE0, false, syscon); + (slot, img) + } - // SAFETY: We've validated the header location is programmed so this - // will not trigger a fault. This is generated from our build scripts - // which we trust. - let header = unsafe { &*header_ptr }; + pub fn get_image_stage0next( + flash: &mut Flash<'_>, + syscon: &SYSCON, + ) -> (FlashSlot, Result) { + // Note that Stage0Next is not XIP until it gets copied to slot Stage0. + let slot = FlashSlot::new(flash, FLASH_STAGE0NEXT); + let img = Image::new(&slot, FLASH_STAGE0, false, syscon); + (slot, img) + } - // Does this look correct? - if header.magic != abi::HEADER_MAGIC { - return false; + /// Before treating a span from a FlashSlot as an image: + /// + /// - Find the image address boundaries. + /// - Sanity check the image. + /// - Check the image signature using the same ROM code as used at boot. + /// + /// If the image does not check out, the ImageError value should narrow + /// down the problem. + /// + // Note: if partially programmed pages, i.e. one or more erased words in a + // page, are a possibility that could be a problem with respect to + // unexpected crashes or catching exfiltration attempts. + fn new( + slot: &FlashSlot, + run: Range, + header_required: bool, + syscon: &SYSCON, + ) -> Result { + // Make sure we can access the page where the vectors live. + // Safety: Link time constants from our own image. + let vector_size = unsafe { core::ptr::addr_of!(__vector_size) as u32 }; + if !slot.is_span_programmed(slot.start(), vector_size) { + return Err(ImageError::FirstPageErased); + } + + let vectors = slot.start() as *const u8 as *const ImageVectorsLpc55; + // Safety: The address derives from link-time constants and + // we have ensured that the first page of the flash slot is not erased. + let vectors: &ImageVectorsLpc55 = unsafe { &*vectors }; + + // Check that the entire image from flash.start to the end of + // the header-declared signature block has been programmed. + let image_length = vectors.nxp_image_length; + let rounded_length = image_length + .checked_next_multiple_of(PAGE_SIZE) + .ok_or(ImageError::PartiallyProgrammed)?; + if !slot.is_span_programmed(slot.start(), rounded_length) { + // This also catches lengths that wrap around or exceed the slot. + return Err(ImageError::PartiallyProgrammed); } - let total_len = match header.total_image_len.checked_add(PAGE_SIZE - 1) + // The ImageHeader page(s) need to be programmed for later calls to be + // safe. + if image_length + < (vector_size + core::mem::size_of::() as u32) { - Some(s) => s & !(PAGE_SIZE - 1), - None => return false, + if header_required { + return Err(ImageError::HeaderNotProgrammed); + } else { + return Err(ImageError::BootloaderTooSmall); + } + } + + // TODO: Check that padding is 0xff. + + // After establishing that the entire image is programmed it's + // ok to start using the Image methods. + let img = Image { + // Safety: Image length has been checked. + span: slot.start()..(slot.start() + image_length), + run, }; - // Last step is to make sure the entire range is programmed - flash.is_page_range_programmed(img_start, total_len) + img.validate(header_required).and_then(|_| { + img.check_signature(syscon)?; + Ok(img) + }) } - pub fn get_fwid(&self, flash: &mut Flash<'_>) -> [u8; 32] { - let mut hash = Sha3_256::new(); + fn get_img_start(&self) -> u32 { + self.span.start + } - for start in self.flash.clone().step_by(BYTES_PER_FLASH_PAGE) { - if flash.is_page_range_programmed(start, PAGE_SIZE) { - // SAFETY: The addresses used in this unsafe code are all - // generated by build.rs from data in the build environment. - // The safety of this code is an extension of our trust in - // the build. - let page = unsafe { - core::slice::from_raw_parts( - start as *const u8, - BYTES_PER_FLASH_PAGE, - ) - }; - hash.update(page); - } - } + // Return a pointer to the NXP vector table in flash. + // N.B: Before calling, check that the first flash page is programmed. + fn get_vectors(&self) -> &ImageVectorsLpc55 { + let vectors = self.span.start as *const u8 as *const ImageVectorsLpc55; + // Safety: The address derives from a link-time constant and + // the caller has ensured that the first page of flash is not + // erased. + unsafe { &*vectors } + } - hash.finalize().into() + fn get_reset_vector(&self) -> u32 { + self.get_vectors().entry } - pub fn get_image_version(&self) -> ImageVersion { - // SAFETY: We checked this previously - let header = unsafe { &*self.get_header() }; + fn get_image_type(&self) -> u32 { + self.get_vectors().nxp_image_type + } - ImageVersion { - epoch: header.epoch, - version: header.version, + // Get a pointer to where the ImageHeader should be. + // Note that it may not be present if the image + // is corrupted or is a bootloader. + fn get_header_ptr(&self) -> *const ImageHeader { + // Safety: This is generated by the linker script which we trust + // Note that this is generated from _this_ image's linker script + // as opposed to the _image_ linker script but those two _must_ + // be the same value! + let vector_size = unsafe { core::ptr::addr_of!(__vector_size) as u32 }; + (self.get_img_start() + vector_size) as *const ImageHeader + } + + fn get_imageheader(&self) -> Result<&ImageHeader, ImageError> { + // Check Hubris header. + // Note that bootloaders without Hubris headers have been released. + let header_ptr = self.get_header_ptr(); + + // Safety: We've validated the header location is programmed so this + // will not trigger a fault. + // The values used are all link-time constants. + let header = unsafe { &*header_ptr }; + if header.magic != abi::HEADER_MAGIC { + return Err(ImageError::BadMagic); } + Ok(header) } - fn pointer_range(&self) -> core::ops::Range<*const u8> { - let img_ptr = self.get_img_start() as *const u8; - // The MPU requires 32 byte alignment and so the compiler pads the - // image accordingly. The length field from the image header does not - // (and should not) account for this padding so we must do that here. - let img_size = (self.get_img_size().unwrap_lite() + 31) & !31; + fn get_imageheader_total_image_len(&self) -> Result { + Ok(self.get_imageheader()?.total_image_len) + } + + /// Test an image for viability. + fn validate(&self, header_required: bool) -> Result<(), ImageError> { + // The signature validation routine could be called now. + // Any additional checks should be moot based on the signing + // procedure only signing "good" images. + // + // However, the price of flashing a bad bootloader is high and + // the criteria for signing images evolves over time. So, as + // long as we can afford the space and time, perform extra checks + // to aid diagnosis of bad images and to protect the system. + // + // There is also the concern that the ROM signature check routine + // might not fully protect itself from bad input. + // + // Consider deleting any of the following tests once there + // is high confidence that non-conforming signed images are + // no longer a threat. + + // Bootloaders without Hubris headers have been released. + // So, check ImageHeader carefully. + if header_required { + let len = self.get_imageheader_total_image_len()?; + if (len % U32_SIZE) != 0 { + return Err(ImageError::UnalignedLength); + } + match self.span.start.checked_add(len) { + Some(end) => { + if !self.span.contains(&end) { + return Err(ImageError::HeaderImageSize); + } + } + None => return Err(ImageError::HeaderImageSize), + }; + } + + // Because of our past experience with the implementation quality of the + // ROM, let's do some basic checks before handing it a blob to inspect, + // shall we? + + const MASK_WITHOUT_28: u32 = !(1 << 28); + let reset_vector = MASK_WITHOUT_28 & self.get_reset_vector(); - // Safety: this is unsafe because the pointer addition could overflow. - // If that happens, we'll produce an empty range or crash with a panic. - // We do not dereference these here pointers. - img_ptr..unsafe { img_ptr.add(img_size) } + // Verify that the reset vector is a valid Thumb-2 function pointer. + if reset_vector & 1 == 0 { + // This'll cause an immediate usage fault. Reject it. + return Err(ImageError::ResetVectorNotThumb2); + } + + // Ensure that the reset vector is within the runtime address range. + let runtime = self.run.start..(self.run.start + self.span.len() as u32); + if !runtime.contains(&reset_vector) { + return Err(ImageError::ResetVector); + } + + // The image type is at offset 0x24. We only allow type 4. + // - 0x0000 Normal image for unsecure boot + // - 0x0001 Plain signed Image + // - 0x0002 Plain CRC Image, CRC at offset 0x28 + // - 0x0004 Plain signed XIP Image + // - 0x0005 Plain CRC XIP Image, CRC at offset 0x28 + // - 0x8001 Signed plain Image with KeyStore Included + if self.get_image_type() != 4 { + return Err(ImageError::UnsupportedType); + } + + Ok(()) } - pub fn contains(&self, address: *const u8) -> bool { - self.pointer_range().contains(&address) + // Assuming a well-formed image, get the result of the ROM signature check + // routine. + fn check_signature(&self, syscon: &SYSCON) -> Result<(), ImageError> { + syscon + .presetctrl2 + .modify(|_, w| w.hash_aes_rst().released()); + + let authorized = unsafe { + lpc55_romapi::authenticate_image(self.span.start).is_ok() + }; + + syscon + .presetctrl2 + .modify(|_, w| w.hash_aes_rst().asserted()); + + if authorized { + Ok(()) + } else { + Err(ImageError::Signature) + } } } diff --git a/lib/lpc55-rot-startup/src/lib.rs b/lib/lpc55-rot-startup/src/lib.rs index 42f7fe9d7..6db2a1876 100644 --- a/lib/lpc55-rot-startup/src/lib.rs +++ b/lib/lpc55-rot-startup/src/lib.rs @@ -18,7 +18,7 @@ use handoff::Handoff; use armv8_m_mpu::{disable_mpu, enable_mpu}; use cortex_m::peripheral::MPU; -use stage0_handoff::{RotBootState, RotSlot}; +use stage0_handoff::{RotBootStateV2, RotImageDetailsV2, RotSlot}; const ROM_VER: u32 = 1; @@ -93,7 +93,7 @@ fn enable_debug(peripherals: &lpc55_pac::Peripherals) { // This information comes from Section 4.5.83 of UM11126 const SYSCON_SWDCPU0: u32 = 0x4000_0FB4; // Enable SWD port access for CPU0 - // SAFETY: writing anything other than the defined magic will lock + // Safety: writing anything other than the defined magic will lock // out debug access which is the behavior we want here! unsafe { core::ptr::write_volatile(SYSCON_SWDCPU0 as *mut u32, SWD_MAGIC); @@ -171,22 +171,55 @@ pub fn startup( #[cfg(any(feature = "dice-mfg", feature = "dice-self"))] puf_check(&peripherals.PUF); - // Write the image details to handoff RAM. Use the address of the current - // function to determine which image is running. - let img_a = images::get_image_a(&mut flash); - let img_b = images::get_image_b(&mut flash); - let here = startup as *const u8; - let active = if img_a.as_ref().map(|i| i.contains(here)).unwrap_or(false) { + // Write the image details to handoff RAM. + + // Pre-main code makes calls to the ROM-based signature + // verification routines and requires its own HASHCRYPT IRQ handler. + set_hashcrypt_rom(); + + let (slot_a, img_a) = + images::Image::get_image_a(&mut flash, &peripherals.SYSCON); + let (slot_b, img_b) = + images::Image::get_image_b(&mut flash, &peripherals.SYSCON); + let (slot_stage0, img_stage0) = + images::Image::get_image_stage0(&mut flash, &peripherals.SYSCON); + let (slot_stage0next, img_stage0next) = + images::Image::get_image_stage0next(&mut flash, &peripherals.SYSCON); + + // Once the kernel is started, the normal HASHCRYPT IRQ handler needs to + // be active. + set_hashcrypt_default(); + + // Use the address of the current function to determine which image + // is running. + let here = startup as *const u8 as u32; + let active = if slot_a.contains(&here) { RotSlot::A - } else if img_b.as_ref().map(|i| i.contains(here)).unwrap_or(false) { + } else if slot_b.contains(&here) { RotSlot::B } else { panic!(); }; - let a = img_a.map(|i| images::image_details(i, &mut flash)); - let b = img_b.map(|i| images::image_details(i, &mut flash)); - let details = RotBootState { active, a, b }; + let details = RotBootStateV2 { + active, + a: RotImageDetailsV2 { + digest: slot_a.fwid(), + status: img_a.map(|_| ()), + }, + b: RotImageDetailsV2 { + digest: slot_b.fwid(), + status: img_b.map(|_| ()), + }, + stage0: RotImageDetailsV2 { + digest: slot_stage0.fwid(), + status: img_stage0.map(|_| ()), + }, + stage0next: RotImageDetailsV2 { + digest: slot_stage0next.fwid(), + status: img_stage0next.map(|_| ()), + }, + }; handoff.store(&details); @@ -290,3 +323,28 @@ extern "C" fn nuke_stack() { ) } } + +static USE_ROM: core::sync::atomic::AtomicBool = + core::sync::atomic::AtomicBool::new(false); + +pub fn set_hashcrypt_default() { + USE_ROM.store(false, core::sync::atomic::Ordering::Relaxed); +} + +pub fn set_hashcrypt_rom() { + USE_ROM.store(true, core::sync::atomic::Ordering::Relaxed); +} + +#[allow(non_snake_case)] +#[no_mangle] +// SAFETY: The atomic bool is only manipulated from the kernel pre-main context. +// This interrupt handler re-directs to the ROM to allow pre-main to use the +// ROM's signature checking routine. All HASHCRYPT interrupts after kernel main() +// use the normal Hubris interrupt handling. +pub unsafe extern "C" fn HASHCRYPT() { + if USE_ROM.load(core::sync::atomic::Ordering::Relaxed) { + lpc55_romapi::skboot_hashcrypt_handler(); + } else { + kern::arch::DefaultHandler(); + } +} diff --git a/lib/stage0-handoff/REAME.md b/lib/stage0-handoff/README.md similarity index 100% rename from lib/stage0-handoff/REAME.md rename to lib/stage0-handoff/README.md diff --git a/lib/stage0-handoff/src/lib.rs b/lib/stage0-handoff/src/lib.rs index 19afd239d..ea29d662c 100644 --- a/lib/stage0-handoff/src/lib.rs +++ b/lib/stage0-handoff/src/lib.rs @@ -12,7 +12,8 @@ use static_assertions::const_assert; mod rot_update_details; pub use rot_update_details::{ - ImageVersion, RotBootState, RotImageDetails, RotSlot, + ImageError, ImageVersion, RotBootState, RotBootStateV2, RotImageDetails, + RotImageDetailsV2, RotSlot, }; // This memory is the USB peripheral SRAM that's 0x4000 bytes long. Changes @@ -28,7 +29,6 @@ const_assert!(DICE_RANGE.end <= UPDATE_RANGE.start); const_assert!(UPDATE_RANGE.end <= MEM_RANGE.end); /// The error returned when `HandoffData::load` fails. #[derive( - Debug, Clone, Copy, PartialEq, @@ -96,7 +96,7 @@ pub unsafe trait HandoffData { { // Cast the MEM_START address to a slice of bytes of MAX_SIZE length. // - // SAFETY: This unsafe block relies on implementers of the trait to + // Safety: This unsafe block relies on implementers of the trait to // validate the memory range denoted by Self::MEM_RANGE. Each // implementation in this module is checked by static assertion. let src = unsafe { diff --git a/lib/stage0-handoff/src/rot_update_details.rs b/lib/stage0-handoff/src/rot_update_details.rs index 6b4acdd34..ce20d36e3 100644 --- a/lib/stage0-handoff/src/rot_update_details.rs +++ b/lib/stage0-handoff/src/rot_update_details.rs @@ -7,12 +7,49 @@ use core::ops::Range; use hubpack::SerializedSize; use serde::{Deserialize, Serialize}; -unsafe impl HandoffData for RotBootState { +unsafe impl HandoffData for RotBootStateV2 { const VERSION: u32 = 0; const MAGIC: [u8; 12] = *b"whatwhatwhat"; const MEM_RANGE: Range = UPDATE_RANGE; } +/// The pre-kernel Hubris code evaluates all flash slots. +/// It is expected that the stage0 and running Hubris image will be ok. +/// The information on stage0next and the other Hubris partition is used +/// by the update_server to qualify stage0next before promotion to stage0 and +/// can be used by the control plane to diagnose failing updates. +#[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, + /// An image not requiring an ImageHeader 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, +} + /// Top-level type describing images loaded into flash on the RoT. /// /// This data is injected into RAM at `UPDATE_RANGE` by stage0. @@ -30,22 +67,113 @@ pub struct RotBootState { impl RotBootState { pub fn active_image(&self) -> Option { match self.active { - RotSlot::A => self.a.clone(), - RotSlot::B => self.b.clone(), + RotSlot::A => self.a, + RotSlot::B => self.b, } } } -fits_in_ram!(RotBootState); +impl From for RotBootState { + // Conversion to handle deprecated APIs. + fn from(v2: RotBootStateV2) -> Self { + let a = match v2.a.status { + Ok(_status) => Some(RotImageDetails { + digest: v2.a.digest, + version: ImageVersion { + version: 0, + epoch: 0, + }, + }), + Err(_) => None, + }; + let b = match v2.b.status { + Ok(_status) => Some(RotImageDetails { + digest: v2.b.digest, + version: ImageVersion { + version: 0, + epoch: 0, + }, + }), + Err(_) => None, + }; + RotBootState { + active: v2.active, + a, + b, + } + } +} #[derive( - Debug, Clone, PartialEq, Eq, Deserialize, Serialize, SerializedSize, + Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, SerializedSize, +)] +pub struct RotBootStateV2 { + pub active: RotSlot, + pub a: RotImageDetailsV2, + pub b: RotImageDetailsV2, + pub stage0: RotImageDetailsV2, + pub stage0next: RotImageDetailsV2, +} + +impl RotBootStateV2 { + pub fn active_image(&self) -> Option { + RotBootState::from(*self).active_image() + } +} + +fits_in_ram!(RotBootStateV2); + +#[derive( + Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, SerializedSize, )] pub struct RotImageDetails { + /// The SHA3-256 measurement of all programmed pages in the flash slot. pub digest: [u8; 32], + /// ImageVersion is not used anywhere and should be deprecated. pub version: ImageVersion, } +/// A measurement of all programmed pages in the flash slot. +/// +/// FWID is not a simple digest of the image bytes. +/// The image is padded to the next native flash page with 0xff bytes. +/// Any additional programmed pages in the flash slot beyond the image are +/// included. +/// If there is no valid image, then the digest is over all the programmed +/// pages. +/// +/// The intent is to detect incomplete updates where unused pages are not +/// erased or possible exfiltration of date in the unused pages. +/// +/// The unused pages in a flash slot could be put to use in some later release +/// to store state. But, no use case has been identified at this time. +/// +/// Note that no page is supposed to be partially programmed/partially erased. +/// The LPC55 might reasonably report any page that does not have the final +/// internal ECC syndrome written as being erased. If that +/// is the case, then one would not expect to +/// TODO: There should be testing that creates a partially programmed page if +/// that is possible. +/// +#[derive( + Copy, Clone, PartialEq, Eq, Deserialize, Serialize, SerializedSize, +)] +pub enum Fwid { + /// Image should have been written with the last flash page padded with + /// 0xff bytes. All non-erased pages in the flash slot are included in the + /// digest. + Sha3_256([u8; 32]), +} + +#[derive( + Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, SerializedSize, +)] +pub struct RotImageDetailsV2 { + pub digest: [u8; 32], + // Image is valid and properly signed or has a specific ImageError to help find the problem. + pub status: Result<(), ImageError>, +} + #[derive( Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, SerializedSize, )] diff --git a/task/control-plane-agent/src/main.rs b/task/control-plane-agent/src/main.rs index f1a13e6ec..7e12803c8 100644 --- a/task/control-plane-agent/src/main.rs +++ b/task/control-plane-agent/src/main.rs @@ -46,7 +46,7 @@ task_slot!(NET, net); task_slot!(SYS, sys); #[allow(dead_code)] // Not all cases are used by all variants -#[derive(Debug, Clone, Copy, PartialEq, ringbuf::Count)] +#[derive(Clone, Copy, PartialEq, ringbuf::Count)] enum Log { #[count(skip)] Empty, @@ -85,7 +85,7 @@ enum Log { // `Log` enum above (which itself is only used by our ringbuf logs). The MGS // protocol is defined in the `gateway-messages` crate (which is shared with // MGS proper and other tools like `sp-sim` in the omicron repository). -#[derive(Debug, Clone, Copy, PartialEq, ringbuf::Count)] +#[derive(Clone, Copy, PartialEq, ringbuf::Count)] enum MgsMessage { Discovery, IgnitionState { @@ -165,11 +165,14 @@ enum MgsMessage { }, ReadRotPage, VpdLockStatus, + VersionedRotBootInfo { + version: u8, + }, } // This enum does not define the actual IPC protocol - it is only used in the // `Log` enum above (which itself is only used by our ringbuf logs). -#[derive(Debug, Clone, Copy, PartialEq, ringbuf::Count)] +#[derive(Clone, Copy, PartialEq, ringbuf::Count)] enum IpcRequest { FetchHostPhase2Data, GetHostPhase2Data, @@ -190,7 +193,7 @@ enum IpcRequest { counted_ringbuf!(Log, 16, Log::Empty); -#[derive(Copy, Clone, Debug, PartialEq, ringbuf::Count)] +#[derive(Copy, Clone, PartialEq, ringbuf::Count)] enum CriticalEvent { Empty, /// We have received a network request to change power states. This record diff --git a/task/control-plane-agent/src/mgs_common.rs b/task/control-plane-agent/src/mgs_common.rs index f92e56816..f135a8c57 100644 --- a/task/control-plane-agent/src/mgs_common.rs +++ b/task/control-plane-agent/src/mgs_common.rs @@ -9,15 +9,33 @@ use crate::{ }; use drv_caboose::{CabooseError, CabooseReader}; use drv_sprot_api::{ - CabooseOrSprotError, RotState as SprotRotState, SpRot, SprotError, - SprotProtocolError, SwitchDuration, + CabooseOrSprotError, + Fwid as SpFwid, + ImageError as SpImageError, + ImageVersion as SpImageVersion, + RotBootInfo as SpRotBootInfo, + RotBootInfoV2 as SpRotBootInfoV2, + RotComponent as SpRotComponent, + // RotImageDetails as SpRotImageDetails, + SlotId as SpSlotId, + SpRot, + SprotError, + SprotProtocolError, + SwitchDuration, + VersionedRotBootInfo as SpVersionedRotBootInfo, }; use drv_stm32h7_update_api::Update; use gateway_messages::{ - CfpaPage, DiscoverResponse, PowerState, RotError, RotRequest, RotResponse, - RotSlotId, RotStateV2, SensorReading, SensorRequest, SensorRequestKind, - SensorResponse, SpComponent, SpError, SpPort, SpStateV2, UpdateStatus, - VpdError as GatewayVpdError, WatchdogError, + CfpaPage, DiscoverResponse, Fwid as GwFwid, ImageError as GwImageError, + ImageVersion as GwImageVersion, PowerState, RotBootInfo as GwRotBootInfo, + RotBootState as GwRotBootState, RotError, + RotImageDetails as GwRotImageDetails, RotRequest, RotResponse, + RotSlotId as GwRotSlotId, RotState as GwRotState, + RotStateV2 as GwRotStateV2, RotStateV3 as GwRotStateV3, + RotUpdateDetails as GwRotUpdateDetails, SensorReading, SensorRequest, + SensorRequestKind, SensorResponse, SpComponent, SpError as GwSpError, + SpPort as GwSpPort, SpStateV2 as GwSpStateV2, UpdateStatus, + VpdError as GwVpdError, WatchdogError, }; use ringbuf::ringbuf_entry_root as ringbuf_entry; use static_assertions::const_assert; @@ -69,8 +87,8 @@ impl MgsCommon { pub(crate) fn discover( &mut self, - port: SpPort, - ) -> Result { + port: GwSpPort, + ) -> Result { ringbuf_entry!(Log::MgsMessage(MgsMessage::Discovery)); Ok(DiscoverResponse { sp_port: port }) } @@ -87,7 +105,7 @@ impl MgsCommon { pub(crate) fn sp_state( &mut self, power_state: PowerState, - ) -> Result { + ) -> Result { // SpState has extra-wide fields for the serial and model number. Below // when we fill them in we use `usize::min` to pick the right length // regardless of which is longer, but really we want to know we aren't @@ -101,14 +119,18 @@ impl MgsCommon { let id = self.identity(); - let mut state = SpStateV2 { + let mut state = GwSpStateV2 { serial_number: [0; SP_STATE_FIELD_WIDTH], model: [0; SP_STATE_FIELD_WIDTH], revision: id.revision, hubris_archive_id: kipc::read_image_id().to_le_bytes(), base_mac_address: self.base_mac_address.0, power_state, - rot: rot_state(&self.sprot), + rot: self + .sprot + .rot_boot_info() + .map(|s| MgsRotStateV2::from(s).0) + .map_err(RotError::from), }; let n = usize::min(state.serial_number.len(), id.serial.len()); @@ -131,22 +153,22 @@ impl MgsCommon { slot: u16, key: [u8; 4], buf: &mut [u8], - ) -> Result { + ) -> Result { let caboose_to_sp_error = |e| { match e { - CabooseError::NoSuchTag => SpError::NoSuchCabooseKey(key), - CabooseError::MissingCaboose => SpError::NoCaboose, - CabooseError::BadChecksum => SpError::BadCabooseChecksum, + CabooseError::NoSuchTag => GwSpError::NoSuchCabooseKey(key), + CabooseError::MissingCaboose => GwSpError::NoCaboose, + CabooseError::BadChecksum => GwSpError::BadCabooseChecksum, CabooseError::TlvcReaderBeginFailed | CabooseError::RawReadFailed | CabooseError::InvalidRead | CabooseError::TlvcReadExactFailed => { - SpError::CabooseReadError + GwSpError::CabooseReadError } // NoImageHeader is only returned when reading the caboose of the // bank2 slot; it shouldn't ever be returned by the local reader. - CabooseError::NoImageHeader => SpError::NoCaboose, + CabooseError::NoImageHeader => GwSpError::NoCaboose, } }; let caboose_or_sprot_to_sp_error = |e| match e { @@ -160,11 +182,11 @@ impl MgsCommon { let reader = drv_caboose_pos::CABOOSE_POS .as_slice() .map(CabooseReader::new) - .ok_or(SpError::NoCaboose)?; + .ok_or(GwSpError::NoCaboose)?; let v = reader.get(key).map_err(caboose_to_sp_error)?; let len = v.len(); if len > buf.len() { - Err(SpError::CabooseValueOverflow(len as u32)) + Err(GwSpError::CabooseValueOverflow(len as u32)) } else { buf[..len].copy_from_slice(v); Ok(len) @@ -177,19 +199,39 @@ impl MgsCommon { .map_err(caboose_to_sp_error)?; Ok(len as usize) } - _ => Err(SpError::InvalidSlotForComponent), + _ => Err(GwSpError::InvalidSlotForComponent), }, SpComponent::ROT => { let slot_id = slot .try_into() - .map_err(|()| SpError::InvalidSlotForComponent)?; + .map_err(|()| GwSpError::InvalidSlotForComponent)?; let len = self .sprot - .read_caboose_value(slot_id, key, buf) + .read_caboose_value( + SpRotComponent::Hubris, + slot_id, + key, + buf, + ) .map_err(caboose_or_sprot_to_sp_error)?; Ok(len as usize) } - _ => Err(SpError::RequestUnsupportedForComponent), + SpComponent::STAGE0 => { + let slot_id = slot + .try_into() + .map_err(|()| GwSpError::InvalidSlotForComponent)?; + let len = self + .sprot + .read_caboose_value( + SpRotComponent::Stage0, + slot_id, + key, + buf, + ) + .map_err(caboose_or_sprot_to_sp_error)?; + Ok(len as usize) + } + _ => Err(GwSpError::RequestUnsupportedForComponent), } } @@ -208,7 +250,7 @@ impl MgsCommon { pub(crate) fn reset_component_prepare( &mut self, component: SpComponent, - ) -> Result<(), SpError> { + ) -> Result<(), GwSpError> { self.reset_component_requested = Some(component); Ok(()) } @@ -227,9 +269,9 @@ impl MgsCommon { pub(crate) fn reset_component_trigger( &mut self, component: SpComponent, - ) -> Result<(), SpError> { + ) -> Result<(), GwSpError> { if self.reset_component_requested != Some(component) { - return Err(SpError::ResetComponentTriggerWithoutPrepare); + return Err(GwSpError::ResetComponentTriggerWithoutPrepare); } // If we are not resetting the SP_ITSELF, then we may come back here // to reset something else or to run another prepare/trigger on @@ -285,24 +327,25 @@ impl MgsCommon { } // mgs_{gimlet,psc,sidecar}.rs deal with any board specific // reset strategy. Here we take care of common SP and RoT cases. - _ => Err(SpError::RequestUnsupportedForComponent), + _ => Err(GwSpError::RequestUnsupportedForComponent), } } pub(crate) fn component_get_active_slot( &mut self, component: SpComponent, - ) -> Result { + ) -> Result { match component { SpComponent::ROT => { - let SprotRotState::V1 { state, .. } = self.sprot.rot_state()?; - let slot = match state.active { - drv_sprot_api::RotSlot::A => 0, - drv_sprot_api::RotSlot::B => 1, + let slot = match self.sprot.rot_boot_info()?.active { + SpSlotId::A => 0, + SpSlotId::B => 1, }; Ok(slot) } - _ => Err(SpError::RequestUnsupportedForComponent), + // We know that the LPC55S69 RoT bootloader does not have switchable banks. + SpComponent::STAGE0 => Ok(0), + _ => Err(GwSpError::RequestUnsupportedForComponent), } } @@ -311,12 +354,12 @@ impl MgsCommon { component: SpComponent, slot: u16, persist: bool, - ) -> Result<(), SpError> { + ) -> Result<(), GwSpError> { match component { SpComponent::ROT => { let slot = slot .try_into() - .map_err(|()| SpError::RequestUnsupportedForComponent)?; + .map_err(|()| GwSpError::RequestUnsupportedForComponent)?; let duration = if persist { SwitchDuration::Forever } else { @@ -326,6 +369,23 @@ impl MgsCommon { Ok(()) } + SpComponent::STAGE0 => { + let slot = slot + .try_into() + .map_err(|()| GwSpError::RequestUnsupportedForComponent)?; + let duration = if persist { + SwitchDuration::Forever + } else { + SwitchDuration::Once + }; + self.sprot.component_switch_default_image( + SpRotComponent::Stage0, + slot, + duration, + )?; + Ok(()) + } + // SpComponent::SP_ITSELF: // update_server for SP needs to decouple finish_update() // from swap_banks() for SwitchDuration::Forever to make sense. @@ -333,17 +393,17 @@ impl MgsCommon { // enables SwitchDuration::Once. // // Other components might also be served someday. - _ => Err(SpError::RequestUnsupportedForComponent), + _ => Err(GwSpError::RequestUnsupportedForComponent), } } pub(crate) fn read_sensor( &mut self, req: SensorRequest, - ) -> Result { + ) -> Result { use gateway_messages::SensorError; let id = SensorId::try_from(req.id) - .map_err(|_| SpError::Sensor(SensorError::InvalidSensor))?; + .map_err(|_| GwSpError::Sensor(SensorError::InvalidSensor))?; match req.kind { SensorRequestKind::ErrorCount => { @@ -354,7 +414,7 @@ impl MgsCommon { let (value, timestamp) = self .sensor .get_raw_reading(id) - .ok_or(SpError::Sensor(SensorError::NoReading))?; + .ok_or(GwSpError::Sensor(SensorError::NoReading))?; Ok(SensorResponse::LastReading(SensorReading { value: value.map_err(translate_sensor_nodata), timestamp, @@ -364,14 +424,14 @@ impl MgsCommon { let (value, timestamp) = self .sensor .get_last_data(id) - .ok_or(SpError::Sensor(SensorError::NoReading))?; + .ok_or(GwSpError::Sensor(SensorError::NoReading))?; Ok(SensorResponse::LastData { value, timestamp }) } SensorRequestKind::LastError => { let (nodata, timestamp) = self .sensor .get_last_nodata(id) - .ok_or(SpError::Sensor(SensorError::NoReading))?; + .ok_or(GwSpError::Sensor(SensorError::NoReading))?; Ok(SensorResponse::LastError { value: translate_sensor_nodata(nodata), timestamp, @@ -380,7 +440,7 @@ impl MgsCommon { } } - pub(crate) fn current_time(&mut self) -> Result { + pub(crate) fn current_time(&mut self) -> Result { Ok(sys_get_timer().now) } @@ -388,7 +448,7 @@ impl MgsCommon { &mut self, req: RotRequest, buf: &mut [u8], - ) -> Result { + ) -> Result { ringbuf_entry!(Log::MgsMessage(MgsMessage::ReadRotPage)); let page = match req { RotRequest::ReadCmpa => drv_sprot_api::RotPage::Cmpa, @@ -416,16 +476,16 @@ impl MgsCommon { pub(crate) fn vpd_lock_status_all( &self, _buf: &mut [u8], - ) -> Result { + ) -> Result { ringbuf_entry!(Log::MgsMessage(MgsMessage::VpdLockStatus)); - Err(SpError::Vpd(GatewayVpdError::NotImplemented)) + Err(GwSpError::Vpd(GwVpdError::NotImplemented)) } #[cfg(feature = "vpd")] pub(crate) fn vpd_lock_status_all( &self, buf: &mut [u8], - ) -> Result { + ) -> Result { use task_vpd_api::{Vpd, VpdError}; task_slot!(VPD, vpd); @@ -446,35 +506,25 @@ impl MgsCommon { *entry = match vpd.is_locked(idx) { Ok(v) => v.into(), Err(e) => { - return Err(SpError::Vpd(match e { - VpdError::InvalidDevice => { - GatewayVpdError::InvalidDevice - } - VpdError::NotPresent => GatewayVpdError::NotPresent, - VpdError::DeviceError => GatewayVpdError::DeviceError, - VpdError::Unavailable => GatewayVpdError::Unavailable, - VpdError::DeviceTimeout => { - GatewayVpdError::DeviceTimeout - } - VpdError::DeviceOff => GatewayVpdError::DeviceOff, - VpdError::BadAddress => GatewayVpdError::BadAddress, - VpdError::BadBuffer => GatewayVpdError::BadBuffer, - VpdError::BadRead => GatewayVpdError::BadRead, - VpdError::BadWrite => GatewayVpdError::BadWrite, - VpdError::BadLock => GatewayVpdError::BadLock, - VpdError::NotImplemented => { - GatewayVpdError::NotImplemented - } - VpdError::IsLocked => GatewayVpdError::IsLocked, + return Err(GwSpError::Vpd(match e { + VpdError::InvalidDevice => GwVpdError::InvalidDevice, + VpdError::NotPresent => GwVpdError::NotPresent, + VpdError::DeviceError => GwVpdError::DeviceError, + VpdError::Unavailable => GwVpdError::Unavailable, + VpdError::DeviceTimeout => GwVpdError::DeviceTimeout, + VpdError::DeviceOff => GwVpdError::DeviceOff, + VpdError::BadAddress => GwVpdError::BadAddress, + VpdError::BadBuffer => GwVpdError::BadBuffer, + VpdError::BadRead => GwVpdError::BadRead, + VpdError::BadWrite => GwVpdError::BadWrite, + VpdError::BadLock => GwVpdError::BadLock, + VpdError::NotImplemented => GwVpdError::NotImplemented, + VpdError::IsLocked => GwVpdError::IsLocked, VpdError::PartiallyLocked => { - GatewayVpdError::PartiallyLocked - } - VpdError::AlreadyLocked => { - GatewayVpdError::AlreadyLocked - } - VpdError::ServerRestarted => { - GatewayVpdError::TaskRestarted + GwVpdError::PartiallyLocked } + VpdError::AlreadyLocked => GwVpdError::AlreadyLocked, + VpdError::ServerRestarted => GwVpdError::TaskRestarted, })) } } @@ -487,12 +537,12 @@ impl MgsCommon { &mut self, component: SpComponent, time_ms: u32, - ) -> Result<(), SpError> { + ) -> Result<(), GwSpError> { if self.reset_component_requested != Some(component) { - return Err(SpError::ResetComponentTriggerWithoutPrepare); + return Err(GwSpError::ResetComponentTriggerWithoutPrepare); } if !matches!(self.sp_update.status(), UpdateStatus::Complete(..)) { - return Err(SpError::Watchdog(WatchdogError::NoCompletedUpdate)); + return Err(GwSpError::Watchdog(WatchdogError::NoCompletedUpdate)); } if component == SpComponent::SP_ITSELF { @@ -501,18 +551,18 @@ impl MgsCommon { .request_reset(); panic!(); // we really really shouldn't get here } else { - Err(SpError::RequestUnsupportedForComponent) + Err(GwSpError::RequestUnsupportedForComponent) } } pub(crate) fn disable_component_watchdog( &mut self, component: SpComponent, - ) -> Result<(), SpError> { + ) -> Result<(), GwSpError> { if component == SpComponent::SP_ITSELF { self.sprot.disable_sp_slot_watchdog()?; } else { - return Err(SpError::RequestUnsupportedForComponent); + return Err(GwSpError::RequestUnsupportedForComponent); } Ok(()) } @@ -520,14 +570,36 @@ impl MgsCommon { pub(crate) fn component_watchdog_supported( &mut self, component: SpComponent, - ) -> Result<(), SpError> { + ) -> Result<(), GwSpError> { if component == SpComponent::SP_ITSELF { self.sprot.sp_slot_watchdog_supported()?; } else { - return Err(SpError::RequestUnsupportedForComponent); + return Err(GwSpError::RequestUnsupportedForComponent); } Ok(()) } + + pub(crate) fn versioned_rot_boot_info( + &mut self, + version: u8, + ) -> Result { + ringbuf_entry!(Log::MgsMessage(MgsMessage::VersionedRotBootInfo { + version + })); + + match self.sprot.versioned_rot_boot_info(version)? { + SpVersionedRotBootInfo::V1(v1) => { + Ok(GwRotBootInfo::V1(MgsRotState::from(v1).0)) + } + SpVersionedRotBootInfo::V2(v2) => match version { + 2 => Ok(GwRotBootInfo::V2(MgsRotStateV2::from(v2).0)), + // RoT's V2 is MGS V3 and the highest version that we can offer today. + _ => Ok(GwRotBootInfo::V3(MgsRotStateV3::from(v2).0)), + }, + // New variants that this code doesn't know about yet will + // result in a deserialization error. + } + } } fn translate_sensor_nodata( @@ -545,29 +617,188 @@ fn translate_sensor_nodata( } // conversion between gateway_messages types and hubris types is quite tedious. -fn rot_state(sprot: &SpRot) -> Result { - let boot_info = sprot.rot_boot_info()?; - - let convert_slot_id = |s| match s { - drv_lpc55_update_api::SlotId::A => RotSlotId::A, - drv_lpc55_update_api::SlotId::B => RotSlotId::B, - }; - - let active = convert_slot_id(boot_info.active); - let persistent_boot_preference = - convert_slot_id(boot_info.persistent_boot_preference); - let pending_persistent_boot_preference = boot_info - .pending_persistent_boot_preference - .map(convert_slot_id); - let transient_boot_preference = - boot_info.transient_boot_preference.map(convert_slot_id); - - Ok(RotStateV2 { - active, - persistent_boot_preference, - pending_persistent_boot_preference, - transient_boot_preference, - slot_a_sha3_256_digest: boot_info.slot_a_sha3_256_digest, - slot_b_sha3_256_digest: boot_info.slot_b_sha3_256_digest, - }) +struct MgsFwid(GwFwid); +impl From for MgsFwid { + fn from(fwid: SpFwid) -> MgsFwid { + MgsFwid(match fwid { + SpFwid::Sha3_256(digest) => GwFwid::Sha3_256(digest), + }) + } +} + +struct MgsRotSlotId(GwRotSlotId); +impl From for MgsRotSlotId { + // This is use to convert an external input from the + // SpRot connection. What happens if a newer RoT image gives us + // something we don't yet know about? + fn from(id: SpSlotId) -> MgsRotSlotId { + MgsRotSlotId(match id { + SpSlotId::A => GwRotSlotId::A, + SpSlotId::B => GwRotSlotId::B, + }) + } +} + +struct MgsRotState(GwRotState); + +impl From for MgsRotState { + fn from(v1: SpRotBootInfo) -> MgsRotState { + MgsRotState(GwRotState { + rot_updates: GwRotUpdateDetails { + boot_state: GwRotBootState { + active: MgsRotSlotId::from(v1.active).0, + slot_a: v1.slot_a_sha3_256_digest.map(|digest| { + GwRotImageDetails { + version: MgsImageVersion::from(SpImageVersion { + version: 0, + epoch: 0, + }) + .0, + digest, + } + }), + slot_b: v1.slot_b_sha3_256_digest.map(|digest| { + GwRotImageDetails { + version: MgsImageVersion::from(SpImageVersion { + version: 0, + epoch: 0, + }) + .0, + digest, + } + }), + }, + }, + }) + } +} + +impl From for MgsRotStateV2 { + fn from(boot_info: SpRotBootInfo) -> MgsRotStateV2 { + MgsRotStateV2(GwRotStateV2 { + active: MgsRotSlotId::from(boot_info.active).0, + persistent_boot_preference: MgsRotSlotId::from( + boot_info.persistent_boot_preference, + ) + .0, + pending_persistent_boot_preference: boot_info + .pending_persistent_boot_preference + .map(|id| MgsRotSlotId::from(id).0), + transient_boot_preference: boot_info + .transient_boot_preference + .map(|id| MgsRotSlotId::from(id).0), + slot_a_sha3_256_digest: boot_info.slot_a_sha3_256_digest, + slot_b_sha3_256_digest: boot_info.slot_b_sha3_256_digest, + }) + } +} + +struct MgsRotStateV2(GwRotStateV2); + +impl From for MgsRotStateV2 { + fn from(boot_info: SpRotBootInfoV2) -> MgsRotStateV2 { + MgsRotStateV2(GwRotStateV2 { + active: MgsRotSlotId::from(boot_info.active).0, + persistent_boot_preference: MgsRotSlotId::from( + boot_info.persistent_boot_preference, + ) + .0, + pending_persistent_boot_preference: boot_info + .pending_persistent_boot_preference + .map(|id| MgsRotSlotId::from(id).0), + transient_boot_preference: boot_info + .transient_boot_preference + .map(|id| MgsRotSlotId::from(id).0), + slot_a_sha3_256_digest: match boot_info.slot_a_status { + Ok(_) => { + let SpFwid::Sha3_256(digest) = boot_info.slot_a_fwid; + Some(digest) + } + Err(_) => None, + }, + slot_b_sha3_256_digest: match boot_info.slot_b_status { + Ok(_) => { + let SpFwid::Sha3_256(digest) = boot_info.slot_b_fwid; + Some(digest) + } + Err(_) => None, + }, + }) + } +} + +struct MgsImageVersion(GwImageVersion); + +impl From for MgsImageVersion { + fn from(iv: SpImageVersion) -> Self { + MgsImageVersion(GwImageVersion { + version: iv.version, + epoch: iv.epoch, + }) + } +} + +struct MgsRotStateV3(GwRotStateV3); + +impl From for MgsRotStateV3 { + fn from(boot_info: SpRotBootInfoV2) -> MgsRotStateV3 { + MgsRotStateV3(GwRotStateV3 { + active: MgsRotSlotId::from(boot_info.active).0, + persistent_boot_preference: MgsRotSlotId::from( + boot_info.persistent_boot_preference, + ) + .0, + pending_persistent_boot_preference: boot_info + .pending_persistent_boot_preference + .map(|s| MgsRotSlotId::from(s).0), + transient_boot_preference: boot_info + .transient_boot_preference + .map(|s| MgsRotSlotId::from(s).0), + slot_a_fwid: MgsFwid::from(boot_info.slot_a_fwid).0, + slot_b_fwid: MgsFwid::from(boot_info.slot_b_fwid).0, + stage0_fwid: MgsFwid::from(boot_info.stage0_fwid).0, + stage0next_fwid: MgsFwid::from(boot_info.stage0next_fwid).0, + slot_a_status: boot_info + .slot_a_status + .map_err(|e| MgsImageError::from(e).0), + slot_b_status: boot_info + .slot_b_status + .map_err(|e| MgsImageError::from(e).0), + stage0_status: boot_info + .stage0_status + .map_err(|e| MgsImageError::from(e).0), + stage0next_status: boot_info + .stage0next_status + .map_err(|e| MgsImageError::from(e).0), + }) + } +} + +struct MgsImageError(GwImageError); +impl From for MgsImageError { + fn from(ie: SpImageError) -> MgsImageError { + MgsImageError(match ie { + SpImageError::Unchecked => GwImageError::Unchecked, + SpImageError::FirstPageErased => GwImageError::FirstPageErased, + SpImageError::PartiallyProgrammed => { + GwImageError::PartiallyProgrammed + } + SpImageError::InvalidLength => GwImageError::InvalidLength, + SpImageError::HeaderNotProgrammed => { + GwImageError::HeaderNotProgrammed + } + SpImageError::BootloaderTooSmall => { + GwImageError::BootloaderTooSmall + } + SpImageError::BadMagic => GwImageError::BadMagic, + SpImageError::HeaderImageSize => GwImageError::HeaderImageSize, + SpImageError::UnalignedLength => GwImageError::UnalignedLength, + SpImageError::UnsupportedType => GwImageError::UnsupportedType, + SpImageError::ResetVectorNotThumb2 => { + GwImageError::ResetVectorNotThumb2 + } + SpImageError::ResetVector => GwImageError::ResetVector, + SpImageError::Signature => GwImageError::Signature, + }) + } } diff --git a/task/control-plane-agent/src/mgs_gimlet.rs b/task/control-plane-agent/src/mgs_gimlet.rs index 5c4f871b1..652990f08 100644 --- a/task/control-plane-agent/src/mgs_gimlet.rs +++ b/task/control-plane-agent/src/mgs_gimlet.rs @@ -17,9 +17,9 @@ use gateway_messages::sp_impl::{ use gateway_messages::{ ignition, ComponentAction, ComponentDetails, ComponentUpdatePrepare, DiscoverResponse, Header, IgnitionCommand, IgnitionState, Message, - MessageKind, MgsError, PowerState, RotRequest, RotResponse, SensorRequest, - SensorResponse, SpComponent, SpError, SpPort, SpRequest, SpStateV2, - SpUpdatePrepare, UpdateChunk, UpdateId, UpdateStatus, + MessageKind, MgsError, PowerState, RotBootInfo, RotRequest, RotResponse, + SensorRequest, SensorResponse, SpComponent, SpError, SpPort, SpRequest, + SpStateV2, SpUpdatePrepare, UpdateChunk, UpdateId, UpdateStatus, SERIAL_CONSOLE_IDLE_TIMEOUT, }; use heapless::{Deque, Vec}; @@ -1155,6 +1155,15 @@ impl SpHandler for MgsHandler { ) -> Result<(), SpError> { self.common.component_watchdog_supported(component) } + + fn versioned_rot_boot_info( + &mut self, + _sender: SocketAddrV6, + _port: SpPort, + version: u8, + ) -> Result { + self.common.versioned_rot_boot_info(version) + } } struct UsartHandler { diff --git a/task/control-plane-agent/src/mgs_psc.rs b/task/control-plane-agent/src/mgs_psc.rs index 4d58e4b87..b59729eaa 100644 --- a/task/control-plane-agent/src/mgs_psc.rs +++ b/task/control-plane-agent/src/mgs_psc.rs @@ -13,9 +13,9 @@ use gateway_messages::sp_impl::{ use gateway_messages::{ ignition, ComponentAction, ComponentDetails, ComponentUpdatePrepare, DiscoverResponse, IgnitionCommand, IgnitionState, MgsError, PowerState, - RotRequest, RotResponse, SensorRequest, SensorResponse, SpComponent, - SpError, SpPort, SpStateV2, SpUpdatePrepare, UpdateChunk, UpdateId, - UpdateStatus, + RotBootInfo, RotRequest, RotResponse, SensorRequest, SensorResponse, + SpComponent, SpError, SpPort, SpStateV2, SpUpdatePrepare, UpdateChunk, + UpdateId, UpdateStatus, }; use host_sp_messages::HostStartupOptions; use idol_runtime::{Leased, RequestError}; @@ -671,4 +671,13 @@ impl SpHandler for MgsHandler { ) -> Result<(), SpError> { self.common.component_watchdog_supported(component) } + + fn versioned_rot_boot_info( + &mut self, + _sender: SocketAddrV6, + _port: SpPort, + version: u8, + ) -> Result { + self.common.versioned_rot_boot_info(version) + } } diff --git a/task/control-plane-agent/src/mgs_sidecar.rs b/task/control-plane-agent/src/mgs_sidecar.rs index d6d6849ff..cacdeb33f 100644 --- a/task/control-plane-agent/src/mgs_sidecar.rs +++ b/task/control-plane-agent/src/mgs_sidecar.rs @@ -16,9 +16,9 @@ use gateway_messages::sp_impl::{ use gateway_messages::{ ignition, ComponentAction, ComponentDetails, ComponentUpdatePrepare, DiscoverResponse, IgnitionCommand, IgnitionState, MgsError, PowerState, - RotRequest, RotResponse, SensorRequest, SensorResponse, SpComponent, - SpError, SpPort, SpStateV2, SpUpdatePrepare, UpdateChunk, UpdateId, - UpdateStatus, + RotBootInfo, RotRequest, RotResponse, SensorRequest, SensorResponse, + SpComponent, SpError, SpPort, SpStateV2, SpUpdatePrepare, UpdateChunk, + UpdateId, UpdateStatus, }; use host_sp_messages::HostStartupOptions; use idol_runtime::{Leased, RequestError}; @@ -780,6 +780,15 @@ impl SpHandler for MgsHandler { ) -> Result<(), SpError> { self.common.component_watchdog_supported(component) } + + fn versioned_rot_boot_info( + &mut self, + _sender: SocketAddrV6, + _port: SpPort, + version: u8, + ) -> Result { + self.common.versioned_rot_boot_info(version) + } } // Helper function for `.map_err()`; we can't use `?` because we can't implement diff --git a/task/control-plane-agent/src/update/rot.rs b/task/control-plane-agent/src/update/rot.rs index 928301f31..a1e3bf1ce 100644 --- a/task/control-plane-agent/src/update/rot.rs +++ b/task/control-plane-agent/src/update/rot.rs @@ -16,12 +16,13 @@ use gateway_messages::{ ringbuf!(Trace, 64, Trace::None); -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, PartialEq, Eq)] enum Trace { None, IngestChunkInput { offset: u32, len: usize }, IngestChunkState { offset: u32, len: usize }, WriteOneBlock(u32, usize, usize), + Target(u8, u16), } pub(crate) struct RotUpdate { @@ -77,10 +78,11 @@ impl ComponentUpdater for RotUpdate { .map_err(SpError::OtherComponentUpdateInProgress)?; // Which target are we updating? + ringbuf_entry!(Trace::Target(update.component.id[0], update.slot)); let target = match (update.component, update.slot) { (SpComponent::ROT, 0) => UpdateTarget::ImageA, (SpComponent::ROT, 1) => UpdateTarget::ImageB, - (SpComponent::STAGE0, 0) => UpdateTarget::Bootloader, + (SpComponent::STAGE0, 1) => UpdateTarget::Bootloader, _ => return Err(SpError::InvalidSlotForComponent), };