-
Notifications
You must be signed in to change notification settings - Fork 35
Avoid appending after torn item headers #137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| #![no_main] | ||
|
|
||
| use futures::executor::block_on; | ||
| use libfuzzer_sys::fuzz_target; | ||
| use sequential_storage::{ | ||
| cache::Cache, | ||
| map::{MapConfig, MapStorage}, | ||
| mock_flash::{MockFlashBase, MockFlashError, WriteCountCheck}, | ||
| Error, | ||
| }; | ||
|
|
||
| const ERASE_SIZE: usize = 128; | ||
| const CAPACITY: usize = ERASE_SIZE * 2; | ||
| const VALUE_SIZE: usize = 8; | ||
|
|
||
| type Flash = MockFlashBase<2, 1, ERASE_SIZE>; | ||
|
|
||
| fn config() -> MapConfig<Flash> { | ||
| const { MapConfig::new(0..CAPACITY as u32) } | ||
| } | ||
|
|
||
| fn fetch(flash: Flash, buffer: &mut [u8]) -> (Flash, Option<[u8; VALUE_SIZE]>) { | ||
| let mut storage = MapStorage::<u8, _, _>::new(flash, config(), Cache::new_uncached()); | ||
| let value = block_on(storage.fetch_item::<[u8; VALUE_SIZE]>(buffer, &0)).unwrap(); | ||
| (storage.destroy().0, value) | ||
| } | ||
|
|
||
| fuzz_target!(|input: &[u8]| { | ||
| let mut flash = Flash::new(WriteCountCheck::OnceOnly, None, false); | ||
| let mut expected = None; | ||
| let mut buffer = [0; 256]; | ||
|
|
||
| for offset in (0..input.len().saturating_sub(2)).step_by(3).take(512) { | ||
| let command = &input[offset..offset + 3]; | ||
| let next = [command[1]; VALUE_SIZE]; | ||
| if command[0] & 1 != 0 { | ||
| flash.bytes_until_shutoff = Some(u32::from(command[2])); | ||
| } | ||
|
|
||
| let mut storage = MapStorage::<u8, _, _>::new(flash, config(), Cache::new_uncached()); | ||
| let result = block_on(storage.store_item(&mut buffer, &0, &next)); | ||
| (flash, _) = storage.destroy(); | ||
|
|
||
| // A reboot restores power and discards all cache state while keeping | ||
| // every byte that reached the flash before the shutdown. | ||
| flash.bytes_until_shutoff = None; | ||
| let recovered; | ||
| (flash, recovered) = fetch(flash, &mut buffer); | ||
|
|
||
| match result { | ||
| Ok(()) => { | ||
| assert_eq!(recovered, Some(next)); | ||
| expected = Some(next); | ||
| } | ||
| Err(Error::Storage { | ||
| value: MockFlashError::EarlyShutoff(_, _), | ||
| .. | ||
| }) => { | ||
| assert!(recovered == expected || recovered == Some(next)); | ||
| expected = recovered; | ||
| } | ||
| Err(error) => panic!("unexpected store error: {error:?}"), | ||
| } | ||
|
|
||
| let loaded; | ||
| (flash, loaded) = fetch(flash, &mut buffer); | ||
| assert_eq!(loaded, expected); | ||
| } | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -502,16 +502,24 @@ impl<S: NorFlash, C: CacheImpl<KEY>, KEY> GenericStorage<S, C, KEY> { | |
| let free_item_address = match self.cache.first_item_after_written(page_index) { | ||
| Some(free_item_address) => free_item_address, | ||
| None => { | ||
| ItemHeaderIter::new( | ||
| let mut headers = ItemHeaderIter::new( | ||
| self.cache | ||
| .first_item_after_erased(page_index) | ||
| .unwrap_or(0) | ||
| .max(start_address), | ||
| end_address, | ||
| ) | ||
| .traverse(&mut self.flash, |_, _| true) | ||
| .await? | ||
| .1 | ||
| ); | ||
| let free_item_address = headers.traverse(&mut self.flash, |_, _| true).await?.1; | ||
|
|
||
| // A torn header can end immediately before this erased area. | ||
| // Writing a new header there could complete a valid-looking | ||
| // phantom header that overlaps the new item. Stop using the | ||
| // page instead; callers will rotate to a clean page. | ||
| if headers.encountered_corruption { | ||
| return Ok(None); | ||
| } | ||
|
|
||
|
Comment on lines
+514
to
+521
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Jumping to a new page seems a bit wasteful. Say someone has a noisy SPI or a bad NOR chip and so there's a 0.01% of a bit flip for each bit. Then on a 4k page there'll be 3 wrong bits and so up to 3 corrupted items. We'd waste a lot of space by skipping 2/3rd of pages on average. That percentage is unrealistically high I think, but still |
||
| free_item_address | ||
| } | ||
| }; | ||
|
|
||
|
|
@@ -607,13 +615,15 @@ impl ItemIter { | |
| pub(crate) struct ItemHeaderIter { | ||
| current_address: u32, | ||
| end_address: u32, | ||
| encountered_corruption: bool, | ||
| } | ||
|
|
||
| impl ItemHeaderIter { | ||
| pub(crate) fn new(start_address: u32, end_address: u32) -> Self { | ||
| Self { | ||
| current_address: start_address, | ||
| end_address, | ||
| encountered_corruption: false, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -650,6 +660,7 @@ impl ItemHeaderIter { | |
| return Ok((None, self.current_address)); | ||
| } | ||
| Err(Error::Corrupted { .. }) => { | ||
| self.encountered_corruption = true; | ||
| self.current_address += S::WORD_SIZE as u32; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if it would also be fixed by jumping ahead a full header instead of jumping by a word... |
||
| } | ||
| Err(e) => return Err(e), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about this new fuzz target.
The real solution is to have the existing fuzz targets be able to insert multiple shutoffs (instead of the fixed 1 shutoff they now have)