Skip to content

Commit bf1b66d

Browse files
Merge pull request #1305 from dykdee/feat/fuzz-harness-generator-415
feat(engine): Fuzz-Harness Generator — export native AFL/honggfuzz test scaffolds
2 parents 326afe7 + 290cc2f commit bf1b66d

12 files changed

Lines changed: 1351 additions & 11 deletions

File tree

DOCUMENTATION_INDEX.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@
6969
- Cross-contract message wire format stability
7070
- Local reproduction recipes
7171

72+
### Fuzz-Harness Generator
73+
74+
**[docs/fuzz-harness-generator.md](docs/fuzz-harness-generator.md)** - `sanctifier harness` CLI command
75+
76+
- Generates native `afl.rs` / `honggfuzz` fuzz-target scaffolds from a contract's ABI
77+
- Bridges static analysis (AST-level function/parameter extraction) to dynamic analysis
78+
- `SorobanArbitrary`-based input generation, crate auto-detection, usage examples
79+
7280
### Technical Architecture
7381

7482
**[ARCHITECTURE.md](ARCHITECTURE.md)** - System design and components

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ sanctifier diff [PATH] --baseline <report.json> # new/resolved findings
221221
sanctifier watch [PATH] # re-runs on file change
222222
sanctifier workspace [PATH] # cargo-workspace-aware scan
223223
sanctifier callgraph [PATH] --output callgraph.dot
224+
sanctifier harness [PATH] --output fuzz-harness --target afl|honggfuzz|both
224225
sanctifier badge --report report.json --svg-output sanctifier.svg
225226
sanctifier fix [PATH] --rule S003 # apply patcher fixes
226227
sanctifier verify [PATH] # Z3-only invariant pass

