Building Trust Evidence for Open-Source Network Stacks via AI-Guided Fuzzing

Building Trust Evidence for Open-Source Network Stacks via AI-Guided Fuzzing

The Problem

When an engineering team evaluates an open-source networking library — an HTTP/2 parser, a QUIC implementation, a TLS stack, a protobuf decoder — standard due diligence consists of checking GitHub stars, confirming a security policy exists, and verifying known CVEs are patched. None of these signals indicate whether the library crashes on malformed network input. Stars do not correlate with protocol parsing robustness. A security policy does not measure how many HPACK edge cases the implementation handles under adversarial input.

Network libraries are inherently trust boundaries. Any library parsing bytes from an untrusted peer must handle every conceivable byte sequence without crashing, disclosing memory, or entering an undefined state. The only way to build confidence in this property is empirical: deliver a very large number of inputs and observe whether the library survives. Fuzzing does exactly this. The question has always been whether the campaign reaches enough code to be meaningful — a fuzzer exploring only the happy path produces faster unit testing, not security evidence.

AI assistance changes the seed corpus quality problem that has historically constrained coverage. Generating seeds that explore protocol edge cases requires understanding the specification: which values are valid but unusual, which field combinations trigger rarely-executed paths, which boundary conditions the RFC explicitly calls out. An LLM can generate these seeds from RFC text in minutes, seeding the fuzzer’s queue with inputs that would otherwise take weeks of random mutation to discover.

A structured week-long campaign with LLM-generated seeds, coverage-driven mutation, and systematic crash triage produces a fuzz certificate — a quantifiable record of what was tested, to what depth, and with what findings. This document gives OSS adoption review boards concrete security evidence rather than proxy signals.

Threat Model

Remote denial of service via malformed packet. A crafted packet delivered to an application using the library crashes the parser. If the crash is reproducible and the service is internet-facing, the library’s crash constitutes a remotely exploitable DoS vulnerability. This is the most common class of parsing vulnerability in network libraries. HTTP/2, HPACK, QUIC, and TLS all have specification edge cases that implementations handle inconsistently, and inconsistent handling frequently means null pointer dereference or assertion failure on unexpected input.

Heap overflow via integer overflow in length parsing. A packet contains a length field whose value causes an integer overflow in the parser’s size calculation. The parser allocates a buffer smaller than the data that follows, resulting in a heap write beyond the allocation boundary. This class of vulnerability has produced numerous critical CVEs in network libraries — HTTP/2 CONTINUATION frame handling, TLS record length parsing, protobuf varint decoding — and frequently enables remote code execution.

Request smuggling via protocol state machine confusion. The library’s parser accepts a sequence of packets that causes its internal state machine to diverge from the intended protocol state. A downstream proxy or application component makes routing decisions based on the library’s interpretation of the request, while an upstream component interprets the same bytes differently. The discrepancy is exploited to smuggle requests that bypass authentication or rate limiting. Fuzzing with valid-but-unusual state sequences — multiple headers with the same field name, zero-length frames, duplicate pseudo-headers — finds these divergences.

Memory disclosure via bounds error. A read past the end of a buffer in response to a malformed packet returns bytes from adjacent heap allocations to the remote peer. In a library handling TLS, this is structurally similar to the Heartbleed vulnerability: a bounds check failure on a user-supplied length value exposes memory contents. ASAN-instrumented fuzzing detects the out-of-bounds read before the bytes are sent over the network, converting a disclosure vulnerability into a fuzzer crash.

Hardening Configuration

Step 1: AFL++ Harness Setup

A fuzzing harness wraps the target library function so AFL++ can deliver mutated inputs to it. The harness must be minimal: no I/O, no external state, only the parsing logic being tested.

/* fuzz_http2_harness.c — AFL++ persistent mode harness for HTTP/2 frame parser */
/* Compile: afl-clang-fast -o fuzz_http2 fuzz_http2_harness.c -lnghttp2 -fsanitize=address,undefined */
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <nghttp2/nghttp2.h>

/* AFL++ persistent mode: fuzz in-process without fork overhead */
__AFL_FUZZ_INIT();

/* Minimal session callbacks — we only care about parser behaviour */
static int on_frame_recv(nghttp2_session *session,
                          const nghttp2_frame *frame, void *user_data) {
    (void)session; (void)frame; (void)user_data;
    return 0;
}

static int on_data_chunk_recv(nghttp2_session *session, uint8_t flags,
                               int32_t stream_id, const uint8_t *data,
                               size_t len, void *user_data) {
    (void)session; (void)flags; (void)stream_id; (void)data;
    (void)len; (void)user_data;
    return 0;
}

int main(void) {
    nghttp2_session_callbacks *callbacks;
    nghttp2_session *session;

    nghttp2_session_callbacks_new(&callbacks);
    nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, on_frame_recv);
    nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, on_data_chunk_recv);

    __AFL_INIT();

    unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;

    while (__AFL_LOOP(100000)) {
        size_t len = __AFL_FUZZ_TESTCASE_LEN;

        /* Create a fresh server-side session for each test case */
        nghttp2_session_server_new(&session, callbacks, NULL);

        /* Feed the fuzz input to the HTTP/2 parser */
        nghttp2_session_mem_recv(session, buf, len);

        /* Clean up: memory errors during free are also bugs */
        nghttp2_session_del(session);
    }

    nghttp2_session_callbacks_del(callbacks);
    return 0;
}

Build and configure AFL++ for the campaign:

#!/usr/bin/env bash
# setup-fuzzing-campaign.sh
set -euo pipefail

CAMPAIGN_DIR="/opt/fuzz-campaigns/nghttp2-$(date +%Y%m%d)"
mkdir -p "$CAMPAIGN_DIR"/{seeds,findings,corpus,coverage}

# Build the harness with AFL++ instrumentation and sanitisers
CC=afl-clang-fast CXX=afl-clang-fast++ \
  cmake -DCMAKE_BUILD_TYPE=Debug \
        -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \
        -B "$CAMPAIGN_DIR/build" .

cmake --build "$CAMPAIGN_DIR/build" --target fuzz_http2 -- -j"$(nproc)"

echo "[*] Harness built at $CAMPAIGN_DIR/build/fuzz_http2"
echo "[*] Now generate seeds — see Step 2"

Step 2: LLM Seed Corpus Generation from Protocol Specifications

The quality of the seed corpus directly determines how much of the library’s code the fuzzer will reach. An LLM can read RFC sections and generate binary inputs covering edge cases that would otherwise require days of manual corpus construction.

#!/usr/bin/env python3
# generate-seeds.py — LLM-generated seed corpus for HTTP/2 HPACK edge cases
import anthropic
import struct
import os
import sys

OUTPUT_DIR = sys.argv[1] if len(sys.argv) > 1 else "/tmp/fuzz-seeds"
os.makedirs(OUTPUT_DIR, exist_ok=True)

client = anthropic.Anthropic()

PROTOCOL_CONTEXT = """
HTTP/2 frame format (RFC 9113):
- 3 bytes: payload length (24-bit unsigned)
- 1 byte: frame type (0=DATA, 1=HEADERS, 2=PRIORITY, 3=RST_STREAM, 4=SETTINGS, 5=PUSH_PROMISE, 6=PING, 7=GOAWAY, 8=WINDOW_UPDATE, 9=CONTINUATION)
- 1 byte: flags
- 4 bytes: stream identifier (31-bit, MSB reserved)
- N bytes: frame-specific payload

HPACK compressed header block (RFC 7541):
- Indexed Header Field: byte starting with 1xxxxxxx (index into table)
- Literal Header with Incremental Indexing: byte starting with 01xxxxxx
- Literal Header without Indexing: byte starting with 0001xxxx
- Dynamic Table Size Update: byte starting with 001xxxxx
- String literals: 1-bit Huffman flag + 7-bit length prefix, then data

Edge cases documented in the HTTP/2 specification:
- HEADERS frame with END_HEADERS flag not set (requires following CONTINUATION frames)
- PUSH_PROMISE on stream 0 (invalid, should be RST)
- WINDOW_UPDATE with increment of 0 (protocol error)
- Padding length equal to payload length (no remaining payload)
- Settings parameter with unknown identifier (must be silently ignored)
- RST_STREAM on stream 0 (connection error)
- Multiple SETTINGS frames sent before SETTINGS ACK received
"""

SEED_PROMPT = f"""You are generating binary test cases for fuzzing an HTTP/2 frame parser.

{PROTOCOL_CONTEXT}

Generate 30 seed inputs as Python bytes literals covering these categories:
1. Valid frames that exercise each frame type (DATA, HEADERS, SETTINGS, PING, etc.)
2. Frames with maximum valid field values (payload length = 2^24-1, stream ID = 2^31-1)
3. Frames with minimum non-zero field values (payload length = 1, stream ID = 1)
4. Frames with flag combinations that exercise rarely-taken code paths
5. Frames with zero-length payloads for frame types that allow it
6. Sequences that test the CONTINUATION frame requirement after HEADERS without END_HEADERS
7. SETTINGS frames with all known parameters at boundary values
8. GOAWAY frames with various error codes including unknown error codes
9. HPACK integer encoding with multi-byte prefixes (values > 2^N - 1 requiring additional bytes)
10. HPACK Huffman-encoded strings, including empty strings and maximum-length strings

For each seed, output Python code:
seeds = [
    (b'\\x00\\x00\\x06\\x04\\x00\\x00\\x00\\x00\\x00...', 'description of what edge case this exercises'),
    ...
]

Output only valid Python code with the seeds list."""

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=8192,
    messages=[{"role": "user", "content": SEED_PROMPT}]
)

# Execute the LLM output to get the seeds list
exec_globals = {}
try:
    exec(response.content[0].text, exec_globals)
    seeds = exec_globals.get("seeds", [])
except SyntaxError as e:
    print(f"[!] LLM output parsing failed: {e}")
    sys.exit(1)

print(f"[*] Generated {len(seeds)} seeds from LLM")
for i, (seed_bytes, description) in enumerate(seeds):
    seed_path = os.path.join(OUTPUT_DIR, f"seed_{i:03d}")
    with open(seed_path, "wb") as f:
        f.write(seed_bytes)
    print(f"  [{i:03d}] {len(seed_bytes):4d} bytes — {description}")

print(f"\n[*] Seeds written to {OUTPUT_DIR}/")

Step 3: LLM-Assisted AFL++ Dictionary Generation

AFL++ dictionaries direct mutation toward protocol-meaningful byte patterns, dramatically improving the efficiency of bit-flip and splice mutations.

#!/usr/bin/env python3
# generate-dictionary.py — AFL++ dictionary from protocol specification
import anthropic
import sys

client = anthropic.Anthropic()

DICT_PROMPT = """Generate an AFL++ dictionary file for fuzzing an HTTP/2 (RFC 9113) and HPACK (RFC 7541) parser.

AFL++ dictionary format: each entry is a quoted string or hex value on its own line.
String values: keyword="value"
Binary values: keyword=\\x00\\x01...

Include dictionary entries for:
1. HTTP/2 frame type bytes (all 10 defined types)
2. HTTP/2 flag byte values (END_STREAM, END_HEADERS, PADDED, PRIORITY)
3. HPACK indexed header field representations (common indexes 1-61 from static table)
4. HTTP/2 error codes (NO_ERROR through HTTP_1_1_REQUIRED)
5. SETTINGS parameter identifiers (HEADER_TABLE_SIZE through MAX_HEADER_LIST_SIZE)
6. The HTTP/2 client preface magic bytes (PRI * HTTP/2.0\\r\\n\\r\\nSM\\r\\n\\r\\n)
7. HPACK Huffman symbol boundaries (lengths 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 30)
8. Interesting 24-bit payload lengths: 0, 1, 16383, 16384, 16385, 16777215

Output only the dictionary file contents, no prose."""

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=4096,
    messages=[{"role": "user", "content": DICT_PROMPT}]
)

dict_content = response.content[0].text.strip()
dict_path = sys.argv[1] if len(sys.argv) > 1 else "/tmp/http2.dict"

with open(dict_path, "w") as f:
    f.write("# AFL++ dictionary for HTTP/2 and HPACK — LLM-generated\n")
    f.write("# Generated by generate-dictionary.py\n\n")
    f.write(dict_content)

print(f"[*] Dictionary written to {dict_path}")
print(f"[*] Entries: {dict_content.count(chr(10))} lines")

Step 4: Coverage Instrumentation and Campaign Launch

#!/usr/bin/env bash
# launch-campaign.sh — start multi-core AFL++ campaign with coverage tracking
set -euo pipefail

CAMPAIGN_DIR="$1"
HARNESS="$CAMPAIGN_DIR/build/fuzz_http2"
SEEDS_DIR="$CAMPAIGN_DIR/seeds"
DICT="$CAMPAIGN_DIR/http2.dict"
CORPUS_DIR="$CAMPAIGN_DIR/corpus"
FINDINGS_DIR="$CAMPAIGN_DIR/findings"

# Confirm kernel settings for AFL++
echo core | sudo tee /proc/sys/kernel/core_pattern
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null || true

NUM_CORES=$(nproc)
echo "[*] Starting $NUM_CORES-core AFL++ campaign"

# Primary instance with all settings
afl-fuzz \
  -i "$SEEDS_DIR" \
  -o "$FINDINGS_DIR" \
  -x "$DICT" \
  -M "primary" \
  -t 1000 \
  -m 512m \
  -- "$HARNESS" &

# Secondary instances for parallel exploration
for i in $(seq 2 "$NUM_CORES"); do
  afl-fuzz \
    -i "$SEEDS_DIR" \
    -o "$FINDINGS_DIR" \
    -x "$DICT" \
    -S "secondary_$i" \
    -t 1000 \
    -m 512m \
    -- "$HARNESS" &
done

echo "[*] Campaign running — check afl-whatsup $FINDINGS_DIR for status"
echo "[*] Run for minimum 7 days for meaningful coverage evidence"

Build a separate coverage-instrumented binary for measuring which library code the fuzzer reaches:

#!/usr/bin/env bash
# measure-coverage.sh — run corpus through gcov-instrumented build to measure coverage
set -euo pipefail

CAMPAIGN_DIR="$1"
FINDINGS_DIR="$CAMPAIGN_DIR/findings"

# Build with gcov instrumentation
CC=gcc CFLAGS="--coverage -O0 -fno-omit-frame-pointer" \
  cmake -B "$CAMPAIGN_DIR/cov-build" .
cmake --build "$CAMPAIGN_DIR/cov-build" --target fuzz_http2 -- -j"$(nproc)"

# Run all queued corpus inputs through the coverage build
find "$FINDINGS_DIR" -path "*/queue/id:*" -type f \
  | xargs -P"$(nproc)" -I{} "$CAMPAIGN_DIR/cov-build/fuzz_http2" {} 2>/dev/null || true

# Aggregate coverage with lcov
lcov --capture \
     --directory "$CAMPAIGN_DIR/cov-build" \
     --output-file "$CAMPAIGN_DIR/coverage/total.info" \
     --rc lcov_branch_coverage=1

genhtml "$CAMPAIGN_DIR/coverage/total.info" \
        --output-directory "$CAMPAIGN_DIR/coverage/report" \
        --branch-coverage

