Add entropic schedule, autotokens, crash clustering - #4
Conversation
…tering into report Co-authored-by: daedalus <115175+daedalus@users.noreply.github.com>
Co-authored-by: daedalus <115175+daedalus@users.noreply.github.com>
Reviewer's GuideThis PR adds three integrated capabilities: an Sequence diagram for corpus import with autotoken dictionary generationsequenceDiagram
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
Sequence diagram for crash clustering in reportssequenceDiagram
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
Flow diagram for entropic seed schedulingflowchart 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]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| frame_lists = [frames.get(s, []) for s in sig_list] | ||
| clusters = cluster_crashes(sig_list, frame_lists=frame_lists) |
There was a problem hiding this comment.
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.
| 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, | |
| ) |
There was a problem hiding this comment.
🟡 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) toSeedScorerand 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
| File | Description |
|---|---|
| tests/test_schedules.py | Adds entropic schedule unit tests and score integration check |
| tests/test_report.py | Adds tests for stack-similarity crash clustering output |
| tests/test_import_corpus.py | Adds tests for token extraction, autotoken ranking, and dictionary round-trip |
| src/fuzzer_tool/services/report.py | Wires crash clustering into crash-signature report section |
| src/fuzzer_tool/services/import_corpus.py | Implements token extraction, autotoken dictionary build, and AFL dict writer + CLI flag |
| src/fuzzer_tool/core/schedules.py | Adds entropic schedule option and _entropic_factor() |
| src/fuzzer_tool/cli/commands.py | Exposes entropic schedule and import --autotokens in main CLI |
| README.md | Documents entropic schedule and autotokens import flag |
| docs/web_research_port_candidates_2026-08.md | Updates port-candidate status to reflect landed items |
| docs/TODO.md | Marks the “trivial items” tier as completed and documents what landed |
| docs/DEEP_DIVE.md | Documents entropic schedule, autotokens import flow, and crash clustering behavior |
Review details
Suppressed comments (1)
src/fuzzer_tool/cli/commands.py:2573
- The fuzzer-tool
importsubcommand still defaults--formatto "afl". Importing a flat libFuzzer corpus without--formatwill 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.
| sig_list = list(sigs.keys()) | ||
| frame_lists = [frames.get(s, []) for s in sig_list] | ||
| clusters = cluster_crashes(sig_list, frame_lists=frame_lists) |
| 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)] |
| from fuzzer_tool.services.import_corpus import ( | ||
| build_autotoken_dictionary, | ||
| import_from_afl, | ||
| import_from_honggfuzz, | ||
| import_from_libfuzzer, |
daedalus
commented
Aug 24, 2026
@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. |
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.
…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.
…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.
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
Summary by Sourcery
Add entropic scheduling, automated token dictionary generation, and clustered crash reporting.
New Features:
Enhancements:
Documentation:
Tests: