Skip to content

sandbornm/fuzzah

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fuzzah — Fuzzing Assistant Harness

A minimal, target-agnostic kit for running AFL++ fuzz rigs with the same conventions across many targets, driven from either the terminal or an AI coding agent (Claude Code / Codex).

Each target gets three concurrent AFL++ workers (fast + ASAN + explore), a CMPLOG companion, a crash-triage loop that deduplicates by ASAN stack hash, a workflow state machine for crashes, and a shared 5-minute watchdog that respawns anything that dies. Two commands — check-in ("are we finding bugs?") and rig-check ("is the box healthy?") — tell you everything you need in under a second.

Expected layout

fuzzah assumes a strict split between control-plane files and runtime state:

<control-root>/
  fuzzah/                    # reusable toolkit repo
    shared/
    target-template/         # blank per-target script pack
    examples/                # filled-in illustrations of target-template
    .agents/skills/
    .claude/{commands,skills}/
  <target>-setup/            # host-side, versioned target setup
    SETUP.md
    scripts/

$HOME/fuzzig-shared/         # installed shared scripts on the fuzz host
  check-in.sh
  rig-check.sh
  fuzz-watchdog.sh

$HOME/fuzzing/               # live runtime state on the fuzz host
  tools/AFLplusplus/
  targets/<target>/
    src/
    build-afl/
    build-afl-asan/
    build-afl-cmplog/
    seeds/
    findings/
    crashes-triaged/
    scripts/
    SETUP.md

The helpers under shared/ assume that shape. If you keep fuzzah/ nested inside a larger control-plane repo, set FUZZAH_CONTROL_ROOT or rely on the current parent-directory auto-detection.

Is this for me?

Yes, if you want long-lived fuzz rigs that survive OOMs, reboots, and operator absences, across multiple targets, with deduplicated crashes organized for review.

No, if you want a one-shot afl-fuzz run on a single target — just use AFL++ directly.

How it works

One picture: you drive the rig three ways, it runs on one host, and every target is a self-contained loop that finds crashes and files them away for you.

flowchart TB
    subgraph drive["Drive the rig 3 ways"]
        direction LR
        cli["Terminal<br/>check-in &middot; rig-check"]
        agent["Claude Code / Codex<br/>/fuzz-status &middot; /fuzz-review"]
        web["Browser<br/>fuhq dashboard"]
    end

    subgraph host["Fuzz host &mdash; a Linux box, or a VM on your Mac"]
        direction TB
        subgraph rig["One self-contained rig per target &middot; ~/fuzzing/targets/&lt;name&gt;"]
            direction LR
            w["3 AFL++ workers<br/>primary +cmplog &middot; asan &middot; explore"]
            t["Triage loop<br/>dedupe by ASAN stack hash"]
            c["crashes-triaged/&lt;hash&gt;<br/>meta &middot; trace &middot; poc"]
            w --> t --> c
        end
        wd["fuzz-watchdog.timer<br/>relaunches any dead worker every 5 min"]
        wd -. respawn .-> w
    end

    drive --> host
Loading
  • Drive it 3 ways — the same rig is reachable from the terminal, from an AI agent (Claude Code / Codex), or from the fuhq browser dashboard.
  • One rig per target — three AFL++ workers feed a triage loop that deduplicates crashes by ASAN stack hash and drops each unique one into crashes-triaged/<hash>/.
  • Self-healing — a 5-minute watchdog relaunches any worker that dies (OOM, reboot, …), so the rig survives being left alone for weeks.

The on-disk layout under ~/fuzzing/ is the directory tree shown in Expected layout above.


Quickstart

Two paths. Pick the one that matches your host.

Path A — Mac + orb (recommended on macOS)

Prereq: Orbstack installed (brew install --cask orbstack).

# 1. Clone. You can clone anywhere, but ~/fuzzig/fuzzah matches the
#    operator layout used by the local docs and agent instructions.
mkdir -p ~/fuzzig
cd ~/fuzzig
git clone https://github.com/sandbornm/fuzzah.git
cd fuzzah

# 2. Create/reuse the OrbStack VM, install packages, build AFL++, install
#    shared scripts, enable the watchdog, and run smoke checks.
bash shared/setup-macos-orb.sh

