Skip to content

Commit 62802ff

Browse files
committed
Implement epoch-based rollback protection in LPC55 update_server
1 parent 21eb8ba commit 62802ff

File tree

10 files changed

+192
-31
lines changed

10 files changed

+192
-31
lines changed

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,20 @@ zip = { version = "0.6", default-features = false, features = ["bzip2"] }
120120
# Oxide forks and repos
121121
attest-data = { git = "https://github.com/oxidecomputer/dice-util", default-features = false, version = "0.2.0" }
122122
dice-mfg-msgs = { git = "https://github.com/oxidecomputer/dice-util", default-features = false, version = "0.2.1" }
123-
gateway-messages = { git = "https://github.com/oxidecomputer/management-gateway-service", default-features = false, features = ["smoltcp"] }
123+
#gateway-messages = { git = "https://github.com/oxidecomputer/management-gateway-service", default-features = false, features = ["smoltcp"] }
124+
# XXX fix before push
125+
gateway-messages = { path = "/home/stoltz/Oxide/src/mgs/epoch/gateway-messages", default-features = false, features = ["smoltcp"] }
124126
gimlet-inspector-protocol = { git = "https://github.com/oxidecomputer/gimlet-inspector-protocol", version = "0.1.0" }
125127
hif = { git = "https://github.com/oxidecomputer/hif", default-features = false }
126128
humpty = { git = "https://github.com/oxidecomputer/humpty", default-features = false, version = "0.1.3" }
127-
hubtools = { git = "https://github.com/oxidecomputer/hubtools", default-features = false, version = "0.4.1" }
129+
#hubtools = { git = "https://github.com/oxidecomputer/hubtools", default-features = false, version = "0.4.1" }
130+
# XXX fix before push
131+
# hubtools = { git = "https://github.com/oxidecomputer/hubtools", default-features = false, branch = "epoch", version = "0.4.7" }
132+
hubtools = { path = "/home/stoltz/Oxide/src/hubtools/epoch/hubtools" }
128133
idol = { git = "https://github.com/oxidecomputer/idolatry.git", default-features = false }
129134
idol-runtime = { git = "https://github.com/oxidecomputer/idolatry.git", default-features = false }
130-
lpc55_sign = { git = "https://github.com/oxidecomputer/lpc55_support", default-features = false }
135+
#lpc55_sign = { git = "https://github.com/oxidecomputer/lpc55_support", default-features = false }
136+
lpc55_sign = { path = "/home/stoltz/Oxide/src/lpc55_support/lpc55_sign", default-features = false }
131137
ordered-toml = { git = "https://github.com/oxidecomputer/ordered-toml", default-features = false }
132138
pmbus = { git = "https://github.com/oxidecomputer/pmbus", default-features = false }
133139
salty = { version = "0.3", default-features = false }

app/gimlet/base.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ chip = "../../chips/stm32h7"
33
memory = "memory-large.toml"
44
stacksize = 896
55
fwid = true
6+
epoch = 0
67

78
[kernel]
89
name = "gimlet"

app/lpc55xpresso/app.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ start = true
4848
[tasks.update_server]
4949
name = "lpc55-update-server"
5050
priority = 3
51-
max-sizes = {flash = 27008, ram = 16704}
51+
max-sizes = {flash = 30368, ram = 16704}
5252
stacksize = 8192
5353
start = true
5454
sections = {bootstate = "usbsram"}

app/oxide-rot-1/app-dev.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ start = true
4949
[tasks.update_server]
5050
name = "lpc55-update-server"
5151
priority = 3
52-
max-sizes = {flash = 27904, ram = 17344, usbsram = 4096}
52+
max-sizes = {flash = 30368, ram = 17344, usbsram = 4096}
5353
# TODO: Size this appropriately
5454
stacksize = 8192
5555
start = true

build/xtask/src/dist.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,7 @@ pub fn package(
611611
// The Git hash is included in the default caboose under the key
612612
// `GITC`, so we don't include it in the pseudo-version.
613613
archive
614-
.write_default_caboose(None)
614+
.write_default_caboose(None, None)
615615
.context("writing caboose into archive")?;
616616
archive.overwrite().context("overwriting archive")?;
617617
}
@@ -621,8 +621,13 @@ pub fn package(
621621
if let Some(signing) = &cfg.toml.signing {
622622
let mut archive = hubtools::RawHubrisArchive::load(&archive_name)
623623
.context("loading archive with hubtools")?;
624+
let priv_key_rel_path = signing
625+
.certs
626+
.private_key
627+
.clone()
628+
.context("missing private key path")?;
624629
let private_key = lpc55_sign::cert::read_rsa_private_key(
625-
&cfg.app_src_dir.join(&signing.certs.private_key),
630+
&cfg.app_src_dir.join(priv_key_rel_path),
626631
)
627632
.with_context(|| {
628633
format!(

drv/lpc55-update-api/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,16 @@ impl From<RotSlot> for SlotId {
242242
}
243243
}
244244

245+
impl SlotId {
246+
pub fn other(&self) -> SlotId {
247+
if *self == SlotId::A {
248+
SlotId::B
249+
} else {
250+
SlotId::A
251+
}
252+
}
253+
}
254+
245255
impl TryFrom<u16> for SlotId {
246256
type Error = ();
247257
fn try_from(i: u16) -> Result<Self, Self::Error> {

drv/lpc55-update-server/src/images.rs

Lines changed: 140 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ use crate::{
2020
use abi::{ImageHeader, CABOOSE_MAGIC, HEADER_MAGIC};
2121
use core::ops::Range;
2222
use core::ptr::addr_of;
23-
use drv_lpc55_update_api::{RawCabooseError, RotComponent, SlotId};
23+
use drv_caboose::CabooseReader;
24+
use drv_lpc55_update_api::{
25+
RawCabooseError, RotComponent, SlotId, BLOCK_SIZE_BYTES,
26+
};
2427
use drv_update_api::UpdateError;
2528
use zerocopy::{AsBytes, FromBytes};
2629

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

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

@@ -208,15 +212,17 @@ pub fn validate_header_block(
208212
// Note that `ImageHeader.epoch` is used by rollback protection for early
209213
// rejection of invalid images.
210214
// TODO: Improve estimate of where the first executable instruction can be.
211-
let code_offset = if header.magic == HEADER_MAGIC {
215+
let (code_offset, epoch) = if header.magic == HEADER_MAGIC {
212216
if header.total_image_len != vectors.nxp_offset_to_specific_header {
213217
// ImageHeader disagrees with LPC55 vectors.
214218
return Err(UpdateError::InvalidHeaderBlock);
215219
}
216-
// Adding constants should be resolved at compile time: no call to panic.
217-
IMAGE_HEADER_OFFSET + (core::mem::size_of::<ImageHeader>() as u32)
220+
(
221+
IMAGE_HEADER_OFFSET + (core::mem::size_of::<ImageHeader>() as u32),
222+
Some(Epoch::from(header.epoch)),
223+
)
218224
} else {
219-
IMAGE_HEADER_OFFSET
225+
(IMAGE_HEADER_OFFSET, None)
220226
};
221227

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

246-
Ok(vectors.nxp_offset_to_specific_header)
252+
Ok((epoch, vectors.nxp_offset_to_specific_header))
247253
}
248254

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

266272
// By construction, the last word of the caboose is its size as a `u32`
@@ -318,7 +324,7 @@ enum Accessor<'a> {
318324
},
319325
// Hybrid is used for later implementation of rollback protection.
320326
// The buffer is used in place of the beginning of the flash range.
321-
_Hybrid {
327+
Hybrid {
322328
buffer: &'a [u8],
323329
flash: &'a drv_lpc55_flash::Flash<'a>,
324330
span: FlashRange,
@@ -330,7 +336,7 @@ impl Accessor<'_> {
330336
match self {
331337
Accessor::Flash { span, .. }
332338
| Accessor::Ram { span, .. }
333-
| Accessor::_Hybrid { span, .. } => &span.at_runtime,
339+
| Accessor::Hybrid { span, .. } => &span.at_runtime,
334340
}
335341
}
336342
}
@@ -375,15 +381,15 @@ impl ImageAccess<'_> {
375381
}
376382
}
377383

378-
pub fn _new_hybrid<'a>(
384+
pub fn new_hybrid<'a>(
379385
flash: &'a drv_lpc55_flash::Flash<'a>,
380386
buffer: &'a [u8],
381387
component: RotComponent,
382388
slot: SlotId,
383389
) -> ImageAccess<'a> {
384390
let span = flash_range(component, slot);
385391
ImageAccess {
386-
accessor: Accessor::_Hybrid {
392+
accessor: Accessor::Hybrid {
387393
flash,
388394
buffer,
389395
span,
@@ -430,7 +436,7 @@ impl ImageAccess<'_> {
430436
.and_then(u32::read_from)
431437
.ok_or(UpdateError::OutOfBounds)?)
432438
}
433-
Accessor::_Hybrid {
439+
Accessor::Hybrid {
434440
buffer,
435441
flash,
436442
span,
@@ -491,7 +497,7 @@ impl ImageAccess<'_> {
491497
Err(UpdateError::OutOfBounds)
492498
}
493499
}
494-
Accessor::_Hybrid {
500+
Accessor::Hybrid {
495501
buffer: ram,
496502
flash,
497503
span,
@@ -547,7 +553,7 @@ impl ImageAccess<'_> {
547553
ImageVectorsLpc55::read_from_prefix(&buffer[..])
548554
.ok_or(UpdateError::OutOfBounds)
549555
}
550-
Accessor::Ram { buffer, .. } | Accessor::_Hybrid { buffer, .. } => {
556+
Accessor::Ram { buffer, .. } | Accessor::Hybrid { buffer, .. } => {
551557
ImageVectorsLpc55::read_from_prefix(buffer)
552558
.ok_or(UpdateError::OutOfBounds)
553559
}
@@ -556,3 +562,122 @@ impl ImageAccess<'_> {
556562
round_up_to_flash_page(len).ok_or(UpdateError::BadLength)
557563
}
558564
}
565+
566+
#[derive(Clone, PartialEq)]
567+
pub struct Epoch {
568+
value: u32,
569+
}
570+
571+
/// Convert from the ImageHeader.epoch format
572+
impl From<u32> for Epoch {
573+
fn from(number: u32) -> Self {
574+
Epoch { value: number }
575+
}
576+
}
577+
578+
/// Convert from the caboose EPOC value format.
579+
//
580+
// Invalid EPOC values converted to Epoch{value:0} include:
581+
// - empty slice
582+
// - any non-ASCII-digits in slice
583+
// - any non-UTF8 in slice
584+
// - leading '+' normally allowed by parse()
585+
// - values greater than u32::MAX
586+
//
587+
// Hand coding reduces size by about 950 bytes.
588+
impl From<&[u8]> for Epoch {
589+
fn from(chars: &[u8]) -> Self {
590+
let epoch =
591+
if chars.first().map(|c| c.is_ascii_digit()).unwrap_or(false) {
592+
if let Ok(chars) = core::str::from_utf8(chars) {
593+
chars.parse::<u32>().unwrap_or(0)
594+
} else {
595+
0
596+
}
597+
} else {
598+
0
599+
};
600+
Epoch::from(epoch)
601+
}
602+
}
603+
604+
impl Epoch {
605+
pub fn can_update_to(&self, next: Epoch) -> bool {
606+
self.value >= next.value
607+
}
608+
}
609+
610+
/// Check a next image against an active image to determine
611+
/// if rollback policy allows the next image.
612+
/// If ImageHeader and Caboose are absent or Caboose does
613+
/// not have an `EPOC` tag, then the Epoch is defaulted to zero.
614+
/// This test is also used when the header block first arrives
615+
/// so that images can be rejected early, i.e. before any flash has
616+
/// been altered.
617+
pub fn check_rollback_policy(
618+
next_image: ImageAccess<'_>,
619+
active_image: ImageAccess<'_>,
620+
complete: bool,
621+
) -> Result<(), UpdateError> {
622+
let next_epoch = get_image_epoch(&next_image)?;
623+
let active_epoch = get_image_epoch(&active_image)?;
624+
match (active_epoch, next_epoch) {
625+
// No active_epoch is treated as zero; update can proceed.
626+
(None, _) => Ok(()),
627+
(Some(active_epoch), None) => {
628+
// If next_image is partial and HEADER_BLOCK has no ImageHeader,
629+
// then there is no early rejection, proceed.
630+
if !complete || active_epoch.can_update_to(Epoch::from(0u32)) {
631+
Ok(())
632+
} else {
633+
Err(UpdateError::RollbackProtection)
634+
}
635+
}
636+
(Some(active_epoch), Some(next_epoch)) => {
637+
if active_epoch.can_update_to(next_epoch) {
638+
Ok(())
639+
} else {
640+
Err(UpdateError::RollbackProtection)
641+
}
642+
}
643+
}
644+
}
645+
646+
/// Get ImageHeader epoch and/or caboose EPOC from an Image if it exists.
647+
/// Return default of zero epoch if neither is present.
648+
/// This function is called at points where the image's signature has not been
649+
/// checked or the image is incomplete. Sanity checks are required before using
650+
/// any data.
651+
fn get_image_epoch(
652+
image: &ImageAccess<'_>,
653+
) -> Result<Option<Epoch>, UpdateError> {
654+
let (header_epoch, _caboose_offset) = validate_header_block(image)?;
655+
656+
if let Ok(span) = caboose_slice(image) {
657+
let mut block = [0u8; BLOCK_SIZE_BYTES];
658+
let caboose = block[0..span.len()].as_bytes_mut();
659+
image.read_bytes(span.start, caboose)?;
660+
let reader = CabooseReader::new(caboose);
661+
let caboose_epoch = if let Ok(epoc) = reader.get(CABOOSE_TAG_EPOC) {
662+
Some(Epoch::from(epoc))
663+
} else {
664+
None
665+
};
666+
match (header_epoch, caboose_epoch) {
667+
(None, None) => Ok(None),
668+
(Some(header_epoch), None) => Ok(Some(header_epoch)),
669+
(None, Some(caboose_epoch)) => Ok(Some(caboose_epoch)),
670+
(Some(header_epoch), Some(caboose_epoch)) => {
671+
if caboose_epoch == header_epoch {
672+
Ok(Some(caboose_epoch))
673+
} else {
674+
// Epochs present in both and not matching is invalid.
675+
// The image will be rejected after epoch 0.
676+
Ok(Some(Epoch::from(0u32)))
677+
}
678+
}
679+
}
680+
} else {
681+
Ok(header_epoch)
682+
}
683+
}

0 commit comments

Comments
 (0)