|
| 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