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.
There was a problem hiding this comment.
I'm inclined to say that the change is fine and that it's okay to give failures if we're in the bootloader. For the SP in particular we should never expect to end up in the bootloader so any attempt to read things from hubris RAM is likely to be questionable.
The RoT may be a more interesting case because we do expect to briefly go through a non-hubris bootloader but that code is designed to be so minimal I think rejecting it is okay. Really I think if future us actually finds a situation where something like --skip-pc-check is valuable/solves a real problem I think we can add it then.
53952e2 to
e6726f4
Compare
labbott
left a comment
There was a problem hiding this comment.
LGTM. This is actually originally my PR so I can't actually approve it!
| if let Ok(PcResult::NotInArchive { pc }) = hubris.is_pc_within_archive(core) | ||
| { | ||
| warn!( | ||
| log, | ||
| "PC at 0x{pc:x} is not part of any module. Maybe you're \ | ||
| flashing an A image while a B image is running, or vice \ | ||
| versa?" | ||
| ); | ||
| } |
There was a problem hiding this comment.
we drop the Err case here. That should only happen if we can't read the PC and we're probably going to have other errors that would get caught below so I think it's fine.
There was a problem hiding this comment.
Yeah my thought was for it to just log nothing unless it's sure the PC location is bad. Would you prefer to have a "failed to check PC location" warning too?
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.