Skip to content
Merged
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
22 changes: 22 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,25 @@ jobs:
- name: cargo +${{ matrix.msrv }} check (bootloader-tool)
run: cargo check
working-directory: "./bootloader-tool"

fuzz:
runs-on: ubuntu-latest
name: ubuntu / stable / fuzz
strategy:
fail-fast: false
matrix:
bin: [random-flash, interrupted]
steps:
- uses: actions/checkout@v4
with:
submodules: true

- name: Install stable
uses: dtolnay/rust-toolchain@stable

- name: cargo install cargo-fuzz
run: cargo install cargo-fuzz --locked

- name: cargo fuzz (libs/state)
run: cargo fuzz run --sanitizer none -j`nproc` ${{ matrix.bin }} -- -max_total_time=10
working-directory: "./libs/ec-slimloader-state/fuzz"
6 changes: 6 additions & 0 deletions libs/ec-slimloader-state/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,16 @@ embedded-storage-async = { workspace = true }
defmt = { workspace = true, optional = true }
defmt-or-log = { workspace = true }
log = { workspace = true, optional = true }
arbitrary = { version = "1.4.2", features = ["derive"], optional = true }

[dev-dependencies]
embassy-futures = "0.1.1"

[features]
defmt = ["dep:defmt", "defmt-or-log/defmt"]
log = ["dep:log", "defmt-or-log/log"]

# Used for the fuzzing framework
_test = ["dep:arbitrary"]

default = []
4 changes: 4 additions & 0 deletions libs/ec-slimloader-state/fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
target
corpus
artifacts
Cargo.lock
36 changes: 36 additions & 0 deletions libs/ec-slimloader-state/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[package]
name = "ec-slimloader-state-fuzz"
version = "0.0.0"
edition = "2021"
publish = false

[package.metadata]
cargo-fuzz = true

# Prevent this from interfering with workspaces
[workspace]
members = ["."]

[dependencies]
ec-slimloader-state = { path = "..", features = ["_test"] }

libfuzzer-sys = "0.4.9"
arbitrary = { version = "1.4.1", features = ["derive"] }
rand = "0.9.1"
rand_pcg = "0.9.0"
futures = { version = "0.3.31", features = ["executor"] }

[profile.release]
debug = 1

[[bin]]
name = "random-flash"
path = "fuzz_targets/random-flash.rs"
test = false
doc = false

[[bin]]
name = "interrupted"
path = "fuzz_targets/interrupted.rs"
test = false
doc = false
76 changes: 76 additions & 0 deletions libs/ec-slimloader-state/fuzz/fuzz_targets/interrupted.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#![no_main]

extern crate libfuzzer_sys;
extern crate std;

use arbitrary::Arbitrary;
use ec_slimloader_state::{
flash::{
self,
mock::{MockFlashBase, MockFlashError::EarlyShutoff},
FlashJournal,
},
state::State,
};
use libfuzzer_sys::fuzz_target;

fuzz_target!(|input: Input| fuzz(input.states, input.fail_at));

#[derive(Debug)]
struct Input {
/// Set of consecutive states to write to disk.
pub states: Vec<State>,

/// Byte number to fail at when doing disk operations.
pub fail_at: usize,
}

impl<'a> Arbitrary<'a> for Input {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let states: Vec<State> = Arbitrary::arbitrary(u)?;
let fail_at = u.int_in_range(0..=states.len() * 2)?;
Ok(Input { states, fail_at })
}
}

const PAGES: usize = 4;
const WORD_SIZE: usize = 2;
const WORDS_PER_PAGE: usize = 16;

/// Tests for 'any input disk, with valid or invalid data, does not cause a crash'.
fn fuzz(states: Vec<State>, fail_at: usize) {
let mut flash = MockFlashBase::<PAGES, WORD_SIZE, WORDS_PER_PAGE>::new(Some(fail_at as u32), false);
let mut states = states.into_iter().peekable();

futures::executor::block_on(async {
let mut journal = FlashJournal::new::<4>(&mut flash).await.unwrap();

let mut prev_state = None;
while let Some(new_state) = states.peek() {
match journal.set::<4>(new_state).await {
Ok(_) => {
assert_eq!(journal.get(), Some(new_state));
}
Err(flash::Error::Other(EarlyShutoff(_, _))) => {
drop(journal);
flash.remove_shutoff();
journal = FlashJournal::new::<4>(&mut flash).await.unwrap();
let old_state = journal.get();

if old_state == prev_state.as_ref() {
// Old state was at least kept, even though new state was not persisted.
continue;
} else if old_state == Some(new_state) {
// New state was successfully persisted, even though we had a shutoff (probably stopped during some bookkeeping?).
} else {
panic!("State not maintained or persisted");
}
break;
}
Err(e) => panic!("Unexpected error {:?}", e),
}

prev_state = states.next(); // Successfully persisted, drop the state.
}
});
}
40 changes: 40 additions & 0 deletions libs/ec-slimloader-state/fuzz/fuzz_targets/random-flash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#![no_main]

