mirror of
https://github.com/logos-blockchain/lez-fuzzing.git
synced 2026-07-01 23:39:34 +00:00
fix: repo pinning
- minor invariant changes - missing target in CI added - docs
This commit is contained in:
parent
119c867b89
commit
56162a66a3
20
.github/actions/checkout-lez/action.yml
vendored
20
.github/actions/checkout-lez/action.yml
vendored
@ -4,6 +4,18 @@ description: >
|
||||
symlinks it to the expected sibling path (../logos-execution-zone) so that
|
||||
Cargo path dependencies resolve correctly.
|
||||
|
||||
inputs:
|
||||
ref:
|
||||
description: >
|
||||
Git ref (SHA, tag, or branch) of logos-execution-zone to check out.
|
||||
Defaults to the pinned, reviewed revision below — this single line is the
|
||||
source of truth for the LEZ version every PR-gating workflow builds against.
|
||||
To bump it: update ../logos-execution-zone to a tested commit, run
|
||||
`just update-lez`, replace this SHA, and open a PR. The scheduled
|
||||
lez-compat workflow overrides this with `main` to detect upstream drift.
|
||||
required: false
|
||||
default: dac429a94af932b0c827544fff8b9de85b83e6f3
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
@ -11,8 +23,16 @@ runs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: logos-blockchain/logos-execution-zone
|
||||
ref: ${{ inputs.ref }}
|
||||
path: logos-execution-zone
|
||||
|
||||
- name: Symlink logos-execution-zone to sibling directory
|
||||
run: ln -s "$GITHUB_WORKSPACE/logos-execution-zone" "$GITHUB_WORKSPACE/../logos-execution-zone"
|
||||
shell: bash
|
||||
|
||||
- name: Report pinned LEZ revision
|
||||
run: |
|
||||
echo "LEZ ref: ${{ inputs.ref }}"
|
||||
echo "LEZ SHA: $(git -C "$GITHUB_WORKSPACE/logos-execution-zone" rev-parse HEAD)" \
|
||||
| tee -a "$GITHUB_STEP_SUMMARY"
|
||||
shell: bash
|
||||
|
||||
1
.github/workflows/fuzz-afl.yml
vendored
1
.github/workflows/fuzz-afl.yml
vendored
@ -46,6 +46,7 @@ jobs:
|
||||
fuzz_privacy_preserving_witness
|
||||
fuzz_encoding_privacy_preserving
|
||||
fuzz_nullifier_set_roundtrip
|
||||
fuzz_privacy_preserving_state_transition
|
||||
EOF
|
||||
)
|
||||
echo "targets=$targets" >> "$GITHUB_OUTPUT"
|
||||
|
||||
13
.github/workflows/fuzz.yml
vendored
13
.github/workflows/fuzz.yml
vendored
@ -12,6 +12,14 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
# ── Target-inventory gate: fail if any workflow/script/doc omits a target ────
|
||||
target-inventory:
|
||||
name: Target inventory in sync
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: python3 scripts/check_target_inventory.py
|
||||
|
||||
# ── Smoke fuzz: 60 s per target ─────────────────────────────────────────────
|
||||
smoke-fuzz:
|
||||
name: Smoke fuzz (${{ matrix.target }})
|
||||
@ -40,6 +48,7 @@ jobs:
|
||||
- fuzz_privacy_preserving_witness
|
||||
- fuzz_encoding_privacy_preserving
|
||||
- fuzz_nullifier_set_roundtrip
|
||||
- fuzz_privacy_preserving_state_transition
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@ -228,6 +237,7 @@ jobs:
|
||||
- fuzz_privacy_preserving_witness
|
||||
- fuzz_encoding_privacy_preserving
|
||||
- fuzz_nullifier_set_roundtrip
|
||||
- fuzz_privacy_preserving_state_transition
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout logos-execution-zone
|
||||
@ -300,7 +310,8 @@ jobs:
|
||||
fuzz_transaction_properties \
|
||||
fuzz_privacy_preserving_witness \
|
||||
fuzz_encoding_privacy_preserving \
|
||||
fuzz_nullifier_set_roundtrip; do
|
||||
fuzz_nullifier_set_roundtrip \
|
||||
fuzz_privacy_preserving_state_transition; 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
|
||||
|
||||
53
.github/workflows/lez-compat.yml
vendored
Normal file
53
.github/workflows/lez-compat.yml
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
name: LEZ upstream compatibility
|
||||
|
||||
# Detects drift between this fuzzing repo and the *latest* logos-execution-zone
|
||||
# main, independently of PR gating (which builds against the pinned SHA in
|
||||
# .github/actions/checkout-lez). A failure here means upstream main moved in a
|
||||
# way that breaks the harness — bump the pinned ref once the break is resolved.
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 4 * * *"
|
||||
push:
|
||||
branches: [ test-privacy-features ]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
RISC0_DEV_MODE: "1"
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build-against-upstream-main:
|
||||
name: Build + test against LEZ main
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout logos-execution-zone (main, not the pinned SHA)
|
||||
uses: ./.github/actions/checkout-lez
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: llvm-tools-preview
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: lez-compat-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Install logos-blockchain-circuits
|
||||
uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build shared fuzz harness against upstream main
|
||||
run: cargo build -p fuzz_props --release
|
||||
|
||||
- name: Run fuzz_props tests against upstream main
|
||||
run: cargo test -p fuzz_props --release
|
||||
3
.github/workflows/mutants.yml
vendored
3
.github/workflows/mutants.yml
vendored
@ -164,7 +164,8 @@ jobs:
|
||||
fuzz_apply_state_diff_split_path fuzz_multi_block_state_sequence \
|
||||
fuzz_sequencer_vs_replayer fuzz_merkle_tree \
|
||||
fuzz_transaction_properties fuzz_privacy_preserving_witness \
|
||||
fuzz_encoding_privacy_preserving fuzz_nullifier_set_roundtrip; do
|
||||
fuzz_encoding_privacy_preserving fuzz_nullifier_set_roundtrip \
|
||||
fuzz_privacy_preserving_state_transition; do
|
||||
cargo fuzz build "${target}"
|
||||
done
|
||||
|
||||
|
||||
11
Justfile
11
Justfile
@ -299,7 +299,9 @@ fuzz-afl TARGET="" TIME="30":
|
||||
echo "Binary not found — building $t first…"
|
||||
just afl-build-target "$t"
|
||||
fi
|
||||
timeout "$TIME" afl-fuzz -i "$CORPUS" -o "$OUTPUT" -- "$BINARY" || true
|
||||
# Use afl-fuzz's own -V (run for N seconds then exit) instead of GNU
|
||||
# `timeout`, which is not installed by default on macOS.
|
||||
afl-fuzz -V "$TIME" -i "$CORPUS" -o "$OUTPUT" -- "$BINARY" || true
|
||||
}
|
||||
for t in "${TARGETS[@]}"; do
|
||||
echo "=== afl++ $t for ${TIME}s ==="
|
||||
@ -427,7 +429,9 @@ fuzz-afl-parallel TIME="30" JOBS="4":
|
||||
echo "Binary not found — building $t first…"
|
||||
just afl-build-target "$t"
|
||||
fi
|
||||
timeout {{TIME}} afl-fuzz -i "$CORPUS" -o "$OUTPUT" -- "$BINARY" || true
|
||||
# Use afl-fuzz's own -V (run for N seconds then exit) instead of GNU
|
||||
# `timeout`, which is not installed by default on macOS.
|
||||
afl-fuzz -V {{TIME}} -i "$CORPUS" -o "$OUTPUT" -- "$BINARY" || true
|
||||
}
|
||||
echo "Targets: ${#TARGETS[@]} | max parallel: {{JOBS}} | time per target: {{TIME}}s"
|
||||
for t in "${TARGETS[@]}"; do
|
||||
@ -462,7 +466,8 @@ afl-corpus-sync:
|
||||
[ -d "$QUEUE" ] || continue
|
||||
for f in "$QUEUE"/id:*; do
|
||||
[ -f "$f" ] || continue
|
||||
HASH=$(sha1sum "$f" | cut -d' ' -f1)
|
||||
# sha1sum (GNU/Linux) is absent on stock macOS; fall back to shasum.
|
||||
HASH=$( { sha1sum "$f" 2>/dev/null || shasum -a 1 "$f"; } | cut -d' ' -f1)
|
||||
DEST_FILE="${DEST}/${HASH}"
|
||||
if [ ! -f "$DEST_FILE" ]; then
|
||||
cp "$f" "$DEST_FILE"
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
[Logos Execution Zone (LEZ)](https://github.com/logos-blockchain/logos-execution-zone) protocol.**
|
||||
|
||||
[](rust-toolchain.toml)
|
||||
[](#-fuzz-targets)
|
||||
[](#-fuzz-targets)
|
||||
[](.github/workflows/mutants.yml)
|
||||
[](LICENSE-MIT)
|
||||
|
||||
@ -31,7 +31,7 @@ lez-fuzzing/
|
||||
│ └── generators.rs # Arbitrary / proptest strategies
|
||||
├── fuzz/ # cargo-fuzz crate (own [workspace] sentinel)
|
||||
│ ├── Cargo.toml
|
||||
│ ├── fuzz_targets/ # 20 targets total — see table below
|
||||
│ ├── fuzz_targets/ # 21 targets total — see table below
|
||||
│ │ ├── _template.rs # Template for `just new-target`
|
||||
│ │ └── fuzz_*.rs
|
||||
│ └── corpus/ # Curated seed inputs (one dir per target)
|
||||
@ -130,6 +130,7 @@ just fuzz-props
|
||||
| 18 | `fuzz_privacy_preserving_witness` | `privacy_preserving_transaction::WitnessSet`: CorrectVerification (witness for msg A passes `signatures_are_valid_for(A)`) + MessageIsolation + SignerIdsMatchWitnessKeys |
|
||||
| 19 | `fuzz_encoding_privacy_preserving` | Privacy-preserving encoding: MessageEncodingRoundtrip + TxEncodingDeterministic/NonEmpty |
|
||||
| 20 | `fuzz_nullifier_set_roundtrip` | `NullifierSet` Borsh serialisation: NullifierSetRoundtrip (decode→encode identity for the hand-written impl) |
|
||||
| 21 | `fuzz_privacy_preserving_state_transition` | Path B — `NSSATransaction::PrivacyPreserving` through `execute_check_on_state` with a dev-mode passing proof: reaches commitment/nullifier checks 5–6 + `apply_state_diff`. Asserts no-panic, StateIsolationOnFailure, PrivateStateIsolationOnFailure, CommitmentInsertion, NonceIncrementCorrectness, PostStateApplied, ReplayRejection (balance conservation intentionally not asserted — the fake proof bypasses the circuit guarantee) |
|
||||
|
||||
Each target lives at `fuzz/fuzz_targets/<name>.rs`.
|
||||
|
||||
|
||||
@ -129,6 +129,7 @@ just fuzz-regression
|
||||
| `fuzz_privacy_preserving_witness` | `privacy_preserving_transaction::WitnessSet`: **CorrectVerification** (witness for message A passes `signatures_are_valid_for(A)`), **MessageIsolation**, **SignerIdsMatchWitnessKeys** | `fuzz/fuzz_targets/fuzz_privacy_preserving_witness.rs` |
|
||||
| `fuzz_encoding_privacy_preserving` | Privacy-preserving encoding: **MessageEncodingRoundtrip**, **TxEncodingDeterministic** / **NonEmpty** | `fuzz/fuzz_targets/fuzz_encoding_privacy_preserving.rs` |
|
||||
| `fuzz_nullifier_set_roundtrip` | `NullifierSet` Borsh serialisation: **NullifierSetRoundtrip** (decode→encode identity for the hand-written impl) | `fuzz/fuzz_targets/fuzz_nullifier_set_roundtrip.rs` |
|
||||
| `fuzz_privacy_preserving_state_transition` | Path B — `NSSATransaction::PrivacyPreserving` through `execute_check_on_state` with a dev-mode passing proof, reaching checks 5–6 (`check_commitments_are_new` / `check_nullifiers_are_valid`) and `apply_state_diff`: **No panic**, **StateIsolationOnFailure**, **PrivateStateIsolationOnFailure**, **CommitmentInsertion**, **NonceIncrementCorrectness**, **PostStateApplied**, **ReplayRejection**. Balance conservation is intentionally *not* asserted — the synthesised fake receipt bypasses the circuit guarantee. Requires `RISC0_DEV_MODE=1` | `fuzz/fuzz_targets/fuzz_privacy_preserving_state_transition.rs` |
|
||||
|
||||
---
|
||||
|
||||
@ -376,8 +377,8 @@ The nightly AFL++ CI workflow has two jobs:
|
||||
|
||||
| Job | Triggers | Matrix |
|
||||
|-----|----------|--------|
|
||||
| `afl-smoke` | nightly + `workflow_dispatch` | all 20 targets, 60 s each |
|
||||
| `afl-coverage-aggregate` | nightly, `needs: afl-smoke` | all 20 targets merged into one LLVM HTML report |
|
||||
| `afl-smoke` | nightly + `workflow_dispatch` | all 21 targets, 60 s each |
|
||||
| `afl-coverage-aggregate` | nightly, `needs: afl-smoke` | all 21 targets merged into one LLVM HTML report |
|
||||
|
||||
The smoke job (one matrix leg per target, on `ubuntu-latest`):
|
||||
1. Builds AFL++ from source, then builds the target with `cargo afl build --no-default-features --features fuzzer-afl`
|
||||
@ -387,7 +388,7 @@ The smoke job (one matrix leg per target, on `ubuntu-latest`):
|
||||
|
||||
The coverage-aggregate job:
|
||||
1. Downloads every smoke leg's findings
|
||||
2. Rebuilds all 20 targets with `RUSTFLAGS="-C instrument-coverage"`
|
||||
2. Rebuilds all 21 targets with `RUSTFLAGS="-C instrument-coverage"`
|
||||
3. Runs all checked-in corpus + AFL queue inputs through each binary
|
||||
4. Merges every `.profraw` → one `.profdata` → a single combined HTML report via `llvm-cov show`
|
||||
|
||||
@ -620,6 +621,7 @@ Measured on a 4-core x86_64 Linux runner with `RISC0_DEV_MODE=1`:
|
||||
| `fuzz_privacy_preserving_witness` | ~15 000 exec/sec *(estimate)* |
|
||||
| `fuzz_encoding_privacy_preserving` | ~50 000 exec/sec *(estimate)* |
|
||||
| `fuzz_nullifier_set_roundtrip` | ~100 000 exec/sec *(estimate)* |
|
||||
| `fuzz_privacy_preserving_state_transition` | slow — dev-mode proof synthesis + verification per exec dominates runtime *(estimate)* |
|
||||
|
||||
> [!NOTE]
|
||||
> Throughput figures for the five new targets are rough estimates; run `just perf-baseline`
|
||||
@ -691,6 +693,7 @@ from `data`; if a check doesn't depend on the input, write it as a unit test in
|
||||
|
||||
| Item | Notes |
|
||||
|------|-------|
|
||||
| `PrivacyPreservingTransaction` coverage | Excluded from `fuzz_encoding_roundtrip` because its ZK receipt cannot be reconstructed in a fuzzing loop. A dedicated slow target with `RISC0_DEV_MODE=1` and `proptest` should be added after the current targets are stable |
|
||||
| `PrivacyPreservingTransaction` encoding | Still excluded from `fuzz_encoding_roundtrip` because a real ZK receipt cannot be reconstructed in a fast fuzzing loop. Encoding is instead covered by the dedicated `fuzz_encoding_privacy_preserving` target |
|
||||
| `PrivacyPreservingTransaction` state transition (fake-receipt caveat) | Covered by `fuzz_privacy_preserving_state_transition`, which drives `execute_check_on_state` under `RISC0_DEV_MODE=1` using a synthesised *passing* proof (a dev-mode fake receipt). Because the proof is forced to pass, the circuit's balance-conservation guarantee is bypassed, so this target intentionally does **not** assert balance conservation — see `fuzz_props::privacy::synthesize_passing_proof` for the binding caveat |
|
||||
| `fuzz_validate_execute_consistency` new-account detection | If `execute_check_on_state` creates a brand-new account absent from both the genesis set and the diff, that state-widening will not be detected — full detection requires iterating all accounts in `V03State`, which the API does not currently expose |
|
||||
| LEZ version tracking | There is no submodule pin — `lez-fuzzing` reads `../logos-execution-zone` as checked out. Update that repo to a release tag or a tested commit, then run `just update-lez` (which does `git pull --ff-only`) and open a PR to bump it |
|
||||
| LEZ version tracking | CI pins the upstream revision in a single place — the `ref` input default of `.github/actions/checkout-lez`. PR-gating workflows build against that pinned SHA for reproducibility; the scheduled `lez-compat.yml` workflow overrides it with `main` to flag upstream drift. Locally, `lez-fuzzing` still reads `../logos-execution-zone` as checked out. To bump: update that repo to a tested commit, run `just update-lez` (which does `git pull --ff-only`), replace the SHA in the action, and open a PR |
|
||||
|
||||
@ -84,6 +84,9 @@ pub fn arbitrary_fuzz_state(u: &mut Unstructured<'_>) -> arbitrary::Result<Vec<F
|
||||
///
|
||||
/// Self-transfers (`from_idx == to_idx`) are allowed since they are a useful
|
||||
/// edge case (balance should remain unchanged).
|
||||
///
|
||||
/// The `nonce`/`amount` draw is biased toward valid inputs so the success path
|
||||
/// is actually reached, with a minority branch for the rejection paths.
|
||||
pub fn arb_fuzz_native_transfer(
|
||||
u: &mut Unstructured<'_>,
|
||||
accounts: &[FuzzAccount],
|
||||
@ -93,12 +96,20 @@ pub fn arb_fuzz_native_transfer(
|
||||
}
|
||||
let from_idx = (u8::arbitrary(u)? as usize) % accounts.len();
|
||||
let to_idx = (u8::arbitrary(u)? as usize) % accounts.len();
|
||||
let nonce = u128::arbitrary(u)?;
|
||||
let amount = u128::arbitrary(u)?;
|
||||
|
||||
let from = &accounts[from_idx];
|
||||
let to = &accounts[to_idx];
|
||||
|
||||
let (nonce, amount) = if bool::arbitrary(u)? {
|
||||
// Biased valid: nonce near the genesis value, amount within balance.
|
||||
let nonce = u128::from(u8::arbitrary(u)?) % 4; // 0..=3
|
||||
let amount = u128::arbitrary(u)? % from.balance.saturating_add(1); // 0..=balance
|
||||
(nonce, amount)
|
||||
} else {
|
||||
// Adversarial: full range drives the rejection paths.
|
||||
(u128::arbitrary(u)?, u128::arbitrary(u)?)
|
||||
};
|
||||
|
||||
Ok(
|
||||
common::test_utils::create_transaction_native_token_transfer(
|
||||
from.account_id,
|
||||
|
||||
@ -241,8 +241,10 @@ pub fn arb_privacy_preserving_tx(
|
||||
}
|
||||
}
|
||||
|
||||
// ── public_post_states (length varied to exercise the apply/zip-truncation path) ──
|
||||
let n_post = (u8::arbitrary(u)? as usize) % (public_account_ids.len() + 1);
|
||||
// ── public_post_states ──
|
||||
// Range 0..=len+3 so lengths can exceed the public-account count, exercising
|
||||
// both the truncation path and the oversized/length-mismatch path.
|
||||
let n_post = (u8::arbitrary(u)? as usize) % (public_account_ids.len() + 4);
|
||||
let public_post_states = std::iter::repeat_with(|| arb_account(u))
|
||||
.take(n_post)
|
||||
.collect::<ArbResult<Vec<_>>>()?;
|
||||
|
||||
@ -280,6 +280,7 @@ fn arb_privacy_preserving_tx_generator_invariants() {
|
||||
let mut max_commitments = 0_usize;
|
||||
let mut max_nullifiers = 0_usize;
|
||||
let mut saw_empty_comm_nonempty_null = false;
|
||||
let mut saw_oversize_post_states = false;
|
||||
let mut garbage = 0_usize;
|
||||
let mut saw_garbage = false;
|
||||
|
||||
@ -308,14 +309,16 @@ fn arb_privacy_preserving_tx_generator_invariants() {
|
||||
// The signer count is drawn modulo `max_signers + 1`, so it can never exceed
|
||||
// the cap of 3 distinct signers.
|
||||
assert!(n_signers <= 3, "n_signers {n_signers} exceeds the cap of 3");
|
||||
// Post-states are drawn modulo `public_account_ids.len() + 1`, so there is
|
||||
// never a post-state without a corresponding public account.
|
||||
// Post-states are drawn modulo `public_account_ids.len() + 4` (0..=len+3).
|
||||
assert!(
|
||||
msg.public_post_states.len() <= msg.public_account_ids.len(),
|
||||
"public_post_states {} exceeds public_account_ids {}",
|
||||
msg.public_post_states.len() <= msg.public_account_ids.len() + 3,
|
||||
"public_post_states {} exceeds public_account_ids + 3 ({})",
|
||||
msg.public_post_states.len(),
|
||||
msg.public_account_ids.len()
|
||||
msg.public_account_ids.len() + 3
|
||||
);
|
||||
if msg.public_post_states.len() > msg.public_account_ids.len() {
|
||||
saw_oversize_post_states = true;
|
||||
}
|
||||
// At most 3 signers plus at most 3 extra ids (both deduplicated).
|
||||
assert!(
|
||||
msg.public_account_ids.len() <= 6,
|
||||
@ -406,6 +409,11 @@ fn arb_privacy_preserving_tx_generator_invariants() {
|
||||
saw_empty_comm_nonempty_null,
|
||||
"the generator never produced empty commitments with non-empty nullifiers"
|
||||
);
|
||||
// The oversized shape (more post-states than public account ids) must be reachable.
|
||||
assert!(
|
||||
saw_oversize_post_states,
|
||||
"the generator never produced more post-states than public account ids"
|
||||
);
|
||||
// The garbage-proof branch (~1 in 8) must be reachable at all.
|
||||
assert!(saw_garbage, "the generator never produced a garbage proof");
|
||||
// The garbage-proof rate must sit near the intended 1/8. Integer bands avoid float
|
||||
|
||||
@ -196,6 +196,15 @@ def main() -> None:
|
||||
print()
|
||||
print(" 4. Run with libFuzzer: just fuzz-one", target)
|
||||
print(" Run with AFL++: just fuzz-afl", target)
|
||||
print()
|
||||
print(" 5. This script only edits .github/workflows/fuzz.yml. Add the")
|
||||
print(" target to the other enumeration sites too, then verify with:")
|
||||
print(" python3 scripts/check_target_inventory.py")
|
||||
print(" (the same check runs in CI and will fail the build on drift):")
|
||||
print(" .github/workflows/fuzz-afl.yml")
|
||||
print(" .github/workflows/mutants.yml")
|
||||
print(" scripts/mutants-corpus-test.sh")
|
||||
print(" README.md, docs/fuzzing.md")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
92
scripts/check_target_inventory.py
Executable file
92
scripts/check_target_inventory.py
Executable file
@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail if any fuzz target registered in fuzz/Cargo.toml is missing from a
|
||||
workflow / script / doc that enumerates the target list.
|
||||
|
||||
`fuzz/Cargo.toml` is the single source of truth: every `[[bin]] name = "fuzz_*"`
|
||||
must be mentioned by name in each of the consumer files below. This guards
|
||||
against the drift that `scripts/add_fuzz_target.py` cannot prevent on its own
|
||||
(it only edits `.github/workflows/fuzz.yml`).
|
||||
|
||||
Usage:
|
||||
python3 scripts/check_target_inventory.py
|
||||
|
||||
Exit code 0 = all consumers list every target; 1 = drift detected (prints the
|
||||
missing target/file pairs). Run from anywhere; paths are resolved relative to
|
||||
the repository root.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Files that enumerate the full target list and must stay in sync with Cargo.toml.
|
||||
# Paths are relative to the repository root.
|
||||
CONSUMERS = [
|
||||
".github/workflows/fuzz.yml",
|
||||
".github/workflows/fuzz-afl.yml",
|
||||
".github/workflows/mutants.yml",
|
||||
"scripts/mutants-corpus-test.sh",
|
||||
"README.md",
|
||||
"docs/fuzzing.md",
|
||||
]
|
||||
|
||||
_BIN_NAME_RE = re.compile(r'name\s*=\s*"(fuzz_[a-z0-9_]+)"')
|
||||
|
||||
|
||||
def registered_targets(cargo_toml: Path) -> list[str]:
|
||||
"""Every `[[bin]] name = "fuzz_*"` in fuzz/Cargo.toml, in file order."""
|
||||
names = _BIN_NAME_RE.findall(cargo_toml.read_text())
|
||||
# Preserve order, drop duplicates defensively.
|
||||
seen: set[str] = set()
|
||||
ordered: list[str] = []
|
||||
for n in names:
|
||||
if n not in seen:
|
||||
seen.add(n)
|
||||
ordered.append(n)
|
||||
return ordered
|
||||
|
||||
|
||||
def main() -> None:
|
||||
root = Path(__file__).parent.parent # repository root
|
||||
cargo_toml = root / "fuzz" / "Cargo.toml"
|
||||
if not cargo_toml.exists():
|
||||
print(f"ERROR: {cargo_toml} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
targets = registered_targets(cargo_toml)
|
||||
if not targets:
|
||||
print(f"ERROR: no [[bin]] targets found in {cargo_toml}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
missing: list[tuple[str, str]] = []
|
||||
for rel in CONSUMERS:
|
||||
path = root / rel
|
||||
if not path.exists():
|
||||
print(f"ERROR: consumer file not found: {rel}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
text = path.read_text()
|
||||
for target in targets:
|
||||
if target not in text:
|
||||
missing.append((rel, target))
|
||||
|
||||
if missing:
|
||||
print(
|
||||
f"Target-inventory drift: {len(targets)} targets registered in "
|
||||
f"fuzz/Cargo.toml, but some consumers are missing entries:\n",
|
||||
file=sys.stderr,
|
||||
)
|
||||
for rel, target in missing:
|
||||
print(f" MISSING {rel} -> {target}", file=sys.stderr)
|
||||
print(
|
||||
"\nAdd the target(s) above to each listed file "
|
||||
"(see scripts/add_fuzz_target.py for the canonical insertion points).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"OK: all {len(CONSUMERS)} consumers list every one of the "
|
||||
f"{len(targets)} registered targets.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -41,6 +41,7 @@ targets=(
|
||||
fuzz_privacy_preserving_witness
|
||||
fuzz_encoding_privacy_preserving
|
||||
fuzz_nullifier_set_roundtrip
|
||||
fuzz_privacy_preserving_state_transition
|
||||
)
|
||||
|
||||
# cargo-fuzz requires the nightly toolchain (-Zsanitizer=address etc.).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user