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 ClusterFuzzLite for CI fuzzing #727

Closed
wants to merge 16 commits into from
Closed
4 changes: 4 additions & 0 deletions .clusterfuzzlite/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM gcr.io/oss-fuzz-base/base-builder-rust:v1
COPY . $SRC/fuel-vm
WORKDIR fuel-vm
COPY .clusterfuzzlite/build.sh $SRC/
5 changes: 5 additions & 0 deletions .clusterfuzzlite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# ClusterFuzzLite

This directory contains the configuration for [ClusterFuzzLite](https://google.github.io/clusterfuzzlite/). CFL is used in the GitHub Actions CI in several workflows.

The corpus and more documentation can be found in [FuelLabs/fuel-fuzzing-corpus](https://github.com/FuelLabs/fuel-fuzzing-corpus).
10 changes: 10 additions & 0 deletions .clusterfuzzlite/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash -eu

cd $SRC/fuel-vm

cd fuel-vm

export CARGO_CFG_CURVE25519_DALEK_BACKEND=serial # This fixes building on nightly-2023-12-28-x86_64-unknown-linux-gnu, which is no longer compatible with the SIMD feature of curve25519; building on stable does not work because ASan is a dependency of coverage
cargo fuzz build -O --sanitizer none

cp fuzz/target/x86_64-unknown-linux-gnu/release/grammar_aware_advanced $OUT/
1 change: 1 addition & 0 deletions .clusterfuzzlite/project.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
language: rust
25 changes: 25 additions & 0 deletions .github/workflows/cflite_batch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: ClusterFuzzLite batch fuzzing
on:
schedule:
- cron: '0 0/24 * * *' # Every 24th hour
permissions: read-all
jobs:
BatchFuzzing:
runs-on: ubuntu-latest
steps:
- name: Build Fuzzers (${{ matrix.sanitizer }})
id: build
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
with:
language: rust
- name: Run Fuzzers (${{ matrix.sanitizer }})
id: run
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
fuzz-seconds: 3600
mode: 'batch'
output-sarif: true
storage-repo: https://${{ secrets.FUZZ_STORAGE_PAT }}@github.com/FuelLabs/fuel-fuzzing-corpus.git
storage-repo-branch: main
storage-repo-branch-coverage: gh-pages
45 changes: 45 additions & 0 deletions .github/workflows/cflite_cron.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: ClusterFuzzLite cron tasks
on:
schedule:
- cron: '0 0/72 * * *'
permissions: read-all
jobs:
Pruning:
runs-on: ubuntu-latest
steps:
- name: Build Fuzzers
id: build
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
with:
language: rust
- name: Run Fuzzers
id: run
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
fuzz-seconds: 600 # Time after which minimization is aborted
mode: 'prune'
output-sarif: true
storage-repo: https://${{ secrets.FUZZ_STORAGE_PAT }}@github.com/FuelLabs/fuel-fuzzing-corpus.git
storage-repo-branch: main
storage-repo-branch-coverage: gh-pages
Coverage:
runs-on: ubuntu-latest
steps:
- name: Build Fuzzers
id: build
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
with:
language: rust
sanitizer: coverage
- name: Run Fuzzers
id: run
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
fuzz-seconds: 600
mode: 'coverage'
sanitizer: 'coverage'
storage-repo: https://${{ secrets.FUZZ_STORAGE_PAT }}@github.com/FuelLabs/fuel-fuzzing-corpus.git
storage-repo-branch: main
storage-repo-branch-coverage: gh-pages
30 changes: 30 additions & 0 deletions .github/workflows/cflite_pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: ClusterFuzzLite PR fuzzing
on:
pull_request:
paths:
- '**'
permissions: read-all
jobs:
PR:
runs-on: ubuntu-latest
steps:
- name: Build Fuzzers
id: build
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
with:
language: rust
github-token: ${{ secrets.GITHUB_TOKEN }}
storage-repo: https://${{ secrets.FUZZ_STORAGE_PAT }}@github.com/FuelLabs/fuel-fuzzing-corpus.git
storage-repo-branch: main
storage-repo-branch-coverage: gh-pages
- name: Run Fuzzers
id: run
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
fuzz-seconds: 600
mode: 'code-change'
output-sarif: true
storage-repo: https://${{ secrets.FUZZ_STORAGE_PAT }}@github.com/FuelLabs/fuel-fuzzing-corpus.git
storage-repo-branch: main
storage-repo-branch-coverage: gh-pages
1 change: 1 addition & 0 deletions fuel-asm/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,7 @@ macro_rules! impl_instructions {
/// Solely the opcode portion of an instruction represented as a single byte.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[repr(u8)]
pub enum Opcode {
$(
Expand Down
2 changes: 2 additions & 0 deletions fuel-crypto/src/secp256/signature_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ impl TryFrom<secp256k1::ecdsa::RecoveryId> for RecoveryId {
/// Panics if the highest bit of byte at index 32 is set, as this indicates non-normalized
/// signature. Panics if the recovery id is in reduced-x form.
pub fn encode_signature(mut signature: [u8; 64], recovery_id: RecoveryId) -> [u8; 64] {
// This assertion is hit during fuzzing. Verify it is safe to disable.
#[cfg(not(any(fuzzing, feature = "test-helpers")))]
assert!(signature[32] >> 7 == 0, "Non-normalized signature");
let v = recovery_id.is_y_odd as u8;

Expand Down
2 changes: 2 additions & 0 deletions fuel-tx/src/transaction/validity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ impl Input {
recover_address()?
};

// This error is reached during fuzzing often, verify it is safe to disable it
#[cfg(not(any(fuzzing, feature = "test-helpers")))]
if owner != &recovered_address {
return Err(ValidityError::InputInvalidSignature { index });
}
Expand Down
32 changes: 30 additions & 2 deletions fuel-vm/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,50 @@ 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"] }
fuel-vm = { path = "..", features = ["arbitrary", "test-helpers"] }
# For LibAFL: libfuzzer-sys = { features = ["arbitrary-derive"], package = "libafl_libfuzzer" }
libfuzzer-sys = "0.4"
clap = { version = "4.0", features = ["derive"] }
hex = "*"

# 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"
test = false
doc = false

[[bin]]
name = "grammar_aware_advanced"
path = "fuzz_targets/grammar_aware_advanced.rs"
test = false
doc = false

[[bin]]
name = "seed"
path = "src/seed.rs"

[[bin]]
name = "execute"
path = "src/execute.rs"

[[bin]]
name = "collect"
path = "src/collect.rs"

58 changes: 58 additions & 0 deletions fuel-vm/fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@

## Manual for grammar_aware_advanced fuzzer


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

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


### Generate Seeds

It is necessary to first convert Sway programs into a suitable format for use as seed input to the fuzzer. This can be done with the following command:
```
cargo run --bin seed <input dir> <output dir>
```

### Running the Fuzzer
The Rust nightly version is required for executing cargo-fuzz. We also 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, which our [cargo-geiger](https://github.com/rust-secure-code/cargo-geiger) analysis showed. It makes sense to leave AddressSanitizer turned on if the Fuel project uses more unsafe Rust in the future (either directly or through dependencies). The remaining flags are either required for LibAFL or are useful to make it use seven cores.
```
cargo +nightly fuzz run --sanitizer none grammar_aware -- \
-ignore_crashes=1 -ignore_timeouts=1 -ignore_ooms=1 -fork=7
```

If you use libfuzzer (default) then the following command is enough:

```
cargo fuzz run --sanitizer none grammar_aware
```

### Execute a Test Case
Test cases can be executed using the following command. This is useful for triaging issues.
```
cd fuzz/
cargo run --bin execute <file/dir>
```

### Collect Statistics
We created a tool that 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 <file/dir>
```

### Generate Coverage
Regardless of how inputs are generated, it is important to measure a fuzzing campaign’s coverage after its run. To perform this measure, we used the support 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 corpus/grammar_aware
```
Finally, generate an HTML file using LLVM:

```
cargo cov -- show
target/x86_64-unknown-linux-gnu/coverage/x86_64-unknown-linux-gnu/release/grammar_aware --format=html -instr-profile=coverage/grammar_aware/coverage.profdata /root/audit/fuel-vm > index.html
```
20 changes: 11 additions & 9 deletions fuel-vm/fuzz/fuzz_targets/grammar_aware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
use std::hint::black_box;

use libfuzzer_sys::fuzz_target;
use fuel_vm::fuel_tx::field::MaxFeeLimit;

use fuel_vm::prelude::*;
use fuel_vm::prelude::policies::Policies;

#[derive(arbitrary::Arbitrary, Debug)]
struct FuzzData {
Expand All @@ -18,22 +20,22 @@ fuzz_target!(|data: FuzzData| {

let gas_price = 0;
let gas_limit = 1_000;
let maturity = Default::default();
let height = Default::default();
let params = ConsensusParameters::DEFAULT;
let params = ConsensusParameters::standard();

let tx = Transaction::script(
gas_price,
let mut tx = Transaction::script(
gas_limit,
maturity,
data.program.iter().copied().collect(),
data.program.iter().copied().map(|op| op as u8).collect::<Vec<u8>>(),
data.script_data,
Policies::new(),
vec![],
vec![],
vec![],
)
.into_checked(height, &params)
.expect("failed to generate a checked tx");
);

tx.set_max_fee_limit(1_000);

let tx = tx.into_checked(height, &params).expect("failed to generate a checked tx");

drop(black_box(client.transact(tx)));
});
10 changes: 10 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,10 @@
#![no_main]

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

fuzz_target!(|data: &[u8]| {
if let Some(data) = decode(data) {
execute(data);
}
});
50 changes: 50 additions & 0 deletions fuel-vm/fuzz/src/collect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use crate::fs::File;
use fuel_vm::consts::WORD_SIZE;
use fuel_vm::fuel_asm::op;
use fuel_vm::fuel_asm::RegId;
use fuel_vm::fuel_asm::{Instruction, RawInstruction};
use fuel_vm::fuel_crypto::rand::Rng;
use fuel_vm::fuel_crypto::rand::SeedableRng;
use fuel_vm::fuel_types::Word;
use fuel_vm::prelude::SecretKey;
use fuel_vm_fuzz::execute;
use fuel_vm_fuzz::FuzzData;
use fuel_vm_fuzz::{decode, decode_instructions, encode};
use std::convert::TryFrom;
use std::convert::TryInto;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
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").unwrap();

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

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

for path in paths {
let entry = path.unwrap();
let data = std::fs::read(entry.path()).unwrap();
let name = entry.file_name();
let name = name.to_str().unwrap();
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()).unwrap();
if result.success {
println!("{:?}:{}", name, result.success);
}
}
}
}
Loading
Loading