Skip to content

Commit 048f70c

Browse files
committed
fix: simply fuzz targets, add a coverage viewer
1 parent 1efb8d5 commit 048f70c

File tree

8 files changed

+128
-137
lines changed

8 files changed

+128
-137
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,10 @@ jobs:
6363
- uses: extractions/setup-just@v3
6464
- uses: actions-rust-lang/setup-rust-toolchain@v1
6565
- run: just bench
66-
# Light smoke test fuzz.
6766
fuzz:
6867
runs-on: ubuntu-latest
6968
steps:
7069
- uses: actions/checkout@v4
7170
- uses: extractions/setup-just@v3
7271
- uses: actions-rust-lang/setup-rust-toolchain@v1
73-
- run: just fuzz
72+
- run: just test fuzz

.gitignore

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
11
Cargo.lock
2-
/target
3-
# IDEs
4-
.vscode/
2+
**/target

justfile

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
# The recipes make heavy use of `rustup`'s toolchain syntax (e.g. `cargo +nightly`). `rustup` is
44
# required on the system in order to intercept the `cargo` commands and to install and use the appropriate toolchain with components.
55

6-
NIGHTLY_TOOLCHAIN := "nightly-2025-06-19"
6+
NIGHTLY_TOOLCHAIN := "nightly-2025-07-10"
77
STABLE_TOOLCHAIN := "1.87.0"
8+
FUZZ_VERSION := "0.12.0"
89

910
_default:
1011
@just --list
@@ -29,7 +30,7 @@ _default:
2930
# Adding --fix flag to apply suggestions with --allow-dirty.
3031
cargo +{{NIGHTLY_TOOLCHAIN}} clippy --workspace --all-features --all-targets --fix --allow-dirty -- -D warnings
3132

32-
# Run a test suite: unit, features, msrv, constraints, or no-std.
33+
# Run a test suite: unit, features, msrv, constraints, no-std, or fuzz.
3334
@test suite="unit":
3435
just _test-{{suite}}
3536

@@ -71,14 +72,25 @@ _default:
7172
cargo install cross@0.2.5
7273
$HOME/.cargo/bin/cross build --package bip324 --target thumbv7m-none-eabi --no-default-features
7374

75+
# Type check the fuzz targets.
76+
@_test-fuzz:
77+
cargo install cargo-fuzz@{{FUZZ_VERSION}}
78+
cd protocol && cargo +{{NIGHTLY_TOOLCHAIN}} fuzz check
79+
7480
# Run benchmarks.
7581
bench:
7682
cargo +{{NIGHTLY_TOOLCHAIN}} bench --package bip324 --bench cipher_session
7783

78-
# Run fuzz test: receive_key, receive_garbage, receive_version.
79-
@fuzz target="receive_garbage" time="60":
80-
cargo install cargo-fuzz@0.12.0
81-
cd protocol && cargo +{{NIGHTLY_TOOLCHAIN}} fuzz run {{target}} -- -max_total_time={{time}}
84+
# Run fuzz target: receive_key or receive_garbage.
85+
@fuzz target seconds:
86+
rustup component add --toolchain {{NIGHTLY_TOOLCHAIN}} llvm-tools-preview
87+
cargo install cargo-fuzz@{{FUZZ_VERSION}}
88+
# Generate new test cases and add to corpus. Bumping length for garbage.
89+
cd protocol && cargo +{{NIGHTLY_TOOLCHAIN}} fuzz run {{target}} -- -max_len=5120 -max_total_time={{seconds}}
90+
# Measure coverage of corpus against code.
91+
cd protocol && cargo +{{NIGHTLY_TOOLCHAIN}} fuzz coverage {{target}}
92+
# Generate HTML coverage report.
93+
protocol/fuzz/coverage.sh {{NIGHTLY_TOOLCHAIN}} {{target}}
8294

8395
# Add a release tag and publish to the upstream remote. Need write privileges on the repository.
8496
@tag crate version remote="upstream":

protocol/fuzz/Cargo.toml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ cargo-fuzz = true
1010
[dependencies]
1111
libfuzzer-sys = "0.4"
1212
bip324 = { path = ".." }
13+
rand = "0.8"
14+
secp256k1 = "0.29"
1315

1416
[[bin]]
1517
name = "receive_garbage"
@@ -18,13 +20,6 @@ test = false
1820
doc = false
1921
bench = false
2022

21-
[[bin]]
22-
name = "receive_version"
23-
path = "fuzz_targets/receive_version.rs"
24-
test = false
25-
doc = false
26-
bench = false
27-
2823
[[bin]]
2924
name = "receive_key"
3025
path = "fuzz_targets/receive_key.rs"

protocol/fuzz/coverage.sh

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env bash
2+
3+
# Generate HTML coverage report
4+
# Usage: ./coverage.sh <toolchain> <target>
5+
# Example: ./coverage.sh nightly-2025-07-10 receive_key
6+
#
7+
# Grabbed from this blog: https://tweedegolf.nl/en/blog/154/what-is-my-fuzzer-doing
8+
# Hopefully standardized soon: https://github.com/taiki-e/cargo-llvm-cov/pull/431
9+
10+
set -euo pipefail
11+
12+
if [ $# -ne 2 ]; then
13+
echo "Usage: $0 <toolchain> <target>"
14+
echo "Example: $0 nightly-2025-07-10 receive_key"
15+
exit 1
16+
fi
17+
18+
TOOLCHAIN="$1"
19+
TARGET="$2"
20+
21+
# Change to protocol directory.
22+
cd "$(dirname "$0")/.."
23+
24+
# Install rustfilt for demangling.
25+
cargo install rustfilt@0.2.1
26+
RUSTFILT="$HOME/.cargo/bin/rustfilt"
27+
28+
# Get toolchain info
29+
SYSROOT=$(rustc +${TOOLCHAIN} --print sysroot)
30+
HOST_TUPLE=$(rustc +${TOOLCHAIN} --print host-tuple)
31+
32+
BINARY="target/$HOST_TUPLE/coverage/$HOST_TUPLE/release/$TARGET"
33+
34+
echo "Generating HTML coverage report for $TARGET..."
35+
"$SYSROOT/lib/rustlib/$HOST_TUPLE/bin/llvm-cov" show \
36+
"$BINARY" \
37+
-instr-profile=fuzz/coverage/"$TARGET"/coverage.profdata \
38+
-Xdemangler="$RUSTFILT" \
39+
--format=html \
40+
-output-dir=fuzz/coverage/"$TARGET"/html \
41+
-ignore-filename-regex="\.cargo|\.rustup|fuzz_target|/rustc/"
42+
43+
REPORT_PATH="$(pwd)/fuzz/coverage/$TARGET/html/index.html"
44+
echo "$REPORT_PATH"
Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,66 @@
11
// SPDX-License-Identifier: CC0-1.0
22

33
//! Fuzz test for the receive_garbage function.
4-
//!
5-
//! This focused test fuzzes only the garbage terminator detection logic,
6-
//! which is more effective than trying to fuzz the entire handshake.
74
85
#![no_main]
9-
use bip324::{GarbageResult, Handshake, Initialized, Network, ReceivedKey, Role};
6+
use bip324::{Handshake, Initialized, Network, ReceivedKey, Role};
107
use libfuzzer_sys::fuzz_target;
8+
use rand::SeedableRng;
119

1210
fuzz_target!(|data: &[u8]| {
11+
// Cap input size to avoid wasting time on obviously invalid large inputs
12+
// The protocol limit is 4095 garbage bytes + 16 terminator bytes = 4111 total
13+
// Test up to ~5000 bytes to cover boundary cases
14+
if data.len() > 5000 {
15+
return;
16+
}
17+
18+
// Use deterministic seeds for reproducible fuzzing
19+
let seed = [42u8; 32];
20+
let mut rng = rand::rngs::StdRng::from_seed(seed);
21+
let secp = secp256k1::Secp256k1::signing_only();
22+
1323
// Set up a valid handshake in the SentVersion state
14-
let initiator = Handshake::<Initialized>::new(Network::Bitcoin, Role::Initiator).unwrap();
24+
let initiator =
25+
Handshake::<Initialized>::new_with_rng(Network::Bitcoin, Role::Initiator, &mut rng, &secp)
26+
.unwrap();
1527
let mut initiator_key = vec![0u8; Handshake::<Initialized>::send_key_len(None)];
1628
let initiator = initiator.send_key(None, &mut initiator_key).unwrap();
1729

18-
let responder = Handshake::<Initialized>::new(Network::Bitcoin, Role::Responder).unwrap();
30+
let mut rng2 = rand::rngs::StdRng::from_seed([43u8; 32]);
31+
let responder =
32+
Handshake::<Initialized>::new_with_rng(Network::Bitcoin, Role::Responder, &mut rng2, &secp)
33+
.unwrap();
1934
let mut responder_key = vec![0u8; Handshake::<Initialized>::send_key_len(None)];
2035
let responder = responder.send_key(None, &mut responder_key).unwrap();
2136

2237
// Exchange keys using real keys to get valid ECDH shared secrets
2338
let initiator = initiator
2439
.receive_key(responder_key[..64].try_into().unwrap())
2540
.unwrap();
26-
let _responder = responder
41+
let responder = responder
2742
.receive_key(initiator_key[..64].try_into().unwrap())
2843
.unwrap();
2944

45+
// Get the real responder's garbage terminator from responder's send_version output
46+
let mut responder_version = vec![0u8; Handshake::<ReceivedKey>::send_version_len(None)];
47+
let _responder = responder
48+
.send_version(&mut responder_version, None)
49+
.unwrap();
50+
51+
// The responder's garbage terminator is in the first 16 bytes of their version output
52+
let responder_terminator = &responder_version[..16];
53+
3054
// Send version to reach SentVersion state
3155
let mut initiator_version = vec![0u8; Handshake::<ReceivedKey>::send_version_len(None)];
3256
let initiator = initiator
3357
.send_version(&mut initiator_version, None)
3458
.unwrap();
3559

36-
// Now fuzz the receive_garbage function with arbitrary data
37-
match initiator.receive_garbage(data) {
38-
Ok(GarbageResult::FoundGarbage {
39-
handshake: _,
40-
consumed_bytes,
41-
}) => {
42-
// Successfully found garbage terminator
43-
// Verify consumed_bytes is reasonable
44-
assert!(consumed_bytes <= data.len());
45-
assert!(consumed_bytes >= 16); // At least the terminator size
46-
47-
// The garbage should be everything before the terminator
48-
let garbage_len = consumed_bytes - 16;
49-
assert!(garbage_len <= 4095); // Max garbage size
50-
}
51-
Ok(GarbageResult::NeedMoreData(_)) => {
52-
// Need more data - valid outcome for short inputs
53-
// This should happen when:
54-
// 1. Buffer is too short to contain terminator
55-
// 2. Buffer doesn't contain the terminator yet
56-
}
57-
Err(_) => {
58-
// Error parsing garbage - valid outcome
59-
// This should happen when:
60-
// 1. No terminator found within max garbage size
61-
}
62-
}
60+
// Create realistic test case: fuzz_data + real_terminator
61+
let mut realistic_input = data.to_vec();
62+
realistic_input.extend_from_slice(responder_terminator);
63+
64+
// Test the receive_garbage function with realistic input
65+
let _ = initiator.receive_garbage(&realistic_input);
6366
});

protocol/fuzz/fuzz_targets/receive_key.rs

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,47 @@
77
#![no_main]
88
use bip324::{Handshake, Initialized, Network, Role};
99
use libfuzzer_sys::fuzz_target;
10+
use rand::SeedableRng;
1011

1112
fuzz_target!(|data: &[u8]| {
12-
// Skip if data is not exactly 64 bytes
13+
// We need exactly 64 bytes for an ElligatorSwift key.
14+
// This gives the fuzzer a clear signal about the expected input size.
1315
if data.len() != 64 {
1416
return;
1517
}
1618

17-
// Set up a handshake in the SentKey state
18-
let handshake = Handshake::<Initialized>::new(Network::Bitcoin, Role::Initiator).unwrap();
19+
// Use a fixed seed to make the fuzzing deterministic.
20+
// This ensures same input always produces same result.
21+
let seed = [42u8; 32];
22+
let mut rng = rand::rngs::StdRng::from_seed(seed);
23+
let secp = secp256k1::Secp256k1::signing_only();
24+
25+
// Set up a handshake in the SentKey state.
26+
let handshake =
27+
Handshake::<Initialized>::new_with_rng(Network::Bitcoin, Role::Initiator, &mut rng, &secp)
28+
.unwrap();
1929
let mut key_buffer = vec![0u8; Handshake::<Initialized>::send_key_len(None)];
2030
let handshake = handshake.send_key(None, &mut key_buffer).unwrap();
2131

22-
// Fuzz the receive_key function with arbitrary 64-byte data
32+
// Convert the data to the required array format.
2333
let mut key_bytes = [0u8; 64];
2434
key_bytes.copy_from_slice(data);
2535

2636
match handshake.receive_key(key_bytes) {
2737
Ok(_handshake) => {
28-
// Successfully processed the key
29-
// This means:
30-
// 1. The 64 bytes represent a valid ElligatorSwift encoding
31-
// 2. The ECDH operation succeeded
32-
// 3. The key derivation worked
33-
// 4. It's not the V1 protocol magic bytes
38+
// Successfully processed the key.
39+
//
40+
// 1. The 64 bytes represent a valid ElligatorSwift encoding.
41+
// 2. The ECDH operation succeeded.
42+
// 3. The key derivation worked.
43+
// 4. It's not the V1 protocol magic bytes.
3444
}
3545
Err(_) => {
36-
// Failed to process the key
37-
// This could be:
38-
// 1. Invalid ElligatorSwift encoding
39-
// 2. V1 protocol detected (first 4 bytes match network magic)
40-
// 3. ECDH or key derivation failure
46+
// Failed to process the key.
47+
//
48+
// 1. Invalid ElligatorSwift encoding.
49+
// 2. V1 protocol detected (first 4 bytes match network magic).
50+
// 3. ECDH or key derivation failure.
4151
}
4252
}
4353
});

protocol/fuzz/fuzz_targets/receive_version.rs

Lines changed: 0 additions & 70 deletions
This file was deleted.

0 commit comments

Comments
 (0)