Attempt a basic PC range check as part of hubris.validate#711
Conversation
`validate` on a hubris archive will verify the `image_id` present in an archive matches what's in flash. This has an unfortunate failure mode for the RoT where we partition the flash manually. It's possible to have the image ID check succeed on the archive but actually be running the opposite image. This causes all kinds of confusion. Add a basic spot check that the PC is in at least one module range. This could cause potential failures if the PC is actually running in a range that does not correspond to a module. Based on other work, the chances of this happening are very limited and should be transient.
|
| if let Ok(pc) = core.read_reg(ARMRegister::PC) { | ||
| if self.instr_mod(pc).is_none() { | ||
| bail!("PC at 0x{pc:x} is not part of any module. This is \ | ||
| likely an incorrect archive."); | ||
| } | ||
| } |
There was a problem hiding this comment.
i believe clippy would like you to have written something more like:
| if let Ok(pc) = core.read_reg(ARMRegister::PC) { | |
| if self.instr_mod(pc).is_none() { | |
| bail!("PC at 0x{pc:x} is not part of any module. This is \ | |
| likely an incorrect archive."); | |
| } | |
| } | |
| if let Ok(pc) = core.read_reg(ARMRegister::PC) | |
| && self.instr_mod(pc).is_none() { | |
| bail!("PC at 0x{pc:x} is not part of any module. \ | |
| This is likely an incorrect archive."); | |
| } |
| core.halt()?; | ||
| if let Ok(pc) = core.read_reg(ARMRegister::PC) { | ||
| if self.instr_mod(pc).is_none() { | ||
| bail!("PC at 0x{pc:x} is not part of any module. This is \ |
There was a problem hiding this comment.
unimportant turbo nitpick: if we write
| bail!("PC at 0x{pc:x} is not part of any module. This is \ | |
| bail!("PC at {pc:#x} is not part of any module. This is \ |
that tells the formatter to emit the 0x prefix itself. which...changes absolutely nothing here so it doesn't matter...
| core.halt()?; | ||
| if let Ok(pc) = core.read_reg(ARMRegister::PC) { | ||
| if self.instr_mod(pc).is_none() { | ||
| bail!("PC at 0x{pc:x} is not part of any module. This is \ |
There was a problem hiding this comment.
I think the way this interacts with flashing is not ideal. If you try to re-flash an A image that's already been flashed to the A slot while the RoT is running a B image:
- the old behavior was to fail with "archive appears to be already flashed on attached device; use -F ("--force") to force re-flash"
- this PR's behavior is to re-flash the A image
- the ideal behavior is probably to warn the user that they probably meant to flash the other image, then fail with the "archive appears to be already flashed" error.
There was a problem hiding this comment.
yeah you're right that's confusing behavior. I think that would point to needing to fix validate to return a more specific error
There was a problem hiding this comment.
Or don't use validate for the pre-flash check. In my PR, I considered a bunch of different return types then gave up trying to make the same validate function work for flashing and for both of the HubrisValidate options.
| if self.task_dump().is_some() { | ||
| return Ok(()); | ||
| } else { | ||
| core.halt()?; |
There was a problem hiding this comment.
Should we do core.run()? again after checking the PC?
There was a problem hiding this comment.
Absolutely yes this got lost in some refactoring 😬
labbott
left a comment
There was a problem hiding this comment.
Thanks for doing this
| pub fn is_pc_within_archive( | ||
| &self, | ||
| core: &mut dyn crate::core::Core | ||
| ) -> Result<(bool, u32)> { |
There was a problem hiding this comment.
I don't quite follow this return type. Can you explain it with a comment?
There was a problem hiding this comment.
Done. Yeah it's kinda weird. I wanted a helper function for doing the check, but also didn't want to have to remove the PC value from your error message. It will be useful for debugging when this error gets triggered by some other strange situation someday.
There was a problem hiding this comment.
No need to change it now but it's also worth considering something like
enum PcResult {
InArchive,
NotInArchive { pc: u32 },
}
There was a problem hiding this comment.
I do like that, changed.
| Err(e) => { | ||
| return Ok(ImageStateResult::DoesNotMatch( | ||
| ImageStateMismatch::FlashArchiveMismatch(e), | ||
| )); | ||
| } | ||
| Ok(id) if id != hubris.image_id() => { | ||
| return Ok(ImageStateResult::DoesNotMatch( | ||
| ImageStateMismatch::FlashArchiveMismatch(anyhow!( | ||
| "image ID in archive ({:x?}) does not equal \ | ||
| image ID of target ({:x?})", | ||
| hubris.image_id(), | ||
| id, | ||
| )), | ||
| )); | ||
| } |
There was a problem hiding this comment.
I think these should be two separate error types: one to represent an error reading the image ID and a separate one to indicate an actual mismatch. This also means we can avoid having to create an extra error message with anyhow and have all the information in the ImageStateMismatch enum.
There was a problem hiding this comment.
Done. That's a nice side effect of not calling validate anymore.
|
Test this corner case: corrupt/erase both Hubris images, reset the RoT. Use humility to dump the registers. It should be in a bootleby loop. (check bootleby source). |
|
|
||
| if criteria == HubrisValidate::ArchiveMatch { | ||
| if self.task_dump().is_some() { | ||
| return Ok(()); |
There was a problem hiding this comment.
@labbott What does it mean for an archive to be "a dump from a single task"? I'm wondering if it's intentional/correct that if you pass in HubrisValidate::Booted in that case, it returns early here and skips the boot check.
There was a problem hiding this comment.
I think it's possible to take a dump from only a single task's memory range (e.g. a "single task dump"), but not an "entire system" dump. Laura can confirm though :)
There was a problem hiding this comment.
James is correct. We have the notion of single task dumps vs a wholes system dump. You can run many of the same commands like e.g. humility tasks and humility ringbuf and things should work as expected. I don't think we capture the boot information in a single task dump which is why it gets skipped.
There was a problem hiding this comment.
Thanks, I added a comment about it.
I corrupted both the A and B images so that it would stay in the bootloader. It's looking like we won't need a |
There was a problem hiding this comment.
I split up the test-case fixes into 3 commits. The first is straightforward changes like this file, where just the error message changed. But the other two are tests where the command used to succeed (or appear to succeed?) but now fails the PC check. Does anyone disagree with any of these changes and think that we should allow someone to, for example, run humility readvar while in the bootloader? If so, that might be a reason to add a --skip-pc-check flag.
53952e2 to
e6726f4
Compare
validateon a hubris archive will verify theimage_idpresent in an archive matches what's in flash. This has an unfortunate failure mode for the RoT where we partition the flash manually. It's possible to have the image ID check succeed on the archive but actually be running the opposite image. This causes all kinds of confusion. Add a basic spot check that the PC is in at least one module range. This could cause potential failures if the PC is actually running in a range that does not correspond to a module. Based on other work, the chances of this happening are very limited and should be transient.