# 3. Health check.
bash shared/rig-check.sh

Done. rig-check will report "no targets yet" until you add one (next section).

For a brand-new node where you have not cloned the repo yet, the setup script can bootstrap the checkout first:

curl -fsSL https://raw.githubusercontent.com/sandbornm/fuzzah/main/shared/setup-macos-orb.sh \
  -o /tmp/setup-macos-orb.sh
bash /tmp/setup-macos-orb.sh

That clones fuzzah into ~/fuzzig/fuzzah by default, then re-execs the repo copy of the script.

The script defaults to a VM named fuzzer, Ubuntu 24.04, 10 CPUs, 8 GB RAM, and a 256 GB disk cap. Override these with flags:

bash shared/setup-macos-orb.sh \
  --vm-name fuzzer \
  --distro ubuntu:24.04 \
  --cpus 10 \
  --memory 8G \
  --disk 256G \
  --user "$USER"

If you create the VM manually, use OrbStack's plural flag names:

orb create --cpus 10 --memory 8G --disk 256G --user "$USER" ubuntu:24.04 fuzzer

For any command that should run on the fuzz host, prefer:

bash shared/run-on-fuzz-host.sh '<command that should run on the fuzz host>'

It auto-detects direct Linux vs Orb-on-macOS and makes sure $HOME expands on the fuzz host.

If OrbStack is present but unhealthy, this wrapper now prints a diagnostic hint instead of silently assuming the proxy path works. For a focused host-side debug snapshot, run:

bash shared/orb-debug.sh

If you keep target setup dirs outside the fuzzah/ repo itself, set FUZZAH_CONTROL_ROOT=/path/to/control-plane. If fuzzah/ is nested under a larger control-plane repo that has AGENTS.md / CLAUDE.md at the parent level, the helpers auto-detect that parent as the control root.

Path B — Linux host (bare metal, VM, cloud box)

SSH into the host, then:

git clone https://github.com/sandbornm/fuzzah.git && cd fuzzah

# Install AFL++ and deps
sudo apt-get update && sudo apt-get install -y \
  build-essential clang llvm lld cmake git python3 python3-dev \
  automake libtool pkg-config libglib2.0-dev bison flex gdb jq \
  nftables tmux
mkdir -p $HOME/fuzzing/tools && cd $HOME/fuzzing/tools
git clone --depth 1 https://github.com/AFLplusplus/AFLplusplus.git
cd AFLplusplus && make distrib -j$(nproc)
cd -

# Install shared infrastructure
mkdir -p $HOME/fuzzig-shared $HOME/fuzzing/logs $HOME/.config/systemd/user
cp shared/{check-in,rig-check,fuzz-watchdog}.sh $HOME/fuzzig-shared/
chmod +x $HOME/fuzzig-shared/*.sh
cp shared/fuzz-watchdog.{service,timer} $HOME/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now fuzz-watchdog.timer

# Smoke test
bash shared/rig-check.sh

About Apple container

Apple's container project is relevant as a possible future Linux execution backend on Apple silicon, but it is not a replacement for this rig today. It runs Linux containers as lightweight VMs on macOS and consumes/produces OCI images. That makes it potentially useful for reproducible build/test environments or short-lived isolated fuzz jobs.

It is not a way to fuzz macOS frameworks such as ImageIO inside a Darwin container. Those jobs need to run on the macOS host or a real macOS guest VM. For the existing AFL++ lane, keep OrbStack as the supported backend until there is a concrete reason to add a second Linux backend and maintain the extra host-routing surface.

Sizing

  • CPU: 3 cores per target (fast + asan + explore). 10 cores fits 3 targets with headroom.
  • RAM: 8 GB minimum per target. ASAN shadow memory allocates virtual address space aggressively; 4 GB hosts OOM constantly.
  • Disk: 5–15 GB per target (source + 3 build dirs + findings growth).

Add your first target

The fastest path: let an agent walk you through it.

  • Claude Code: open this directory and say "add a fuzz target for <name>".
  • Codex: same, or invoke $fuzz-add-target.

The fuzz-add-target skill handles everything — copying the template, wiring your build system, scraping seeds, and starting the rig.

By hand

Copy the per-target template and edit four files. Assume target name mytool:

# Create a host-side editable setup dir.
bash shared/scaffold-target.sh mytool

# Sync that setup dir into the fuzz host.
bash shared/sync-target.sh mytool

By default this creates:

<control-root>/mytool-setup/
  SETUP.md
  scripts/

Now edit these files in the host-side mytool-setup/scripts/ dir (every other script auto-derives the target name from its filesystem location):

file what to set
start-fuzz.sh HARNESS_SUBPATH (binary path inside each build dir), HARNESS_ARGS (@@ = input)
build-afl-fast.sh Uncomment your build system (autoconf / cmake / meson), set SRC_GIT_URL; mirror into build-afl-asan.sh and build-afl-cmplog.sh
fetch-seeds.sh Fill SOURCES=() with 2–5 seed repos: `label
filter-seeds.sh Set VALID_MAGIC_HEX (e.g. "25504446" for PDF) and VALID_EXTENSIONS
apt-packages.txt Optional extra Debian packages for this target's build (one per line)

Then bootstrap and launch:

# One command for sync + seed prep + 3 builds + cmin + systemd start.
bash shared/bootstrap-target.sh mytool

# Wait ~30 s for calibration, then:
bash shared/check-in.sh
# mytool  3  <execs/s>  0  0  0  0   ← 3 fuzzers alive, zero crashes yet

Every step is idempotent; re-run the one that failed after fixing the edit.

Before you bootstrap, inspect the target summary:

bash shared/inspect-target.sh mytool

AFL++ assumptions

This toolkit is intentionally opinionated around a baseline AFL++ workflow:

  • fast build
  • ASAN build
  • CMPLOG build
  • three long-lived workers (primary, asan, explore)
  • dictionary support via scripts/<target>.dict
  • seed scraping, filtering, and corpus minimization
  • crash triage via ASAN/GDB-style post-processing

That baseline is what the current helpers, skills, watchdog, and review flow understand.

Not first-class yet

fuzzah does not currently automate grammar-aware or schema-aware target types as a first-class workflow. In practice that means:

  • no built-in custom mutator build/install flow
  • no grammar/tree sidecar directory conventions
  • no grammar-aware seed generation bootstrap
  • no mutator-specific crash replay/triage assumptions

If you want to go there, use the current toolkit as the process shell and layer your mutator work in manually for now. Good starting points:


Daily ops

Two commands cover 90% of what you'll do:

bash shared/check-in.sh    # fuzz domain: crashes, workflow state
bash shared/rig-check.sh   # system domain: memory, OOM, systemd, disk

The fuzzers-alive count shown by check-in.sh is verified via fuzzer_pid + kill -0 (not pgrep), so stale fuzzer_stats files left by a crashed worker do not inflate the count.

Or from an agent session:

task Claude Code Codex
check-in (all targets) /check-in $check-in
system health /rig-check $rig-check
one target's status /fuzz-status mytool $fuzz-status mytool
list crashes /fuzz-crashes mytool $fuzz-crashes mytool
triage one crash /fuzz-review <hash> mytool $fuzz-review <hash> mytool
browser dashboard /fuzz-dashboard bash shared/fuzz-dashboard/run.sh

Crash triage workflow

Every unique crash lands at ~/fuzzing/targets/<t>/crashes-triaged/<hash>/ with meta.json + trace.txt + poc.bin, and moves through states:

new → reviewed → repro-ok → reported     (progress)
  \→ dup                                  (duplicate of another hash)
  \→ ignore                               (false positive / noise)

Mark state:

bash shared/run-on-fuzz-host.sh \
  'echo reviewed > "$HOME/fuzzing/targets/mytool/crashes-triaged/<hash>/.status"'

The fuzz-crash-review skill (Claude + Codex) walks classification — loads the trace, inspects source at the top frame, recommends action.


See it in your browser — the fuhq dashboard

fuhq is a live web view of the whole rig — fuzzer health, per-target stats, and per-crash drill-down with a report-aware priority score that tells you, at a glance, which crashes are worth your time. It's Python stdlib only — nothing to pip install.

1. Start it on the host that can reach your fuzz host (the Mac, or the Linux box itself):

bash shared/fuzz-dashboard/run.sh          # foreground; Ctrl-C to stop
# from an agent session, just say:  /fuzz-dashboard

It binds 127.0.0.1:8765 only — never exposed to the network.

2. Open it. On the same machine, browse to http://localhost:8765. From a different machine (e.g. the dashboard runs on a headless Linux box or your Mac mini), forward the port over SSH first, then browse to localhost:

ssh -L 8765:127.0.0.1:8765 <user>@<host-running-fuhq>
# then open http://localhost:8765

That's the whole setup. Three pages:

page shows
/ health banner, aggregate KPIs, target list
/t/<target> per-worker stats + the crash table, filterable by status and priority (high / med / low / noise)
/c/<target>/<hash> REPORT.md, POC.md, REPRO.md, optional REVIEW.md, NOTES.md, trace, meta.json, PoC hexdump + download

The crash table sorts highest-priority-first, so the handful of crashes that matter float to the top of a list that may be hundreds long. When REPORT.md contains report_priority, that value drives the score and raw hits remain a separate stability signal. Hover any priority tag to see why it scored that way.

Six-hour crash digest

shared/crash-digest/ turns the dashboard into a phone-friendly email workflow. The scheduled entry point is:

bash shared/crash-digest/send-digest.sh

It runs a bounded raw-crash triage pass, promotes a small number of high-signal clusters into reproducible REPORT.md/REPRO.md/POC.md artifacts, collects live state, then sends a Resend email with direct dashboard links. By default, the email top list is stricter than the dashboard: it requires FUZZ_DIGEST_MIN_REPORT_PRIORITY=80 and FUZZ_DIGEST_ONLY_HIGH_VALUE=1, so assertions, UBSan-only reports, JavaScript exceptions, and parser DoS stay visible in the dashboard without being flagged for manual security triage.

Dry-run first:

bash shared/crash-digest/send-digest.sh --dry-run

Install on macOS with launchd jobs for both the dashboard and six-hour digest:

bash shared/crash-digest/install-macos.sh --dry-run
bash shared/crash-digest/install-macos.sh --tailscale-serve

The email intentionally links to crash pages instead of attaching raw PoCs. This keeps mail delivery predictable and preserves the highest-fidelity view: summary, reproducer code, hexdump/download, trace, metadata, and status controls. The HTML email uses a light high-contrast palette because mobile mail clients often rewrite dark-mode colors.

With --tailscale-serve, the dashboard is tailnet-only and read-only. It proxies to 127.0.0.1:8765 via Tailscale Serve; do not enable Tailscale Funnel for crash reports/PoCs.


Orb troubleshooting

On macOS, an OrbStack control-plane failure does not usually mean your fuzz state is gone. The backing disk image persists separately from the CLI state.

  • Orb-backed commands fail but vmgr.log still shows container started: the backend likely booted, but the client/proxy path is wedged.
  • orbctl status says Stopped while the helper is alive: treat orbctl status as advisory only and probe with a real orb -m ... command before assuming the VM is absent.
  • proxy dialer did not pass back a connection or mm_receive_fd: OrbStack is stuck in the proxy handoff path. Fully quit and relaunch the app; if it persists, reboot macOS.
  • very large OrbStack Helper vmgr RSS on the Mac host: the helper is likely wedged; quit and relaunch OrbStack before trusting any status output.

Useful commands:

bash shared/orb-debug.sh
bash shared/rig-check.sh

On macOS, the persisted OrbStack disk image is usually under:

$HOME/Library/Group Containers/*.dev.orbstack/data/data.img.raw

That image is where your ~/fuzzing/targets/*/{findings,crashes-triaged} data lives, so access failures are usually a control-plane problem rather than a data-loss event.


What To Commit

Safe to commit/push from a target setup dir like <control-root>/poppler-setup/ (or one of the illustration examples under fuzzah/examples/, e.g. fuzzah/examples/jq/):

  • SETUP.md
  • scripts/*.sh
  • scripts/*.dict
  • scripts/*-fuzz.service
  • scripts/apt-packages.txt

Do not commit/push runtime artifacts from the fuzz host:

  • src/
  • build-afl/, build-afl-asan/, build-afl-cmplog/
  • large generated seed corpora
  • findings/
  • crashes-triaged/

Design notes

Three workers per target. Standard AFL++ multi-core pattern: primary accumulates coverage-enriched entries; asan runs the ASAN build so crashes come with stack traces without taxing the master's execs/s; explore uses the broader power schedule to prioritize novel paths.

CMPLOG on primary. CMPLOG logs comparison operands at runtime and feeds them back to the mutator — the difference between "random bytes" and "the exact magic sentinel the parser expects" for format-heavy targets. Often worth 10× in coverage growth.

-m 1024 on fast workers, -m none on ASAN. AFL++ applies -m as RLIMIT_AS on the target child. ASAN pre-allocates tebibytes of virtual address space for shadow memory, so any finite limit kills it at startup. Fast workers get 1 GB — high enough for normal decodes, low enough that six concurrent children can't OOM an 8 GB host.

Watchdog at 5 minutes. systemd's Restart=on-failure handles service-level crashes. The watchdog covers the sub-service case where one afl-fuzz worker dies and its siblings keep running — without it, a dead worker silently drops a third of throughput.

Tuning when things feel slow

  • Seeds — the biggest lever. 20 curated seeds beat 2000 random ones. Favor diverse samples. Re-run min-corpus.sh after adding any.
  • Dict files — drop one at scripts/<target>.dict in the target setup (preferred, self-contained) or at $HOME/fuzzing/tools/AFLplusplus/dictionaries/<target>.dict. start-fuzz.sh now prefers the target-local file and falls back to AFL++'s shared dictionaries. AFL++ ships dicts for pdf, json, xml, png, and more.
  • Persistent-mode harness — 10–100× execs/s on fork-heavy targets. Worth it if you're stuck below 1k execs/s on a fast machine.
  • Timeoutstart-fuzz.sh uses 3000 ms (fast) / 5000 ms (ASAN). Raise for legitimate slow inputs; lower if the queue fills with pathological long-runners.

Troubleshooting

Watchdog keeps respawning workers. OOM flap. Run rig-check and read the dmesg section. ASAN children that balloon to 6+ GB are normal (watchdog handles it). Fast-build children over 1 GB are not — inspect with pgrep -af afl-fuzz.

dmesg shows OOM kills at total-vm ≈ 38 TB. That's ASAN shadow memory. The RSS number tells you the real pressure. Sporadic: expected. More than 10/hour: investigate.

check-in shows execs/s < 200 for a target. Either calibration (wait a minute), a slow harness (you may have picked a binary that renders when you wanted a parse), or pathological inputs in the queue (check hangs/).

Memlimit-kill entries cluttering new state. triage-one.sh auto-tags new ones as ignore. For old pre-fix entries, bulk-sweep:

cd ~/fuzzing/targets/<t>/crashes-triaged
for d in */; do
  tf=$(python3 -c "import json;print(json.load(open('$d/meta.json')).get('top_frame',''))" 2>/dev/null)
  [[ "$tf" != "no-frames"* ]] && continue
  asan=$(python3 -c "import json;print(json.load(open('$d/meta.json')).get('fuzzers',{}).get('asan',0))" 2>/dev/null)
  [[ "$asan" == "0" ]] && echo ignore > "$d/.status"
done

Without orb? Use any Linux host. shared/*.sh auto-detects — if $HOME/fuzzing/targets/ exists locally, it runs commands directly instead of via orb.

Without Claude or Codex? Every script in shared/ and target-template/ is pure bash. Agents just wrap them with summaries. Run the rig entirely from the CLI if you prefer.


What's not included

  • No default CVE scrape. Seed sources are target-specific; wire your own in fetch-seeds.sh.
  • No libfuzzer / honggfuzz / libafl integration. AFL++ only by design.
  • No coverage visualization. afl-cov exists; adding it would balloon the kit's scope.
  • No email or Slack alerts. Crashes land in crashes-triaged/<hash>/ and in INDEX.md — wire your own alerting on top.
  • No upstream bug-report templates. Write your own in each target's SETUP.md once you land a real bug.

License

Personal fuzzing rig, shared as-is. AFL++ itself is Apache-2.0 (see upstream). This kit contains no copyleft code; do what you want with it.

About

Target-agnostic AFL++ fuzzing harness with agent integrations (Claude Code + Codex)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors