Skip to content

Commit

Permalink
Implement epoch-based rollback protection in LPC55 update_server
Browse files Browse the repository at this point in the history
  • Loading branch information
lzrd committed Sep 30, 2024
1 parent 6ca7ccc commit a50ba38
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 31 deletions.
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 9 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,20 @@ zip = { version = "0.6", default-features = false, features = ["bzip2"] }
# Oxide forks and repos
attest-data = { git = "https://github.com/oxidecomputer/dice-util", default-features = false, version = "0.3.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"] }
#gateway-messages = { git = "https://github.com/oxidecomputer/management-gateway-service", default-features = false, features = ["smoltcp"] }
# XXX fix before push
gateway-messages = { path = "/home/stoltz/Oxide/src/mgs/epoch/gateway-messages", default-features = false, features = ["smoltcp"] }
gimlet-inspector-protocol = { git = "https://github.com/oxidecomputer/gimlet-inspector-protocol", version = "0.1.0" }
hif = { git = "https://github.com/oxidecomputer/hif", default-features = false }
humpty = { git = "https://github.com/oxidecomputer/humpty", default-features = false, version = "0.1.3" }
hubtools = { git = "https://github.com/oxidecomputer/hubtools", default-features = false, version = "0.4.1" }
#hubtools = { git = "https://github.com/oxidecomputer/hubtools", default-features = false, version = "0.4.1" }
# XXX fix before push
# hubtools = { git = "https://github.com/oxidecomputer/hubtools", default-features = false, branch = "epoch", version = "0.4.7" }
hubtools = { path = "/home/stoltz/Oxide/src/hubtools/epoch/hubtools" }
idol = { git = "https://github.com/oxidecomputer/idolatry.git", default-features = false }
idol-runtime = { git = "https://github.com/oxidecomputer/idolatry.git", default-features = false }
lpc55_sign = { git = "https://github.com/oxidecomputer/lpc55_support", default-features = false }
#lpc55_sign = { git = "https://github.com/oxidecomputer/lpc55_support", default-features = false }
lpc55_sign = { path = "/home/stoltz/Oxide/src/lpc55_support/lpc55_sign", default-features = false }
ordered-toml = { git = "https://github.com/oxidecomputer/ordered-toml", default-features = false }
pmbus = { git = "https://github.com/oxidecomputer/pmbus", default-features = false }
salty = { version = "0.3", default-features = false }
Expand Down
1 change: 1 addition & 0 deletions app/gimlet/base.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ chip = "../../chips/stm32h7"
memory = "memory-large.toml"
stacksize = 896
fwid = true
epoch = 0

