From 9e07d7b8c3a4c135c608e5054dbdb5ad12e63d59 Mon Sep 17 00:00:00 2001 From: Stan Yolo Date: Mon, 31 Aug 2026 00:20:11 +0300 Subject: [PATCH] Fix phantom items after torn headers --- fuzz/Cargo.toml | 6 +++ fuzz/fuzz_targets/map-power-cuts.rs | 69 +++++++++++++++++++++++++++++ src/item.rs | 21 ++++++--- src/map.rs | 50 +++++++++++++++++++++ 4 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 fuzz/fuzz_targets/map-power-cuts.rs diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index e081151..4899c9e 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -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)'] } diff --git a/fuzz/fuzz_targets/map-power-cuts.rs b/fuzz/fuzz_targets/map-power-cuts.rs new file mode 100644 index 0000000..fc934b7 --- /dev/null +++ b/fuzz/fuzz_targets/map-power-cuts.rs @@ -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 { + const { MapConfig::new(0..CAPACITY as u32) } +} + +fn fetch(flash: Flash, buffer: &mut [u8]) -> (Flash, Option<[u8; VALUE_SIZE]>) { + let mut storage = MapStorage::::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::::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); + } +}); diff --git a/src/item.rs b/src/item.rs index 373ed90..ed033aa 100644 --- a/src/item.rs +++ b/src/item.rs @@ -502,16 +502,24 @@ impl, KEY> GenericStorage { 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); + } + + free_item_address } }; @@ -607,6 +615,7 @@ impl ItemIter { pub(crate) struct ItemHeaderIter { current_address: u32, end_address: u32, + encountered_corruption: bool, } impl ItemHeaderIter { @@ -614,6 +623,7 @@ impl ItemHeaderIter { 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; } Err(e) => return Err(e), diff --git a/src/map.rs b/src/map.rs index 88a4bda..beedd7a 100644 --- a/src/map.rs +++ b/src/map.rs @@ -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 { + 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::::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::::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::::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::::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(