extern crate libfuzzer_sys;
extern crate std;

use arbitrary::Arbitrary;
use ec_slimloader_state::{
flash::{mock::MockFlashBase, FlashJournal},
state::State,
};
use libfuzzer_sys::fuzz_target;

fuzz_target!(|input: Input<'_>| fuzz(input.data, input.new_state));

#[derive(Arbitrary, Debug)]
struct Input<'a> {
pub data: &'a [u8],
pub new_state: State,
}

const PAGES: usize = 4;
const WORD_SIZE: usize = 2;
const WORDS_PER_PAGE: usize = 16;

/// Tests for 'any input disk, with valid or invalid data, does not cause a crash'.
fn fuzz(random_data: &[u8], new_state: State) {
let mut flash = MockFlashBase::<PAGES, WORD_SIZE, WORDS_PER_PAGE>::new(None, false);

let len = random_data.len().min(flash.as_bytes().len());
flash.as_bytes_mut()[..len].copy_from_slice(&random_data[..len]);

futures::executor::block_on(async {
// Instantiation should never crash. (error is only if there are not enough pages)
let mut journal = FlashJournal::new::<4>(&mut flash).await.unwrap();

// Finally try to update the state.
journal.set::<4>(&new_state).await.unwrap();
assert_eq!(journal.get(), Some(&new_state));
});
}
4 changes: 2 additions & 2 deletions libs/ec-slimloader-state/src/flash.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#[cfg(test)]
mod mock;
#[cfg(any(test, feature = "_test"))]
pub mod mock;

use core::ops::Range;

Expand Down
4 changes: 4 additions & 0 deletions libs/ec-slimloader-state/src/flash/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ impl<const PAGES: usize, const BYTES_PER_WORD: usize, const PAGE_WORDS: usize>
Ok(())
}
}

pub fn remove_shutoff(&mut self) {
self.bytes_until_shutoff = None;
}
}

impl<const PAGES: usize, const BYTES_PER_WORD: usize, const PAGE_WORDS: usize> ErrorType
Expand Down
2 changes: 1 addition & 1 deletion libs/ec-slimloader-state/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! Journal for the EC Slimloader containing [state::State].
#![no_std]
#![cfg_attr(not(feature = "_test"), no_std)]

#[cfg(test)]
#[macro_use]
Expand Down
18 changes: 18 additions & 0 deletions libs/ec-slimloader-state/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ impl core::fmt::Display for Slot {
}
}

#[cfg(feature = "_test")]
impl arbitrary::Arbitrary<'_> for Slot {
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
Ok(Slot::try_from(u.int_in_range(0..=(MAX_SLOT_COUNT - 1) as u8)?).unwrap())
}
}

#[derive(Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum ParseResult {
Expand All @@ -54,6 +61,7 @@ pub enum ParseResult {
/// ensuring minimal wear on the storage.
#[derive(Debug, PartialEq, Clone, Copy, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "_test", derive(arbitrary::Arbitrary))]
#[repr(u8)]
pub enum Status {
/// Initial attempt at booting the target image.
Expand All @@ -77,6 +85,16 @@ pub enum Status {
#[derive(PartialEq, Clone, Copy)]
pub struct State([u8; 2]);

#[cfg(feature = "_test")]
impl arbitrary::Arbitrary<'_> for State {
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
let status = Status::arbitrary(u)?;
let slot_a = Slot::arbitrary(u)?;
let slot_b = Slot::arbitrary(u)?;
Ok(State::new(status, slot_a, slot_b))
}
}

impl State {
pub const fn new(status: Status, target: Slot, backup: Slot) -> Self {
let mut data = 0u8;
Expand Down