Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add advanced fuzzer (#724) #733

Merged
merged 34 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5df2295
Add advanced fuzzer (#724)
maxammann May 13, 2024
03bd742
feat: Store example corpus in version control
netrome Sep 5, 2024
29166d8
chore: Instructions for how to generate a seed corpus
netrome Sep 5, 2024
c79a5a5
chore: Remove unused dependencies in binaries
netrome Sep 5, 2024
6b7b3c8
fix: Cargo fmt
netrome Sep 5, 2024
2ba6921
fix: Clippy
netrome Sep 6, 2024
5543f05
fix: Pass `fuzzing` as rustc flag when compiling fuzz binaries
netrome Sep 6, 2024
7b7f465
feat: Remove broken `grammar_aware` fuzz target in favor of `grammar_…
netrome Sep 6, 2024
c9b035b
feat: Default to LibAFL fuzzer
netrome Sep 6, 2024
3c6d209
docs: Improve code coverage instructions
netrome Sep 6, 2024
52fb706
feat: Use feature flags to toggle between libFuzzer and LibAFL
netrome Sep 6, 2024
8f678c7
docs: Further improvements to code coverage instructions
netrome Sep 6, 2024
3758757
docs: Clarify what the execute binary does
netrome Sep 6, 2024
6a4cf77
fix: Minor cleanup
netrome Sep 6, 2024
0f73e06
chore: Remove `arbitrary` dependency
netrome Sep 6, 2024
e2318fb
feat: Link investigation ticket to ignored assertions
netrome Sep 6, 2024
d560646
fix: Resurrect proptest arbitrary usage in `fuel-merkle` crate
netrome Sep 7, 2024
aabb71b
fix: Remove gas statistics output
netrome Sep 9, 2024
212dcf3
fix: typo in fuel-vm/fuzz/README.md
netrome Sep 9, 2024
89c6827
chore: Remove example corpus and update README.md
netrome Sep 9, 2024
86f7359
fix: Minor enhancements from PR review
netrome Sep 9, 2024
2f31272
fix: Remove redundant magic 256 term
netrome Sep 9, 2024
1b74698
fix: Cargo fmt
netrome Sep 9, 2024
ed5f70c
chore: Bump secp256k1 version and re-enable disabled code paths for f…
netrome Sep 10, 2024
fc8a6ca
chore: Add changelog entry
netrome Sep 10, 2024
e009748
fix: Clippy fix
netrome Sep 10, 2024
5b41f35
fix: Format
netrome Sep 10, 2024
fb72b5b
Update fuel-crypto/src/secp256/signature_format.rs
xgreenx Sep 10, 2024
470d02c
Merge branch 'master' into feature/tob-fuzzer
xgreenx Sep 10, 2024
0ae4ac2
Merge branch 'master' into feature/tob-fuzzer
netrome Sep 10, 2024
1c3f9d8
chore: Add supported rust version to fuzzer readme
netrome Sep 10, 2024
bc8dbf2
refactor: use `expect` instead of `unwrap` in a lot of places
netrome Sep 10, 2024
b7f0d0b
refactor: Apply review suggestions
netrome Sep 10, 2024
28fc3ae
fix: More unwrap -> expect
netrome Sep 10, 2024
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
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]

### Added

- [#670](https://github.com/FuelLabs/fuel-vm/pull/670): Add DA compression functionality to `Transaction` and any types within
- [#733](https://github.com/FuelLabs/fuel-vm/pull/733): Add LibAFL based fuzzer and update `secp256k1` version to 0.29.1.

### Changed

#### Breaking

- [#670](https://github.com/FuelLabs/fuel-vm/pull/670): The `predicate` field of `fuel_tx::input::Coin` is now a wrapper struct `PredicateCode`.

## [Version 0.56.0]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,4 @@ It returns `receipts` that contain result of execution. The `assert_panics` can
The `fuel-tx` provides `fuel_tx::TransactionBuilder` that simplifies the building
of custom transaction for testing purposes.

You can check how `TransactionBuilder::script` or `TransactionBuilder::create` are used for better understanding.
You can check how `TransactionBuilder::script` or `TransactionBuilder::create` are used for better understanding.
1 change: 0 additions & 1 deletion fuel-asm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ repository = { workspace = true }
description = "Atomic types of the FuelVM."

[dependencies]
arbitrary = { version = "1.1", features = ["derive"], optional = true }
bitflags = { workspace = true }
fuel-types = { workspace = true, default-features = false }
serde = { version = "1.0", default-features = false, features = ["derive"], optional = true }
Expand Down
2 changes: 0 additions & 2 deletions fuel-asm/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ crate::enum_try_from! {
#[cfg_attr(feature = "typescript", wasm_bindgen::prelude::wasm_bindgen)]
#[repr(u8)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
/// Argument list for GM (get metadata) instruction
/// The VM is the only who should match this struct, and it *MUST* always perform
/// exhaustive match so all offered variants are covered.
Expand Down Expand Up @@ -51,7 +50,6 @@ crate::enum_try_from! {
#[cfg_attr(feature = "typescript", wasm_bindgen::prelude::wasm_bindgen)]
#[repr(u16)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub enum GTFArgs {
/// Set `$rA` to `tx.type`
Type = 0x001,
Expand Down
1 change: 0 additions & 1 deletion fuel-asm/src/panic_instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use crate::{
#[cfg_attr(feature = "typescript", wasm_bindgen::prelude::wasm_bindgen)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(fuel_types::canonical::Deserialize, fuel_types::canonical::Serialize)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
/// Describe a panic reason with the instruction that generated it
pub struct PanicInstruction {
reason: PanicReason,
Expand Down
1 change: 0 additions & 1 deletion fuel-asm/src/panic_reason.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ enum_from! {
#[cfg_attr(feature = "typescript", wasm_bindgen::prelude::wasm_bindgen)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(fuel_types::canonical::Serialize, fuel_types::canonical::Deserialize)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[repr(u8)]
#[non_exhaustive]
/// Panic reason representation for the interpreter.
Expand Down
5 changes: 4 additions & 1 deletion fuel-crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ p256 = { version = "0.13", default-features = false, features = ["digest", "ecd
rand = { version = "0.8", default-features = false, optional = true }
# `rand-std` is used to further protect the blinders from side-channel attacks and won't compromise
# the deterministic arguments of the signature (key, nonce, message), as defined in the RFC-6979
secp256k1 = { version = "0.26", default-features = false, features = ["rand-std", "recovery"], optional = true }
secp256k1 = { version = "0.29.1", default-features = false, features = ["rand-std", "recovery"], optional = true }
serde = { version = "1.0", default-features = false, features = ["derive"], optional = true }
sha2 = { version = "0.10", default-features = false }
zeroize = { version = "1.5", features = ["derive"] }
Expand All @@ -41,6 +41,9 @@ serde = ["dep:serde", "fuel-types/serde"]
std = ["alloc", "coins-bip32", "secp256k1", "coins-bip39", "fuel-types/std", "lazy_static", "rand?/std_rng", "serde?/default"]
test-helpers = []

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }

[[bench]]
name = "signature"
harness = false
Expand Down
4 changes: 2 additions & 2 deletions fuel-crypto/benches/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ fn signatures(c: &mut Criterion) {

let public = PublicKey::from_secret_key(&secp, &key);
let message = fuel_crypto::Message::new(message);
let message =
Message::from_slice(message.as_ref()).expect("failed to create secp message");
let message = Message::from_digest_slice(message.as_ref())
.expect("failed to create secp message");
let signature = secp_signing.sign_ecdsa(&message, &key);
let recoverable = secp.sign_ecdsa_recoverable(&message, &key);

Expand Down
2 changes: 1 addition & 1 deletion fuel-crypto/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,6 @@ impl fmt::Display for Message {
#[cfg(feature = "std")]
impl From<&Message> for secp256k1::Message {
fn from(message: &Message) -> Self {
secp256k1::Message::from_slice(&*message.0).expect("length always matches")
secp256k1::Message::from_digest_slice(&*message.0).expect("length always matches")
}
}
3 changes: 3 additions & 0 deletions fuel-tx/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,6 @@ alloc = ["hashbrown", "fuel-types/alloc", "itertools/use_alloc", "derivative", "
# serde is requiring alloc because its mandatory for serde_json. to avoid adding a new feature only for serde_json, we just require `alloc` here since as of the moment we don't have a use case of serde without alloc.
serde = ["alloc", "fuel-asm/serde", "fuel-crypto/serde", "fuel-merkle/serde", "serde_json", "hashbrown/serde", "bitflags/serde"]
da-compression = ["serde", "fuel-compression"]

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
4 changes: 3 additions & 1 deletion fuel-vm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ std = [
"itertools/use_std",
]
alloc = ["fuel-asm/alloc", "fuel-tx/alloc", "fuel-tx/alloc"]
arbitrary = ["fuel-asm/arbitrary"]
profile-gas = ["profile-any"]
profile-coverage = ["profile-any"]
profile-any = ["dyn-clone"] # All profiling features should depend on this
Expand All @@ -109,3 +108,6 @@ test-helpers = [
"tai64",
"fuel-crypto/test-helpers",
]

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
2 changes: 2 additions & 0 deletions fuel-vm/fuzz/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build]
rustflags = "--cfg fuzzing"
31 changes: 25 additions & 6 deletions fuel-vm/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,41 @@ name = "fuel-vm-fuzz"
version = "0.0.0"
authors = ["Automatically generated"]
publish = false
edition = "2018"
edition = "2021"

[package.metadata]
cargo-fuzz = true

[dependencies]
arbitrary = { version = "1.0", features = ["derive"] }
fuel-vm = { path = "..", features = ["arbitrary"] }
libfuzzer-sys = "0.4"
fuel-vm = { path = "..", features = ["test-helpers"] }
clap = { version = "4.0", features = ["derive"] }
hex = "*"

[features]
default = ["libfuzzer"]
libfuzzer = ["libfuzzer-sys"]
libafl = ["libafl_libfuzzer"]

[dependencies.libfuzzer-sys]
version = "0.4"
optional = true

[dependencies.libafl_libfuzzer]
version = "0.13"
optional = true

# Prevent this from interfering with workspaces as this crate requires unstable features.
[workspace]
members = ["."]

[profile.release]
panic = 'abort'

[profile.dev]
panic = 'abort'

[[bin]]
name = "grammar_aware"
path = "fuzz_targets/grammar_aware.rs"
name = "grammar_aware_advanced"
path = "fuzz_targets/grammar_aware_advanced.rs"
test = false
doc = false
99 changes: 99 additions & 0 deletions fuel-vm/fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Fuzz test for the Fuel VM
This crate provides the `grammar_aware_advanced` fuzz target which can be run with `cargo fuzz` to fuzz test the Fuel VM.

General information about fuzzing Rust can be found on [appsec.guide](https://appsec.guide/docs/fuzzing/rust/cargo-fuzz/).

### Installation
The fuzzer requires nightly rust and works with rustc version `1.82.0-nightly`. To be able to run the fuzzer, the following tools must be installed.

Install:
```
cargo install cargo-fuzz
apt install clang pkg-config libssl-dev # for LibAFL
rustup component add llvm-tools-preview --toolchain nightly
```

### Seeds

The input to the fuzzer is a byte vector that contains script assembly, script data, and the assembly of a contract to be called. Each of these is separated by a 64-bit magic value `0x00ADBEEF5566CEAA`.

While the fuzzer can be started without any seeds, it is recommended to generate seeds from compiled sway programs.

#### Generate your own seeds

If you want to run the fuzzer with custom input, you can run the `seed` binary against a directory of compiled sway programs.

```
cargo run --bin seed <input dir> <output dir>
```

#### Example: Generating a corpus from the sway examples
This section explains how to use the [sway examples](https://github.com/FuelLabs/sway/tree/master/examples) to generate an initial corpus.

This can be acieved by doing the following:

1. Compile the sway examples with `forc`.
```
# In sway/examples
forc build
```

2. Gather all the resulting binaries in a temporary directory (for example `/tmp/corpus`).
```
# In sway/examples
for file in $(find . -name "*.bin" | rg debug); do cp $file /tmp/corpus; done
```

3. Run the `seed binary` against the generated binaries
```
# In fuel-vm/fuel-vm/fuzz
mkdir generated_seeds
cargo run --bin seed /tmp/corpus ./generated_seeds
```

Now the directory `./generated_seeds` contains the newly generated seeds. Copy this over to `corpus/grammar_aware_advanced` to run the fuzzer with these seeds.

### Running the Fuzzer
The Rust nightly version is required for executing cargo-fuzz. The simplest way to run the fuzzer is to run the following command:
```
cargo +nightly fuzz run grammar_aware_advanced
```

However, we recommend adding a few flags to the command to improve fuzzing efficiency. First, we can add `--no-default-features --features libafl` to ensure we use the LibAFL fuzzer instead of the default libFuzzer. Secondly, we can set `--sanitizer none` to disable AddressSanitizer for a significant speed improvement, as we do not expect memory issues in a Rust program that does not use a significant amount of unsafe code. This has been confirmed by a ToB [cargo-geiger](https://github.com/rust-secure-code/cargo-geiger) analysis showed. It makes sense to leave AddressSanitizer turned on if we use more unsafe Rust in the future (either directly or through dependencies). Finally, the `-ignore_crashes=1 -ignore_timeouts=1 -ignore_ooms=1 -fork=7` flags are useful to ensure a smooth LibAFL experience utilizing 7 cores.

Putting this together we arrive at the following command.
```
cargo +nightly fuzz run --no-default-features --features libafl --sanitizer none grammar_aware_advanced -- -ignore_crashes=1 -ignore_timeouts=1 -ignore_ooms=1 -fork=7
```

### Generate Coverage
It is important to measure a fuzzing campaign’s coverage after its run. To perform this measurement, we can use tools provided by cargo-fuzz and [rustc](https://doc.rust-lang.org/stable/rustc/instrument-coverage.html). First, install [cargo-binutils](https://github.com/rust-embedded/cargo-binutils#installation). After that, execute the following command:
```
cargo +nightly fuzz coverage grammar_aware_advanced corpus/grammar_aware_advanced
```

The code coverage report can now be displayed with the following command:

```
cargo cov -- report target/x86_64-unknown-linux-gnu/coverage/x86_64-unknown-linux-gnu/release/grammar_aware_advanced -instr-profile=coverage/grammar_aware_advanced/coverage.profdata
```

We can also generate a HTML visualization of the code coverage using the following command:

```
cargo cov -- show target/x86_64-unknown-linux-gnu/coverage/x86_64-unknown-linux-gnu/release/grammar_aware_advanced --format=html -instr-profile=coverage/grammar_aware_advanced/coverage.profdata $(pwd | sed "s/fuel-vm\/fuzz//") > index.html
```

### Execute a Test Case
The fuzzing campain will output any crashes to `artifacts/grammar_aware_advanced`. To further investigate these crashes, the `execute` binary can be used.
```
cargo run --bin execute artifacts/grammar_aware_advanced/<crash file>
```

This is useful for triaging issues.

### Collect Gas Statistics
The `collect` binary writes gas statistics to a file called gas_statistics.csv. This can be used to analyze the execution time versus gas usage on a test corpus.
```
cargo run --bin collect
```
39 changes: 0 additions & 39 deletions fuel-vm/fuzz/fuzz_targets/grammar_aware.rs

This file was deleted.

13 changes: 13 additions & 0 deletions fuel-vm/fuzz/fuzz_targets/grammar_aware_advanced.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#![no_main]

#[cfg(feature = "libafl")]
extern crate libafl_libfuzzer as libfuzzer_sys;

use fuel_vm_fuzz::{decode, execute};
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
if let Some(data) = decode(data) {
execute(data);
}
});
39 changes: 39 additions & 0 deletions fuel-vm/fuzz/src/bin/collect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use std::fs::File;
use fuel_vm_fuzz::execute;
use fuel_vm_fuzz::decode;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::time::Instant;

fn main() {
let path = std::env::args().nth(1).expect("no path given");
let mut file = File::create("gas_statistics.csv").expect("couldn't create a file to write gas statistics to");

write!(file, "name\tgas\ttime_ms\n").unwrap();

if Path::new(&path).is_file() {
eprintln!("Pass directory")
} else {
let paths = fs::read_dir(path).expect("unable to read dir");

for path in paths {
let entry = path.expect("unable to yield entry");
let data = std::fs::read(entry.path()).expect("unable to read path {}");
let name = entry.file_name();
let name = name.to_str().expect("failed to read file name as string");
println!("{:?}", name);

let Some(data) = decode(&data) else { eprintln!("unable to decode"); continue; };

let now = Instant::now();
let result = execute(data);
let gas = result.gas_used;

write!(file, "{name}\t{gas}\t{}\n", now.elapsed().as_millis()).expect("unable to write to gas statistics file");
if result.success {
println!("{:?}:{}", name, result.success);
}
}
}
}
Loading
Loading