# Extract summary stats for fuzz certificate
lcov --summary "$CAMPAIGN_DIR/coverage/total.info" 2>&1 \
  | tee "$CAMPAIGN_DIR/coverage/summary.txt"

echo "[*] Coverage report: $CAMPAIGN_DIR/coverage/report/index.html"

Step 5: Crash Triage with ASAN

#!/usr/bin/env bash
# triage-crashes.sh — classify crashes by vulnerability type
set -euo pipefail

CAMPAIGN_DIR="$1"
FINDINGS_DIR="$CAMPAIGN_DIR/findings"
HARNESS="$CAMPAIGN_DIR/build/fuzz_http2"

CRASH_REPORT="$CAMPAIGN_DIR/crash-triage.json"
echo '{"crashes":[' > "$CRASH_REPORT"
FIRST=1

find "$FINDINGS_DIR" -path "*/crashes/id:*" -type f | while read -r crash_file; do
    # Run crash through ASAN-instrumented binary and capture output
    ASAN_OUTPUT=$(ASAN_OPTIONS="symbolize=1:detect_leaks=0" \
      timeout 10 "$HARNESS" "$crash_file" 2>&1 || true)

    # Classify by ASAN error type
    CRASH_TYPE="unknown"
    SEVERITY="unknown"

    if echo "$ASAN_OUTPUT" | grep -q "heap-buffer-overflow"; then
        CRASH_TYPE="heap-buffer-overflow"
        SEVERITY="CRITICAL"  # potential RCE
    elif echo "$ASAN_OUTPUT" | grep -q "stack-buffer-overflow"; then
        CRASH_TYPE="stack-buffer-overflow"
        SEVERITY="CRITICAL"
    elif echo "$ASAN_OUTPUT" | grep -q "heap-use-after-free"; then
        CRASH_TYPE="use-after-free"
        SEVERITY="HIGH"
    elif echo "$ASAN_OUTPUT" | grep -q "null-deref\|SEGV on unknown"; then
        CRASH_TYPE="null-dereference"
        SEVERITY="MEDIUM"  # typically DoS
    elif echo "$ASAN_OUTPUT" | grep -q "undefined-behavior"; then
        CRASH_TYPE="undefined-behavior"
        SEVERITY="MEDIUM"
    fi

    # Extract crash stack trace (first 5 frames)
    STACK=$(echo "$ASAN_OUTPUT" | grep -A5 "AddressSanitizer:" | head -10 | tr '\n' '|')

    [[ "$FIRST" -eq 1 ]] || echo "," >> "$CRASH_REPORT"
    FIRST=0

    jq -n \
      --arg file "$crash_file" \
      --arg type "$CRASH_TYPE" \
      --arg sev "$SEVERITY" \
      --arg stack "$STACK" \
      '{file: $file, type: $type, severity: $sev, stack: $stack}' >> "$CRASH_REPORT"

done

echo ']}' >> "$CRASH_REPORT"

echo "[*] Crash triage complete: $CRASH_REPORT"
CRITICAL=$(jq '[.crashes[] | select(.severity == "CRITICAL")] | length' "$CRASH_REPORT")
HIGH=$(jq '[.crashes[] | select(.severity == "HIGH")] | length' "$CRASH_REPORT")
MEDIUM=$(jq '[.crashes[] | select(.severity == "MEDIUM")] | length' "$CRASH_REPORT")

echo "  CRITICAL: $CRITICAL | HIGH: $HIGH | MEDIUM: $MEDIUM"

Step 6: Producing a Fuzz Certificate

#!/usr/bin/env python3
# generate-fuzz-certificate.py — produce structured adoption evidence record
import json
import subprocess
import sys
import datetime
import hashlib
import anthropic
from pathlib import Path

CAMPAIGN_DIR = Path(sys.argv[1])
LIBRARY_NAME = sys.argv[2]   # e.g. "nghttp2"
LIBRARY_VERSION = sys.argv[3] # e.g. "1.62.0"

def read_coverage_summary(campaign_dir: Path) -> dict:
    summary_file = campaign_dir / "coverage" / "summary.txt"
    if not summary_file.exists():
        return {"line_coverage": "N/A", "branch_coverage": "N/A"}

    text = summary_file.read_text()
    lines_pct = "N/A"
    branch_pct = "N/A"

    for line in text.splitlines():
        if "lines......" in line:
            lines_pct = line.strip().split()[-1]
        if "branches.." in line:
            branch_pct = line.strip().split()[-1]

    return {"line_coverage": lines_pct, "branch_coverage": branch_pct}

def read_afl_stats(campaign_dir: Path) -> dict:
    stats_file = campaign_dir / "findings" / "primary" / "fuzzer_stats"
    if not stats_file.exists():
        return {}

    stats = {}
    for line in stats_file.read_text().splitlines():
        if ":" in line:
            key, _, value = line.partition(":")
            stats[key.strip()] = value.strip()

    return {
        "executions": stats.get("execs_done", "N/A"),
        "exec_per_sec": stats.get("execs_per_sec", "N/A"),
        "start_time": stats.get("start_time", "N/A"),
        "last_update": stats.get("last_update", "N/A"),
        "unique_crashes": stats.get("saved_crashes", "N/A"),
        "unique_hangs": stats.get("saved_hangs", "N/A"),
        "paths_found": stats.get("paths_total", "N/A"),
    }

def summarise_crashes_with_llm(crash_report: dict) -> str:
    client = anthropic.Anthropic()
    crashes_text = json.dumps(crash_report["crashes"][:20], indent=2)

    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": f"""Summarise these fuzzing crash findings for a security adoption review board.
Be concise (3-5 sentences). Cover: crash count by severity, whether any CRITICAL/HIGH findings
indicate blocking issues for adoption, and what additional action is recommended.