[kernel]
name = "gimlet"
Expand Down
2 changes: 1 addition & 1 deletion app/lpc55xpresso/app.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ start = true
[tasks.update_server]
name = "lpc55-update-server"
priority = 3
max-sizes = {flash = 27008, ram = 16704}
max-sizes = {flash = 30368, ram = 16704}
stacksize = 8192
start = true
sections = {bootstate = "usbsram"}
Expand Down
2 changes: 1 addition & 1 deletion app/oxide-rot-1/app-dev.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ start = true
[tasks.update_server]
name = "lpc55-update-server"
priority = 3
max-sizes = {flash = 27904, ram = 17344, usbsram = 4096}
max-sizes = {flash = 30368, ram = 17344, usbsram = 4096}
# TODO: Size this appropriately
stacksize = 8192
start = true
Expand Down
9 changes: 7 additions & 2 deletions build/xtask/src/dist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,7 @@ pub fn package(
// The Git hash is included in the default caboose under the key
// `GITC`, so we don't include it in the pseudo-version.
archive
.write_default_caboose(None)
.write_default_caboose(None, None)
.context("writing caboose into archive")?;
archive.overwrite().context("overwriting archive")?;
}
Expand All @@ -621,8 +621,13 @@ pub fn package(
if let Some(signing) = &cfg.toml.signing {
let mut archive = hubtools::RawHubrisArchive::load(&archive_name)
.context("loading archive with hubtools")?;
let priv_key_rel_path = signing
.certs
.private_key
.clone()
.context("missing private key path")?;
let private_key = lpc55_sign::cert::read_rsa_private_key(
&cfg.app_src_dir.join(&signing.certs.private_key),
&cfg.app_src_dir.join(priv_key_rel_path),
)
.with_context(|| {
format!(
Expand Down
10 changes: 10 additions & 0 deletions drv/lpc55-update-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,16 @@ impl From<RotSlot> for SlotId {
}
}

impl SlotId {
pub fn other(&self) -> SlotId {
if *self == SlotId::A {
SlotId::B
} else {
SlotId::A
}
}
}

impl TryFrom<u16> for SlotId {
type Error = ();
fn try_from(i: u16) -> Result<Self, Self::Error> {
Expand Down
155 changes: 140 additions & 15 deletions drv/lpc55-update-server/src/images.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ use crate::{
use abi::{ImageHeader, CABOOSE_MAGIC, HEADER_MAGIC};
use core::ops::Range;
use core::ptr::addr_of;
use drv_lpc55_update_api::{RawCabooseError, RotComponent, SlotId};
use drv_caboose::CabooseReader;
use drv_lpc55_update_api::{
RawCabooseError, RotComponent, SlotId, BLOCK_SIZE_BYTES,
};
use drv_update_api::UpdateError;
use zerocopy::{AsBytes, FromBytes};

Expand All @@ -47,6 +50,7 @@ pub const HEADER_BLOCK: usize = 0;
// An image may have an ImageHeader located after the
// LPC55's mixed header/vector table.
pub const IMAGE_HEADER_OFFSET: u32 = 0x130;
pub const CABOOSE_TAG_EPOC: [u8; 4] = [b'E', b'P', b'O', b'C'];

/// Address ranges that may contain an image during storage and active use.
/// `stored` and `at_runtime` ranges are the same except for `stage0next`.
Expand Down Expand Up @@ -181,7 +185,7 @@ impl TryFrom<&[u8]> for ImageVectorsLpc55 {
/// the end of optional caboose and the beginning of the signature block.
pub fn validate_header_block(
header_access: &ImageAccess<'_>,
) -> Result<u32, UpdateError> {
) -> Result<(Option<Epoch>, u32), UpdateError> {
let mut vectors = ImageVectorsLpc55::new_zeroed();
let mut header = ImageHeader::new_zeroed();

Expand All @@ -208,15 +212,17 @@ pub fn validate_header_block(
// Note that `ImageHeader.epoch` is used by rollback protection for early
// rejection of invalid images.
// TODO: Improve estimate of where the first executable instruction can be.
let code_offset = if header.magic == HEADER_MAGIC {
let (code_offset, epoch) = if header.magic == HEADER_MAGIC {
if header.total_image_len != vectors.nxp_offset_to_specific_header {
// ImageHeader disagrees with LPC55 vectors.
return Err(UpdateError::InvalidHeaderBlock);
}
// Adding constants should be resolved at compile time: no call to panic.
IMAGE_HEADER_OFFSET + (core::mem::size_of::<ImageHeader>() as u32)
(
IMAGE_HEADER_OFFSET + (core::mem::size_of::<ImageHeader>() as u32),
Some(Epoch::from(header.epoch)),
)
} else {
IMAGE_HEADER_OFFSET
(IMAGE_HEADER_OFFSET, None)
};

if vectors.nxp_image_length as usize > header_access.at_runtime().len() {
Expand All @@ -243,7 +249,7 @@ pub fn validate_header_block(
return Err(UpdateError::InvalidHeaderBlock);
}

Ok(vectors.nxp_offset_to_specific_header)
Ok((epoch, vectors.nxp_offset_to_specific_header))
}

/// Get the range of the caboose contained within an image if it exists.
Expand All @@ -260,7 +266,7 @@ pub fn caboose_slice(
//
// In this context, NoImageHeader actually means that the image
// is not well formed.
let image_end_offset = validate_header_block(image)
let (_epoch, image_end_offset) = validate_header_block(image)
.map_err(|_| RawCabooseError::NoImageHeader)?;

// By construction, the last word of the caboose is its size as a `u32`
Expand Down Expand Up @@ -318,7 +324,7 @@ enum Accessor<'a> {
},
// Hybrid is used for later implementation of rollback protection.
// The buffer is used in place of the beginning of the flash range.
_Hybrid {
Hybrid {
buffer: &'a [u8],
flash: &'a drv_lpc55_flash::Flash<'a>,
span: FlashRange,
Expand All @@ -330,7 +336,7 @@ impl Accessor<'_> {
match self {
Accessor::Flash { span, .. }
| Accessor::Ram { span, .. }
| Accessor::_Hybrid { span, .. } => &span.at_runtime,
| Accessor::Hybrid { span, .. } => &span.at_runtime,
}
}
}
Expand Down Expand Up @@ -375,15 +381,15 @@ impl ImageAccess<'_> {
}
}

pub fn _new_hybrid<'a>(
pub fn new_hybrid<'a>(
flash: &'a drv_lpc55_flash::Flash<'a>,
buffer: &'a [u8],
component: RotComponent,
slot: SlotId,
) -> ImageAccess<'a> {
let span = flash_range(component, slot);
ImageAccess {
accessor: Accessor::_Hybrid {
accessor: Accessor::Hybrid {
flash,
buffer,
span,
Expand Down Expand Up @@ -430,7 +436,7 @@ impl ImageAccess<'_> {
.and_then(u32::read_from)
.ok_or(UpdateError::OutOfBounds)?)
}
Accessor::_Hybrid {
Accessor::Hybrid {
buffer,
flash,
span,
Expand Down Expand Up @@ -491,7 +497,7 @@ impl ImageAccess<'_> {
Err(UpdateError::OutOfBounds)
}
}
Accessor::_Hybrid {
Accessor::Hybrid {
buffer: ram,
flash,
span,
Expand Down Expand Up @@ -547,7 +553,7 @@ impl ImageAccess<'_> {
ImageVectorsLpc55::read_from_prefix(&buffer[..])
.ok_or(UpdateError::OutOfBounds)
}
Accessor::Ram { buffer, .. } | Accessor::_Hybrid { buffer, .. } => {
Accessor::Ram { buffer, .. } | Accessor::Hybrid { buffer, .. } => {
ImageVectorsLpc55::read_from_prefix(buffer)
.ok_or(UpdateError::OutOfBounds)
}
Expand All @@ -556,3 +562,122 @@ impl ImageAccess<'_> {
round_up_to_flash_page(len).ok_or(UpdateError::BadLength)
}
}

#[derive(Clone, PartialEq)]
pub struct Epoch {
value: u32,
}

/// Convert from the ImageHeader.epoch format
impl From<u32> for Epoch {
fn from(number: u32) -> Self {
Epoch { value: number }
}
}

/// Convert from the caboose EPOC value format.
//
// Invalid EPOC values converted to Epoch{value:0} include:
// - empty slice
// - any non-ASCII-digits in slice
// - any non-UTF8 in slice
// - leading '+' normally allowed by parse()
// - values greater than u32::MAX
//
// Hand coding reduces size by about 950 bytes.
impl From<&[u8]> for Epoch {
fn from(chars: &[u8]) -> Self {
let epoch =
if chars.first().map(|c| c.is_ascii_digit()).unwrap_or(false) {
if let Ok(chars) = core::str::from_utf8(chars) {
chars.parse::<u32>().unwrap_or(0)
} else {
0
}
} else {
0
};
Epoch::from(epoch)
}
}

impl Epoch {
pub fn can_update_to(&self, next: Epoch) -> bool {
self.value >= next.value
}
}

/// Check a next image against an active image to determine
/// if rollback policy allows the next image.
/// If ImageHeader and Caboose are absent or Caboose does
/// not have an `EPOC` tag, then the Epoch is defaulted to zero.
/// This test is also used when the header block first arrives
/// so that images can be rejected early, i.e. before any flash has
/// been altered.
pub fn check_rollback_policy(
next_image: ImageAccess<'_>,
active_image: ImageAccess<'_>,
complete: bool,
) -> Result<(), UpdateError> {
let next_epoch = get_image_epoch(&next_image)?;
let active_epoch = get_image_epoch(&active_image)?;
match (active_epoch, next_epoch) {
// No active_epoch is treated as zero; update can proceed.
(None, _) => Ok(()),
(Some(active_epoch), None) => {
// If next_image is partial and HEADER_BLOCK has no ImageHeader,
// then there is no early rejection, proceed.
if !complete || active_epoch.can_update_to(Epoch::from(0u32)) {
Ok(())
} else {
Err(UpdateError::RollbackProtection)
}
}
(Some(active_epoch), Some(next_epoch)) => {
if active_epoch.can_update_to(next_epoch) {
Ok(())
} else {
Err(UpdateError::RollbackProtection)
}
}
}
}

/// Get ImageHeader epoch and/or caboose EPOC from an Image if it exists.
/// Return default of zero epoch if neither is present.
/// This function is called at points where the image's signature has not been
/// checked or the image is incomplete. Sanity checks are required before using
/// any data.
fn get_image_epoch(
image: &ImageAccess<'_>,
) -> Result<Option<Epoch>, UpdateError> {
let (header_epoch, _caboose_offset) = validate_header_block(image)?;

if let Ok(span) = caboose_slice(image) {
let mut block = [0u8; BLOCK_SIZE_BYTES];
let caboose = block[0..span.len()].as_bytes_mut();
image.read_bytes(span.start, caboose)?;
let reader = CabooseReader::new(caboose);
let caboose_epoch = if let Ok(epoc) = reader.get(CABOOSE_TAG_EPOC) {
Some(Epoch::from(epoc))
} else {
None
};
match (header_epoch, caboose_epoch) {
(None, None) => Ok(None),
(Some(header_epoch), None) => Ok(Some(header_epoch)),
(None, Some(caboose_epoch)) => Ok(Some(caboose_epoch)),
(Some(header_epoch), Some(caboose_epoch)) => {
if caboose_epoch == header_epoch {
Ok(Some(caboose_epoch))
} else {
// Epochs present in both and not matching is invalid.
// The image will be rejected after epoch 0.
Ok(Some(Epoch::from(0u32)))
}
}
}
} else {
Ok(header_epoch)
}
}
Loading

0 comments on commit a50ba38

Please sign in to comment.