From 7f5c0431a3ea858f0699314163fafef7ff2debb6 Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson Date: Wed, 27 Sep 2023 17:19:04 -0400 Subject: [PATCH] Add initial honggfuzz fuzzing infrastructure This initially includes fuzzers for the AVB and boot image parsers. The initial input corpus are the same test files we use for the round trip tests. Issue: #160 Signed-off-by: Andrew Gunnerson --- .github/workflows/ci.yml | 6 +- Cargo.lock | 50 ++++++++++++ Cargo.toml | 2 +- fuzz/.gitignore | 4 + fuzz/Cargo.toml | 13 +++ fuzz/README.md | 53 ++++++++++++ .../avb/input/vbmeta_appended_hash.img | 1 + .../avb/input/vbmeta_appended_hash_tree.img | 1 + .../hfuzz_workspace/avb/input/vbmeta_root.img | 1 + .../bootimage/input/boot_v0.img | 1 + .../bootimage/input/boot_v1.img | 1 + .../bootimage/input/boot_v2.img | 1 + .../bootimage/input/boot_v3.img | 1 + .../bootimage/input/boot_v4.img | 1 + .../bootimage/input/boot_v4_vts.img | 1 + .../bootimage/input/vendor_v3.img | 1 + .../bootimage/input/vendor_v4.img | 1 + fuzz/src/bin/avb.rs | 13 +++ fuzz/src/bin/bootimage.rs | 13 +++ xtask/README.md | 11 +++ xtask/src/main.rs | 5 +- xtask/src/wswrap.rs | 80 +++++++++++++++++++ 22 files changed, 256 insertions(+), 5 deletions(-) create mode 100644 fuzz/.gitignore create mode 100644 fuzz/Cargo.toml create mode 100644 fuzz/README.md create mode 120000 fuzz/hfuzz_workspace/avb/input/vbmeta_appended_hash.img create mode 120000 fuzz/hfuzz_workspace/avb/input/vbmeta_appended_hash_tree.img create mode 120000 fuzz/hfuzz_workspace/avb/input/vbmeta_root.img create mode 120000 fuzz/hfuzz_workspace/bootimage/input/boot_v0.img create mode 120000 fuzz/hfuzz_workspace/bootimage/input/boot_v1.img create mode 120000 fuzz/hfuzz_workspace/bootimage/input/boot_v2.img create mode 120000 fuzz/hfuzz_workspace/bootimage/input/boot_v3.img create mode 120000 fuzz/hfuzz_workspace/bootimage/input/boot_v4.img create mode 120000 fuzz/hfuzz_workspace/bootimage/input/boot_v4_vts.img create mode 120000 fuzz/hfuzz_workspace/bootimage/input/vendor_v3.img create mode 120000 fuzz/hfuzz_workspace/bootimage/input/vendor_v4.img create mode 100644 fuzz/src/bin/avb.rs create mode 100644 fuzz/src/bin/bootimage.rs create mode 100644 xtask/src/wswrap.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff9784f..ee41dd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,13 +54,13 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Clippy - run: cargo clippy --release --workspace --features static + run: cargo xtask wswrap -v -- clippy --release --features static - name: Build - run: cargo build --release --workspace --features static + run: cargo xtask wswrap -v -- build --release --features static - name: Tests - run: cargo test --release --workspace --features static + run: cargo xtask wswrap -v -- test --release --features static - name: Archive documentation uses: actions/upload-artifact@v3 diff --git a/Cargo.lock b/Cargo.lock index a707b5f..9cc9261 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,12 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "arbitrary" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" + [[package]] name = "assert_matches" version = "1.5.0" @@ -626,6 +632,14 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fuzz" +version = "2.1.0" +dependencies = [ + "avbroot", + "honggfuzz", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -706,6 +720,18 @@ dependencies = [ "digest", ] +[[package]] +name = "honggfuzz" +version = "0.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "848e9c511092e0daa0a35a63e8e6e475a3e8f870741448b9f6028d69b142f18e" +dependencies = [ + "arbitrary", + "lazy_static", + "memmap2", + "rustc_version", +] + [[package]] name = "http" version = "0.2.9" @@ -832,6 +858,15 @@ version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e" +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.0" @@ -1308,6 +1343,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.10" @@ -1388,6 +1432,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" + [[package]] name = "serde" version = "1.0.188" diff --git a/Cargo.toml b/Cargo.toml index 140b37d..2e1f300 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] default-members = ["avbroot"] -members = ["avbroot", "e2e", "xtask"] +members = ["avbroot", "e2e", "fuzz", "xtask"] resolver = "2" [workspace.package] diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..ca162a2 --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +/hfuzz_target/ +HONGGFUZZ.REPORT.TXT +*.honggfuzz.cov +*.fuzz diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..16a6e19 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "fuzz" +version.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +avbroot = { path = "../avbroot" } +honggfuzz = "0.5.55" diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 0000000..666d797 --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,53 @@ +# Fuzzing + +While avbroot's parsers are all memory-safe, it is still possible for panics or crashes to occur, for example due to excessive memory allocation, integer overflow, or division by zero. Fuzzing helps to identify these issues by randomizing inputs in a way that tries to increase code coverage. + +## Running the fuzzers + +1. Install the cargo honggfuzz commands. + + ```bash + cargo install honggfuzz + ``` + +2. Pick a fuzz target to run. A fuzz target is the name of the source file in [`src/bin/`](./src/bin) without the `.rs` extension. + + The list of targets can be queried programmatically with: + + ```bash + cargo read-manifest | jq -r '.targets[].name' + ``` + +3. Run the fuzzer. + + ```bash + cargo hfuzz run + ``` + + This will run forever until it is manually killed. At the top of the screen, a summary section like the following is shown: + + ``` + Iterations : 31,243 [31.24k] + Mode [1/3] : Feedback Driven Dry Run [2486/4085] + Target : hfuzz_target/x86_64-unknown-linux-gnu/release/bootimage + Threads : 8, CPUs: 16, CPU%: 800% [50%/CPU] + Speed : 36,126/sec [avg: 31,243] + Crashes : 53 [unique: 1, blocklist: 0, verified: 0] + Timeouts : 0 [1 sec] + Corpus Size : 1,424, max: 24,576 bytes, init: 4,085 files + Cov Update : 0 days 00 hrs 00 mins 00 secs ago + Coverage : edge: 897/224,621 [0%] pc: 2 cmp: 34,736 + ``` + + When a crash occurs, the `Crashes` counter will increment and the input data that triggered the crash will be written to `hfuzz_workspace//*.fuzz`. New files are only written for unique crashes. + +4. If a crash occurs, run the following command to trigger the crash in a debugger. + + ```bash + cargo hfuzz run-debug \ + hfuzz_workspace//.fuzz + ``` + + This defaults to using `rust-lldb`. To use `rust-gdb` instead, set the `HFUZZ_DEBUGGER` environment variable to `rust-gdb`. + + Alternatively, just feed the input file to the appropriate avbroot command directly (eg. `avbroot boot info -i hfuzz_workspace//.fuzz` for boot images). diff --git a/fuzz/hfuzz_workspace/avb/input/vbmeta_appended_hash.img b/fuzz/hfuzz_workspace/avb/input/vbmeta_appended_hash.img new file mode 120000 index 0000000..6383a5b --- /dev/null +++ b/fuzz/hfuzz_workspace/avb/input/vbmeta_appended_hash.img @@ -0,0 +1 @@ +../../../../avbroot/tests/data/vbmeta_appended_hash.img \ No newline at end of file diff --git a/fuzz/hfuzz_workspace/avb/input/vbmeta_appended_hash_tree.img b/fuzz/hfuzz_workspace/avb/input/vbmeta_appended_hash_tree.img new file mode 120000 index 0000000..ba6161b --- /dev/null +++ b/fuzz/hfuzz_workspace/avb/input/vbmeta_appended_hash_tree.img @@ -0,0 +1 @@ +../../../../avbroot/tests/data/vbmeta_appended_hash_tree.img \ No newline at end of file diff --git a/fuzz/hfuzz_workspace/avb/input/vbmeta_root.img b/fuzz/hfuzz_workspace/avb/input/vbmeta_root.img new file mode 120000 index 0000000..47756d1 --- /dev/null +++ b/fuzz/hfuzz_workspace/avb/input/vbmeta_root.img @@ -0,0 +1 @@ +../../../../avbroot/tests/data/vbmeta_root.img \ No newline at end of file diff --git a/fuzz/hfuzz_workspace/bootimage/input/boot_v0.img b/fuzz/hfuzz_workspace/bootimage/input/boot_v0.img new file mode 120000 index 0000000..4226a4b --- /dev/null +++ b/fuzz/hfuzz_workspace/bootimage/input/boot_v0.img @@ -0,0 +1 @@ +../../../../avbroot/tests/data/boot_v0.img \ No newline at end of file diff --git a/fuzz/hfuzz_workspace/bootimage/input/boot_v1.img b/fuzz/hfuzz_workspace/bootimage/input/boot_v1.img new file mode 120000 index 0000000..0bd4c53 --- /dev/null +++ b/fuzz/hfuzz_workspace/bootimage/input/boot_v1.img @@ -0,0 +1 @@ +../../../../avbroot/tests/data/boot_v1.img \ No newline at end of file diff --git a/fuzz/hfuzz_workspace/bootimage/input/boot_v2.img b/fuzz/hfuzz_workspace/bootimage/input/boot_v2.img new file mode 120000 index 0000000..9849242 --- /dev/null +++ b/fuzz/hfuzz_workspace/bootimage/input/boot_v2.img @@ -0,0 +1 @@ +../../../../avbroot/tests/data/boot_v2.img \ No newline at end of file diff --git a/fuzz/hfuzz_workspace/bootimage/input/boot_v3.img b/fuzz/hfuzz_workspace/bootimage/input/boot_v3.img new file mode 120000 index 0000000..d2a7bd4 --- /dev/null +++ b/fuzz/hfuzz_workspace/bootimage/input/boot_v3.img @@ -0,0 +1 @@ +../../../../avbroot/tests/data/boot_v3.img \ No newline at end of file diff --git a/fuzz/hfuzz_workspace/bootimage/input/boot_v4.img b/fuzz/hfuzz_workspace/bootimage/input/boot_v4.img new file mode 120000 index 0000000..3fe4481 --- /dev/null +++ b/fuzz/hfuzz_workspace/bootimage/input/boot_v4.img @@ -0,0 +1 @@ +../../../../avbroot/tests/data/boot_v4.img \ No newline at end of file diff --git a/fuzz/hfuzz_workspace/bootimage/input/boot_v4_vts.img b/fuzz/hfuzz_workspace/bootimage/input/boot_v4_vts.img new file mode 120000 index 0000000..78f5de3 --- /dev/null +++ b/fuzz/hfuzz_workspace/bootimage/input/boot_v4_vts.img @@ -0,0 +1 @@ +../../../../avbroot/tests/data/boot_v4_vts.img \ No newline at end of file diff --git a/fuzz/hfuzz_workspace/bootimage/input/vendor_v3.img b/fuzz/hfuzz_workspace/bootimage/input/vendor_v3.img new file mode 120000 index 0000000..7365a2c --- /dev/null +++ b/fuzz/hfuzz_workspace/bootimage/input/vendor_v3.img @@ -0,0 +1 @@ +../../../../avbroot/tests/data/vendor_v3.img \ No newline at end of file diff --git a/fuzz/hfuzz_workspace/bootimage/input/vendor_v4.img b/fuzz/hfuzz_workspace/bootimage/input/vendor_v4.img new file mode 120000 index 0000000..70d6582 --- /dev/null +++ b/fuzz/hfuzz_workspace/bootimage/input/vendor_v4.img @@ -0,0 +1 @@ +../../../../avbroot/tests/data/vendor_v4.img \ No newline at end of file diff --git a/fuzz/src/bin/avb.rs b/fuzz/src/bin/avb.rs new file mode 100644 index 0000000..105b1b5 --- /dev/null +++ b/fuzz/src/bin/avb.rs @@ -0,0 +1,13 @@ +use std::io::Cursor; + +use avbroot::format::avb; +use honggfuzz::fuzz; + +fn main() { + loop { + fuzz!(|data: &[u8]| { + let reader = Cursor::new(data); + let _ = avb::load_image(reader); + }); + } +} diff --git a/fuzz/src/bin/bootimage.rs b/fuzz/src/bin/bootimage.rs new file mode 100644 index 0000000..957dff8 --- /dev/null +++ b/fuzz/src/bin/bootimage.rs @@ -0,0 +1,13 @@ +use std::io::Cursor; + +use avbroot::{format::bootimage::BootImage, stream::FromReader}; +use honggfuzz::fuzz; + +fn main() { + loop { + fuzz!(|data: &[u8]| { + let reader = Cursor::new(data); + let _ = BootImage::from_reader(reader); + }); + } +} diff --git a/xtask/README.md b/xtask/README.md index 8c39eea..10974f3 100644 --- a/xtask/README.md +++ b/xtask/README.md @@ -30,3 +30,14 @@ cargo xtask modules -a ``` See the main [`README.md`](../README.md#avbroot-magisk-modules) for more details. + +## Platform specific `--workspace`-like wrapper + +Cargo currently doesn't support conditionally including workspace members on certain platforms (https://github.com/rust-lang/cargo/issues/5220 and https://github.com/rust-lang/cargo/issues/6179). Since honggfuzz does not support Windows, building with `--workspace` will fail on Windows. + +The `wswrap` wrapper works around this by running Cargo subcommands with `-p ` for each member supported by the platform. + +```bash +cargo xtask wswrap -- build --release +# Replaces: cargo build --release --workspace +``` diff --git a/xtask/src/main.rs b/xtask/src/main.rs index ede46d5..299934a 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -6,11 +6,12 @@ mod changelog; mod module; mod version; +mod wswrap; use anyhow::Result; use clap::{Parser, Subcommand}; -use crate::{module::ModulesCli, version::SetVersionCli}; +use crate::{module::ModulesCli, version::SetVersionCli, wswrap::WswrapCli}; const WORKSPACE_DIR: &str = env!("CARGO_WORKSPACE_DIR"); @@ -21,6 +22,7 @@ fn main() -> Result<()> { Command::SetVersion(c) => version::set_version_subcommand(&c), Command::Modules(c) => module::modules_subcommand(&c), Command::UpdateChangelog => changelog::update_changelog_subcommand(), + Command::Wswrap(c) => wswrap::wswrap_subcommand(&c), } } @@ -30,6 +32,7 @@ pub enum Command { Modules(ModulesCli), /// Update links in CHANGELOG.md. UpdateChangelog, + Wswrap(WswrapCli), } #[derive(Debug, Parser)] diff --git a/xtask/src/wswrap.rs b/xtask/src/wswrap.rs new file mode 100644 index 0000000..5491601 --- /dev/null +++ b/xtask/src/wswrap.rs @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: 2023 Andrew Gunnerson + * SPDX-License-Identifier: GPL-3.0-only + */ + +use std::{env, fs, path::Path, process::Command}; + +use anyhow::{anyhow, bail, Result}; +use clap::Parser; +use toml_edit::Document; + +use crate::WORKSPACE_DIR; + +fn get_members() -> Result> { + let path = Path::new(WORKSPACE_DIR).join("Cargo.toml"); + let data = fs::read_to_string(&path)?; + + let document: Document = data.parse()?; + let members = document["workspace"]["members"] + .as_array() + .ok_or_else(|| anyhow!("Cargo.toml has no workspace members"))?; + + let mut result = vec![]; + + for member in members { + let member = member + .as_str() + .ok_or_else(|| anyhow!("Member is not a string: {member:?}"))?; + + result.push(member.to_owned()); + } + + Ok(result) +} + +pub fn wswrap_subcommand(cli: &WswrapCli) -> Result<()> { + let cargo = env::var_os("CARGO").ok_or_else(|| anyhow!("CARGO is not set"))?; + let members = get_members()?; + + let mut command = Command::new(cargo); + command.args(&cli.args); + + for member in members { + #[cfg(windows)] + if member == "fuzz" { + continue; + } + + command.arg("-p"); + command.arg(member); + } + + if cli.verbose { + eprintln!("Running: {command:?}"); + } + + let mut child = command.spawn()?; + let status = child.wait()?; + + if !status.success() { + bail!("{command:?} failed with {status}"); + } + + Ok(()) +} + +/// Run a Cargo command against all workspace members compatible with the target +/// platform. +/// +/// This is meant to replace the use of `cargo --workspace`. This +/// currently only filters out the `fuzz` member on Windows. +#[derive(Debug, Parser)] +pub struct WswrapCli { + /// Display command before running. + #[arg(short, long)] + verbose: bool, + + /// Full Cargo command. + args: Vec, +}