Crashes:
{crashes_text}"""
        }]
    )
    return response.content[0].text

crash_report_path = CAMPAIGN_DIR / "crash-triage.json"
crash_data = json.loads(crash_report_path.read_text()) if crash_report_path.exists() else {"crashes": []}
coverage = read_coverage_summary(CAMPAIGN_DIR)
afl_stats = read_afl_stats(CAMPAIGN_DIR)
crash_summary = summarise_crashes_with_llm(crash_data)

total_crashes = len(crash_data["crashes"])
critical_crashes = sum(1 for c in crash_data["crashes"] if c["severity"] == "CRITICAL")
high_crashes = sum(1 for c in crash_data["crashes"] if c["severity"] == "HIGH")

certificate = {
    "fuzz_certificate": {
        "version": "1.0",
        "issued_at": datetime.datetime.utcnow().isoformat() + "Z",
        "library": {
            "name": LIBRARY_NAME,
            "version": LIBRARY_VERSION,
        },
        "campaign": {
            "tool": "AFL++ 4.x with LLM-generated seed corpus",
            "harness": "Persistent-mode, ASAN+UBSAN instrumented",
            "duration_days": 7,
            "protocol": "HTTP/2 (RFC 9113) + HPACK (RFC 7541)",
            **afl_stats
        },
        "coverage": coverage,
        "findings": {
            "total_unique_crashes": total_crashes,
            "critical": critical_crashes,
            "high": high_crashes,
            "medium": sum(1 for c in crash_data["crashes"] if c["severity"] == "MEDIUM"),
            "crashes": crash_data["crashes"][:50],
        },
        "llm_summary": crash_summary,
        "adoption_recommendation": (
            "REJECT — critical memory corruption found, do not adopt" if critical_crashes > 0
            else "REVIEW — high severity findings require vendor response before adoption" if high_crashes > 0
            else "APPROVE — no blocking findings; see full report for medium severity issues"
        ),
    }
}

cert_path = CAMPAIGN_DIR / f"fuzz-certificate-{LIBRARY_NAME}-{LIBRARY_VERSION}.json"
cert_path.write_text(json.dumps(certificate, indent=2))
print(f"[*] Fuzz certificate: {cert_path}")
print(f"[*] Recommendation: {certificate['fuzz_certificate']['adoption_recommendation']}")

Expected Behaviour After Hardening

A team evaluating libquiche (Cloudflare’s QUIC implementation) runs the pipeline over seven days. The LLM generates 45 seeds covering QUIC Initial, Handshake, 1-RTT, connection close, and CRYPTO frames from RFC 9000 edge case tables. AFL++ expands the corpus to 12,000 unique inputs. Coverage reaches 68% line coverage and 54% branch coverage of parsing functions.

Crash triage identifies three unique crashes: two null-pointer dereferences on zero-length CRYPTO frames (MEDIUM, DoS class), and one heap-buffer-overflow in the connection ID parser (CRITICAL). The fuzz certificate recommendation is REJECT. The team files upstream issues; the vendor patches the critical finding within two weeks. A second five-day campaign against the patched version finds no new critical or high crashes, producing a second certificate with recommendation APPROVE. The library enters the internal approved dependency registry.

Trade-offs and Operational Considerations

Consideration Impact Mitigation
Seven-day campaign requires dedicated compute resources CPU cost for multi-core fuzzing can be significant Cloud spot instances reduce cost; campaign can run at off-peak hours
Fuzzing coverage percentage varies greatly by library complexity 54% branch coverage may miss critical paths in deeply branching parsers Supplement with manual seed crafting for known-complex code sections; document coverage gap
LLM seed generation quality depends on protocol context provided Poor prompt yields redundant seeds with little coverage value Always provide RFC section text, not just protocol name; validate seed count and diversity
ASAN overhead (~2x slowdown) reduces fuzzing throughput Fewer executions per hour means less explored coverage Run ASAN campaign in parallel with a non-instrumented high-throughput campaign; triage ASAN crashes separately
Fuzzing finds input-triggered bugs only — not logic bugs Supply chain tampering or intentional backdoors not detected by fuzzing Fuzzing is one layer; combine with static analysis and binary diffing
Certificate scope is limited to the harness function boundary Bugs in library initialisation or teardown paths may not be exercised Write multiple harnesses covering different entry points
LLM cannot generate seeds for proprietary protocols Libraries implementing non-standardised protocols require manual corpus construction Request protocol specification from vendor; manually construct boundary-case seeds

Failure Modes

Failure Mode Trigger Detection Response
AFL++ hangs indefinitely on non-instrumented binary Harness compiled without AFL++ instrumentation AFL++ reports 0 exec/sec or “no instrumentation detected” Rebuild harness with afl-clang-fast; verify AFL_INST_RATIO in fuzzer stats
ASAN false positive on LTO-compiled library Link-time optimisation changes address arithmetic ASAN reports error that cannot be reproduced without LTO Rebuild target library with matching ASAN flags; disable LTO during fuzzing
Coverage report shows 0% for target functions Linker dead-strips the target function gcov data file empty or missing for target TU Verify harness actually calls the target function; add __attribute__((used)) if needed
LLM-generated seeds all parse successfully without errors Seeds only cover the happy path; no edge case seeds generated Coverage measurement shows only common code paths reached Provide specific RFC edge-case sections in the LLM prompt; manually add malformed samples
Campaign finds no crashes but coverage is low Fuzzer stuck in local maximum, not reaching deep parsing code AFL++ “cycles done” counter increases but coverage stagnates Add more seeds from crash corpora of similar libraries; enable AFL++ MOpt mutation scheduler
Certificate generation fails due to missing campaign output files Campaign terminated early or stats not flushed Missing fuzzer_stats or coverage summary files Always run afl-whatsup before certificate generation to confirm campaign completed
Triage misclassifies CRITICAL crash as MEDIUM ASAN output format changes between versions Critical crash incorrectly recommended for adoption Pin ASAN version; run manual verification on all crashes before certificate issuance