Skip to content

docs: n-gram edge coverage implementation plan - #5

Merged
daedalus merged 1 commit into
masterfrom
copilot/analyze-afl-shim-implementation
Aug 24, 2026
Merged

docs: n-gram edge coverage implementation plan#5
daedalus merged 1 commit into
masterfrom
copilot/analyze-afl-shim-implementation

Conversation

Copilot AI commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Adds docs/ngram_coverage_plan.md — the implementation plan produced by the prior research session analysing afl_shim.c and its Python integration surface.

Covers:

  • data-structure changes (ring buffer replacing __afl_prev_loc)
  • n-gram FNV-1a edge-id hash
  • reset-path and fork-server requirements
  • Python-side changes (elf.py, ptrace_coverage.py, edge_tracker.py)
  • full compatibility/ABI concerns table
  • complete file-change list
  • test plan (falsification + adversarial + regression + reset invariant)

Summary by Sourcery

Document the implementation plan for adding configurable n-gram edge coverage while preserving compatibility and reliable reset behavior.

Enhancements:

  • Document a complete implementation plan for configurable n-gram edge coverage across the AFL shim and Python integration layers, including hashing, state management, map sizing, compatibility, and reset behavior.

Documentation:

  • Add user-facing documentation describing the proposed n-gram edge coverage design, affected files, compatibility considerations, and validation strategy.

Tests:

  • Define falsification, adversarial load, regression, and reset-invariant tests for the planned n-gram coverage implementation.

Co-authored-by: daedalus <115175+daedalus@users.noreply.github.com>
@sourcery-ai

sourcery-ai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a detailed implementation plan for configurable n-gram edge coverage, covering AFL shim state and hashing, ELF/Python integration, map sizing, ptrace behavior, compatibility and resume handling, affected files, and validation strategy.

Sequence diagram for n-gram coverage reset and edge mapping

sequenceDiagram
    participant Target
    participant AFLShim
    participant SharedMap
    participant ForkServer

    ForkServer->>AFLShim: initialize ring state
    AFLShim->>AFLShim: zero __afl_prev_locs and reset __afl_prev_idx
    Target->>AFLShim: __afl_map_edge(cur_loc)
    AFLShim->>AFLShim: FNV-1a mix ring slots and cur_loc
    AFLShim->>SharedMap: update edge_id bucket
    AFLShim->>AFLShim: append cur_loc >> 1 and advance ring
    AFLShim->>AFLShim: __afl_guarded_reset()
    AFLShim->>AFLShim: zero ring and reset index
Loading

File-Level Changes

Change Details Files
Define the AFL shim changes needed to compute configurable n-gram edge IDs while preserving the existing default behavior.
  • Replace multi-step predecessor state with a compile-time-sized ring buffer for k > 2.
  • Advertise the configured n-gram size through an ELF symbol.
  • Hash predecessor slots and the current location with order-sensitive FNV-1a, then preserve sentinel handling and advance the ring.
  • Reset ring contents and position during guarded reset and fork-server initialization.
src/fuzzer_tool/adapters/afl_shim.c
Specify Python integration for n-gram detection, coverage-map sizing, and ptrace simulation.
  • Detect n-gram size from ELF symbols and include it in map-size estimates.
  • Apply a bounded n-gram inflation factor alongside context inflation.
  • Extend ptrace coverage with a predecessor ring while retaining its context-free behavior.
  • Update edge-tracker map recommendations for n-gram cardinality.
src/fuzzer_tool/core/elf.py
src/fuzzer_tool/services/ptrace_coverage.py
src/fuzzer_tool/core/edge_tracker.py
Document compatibility and resume requirements for coverage IDs that vary with n-gram configuration.
  • Record n-gram configuration in persisted state and reject incompatible resumes.
  • Describe ABI, corpus portability, SHM layout, direct-lite, cmplog/perf, ASLR, and map-pressure implications.
  • Enumerate the complete expected implementation file list.
state.json / resume logic
src/fuzzer_tool/adapters/shm.py
src/fuzzer_tool/core/elf.py
src/fuzzer_tool/core/edge_tracker.py
Define validation coverage for n-gram discrimination, saturation behavior, backward compatibility, and reset correctness.
  • Test distinct predecessor histories, high-load drop rates, k=2 corpus resume, and deterministic post-reset output.
  • Include falsification, adversarial, regression, and reset-invariant scenarios.
