Skip to content

Commit 46db116

Browse files
Wassasinjerrysxie
andauthored
Add fuzzing for state journal (#28)
Adds two tests: * given a initial random (garbage) state of the NVM, we are always able to set a new state. * given a sequence of states that gets interrupted mid-write (or -erase), either the previous state or the new state can successfully be read back. --------- Co-authored-by: Jerry Xie <139205137+jerrysxie@users.noreply.github.com>
1 parent 2b49e18 commit 46db116

File tree

10 files changed

+209
-3
lines changed

10 files changed

+209
-3
lines changed

.github/workflows/check.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,25 @@ jobs:
239239
- name: cargo +${{ matrix.msrv }} check (bootloader-tool)
240240
run: cargo check
241241
working-directory: "./bootloader-tool"
242+
243+
fuzz:
244+
runs-on: ubuntu-latest
245+
name: ubuntu / stable / fuzz
246+
strategy:
247+
fail-fast: false
248+
matrix:
249+
bin: [random-flash, interrupted]
250+
steps:
251+
- uses: actions/checkout@v4
252+
with:
253+
submodules: true
254+
255+
- name: Install stable
256+
uses: dtolnay/rust-toolchain@stable
257+
258+
- name: cargo install cargo-fuzz
259+
run: cargo install cargo-fuzz --locked
260+
261+
- name: cargo fuzz (libs/state)
262+
run: cargo fuzz run --sanitizer none -j`nproc` ${{ matrix.bin }} -- -max_total_time=10
263+
working-directory: "./libs/ec-slimloader-state/fuzz"

libs/ec-slimloader-state/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,16 @@ embedded-storage-async = { workspace = true }
1313
defmt = { workspace = true, optional = true }
1414
defmt-or-log = { workspace = true }
1515
log = { workspace = true, optional = true }
16+
arbitrary = { version = "1.4.2", features = ["derive"], optional = true }
1617

1718
[dev-dependencies]
1819
embassy-futures = "0.1.1"
1920

2021
[features]
2122
defmt = ["dep:defmt", "defmt-or-log/defmt"]
2223
log = ["dep:log", "defmt-or-log/log"]
24+
25+
# Used for the fuzzing framework
26+
_test = ["dep:arbitrary"]
27+
28+
default = []
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
target
2+
corpus
3+
artifacts
4+
Cargo.lock
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[package]
2+
name = "ec-slimloader-state-fuzz"
3+
version = "0.0.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[package.metadata]
8+
cargo-fuzz = true
9+
10+
# Prevent this from interfering with workspaces
11+
[workspace]
12+
members = ["."]
13+
14+
[dependencies]
15+
ec-slimloader-state = { path = "..", features = ["_test"] }
16+
17+
libfuzzer-sys = "0.4.9"
18+
arbitrary = { version = "1.4.1", features = ["derive"] }
19+
rand = "0.9.1"
20+
rand_pcg = "0.9.0"
21+
futures = { version = "0.3.31", features = ["executor"] }
22+
23+
[profile.release]
24+
debug = 1
25+
26+
[[bin]]
27+
name = "random-flash"
28+
path = "fuzz_targets/random-flash.rs"
29+
test = false
30+
doc = false
31+
32+
[[bin]]
33+
name = "interrupted"
34+
path = "fuzz_targets/interrupted.rs"
35+
test = false
36+
doc = false
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#![no_main]
2+
3+
extern crate libfuzzer_sys;
4+
extern crate std;
5+
6+
use arbitrary::Arbitrary;
7+
use ec_slimloader_state::{
8+
flash::{
9+
self,
10+
mock::{MockFlashBase, MockFlashError::EarlyShutoff},
11+
FlashJournal,
12+
},
13+
state::State,
14+
};
15+
use libfuzzer_sys::fuzz_target;
16+
17+
fuzz_target!(|input: Input| fuzz(input.states, input.fail_at));
18+
19+
#[derive(Debug)]
20+
struct Input {
21+
/// Set of consecutive states to write to disk.
22+
pub states: Vec<State>,
23+
24+
/// Byte number to fail at when doing disk operations.
25+
pub fail_at: usize,
26+
}
27+
28+
impl<'a> Arbitrary<'a> for Input {
29+
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
30+
let states: Vec<State> = Arbitrary::arbitrary(u)?;
31+
let fail_at = u.int_in_range(0..=states.len() * 2)?;
32+
Ok(Input { states, fail_at })
33+
}
34+
}
35+
36+
const PAGES: usize = 4;
37+
const WORD_SIZE: usize = 2;
38+
const WORDS_PER_PAGE: usize = 16;
39+
40+
/// Tests for 'any input disk, with valid or invalid data, does not cause a crash'.
41+
fn fuzz(states: Vec<State>, fail_at: usize) {
42+
let mut flash = MockFlashBase::<PAGES, WORD_SIZE, WORDS_PER_PAGE>::new(Some(fail_at as u32), false);
43+
let mut states = states.into_iter().peekable();
44+
45+
futures::executor::block_on(async {
46+
let mut journal = FlashJournal::new::<4>(&mut flash).await.unwrap();
47+
48+
let mut prev_state = None;
49+
while let Some(new_state) = states.peek() {
50+
match journal.set::<4>(new_state).await {
51+
Ok(_) => {
52+
assert_eq!(journal.get(), Some(new_state));
53+
}
54+
Err(flash::Error::Other(EarlyShutoff(_, _))) => {
55+
drop(journal);
56+
flash.remove_shutoff();
57+
journal = FlashJournal::new::<4>(&mut flash).await.unwrap();
58+
let old_state = journal.get();
59+
60+
if old_state == prev_state.as_ref() {
61+
// Old state was at least kept, even though new state was not persisted.
62+
continue;
63+
} else if old_state == Some(new_state) {
64+
// New state was successfully persisted, even though we had a shutoff (probably stopped during some bookkeeping?).
65+
} else {
66+
panic!("State not maintained or persisted");
67+
}
68+
break;
69+
}
70+
Err(e) => panic!("Unexpected error {:?}", e),
71+
}
72+
73+
prev_state = states.next(); // Successfully persisted, drop the state.
74+
}
75+
});
76+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#![no_main]
2+
3+
extern crate libfuzzer_sys;
4+
extern crate std;
5+
6+
use arbitrary::Arbitrary;
7+
use ec_slimloader_state::{
8+
flash::{mock::MockFlashBase, FlashJournal},
9+
state::State,
10+
};
11+
use libfuzzer_sys::fuzz_target;
12+
13+
fuzz_target!(|input: Input<'_>| fuzz(input.data, input.new_state));
14+
15+
#[derive(Arbitrary, Debug)]
16+
struct Input<'a> {
17+
pub data: &'a [u8],
18+
pub new_state: State,
19+
}
20+
21+
const PAGES: usize = 4;
22+
const WORD_SIZE: usize = 2;
23+
const WORDS_PER_PAGE: usize = 16;
24+
25+
/// Tests for 'any input disk, with valid or invalid data, does not cause a crash'.
26+
fn fuzz(random_data: &[u8], new_state: State) {
27+
let mut flash = MockFlashBase::<PAGES, WORD_SIZE, WORDS_PER_PAGE>::new(None, false);
28+
29+
let len = random_data.len().min(flash.as_bytes().len());
30+
flash.as_bytes_mut()[..len].copy_from_slice(&random_data[..len]);
31+
32+
futures::executor::block_on(async {
33+
// Instantiation should never crash. (error is only if there are not enough pages)
34+
let mut journal = FlashJournal::new::<4>(&mut flash).await.unwrap();
35+
36+
// Finally try to update the state.
37+
journal.set::<4>(&new_state).await.unwrap();
38+
assert_eq!(journal.get(), Some(&new_state));
39+
});
40+
}

libs/ec-slimloader-state/src/flash.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
#[cfg(test)]
2-
mod mock;
1+
#[cfg(any(test, feature = "_test"))]
2+
pub mod mock;
33

44
use core::ops::Range;
55

libs/ec-slimloader-state/src/flash/mock.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ impl<const PAGES: usize, const BYTES_PER_WORD: usize, const PAGE_WORDS: usize>
8888
Ok(())
8989
}
9090
}
91+
92+
pub fn remove_shutoff(&mut self) {
93+
self.bytes_until_shutoff = None;
94+
}
9195
}
9296

9397
impl<const PAGES: usize, const BYTES_PER_WORD: usize, const PAGE_WORDS: usize> ErrorType

libs/ec-slimloader-state/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//! Journal for the EC Slimloader containing [state::State].
2-
#![no_std]
2+
#![cfg_attr(not(feature = "_test"), no_std)]
33

44
#[cfg(test)]
55
#[macro_use]

libs/ec-slimloader-state/src/state.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ impl core::fmt::Display for Slot {
3939
}
4040
}
4141

42+
#[cfg(feature = "_test")]
43+
impl arbitrary::Arbitrary<'_> for Slot {
44+
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
45+
Ok(Slot::try_from(u.int_in_range(0..=(MAX_SLOT_COUNT - 1) as u8)?).unwrap())
46+
}
47+
}
48+
4249
#[derive(Debug)]
4350
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
4451
pub enum ParseResult {
@@ -54,6 +61,7 @@ pub enum ParseResult {
5461
/// ensuring minimal wear on the storage.
5562
#[derive(Debug, PartialEq, Clone, Copy, TryFromPrimitive, IntoPrimitive)]
5663
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
64+
#[cfg_attr(feature = "_test", derive(arbitrary::Arbitrary))]
5765
#[repr(u8)]
5866
pub enum Status {
5967
/// Initial attempt at booting the target image.
@@ -77,6 +85,16 @@ pub enum Status {
7785
#[derive(PartialEq, Clone, Copy)]
7886
pub struct State([u8; 2]);
7987

88+
#[cfg(feature = "_test")]
89+
impl arbitrary::Arbitrary<'_> for State {
90+
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
91+
let status = Status::arbitrary(u)?;
92+
let slot_a = Slot::arbitrary(u)?;
93+
let slot_b = Slot::arbitrary(u)?;
94+
Ok(State::new(status, slot_a, slot_b))
95+
}
96+
}
97+
8098
impl State {
8199
pub const fn new(status: Status, target: Slot, backup: Slot) -> Self {
82100
let mut data = 0u8;

0 commit comments

Comments
 (0)