Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ path = "fuzz_targets/map.rs"
test = false
doc = false

[[bin]]
name = "map-power-cuts"
path = "fuzz_targets/map-power-cuts.rs"
test = false
doc = false

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }

Expand Down
69 changes: 69 additions & 0 deletions fuzz/fuzz_targets/map-power-cuts.rs

Copy link
Copy Markdown
Member

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)

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);
}
});
21 changes: 16 additions & 5 deletions src/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
}
};

Expand Down Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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),
Expand Down
50 changes: 50 additions & 0 deletions src/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1953,6 +1953,56 @@ mod tests {
);
}

#[test]
async fn clean_store_is_visible_after_repeated_shutdowns_during_initialization() {
type Flash = mock_flash::MockFlashBase<2, 1, 128>;
fn config() -> MapConfig<Flash> {
const { MapConfig::new(0..256) }
}

let mut flash = Flash::new(mock_flash::WriteCountCheck::OnceOnly, Some(7), false);
let mut data_buffer = AlignedBuf([0; 256]);

for _ in 0..2 {
let mut storage = MapStorage::<u8, _, _>::new(flash, config(), Cache::new_uncached());
std::assert_matches!(
storage.store_item(&mut data_buffer.0, &0, &[7u8; 8]).await,
Err(Error::Storage {
value: mock_flash::MockFlashError::EarlyShutoff(_, _)
})
);
(flash, _) = storage.destroy();

let mut storage = MapStorage::<u8, _, _>::new(flash, config(), Cache::new_uncached());
assert_eq!(
storage
.fetch_item::<[u8; 8]>(&mut data_buffer.0, &0)
.await
.unwrap(),
None
);
(flash, _) = storage.destroy();
flash.bytes_until_shutoff = Some(7);
}

flash.bytes_until_shutoff = None;
let mut storage = MapStorage::<u8, _, _>::new(flash, config(), Cache::new_uncached());
storage
.store_item(&mut data_buffer.0, &0, &[226u8; 8])
.await
.unwrap();
(flash, _) = storage.destroy();

let mut storage = MapStorage::<u8, _, _>::new(flash, config(), Cache::new_uncached());
assert_eq!(
storage
.fetch_item::<[u8; 8]>(&mut data_buffer.0, &0)
.await
.unwrap(),
Some([226; 8])
);
}

#[test]
async fn store_unit_key() {
let mut storage = MapStorage::new(
Expand Down