docs/ngram_coverage_plan.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@daedalus
daedalus marked this pull request as ready for review August 24, 2026 17:38
Copilot AI lite review requested due to automatic review settings August 24, 2026 17:38
@daedalus
daedalus merged commit e6ab4cf into master Aug 24, 2026
@daedalus
daedalus deleted the copilot/analyze-afl-shim-implementation branch August 24, 2026 17:38

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="docs/ngram_coverage_plan.md" line_range="39-48" />
<code_context>
+uint32_t __afl_prev_loc = 0;
+
+// After
+#ifndef __AFL_NGRAM_K
+#define __AFL_NGRAM_K 2     /* default: retain current 2-node behaviour */
+#endif
+
+#if __AFL_NGRAM_K > 1
</code_context>
<issue_to_address>
**issue (broader_impact):** The documented default `__AFL_NGRAM_K=2` does not retain the existing layout or edge IDs: `#if __AFL_NGRAM_K > 1` selects the new FNV ring path, while the purported original implementation is in the `#else` branch, which is reached only for k <= 1. A default build therefore stops exporting `__afl_prev_loc` and changes every edge ID, breaking the stated k=2 ABI and corpus/resume compatibility.

**Triggers:** When the shim is rebuilt with the documented default configuration.

**Suggested fix:** Use a dedicated `__AFL_NGRAM_K == 2` compatibility branch that preserves `__afl_prev_loc` and the existing XOR hash, and reserve the ring/FNV path for k > 2.
</issue_to_address>

### Comment 2
<location path="docs/ngram_coverage_plan.md" line_range="168-173" />
<code_context>
+The ptrace path simulates `(rel ^ self.prev_location) % map_size`.
+Extend with a ring:
+```python
+# prev_locations: collections.deque(maxlen=k-1), initially all-zero
+edge_id = rel
+for p in self.prev_locations:
+    edge_id ^= p          # simple XOR chain is fine for ptrace (no perf path)
+bucket = edge_id % self.map_size
+self.prev_locations.appendleft(rel >> 1)
+```
+Note: the ptrace path has no `caller_ctx` and never will; this is an
</code_context>
<issue_to_address>
**issue (bug_risk):** The ptrace design replaces `prev_location` with `prev_locations` but does not specify updating `reset_edge_map()` to clear or recreate the deque. After one execution, the next execution hashes its first breakpoint against the previous execution's history, so identical inputs produce different coverage across iterations.

**Triggers:** When ptrace coverage runs more than one target execution on the same `PtraceCoverage` instance.

**Suggested fix:** Clear and reinitialize `prev_locations` in `reset_edge_map()` alongside the existing edge-map reset.
</issue_to_address>

### Comment 3
<location path="docs/ngram_coverage_plan.md" line_range="151" />
<code_context>
+   (drop-rate exceeds 1 % at load > 0.75, see `afl_shim.c:336–339`).
+
+4. **`MapSizeEstimate` (`elf.py:1670`)**: add `ngram_k: int` field alongside
+   `ctx_bits: int`.
+
+### `shm.py` — no layout change
</code_context>
<issue_to_address>
**issue (bug_risk):** Adding `ngram_k` to the `MapSizeEstimate` NamedTuple requires updating every positional constructor, but the complete file-change list only calls out the field addition. The existing `MapSizeEstimate(MAP_SIZE_DEFAULT, 0, "default", ctx_bits, False)` and five-argument success constructor will raise `TypeError` once the tuple has six fields.

**Triggers:** When the planned `MapSizeEstimate` field is implemented without updating all constructors and tests.

**Suggested fix:** Update every `MapSizeEstimate(...)` construction and all positional consumers to carry `ngram_k`, preferably using named arguments for this metadata tuple.
</issue_to_address>

### Comment 4
<location path="docs/ngram_coverage_plan.md" line_range="131-133" />
<code_context>
+
+### `elf.py` — detection and map-size estimation
+
+1. **`detect_ngram_k(target: str) -> int`** (new, mirrors `detect_ctx_bits`
+   at `elf.py:1587`): scan the ELF symtab for `__afl_ngram_k_N`; return N.
+   Return 2 (current default) when the symbol is absent.
+
+2. **`ngram_inflation_factor(k: int) -> float`**: k-gram cardinality grows
</code_context>
<issue_to_address>
**issue (bug_risk):** The proposed detector returns a single unspecified `N` even though a shared object can contain multiple instrumented translation units and therefore multiple `__afl_ngram_k_N` symbols. Without the existing detector's explicit widest-value policy or a validation error for disagreement, map sizing can be based on a smaller k than one of the loaded TUs uses.

**Triggers:** When a target or shared library contains TUs built with different n-gram settings.

**Suggested fix:** Scan all marker symbols, reject conflicting values or deliberately select the maximum, and document that policy as `detect_ctx_bits` does.

```suggestion
1. **`detect_ngram_k(target: str) -> int`** (new, mirrors `detect_ctx_bits`
   at `elf.py:1587`): scan all ELF symtab markers matching `__afl_ngram_k_N`;
   if values disagree, select the maximum so the map is sized for the widest
   instrumented translation unit, matching `detect_ctx_bits`'s policy. Return 2
   (current default) when the symbol is absent.
```
</issue_to_address>

### Comment 5
<location path="docs/ngram_coverage_plan.md" line_range="44-45" />
<code_context>
+#endif
+
+#if __AFL_NGRAM_K > 1
+static uint32_t __afl_prev_locs[__AFL_NGRAM_K - 1];
+static uint8_t  __afl_prev_idx  = 0;
+#else
+uint32_t __afl_prev_loc = 0;   /* k=1: unchanged ABI */
</code_context>
<issue_to_address>
**issue (bug_risk):** The ring index is declared as `uint8_t`, so it cannot represent the full index range when configurable `__AFL_NGRAM_K - 1` exceeds 255. For k > 256 the index wraps at 256 rather than at the ring length, causing out-of-range accesses or incorrect history selection.

**Triggers:** When a build sets `__AFL_NGRAM_K` above 256.

**Suggested fix:** Use an index type wide enough for the configured ring or enforce a compile-time maximum and static assertion for `__AFL_NGRAM_K`.
</issue_to_address>

Sourcery assessment

Approval pending. 5 findings to address first.

Blocking findings: docs/ngram_coverage_plan.md:48, docs/ngram_coverage_plan.md:173, docs/ngram_coverage_plan.md:151, docs/ngram_coverage_plan.md:133, docs/ngram_coverage_plan.md:45


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +39 to +48
#ifndef __AFL_NGRAM_K
#define __AFL_NGRAM_K 2 /* default: retain current 2-node behaviour */
#endif

#if __AFL_NGRAM_K > 1
static uint32_t __afl_prev_locs[__AFL_NGRAM_K - 1];
static uint8_t __afl_prev_idx = 0;
#else
uint32_t __afl_prev_loc = 0; /* k=1: unchanged ABI */
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (broader_impact): The documented default __AFL_NGRAM_K=2 does not retain the existing layout or edge IDs: #if __AFL_NGRAM_K > 1 selects the new FNV ring path, while the purported original implementation is in the #else branch, which is reached only for k <= 1. A default build therefore stops exporting __afl_prev_loc and changes every edge ID, breaking the stated k=2 ABI and corpus/resume compatibility.

Triggers: When the shim is rebuilt with the documented default configuration.

Suggested fix: Use a dedicated __AFL_NGRAM_K == 2 compatibility branch that preserves __afl_prev_loc and the existing XOR hash, and reserve the ring/FNV path for k > 2.

Comment on lines +168 to +173
# prev_locations: collections.deque(maxlen=k-1), initially all-zero
edge_id = rel
for p in self.prev_locations:
edge_id ^= p # simple XOR chain is fine for ptrace (no perf path)
bucket = edge_id % self.map_size
self.prev_locations.appendleft(rel >> 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The ptrace design replaces prev_location with prev_locations but does not specify updating reset_edge_map() to clear or recreate the deque. After one execution, the next execution hashes its first breakpoint against the previous execution's history, so identical inputs produce different coverage across iterations.

Triggers: When ptrace coverage runs more than one target execution on the same PtraceCoverage instance.

Suggested fix: Clear and reinitialize prev_locations in reset_edge_map() alongside the existing edge-map reset.

(drop-rate exceeds 1 % at load > 0.75, see `afl_shim.c:336–339`).

4. **`MapSizeEstimate` (`elf.py:1670`)**: add `ngram_k: int` field alongside
`ctx_bits: int`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Adding ngram_k to the MapSizeEstimate NamedTuple requires updating every positional constructor, but the complete file-change list only calls out the field addition. The existing MapSizeEstimate(MAP_SIZE_DEFAULT, 0, "default", ctx_bits, False) and five-argument success constructor will raise TypeError once the tuple has six fields.

Triggers: When the planned MapSizeEstimate field is implemented without updating all constructors and tests.

Suggested fix: Update every MapSizeEstimate(...) construction and all positional consumers to carry ngram_k, preferably using named arguments for this metadata tuple.

Comment on lines +131 to +133
1. **`detect_ngram_k(target: str) -> int`** (new, mirrors `detect_ctx_bits`
at `elf.py:1587`): scan the ELF symtab for `__afl_ngram_k_N`; return N.
Return 2 (current default) when the symbol is absent.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The proposed detector returns a single unspecified N even though a shared object can contain multiple instrumented translation units and therefore multiple __afl_ngram_k_N symbols. Without the existing detector's explicit widest-value policy or a validation error for disagreement, map sizing can be based on a smaller k than one of the loaded TUs uses.

Triggers: When a target or shared library contains TUs built with different n-gram settings.

Suggested fix: Scan all marker symbols, reject conflicting values or deliberately select the maximum, and document that policy as detect_ctx_bits does.

Suggested change
1. **`detect_ngram_k(target: str) -> int`** (new, mirrors `detect_ctx_bits`
at `elf.py:1587`): scan the ELF symtab for `__afl_ngram_k_N`; return N.
Return 2 (current default) when the symbol is absent.
1. **`detect_ngram_k(target: str) -> int`** (new, mirrors `detect_ctx_bits`
at `elf.py:1587`): scan all ELF symtab markers matching `__afl_ngram_k_N`;
if values disagree, select the maximum so the map is sized for the widest
instrumented translation unit, matching `detect_ctx_bits`'s policy. Return 2
(current default) when the symbol is absent.

Comment on lines +44 to +45
static uint32_t __afl_prev_locs[__AFL_NGRAM_K - 1];
static uint8_t __afl_prev_idx = 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The ring index is declared as uint8_t, so it cannot represent the full index range when configurable __AFL_NGRAM_K - 1 exceeds 255. For k > 256 the index wraps at 256 rather than at the ring length, causing out-of-range accesses or incorrect history selection.

Triggers: When a build sets __AFL_NGRAM_K above 256.

Suggested fix: Use an index type wide enough for the configured ring or enforce a compile-time maximum and static assertion for __AFL_NGRAM_K.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new plan document contains internal inconsistencies/inaccurate references (notably around the k-gating/ABI rationale) that should be corrected before merging to avoid misleading future implementation work.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a design/implementation plan document for introducing configurable n-gram edge coverage to the fuzzer’s AFL shim and Python integration, focusing on hashing, reset semantics, sizing, and compatibility.

Changes:

  • Add docs/ngram_coverage_plan.md describing proposed AFL shim ring-buffer + n-gram hashing approach.
  • Document expected Python-side detection/sizing changes and a compatibility + test plan.
File summaries
File Description
docs/ngram_coverage_plan.md New implementation plan for configurable n-gram edge coverage, including shim/Python touchpoints, compatibility notes, and test strategy.
Review details

Suppressed comments (1)

docs/ngram_coverage_plan.md:193

  • The compatibility table claims __afl_prev_loc is “scanned by fuzzer.py:308”, but src/fuzzer_tool/services/fuzzer.py line ~308 defines _AFL_SYMS = ("__afl_area", "__afl_map_shm", "__sanitizer_cov") and does not reference __afl_prev_loc. Please correct/remove this reference (or point to the actual scanning site if it exists) so the ABI concern is accurately justified.
| **Corpus portability** | edge_ids change when k changes; existing `_seen_edge_ids`, `EdgeTracker`, `state.json` are incompatible | Add `ngram_k` to `state.json`; refuse resume if k mismatches |
| **`__afl_prev_loc` ABI** | Symbol is non-static (`afl_shim.c:283`), scanned by `fuzzer.py:308` | Keep symbol at k=2 default; the ring is introduced only at k > 2, emitting a new symbol `__afl_ngram_k_N` |
| **cmplog/perf shims** | Both use `-include afl_shim.c`; new statics duplicate into each TU | No change — `static` globals are already per-TU; ring follows the same pattern |
  • Files reviewed: 1/1 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +38 to +53
// After
#ifndef __AFL_NGRAM_K
#define __AFL_NGRAM_K 2 /* default: retain current 2-node behaviour */
#endif

#if __AFL_NGRAM_K > 1
static uint32_t __afl_prev_locs[__AFL_NGRAM_K - 1];
static uint8_t __afl_prev_idx = 0;
#else
uint32_t __afl_prev_loc = 0; /* k=1: unchanged ABI */
#endif
```

- `__AFL_NGRAM_K = 2` (default) keeps the existing single-word layout so no
existing binaries need recompilation.
- The ring holds exactly k−1 entries; index wraps modulo k−1.
Comment on lines +165 to +176
The ptrace path simulates `(rel ^ self.prev_location) % map_size`.
Extend with a ring:
```python
# prev_locations: collections.deque(maxlen=k-1), initially all-zero
edge_id = rel
for p in self.prev_locations:
edge_id ^= p # simple XOR chain is fine for ptrace (no perf path)
bucket = edge_id % self.map_size
self.prev_locations.appendleft(rel >> 1)
```
Note: the ptrace path has no `caller_ctx` and never will; this is an
acknowledged coverage-mode gap (`ptrace_coverage.py:430` comment).
@daedalus

Copy link
Copy Markdown
Owner

@copilot Fix the code for all comments in this review thread.

When a review comment includes a suggested change, apply the suggestion exactly.

Do not make changes beyond what is described in the linked review thread.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants