From d38e22464c3991ffad8754020ad0dd533b7ccdde Mon Sep 17 00:00:00 2001 From: Ben Stoltz Date: Wed, 1 May 2024 12:09:50 -0700 Subject: [PATCH] Add an "EPOC" tag to the caboose Bump hubtools version for EPOC tag in caboose Default epoch value is zero if not otherwise specified. Command line --epoch option. Work with the epoch value as a u32. Do not use ImageHeader epoch field. --- Cargo.lock | 2 +- hubedit/src/main.rs | 14 +++-- hubtools/Cargo.toml | 2 +- hubtools/src/bootleby.rs | 11 ++-- hubtools/src/caboose.rs | 61 +++++++++++++++++++++ hubtools/src/lib.rs | 113 +++++++++++++++++++++++++++++---------- 6 files changed, 165 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3153618..1b0e020 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -428,7 +428,7 @@ dependencies = [ [[package]] name = "hubtools" -version = "0.4.6" +version = "0.4.7" dependencies = [ "hex", "lpc55_areas", diff --git a/hubedit/src/main.rs b/hubedit/src/main.rs index 6b56695..d01f90b 100644 --- a/hubedit/src/main.rs +++ b/hubedit/src/main.rs @@ -33,6 +33,9 @@ pub enum Command { #[clap(short, long)] version: String, + #[clap(short, long)] + epoch: Option, + #[clap(short, long)] force: bool, @@ -93,6 +96,8 @@ pub enum Command { board: String, /// Git has for the hubris archive gitc: String, + /// Epoch for rollback protection + epoch: Option, }, } @@ -129,6 +134,7 @@ fn main() -> Result<()> { } Command::WriteCaboose { version, + epoch, force, no_defaults, } => { @@ -143,9 +149,9 @@ fn main() -> Result<()> { } } if no_defaults { - archive.write_version_to_caboose(&version)?; + archive.write_version_to_caboose(&version, epoch)?; } else { - archive.write_default_caboose(Some(&version))?; + archive.write_default_caboose(Some(&version), epoch)?; } archive.overwrite()?; } @@ -202,8 +208,10 @@ fn main() -> Result<()> { board, name, gitc, + epoch, } => { - let archive = bootleby_to_archive(elf_file, board, name, gitc)?; + let archive = + bootleby_to_archive(elf_file, board, name, gitc, epoch)?; std::fs::write(&args.archive, archive)?; diff --git a/hubtools/Cargo.toml b/hubtools/Cargo.toml index dc756cc..3ce77c3 100644 --- a/hubtools/Cargo.toml +++ b/hubtools/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hubtools" -version = "0.4.6" +version = "0.4.7" edition = "2021" rust-version = "1.66" diff --git a/hubtools/src/bootleby.rs b/hubtools/src/bootleby.rs index 8234357..9f8fdcc 100644 --- a/hubtools/src/bootleby.rs +++ b/hubtools/src/bootleby.rs @@ -72,10 +72,7 @@ fn add_image_header(path: PathBuf) -> Result, Error> { let offset = header_offset(&elf)?; drop(elf); - let header = header::ImageHeader { - magic: 0x64_CE_D6_CA, - total_image_len: len as u32, - }; + let header = header::ImageHeader::new(len as usize); header .write_to_prefix(&mut f[(offset as usize)..]) @@ -89,6 +86,7 @@ pub fn bootleby_to_archive( board: String, name: String, gitc: String, + epoc: Option, ) -> Result, Error> { let f = add_image_header(path)?; @@ -103,8 +101,11 @@ pub fn bootleby_to_archive( name = "{}" board = "{}" chip = "lpc55" + epoch = {} "#, - name, board + name, + board, + epoc.unwrap_or(0u32) ); archive.add_file("elf/kernel", &f)?; diff --git a/hubtools/src/caboose.rs b/hubtools/src/caboose.rs index 16a6513..c7a9582 100644 --- a/hubtools/src/caboose.rs +++ b/hubtools/src/caboose.rs @@ -48,6 +48,22 @@ impl Caboose { self.get_tag(tags::SIGN) } + pub fn epoch(&self) -> Result<&[u8], CabooseError> { + self.get_tag(tags::EPOC) + } + + /// Interpret the `EPOC` value as a u32 if present and well formed. + pub fn epoch_u32(&self) -> Option { + if let Ok(epoc) = self.epoch() { + if let Ok(epoc_str) = std::str::from_utf8(epoc) { + if let Ok(number) = epoc_str.parse::() { + return Some(number); + } + } + } + None + } + fn get_tag(&self, tag: [u8; 4]) -> Result<&[u8], CabooseError> { use tlvc::TlvcReader; let mut reader = TlvcReader::begin(self.as_slice()) @@ -85,6 +101,7 @@ pub(crate) mod tags { pub(crate) const NAME: [u8; 4] = *b"NAME"; pub(crate) const VERS: [u8; 4] = *b"VERS"; pub(crate) const SIGN: [u8; 4] = *b"SIGN"; + pub(crate) const EPOC: [u8; 4] = *b"EPOC"; } #[derive(Debug, Default, Clone, PartialEq, Eq)] @@ -94,6 +111,7 @@ pub struct CabooseBuilder { name: Option, version: Option, sign: Option, + epoch: Option, } impl CabooseBuilder { @@ -122,6 +140,11 @@ impl CabooseBuilder { self } + pub fn epoch(mut self, epoch: u32) -> Self { + self.epoch = Some(epoch); + self + } + pub fn build(self) -> Caboose { let mut pieces = Vec::new(); for (tag, maybe_value) in [ @@ -130,6 +153,7 @@ impl CabooseBuilder { (tags::NAME, self.name), (tags::VERS, self.version), (tags::SIGN, self.sign), + (tags::EPOC, self.epoch.map(|e| e.to_string())), ] { let Some(value) = maybe_value else { continue; @@ -166,6 +190,10 @@ mod tests { caboose.version(), Err(CabooseError::MissingTag { tag: tags::VERS }) ); + assert_eq!( + caboose.epoch(), + Err(CabooseError::MissingTag { tag: tags::EPOC }) + ); } #[test] @@ -184,6 +212,10 @@ mod tests { caboose.version(), Err(CabooseError::MissingTag { tag: tags::VERS }) ); + assert_eq!( + caboose.epoch(), + Err(CabooseError::MissingTag { tag: tags::EPOC }) + ); } #[test] @@ -193,10 +225,39 @@ mod tests { .board("bar") .name("fizz") .version("buzz") + .epoch(0) .build(); assert_eq!(caboose.git_commit(), Ok("foo".as_bytes())); assert_eq!(caboose.board(), Ok("bar".as_bytes())); assert_eq!(caboose.name(), Ok("fizz".as_bytes())); assert_eq!(caboose.version(), Ok("buzz".as_bytes())); + assert_eq!(caboose.epoch(), Ok("0".as_bytes())); + } + + #[test] + fn builder_can_make_caboose_with_zero_epoch() { + let caboose = CabooseBuilder::default().epoch(0).build(); + assert_eq!(caboose.epoch(), Ok("0".as_bytes())); + } + + #[test] + fn builder_can_make_caboose_with_non_zero_epoch() { + let caboose = CabooseBuilder::default().epoch(1234567890).build(); + assert_eq!(caboose.epoch(), Ok("1234567890".as_bytes())); + } + + #[test] + fn builder_missing_tag_epoc() { + let caboose = CabooseBuilder::default().build(); + assert_eq!( + caboose.epoch(), + Err(CabooseError::MissingTag { tag: tags::EPOC }) + ); + } + + #[test] + fn builder_will_normalize_short_epoch() { + let caboose = CabooseBuilder::default().epoch(1234).build(); + assert_eq!(caboose.epoch(), Ok("1234".as_bytes())); } } diff --git a/hubtools/src/lib.rs b/hubtools/src/lib.rs index 612a239..487afdd 100644 --- a/hubtools/src/lib.rs +++ b/hubtools/src/lib.rs @@ -277,25 +277,9 @@ impl RawHubrisImage { } fn caboose_range(&self) -> Result, Error> { - let mut found_header = None; + let image_size = self.image_size()?; let start_addr = self.start_addr; - for header_offset in header::POSSIBLE_OFFSETS { - let mut header_magic = 0u32; - self.read(start_addr + header_offset, &mut header_magic)?; - if header::MAGIC.contains(&header_magic) { - found_header = Some(header_offset); - break; - } - } - - let Some(header_offset) = found_header else { - return Err(Error::MissingMagic(header::MAGIC)); - }; - - let mut image_size = 0u32; - self.read(start_addr + header_offset + 4, &mut image_size)?; - let mut caboose_size = 0u32; self.read(start_addr + image_size - 4, &mut caboose_size)?; @@ -326,7 +310,7 @@ impl RawHubrisImage { } } - /// Attempts to read `out.len()` bytes, starting at `start` + /// Read `out.len()` bytes, starting at `start` fn read( &self, start: u32, @@ -338,7 +322,7 @@ impl RawHubrisImage { Ok(()) } - /// Attempts to read `out.len()` bytes, starting at `start` + /// Write `input.len()` bytes, starting at `start` fn write( &mut self, start: u32, @@ -349,6 +333,38 @@ impl RawHubrisImage { .copy_from_slice(input.as_bytes()); Ok(()) } + + // TODO: Knowlege about image formats needs to be + // consolidated into its own crate usable in std and no_std + // contexts. + + fn locate_header(&self) -> Result { + for header_offset in header::POSSIBLE_OFFSETS { + let mut header_magic = 0u32; + self.read(self.start_addr + header_offset, &mut header_magic)?; + if header::MAGIC.contains(&header_magic) { + return Ok(header_offset); + } + } + Err(Error::MissingMagic(header::MAGIC)) + } + + fn read_image_header(&self) -> Result { + let header_offset = self.locate_header()?; + let mut header = header::ImageHeader::new_zeroed(); + // TODO: We assume that the host that we're running on matches + // the little-endianness of the target device. That happens to + // be true in all known cases, but is not good practice and + // should be fixed. + self.read(self.start_addr + header_offset, &mut header)?; + Ok(header) + } + + /// Return the image size up to but not including any signature block. + fn image_size(&self) -> Result { + let header = self.read_image_header()?; + Ok(header.total_image_len) + } } const CABOOSE_MAGIC: u32 = 0xcab0005e; @@ -471,6 +487,9 @@ pub enum Error { #[error("packing error: {0}")] PackingError(String), + + #[error("invalid epoch value: {0}")] + InvalidEpoch(String), } //////////////////////////////////////////////////////////////////////////////// @@ -614,17 +633,25 @@ impl RawHubrisArchive { self.image.write_caboose(data) } - /// Writes the given version (and nothing else) to the caboose + /// Writes the given version (and optional epoch) to the caboose pub fn write_version_to_caboose( &mut self, version: &str, + epoch: Option, ) -> Result<(), Error> { // Manually build the TLV-C data for the caboose - let data = tlvc_text::Piece::Chunk( + let mut chunks = vec![tlvc_text::Piece::Chunk( tlvc_text::Tag::new(caboose::tags::VERS), vec![tlvc_text::Piece::String(version.to_owned())], - ); - let out = tlvc_text::pack(&[data]); + )]; + if let Some(epoc) = epoch { + let data = tlvc_text::Piece::Chunk( + tlvc_text::Tag::new(caboose::tags::EPOC), + vec![tlvc_text::Piece::String(epoc.to_string().to_owned())], + ); + chunks.push(data) + } + let out = tlvc_text::pack(&chunks); self.write_caboose(&out)?; Ok(()) } @@ -636,11 +663,13 @@ impl RawHubrisArchive { /// - `NAME`: image name /// - `BORD`: board name /// - `VERS`: the provided version string (if present) + /// - 'EPOC': epoch value for rollback protection during update /// /// Everything except `VERS` are extracted from the Hubris archive itself. fn generate_default_caboose( &mut self, version: Option<&String>, + epoch: Option, ) -> Result, Error> { let manifest = self.extract_file("app.toml")?; let git = self.extract_file("git-rev")?; @@ -667,8 +696,10 @@ impl RawHubrisArchive { .ok_or(Error::BadTomlType)? .to_owned(); + // Epoch defaults to zero + // If this Hubris archive used our TOML inheritance system, then the - // name could be overridded in the `patches.toml` file. + // name could be overridden in the `patches.toml` file. if let Ok(patches) = self.extract_file("patches.toml") { let patches: toml::Value = toml::from_str( std::str::from_utf8(&patches).map_err(Error::BadManifest)?, @@ -697,6 +728,13 @@ impl RawHubrisArchive { tlvc_text::Tag::new(caboose::tags::NAME), vec![tlvc_text::Piece::String(name)], ), + tlvc_text::Piece::Chunk( + tlvc_text::Tag::new(caboose::tags::EPOC), + // EPOC defaults to "0" + vec![tlvc_text::Piece::String( + epoch.unwrap_or(0u32).to_string(), + )], + ), ]; if let Some(v) = version { let data = tlvc_text::Piece::Chunk( @@ -711,8 +749,10 @@ impl RawHubrisArchive { pub fn write_default_caboose( &mut self, version: Option<&String>, + epoch: Option, ) -> Result<(), Error> { - let out = tlvc_text::pack(&self.generate_default_caboose(version)?); + let out = + tlvc_text::pack(&self.generate_default_caboose(version, epoch)?); self.write_caboose(&out) } @@ -858,6 +898,7 @@ impl RawHubrisArchive { root_certs: Vec, signing_certs: Vec, version: Option<&String>, + epoch: Option, ) -> Result, Error> { match self.is_lpc55() { Ok(_) => { @@ -893,7 +934,8 @@ impl RawHubrisArchive { )?; } - let mut caboose = self.generate_default_caboose(version)?; + let mut caboose = + self.generate_default_caboose(version, epoch)?; caboose.push(tlvc_text::Piece::Chunk( tlvc_text::Tag::new(caboose::tags::SIGN), @@ -912,7 +954,7 @@ impl RawHubrisArchive { Ok(stamped) } Err(_) => { - self.write_default_caboose(version)?; + self.write_default_caboose(version, epoch)?; // Not an LPC55 image, just return the image back for now self.image.to_binary() } @@ -978,11 +1020,26 @@ mod header { // new-style magic numbers, which use the same header layout. pub(crate) const MAGIC: [u32; 2] = [0x15356637, 0x64_CE_D6_CA]; - #[derive(Default, AsBytes, FromBytes)] + #[derive(AsBytes, FromBytes)] #[repr(C)] pub(crate) struct ImageHeader { pub magic: u32, pub total_image_len: u32, + pub _pad: [u32; 16], + pub _version: u32, + pub _epoch: u32, + } + + impl ImageHeader { + pub fn new(total_len: usize) -> Self { + ImageHeader { + magic: MAGIC[1], + total_image_len: total_len as u32, + _pad: [0; 16], + _version: 0, + _epoch: 0, + } + } } // The header is located in one of a few locations, depending on MCU