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.

Agentic-AFL is a neuro-symbolic fuzzing orchestration framework designed around one principle: the fuzzer must never be blocked. The agent runs as an asyncio daemon in a separate process, communicates with AFL++ exclusively through the filesystem, and degrades gracefully under failure rather than crashing. Every inter-component boundary uses typed Python dataclasses defined in models.py, giving the pipeline a clear, auditable data contract from binary ingestion to payload injection.

Component Map

The system is divided into three cooperating subsystems. Each subsystem is isolated from the others by filesystem IPC — no shared memory, no blocking sockets.

Fuzzer Bridge

Monitors AFL++ and writes payloads back to its sync directory.
  • StallDetector — parses fuzzer_stats and plot_data for edge plateaus; performs GDB-based frontier discovery
  • PayloadInjector — atomically writes solved payloads to the AFL++ sync directory
  • DiversityGenerator — after a successful solve, injects valid ICS protocol frame variants for all frame types to maximize post-bypass coverage

Extractor Pipeline

Converts a binary and stall address into a formal constraint description.
  • PCodeSlicer — shells out to Ghidra headless to extract a taint-bounded backward P-Code slice
  • ConstraintProfiler — deterministic heuristic engine that produces structural ConstraintTag sets (no LLM calls)
  • SpecExporter — packages outputs into a VulnerabilitySpec and upserts it to PostgreSQL

Orchestrator

Coordinates the Extract → CARM → Generate → Solve → Inject loop.
  • AgentLoop — the central asyncio daemon; manages the priority queue and ReAct loop
  • CARMRetriever — two-stage PostgreSQL Jaccard retrieval of historical Z3 templates
  • LLMClient — K-way parallel Z3 script generation with multi-provider support
  • Z3Sandbox — subprocess-isolated Z3 script execution with resource limits

Data Flow

Every artifact that crosses a component boundary is a typed dataclass defined in models.py. The pipeline flows as follows:
1

Binary + StallAddr

AFL++ stalls on a coverage plateau. StallDetector emits a StallReport containing the binary path, stall address, severity, and closest seed input.
2

PCodeSlice

PCodeSlicer invokes Ghidra headless analysis and extracts a taint-bounded backward P-Code slice at the stall address. The result is a PCodeSlice dataclass containing ordered PCodeInstruction objects, architecture metadata, and Ghidra’s decompiled C pseudocode.
3

ConstraintProfile

ConstraintProfiler analyzes the PCodeSlice with deterministic heuristics and produces a ConstraintProfile — a frozenset[ConstraintTag] plus numerical density metrics. No LLM call is made at this stage.
4

VulnerabilitySpec

SpecExporter bundles the PCodeSlice and ConstraintProfile into a VulnerabilitySpec, generates a deterministic spec_id (SHA-256 of binary_path + stall_address), and upserts the record to PostgreSQL.
5

StallReport → Z3GenerationRequest

AgentLoop queries CARMRetriever for historical Z3 templates with a similar ConstraintProfile, then constructs a Z3GenerationRequest bundling the spec, seed input, retrieved templates, and correction history.
6

Z3Script (×K)

LLMClient sends the request to the configured LLM provider and receives K candidate Z3Script objects in parallel via asyncio.gather.
7

Z3Result (×K)

Z3Sandbox executes each script in an isolated subprocess and returns a Z3Result per script, carrying a Z3Verdict (SAT / UNSAT / TIMEOUT / SYNTAX_ERROR / RUNTIME_ERROR / UNKNOWN) and, on SAT, a concrete variable assignment model.
8

SolvedPayload → sync_dir/

The best SAT result is converted to a SolvedPayload (raw bytes overlaid onto the seed input), and PayloadInjector atomically writes it to AFL++‘s sync directory. AFL++ ingests the file on its next execution cycle.

Async Architecture

Three design principles govern how the agent interacts with AFL++: 1. Never block AFL++ The agent loop runs as an asyncio daemon process entirely separate from AFL++. All IPC is filesystem-based — the agent reads fuzzer_stats and writes to sync_dir/. AFL++ never waits on the agent; if the agent is computing, AFL++ keeps fuzzing. This ensures the LLM’s response latency (seconds) never impacts AFL++‘s throughput (10,000+ executions per second). 2. Priority queue Detected stalls are enqueued in an asyncio.PriorityQueue keyed by StallSeverity. CRITICAL stalls (zero new edges for N cycles) are dequeued and processed before HIGH, MEDIUM, and LOW stalls. This ensures the most impactful coverage blockages are attacked first regardless of detection order. 3. Graceful degradation If all ReAct turns are exhausted without producing a SAT result, the stall is deferred back to AFL++ for probabilistic mutation. The failure is recorded in the VulnerabilitySpec’s correction_history for future learning. The agent never raises an unhandled exception that would terminate the fuzzing campaign.

PostgreSQL Role

Agentic-AFL uses PostgreSQL as its persistence and retrieval backend rather than a vector database. This is a deliberate architectural choice rooted in the nature of CARM retrieval:
  • Jaccard similarity on formal tag sets, not cosine similarity on embeddings — the ConstraintProfile is a frozenset[ConstraintTag] — a discrete set of structural labels. Jaccard similarity (|A ∩ B| / |A ∪ B|) is the correct metric for comparing set membership, not cosine similarity over dense embedding vectors.
  • Server-side computation — PostgreSQL with the intarray extension computes jaccard_similarity() entirely in the database engine using GIN-indexed INTEGER[] columns. Python receives only the top-N pre-sorted rows — O(log N) index lookup, not an O(N) Python loop over every stored spec.
  • Upsert semantics — re-running the extractor on the same (binary_path, stall_address) pair updates the existing record rather than creating a duplicate, keeping the corpus clean across long fuzzing campaigns.
All filesystem operations that cross the AFL++ boundary — payload writes and temp-file creation — use an atomic write pattern: the data is written to a temporary file in the same directory, then os.rename() is called to move it to the final path. This guarantees AFL++ never reads a partially written payload.

Build docs developers (and LLMs) love