docs/fuzz-harness-generator.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Fuzz-Harness Generator (`sanctifier harness`)
2+
3+
## Overview
4+
5+
`sanctifier harness` bridges Sanctifier's *static* analysis (AST-level
6+
contract/ABI extraction) to *dynamic* analysis by generating ready-to-build
7+
native fuzz-target scaffolds for a Soroban contract source file — no manual
8+
harness-writing required.
9+
10+
Given a contract source file, the command:
11+
12+
1. Parses the file and discovers every `#[contractimpl]` block
13+
([`sanctifier_core::harness_spec`](../tooling/sanctifier-core/src/harness_spec.rs)).
14+
2. For each public, non-reserved function (i.e. excluding `__constructor` and
15+
`__check_auth`), extracts its typed parameter list (skipping the
16+
mandatory leading `Env` parameter).
17+
3. Emits one fuzz target per function for each requested backend
18+
(`afl.rs`, `honggfuzz`, or both), plus a self-contained `Cargo.toml` per
19+
backend.
20+
21+
This targets the same invariant class as the hand-written harnesses
22+
described in [`docs/contracts-fuzz.md`](contracts-fuzz.md) — "does this
23+
contract entry point ever panic on attacker-controlled input?" — but removes
24+
the need to hand-write a harness for every function.
25+
26+
## Usage
27+
28+
```bash
29+
sanctifier harness path/to/contract.rs \
30+
--output fuzz-harness \
31+
--target both # afl | honggfuzz | both (default)
32+
--function transfer # optional: restrict to one function
33+
```
34+
35+
This produces:
36+
37+
```
38+
fuzz-harness/
39+
├── afl/
40+
│ ├── Cargo.toml
41+
│ └── src/bin/
42+
│ ├── token_transfer.rs
43+
│ └── token_balance.rs
44+
└── honggfuzz/
45+
├── Cargo.toml
46+
└── src/bin/
47+
├── token_transfer.rs
48+
└── token_balance.rs
49+
```
50+
51+
Each `afl/` or `honggfuzz/` directory is an independent, workspace-excluded
52+
Cargo package (`[workspace]` with no members — the same convention already
53+
used by `contracts/my-contract/fuzz/Cargo.toml`), so it can be built without
54+
disturbing the analyzed contract's own workspace:
55+
56+
```bash
57+
cd fuzz-harness/afl
58+
cargo afl build
59+
cargo afl fuzz -i in -o out target/debug/token_transfer
60+
```
61+
62+
```bash
63+
cd fuzz-harness/honggfuzz
64+
cargo hfuzz build
65+
cargo hfuzz run token_transfer
66+
```
67+
68+
## How inputs are generated
69+
70+
Every generated target follows the pattern documented by
71+
[`soroban_sdk::testutils::arbitrary`](https://docs.rs/soroban-sdk/latest/soroban_sdk/testutils/arbitrary/index.html)
72+
for fuzzing host-managed contract types: each parameter becomes a
73+
`<ParamType as SorobanArbitrary>::Prototype` field on a derived `Arbitrary`
74+
struct, and the harness body converts each prototype into its real Soroban
75+
value with `.into_val(&env)` before calling `client.try_<function>(..)`. For
76+
example, `transfer(env: Env, from: Address, to: Address, amount: i128)`
77+
generates:
78+
79+
```rust
80+
#[derive(Debug, Arbitrary)]
81+
struct FuzzInput {
82+
from: <Address as SorobanArbitrary>::Prototype,
83+
to: <Address as SorobanArbitrary>::Prototype,
84+
amount: <i128 as SorobanArbitrary>::Prototype,
85+
}
86+
87+
fn main() {
88+
fuzz!(|input: FuzzInput| {
89+
let env = Env::default();
90+
env.mock_all_auths();
91+
let contract_id = env.register_contract(None, Token);
92+
let client = TokenClient::new(&env, &contract_id);
93+
94+
let from: Address = input.from.into_val(&env);
95+
let to: Address = input.to.into_val(&env);
96+
let amount: i128 = input.amount.into_val(&env);
97+
98+
let _ = client.try_transfer(&from, &to, &amount);
99+
});
100+
}
101+
```
102+
103+
`try_<function>` (rather than `<function>`) is used so the fuzzer treats
104+
unexpected host-level panics as crashes while expected `Err` returns (e.g.
105+
validation rejections) do not themselves count as findings; switch to the
106+
non-`try_` call if you want to fuzz for panics *and* logic-level error paths.
107+
108+
Custom `#[contracttype]` structs/enums used as parameters are fuzzable too:
109+
the Soroban SDK derives `SorobanArbitrary` for them automatically whenever
110+
the `testutils` feature is enabled, which is why every generated `Cargo.toml`
111+
enables `soroban-sdk`'s `testutils` feature.
112+
113+
## Crate auto-detection
114+
115+
`sanctifier harness` walks upward from the source file looking for the
116+
nearest `Cargo.toml` to discover the contract crate's package name, and adds
117+
it as a `path` dependency (with `features = ["testutils"]`) in the generated
118+
manifest, plus the matching `use <crate>::{Contract, ContractClient};` in
119+
each harness file. If no manifest is found, both are left as a `// TODO`
120+
placeholder for you to fill in.
121+
122+
> **Note:** the target contract crate must itself define (or you must add) a
123+
> `testutils` feature that turns on `soroban-sdk/testutils` — this is what
124+
> makes `SorobanArbitrary` available for the crate's own `#[contracttype]`
125+
> types. See the `soroban_sdk::testutils::arbitrary` module docs for details.
126+
127+
## Relationship to existing fuzz infrastructure
128+
129+
This command is a *generator*: it produces new, disposable scaffold crates
130+
next to a contract you're analyzing. It does not replace or modify the
131+
hand-written, CI-integrated harnesses described in
132+
[`docs/contracts-fuzz.md`](contracts-fuzz.md), which continue to run in
133+
`.github/workflows/contracts-fuzz.yml` as before.

0 commit comments

Comments
 (0)