Skip to content
Open
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
67 changes: 67 additions & 0 deletions .github/workflows/cont_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,70 @@ jobs:
cache: true
- name: Check docs
run: RUSTDOCFLAGS='-D warnings' cargo doc --workspace --all-features --no-deps

fuzz:
needs: prepare
name: Fuzz (${{ matrix.fuzzer }}, ${{ matrix.target }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
fuzzer: [afl, honggfuzz, libfuzzer]
target: [local_chain_apply_update, local_chain_apply_update_header]
env:
FUZZ_TARGET: ${{ matrix.target }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
# cargo-fuzz (libFuzzer) requires nightly; AFL and honggfuzz work on stable.
toolchain: ${{ matrix.fuzzer == 'libfuzzer' && 'nightly' || needs.prepare.outputs.rust_version }}
override: true
cache: true
- name: Install honggfuzz build dependencies
if: matrix.fuzzer == 'honggfuzz'
run: sudo apt-get update && sudo apt-get install -y binutils-dev libunwind-dev
- name: Install cargo-afl
if: matrix.fuzzer == 'afl'
run: |
cargo install cargo-afl --force
cargo afl config --build --force
- name: Install cargo-hfuzz
if: matrix.fuzzer == 'honggfuzz'
run: cargo install honggfuzz
- name: Install cargo-fuzz
if: matrix.fuzzer == 'libfuzzer'
run: cargo install cargo-fuzz
- name: Fuzz for 1 minute (AFL)
if: matrix.fuzzer == 'afl'
working-directory: ./fuzz
env:
AFL_NO_UI: 1
AFL_SKIP_CPUFREQ: 1
AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES: 1
run: |
mkdir -p ci-seeds && printf 'bdk-fuzz-seed' > ci-seeds/seed
cargo afl build --features afl_fuzz --bin "$FUZZ_TARGET"
cargo afl fuzz -i ci-seeds -o afl-out -V 60 -- "target/debug/$FUZZ_TARGET"
crashes=$(find afl-out -path '*crashes*' -name 'id:*')
if [ -n "$crashes" ]; then
echo "AFL found crashes:"; echo "$crashes"; exit 1
fi
- name: Fuzz for 1 minute (honggfuzz)
if: matrix.fuzzer == 'honggfuzz'
working-directory: ./fuzz
env:
HFUZZ_BUILD_ARGS: --features honggfuzz_fuzz
HFUZZ_RUN_ARGS: --run_time 60 --exit_upon_crash -v
run: |
cargo hfuzz run "$FUZZ_TARGET"
if [ -f "hfuzz_workspace/$FUZZ_TARGET/HONGGFUZZ.REPORT.TXT" ]; then
cat "hfuzz_workspace/$FUZZ_TARGET/HONGGFUZZ.REPORT.TXT"; exit 1
fi
- name: Fuzz for 1 minute (libFuzzer)
if: matrix.fuzzer == 'libfuzzer'
run: cargo fuzz run "$FUZZ_TARGET" --features libfuzzer_fuzz -- -max_total_time=60 -print_final_stats=1
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ Cargo.lock
*.sqlite*

crates/electrum/target
fuzz/target
20 changes: 18 additions & 2 deletions crates/chain/src/local_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,23 @@ where
}

/// Apply the given `changeset`.
///
/// # Errors
///
/// [`ApplyBlockError::MissingGenesis`] occurs if the `changeset` would remove or replace the
/// genesis block. [`ApplyBlockError::PrevBlockhashMismatch`] occurs if it would break a
/// `prev_blockhash` link.
///
/// The chain is left untouched when this fails.
pub fn apply_changeset(&mut self, changeset: &ChangeSet<D>) -> Result<(), ApplyBlockError> {
// Genesis is immutable: a changeset that swaps it belongs to a different chain. Removing
// it is caught further down by `LocalChain::from_blocks`.
if let Some(Some(data)) = changeset.blocks.get(&0) {
if data.to_blockhash() != self.genesis_hash() {
return Err(ApplyBlockError::MissingGenesis);
}
}

let old_tip = self.tip.clone();
let new_tip = apply_changeset_to_checkpoint(old_tip, changeset)?;
self.tip = new_tip;
Expand Down Expand Up @@ -553,7 +569,7 @@ impl<D> FromIterator<(u32, D)> for ChangeSet<D> {
/// Error when applying blocks to a local chain.
#[derive(Clone, Debug, PartialEq)]
pub enum ApplyBlockError {
/// Genesis block is missing.
/// Genesis block is missing, or would be replaced by a different block.
MissingGenesis,
/// Block's `prev_blockhash` doesn't match the expected block.
PrevBlockhashMismatch {
Expand All @@ -566,7 +582,7 @@ impl core::fmt::Display for ApplyBlockError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ApplyBlockError::MissingGenesis => {
write!(f, "genesis block is missing")
write!(f, "genesis block is missing or would be replaced")
}
ApplyBlockError::PrevBlockhashMismatch { expected } => write!(
f,
Expand Down
4 changes: 4 additions & 0 deletions fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
target
corpus
artifacts
coverage
37 changes: 37 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[package]
name = "bdk_fuzz"
version = "0.0.0"
publish = false
edition = "2021"

[package.metadata]
cargo-fuzz = true

[workspace]
members = ["."]

[dependencies]
bdk_chain = { path = "../crates/chain" }
arbitrary = { version = "1.4.1", features = ["derive"] }
libfuzzer-sys = { version = "0.4", optional = true }
honggfuzz = { version = "0.5.61", optional = true }
afl = { version = "0.18.2", optional = true }

[features]
afl_fuzz = ["afl"]
honggfuzz_fuzz = ["honggfuzz"]
libfuzzer_fuzz = ["libfuzzer-sys"]

[[bin]]
name = "local_chain_apply_update"
path = "fuzz_targets/chain/local_chain_apply_update.rs"
test = false
doc = false
bench = false

[[bin]]
name = "local_chain_apply_update_header"
path = "fuzz_targets/chain/local_chain_apply_update_header.rs"
test = false
doc = false
bench = false
101 changes: 101 additions & 0 deletions fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Fuzzing

Fuzz targets for the BDK crates. All commands run from this directory (`fuzz/`).

Targets are grouped per crate: harnesses live in `fuzz_targets/<crate>/` and the
generators and invariant checks they share live in `src/<crate>/`. Engine plumbing
(`src/engines.rs`) is shared by every target.

## Targets

### `bdk_chain`

| Target | What it fuzzes |
| ---------------------------------- | ------------------------------------------------------- |
| `local_chain_apply_update` | `LocalChain<BlockHash>` |
| `local_chain_apply_update_header` | `LocalChain<Header>` (checkpoint gaps become placeholders) |

Each target drives a chain through a sequence of arbitrary operations
(`apply_update`, `apply_changeset`, `insert_block`, `disconnect_from`,
`apply_header{,_connected_to}`) and asserts the invariants in
`src/chain/checks.rs` after every one.

## libFuzzer

Requires nightly.

```sh
cargo install cargo-fuzz
cargo +nightly fuzz run local_chain_apply_update --features libfuzzer_fuzz -- -max_total_time=300
```

Crashes land in `artifacts/<target>/`. Replay one with:

```sh
cargo +nightly fuzz run local_chain_apply_update --features libfuzzer_fuzz artifacts/local_chain_apply_update/crash-<hash>
```

## honggfuzz

```sh
sudo apt-get install -y binutils-dev libunwind-dev # build dependencies
cargo install honggfuzz

HFUZZ_BUILD_ARGS="--features honggfuzz_fuzz" \
HFUZZ_RUN_ARGS="--run_time 300 --exit_upon_crash -v" \
cargo hfuzz run local_chain_apply_update
```

A crash writes `hfuzz_workspace/<target>/HONGGFUZZ.REPORT.TXT`.

## AFL++

```sh
cargo install cargo-afl
cargo afl config --build

mkdir -p afl-seeds && printf 'bdk-fuzz-seed' > afl-seeds/seed
cargo afl build --features afl_fuzz --bin local_chain_apply_update
cargo afl fuzz -i afl-seeds -o afl-out -V 300 -- target/debug/local_chain_apply_update
```

Crashes land in `afl-out/*/crashes/`.

## Replaying without a fuzzer

Built with no engine feature, each target gets a `main` that replays the corpus
files passed as arguments. Useful for debugging a crash under `rust-gdb` or with
a backtrace:

```sh
cargo build --bin local_chain_apply_update
RUST_BACKTRACE=1 ./target/debug/local_chain_apply_update corpus/local_chain_apply_update/*
```

## Coverage report

`cargo fuzz coverage` runs a target over its corpus and writes
`coverage/<target>/coverage.profdata`. Report on that profile against the same
target's binary:

```sh
HOST=$(rustc +nightly -vV | sed -n 's/^host: //p')
LLVM_BIN="$(rustc +nightly --print sysroot)/lib/rustlib/$HOST/bin"
BUILD_DIR="target/$HOST/coverage/$HOST/release"
SOURCES="../crates/chain/src/local_chain.rs ../crates/core/src/checkpoint.rs"

for target in local_chain_apply_update local_chain_apply_update_header; do
cargo +nightly fuzz coverage "$target" --features libfuzzer_fuzz

echo "=== $target"
"$LLVM_BIN/llvm-cov" report \
-instr-profile="coverage/$target/coverage.profdata" \
-object "$BUILD_DIR/$target" \
-sources $SOURCES
done
```

Swap `report` for `show ... --format=html --output-dir=coverage/$target/report`
to get a line-by-line HTML report per target.

Requires the `llvm-tools` rustup component (`rustup component add llvm-tools`).
Loading