Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/AdithyaaSivamal/Agentic-AFL/llms.txt

Use this file to discover all available pages before exploring further.

The fuzzer bridge is the layer between Agentic-AFL and the running AFL++ process. It has two jobs: detecting when AFL++ has stalled on a coverage plateau, and writing solved payloads back into AFL++‘s input corpus. All communication is strictly filesystem-based — no shared memory, no pipes, no sockets. This design guarantees that the agent’s latency (LLM API calls, Ghidra analysis, Z3 solving) never affects AFL++‘s execution throughput.

StallDetector

StallDetector polls AFL++‘s output directory for evidence that coverage has stopped growing. It is called by AgentLoop.run() on every iteration of the main loop. Parsing fuzzer_stats On each detect() call, the detector reads {afl_output_dir}/default/fuzzer_stats (or the first subdirectory containing a fuzzer_stats file in multi-instance setups). It parses the edges_found and cycles_done fields. Two detection modes:

Cycle-based (default)

Triggers when edges_found has not increased for min_stall_cycles consecutive polls. min_stall_cycles must be ≥ 50 to avoid spurious detections. This is the standard mode for batch fuzzing campaigns.

Time-based

When min_stall_time_seconds > 0, triggers when wall-clock time since the last new edge exceeds the threshold. Useful for end-to-end fuzzing campaigns where the cycle counter is less meaningful. When this mode is active, cycle-based detection is bypassed entirely.
Severity computation Each detected stall is assigned a StallSeverity based on how long the plateau has persisted:
SeverityCondition
CRITICALcycles_stalled ≥ min_stall_cycles × 4 — coverage completely blocked
HIGHcycles_stalled ≥ min_stall_cycles × 2
MEDIUMcycles_stalled ≥ min_stall_cycles
LOWcycles_stalled < min_stall_cycles (edge case)
Stall address discovery If stall_address_override is set (recommended when the stall address is known from static analysis), StallDetector returns that address directly. In autonomous mode, it uses a GDB-based frontier discovery strategy: the detector reads function symbols from the binary with nm, generates a GDB Python script that tracks the maximum call stack depth across all breakpoints, runs the binary under GDB with a candidate seed, and reports the function at the deepest call depth as the math wall. Results are cached by seed content hash to avoid redundant GDB runs. Deduplication The same stall address is reported at most once. Once a stall is detected, its address is added to _known_stalls. When a payload is successfully injected for that address, AgentLoop calls resolve_stall(), which additionally adds the address to _resolved_stalls — permanently suppressing re-detection even if coverage later plateaus at the same point. Multi-seed probe StallDetector selects up to 5 AFL++ queue items for GDB probing, prioritizing original corpus seeds (orig: in filename) and sorting by file size descending. Larger seeds are more likely to reach deeper code paths. The detector tries each seed in order and stops on the first one that successfully hits the frontier function’s breakpoint.
from pathlib import Path
from agentic_afl.fuzzer_bridge.stall_detector import StallDetector

detector = StallDetector(
    afl_output_dir=Path("./afl_output"),
    min_stall_cycles=50,
    target_binary=Path("./harness"),
    stall_address_override="0x08001234",  # optional: skip GDB frontier discovery
    min_stall_time_seconds=0,             # 0 = use cycle-based detection (default)
)
stalls = await detector.detect()
for stall in stalls:
    print(stall.stall_address, stall.severity.name)

PayloadInjector

PayloadInjector writes SolvedPayload objects to AFL++‘s sync directory. AFL++ natively monitors external sync directories and ingests new files on its next execution cycle — no AFL++ restart or signaling is required. Atomic write pattern Every payload write follows a temp-file-then-rename protocol:
  1. A NamedTemporaryFile is created in the same directory as the final destination (same filesystem, same mount point)
  2. The payload bytes are written and flushed
  3. os.rename(tmp_path, final_path) is called — this is atomic on POSIX systems because source and destination are on the same filesystem
This guarantees AFL++ never reads a partially written file, regardless of when its sync cycle happens relative to the write. Filename format
agentic_<spec_id_prefix>_<stall_addr>_<YYYYMMDD_HHMMSS>.bin
The spec_id_prefix is the first 8 characters of the VulnerabilitySpec’s SHA-256-derived spec_id. This makes payloads traceable back to the stall site that produced them.
from pathlib import Path
from agentic_afl.fuzzer_bridge.payload_injector import PayloadInjector

injector = PayloadInjector(sync_dir=Path("./afl_output/agentic/queue"))
path = await injector.inject(payload)
print(f"Injected: {path.name}")
AFL++ picks up the file on its next sync cycle. If the payload triggers new coverage, AFL++ minimizes it and adds it to the main corpus automatically.

DiversityGenerator

DiversityGenerator is called automatically by AgentLoop after each successful payload injection. Its purpose is to maximize post-bypass coverage exploration by flooding AFL++‘s sync directory with structurally diverse valid frames for all supported ICS protocol frame types. Why diversity injection matters When the Z3 solver produces a valid payload for one frame type (e.g., a Process Data read-coils frame with correct CRC-32), AFL++ gets one new starting point. But the post-CRC state machine has many handlers — one per frame type. Without diversity injection, AFL++ would need to mutate from a single valid frame and rediscover each handler independently. Diversity injection gives AFL++ a valid entry point for every handler simultaneously, producing a coverage spike proportional to the number of frame types. Frame construction DiversityGenerator builds ICS CRC-32 frames using the _make_ics_frame() function:
Frame layout: [SYNC(2)] [HEADER(4)] [PAYLOAD(N)] [CRC32(4)]
  SYNC:   0xA5 0x5A
  HEADER: type(1) seq(1) payload_len(2 LE)
  CRC32:  IEEE 802.3 over HEADER + PAYLOAD  (computed via zlib.crc32)
The current frame variant table covers 32 frame types across 12 protocol categories: Process Data (read coils, read holding registers, write single coil, read input registers), Parameter Read/Write, Alarm (warning, critical, info), Diagnostics (status, reset, identification, trace capture), Safety (heartbeat SIL3/SIL2, watchdog, emergency stop), Firmware (start, data chunk), Time Sync (valid nsec, UTC source), Auth (token request, challenge response), Config (node ID, watchdog), Network (discover, ping, topology), and Profile (vendor, model, firmware version, serial, capabilities). Each frame’s CRC is verified before injection. Any frame with a CRC mismatch is skipped and logged as an error. Atomic injection All diversity payloads are written using the same temp-file-then-rename pattern as PayloadInjector. Filenames follow the format:
agentic_diverse_<variant_name>_<YYYYMMDD_HHMMSS>.bin
generate_ics_crc32_variants() returns the count of successfully injected payloads, which AgentLoop adds to its payloads_injected metric.
Execution Integrity — Payloads produced by Agentic-AFL are written only to the AFL++ sync directory (SRAM-equivalent input storage). The framework is strictly forbidden from overwriting the CPU’s Program Counter to jump over stall addresses. All constraint solving targets the input bytes that must satisfy the stall-site check — not the branch outcome itself. This design is enforced at the architectural level: PayloadInjector has no mechanism to write to instruction memory, and the Z3 model is applied exclusively to the raw_bytes of a SolvedPayload.

Build docs developers (and LLMs) love