Skip to content

Add entropic schedule, autotokens, crash clustering - #4

Merged
daedalus merged 2 commits into
masterfrom
copilot/trivial-low-effort-changes
Aug 24, 2026
Merged

Add entropic schedule, autotokens, crash clustering#4
daedalus merged 2 commits into
masterfrom
copilot/trivial-low-effort-changes

Conversation

CopilotAI commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an entropic scheduling mode, an autotokens dictionary builder, and wires crash clustering into the report. Updates docs (README, DEEP_DIVE, TODO) to reflect the new capabilities.

Changes

  • Entropic schedule, autotokens dictionary builder, crash clustering wired into report
  • Documentation updates for the above features

Summary by Sourcery

Add entropic scheduling, automated token dictionary generation, and clustered crash reporting.

New Features:

  • Add the libFuzzer-inspired entropic power schedule for allocating more mutation energy to seeds covering rare features.
  • Add corpus autotokenization that generates AFL-format dictionaries during corpus import.
  • Include stack-similarity crash clusters in generated reports to consolidate related crash signatures.

Enhancements:

  • Expose the new scheduling and corpus-import capabilities through the CLI and document their usage and status.

Documentation:

  • Update the README, deep-dive guide, TODO, and research status documentation for entropic scheduling, autotokens, and crash clustering.

Tests:

  • Add coverage for token extraction and dictionary generation, entropic scheduling, and crash-cluster reporting.

CopilotAIand others added 2 commits August 24, 2026 15:48
…tering into report
Co-authored-by: daedalus <115175+daedalus@users.noreply.github.com>
Co-authored-by: daedalus <115175+daedalus@users.noreply.github.com>
@sourcery-ai

sourcery-aiBot commented Aug 24, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR adds three integrated capabilities: an entropic scheduler that boosts seeds hitting rare tracked features, corpus-import autotokenization that writes ranked AFL-format dictionaries, and stack-similarity clustering that consolidates likely duplicate crash signatures in reports. CLI options, implementation documentation, and focused tests are updated for each feature.

Sequence diagram for corpus import with autotoken dictionary generation

sequenceDiagram
participant User
participant CLI as cmd_import
participant Importer as import_corpus
participant Corpus as DestinationCorpus
participant Dictionary as AFLDictionary
User->>CLI: cmd_import
CLI->>Importer: import_from_afl / import_from_libfuzzer / import_from_honggfuzz
Importer->>Corpus: write imported seeds
CLI->>Importer: build_autotoken_dictionary
Importer->>Corpus: discover_seed_files
Corpus-->>Importer: seed files
Importer->>Importer: extract_tokens
Importer->>Dictionary: write_dictionary
Dictionary-->>User: AFL-format dictionary
Loading

Sequence diagram for crash clustering in reports

sequenceDiagram
participant Report as report
participant Signatures as crash_sigs
participant Clusterer as cluster_crashes
participant Output as CrashReport
Report->>Signatures: read signatures and frame lists
Report->>Clusterer: cluster_crashes(sig_list, frame_lists)
Clusterer-->>Report: stack-similarity clusters
Report->>Output: append clustered signature summary
Output-->>Report: consolidated likely bug counts
Loading

Flow diagram for entropic seed scheduling

flowchart LR
Seed[Seed] --> Features[rare_edge_count / tc_ref]
Features --> Rare[rare = max tracked feature counts]
Rare --> Factor[_entropic_factor]
Factor --> Energy[Mutation energy]
Energy --> Mutate[Mutation budget]
Loading

File-Level Changes

ChangeDetailsFiles
Add entropic seed power scheduling based on rare-feature ownership.
  • Expose entropic through scheduler validation and the fuzz CLI.
  • Scale mutation energy by 1 + log2(1 + max(rare_edge_count, tc_ref)), preserving neutral energy when no rare features are tracked.
  • Document and test schedule behavior and score integration.
src/fuzzer_tool/core/schedules.py
src/fuzzer_tool/cli/commands.py
tests/test_schedules.py
README.md
docs/DEEP_DIVE.md
docs/TODO.md
docs/web_research_port_candidates_2026-08.md
Generate AFL-compatible token dictionaries while importing corpora.
  • Extract bounded maximal runs of identifier-like ASCII bytes from each seed.
  • Rank tokens by distinct-seed document frequency and cap the dictionary at 200 entries by default.
  • Add --autotokens FILE to both import entry points and escape tokens into AFL dictionary syntax.
  • Cover token extraction, ranking, limits, empty corpora, and dictionary round-tripping.
src/fuzzer_tool/services/import_corpus.py
src/fuzzer_tool/cli/commands.py
tests/test_import_corpus.py
README.md
docs/DEEP_DIVE.md
docs/TODO.md
docs/web_research_port_candidates_2026-08.md
Include stack-similarity crash clustering in generated crash reports.
  • Invoke the existing cluster_crashes logic when multiple crash signatures are present.
  • Report aggregate counts and members for multi-signature clusters while retaining the signature histogram.
  • Add coverage for similar-stack clustering in report generation.
src/fuzzer_tool/services/report.py
tests/test_report.py
docs/DEEP_DIVE.md
docs/TODO.md
docs/web_research_port_candidates_2026-08.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 16:40
CopilotAI lite review requested due to automatic review settings August 24, 2026 16:40
@daedalus
daedalus merged commit 2dfabc8 into masterAug 24, 2026
2 checks passed
@daedalus
daedalus deleted the copilot/trivial-low-effort-changes branch August 24, 2026 16:41

@sourcery-aisourcery-aiBot 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 1 issue

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments### Comment 1
<locationpath="src/fuzzer_tool/services/report.py"line_range="317-318" />
<code_context>
+ if len(sigs) > 1:
+ from fuzzer_tool.core.crash_metadata import cluster_crashes++ sig_list = list(sigs.keys())+ frame_lists = [frames.get(s, []) for s in sig_list]+ clusters = cluster_crashes(sig_list, frame_lists=frame_lists)+ multi = [c for c in clusters if len(c) > 1]+ if multi:+ lines.append("")
</code_context>
<issue_to_address>
**issue (bug_risk):** When two crash signatures have no recorded stack frames, `frame_lists` still contains an empty list for each signature, so `cluster_crashes` compares the empty frame sequences and treats them as identical with similarity 1.0. The report therefore merges unrelated unsymbolized crashes into one likely bug and reports an incorrect distinct-bug count.
**Triggers:** When multiple signatures lack entries or usable frame lists in `f.crash_frames`.
**Suggested fix:** Use signature similarity when either member lacks usable frames, or omit frame-based clustering for signatures without non-empty frame lists.
```suggestion frame_lists = [frames.get(s, []) for s in sig_list] clusters = cluster_crashes( sig_list, frame_lists=frame_lists if all(frame_lists) else None, )```
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: src/fuzzer_tool/services/report.py:318


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 +317 to +318
frame_lists = [frames.get(s, []) for s in sig_list]
clusters = cluster_crashes(sig_list, frame_lists=frame_lists)

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): When two crash signatures have no recorded stack frames, frame_lists still contains an empty list for each signature, so cluster_crashes compares the empty frame sequences and treats them as identical with similarity 1.0. The report therefore merges unrelated unsymbolized crashes into one likely bug and reports an incorrect distinct-bug count.

Triggers: When multiple signatures lack entries or usable frame lists in f.crash_frames.

Suggested fix: Use signature similarity when either member lacks usable frames, or omit frame-based clustering for signatures without non-empty frame lists.

Suggested change
frame_lists= [frames.get(s, []) forsinsig_list]
clusters=cluster_crashes(sig_list, frame_lists=frame_lists)
frame_lists= [frames.get(s, []) forsinsig_list]
clusters=cluster_crashes(
sig_list,
frame_lists=frame_listsifall(frame_lists) elseNone,
)

CopilotAI 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

There are confirmed functional bugs in crash clustering when crash frames are missing/empty and in fuzzer-tool import format handling that can silently import 0 seeds for libFuzzer corpora.

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

Pull request overview

Adds three user-facing fuzzing improvements—an entropic power schedule, corpus autotoken dictionary generation, and crash-signature clustering in reports—plus documentation and tests covering the new behavior.

Changes:

  • Add --schedule entropic (libFuzzer-style rare-feature log scaling) to SeedScorer and CLI.
  • Add corpus autotoken extraction + AFL dictionary writer, wired into import --autotokens FILE.
  • Cluster crash signatures by stack similarity in the report output, with new report tests and doc updates.
File summaries
FileDescription
tests/test_schedules.pyAdds entropic schedule unit tests and score integration check
tests/test_report.pyAdds tests for stack-similarity crash clustering output
tests/test_import_corpus.pyAdds tests for token extraction, autotoken ranking, and dictionary round-trip
src/fuzzer_tool/services/report.pyWires crash clustering into crash-signature report section
src/fuzzer_tool/services/import_corpus.pyImplements token extraction, autotoken dictionary build, and AFL dict writer + CLI flag
src/fuzzer_tool/core/schedules.pyAdds entropic schedule option and _entropic_factor()
src/fuzzer_tool/cli/commands.pyExposes entropic schedule and import --autotokens in main CLI
README.mdDocuments entropic schedule and autotokens import flag
docs/web_research_port_candidates_2026-08.mdUpdates port-candidate status to reflect landed items
docs/TODO.mdMarks the “trivial items” tier as completed and documents what landed
docs/DEEP_DIVE.mdDocuments entropic schedule, autotokens import flow, and crash clustering behavior
Review details

Suppressed comments (1)

src/fuzzer_tool/cli/commands.py:2573

  • The fuzzer-tool import subcommand still defaults --format to "afl". Importing a flat libFuzzer corpus without --format will silently import 0 seeds (no queue/ dir) even though services/import_corpus.py's standalone CLI now supports auto-detect. Set the default to None and update the help text to reflect auto-detection.
 import_parser.add_argument(
"--autotokens",
default=None,
metavar="FILE",
help="Also tokenize the destination corpus into a whole-token "
  • Files reviewed: 11/11 changed files
  • Comments generated: 3
  • 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 +316 to +318
sig_list = list(sigs.keys())
frame_lists = [frames.get(s, []) for s in sig_list]
clusters = cluster_crashes(sig_list, frame_lists=frame_lists)
Comment on lines +247 to +251
except OSError:
continue
doc_freq.update(extract_tokens(data, min_len, max_len))

return [tok for tok, _count in doc_freq.most_common(max_tokens)]
Comment on lines 633 to 637
from fuzzer_tool.services.import_corpus import (
build_autotoken_dictionary,
import_from_afl,
import_from_honggfuzz,
import_from_libfuzzer,
@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.

daedalus added a commit that referenced this pull request Aug 29, 2026
Each of these carried a status table where most rows had closed, which buried
the handful of open ones. Same treatment in all three: the closed rows are
named in one line so nobody re-surveys them, and the per-row justification goes
to git history.
`six_source_technique_port.md` — nine of sixteen rows were "already done", six
of them by machinery better than what the source proposed. Pruned to a
name-only list. Kept the note that four of them left a narrow residue that is
genuinely NOT covered, which is R5 and still open, and the "Read this first"
framing, which is the point of the document: it was drafted from sources alone
and the audit against live code killed most of it.
`tigerbeetle_four_fuzzers_port.md` — P1-3 (scheduler convergence), P1-5
(exhaustive enumeration) and P2-6 (negative space) are done, and their whole
sections are gone. What each of them FOUND is not lost: all three have a
learnings note (`docs/learnings/2026-08-21-scheduler-convergence.md`,
`2026-08-22-exhaustive-pool-p1-5.md`, `2026-08-22-count-class-exhaustive.md`)
and the status block now points at those instead of restating them. The 20
remaining `rng.random() < 0.5` coin-flip sites are promoted out of the P1-5
prose into their own open item, since that is a P1-5 follow-up rather than
unfinished P1-5 and was easy to misread as the latter. "Suggested sequence"
was a seven-step plan whose first three steps are done; rewritten to the five
that remain.
`web_research_port_candidates_2026-08.md` — Tier 1 is closed in its entirety
and pruned. Four rows landed 2026-08-24; #4 trace-div/trace-gep and #5 n-gram
edge coverage shipped after this doc was last touched and were still tabled
here as `L`-effort candidates. #6 (Zest validity channel) and #7 (SGFuzz enum
states) shipped as mechanism — `--reject-code` and `__sfuzz_state` both exist —
but each left one open design question, and those two questions are already
tracked in `docs/TODO.md` under Scheduling, so tracking them here as well was
the duplication. Tier 2/3 are untouched and still unstarted; the note that
their effort estimates are unaudited guesses now cites Tier 1 as the evidence
for why, since two of its seven rows turned out to be near-free.
Status of every pruned row was checked against live source, not against the
row's own marker.
daedalus added a commit that referenced this pull request Sep 4, 2026
…point
A campaign against ffmpeg_read reported an ASAN heap-buffer-overflow in
mov_read_header. The top two frames were ours:
#0 __afl_put_hexbytes afl_shim.c:1313
#1 __afl_cmplog_bytes afl_shim.c:1419
#2 afl_cmp_strncmp afl_shim.c:1698
#3 strncmp
#4 url_find_protocol
Our instrumentation crashing, filed as a crash in the target.
The length an interceptor passes is a semantic bound -- how many bytes the
real call was permitted to look at -- not a readable one. strncmp stops at
the first NUL or mismatch, so
char *p = malloc(2); p[0] = 'h'; p[1] = 'x';
strncmp(p, "http:", 5);
is legal C: two bytes are compared and byte 2 is never touched. The logger
read all five.
Fix it at __afl_cmplog_bytes rather than in strncmp. There are 18 direct
call sites, each open-coding its own length with a different convention
(n, n + 1, k, a hand-rolled CMPLOG_MAX_OPERAND clamp). strcmp was written
carefully, the memmem site already carries a comment about this exact class
of read, and strncmp and strncasecmp were the ones that were missed. One
guard at the only place that dereferences the operands means the next
interceptor cannot forget it.
Two layers, covering different builds:
- Page clamp, unconditional. A read that stays inside the page holding p
cannot fault, because the caller just read from that page. This is the
only guard a non-ASAN target gets, and it is where it matters most: a
fault in the nosan .so is an in-process SIGSEGV with our frame on top
and no redzone diagnostic to say whose it was.
- __asan_region_is_poisoned when the weak symbol resolves, tightening the
page bound to the exact object bound. NULL in a non-ASAN build, so that
path costs one null check.
Not a truncation to the first mismatch, which was the first thing I tried.
It is provably safe and it guts the record: with buf all 'A',
strncmp(buf, "PREFIX_", 7) mismatches at byte 0, so the literal never
reaches the pool -- and the token is the thing redqueen injects.
test_regression_cmplog_shim_merge catches exactly that, and clamping by
readability instead keeps it green:
before fix: CMP 41414141414141 5052454649585f -15 7
after fix: CMP 41414141414141 5052454649585f -15 7 (unchanged)
overread: CMP 6878 6874 4 5 (was: 5 bytes, ASAN abort)
Adds tests/test_regression_cmplog_strncmp_overread.py, which fails without
the change. It covers strncmp, strncasecmp and a fully readable compare, so
a fix that clamps too aggressively fails the third case.
Two tracecmp_visibility failures in the suite pre-date this change; the same
two fail on a clean tree.
daedalus added a commit that referenced this pull request Sep 4, 2026
…point
A campaign against ffmpeg_read reported an ASAN heap-buffer-overflow in
mov_read_header. The top two frames were ours:
#0 __afl_put_hexbytes afl_shim.c:1313
#1 __afl_cmplog_bytes afl_shim.c:1419
#2 afl_cmp_strncmp afl_shim.c:1698
#3 strncmp
#4 url_find_protocol
Our instrumentation crashing, filed as a crash in the target.
The length an interceptor passes is a semantic bound -- how many bytes the
real call was permitted to look at -- not a readable one. strncmp stops at
the first NUL or mismatch, so
char *p = malloc(2); p[0] = 'h'; p[1] = 'x';
strncmp(p, "http:", 5);
is legal C: two bytes are compared and byte 2 is never touched. The logger
read all five.
Fix it at __afl_cmplog_bytes rather than in strncmp. There are 18 direct
call sites, each open-coding its own length with a different convention
(n, n + 1, k, a hand-rolled CMPLOG_MAX_OPERAND clamp). strcmp was written
carefully, the memmem site already carries a comment about this exact class
of read, and strncmp and strncasecmp were the ones that were missed. One
guard at the only place that dereferences the operands means the next
interceptor cannot forget it.
Two layers, covering different builds:
- Page clamp, unconditional. A read that stays inside the page holding p
cannot fault, because the caller just read from that page. This is the
only guard a non-ASAN target gets, and it is where it matters most: a
fault in the nosan .so is an in-process SIGSEGV with our frame on top
and no redzone diagnostic to say whose it was.
- __asan_region_is_poisoned when the weak symbol resolves, tightening the
page bound to the exact object bound. NULL in a non-ASAN build, so that
path costs one null check.
Not a truncation to the first mismatch, which was the first thing I tried.
It is provably safe and it guts the record: with buf all 'A',
strncmp(buf, "PREFIX_", 7) mismatches at byte 0, so the literal never
reaches the pool -- and the token is the thing redqueen injects.
test_regression_cmplog_shim_merge catches exactly that, and clamping by
readability instead keeps it green:
before fix: CMP 41414141414141 5052454649585f -15 7
after fix: CMP 41414141414141 5052454649585f -15 7 (unchanged)
overread: CMP 6878 6874 4 5 (was: 5 bytes, ASAN abort)
Adds tests/test_regression_cmplog_strncmp_overread.py, which fails without
the change. It covers strncmp, strncasecmp and a fully readable compare, so
a fix that clamps too aggressively fails the third case.
Two tracecmp_visibility failures in the suite pre-date this change; the same
two fail on a clean tree.
Sign up for freeto 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

@daedalus