mirror of
https://github.com/logos-blockchain/lez-fuzzing.git
synced 2026-06-07 11:39:30 +00:00
fix: add new fuzz targets
- template for adding targets
This commit is contained in:
parent
1cb6d314f1
commit
8bd0a1a612
17
.github/workflows/fuzz.yml
vendored
17
.github/workflows/fuzz.yml
vendored
@ -25,6 +25,11 @@ jobs:
|
||||
- fuzz_stateless_verification
|
||||
- fuzz_state_transition
|
||||
- fuzz_block_verification
|
||||
- fuzz_encoding_roundtrip
|
||||
- fuzz_signature_verification
|
||||
- fuzz_replay_prevention
|
||||
- fuzz_state_diff_computation
|
||||
- fuzz_validate_execute_consistency
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@ -78,6 +83,11 @@ jobs:
|
||||
- fuzz_stateless_verification
|
||||
- fuzz_state_transition
|
||||
- fuzz_block_verification
|
||||
- fuzz_encoding_roundtrip
|
||||
- fuzz_signature_verification
|
||||
- fuzz_replay_prevention
|
||||
- fuzz_state_diff_computation
|
||||
- fuzz_validate_execute_consistency
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout logos-execution-zone alongside lez-fuzzing
|
||||
@ -130,7 +140,12 @@ jobs:
|
||||
fuzz_transaction_decoding \
|
||||
fuzz_stateless_verification \
|
||||
fuzz_state_transition \
|
||||
fuzz_block_verification; do
|
||||
fuzz_block_verification \
|
||||
fuzz_encoding_roundtrip \
|
||||
fuzz_signature_verification \
|
||||
fuzz_replay_prevention \
|
||||
fuzz_state_diff_computation \
|
||||
fuzz_validate_execute_consistency; do
|
||||
echo "=== $target ===" | tee -a perf_baseline.txt
|
||||
cargo fuzz run "$target" -- -max_total_time=30 2>&1 \
|
||||
| grep -E "exec/s|execs_per_sec" | tail -1 | tee -a perf_baseline.txt
|
||||
|
||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1738,6 +1738,7 @@ dependencies = [
|
||||
"borsh",
|
||||
"common",
|
||||
"nssa",
|
||||
"nssa_core",
|
||||
"proptest",
|
||||
"testnet_initial_state",
|
||||
]
|
||||
|
||||
83
Justfile
83
Justfile
@ -1,19 +1,30 @@
|
||||
# ── Fuzzing ───────────────────────────────────────────────────────────────────
|
||||
export RISC0_DEV_MODE := "1"
|
||||
|
||||
# Run all fuzz targets for TIME seconds each (default: 30)
|
||||
fuzz TIME="30":
|
||||
cargo fuzz run fuzz_transaction_decoding -- -max_total_time={{TIME}}
|
||||
cargo fuzz run fuzz_stateless_verification -- -max_total_time={{TIME}}
|
||||
cargo fuzz run fuzz_state_transition -- -max_total_time={{TIME}}
|
||||
cargo fuzz run fuzz_block_verification -- -max_total_time={{TIME}}
|
||||
# List all registered fuzz targets (reads fuzz/Cargo.toml via cargo-fuzz)
|
||||
list-targets:
|
||||
cargo fuzz list
|
||||
|
||||
# Re-run the saved corpus (regression mode, no new mutations)
|
||||
# Run all fuzz targets for TIME seconds each (default: 30).
|
||||
# Targets are discovered automatically from fuzz/Cargo.toml — no edit needed here
|
||||
# when a new [[bin]] entry is added.
|
||||
fuzz TIME="30":
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
for target in $(cargo fuzz list 2>/dev/null); do
|
||||
echo "=== fuzzing $target for {{TIME}}s ==="
|
||||
cargo fuzz run "$target" -- -max_total_time={{TIME}}
|
||||
done
|
||||
|
||||
# Re-run the saved corpus for every target (regression mode, no new mutations)
|
||||
fuzz-regression:
|
||||
cargo fuzz run fuzz_transaction_decoding fuzz/corpus/fuzz_transaction_decoding -- -runs=0
|
||||
cargo fuzz run fuzz_stateless_verification fuzz/corpus/fuzz_stateless_verification -- -runs=0
|
||||
cargo fuzz run fuzz_state_transition fuzz/corpus/fuzz_state_transition -- -runs=0
|
||||
cargo fuzz run fuzz_block_verification fuzz/corpus/fuzz_block_verification -- -runs=0
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
for target in $(cargo fuzz list 2>/dev/null); do
|
||||
echo "=== regression $target ==="
|
||||
mkdir -p "fuzz/corpus/$target"
|
||||
cargo fuzz run "$target" "fuzz/corpus/$target" -- -runs=0
|
||||
done
|
||||
|
||||
# Minimise a crash artifact
|
||||
# Usage: just fuzz-tmin fuzz_state_transition fuzz/artifacts/fuzz_state_transition/crash-XXX
|
||||
@ -30,18 +41,58 @@ update-lez:
|
||||
|
||||
# ── Corpus management ─────────────────────────────────────────────────────────
|
||||
|
||||
# Minimise the corpus for all four targets (removes dominated inputs)
|
||||
# Minimise the corpus for all targets (removes dominated inputs)
|
||||
corpus-cmin:
|
||||
cargo fuzz cmin fuzz_transaction_decoding
|
||||
cargo fuzz cmin fuzz_stateless_verification
|
||||
cargo fuzz cmin fuzz_state_transition
|
||||
cargo fuzz cmin fuzz_block_verification
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
for target in $(cargo fuzz list 2>/dev/null); do
|
||||
echo "=== cmin $target ==="
|
||||
cargo fuzz cmin "$target"
|
||||
done
|
||||
|
||||
# Minimise the corpus for a single target
|
||||
# Usage: just corpus-cmin-target fuzz_state_transition
|
||||
corpus-cmin-target TARGET:
|
||||
cargo fuzz cmin {{TARGET}}
|
||||
|
||||
# ── Adding a new target ───────────────────────────────────────────────────────
|
||||
|
||||
# Scaffold a new fuzz target — fully automated, no manual edits required.
|
||||
#
|
||||
# Steps performed automatically:
|
||||
# 1. Creates fuzz/corpus/<TARGET>/
|
||||
# 2. Copies fuzz/fuzz_targets/_template.rs → fuzz/fuzz_targets/<TARGET>.rs
|
||||
# 3. Appends the [[bin]] entry to fuzz/Cargo.toml
|
||||
# 4. Inserts <TARGET> into every strategy matrix in .github/workflows/fuzz.yml
|
||||
#
|
||||
# Usage: just new-target my_feature
|
||||
# (the "fuzz_" prefix is added automatically)
|
||||
new-target NAME:
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
TARGET="fuzz_{{NAME}}"
|
||||
TEMPLATE="fuzz/fuzz_targets/_template.rs"
|
||||
RS_FILE="fuzz/fuzz_targets/${TARGET}.rs"
|
||||
CORPUS_DIR="fuzz/corpus/${TARGET}"
|
||||
|
||||
# ── 1. Create corpus directory ────────────────────────────────────────────
|
||||
mkdir -p "$CORPUS_DIR"
|
||||
echo "[1/4] Created corpus directory: $CORPUS_DIR"
|
||||
|
||||
# ── 2. Copy the typed fuzz target template ────────────────────────────────
|
||||
if [ -f "$RS_FILE" ]; then
|
||||
echo "SKIP [2/4]: $RS_FILE already exists — not overwriting."
|
||||
else
|
||||
cp "$TEMPLATE" "$RS_FILE"
|
||||
echo "[2/4] Created target from template: $RS_FILE"
|
||||
fi
|
||||
|
||||
# ── 3 & 4. Update Cargo.toml and fuzz.yml automatically ──────────────────
|
||||
python3 scripts/add_fuzz_target.py "$TARGET"
|
||||
echo ""
|
||||
echo "Done! Verify the build with:"
|
||||
echo " RISC0_DEV_MODE=1 cargo fuzz build ${TARGET}"
|
||||
|
||||
# ── Housekeeping ──────────────────────────────────────────────────────────────
|
||||
|
||||
# Remove all Cargo build artefacts (workspace + fuzz sub-crate)
|
||||
|
||||
@ -72,26 +72,79 @@ just fuzz-regression
|
||||
|
||||
## How to Add a New Fuzz Target
|
||||
|
||||
1. Create `fuzz/fuzz_targets/fuzz_<name>.rs` using the template below.
|
||||
2. Add a `[[bin]]` entry to `fuzz/Cargo.toml`.
|
||||
3. Create an empty seed corpus directory: `mkdir -p fuzz/corpus/fuzz_<name>`.
|
||||
4. Add the target to the CI matrix in `.github/workflows/fuzz.yml`.
|
||||
5. Run `RISC0_DEV_MODE=1 cargo fuzz build fuzz_<name>` to verify it compiles.
|
||||
### Step 1 — Scaffold with `just new-target`
|
||||
|
||||
**Template:**
|
||||
|
||||
```rust
|
||||
#![no_main]
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
// 1. Parse / decode `data` into your target type
|
||||
// 2. Call the function under test
|
||||
// 3. Assert invariants using `fuzz_props::invariants::assert_invariants()`
|
||||
// 4. Never panic on invalid input; only panic on invariant violations
|
||||
});
|
||||
```bash
|
||||
just new-target my_feature
|
||||
```
|
||||
|
||||
This single command does four things automatically:
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| Creates the corpus directory | `fuzz/corpus/fuzz_my_feature/` |
|
||||
| Writes a typed fuzz target template | `fuzz/fuzz_targets/fuzz_my_feature.rs` |
|
||||
| Appends `[[bin]]` entry | `fuzz/Cargo.toml` |
|
||||
| Inserts target into every CI matrix + perf loop | `.github/workflows/fuzz.yml` |
|
||||
|
||||
The generated template uses `ArbNSSATransaction` from `fuzz_props::arbitrary_types`
|
||||
so libfuzzer drives every field of `NSSATransaction` independently — no manual
|
||||
`Unstructured` wiring required.
|
||||
|
||||
### Step 2 — Implement the target
|
||||
|
||||
Edit `fuzz/fuzz_targets/fuzz_my_feature.rs`. Replace the placeholder with the
|
||||
function under test and any invariant assertions. Use the typed wrappers from
|
||||
[`fuzz_props::arbitrary_types`](../fuzz_props/src/arbitrary_types.rs) for
|
||||
structured input, or the proptest generators from
|
||||
[`fuzz_props::generators`](../fuzz_props/src/generators.rs) for richer strategies.
|
||||
|
||||
### Step 3 — Register the binary (automated)
|
||||
|
||||
`just new-target` calls [`scripts/add_fuzz_target.py`](../scripts/add_fuzz_target.py)
|
||||
which appends the `[[bin]]` entry to [`fuzz/Cargo.toml`](../fuzz/Cargo.toml)
|
||||
automatically. Once present, `cargo fuzz list` (and therefore `just fuzz`,
|
||||
`just fuzz-regression`, `just corpus-cmin`) pick up the target automatically — no
|
||||
further Justfile edits required.
|
||||
|
||||
> **Manual fallback:** if you create a target without `just new-target`, add the
|
||||
> entry yourself:
|
||||
>
|
||||
> ```toml
|
||||
> [[bin]]
|
||||
> name = "fuzz_my_feature"
|
||||
> path = "fuzz_targets/fuzz_my_feature.rs"
|
||||
> test = false
|
||||
> bench = false
|
||||
> ```
|
||||
|
||||
### Step 4 — Add to CI matrix (automated)
|
||||
|
||||
`just new-target` also inserts `fuzz_my_feature` into every strategy matrix and the
|
||||
perf-baseline shell loop in [`.github/workflows/fuzz.yml`](../.github/workflows/fuzz.yml)
|
||||
automatically via `scripts/add_fuzz_target.py`.
|
||||
|
||||
> **Manual fallback:** if you created the target without `just new-target`, add
|
||||
> `- fuzz_my_feature` to the `target:` list in the three places shown in
|
||||
> `.github/workflows/fuzz.yml` (smoke-fuzz, regression, perf-baseline).
|
||||
|
||||
### Step 5 — Verify
|
||||
|
||||
```bash
|
||||
RISC0_DEV_MODE=1 cargo fuzz build fuzz_my_feature
|
||||
just fuzz-regression # runs the new target against its (empty) corpus
|
||||
```
|
||||
|
||||
### Quick reference: what to touch
|
||||
|
||||
| File | Action | Automated? |
|
||||
|---|---|---|
|
||||
| `fuzz/fuzz_targets/fuzz_<name>.rs` | Create | ✅ `just new-target` |
|
||||
| `fuzz/corpus/fuzz_<name>/` | Create | ✅ `just new-target` |
|
||||
| `fuzz/Cargo.toml` | Add `[[bin]]` | ✅ `just new-target` |
|
||||
| `Justfile` | Nothing — auto-discovers | ✅ automatic |
|
||||
| `.github/workflows/fuzz.yml` | Add to 3 matrix lists | ✅ `just new-target` |
|
||||
|
||||
---
|
||||
|
||||
## Updating the LEZ Dependency
|
||||
|
||||
1
fuzz/Cargo.lock
generated
1
fuzz/Cargo.lock
generated
@ -1754,6 +1754,7 @@ dependencies = [
|
||||
"borsh",
|
||||
"common",
|
||||
"nssa",
|
||||
"nssa_core",
|
||||
"proptest",
|
||||
"testnet_initial_state",
|
||||
]
|
||||
|
||||
@ -47,3 +47,33 @@ testnet_initial_state = { path = "../../logos-execution-zone/testnet_initial_sta
|
||||
[profile.release]
|
||||
debug = true
|
||||
opt-level = 3
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_encoding_roundtrip"
|
||||
path = "fuzz_targets/fuzz_encoding_roundtrip.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_signature_verification"
|
||||
path = "fuzz_targets/fuzz_signature_verification.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_replay_prevention"
|
||||
path = "fuzz_targets/fuzz_replay_prevention.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_state_diff_computation"
|
||||
path = "fuzz_targets/fuzz_state_diff_computation.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_validate_execute_consistency"
|
||||
path = "fuzz_targets/fuzz_validate_execute_consistency.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
23
fuzz/fuzz_targets/_template.rs
Normal file
23
fuzz/fuzz_targets/_template.rs
Normal file
@ -0,0 +1,23 @@
|
||||
#![no_main]
|
||||
|
||||
use fuzz_props::arbitrary_types::ArbNSSATransaction;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|wrapped: ArbNSSATransaction| {
|
||||
let tx = wrapped.0;
|
||||
|
||||
// ── Stateless gate ────────────────────────────────────────────────────────
|
||||
// Remove this block to fuzz malformed / unsigned transactions too.
|
||||
let Ok(tx) = tx.transaction_stateless_check() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// ── Call the function under test ──────────────────────────────────────────
|
||||
// Example:
|
||||
// let mut state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
|
||||
// let result = tx.execute_check_on_state(&mut state, block_id, timestamp);
|
||||
|
||||
// ── Assert invariants ─────────────────────────────────────────────────────
|
||||
// Use fuzz_props::invariants::assert_invariants(&ctx) or inline assertions.
|
||||
let _ = tx; // replace once the target body is implemented
|
||||
});
|
||||
45
fuzz/fuzz_targets/fuzz_encoding_roundtrip.rs
Normal file
45
fuzz/fuzz_targets/fuzz_encoding_roundtrip.rs
Normal file
@ -0,0 +1,45 @@
|
||||
#![no_main]
|
||||
//! Fuzz target: encoding round-trip for all transaction types.
|
||||
//!
|
||||
//! Invariant: `decode(encode(tx)) == Ok(tx)` and `encode(decode(encode(tx))) == encode(tx)`
|
||||
//! for every `PublicTransaction` and `ProgramDeploymentTransaction`.
|
||||
//!
|
||||
//! `PrivacyPreservingTransaction` is excluded because its ZK receipt cannot be
|
||||
//! reconstructed in a fuzzing loop.
|
||||
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
use fuzz_props::arbitrary_types::{ArbProgramDeploymentTransaction, ArbPublicTransaction};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use nssa::{ProgramDeploymentTransaction, PublicTransaction};
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let mut u = Unstructured::new(data);
|
||||
|
||||
// ── Test 1: PublicTransaction round-trip ──────────────────────────────────
|
||||
if let Ok(wrapped) = ArbPublicTransaction::arbitrary(&mut u) {
|
||||
let tx = wrapped.0;
|
||||
let encoded = tx.to_bytes();
|
||||
let decoded = PublicTransaction::from_bytes(&encoded)
|
||||
.expect("INVARIANT VIOLATION: PublicTransaction::to_bytes() produced un-decodable output");
|
||||
let re_encoded = decoded.to_bytes();
|
||||
assert_eq!(
|
||||
encoded,
|
||||
re_encoded,
|
||||
"INVARIANT VIOLATION: encode(decode(encode(tx))) != encode(tx) for PublicTransaction"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Test 2: ProgramDeploymentTransaction round-trip ───────────────────────
|
||||
if let Ok(wrapped) = ArbProgramDeploymentTransaction::arbitrary(&mut u) {
|
||||
let tx = wrapped.0;
|
||||
let encoded = tx.to_bytes();
|
||||
let decoded = ProgramDeploymentTransaction::from_bytes(&encoded)
|
||||
.expect("INVARIANT VIOLATION: ProgramDeploymentTransaction::to_bytes() produced un-decodable output");
|
||||
let re_encoded = decoded.to_bytes();
|
||||
assert_eq!(
|
||||
encoded,
|
||||
re_encoded,
|
||||
"INVARIANT VIOLATION: encode(decode(encode(tx))) != encode(tx) for ProgramDeploymentTransaction"
|
||||
);
|
||||
}
|
||||
});
|
||||
42
fuzz/fuzz_targets/fuzz_replay_prevention.rs
Normal file
42
fuzz/fuzz_targets/fuzz_replay_prevention.rs
Normal file
@ -0,0 +1,42 @@
|
||||
#![no_main]
|
||||
//! Fuzz target: transaction replay prevention.
|
||||
//!
|
||||
//! Invariant: a transaction that is accepted in block N must be rejected when
|
||||
//! replayed in block N+1, because the nonce is consumed on first acceptance.
|
||||
//!
|
||||
//! `execute_check_on_state` returns the transaction back on success (`Ok(tx)`),
|
||||
//! so we can feed the same struct to the second application without cloning.
|
||||
|
||||
use arbitrary::Unstructured;
|
||||
use fuzz_props::generators::arbitrary_transaction;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use nssa::V03State;
|
||||
use testnet_initial_state::initial_accounts;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let mut u = Unstructured::new(data);
|
||||
|
||||
let accs_data = initial_accounts();
|
||||
let init_accs: Vec<(nssa::AccountId, u128)> = accs_data
|
||||
.iter()
|
||||
.map(|a| (a.account_id, a.balance))
|
||||
.collect();
|
||||
let mut state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
|
||||
|
||||
let Ok(tx) = arbitrary_transaction(&mut u) else { return; };
|
||||
|
||||
// Stateless gate: skip structurally malformed transactions.
|
||||
let Ok(tx) = tx.transaction_stateless_check() else { return; };
|
||||
|
||||
// First application — may legitimately fail for state-level reasons.
|
||||
let result = tx.execute_check_on_state(&mut state, 1, 0);
|
||||
|
||||
if let Ok(tx) = result {
|
||||
// tx is returned on success; try applying the identical transaction again.
|
||||
let result2 = tx.execute_check_on_state(&mut state, 2, 1);
|
||||
assert!(
|
||||
result2.is_err(),
|
||||
"INVARIANT VIOLATION: transaction accepted a second time — nonce replay not prevented"
|
||||
);
|
||||
}
|
||||
});
|
||||
55
fuzz/fuzz_targets/fuzz_signature_verification.rs
Normal file
55
fuzz/fuzz_targets/fuzz_signature_verification.rs
Normal file
@ -0,0 +1,55 @@
|
||||
#![no_main]
|
||||
//! Fuzz target: signature creation and verification.
|
||||
//!
|
||||
//! Invariants exercised:
|
||||
//!
|
||||
//! 1. **Correctness** — `Signature::new(key, msg).is_valid_for(msg, pub_key)` is always `true`
|
||||
//! for the matching public key.
|
||||
//! 2. **No panics** — random (possibly invalid) signatures and public keys must never cause a
|
||||
//! panic in `is_valid_for`.
|
||||
//! 3. **Cross-key soundness** — signing with key A and verifying against key B must not panic
|
||||
//! (the result may be `false` or, with negligible probability, accidentally `true`).
|
||||
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
use fuzz_props::arbitrary_types::{ArbPrivateKey, ArbPublicKey, ArbSignature};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use nssa::{PublicKey, Signature};
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let mut u = Unstructured::new(data);
|
||||
|
||||
// ── 1. Freshly signed message always verifies with the correct key ─────────
|
||||
if let Ok(key_wrap) = ArbPrivateKey::arbitrary(&mut u) {
|
||||
let private_key = key_wrap.0;
|
||||
let public_key = PublicKey::new_from_private_key(&private_key);
|
||||
let msg: Vec<u8> = u.arbitrary().unwrap_or_default();
|
||||
|
||||
let sig = Signature::new(&private_key, &msg);
|
||||
assert!(
|
||||
sig.is_valid_for(&msg, &public_key),
|
||||
"INVARIANT VIOLATION: Signature::new + is_valid_for returned false for the signing key"
|
||||
);
|
||||
}
|
||||
|
||||
// ── 2. Random bytes as signature must never panic ──────────────────────────
|
||||
if let (Ok(sig_wrap), Ok(pk_wrap)) = (
|
||||
ArbSignature::arbitrary(&mut u),
|
||||
ArbPublicKey::arbitrary(&mut u),
|
||||
) {
|
||||
let msg: Vec<u8> = u.arbitrary().unwrap_or_default();
|
||||
// The result may be true or false — we only assert no panic.
|
||||
let _ = sig_wrap.0.is_valid_for(&msg, &pk_wrap.0);
|
||||
}
|
||||
|
||||
// ── 3. Cross-key verification must not panic ───────────────────────────────
|
||||
if let (Ok(key_a_wrap), Ok(key_b_wrap)) = (
|
||||
ArbPrivateKey::arbitrary(&mut u),
|
||||
ArbPrivateKey::arbitrary(&mut u),
|
||||
) {
|
||||
let public_b = PublicKey::new_from_private_key(&key_b_wrap.0);
|
||||
let msg: Vec<u8> = u.arbitrary().unwrap_or_default();
|
||||
let sig_from_a = Signature::new(&key_a_wrap.0, &msg);
|
||||
// Must not panic regardless of key mismatch.
|
||||
let _ = sig_from_a.is_valid_for(&msg, &public_b);
|
||||
}
|
||||
});
|
||||
46
fuzz/fuzz_targets/fuzz_state_diff_computation.rs
Normal file
46
fuzz/fuzz_targets/fuzz_state_diff_computation.rs
Normal file
@ -0,0 +1,46 @@
|
||||
#![no_main]
|
||||
//! Fuzz target: state diff isolation.
|
||||
//!
|
||||
//! Invariant: `ValidatedStateDiff::from_public_transaction` must only produce
|
||||
//! account changes for accounts that appear in `tx.affected_public_account_ids()`.
|
||||
//!
|
||||
//! A diff that modifies an account outside that set would allow a transaction
|
||||
//! to silently corrupt unrelated accounts' balances.
|
||||
|
||||
use fuzz_props::arbitrary_types::ArbPublicTransaction;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use nssa::{V03State, ValidatedStateDiff};
|
||||
use testnet_initial_state::initial_accounts;
|
||||
|
||||
fuzz_target!(|wrapped: ArbPublicTransaction| {
|
||||
let pub_tx = wrapped.0;
|
||||
|
||||
let accs_data = initial_accounts();
|
||||
let init_accs: Vec<(nssa::AccountId, u128)> = accs_data
|
||||
.iter()
|
||||
.map(|a| (a.account_id, a.balance))
|
||||
.collect();
|
||||
let state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
|
||||
|
||||
// Collect the set of accounts the transaction claims to touch.
|
||||
let affected = pub_tx.affected_public_account_ids();
|
||||
|
||||
match ValidatedStateDiff::from_public_transaction(&pub_tx, &state, 1, 0) {
|
||||
Ok(diff) => {
|
||||
// INVARIANT: every key in the public diff must be in `affected`.
|
||||
let public_diff = diff.public_diff();
|
||||
for changed_id in public_diff.keys() {
|
||||
assert!(
|
||||
affected.contains(changed_id),
|
||||
"INVARIANT VIOLATION: diff modified account {:?} which is not in \
|
||||
affected_public_account_ids() {:?}",
|
||||
changed_id,
|
||||
affected
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Validation failure is expected for structurally or semantically invalid inputs.
|
||||
}
|
||||
}
|
||||
});
|
||||
105
fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs
Normal file
105
fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs
Normal file
@ -0,0 +1,105 @@
|
||||
#![no_main]
|
||||
//! Fuzz target: `validate_on_state` and `execute_check_on_state` consistency.
|
||||
//!
|
||||
//! Invariants:
|
||||
//!
|
||||
//! 1. **Agreement** — both methods must agree on success or failure for the same
|
||||
//! transaction and state. A divergence (one succeeds, the other fails) is a bug.
|
||||
//!
|
||||
//! 2. **Diff accuracy (bidirectional)** — when both succeed:
|
||||
//! - every account change recorded in the `ValidatedStateDiff` returned by
|
||||
//! `validate_on_state` must exactly match the post-execution state, AND
|
||||
//! - every account changed by `execute_check_on_state` must appear in the diff;
|
||||
//! a silent state-widening bug (execute touches an extra account not declared
|
||||
//! in the diff) is caught by the reverse check.
|
||||
|
||||
use fuzz_props::arbitrary_types::ArbNSSATransaction;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use nssa::V03State;
|
||||
use testnet_initial_state::initial_accounts;
|
||||
|
||||
fuzz_target!(|wrapped: ArbNSSATransaction| {
|
||||
let tx = wrapped.0;
|
||||
|
||||
// Stateless gate — skip structurally malformed transactions.
|
||||
let Ok(tx) = tx.transaction_stateless_check() else { return; };
|
||||
|
||||
let accs_data = initial_accounts();
|
||||
let init_accs: Vec<(nssa::AccountId, u128)> = accs_data
|
||||
.iter()
|
||||
.map(|a| (a.account_id, a.balance))
|
||||
.collect();
|
||||
let state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
|
||||
|
||||
// validate_on_state borrows `tx` and `state` — does NOT mutate state.
|
||||
let validate_result = tx.validate_on_state(&state, 1, 0);
|
||||
|
||||
// execute_check_on_state consumes `tx` and mutates `exec_state`.
|
||||
let mut exec_state = state.clone();
|
||||
let execute_result = tx.execute_check_on_state(&mut exec_state, 1, 0);
|
||||
|
||||
// INVARIANT 1: both must agree on success vs failure.
|
||||
match (validate_result, execute_result) {
|
||||
(Ok(diff), Ok(_)) => {
|
||||
let public_diff = diff.public_diff();
|
||||
|
||||
// INVARIANT 2a (forward): every account in the diff matches the post-execute state.
|
||||
for (account_id, expected_account) in &public_diff {
|
||||
let actual = exec_state.get_account_by_id(*account_id);
|
||||
assert_eq!(
|
||||
*expected_account,
|
||||
actual,
|
||||
"INVARIANT VIOLATION: validate diff and execute state disagree \
|
||||
for account {:?}",
|
||||
account_id
|
||||
);
|
||||
}
|
||||
|
||||
// INVARIANT 2b (reverse): every account changed by execute_check_on_state must
|
||||
// be captured in the validate diff. A silent state-widening bug — where
|
||||
// execute modifies accounts that validate_on_state did not declare — would
|
||||
// pass the forward check above but is caught here.
|
||||
//
|
||||
// We check a superset of accounts: genesis accounts PLUS any account the
|
||||
// diff explicitly declares. This covers the common case of both mutations
|
||||
// to existing accounts and accounts the diff itself declares as new.
|
||||
//
|
||||
// Known limitation: if execute_check_on_state creates a brand-new account
|
||||
// that is absent from both the genesis set and the diff, that state-widening
|
||||
// will NOT be detected here. Full detection would require iterating over all
|
||||
// accounts in exec_state, which V03State does not currently expose.
|
||||
let mut all_checked_ids: std::collections::HashSet<nssa::AccountId> =
|
||||
init_accs.iter().map(|&(id, _)| id).collect();
|
||||
for acc_id in public_diff.keys() {
|
||||
all_checked_ids.insert(*acc_id);
|
||||
}
|
||||
for acc_id in all_checked_ids {
|
||||
let before = state.get_account_by_id(acc_id);
|
||||
let after = exec_state.get_account_by_id(acc_id);
|
||||
if before != after {
|
||||
assert!(
|
||||
public_diff.contains_key(&acc_id),
|
||||
"INVARIANT VIOLATION: execute_check_on_state modified account {:?} \
|
||||
which is absent from validate_on_state diff",
|
||||
acc_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
(Err(_), Err(_)) => {
|
||||
// Both failed — correct.
|
||||
}
|
||||
(Ok(_), Err(e)) => {
|
||||
panic!(
|
||||
"INVARIANT VIOLATION: validate_on_state succeeded but \
|
||||
execute_check_on_state failed: {e:?}"
|
||||
);
|
||||
}
|
||||
(Err(e), Ok(_)) => {
|
||||
panic!(
|
||||
"INVARIANT VIOLATION: validate_on_state failed but \
|
||||
execute_check_on_state succeeded: {e:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -8,6 +8,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
nssa = { workspace = true }
|
||||
nssa_core = { workspace = true }
|
||||
common = { workspace = true }
|
||||
borsh = { workspace = true }
|
||||
proptest = "1.4"
|
||||
|
||||
256
fuzz_props/src/arbitrary_types.rs
Normal file
256
fuzz_props/src/arbitrary_types.rs
Normal file
@ -0,0 +1,256 @@
|
||||
//! Newtype wrappers that implement [`arbitrary::Arbitrary`] for LEZ types.
|
||||
//!
|
||||
//! **No changes to `../logos-execution-zone` are required.**
|
||||
//!
|
||||
//! The Rust orphan rule forbids `impl Arbitrary for NSSATransaction` when both
|
||||
//! the trait and the type come from external crates. Using newtypes (`ArbXxx`)
|
||||
//! sidesteps the restriction entirely.
|
||||
//!
|
||||
//! # Usage in a fuzz target
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! #![no_main]
|
||||
//! use fuzz_props::arbitrary_types::ArbNSSATransaction;
|
||||
//! use libfuzzer_sys::fuzz_target;
|
||||
//!
|
||||
//! fuzz_target!(|wrapped: ArbNSSATransaction| {
|
||||
//! let tx = wrapped.0;
|
||||
//! let Ok(valid_tx) = tx.transaction_stateless_check() else { return; };
|
||||
//! // …
|
||||
//! });
|
||||
//! ```
|
||||
|
||||
use arbitrary::{Arbitrary, Result as ArbResult, Unstructured};
|
||||
use common::{HashType, block::HashableBlockData, transaction::NSSATransaction};
|
||||
use nssa::{
|
||||
AccountId, PrivateKey, PublicKey, Signature,
|
||||
program_deployment_transaction::ProgramDeploymentTransaction,
|
||||
public_transaction::{Message, PublicTransaction, WitnessSet},
|
||||
};
|
||||
use nssa_core::account::Nonce;
|
||||
|
||||
// ── AccountId ─────────────────────────────────────────────────────────────────
|
||||
// `AccountId::new([u8; 32])` accepts any byte array — no validity constraint.
|
||||
|
||||
/// Newtype wrapper providing [`Arbitrary`] for [`AccountId`].
|
||||
#[derive(Debug)]
|
||||
pub struct ArbAccountId(pub AccountId);
|
||||
|
||||
impl<'a> Arbitrary<'a> for ArbAccountId {
|
||||
fn arbitrary(u: &mut Unstructured<'a>) -> ArbResult<Self> {
|
||||
Ok(Self(AccountId::new(<[u8; 32]>::arbitrary(u)?)))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Nonce ─────────────────────────────────────────────────────────────────────
|
||||
// `Nonce` wraps `u128` and exposes `From<u128>`.
|
||||
|
||||
/// Newtype wrapper providing [`Arbitrary`] for [`Nonce`].
|
||||
#[derive(Debug)]
|
||||
pub struct ArbNonce(pub Nonce);
|
||||
|
||||
impl<'a> Arbitrary<'a> for ArbNonce {
|
||||
fn arbitrary(u: &mut Unstructured<'a>) -> ArbResult<Self> {
|
||||
Ok(Self(Nonce::from(u128::arbitrary(u)?)))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Signature ─────────────────────────────────────────────────────────────────
|
||||
// `Signature.value` is `pub [u8; 64]`, so we can construct a value directly.
|
||||
// Cryptographic validity is only checked at verification time, meaning invalid
|
||||
// byte patterns are legal at the struct level and will exercise the rejection
|
||||
// path in `WitnessSet::is_valid_for`.
|
||||
|
||||
/// Newtype wrapper providing [`Arbitrary`] for [`Signature`].
|
||||
#[derive(Debug)]
|
||||
pub struct ArbSignature(pub Signature);
|
||||
|
||||
impl<'a> Arbitrary<'a> for ArbSignature {
|
||||
fn arbitrary(u: &mut Unstructured<'a>) -> ArbResult<Self> {
|
||||
Ok(Self(Signature { value: <[u8; 64]>::arbitrary(u)? }))
|
||||
}
|
||||
}
|
||||
|
||||
// ── PrivateKey ────────────────────────────────────────────────────────────────
|
||||
// `PrivateKey::try_new` succeeds for almost all non-zero 32-byte values: only
|
||||
// the zero scalar and values ≥ the secp256k1 group order (< 2⁻¹²⁸ of the
|
||||
// input space) are rejected. A known-good fallback handles the rare failure.
|
||||
|
||||
/// Newtype wrapper providing [`Arbitrary`] for [`PrivateKey`].
|
||||
#[derive(Debug)]
|
||||
pub struct ArbPrivateKey(pub PrivateKey);
|
||||
|
||||
impl<'a> Arbitrary<'a> for ArbPrivateKey {
|
||||
fn arbitrary(u: &mut Unstructured<'a>) -> ArbResult<Self> {
|
||||
let bytes = <[u8; 32]>::arbitrary(u)?;
|
||||
let key = PrivateKey::try_new(bytes)
|
||||
.unwrap_or_else(|_| PrivateKey::try_new([1_u8; 32]).expect("known-good seed"));
|
||||
Ok(Self(key))
|
||||
}
|
||||
}
|
||||
|
||||
// ── PublicKey ─────────────────────────────────────────────────────────────────
|
||||
// `PublicKey::try_new` validates that the bytes form a valid secp256k1
|
||||
// x-coordinate (roughly 50% of random inputs succeed). Two modes:
|
||||
// 1. Derive from a valid `PrivateKey` → exercises the happy-path verification.
|
||||
// 2. Use raw bytes → exercises the rejection path in `is_valid_for`; on
|
||||
// construction failure falls back to a derived key so upstream callers
|
||||
// (ArbWitnessSet, ArbPublicTransaction) are not silently discarded.
|
||||
|
||||
/// Newtype wrapper providing [`Arbitrary`] for [`PublicKey`].
|
||||
#[derive(Debug)]
|
||||
pub struct ArbPublicKey(pub PublicKey);
|
||||
|
||||
impl<'a> Arbitrary<'a> for ArbPublicKey {
|
||||
fn arbitrary(u: &mut Unstructured<'a>) -> ArbResult<Self> {
|
||||
if bool::arbitrary(u)? {
|
||||
// Valid key pair — exercises happy-path signature verification.
|
||||
let pk = PublicKey::new_from_private_key(&ArbPrivateKey::arbitrary(u)?.0);
|
||||
Ok(Self(pk))
|
||||
} else {
|
||||
// Raw bytes — may be an invalid x-coordinate, exercises the rejection
|
||||
// path in `is_valid_for`. On failure we fall back to a key derived
|
||||
// from a valid private key so that upstream callers (ArbWitnessSet,
|
||||
// ArbPublicTransaction) are not silently discarded ~25% of the time.
|
||||
// The ArbSignature type (random bytes) already exercises the full
|
||||
// rejection path in `is_valid_for` independently.
|
||||
let bytes = <[u8; 32]>::arbitrary(u)?;
|
||||
let pk = PublicKey::try_new(bytes)
|
||||
.unwrap_or_else(|_| PublicKey::new_from_private_key(&ArbPrivateKey::arbitrary(u).map(|w| w.0).unwrap_or_else(|_| PrivateKey::try_new([1_u8; 32]).expect("known-good seed"))));
|
||||
Ok(Self(pk))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Message (public transaction) ──────────────────────────────────────────────
|
||||
// `Message::new_preserialized` takes all fields directly without any validity
|
||||
// constraint — any combination of program_id, account_ids, nonces, and
|
||||
// instruction_data is accepted.
|
||||
|
||||
/// Newtype wrapper providing [`Arbitrary`] for the public-transaction [`Message`].
|
||||
#[derive(Debug)]
|
||||
pub struct ArbPubTxMessage(pub Message);
|
||||
|
||||
impl<'a> Arbitrary<'a> for ArbPubTxMessage {
|
||||
fn arbitrary(u: &mut Unstructured<'a>) -> ArbResult<Self> {
|
||||
let program_id: [u32; 8] = <[u32; 8]>::arbitrary(u)?;
|
||||
// Generate 0–7 accounts; nonces vector is given the same length.
|
||||
let len = (u8::arbitrary(u)? as usize) % 8;
|
||||
let account_ids = (0..len)
|
||||
.map(|_| ArbAccountId::arbitrary(u).map(|a| a.0))
|
||||
.collect::<ArbResult<Vec<_>>>()?;
|
||||
let nonces = (0..len)
|
||||
.map(|_| ArbNonce::arbitrary(u).map(|n| n.0))
|
||||
.collect::<ArbResult<Vec<_>>>()?;
|
||||
let instruction_data: Vec<u32> = Vec::<u32>::arbitrary(u)?;
|
||||
Ok(Self(Message::new_preserialized(
|
||||
program_id,
|
||||
account_ids,
|
||||
nonces,
|
||||
instruction_data,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// ── WitnessSet ────────────────────────────────────────────────────────────────
|
||||
// `WitnessSet::from_raw_parts` accepts any `Vec<(Signature, PublicKey)>`.
|
||||
// We deliberately mix valid and invalid pairs so the fuzzer exercises both
|
||||
// the accept and reject branches of `WitnessSet::is_valid_for`.
|
||||
|
||||
/// Newtype wrapper providing [`Arbitrary`] for [`WitnessSet`].
|
||||
#[derive(Debug)]
|
||||
pub struct ArbWitnessSet(pub WitnessSet);
|
||||
|
||||
impl<'a> Arbitrary<'a> for ArbWitnessSet {
|
||||
fn arbitrary(u: &mut Unstructured<'a>) -> ArbResult<Self> {
|
||||
// 0–3 (signature, public_key) pairs
|
||||
let n = (u8::arbitrary(u)? as usize) % 4;
|
||||
let pairs = (0..n)
|
||||
.map(|_| {
|
||||
Ok((
|
||||
ArbSignature::arbitrary(u)?.0,
|
||||
ArbPublicKey::arbitrary(u)?.0,
|
||||
))
|
||||
})
|
||||
.collect::<ArbResult<Vec<_>>>()?;
|
||||
Ok(Self(WitnessSet::from_raw_parts(pairs)))
|
||||
}
|
||||
}
|
||||
|
||||
// ── PublicTransaction ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Newtype wrapper providing [`Arbitrary`] for [`PublicTransaction`].
|
||||
#[derive(Debug)]
|
||||
pub struct ArbPublicTransaction(pub PublicTransaction);
|
||||
|
||||
impl<'a> Arbitrary<'a> for ArbPublicTransaction {
|
||||
fn arbitrary(u: &mut Unstructured<'a>) -> ArbResult<Self> {
|
||||
Ok(Self(PublicTransaction::new(
|
||||
ArbPubTxMessage::arbitrary(u)?.0,
|
||||
ArbWitnessSet::arbitrary(u)?.0,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// ── ProgramDeploymentTransaction ──────────────────────────────────────────────
|
||||
// `ProgramDeploymentTransaction` wraps a single `Message { bytecode: Vec<u8> }`.
|
||||
|
||||
/// Newtype wrapper providing [`Arbitrary`] for [`ProgramDeploymentTransaction`].
|
||||
#[derive(Debug)]
|
||||
pub struct ArbProgramDeploymentTransaction(pub ProgramDeploymentTransaction);
|
||||
|
||||
impl<'a> Arbitrary<'a> for ArbProgramDeploymentTransaction {
|
||||
fn arbitrary(u: &mut Unstructured<'a>) -> ArbResult<Self> {
|
||||
let bytecode = Vec::<u8>::arbitrary(u)?;
|
||||
let msg = nssa::program_deployment_transaction::Message::new(bytecode);
|
||||
Ok(Self(ProgramDeploymentTransaction::new(msg)))
|
||||
}
|
||||
}
|
||||
|
||||
// ── NSSATransaction ───────────────────────────────────────────────────────────
|
||||
// `PrivacyPreservingTransaction` is intentionally excluded: it embeds a risc0
|
||||
// ZK receipt that cannot be generated inside a hot fuzzing loop. This matches
|
||||
// the known limitation documented in `docs/fuzzing.md`.
|
||||
|
||||
/// Newtype wrapper providing [`Arbitrary`] for [`NSSATransaction`].
|
||||
///
|
||||
/// Generates `Public` and `ProgramDeployment` variants only.
|
||||
#[derive(Debug)]
|
||||
pub struct ArbNSSATransaction(pub NSSATransaction);
|
||||
|
||||
impl<'a> Arbitrary<'a> for ArbNSSATransaction {
|
||||
fn arbitrary(u: &mut Unstructured<'a>) -> ArbResult<Self> {
|
||||
match u8::arbitrary(u)? % 2 {
|
||||
0 => Ok(Self(NSSATransaction::Public(
|
||||
ArbPublicTransaction::arbitrary(u)?.0,
|
||||
))),
|
||||
_ => Ok(Self(NSSATransaction::ProgramDeployment(
|
||||
ArbProgramDeploymentTransaction::arbitrary(u)?.0,
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── HashableBlockData ─────────────────────────────────────────────────────────
|
||||
// All fields of `HashableBlockData` are `pub`, so we can construct it with a
|
||||
// struct literal after generating each field independently.
|
||||
|
||||
/// Newtype wrapper providing [`Arbitrary`] for [`HashableBlockData`].
|
||||
#[derive(Debug)]
|
||||
pub struct ArbHashableBlockData(pub HashableBlockData);
|
||||
|
||||
impl<'a> Arbitrary<'a> for ArbHashableBlockData {
|
||||
fn arbitrary(u: &mut Unstructured<'a>) -> ArbResult<Self> {
|
||||
// 0–7 transactions per block
|
||||
let n = (u8::arbitrary(u)? as usize) % 8;
|
||||
let transactions = (0..n)
|
||||
.map(|_| ArbNSSATransaction::arbitrary(u).map(|t| t.0))
|
||||
.collect::<ArbResult<Vec<_>>>()?;
|
||||
Ok(Self(HashableBlockData {
|
||||
block_id: u64::arbitrary(u)?,
|
||||
prev_block_hash: HashType(<[u8; 32]>::arbitrary(u)?),
|
||||
timestamp: u64::arbitrary(u)?,
|
||||
transactions,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
#![allow(clippy::missing_docs_in_private_items)]
|
||||
|
||||
pub mod arbitrary_types;
|
||||
pub mod generators;
|
||||
pub mod invariants;
|
||||
|
||||
|
||||
177
scripts/add_fuzz_target.py
Normal file
177
scripts/add_fuzz_target.py
Normal file
@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fully automates registering a new cargo-fuzz target.
|
||||
|
||||
Usage:
|
||||
python3 scripts/add_fuzz_target.py <TARGET_NAME>
|
||||
|
||||
Where TARGET_NAME is the full binary name, e.g. fuzz_my_feature.
|
||||
|
||||
Actions performed:
|
||||
1. Appends a [[bin]] entry to fuzz/Cargo.toml
|
||||
2. Inserts TARGET_NAME into every YAML matrix block in
|
||||
.github/workflows/fuzz.yml (smoke-fuzz, regression)
|
||||
3. Inserts TARGET_NAME into the perf-baseline shell for-loop in
|
||||
.github/workflows/fuzz.yml
|
||||
|
||||
Run from the repository root.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Target names must follow the cargo-fuzz binary naming convention.
|
||||
_TARGET_RE = re.compile(r"^fuzz_[a-z][a-z0-9_]*$")
|
||||
|
||||
|
||||
def append_cargo_bin(target: str, cargo_toml: Path) -> None:
|
||||
"""Append a [[bin]] entry to fuzz/Cargo.toml if not already present."""
|
||||
content = cargo_toml.read_text()
|
||||
if f'name = "{target}"' in content:
|
||||
print(f" SKIP fuzz/Cargo.toml — [[bin]] {target!r} already present")
|
||||
return
|
||||
|
||||
entry = (
|
||||
f"\n[[bin]]\n"
|
||||
f'name = "{target}"\n'
|
||||
f'path = "fuzz_targets/{target}.rs"\n'
|
||||
f"test = false\n"
|
||||
f"bench = false\n"
|
||||
)
|
||||
cargo_toml.write_text(content.rstrip("\n") + "\n" + entry)
|
||||
print(f" [+] fuzz/Cargo.toml — added [[bin]] {target!r}")
|
||||
|
||||
|
||||
def insert_into_yaml_matrices(target: str, content: str) -> tuple[str, int]:
|
||||
"""Insert target into YAML strategy matrix blocks.
|
||||
|
||||
Matches blocks of the form::
|
||||
|
||||
target:
|
||||
- fuzz_a
|
||||
- fuzz_b
|
||||
|
||||
and appends `` - <target>`` after the last existing entry.
|
||||
"""
|
||||
pattern = re.compile(
|
||||
r"( target:\n(?: - fuzz_\w+\n)+)",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
def add_target(m: re.Match) -> str:
|
||||
return m.group(0) + f" - {target}\n"
|
||||
|
||||
new_content, count = pattern.subn(add_target, content)
|
||||
return new_content, count
|
||||
|
||||
|
||||
def insert_into_shell_loop(target: str, content: str) -> tuple[str, int]:
|
||||
"""Insert target into a 'for target in ... ; do' shell loop.
|
||||
|
||||
The last entry in the loop ends with ``; do``. We change it to end with
|
||||
a backslash continuation and append the new entry with ``; do``.
|
||||
|
||||
Example — before::
|
||||
|
||||
fuzz_block_verification; do
|
||||
|
||||
After::
|
||||
|
||||
fuzz_block_verification \\
|
||||
fuzz_new_target; do
|
||||
"""
|
||||
# Match the last fuzz target in the for-loop: " fuzz_xxx; do"
|
||||
# Indentation: 12 spaces (inside a run: | block).
|
||||
pattern = re.compile(r"( fuzz_\w+)(; do)", re.MULTILINE)
|
||||
|
||||
# We only want to replace the *last* occurrence (the closing entry).
|
||||
matches = list(pattern.finditer(content))
|
||||
if not matches:
|
||||
return content, 0
|
||||
|
||||
if len(matches) > 1:
|
||||
print(
|
||||
f" ERROR: found {len(matches)} shell loops matching the pattern; "
|
||||
"cannot determine which one to update. "
|
||||
"Please edit .github/workflows/fuzz.yml manually.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
m = matches[-1]
|
||||
replacement = f"{m.group(1)} \\\n {target}{m.group(2)}"
|
||||
new_content = content[: m.start()] + replacement + content[m.end() :]
|
||||
return new_content, 1
|
||||
|
||||
|
||||
def insert_into_workflow(target: str, workflow: Path) -> None:
|
||||
"""Update all target lists in the fuzz workflow file."""
|
||||
content = workflow.read_text()
|
||||
|
||||
if target in content:
|
||||
print(f" SKIP .github/workflows/fuzz.yml — {target!r} already present")
|
||||
return
|
||||
|
||||
# 1. YAML matrix blocks (smoke-fuzz, regression)
|
||||
content, yaml_count = insert_into_yaml_matrices(target, content)
|
||||
if yaml_count:
|
||||
print(
|
||||
f" [+] .github/workflows/fuzz.yml — inserted {target!r} into "
|
||||
f"{yaml_count} YAML matrix block(s)"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f" ERROR: no YAML matrix blocks matched in {workflow} — please edit manually",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# 2. Shell for-loop (perf-baseline)
|
||||
content, loop_count = insert_into_shell_loop(target, content)
|
||||
if loop_count:
|
||||
print(
|
||||
f" [+] .github/workflows/fuzz.yml — inserted {target!r} into "
|
||||
f"perf-baseline shell loop"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f" ERROR: perf-baseline shell loop not found in {workflow} — please edit manually",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
workflow.write_text(content)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) != 2:
|
||||
print(f"Usage: {sys.argv[0]} <TARGET_NAME>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
target = sys.argv[1]
|
||||
|
||||
if not _TARGET_RE.match(target):
|
||||
print(
|
||||
f"ERROR: target name must match fuzz_[a-z][a-z0-9_]*, got: {target!r}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
root = Path(__file__).parent.parent # repository root
|
||||
|
||||
cargo_toml = root / "fuzz" / "Cargo.toml"
|
||||
workflow = root / ".github" / "workflows" / "fuzz.yml"
|
||||
|
||||
if not cargo_toml.exists():
|
||||
print(f"ERROR: {cargo_toml} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not workflow.exists():
|
||||
print(f"ERROR: {workflow} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
append_cargo_bin(target, cargo_toml)
|
||||
insert_into_workflow(target, workflow)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
x
Reference in New Issue
Block a user