diff --git a/Cargo.lock b/Cargo.lock index f1ffe1ec45..efec10a2ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,6 +108,15 @@ version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +[[package]] +name = "apob" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/apob#cc0755a5dc891b27b45a1d6ec6728e49bc1a991b" +dependencies = [ + "strum_macros", + "zerocopy 0.8.27", +] + [[package]] name = "app-donglet" version = "0.1.0" @@ -1054,10 +1063,12 @@ dependencies = [ name = "drv-cosmo-hf" version = "0.1.0" dependencies = [ + "apob", "build-fpga-regmap", "build-util", "cortex-m", "counters", + "crc", "drv-hash-api", "drv-hf-api", "drv-spartan7-loader-api", @@ -1067,6 +1078,9 @@ dependencies = [ "num-traits", "ringbuf", "serde", + "sha2", + "static-cell", + "static_assertions", "stm32h7", "userlib", "zerocopy 0.8.27", diff --git a/Cargo.toml b/Cargo.toml index 9a739c2308..c3785c57c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,6 +143,7 @@ zeroize = { version = "1.5.7", default-features = false, features = ["zeroize_de zip = { version = "0.6", default-features = false, features = ["bzip2", "deflate", "zstd"] } # Oxide forks and repos +apob = { git = "https://github.com/oxidecomputer/apob", default-features = false } attest-data = { git = "https://github.com/oxidecomputer/dice-util", default-features = false, version = "0.4.0" } dice-mfg-msgs = { git = "https://github.com/oxidecomputer/dice-util", default-features = false, version = "0.2.1" } gateway-messages = { git = "https://github.com/oxidecomputer/management-gateway-service", default-features = false, features = ["smoltcp"] } diff --git a/app/cosmo/base.toml b/app/cosmo/base.toml index a4419cc326..943517bde2 100644 --- a/app/cosmo/base.toml +++ b/app/cosmo/base.toml @@ -256,7 +256,7 @@ features = ["stm32h753", "usart6", "baud_rate_3M", "hardware_flow_control", "vla uses = ["usart6", "dbgmcu"] interrupts = {"usart6.irq" = "usart-irq"} priority = 9 -max-sizes = {flash = 66944, ram = 65536} +max-sizes = {flash = 69120, ram = 65536} stacksize = 5400 start = true task-slots = ["sys", { cpu_seq = "cosmo_seq" }, "hf", "control_plane_agent", "net", "packrat", "i2c_driver", { spi_driver = "spi2_driver" }, "sprot", "auxflash"] diff --git a/app/gimlet/base.toml b/app/gimlet/base.toml index b7133df17e..42d57b89aa 100644 --- a/app/gimlet/base.toml +++ b/app/gimlet/base.toml @@ -240,7 +240,7 @@ features = ["stm32h753", "uart7", "baud_rate_3M", "hardware_flow_control", "vlan uses = ["uart7", "dbgmcu"] interrupts = {"uart7.irq" = "usart-irq"} priority = 8 -max-sizes = {flash = 65952, ram = 65536} +max-sizes = {flash = 67648, ram = 65536} stacksize = 5376 start = true task-slots = ["sys", { cpu_seq = "gimlet_seq" }, "hf", "control_plane_agent", "net", "packrat", "i2c_driver", { spi_driver = "spi2_driver" }, "sprot"] diff --git a/drv/cosmo-hf/Cargo.toml b/drv/cosmo-hf/Cargo.toml index ca818dbc68..3dd0519c4f 100644 --- a/drv/cosmo-hf/Cargo.toml +++ b/drv/cosmo-hf/Cargo.toml @@ -9,14 +9,19 @@ drv-hash-api = { path = "../hash-api" } drv-hf-api = { path = "../hf-api" } drv-spartan7-loader-api = { path = "../spartan7-loader-api" } ringbuf = { path = "../../lib/ringbuf" } +static-cell = { path = "../../lib/static-cell" } userlib = { path = "../../sys/userlib", features = ["panic-messages"] } +apob = { workspace = true } cortex-m = { workspace = true } +crc = { workspace = true } +hubpack = { workspace = true } idol-runtime = { workspace = true } num-traits = { workspace = true } -stm32h7 = { workspace = true } serde = { workspace = true } -hubpack = { workspace = true } +sha2 = { workspace = true } +static_assertions = { workspace = true } +stm32h7 = { workspace = true } zerocopy = { workspace = true } zerocopy-derive = { workspace = true } diff --git a/drv/cosmo-hf/src/apob.rs b/drv/cosmo-hf/src/apob.rs index 1879bae644..cb0771c1d2 100644 --- a/drv/cosmo-hf/src/apob.rs +++ b/drv/cosmo-hf/src/apob.rs @@ -7,10 +7,17 @@ //! For details, see AMD document 57299; tables and sections in this code refer //! to Rev. 2.0 February 2025. -use crate::hf::ServerImpl; -use drv_hf_api::HfError; +use crate::{ + hf::ServerImpl, FlashAddr, FlashDriver, PAGE_SIZE_BYTES, SECTOR_SIZE_BYTES, +}; +use drv_hf_api::{ + ApobBeginError, ApobCommitError, ApobHash, ApobReadError, ApobWriteError, + HfError, +}; +use idol_runtime::{Leased, R, W}; +use ringbuf::{counted_ringbuf, ringbuf_entry}; use userlib::UnwrapLite; -use zerocopy::{FromBytes, Immutable, IntoBytes}; +use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout}; /// Embedded firmware structure (Table 3) /// @@ -122,3 +129,750 @@ impl From for ApobError { Self::Hf(value) } } + +//////////////////////////////////////////////////////////////////////////////// + +pub const APOB_PERSISTENT_DATA_MAGIC: u32 = 0x3ca9_9496; // chosen at random +pub const APOB_PERSISTENT_DATA_STRIDE: usize = 128; +pub const APOB_PERSISTENT_DATA_HEADER_VERSION: u32 = 1; + +pub const APOB_META_SIZE: u32 = SECTOR_SIZE_BYTES; +pub const APOB_SLOT_SIZE: u32 = 2 * 1024 * 1024; // 2 MiB (chosen arbitrarily) + +// The layout is [meta0, meta1, slot0, slot1] +pub const APOB_META0_ADDR: u32 = crate::hf::SLOT_SIZE_BYTES * 2; +pub const APOB_META1_ADDR: u32 = APOB_META0_ADDR + APOB_META_SIZE; +pub const APOB_SLOT0_ADDR: u32 = APOB_META1_ADDR + APOB_META_SIZE; +pub const APOB_SLOT1_ADDR: u32 = APOB_SLOT0_ADDR + APOB_SLOT_SIZE; + +#[derive(Copy, Clone, PartialEq, counters::Count)] +enum Trace { + #[count(skip)] + None, + State(#[count(children)] ApobState), + GotPersistentData { + #[count(children)] + meta: Meta, + data: Option, + }, + WrotePersistentData { + #[count(children)] + meta: Meta, + data: ApobRawPersistentData, + }, + HashMismatch { + expected_hash: [u8; 32], + actual_hash: [u8; 32], + }, + ApobSlotErase { + #[count(children)] + slot: ApobSlot, + size: u32, + }, + ApobSlotEraseDone { + #[count(children)] + slot: ApobSlot, + time_ms: u64, + num_sectors_erased: usize, + }, + ApobSlotEraseSkipped { + #[count(children)] + slot: ApobSlot, + time_ms: u64, + }, + ApobSlotSectorErase { + #[count(children)] + slot: ApobSlot, + offset: u32, + }, + BadApobSig { + expected: [u8; 4], + actual: [u8; 4], + }, + BadApobVersion { + expected: u32, + actual: u32, + }, + BadApobSize { + expected: u32, + actual: u32, + }, + BadApobWalk { + expected: u32, + actual: u32, + }, +} +counted_ringbuf!(Trace, 16, Trace::None); + +#[derive(Copy, Clone, PartialEq, counters::Count)] +pub(crate) enum ApobSlot { + Slot0, + Slot1, +} + +impl core::ops::Not for ApobSlot { + type Output = Self; + fn not(self) -> Self::Output { + match self { + ApobSlot::Slot0 => ApobSlot::Slot1, + ApobSlot::Slot1 => ApobSlot::Slot0, + } + } +} + +impl ApobSlot { + pub fn base_addr(&self) -> FlashAddr { + match self { + ApobSlot::Slot0 => FlashAddr::new(APOB_SLOT0_ADDR).unwrap(), + ApobSlot::Slot1 => FlashAddr::new(APOB_SLOT1_ADDR).unwrap(), + } + } + + pub fn flash_addr(&self, offset: u32) -> Option { + let base = self.base_addr(); + if offset >= APOB_SLOT_SIZE { + return None; + } + base.0.checked_add(offset).and_then(FlashAddr::new) + } +} + +pub(crate) struct ApobBufs { + persistent_data: &'static mut [u8; APOB_PERSISTENT_DATA_STRIDE], + page: &'static mut [u8; PAGE_SIZE_BYTES], + scratch: &'static mut [u8; PAGE_SIZE_BYTES], +} + +/// Grabs references to the static buffers. Can only be called once. +impl ApobBufs { + pub fn claim_statics() -> Self { + use static_cell::ClaimOnceCell; + static BUFS: ClaimOnceCell<( + [u8; APOB_PERSISTENT_DATA_STRIDE], + [u8; PAGE_SIZE_BYTES], + [u8; PAGE_SIZE_BYTES], + )> = ClaimOnceCell::new(( + [0; APOB_PERSISTENT_DATA_STRIDE], + [0; PAGE_SIZE_BYTES], + [0; PAGE_SIZE_BYTES], + )); + let (persistent_data, page, scratch) = BUFS.claim(); + Self { + persistent_data, + page, + scratch, + } + } +} + +/// State machine data, which implements the logic from RFD 593 +/// +/// See rfd.shared.oxide.computer/rfd/593#_production_strength_implementation +/// for details on the states and transitions. Note that the diagram in the RFD +/// includes fine-grained states (e.g. writing), which the actual implementation +/// never dwells in; these states are not explicit in `ApobState`. +#[derive(Copy, Clone, PartialEq, counters::Count)] +pub(crate) enum ApobState { + /// Waiting for `ApobStart` + Waiting { + #[count(children)] + read_slot: Option, + write_slot: ApobSlot, + }, + /// Receiving and writing data to host flash + Ready { + #[count(children)] + write_slot: ApobSlot, + expected_length: u32, + expected_hash: ApobHash, + any_written: bool, + }, + /// Writing data to flash is no longer allowed + Locked { + /// We store the first commit result for idempotency, because the host + /// is allowed to retry the `ApobCommit` message. Subsequent commits + /// return the same result. + commit_result: Result<(), ApobCommitError>, + }, +} + +/// Persistent data, stored in Bonus Flash to select an APOB slot +#[derive( + Copy, Clone, Eq, PartialEq, IntoBytes, FromBytes, Immutable, KnownLayout, +)] +#[repr(C)] +pub struct ApobRawPersistentData { + /// Must always be `APOB_PERSISTENT_DATA_MAGIC`. + oxide_magic: u32, + + /// Must always be `APOB_PERSISTENT_DATA_HEADER_VERSION` (for now) + header_version: u32, + + /// Monotonically increasing counter + pub monotonic_counter: u64, + + /// Either 0 or 1; directly translatable to [`ApobSlot`] + pub slot_select: u32, + + /// CRC-32 over the rest of the data using the iSCSI polynomial + checksum: u32, +} + +impl core::cmp::PartialOrd for ApobRawPersistentData { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl core::cmp::Ord for ApobRawPersistentData { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.monotonic_counter.cmp(&other.monotonic_counter) + } +} + +impl ApobRawPersistentData { + pub fn new(slot: ApobSlot, monotonic_counter: u64) -> Self { + static_assertions::const_assert!( + APOB_PERSISTENT_DATA_STRIDE + >= core::mem::size_of::(), + ); + let mut out = Self { + oxide_magic: APOB_PERSISTENT_DATA_MAGIC, + header_version: APOB_PERSISTENT_DATA_HEADER_VERSION, + monotonic_counter, + slot_select: match slot { + ApobSlot::Slot0 => 0, + ApobSlot::Slot1 => 1, + }, + checksum: 0, // dummy value + }; + out.checksum = out.expected_checksum(); + assert!(out.is_valid()); + out + } + + fn expected_checksum(&self) -> u32 { + static CRC: crc::Crc = crc::Crc::::new(&crc::CRC_32_ISCSI); + let mut c = CRC.digest(); + // We do a CRC32 of everything except the checksum, which is positioned + // at the end of the struct and is a `u32` + let size = core::mem::size_of::() + - core::mem::size_of::(); + c.update(&self.as_bytes()[..size]); + c.finalize() + } + + pub fn is_valid(&self) -> bool { + self.oxide_magic == APOB_PERSISTENT_DATA_MAGIC + && self.header_version == APOB_PERSISTENT_DATA_HEADER_VERSION + && self.slot_select <= 1 + && self.checksum == self.expected_checksum() + } +} + +#[derive(Copy, Clone, PartialEq, counters::Count)] +enum Meta { + Meta0, + Meta1, +} + +impl Meta { + fn base_addr(&self) -> FlashAddr { + match self { + Meta::Meta0 => FlashAddr::new(APOB_META0_ADDR).unwrap(), + Meta::Meta1 => FlashAddr::new(APOB_META1_ADDR).unwrap(), + } + } + fn flash_addr(&self, offset: u32) -> Option { + let base = self.base_addr(); + if offset >= APOB_META_SIZE { + return None; + } + base.0.checked_add(offset).and_then(FlashAddr::new) + } +} + +impl ApobState { + /// Initializes the `ApobState` + /// + /// Searches for an active slot in the metadata regions, updating the offset + /// in the FPGA driver if found, and erases unused or invalid slots. + pub(crate) fn init(drv: &mut FlashDriver, buf: &mut ApobBufs) -> Self { + // Look up persistent data, which specifies an active slot + let out = if let Some(s) = Self::get_slot(drv) { + // Erase the inactive slot, in preparation for writing + Self::slot_erase(drv, buf, !s); + + // Set the FPGA's offset so that the PSP reads valid data + drv.set_apob_offset(s.base_addr()); + + ApobState::Waiting { + read_slot: Some(s), + write_slot: !s, + } + } else { + // Erase both slots + Self::slot_erase(drv, buf, ApobSlot::Slot0); + Self::slot_erase(drv, buf, ApobSlot::Slot1); + + // Pick a slot arbitrarily; it has just been erased and will fail + // cryptographic checks in the PSP. + drv.set_apob_offset(ApobSlot::Slot1.base_addr()); + + ApobState::Waiting { + read_slot: None, + write_slot: ApobSlot::Slot0, + } + }; + ringbuf_entry!(Trace::State(out)); + out + } + + fn get_raw_persistent_data( + drv: &mut FlashDriver, + ) -> Option { + let a = Self::slot_scan(drv, Meta::Meta0); + let b = Self::slot_scan(drv, Meta::Meta1); + + // None is always less than Some(..), so this picks the largest option + a.max(b) + } + + fn get_slot(drv: &mut FlashDriver) -> Option { + Self::get_raw_persistent_data(drv).map(|b| match b.slot_select { + 0 => ApobSlot::Slot0, + 1 => ApobSlot::Slot1, + // prevented by is_valid check in slot_scan + _ => unreachable!(), + }) + } + + /// Erases the given APOB slot + fn slot_erase(drv: &mut FlashDriver, buf: &mut ApobBufs, slot: ApobSlot) { + static_assertions::const_assert!( + APOB_SLOT_SIZE.is_multiple_of(SECTOR_SIZE_BYTES) + ); + Self::slot_erase_range(drv, buf, slot, APOB_SLOT_SIZE); + } + + /// Erases the first `size` bytes of the given APOB slot (rounding up) + /// + /// `size` is rounded up to `SECTOR_SIZE_BYTES`. + /// + /// # Panics + /// If `size > APOB_SLOT_SIZE` + fn slot_erase_range( + drv: &mut FlashDriver, + buf: &mut ApobBufs, + slot: ApobSlot, + size: u32, + ) { + let start = userlib::sys_get_timer().now; + ringbuf_entry!(Trace::ApobSlotErase { slot, size }); + static_assertions::const_assert!( + (SECTOR_SIZE_BYTES as usize).is_multiple_of(PAGE_SIZE_BYTES) + ); + let size = size.next_multiple_of(SECTOR_SIZE_BYTES); + assert!(size <= APOB_SLOT_SIZE); + + // Read back each sector and decide whether to erase it. We round up + // here to the nearest sector + let mut num_sectors_erased = 0; + for sector_offset in (0..size).step_by(SECTOR_SIZE_BYTES as usize) { + for page_offset in (0..SECTOR_SIZE_BYTES).step_by(PAGE_SIZE_BYTES) { + let offset = sector_offset + page_offset; + drv.flash_read( + slot.flash_addr(offset).unwrap_lite(), + &mut buf.page.as_mut_slice(), + ) + .unwrap_lite(); + if buf.page.iter().any(|b| *b != 0xFF) { + ringbuf_entry!(Trace::ApobSlotSectorErase { slot, offset }); + num_sectors_erased += 1; + drv.flash_sector_erase( + slot.flash_addr(offset).unwrap_lite(), + ); + break; + } + } + } + let end = userlib::sys_get_timer().now; + if num_sectors_erased > 0 { + ringbuf_entry!(Trace::ApobSlotEraseDone { + slot, + time_ms: end - start, + num_sectors_erased, + }); + } else { + ringbuf_entry!(Trace::ApobSlotEraseSkipped { + slot, + time_ms: end - start, + }); + } + } + + /// Finds a valid APOB slot within the given meta region + fn slot_scan( + drv: &mut FlashDriver, + meta: Meta, + ) -> Option { + let mut best: Option = None; + for offset in (0..APOB_META_SIZE).step_by(APOB_PERSISTENT_DATA_STRIDE) { + let mut data = ApobRawPersistentData::new_zeroed(); + let addr = meta.flash_addr(offset).unwrap_lite(); + // flash_read is infallible when using a slice + drv.flash_read(addr, &mut data.as_mut_bytes()).unwrap_lite(); + if data.is_valid() { + best = best.max(Some(data)); + } + } + ringbuf_entry!(Trace::GotPersistentData { meta, data: best }); + best + } + + pub(crate) fn begin( + &mut self, + drv: &mut FlashDriver, + length: u32, + algorithm: ApobHash, + ) -> Result<(), ApobBeginError> { + drv.check_flash_mux_state() + .map_err(|_| ApobBeginError::InvalidState)?; + if length > APOB_SLOT_SIZE { + // XXX should this lock the state machine? + return Err(ApobBeginError::BadDataLength); + } + match *self { + ApobState::Waiting { write_slot, .. } => { + *self = ApobState::Ready { + write_slot, + any_written: false, + expected_length: length, + expected_hash: algorithm, + }; + ringbuf_entry!(Trace::State(*self)); + + Ok(()) + } + ApobState::Locked { .. } => Err(ApobBeginError::InvalidState), + ApobState::Ready { + expected_length, + expected_hash, + any_written, + .. + } => { + // Idempotent begin messages are allowed + if !any_written + && expected_length == length + && expected_hash == algorithm + { + Ok(()) + } else { + // XXX should this lock the state machine? + Err(ApobBeginError::InvalidState) + } + } + } + } + + pub(crate) fn write( + &mut self, + drv: &mut FlashDriver, + buf: &mut ApobBufs, + offset: u32, + data: Leased, + ) -> Result<(), ApobWriteError> { + // Check that the flash is muxed to the SP + drv.check_flash_mux_state() + .map_err(|_| ApobWriteError::InvalidState)?; + + // Check that the offset is within the slot + if offset > APOB_SLOT_SIZE { + return Err(ApobWriteError::InvalidOffset); + } + + // Check that we're in a writable state, and set the "any written" flag + let ApobState::Ready { + write_slot, + expected_length, + any_written, + .. + } = self + else { + return Err(ApobWriteError::InvalidState); + }; + *any_written = true; + let write_slot = *write_slot; + let expected_length = *expected_length; + + // Check that the end of the data range is within our expected length + if offset + .checked_add(data.len() as u32) + .is_none_or(|d| d > expected_length) + { + return Err(ApobWriteError::InvalidSize); + } + for i in (0..data.len()).step_by(PAGE_SIZE_BYTES) { + // Read data from the lease into local storage + let n = (data.len() - i).min(PAGE_SIZE_BYTES); + data.read_range(i..(i + n), &mut buf.page[..n]) + .map_err(|_| ApobWriteError::WriteFailed)?; + let addr = write_slot + .flash_addr(offset + u32::try_from(i).unwrap_lite()) + .unwrap_lite(); + + // Read back the current data; it must be erased or match (for + // idempotency) + drv.flash_read(addr, &mut &mut buf.scratch[..n]) + .map_err(|_| ApobWriteError::WriteFailed)?; + + // This is a little tricky: we allow for bytes to either match our + // expected write (for idempotency), _or_ to be `0xFF` (because that + // means they're erased). We have to check every byte to confirm + // that they all match, but can bail immediately if we find a + // non-matching byte that is *also* not erased. + let mut needs_write = false; + for (a, b) in buf.scratch[..n].iter().zip(buf.page[..n].iter()) { + if *a != *b { + // You may be tempted to insert a `break` here, but that + // would be incorrect: there could be subsequent bytes which + // do not match *and* are not erased, in which case we must + // return `NotErased`. + needs_write = true; + if *a != 0xFF { + return Err(ApobWriteError::NotErased); + } + } + } + // If any byte is not a match, then we have to do a flash write + // (otherwise, it's an idempotent write and we can skip it) + if needs_write { + drv.flash_write(addr, &mut &buf.page[..n]) + .map_err(|_| ApobWriteError::WriteFailed)?; + } + } + Ok(()) + } + + pub(crate) fn read( + &mut self, + drv: &mut FlashDriver, + buf: &mut ApobBufs, + offset: u32, + data: Leased, + ) -> Result { + // Check that the flash is muxed to the SP + drv.check_flash_mux_state() + .map_err(|_| ApobReadError::InvalidState)?; + + // Check that the offset is within the slot + if offset > APOB_SLOT_SIZE { + return Err(ApobReadError::InvalidOffset); + } + + // Check that we're in a writable state + let ApobState::Waiting { read_slot, .. } = *self else { + return Err(ApobReadError::InvalidState); + }; + let Some(read_slot) = read_slot else { + return Err(ApobReadError::NoValidApob); + }; + + // Check that the end of the data range is within a slot size + if offset + .checked_add(data.len() as u32) + .is_none_or(|d| d > APOB_SLOT_SIZE) + { + return Err(ApobReadError::InvalidSize); + } + + for i in (0..data.len()).step_by(PAGE_SIZE_BYTES) { + // Read data from the lease into local storage + let n = (data.len() - i).min(PAGE_SIZE_BYTES); + let addr = read_slot.flash_addr(i as u32 + offset).unwrap_lite(); + + // Read back the current data, then write it to the lease + drv.flash_read(addr, &mut &mut buf.page[..n]) + .map_err(|_| ApobReadError::ReadFailed)?; + data.write_range(i..(i + n), &buf.page[..n]) + .map_err(|_| ApobReadError::ReadFailed)?; + } + Ok(data.len()) + } + + pub(crate) fn lock(&mut self) { + match *self { + ApobState::Ready { .. } | ApobState::Waiting { .. } => { + *self = ApobState::Locked { + commit_result: Err(ApobCommitError::InvalidState), + }; + } + ApobState::Locked { .. } => { + // Nothing to do here + } + } + } + + pub(crate) fn commit( + &mut self, + drv: &mut FlashDriver, + buf: &mut ApobBufs, + ) -> Result<(), ApobCommitError> { + drv.check_flash_mux_state() + .map_err(|_| ApobCommitError::InvalidState)?; + let (expected_length, expected_hash, write_slot) = match *self { + // Locking without writing anything is fine + ApobState::Waiting { .. } => { + *self = ApobState::Locked { + commit_result: Ok(()), + }; + ringbuf_entry!(Trace::State(*self)); + return Ok(()); + } + ApobState::Locked { commit_result } => return commit_result, + ApobState::Ready { + expected_length, + expected_hash, + write_slot, + .. + } => (expected_length, expected_hash, write_slot), + }; + + let r = Self::apob_validate( + drv, + buf, + expected_length, + expected_hash, + write_slot, + ); + *self = ApobState::Locked { commit_result: r }; + ringbuf_entry!(Trace::State(*self)); + + // If validation failed, then erase the just-written data and return the + // error code (without updating the active slot). + if r.is_err() { + Self::slot_erase_range(drv, buf, write_slot, expected_length); + return r; + } + + // We will write persistent data to flash which selects our new slot + let old_meta_data = Self::get_raw_persistent_data(drv); + let new_counter = old_meta_data + .map(|p| p.monotonic_counter) + .unwrap_or(1) + .wrapping_add(1); + let new_meta_data = ApobRawPersistentData::new(write_slot, new_counter); + + for m in [Meta::Meta0, Meta::Meta1] { + Self::write_raw_persistent_data(drv, buf, new_meta_data, m); + ringbuf_entry!(Trace::WrotePersistentData { + data: new_meta_data, + meta: m + }); + } + + Ok(()) + } + + fn apob_validate( + drv: &mut FlashDriver, + buf: &mut ApobBufs, + expected_length: u32, + expected_hash: ApobHash, + write_slot: ApobSlot, + ) -> Result<(), ApobCommitError> { + // Confirm that the hash of data matches our expectations + match expected_hash { + ApobHash::Sha256(expected_hash) => { + let mut hasher = sha2::Sha256::new(); + use sha2::Digest; + for i in (0..expected_length).step_by(PAGE_SIZE_BYTES) { + let n = + ((expected_length - i) as usize).min(PAGE_SIZE_BYTES); + let addr = write_slot.flash_addr(i).unwrap_lite(); + drv.flash_read(addr, &mut &mut buf.page[..n]) + .map_err(|_| ApobCommitError::CommitFailed)?; + hasher.update(&buf.page[..n]); + } + let out = hasher.finalize(); + if out != expected_hash.into() { + ringbuf_entry!(Trace::HashMismatch { + expected_hash, + actual_hash: out.into() + }); + return Err(ApobCommitError::ValidationFailed); + } + } + } + + // Check the APOB itself + let mut header = apob::ApobHeader::new_zeroed(); + let addr = write_slot.flash_addr(0).unwrap_lite(); + drv.flash_read(addr, &mut header.as_mut_bytes()) + .unwrap_lite(); + if header.sig != apob::APOB_SIG { + ringbuf_entry!(Trace::BadApobSig { + expected: apob::APOB_SIG, + actual: header.sig + }); + return Err(ApobCommitError::ValidationFailed); + } + if header.version != apob::APOB_VERSION { + ringbuf_entry!(Trace::BadApobVersion { + expected: apob::APOB_VERSION, + actual: header.version, + }); + return Err(ApobCommitError::ValidationFailed); + } + if header.size != expected_length { + ringbuf_entry!(Trace::BadApobSize { + expected: expected_length, + actual: header.size, + }); + return Err(ApobCommitError::ValidationFailed); + } + let mut pos = header.offset; + while pos < expected_length { + let mut entry = apob::ApobEntry::new_zeroed(); + let addr = write_slot.flash_addr(pos).unwrap_lite(); + drv.flash_read(addr, &mut entry.as_mut_bytes()) + .unwrap_lite(); + pos += entry.size; + } + if pos != expected_length { + ringbuf_entry!(Trace::BadApobWalk { + expected: expected_length, + actual: pos, + }); + return Err(ApobCommitError::ValidationFailed); + } + + Ok(()) + } + + fn write_raw_persistent_data( + drv: &mut FlashDriver, + buf: &mut ApobBufs, + data: ApobRawPersistentData, + meta: Meta, + ) { + let mut found: Option = None; + for offset in (0..APOB_META_SIZE).step_by(APOB_PERSISTENT_DATA_STRIDE) { + let addr = meta.flash_addr(offset).unwrap_lite(); + // Infallible when using a slice + drv.flash_read(addr, &mut buf.persistent_data.as_mut_slice()) + .unwrap_lite(); + if buf.persistent_data.iter().all(|c| *c == 0xFF) { + found = Some(addr); + break; + } + } + let addr = found.unwrap_or_else(|| { + let addr = meta.flash_addr(0).unwrap_lite(); + drv.flash_sector_erase(addr); + addr + }); + // Infallible when using a slice + drv.flash_write(addr, &mut data.as_bytes()).unwrap_lite(); + } +} diff --git a/drv/cosmo-hf/src/hf.rs b/drv/cosmo-hf/src/hf.rs index 46d3d7173c..902b63e7a1 100644 --- a/drv/cosmo-hf/src/hf.rs +++ b/drv/cosmo-hf/src/hf.rs @@ -17,7 +17,7 @@ use userlib::{set_timer_relative, task_slot, RecvMessage, UnwrapLite}; use zerocopy::{FromZeros, IntoBytes}; use crate::{ - FlashAddr, FlashDriver, Trace, PAGE_SIZE_BYTES, SECTOR_SIZE_BYTES, + apob, FlashAddr, FlashDriver, Trace, PAGE_SIZE_BYTES, SECTOR_SIZE_BYTES, }; task_slot!(HASH, hash_driver); @@ -25,13 +25,15 @@ task_slot!(HASH, hash_driver); /// We break the 128 MiB flash chip into 2x 32 MiB slots, to match Gimlet /// /// The upper 64 MiB are used for Bonus Data. -const SLOT_SIZE_BYTES: u32 = 1024 * 1024 * 32; -const BONUS_SIZE_BYTES: u32 = 1024 * 1024 * 64; +pub(crate) const SLOT_SIZE_BYTES: u32 = 1024 * 1024 * 32; pub struct ServerImpl { pub drv: FlashDriver, pub dev: HfDevSelect, hash: HashData, + + pub(crate) apob_state: apob::ApobState, + pub(crate) apob_buf: apob::ApobBufs, } /// This tunes how many bytes we hash in a single async timer notification @@ -46,11 +48,16 @@ impl ServerImpl { /// /// Persistent data is loaded from the flash chip and used to select `dev`; /// in addition, it is made redundant (written to both virtual devices). - pub fn new(drv: FlashDriver) -> Self { + pub fn new(mut drv: FlashDriver) -> Self { + let mut apob_buf = apob::ApobBufs::claim_statics(); + let apob_state = apob::ApobState::init(&mut drv, &mut apob_buf); + let mut out = Self { dev: drv_hf_api::HfDevSelect::Flash0, drv, hash: HashData::new(HASH.get_task_id()), + apob_state, + apob_buf, }; out.drv.set_flash_mux_state(HfMuxState::SP); out.ensure_persistent_data_is_redundant(); @@ -98,21 +105,6 @@ impl ServerImpl { } } - /// Converts a relative address to an absolute address in bonus space - fn bonus_addr(offset: u32, len: u32) -> Result { - if offset - .checked_add(len) - .is_some_and(|a| a <= BONUS_SIZE_BYTES) - { - let addr = offset - .checked_add(2 * SLOT_SIZE_BYTES) - .ok_or(HfError::BadAddress)?; - Ok(FlashAddr(addr)) - } else { - Err(HfError::BadAddress) - } - } - /// Converts a relative address to an absolute address in a device slot fn flash_addr_for( offset: u32, @@ -516,49 +508,57 @@ impl idl::InOrderHostFlashImpl for ServerImpl { r } - /// Writes a page to the bonus region of flash - fn bonus_page_program( + /// Begins an APOB write + fn apob_begin( &mut self, _: &RecvMessage, - addr: u32, - data: LenLimit, PAGE_SIZE_BYTES>, - ) -> Result<(), RequestError> { - self.drv.check_flash_mux_state()?; - self.drv - .flash_write( - Self::bonus_addr(addr, data.len() as u32)?, - &mut LeaseBufReader::<_, 32>::from(data.into_inner()), - ) - .map_err(|()| RequestError::went_away()) + length: u32, + algorithm: drv_hf_api::ApobHash, + ) -> Result<(), RequestError> { + self.apob_state + .begin(&mut self.drv, length, algorithm) + .map_err(RequestError::from) } - /// Reads a page from the bonus region of flash - fn bonus_read( + fn apob_write( &mut self, _: &RecvMessage, - addr: u32, - dest: LenLimit, PAGE_SIZE_BYTES>, - ) -> Result<(), RequestError> { - self.drv.check_flash_mux_state()?; - self.drv - .flash_read( - Self::bonus_addr(addr, dest.len() as u32)?, - &mut LeaseBufWriter::<_, 32>::from(dest.into_inner()), - ) - .map_err(|_| RequestError::went_away()) + offset: u32, + data: Leased, + ) -> Result<(), RequestError> { + self.apob_state + .write(&mut self.drv, &mut self.apob_buf, offset, data) + .map_err(RequestError::from) } - /// Erases a 64 KiB sector in the bonus flash device - fn bonus_sector_erase( + fn apob_commit( &mut self, _: &RecvMessage, - addr: u32, - ) -> Result<(), RequestError> { - self.drv.check_flash_mux_state()?; - self.drv.flash_sector_erase(Self::bonus_addr(addr, 0)?); + ) -> Result<(), RequestError> { + self.apob_state + .commit(&mut self.drv, &mut self.apob_buf) + .map_err(RequestError::from) + } + + fn apob_lock( + &mut self, + _: &RecvMessage, + ) -> Result<(), RequestError> { + self.apob_state.lock(); Ok(()) } + fn apob_read( + &mut self, + _: &RecvMessage, + offset: u32, + data: Leased, + ) -> Result> { + self.apob_state + .read(&mut self.drv, &mut self.apob_buf, offset, data) + .map_err(RequestError::from) + } + fn get_mux( &mut self, _: &RecvMessage, @@ -585,6 +585,11 @@ impl idl::InOrderHostFlashImpl for ServerImpl { self.drv.clear_apob_pos(); } } + // Reinitialize APOB state to correctly pick the active APOB slot. + // This also unlocks the APOB so it can be written (once muxed back + // to the SP). + self.apob_state = + apob::ApobState::init(&mut self.drv, &mut self.apob_buf); } self.drv.set_flash_mux_state(state); self.invalidate_mux_switch(); @@ -792,10 +797,233 @@ impl NotificationHandler for ServerImpl { } } +pub(crate) struct FailServer(pub drv_hf_api::HfError); + +impl NotificationHandler for FailServer { + fn current_notification_mask(&self) -> u32 { + 0 + } + + fn handle_notification(&mut self, _bits: userlib::NotificationBits) { + unreachable!() + } +} + +impl idl::InOrderHostFlashImpl for FailServer { + fn read_id( + &mut self, + _: &RecvMessage, + ) -> Result> { + Err(self.0.into()) + } + + fn capacity( + &mut self, + _: &RecvMessage, + ) -> Result> { + Err(self.0.into()) + } + + fn read_status( + &mut self, + _: &RecvMessage, + ) -> Result> { + Err(self.0.into()) + } + + fn bulk_erase( + &mut self, + _: &RecvMessage, + _protect: HfProtectMode, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn page_program( + &mut self, + _: &RecvMessage, + _addr: u32, + _protect: HfProtectMode, + _data: LenLimit, PAGE_SIZE_BYTES>, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn page_program_dev( + &mut self, + _msg: &RecvMessage, + _dev: HfDevSelect, + _addr: u32, + _protect: HfProtectMode, + _data: LenLimit, PAGE_SIZE_BYTES>, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn read( + &mut self, + _: &RecvMessage, + _addr: u32, + _dest: LenLimit, PAGE_SIZE_BYTES>, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn read_dev( + &mut self, + _msg: &RecvMessage, + _dev: HfDevSelect, + _addr: u32, + _dest: LenLimit, PAGE_SIZE_BYTES>, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn sector_erase( + &mut self, + _: &RecvMessage, + _addr: u32, + _protect: HfProtectMode, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn sector_erase_dev( + &mut self, + _: &RecvMessage, + _dev: HfDevSelect, + _addr: u32, + _protect: HfProtectMode, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn get_mux( + &mut self, + _: &RecvMessage, + ) -> Result> { + Err(self.0.into()) + } + + fn set_mux( + &mut self, + _: &RecvMessage, + _state: HfMuxState, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn get_dev( + &mut self, + _: &RecvMessage, + ) -> Result> { + Err(self.0.into()) + } + + fn set_dev( + &mut self, + _: &RecvMessage, + _state: HfDevSelect, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn check_dev( + &mut self, + _: &RecvMessage, + _state: HfDevSelect, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn hash( + &mut self, + _: &RecvMessage, + _addr: u32, + _len: u32, + ) -> Result<[u8; SHA256_SZ], RequestError> { + Err(self.0.into()) + } + + fn hash_significant_bits( + &mut self, + _: &RecvMessage, + _dev: HfDevSelect, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn get_cached_hash( + &mut self, + _: &RecvMessage, + _dev: HfDevSelect, + ) -> Result<[u8; SHA256_SZ], RequestError> { + Err(self.0.into()) + } + + fn get_persistent_data( + &mut self, + _: &RecvMessage, + ) -> Result> { + Err(self.0.into()) + } + + fn write_persistent_data( + &mut self, + _: &RecvMessage, + _dev_select: HfDevSelect, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn apob_begin( + &mut self, + _: &RecvMessage, + _length: u32, + _alg: drv_hf_api::ApobHash, + ) -> Result<(), RequestError> { + Err(drv_hf_api::ApobBeginError::InvalidState.into()) + } + + fn apob_write( + &mut self, + _: &RecvMessage, + _offset: u32, + _data: Leased, + ) -> Result<(), RequestError> { + Err(drv_hf_api::ApobWriteError::InvalidState.into()) + } + + fn apob_commit( + &mut self, + _: &RecvMessage, + ) -> Result<(), RequestError> { + Err(drv_hf_api::ApobCommitError::InvalidState.into()) + } + + fn apob_lock( + &mut self, + _: &RecvMessage, + ) -> Result<(), RequestError> { + // Locking is tautological if we're running the error server + Ok(()) + } + + fn apob_read( + &mut self, + _: &RecvMessage, + _offset: u32, + _data: Leased, + ) -> Result> { + Err(drv_hf_api::ApobReadError::InvalidState.into()) + } +} + pub mod idl { use drv_hf_api::{ - HfChipId, HfDevSelect, HfError, HfMuxState, HfPersistentData, - HfProtectMode, + ApobBeginError, ApobCommitError, ApobHash, ApobReadError, + ApobWriteError, HfChipId, HfDevSelect, HfError, HfMuxState, + HfPersistentData, HfProtectMode, }; include!(concat!(env!("OUT_DIR"), "/server_stub.rs")); } diff --git a/drv/cosmo-hf/src/main.rs b/drv/cosmo-hf/src/main.rs index 2b26578524..00cef52946 100644 --- a/drv/cosmo-hf/src/main.rs +++ b/drv/cosmo-hf/src/main.rs @@ -420,6 +420,10 @@ impl FlashDriver { self.drv.apob_flash_len.set_offset(pos.size); } + pub(crate) fn set_apob_offset(&self, addr: FlashAddr) { + self.drv.apob_flash_offset.set_offset(addr.0); + } + pub(crate) fn clear_apob_pos(&self) { self.drv.apob_flash_addr.set_offset(0); self.drv.apob_flash_len.set_offset(0); @@ -429,7 +433,7 @@ impl FlashDriver { /// Failure function, running an Idol response loop that always returns an error fn fail(err: drv_hf_api::HfError) { let mut buffer = [0; hf::idl::INCOMING_SIZE]; - let mut server = hf::idl::FailServer::new(err); + let mut server = hf::FailServer(err); loop { idol_runtime::dispatch(&mut buffer, &mut server); } diff --git a/drv/gimlet-hf-server/src/main.rs b/drv/gimlet-hf-server/src/main.rs index 740722c783..a790e66d87 100644 --- a/drv/gimlet-hf-server/src/main.rs +++ b/drv/gimlet-hf-server/src/main.rs @@ -631,15 +631,6 @@ impl idl::InOrderHostFlashImpl for ServerImpl { r } - fn bonus_page_program( - &mut self, - _: &RecvMessage, - _addr: u32, - _data: LenLimit, PAGE_SIZE_BYTES>, - ) -> Result<(), RequestError> { - Err(HfError::BadAddress.into()) - } - fn read( &mut self, _: &RecvMessage, @@ -672,15 +663,6 @@ impl idl::InOrderHostFlashImpl for ServerImpl { r } - fn bonus_read( - &mut self, - _: &RecvMessage, - _addr: u32, - _dest: LenLimit, PAGE_SIZE_BYTES>, - ) -> Result<(), RequestError> { - Err(HfError::BadAddress.into()) - } - fn sector_erase( &mut self, _: &RecvMessage, @@ -704,14 +686,6 @@ impl idl::InOrderHostFlashImpl for ServerImpl { r } - fn bonus_sector_erase( - &mut self, - _: &RecvMessage, - _addr: u32, - ) -> Result<(), RequestError> { - Err(HfError::BadAddress.into()) - } - fn get_mux( &mut self, _: &RecvMessage, @@ -940,6 +914,47 @@ impl idl::InOrderHostFlashImpl for ServerImpl { Ok(()) } + + fn apob_begin( + &mut self, + _: &RecvMessage, + _length: u32, + _alg: drv_hf_api::ApobHash, + ) -> Result<(), RequestError> { + Err(drv_hf_api::ApobBeginError::NotImplemented.into()) + } + + fn apob_write( + &mut self, + _: &RecvMessage, + _offset: u32, + _data: Leased, + ) -> Result<(), RequestError> { + Err(drv_hf_api::ApobWriteError::NotImplemented.into()) + } + + fn apob_commit( + &mut self, + _: &RecvMessage, + ) -> Result<(), RequestError> { + Err(drv_hf_api::ApobCommitError::NotImplemented.into()) + } + + fn apob_lock( + &mut self, + _: &RecvMessage, + ) -> Result<(), RequestError> { + Err(RequestError::Fail(ClientError::BadMessageContents)) + } + + fn apob_read( + &mut self, + _: &RecvMessage, + _offset: u32, + _data: Leased, + ) -> Result> { + Err(drv_hf_api::ApobReadError::NotImplemented.into()) + } } impl NotificationHandler for ServerImpl { @@ -954,10 +969,231 @@ impl NotificationHandler for ServerImpl { } } +struct FailServer(drv_hf_api::HfError); + +impl NotificationHandler for FailServer { + fn current_notification_mask(&self) -> u32 { + 0 + } + + fn handle_notification(&mut self, _bits: userlib::NotificationBits) { + unreachable!() + } +} + +impl idl::InOrderHostFlashImpl for FailServer { + fn read_id( + &mut self, + _: &RecvMessage, + ) -> Result> { + Err(self.0.into()) + } + + fn capacity( + &mut self, + _: &RecvMessage, + ) -> Result> { + Err(self.0.into()) + } + + fn read_status( + &mut self, + _: &RecvMessage, + ) -> Result> { + Err(self.0.into()) + } + + fn bulk_erase( + &mut self, + _: &RecvMessage, + _protect: HfProtectMode, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn page_program( + &mut self, + _: &RecvMessage, + _addr: u32, + _protect: HfProtectMode, + _data: LenLimit, PAGE_SIZE_BYTES>, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn page_program_dev( + &mut self, + _msg: &RecvMessage, + _dev: HfDevSelect, + _addr: u32, + _protect: HfProtectMode, + _data: LenLimit, PAGE_SIZE_BYTES>, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn read( + &mut self, + _: &RecvMessage, + _addr: u32, + _dest: LenLimit, PAGE_SIZE_BYTES>, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn read_dev( + &mut self, + _msg: &RecvMessage, + _dev: HfDevSelect, + _addr: u32, + _dest: LenLimit, PAGE_SIZE_BYTES>, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn sector_erase( + &mut self, + _: &RecvMessage, + _addr: u32, + _protect: HfProtectMode, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn sector_erase_dev( + &mut self, + _: &RecvMessage, + _dev: HfDevSelect, + _addr: u32, + _protect: HfProtectMode, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn get_mux( + &mut self, + _: &RecvMessage, + ) -> Result> { + Err(self.0.into()) + } + + fn set_mux( + &mut self, + _: &RecvMessage, + _state: HfMuxState, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn get_dev( + &mut self, + _: &RecvMessage, + ) -> Result> { + Err(self.0.into()) + } + + fn set_dev( + &mut self, + _: &RecvMessage, + _state: HfDevSelect, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn check_dev( + &mut self, + _: &RecvMessage, + _state: HfDevSelect, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn hash( + &mut self, + _: &RecvMessage, + _addr: u32, + _len: u32, + ) -> Result<[u8; SHA256_SZ], RequestError> { + Err(self.0.into()) + } + + fn hash_significant_bits( + &mut self, + _: &RecvMessage, + _dev: HfDevSelect, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn get_cached_hash( + &mut self, + _: &RecvMessage, + _dev: HfDevSelect, + ) -> Result<[u8; SHA256_SZ], RequestError> { + Err(self.0.into()) + } + + fn get_persistent_data( + &mut self, + _: &RecvMessage, + ) -> Result> { + Err(self.0.into()) + } + + fn write_persistent_data( + &mut self, + _: &RecvMessage, + _dev_select: HfDevSelect, + ) -> Result<(), RequestError> { + Err(self.0.into()) + } + + fn apob_begin( + &mut self, + _: &RecvMessage, + _length: u32, + _alg: drv_hf_api::ApobHash, + ) -> Result<(), RequestError> { + Err(drv_hf_api::ApobBeginError::NotImplemented.into()) + } + + fn apob_write( + &mut self, + _: &RecvMessage, + _offset: u32, + _data: Leased, + ) -> Result<(), RequestError> { + Err(drv_hf_api::ApobWriteError::NotImplemented.into()) + } + + fn apob_commit( + &mut self, + _: &RecvMessage, + ) -> Result<(), RequestError> { + Err(drv_hf_api::ApobCommitError::NotImplemented.into()) + } + + fn apob_lock( + &mut self, + _: &RecvMessage, + ) -> Result<(), RequestError> { + Ok(()) + } + + fn apob_read( + &mut self, + _: &RecvMessage, + _offset: u32, + _data: Leased, + ) -> Result> { + Err(drv_hf_api::ApobReadError::NotImplemented.into()) + } +} + /// Failure function, running an Idol response loop that always returns an error fn fail(err: drv_hf_api::HfError) -> ! { let mut buffer = [0; idl::INCOMING_SIZE]; - let mut server = idl::FailServer::new(err); + let mut server = FailServer(err); loop { idol_runtime::dispatch(&mut buffer, &mut server); } @@ -968,6 +1204,10 @@ mod idl { HfChipId, HfDevSelect, HfError, HfMuxState, HfPersistentData, HfProtectMode, }; + use drv_hf_api::{ + ApobBeginError, ApobCommitError, ApobHash, ApobReadError, + ApobWriteError, + }; include!(concat!(env!("OUT_DIR"), "/server_stub.rs")); } diff --git a/drv/hf-api/src/lib.rs b/drv/hf-api/src/lib.rs index 32e453c1b9..1069faa7c1 100644 --- a/drv/hf-api/src/lib.rs +++ b/drv/hf-api/src/lib.rs @@ -271,4 +271,118 @@ pub struct HfChipId { pub unique_id: [u8; 17], } +//////////////////////////////////////////////////////////////////////////////// +// APOB types are below! + +/// Hash type used when writing an APOB to bonus flash +#[derive( + Copy, Clone, Debug, PartialEq, Serialize, Deserialize, SerializedSize, +)] +#[repr(u8)] +pub enum ApobHash { + Sha256([u8; 32]), +} + +#[derive( + Debug, + Copy, + Clone, + Eq, + PartialEq, + Deserialize, + Serialize, + SerializedSize, + IdolError, + FromPrimitive, + counters::Count, +)] +pub enum ApobBeginError { + /// APOB is not implemented on this hardware + NotImplemented = 1, + /// The APOB state machine does not allow a `Begin` message + InvalidState, + /// The data length will not fit in an APOB slot + BadDataLength, +} + +#[derive( + Debug, + Copy, + Clone, + Eq, + PartialEq, + Deserialize, + Serialize, + SerializedSize, + IdolError, + FromPrimitive, + counters::Count, +)] +pub enum ApobWriteError { + /// APOB is not implemented on this hardware + NotImplemented = 1, + /// The APOB state machine does not allow a `Data` message + InvalidState, + /// Offset exceeds the slot size + InvalidOffset, + /// Write size exceeds the slot size + InvalidSize, + /// Flash write failed + WriteFailed, + /// Flash write would change data in an unerased region + NotErased, +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Deserialize, + Serialize, + SerializedSize, + FromPrimitive, + IdolError, + counters::Count, +)] +pub enum ApobReadError { + /// APOB is not implemented on this hardware + NotImplemented = 1, + /// The state machine is currently expecting a write or commit message + InvalidState, + /// There is no valid APOB available to read + NoValidApob, + /// Offset exceeds the slot size + InvalidOffset, + /// Write size exceeds the slot size + InvalidSize, + /// Flash read failed + ReadFailed, +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Deserialize, + Serialize, + SerializedSize, + FromPrimitive, + IdolError, + counters::Count, +)] +pub enum ApobCommitError { + /// APOB is not implemented on this hardware + NotImplemented = 1, + /// Committing APOB state has been disallowed for this boot + InvalidState, + /// Validating the APOB failed, e.g. due to invalid data + ValidationFailed, + /// Committing the APOB failed, e.g. due to a flash write error + CommitFailed, +} + include!(concat!(env!("OUT_DIR"), "/client_stub.rs")); diff --git a/drv/mock-gimlet-hf-server/src/main.rs b/drv/mock-gimlet-hf-server/src/main.rs index e7abedefe6..a5d6999c05 100644 --- a/drv/mock-gimlet-hf-server/src/main.rs +++ b/drv/mock-gimlet-hf-server/src/main.rs @@ -239,30 +239,45 @@ impl idl::InOrderHostFlashImpl for ServerImpl { Err(HfError::HashNotConfigured.into()) } - fn bonus_page_program( + fn apob_begin( &mut self, _: &RecvMessage, - _addr: u32, - _data: LenLimit, PAGE_SIZE_BYTES>, - ) -> Result<(), RequestError> { - Err(HfError::BadAddress.into()) + _length: u32, + _alg: drv_hf_api::ApobHash, + ) -> Result<(), RequestError> { + Err(drv_hf_api::ApobBeginError::NotImplemented.into()) } - fn bonus_read( + fn apob_write( &mut self, _: &RecvMessage, - _addr: u32, - _dest: LenLimit, PAGE_SIZE_BYTES>, - ) -> Result<(), RequestError> { - Err(HfError::BadAddress.into()) + _offset: u32, + _data: Leased, + ) -> Result<(), RequestError> { + Err(drv_hf_api::ApobWriteError::NotImplemented.into()) } - fn bonus_sector_erase( + fn apob_commit( &mut self, _: &RecvMessage, - _addr: u32, - ) -> Result<(), RequestError> { - Err(HfError::BadAddress.into()) + ) -> Result<(), RequestError> { + Err(drv_hf_api::ApobCommitError::NotImplemented.into()) + } + + fn apob_lock( + &mut self, + _: &RecvMessage, + ) -> Result<(), RequestError> { + Ok(()) + } + + fn apob_read( + &mut self, + _: &RecvMessage, + _offset: u32, + _data: Leased, + ) -> Result> { + Err(drv_hf_api::ApobReadError::InvalidState.into()) } } @@ -281,6 +296,10 @@ mod idl { HfChipId, HfDevSelect, HfError, HfMuxState, HfPersistentData, HfProtectMode, }; + use drv_hf_api::{ + ApobBeginError, ApobCommitError, ApobHash, ApobReadError, + ApobWriteError, + }; include!(concat!(env!("OUT_DIR"), "/server_stub.rs")); } diff --git a/drv/spartan7-loader/cosmo-seq/README.md b/drv/spartan7-loader/cosmo-seq/README.md index 98a5c35b14..01954cff48 100644 --- a/drv/spartan7-loader/cosmo-seq/README.md +++ b/drv/spartan7-loader/cosmo-seq/README.md @@ -1,3 +1,3 @@ FPGA images and collateral are generated from: -[this sha](https://github.com/oxidecomputer/quartz/commit/bdc5fb31e1905a1b66c19647fe2d156dd1b97b7b) -[release](https://api.github.com/repos/oxidecomputer/quartz/releases/242278756) \ No newline at end of file +[this sha](https://github.com/oxidecomputer/quartz/commit/cd50a1785b4fcc075036f2ed0e1e8de6c8797122) +[release](https://api.github.com/repos/oxidecomputer/quartz/releases/253414231) \ No newline at end of file diff --git a/drv/spartan7-loader/cosmo-seq/cosmo_seq.bz2 b/drv/spartan7-loader/cosmo-seq/cosmo_seq.bz2 index 271f0fd82b..4260a8132c 100644 Binary files a/drv/spartan7-loader/cosmo-seq/cosmo_seq.bz2 and b/drv/spartan7-loader/cosmo-seq/cosmo_seq.bz2 differ diff --git a/drv/spartan7-loader/cosmo-seq/cosmo_seq_top.json b/drv/spartan7-loader/cosmo-seq/cosmo_seq_top.json index 0e3eb61297..251e8083eb 100644 --- a/drv/spartan7-loader/cosmo-seq/cosmo_seq_top.json +++ b/drv/spartan7-loader/cosmo-seq/cosmo_seq_top.json @@ -15,7 +15,7 @@ }, { "type": "addrmap", - "addr_span_bytes": 44, + "addr_span_bytes": 48, "inst_name": "spi_nor", "orig_type_name": "spi_nor_regs", "addr_offset": 256, diff --git a/drv/spartan7-loader/cosmo-seq/spi_nor_regs.json b/drv/spartan7-loader/cosmo-seq/spi_nor_regs.json index ae86d6aff9..f656c18a6f 100644 --- a/drv/spartan7-loader/cosmo-seq/spi_nor_regs.json +++ b/drv/spartan7-loader/cosmo-seq/spi_nor_regs.json @@ -1,6 +1,6 @@ { "type": "addrmap", - "addr_span_bytes": 44, + "addr_span_bytes": 48, "inst_name": "spi_nor_regs", "addr_offset": 0, "children": [ @@ -311,6 +311,26 @@ "desc": "Length of the APOB flash region" } ] + }, + { + "type": "reg", + "inst_name": "ApobFlashOffset", + "addr_offset": 44, + "regwidth": 32, + "min_accesswidth": 32, + "children": [ + { + "type": "field", + "inst_name": "offset", + "lsb": 0, + "msb": 31, + "reset": 0, + "sw_access": "rw", + "se_onread": null, + "se_onwrite": null, + "desc": "Offset of the APOB flash slot" + } + ] } ] } \ No newline at end of file diff --git a/drv/spartan7-loader/grapefruit/README.md b/drv/spartan7-loader/grapefruit/README.md index 32557e2364..d60224aef5 100644 --- a/drv/spartan7-loader/grapefruit/README.md +++ b/drv/spartan7-loader/grapefruit/README.md @@ -1,3 +1,3 @@ FPGA images and collateral are generated from: -[this sha](https://github.com/oxidecomputer/quartz/commit/bdc5fb31e1905a1b66c19647fe2d156dd1b97b7b) -[release](https://api.github.com/repos/oxidecomputer/quartz/releases/242277257) \ No newline at end of file +[this sha](https://github.com/oxidecomputer/quartz/commit/cd50a1785b4fcc075036f2ed0e1e8de6c8797122) +[release](https://api.github.com/repos/oxidecomputer/quartz/releases/253414406) \ No newline at end of file diff --git a/drv/spartan7-loader/grapefruit/gfruit_top.json b/drv/spartan7-loader/grapefruit/gfruit_top.json index 2101908021..86957c1b88 100644 --- a/drv/spartan7-loader/grapefruit/gfruit_top.json +++ b/drv/spartan7-loader/grapefruit/gfruit_top.json @@ -15,7 +15,7 @@ }, { "type": "addrmap", - "addr_span_bytes": 44, + "addr_span_bytes": 48, "inst_name": "spi_nor", "orig_type_name": "spi_nor_regs", "addr_offset": 256, diff --git a/drv/spartan7-loader/grapefruit/grapefruit.bz2 b/drv/spartan7-loader/grapefruit/grapefruit.bz2 index b9a30c4b54..23242bd657 100644 Binary files a/drv/spartan7-loader/grapefruit/grapefruit.bz2 and b/drv/spartan7-loader/grapefruit/grapefruit.bz2 differ diff --git a/drv/spartan7-loader/grapefruit/spi_nor_regs.json b/drv/spartan7-loader/grapefruit/spi_nor_regs.json index ae86d6aff9..f656c18a6f 100644 --- a/drv/spartan7-loader/grapefruit/spi_nor_regs.json +++ b/drv/spartan7-loader/grapefruit/spi_nor_regs.json @@ -1,6 +1,6 @@ { "type": "addrmap", - "addr_span_bytes": 44, + "addr_span_bytes": 48, "inst_name": "spi_nor_regs", "addr_offset": 0, "children": [ @@ -311,6 +311,26 @@ "desc": "Length of the APOB flash region" } ] + }, + { + "type": "reg", + "inst_name": "ApobFlashOffset", + "addr_offset": 44, + "regwidth": 32, + "min_accesswidth": 32, + "children": [ + { + "type": "field", + "inst_name": "offset", + "lsb": 0, + "msb": 31, + "reset": 0, + "sw_access": "rw", + "se_onread": null, + "se_onwrite": null, + "desc": "Offset of the APOB flash slot" + } + ] } ] } \ No newline at end of file diff --git a/idl/hf.idol b/idl/hf.idol index 26311ec81c..d4d37a933a 100644 --- a/idl/hf.idol +++ b/idl/hf.idol @@ -251,41 +251,59 @@ Interface( err: CLike("HfError"), ), ), - "bonus_page_program": ( - description: "programs a page into the bonus region of host flash", + "apob_begin": ( + description: "begin writing APOB data to bonus flash", args: { - "address": "u32", - }, - leases: { - "data": (type: "[u8]", read: true, max_len: Some(256)), + "length": "u32", + "algorithm": "ApobHash", }, reply: Result( ok: "()", - err: CLike("HfError"), + err: CLike("ApobBeginError"), ), + encoding: Hubpack, + idempotent: true, ), - "bonus_read": ( - description: "reads from the bonus region of host flash", + "apob_write": ( + description: "writes to the current APOB slot", args: { - "address": "u32", + "offset": "u32", }, leases: { - "data": (type: "[u8]", write: true, max_len: Some(256)), + "data": (type: "[u8]", read: true), }, reply: Result( ok: "()", - err: CLike("HfError"), + err: CLike("ApobWriteError"), + ), + idempotent: true, + ), + "apob_commit": ( + description: "commits the written APOB slot", + reply: Result( + ok: "()", + err: CLike("ApobCommitError"), ), + idempotent: true, ), - "bonus_sector_erase": ( - description: "erases a 64 KiB sector from the bonus region of host flash", + "apob_lock": ( + description: "locks the APOB state machine", + reply: Simple("()"), + idempotent: true, + ), + "apob_read": ( + description: "reads from the current APOB slot", args: { - "address": "u32", + "offset": "u32", + }, + leases: { + "data": (type: "[u8]", write: true), }, reply: Result( - ok: "()", - err: CLike("HfError"), + ok: "usize", + err: CLike("ApobReadError"), ), + idempotent: true, ), }, ) diff --git a/lib/host-sp-messages/src/lib.rs b/lib/host-sp-messages/src/lib.rs index ee5e73fd3d..a5fe720a52 100644 --- a/lib/host-sp-messages/src/lib.rs +++ b/lib/host-sp-messages/src/lib.rs @@ -123,6 +123,23 @@ pub enum HostToSp { // We use a raw `u8` here for the same reason as in `KeyLookup` above. key: u8, }, + // ApobBegin begins an APOB write + ApobBegin { + length: u64, + algorithm: u8, + // Followed by a binary blob for the hash, with length depending on + // algorithm + }, + ApobCommit, + ApobData { + offset: u64, + // Followed by trailing data, implicitly sized + }, + // ApobRead return `ApobReadResult` followed by trailing data + ApobRead { + offset: u64, + size: u64, + }, } /// The order of these cases is critical! We are relying on hubpack's encoding @@ -185,6 +202,10 @@ pub enum SpToHost { name: [u8; 32], }, KeySetResult(#[count(children)] KeySetResult), + ApobBegin(#[count(children)] ApobBeginResult), + ApobCommit(#[count(children)] ApobCommitResult), + ApobData(#[count(children)] ApobDataResult), + ApobRead(#[count(children)] ApobReadResult), } #[derive(Debug, Clone, Copy, PartialEq, Eq, num_derive::FromPrimitive)] @@ -244,6 +265,108 @@ pub enum KeySetResult { DataTooLong, } +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Deserialize, + Serialize, + SerializedSize, + counters::Count, +)] +pub enum ApobBeginResult { + Ok, + /// APOB is not implemented on this hardware + NotImplemented, + /// The APOB state machine does not allow a `Begin` message + InvalidState, + /// The algorithm specified is invalid + InvalidAlgorithm, + /// The hash length does not match the algorithm + BadHashLength, + /// The data length will not fit in an APOB slot + BadDataLength, +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Deserialize, + Serialize, + SerializedSize, + counters::Count, +)] +pub enum ApobCommitResult { + Ok, + /// APOB is not implemented on this hardware + NotImplemented, + /// Committing APOB state has been disallowed for this boot + InvalidState, + /// Validating the APOB failed, e.g. due to invalid data + ValidationFailed, + /// Committing the APOB failed, e.g. due to a flash write error + CommitFailed, +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Deserialize, + Serialize, + SerializedSize, + counters::Count, +)] +pub enum ApobDataResult { + Ok, + /// APOB is not implemented on this hardware + NotImplemented, + /// The APOB state machine does not allow a `Data` message + InvalidState, + /// Offset exceeds the slot size + InvalidOffset, + /// Write size exceeds the slot size + InvalidSize, + /// Flash write failed + WriteFailed, + /// Flash write would change data in an unerased region + NotErased, +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Deserialize, + Serialize, + SerializedSize, + counters::Count, +)] +pub enum ApobReadResult { + Ok, + /// APOB is not implemented on this hardware + NotImplemented, + /// The state machine is currently expecting a write or commit message + InvalidState, + /// There is no valid APOB available to read + NoValidApob, + /// Offset exceeds the slot size + InvalidOffset, + /// Write size exceeds the slot size + InvalidSize, + /// Flash read failed + ReadFailed, +} + /// Results for an inventory data request /// /// These **cannot be reordered**; the host and SP must agree on them. diff --git a/task/host-sp-comms/src/main.rs b/task/host-sp-comms/src/main.rs index 42d7678175..49ac42b854 100644 --- a/task/host-sp-comms/src/main.rs +++ b/task/host-sp-comms/src/main.rs @@ -145,6 +145,16 @@ enum Trace { #[count(children)] message: SpToHost, }, + ApobWriteError { + offset: u32, + #[count(children)] + err: drv_hf_api::ApobWriteError, + }, + ApobReadError { + offset: u32, + #[count(children)] + err: drv_hf_api::ApobReadError, + }, } counted_ringbuf!(Trace, 20, Trace::None); @@ -812,6 +822,27 @@ impl ServerImpl { self.tx_buf.reset(); } + // If we receive an out-of-sequence message, then lock the APOB state + // machine. This makes it harder for malicious hosts to exfiltrate + // data via the host flash APOB slots. + match request { + HostToSp::KeyLookup { .. } + | HostToSp::GetBootStorageUnit + | HostToSp::GetIdentity + | HostToSp::GetStatus + | HostToSp::AckSpStart + | HostToSp::ApobBegin { .. } + | HostToSp::ApobData { .. } + | HostToSp::ApobRead { .. } + | HostToSp::ApobCommit => { + // These are explicitly allowed + } + _ => { + // Anything not allowed is prohibited! + self.hf.apob_lock(); + } + } + // We defer any actions until after we've serialized our response to // avoid borrow checker issues with calling methods on `self`. let mut action = None; @@ -1024,6 +1055,39 @@ impl ServerImpl { }), } } + HostToSp::ApobBegin { length, algorithm } => { + Some(SpToHost::ApobBegin(Self::apob_begin( + &self.hf, length, algorithm, data, + ))) + } + HostToSp::ApobCommit => { + // Call into `hf` to do the work here + use drv_hf_api::ApobCommitError; + use host_sp_messages::ApobCommitResult; + Some(SpToHost::ApobCommit(match self.hf.apob_commit() { + Ok(()) => ApobCommitResult::Ok, + Err(ApobCommitError::NotImplemented) => { + ApobCommitResult::NotImplemented + } + Err(ApobCommitError::InvalidState) => { + ApobCommitResult::InvalidState + } + Err(ApobCommitError::ValidationFailed) => { + ApobCommitResult::ValidationFailed + } + Err(ApobCommitError::CommitFailed) => { + ApobCommitResult::CommitFailed + } + })) + } + HostToSp::ApobData { offset } => Some(SpToHost::ApobData( + Self::apob_write(&self.hf, offset, data), + )), + HostToSp::ApobRead { offset, size } => { + // apob_read does serialization itself + self.apob_read(header.sequence, offset, size); + None + } }; if let Some(response) = response { @@ -1052,6 +1116,128 @@ impl ServerImpl { Ok(()) } + fn apob_begin( + hf: &HostFlash, + length: u64, + algorithm: u8, + data: &[u8], + ) -> host_sp_messages::ApobBeginResult { + // Decode into internal types, then call into `hf` + // XXX should bad hash algorithms or lengths lock the APOB? + use drv_hf_api::{ApobBeginError, ApobHash}; + use host_sp_messages::ApobBeginResult; + let Ok(length) = u32::try_from(length) else { + return host_sp_messages::ApobBeginResult::BadDataLength; + }; + match algorithm { + 0 => { + if let Ok(d) = data.try_into() { + let hash = ApobHash::Sha256(d); + match hf.apob_begin(length, hash) { + Ok(()) => ApobBeginResult::Ok, + Err(ApobBeginError::NotImplemented) => { + ApobBeginResult::NotImplemented + } + Err(ApobBeginError::InvalidState) => { + ApobBeginResult::InvalidState + } + Err(ApobBeginError::BadDataLength) => { + ApobBeginResult::BadDataLength + } + } + } else { + ApobBeginResult::BadHashLength + } + } + _ => ApobBeginResult::InvalidAlgorithm, + } + } + + /// Write data to the bonus region of flash + /// + /// This does not take `&self` because we need to force a split borrow + fn apob_write( + hf: &HostFlash, + offset: u64, + data: &[u8], + ) -> host_sp_messages::ApobDataResult { + use drv_hf_api::ApobWriteError; + use host_sp_messages::ApobDataResult; + let Ok(offset) = u32::try_from(offset) else { + return ApobDataResult::InvalidOffset; + }; + match hf.apob_write(offset, data) { + Ok(()) => ApobDataResult::Ok, + Err(err) => { + ringbuf_entry!(Trace::ApobWriteError { offset, err }); + match err { + ApobWriteError::NotImplemented => { + ApobDataResult::NotImplemented + } + ApobWriteError::InvalidState => { + ApobDataResult::InvalidState + } + ApobWriteError::InvalidOffset => { + ApobDataResult::InvalidOffset + } + ApobWriteError::InvalidSize => ApobDataResult::InvalidSize, + ApobWriteError::WriteFailed => ApobDataResult::WriteFailed, + ApobWriteError::NotErased => ApobDataResult::NotErased, + } + } + } + } + + /// Reads and encodes data from the bonus region of flash + fn apob_read(&mut self, sequence: u64, offset: u64, size: u64) { + use drv_hf_api::ApobReadError; + use host_sp_messages::ApobReadResult; + let Ok(size) = usize::try_from(size) else { + self.tx_buf.encode_response( + sequence, + &SpToHost::ApobRead(ApobReadResult::InvalidSize), + |_buf| 0, + ); + return; + }; + let Ok(offset) = u32::try_from(offset) else { + self.tx_buf.encode_response( + sequence, + &SpToHost::ApobRead(ApobReadResult::InvalidOffset), + |_buf| 0, + ); + return; + }; + self.tx_buf.try_encode_response( + sequence, + &SpToHost::ApobRead(ApobReadResult::Ok), + |buf| match self.hf.apob_read(offset, &mut buf[..size]) { + Ok(n) => Ok(n), + Err(err) => { + ringbuf_entry!(Trace::ApobReadError { offset, err }); + Err(SpToHost::ApobRead(match err { + ApobReadError::NotImplemented => { + ApobReadResult::NotImplemented + } + ApobReadError::InvalidState => { + ApobReadResult::InvalidState + } + ApobReadError::NoValidApob => { + ApobReadResult::NoValidApob + } + ApobReadError::InvalidOffset => { + ApobReadResult::InvalidOffset + } + ApobReadError::InvalidSize => { + ApobReadResult::InvalidSize + } + ApobReadError::ReadFailed => ApobReadResult::ReadFailed, + })) + } + }, + ); + } + fn handle_sprot( &mut self, sequence: u64,