Skip to content

✨ feat(fuzz): drive Atheris with a failure header - #1234

Merged
gaborbernat merged 4 commits into
tox-dev:mainfrom
gaborbernat:feat/atheris-driver-1014
Oct 8, 2026
Merged

gaborbernat merged 4 commits into
tox-dev:mainfrom
gaborbernat:feat/atheris-driver-1014

Conversation

@gaborbernat

Copy link
Copy Markdown
Member

The Atheris driver handed raw bytes to each target, so a run could not fail an allocation, and the incremental APIs ran one unit per feed. Each input now starts with libxml2's [opts][failure_pos][max_chunk] header (xml.c), and the bridge's custom mutator changes one header field or the payload per call, as xmlFuzzMutateChunks does (fuzz.c).

failure_pos drives the _fuzz_inject_failure hook through the call pattern fuzz.py's self-test uses, bounded by the input size plus 100 as libxml2 bounds it. When the hook fires, the target must raise MemoryError and nothing else. The atheris env builds with -Dfuzzing=true so the hook exists, and the Linux test fails the first allocation of a document seed and requires the hook to report it.

IncrementalParser, Tokenizer, EncodingDetector and the stdlib HTMLParser shim feed the payload in max_chunk pieces with empty feeds around each, capped at libxml2's 50 + len/100 pieces (xml.c), and compare against one call. Atheris's own custom mutator example keeps a zlib frame intact the same way this mutator keeps the header.

The public-module list now comes from walking the package, so a new module fails the owner check until a target claims it. Seven round-trip oracles that ran only in fuzz.py's generational mode now run inside the document, schema, CSS object model, selector and URL targets. closes #1014

The Atheris gap check compared owners against a hand-kept module list, so
a new public module would get no fuzz target and the check would still
pass. Walking the package picks up each module with no underscore in its
path that declares __all__, and the owner check then fails until a target
claims its exports.
The Atheris driver passed raw bytes to each target, so no run could fail
an allocation, and coverage feedback did not reach the cleanup paths
after a failed PyMem allocation. The fuzz build already has a hook that
fails the Nth one.

Each input now starts with libxml2's [opts][failure_pos][max_chunk]
header. The driver bounds failure_pos by the input size plus 100, as
xml.c does, and opens the injection window around the target. When the
hook fires, the target must raise MemoryError; a MemoryError without a
fired hook counts as a finding too.

The bridge's custom mutator follows xmlFuzzMutateChunks and picks one
header field or the payload per call, so mutations keep the fields in
place. Seeds get libxml2's seed header, and the atheris env builds with
-Dfuzzing=true to get the hook.
IncrementalParser, Tokenizer, EncodingDetector and the stdlib HTMLParser
shim ran one unit per feed, so coverage feedback could not steer chunk
boundaries, and the detector and HTMLParser saw a fixed hex envelope in
place of the fuzz bytes.

Targets with an incremental API now feed the payload in max_chunk pieces
with an empty feed around each, capped at libxml2's 50 + len/100 pieces,
and compare the result with a one-shot call. Bytes APIs split inside
UTF-8 sequences whenever a chunk edge lands there. The opts bits move
the IncrementalParser and Tokenizer options off their defaults, so the
mutator reaches the option combinations too.
Seven round-trip oracles ran only in fuzz.py's generational round-trip
mode, out of reach of coverage-guided inputs. They cover the HTML and XML
fixpoints, source spans, StyleDeclaration re-reads, XPath and CSS
entry-point agreement, and URL split and recompose.

The document, schema, CSS object model and selector targets now run
those oracles on the input they already decode, and the URL check adds
the reparse oracle. An out-of-scope case skips only that oracle, so the
input keeps its other checks and its corpus entry.
@gaborbernat gaborbernat added the enhancement New feature or request label Oct 8, 2026
@codspeed

codspeed Bot commented Oct 8, 2026

Copy link
Copy Markdown

Merging this PR will regress 1 benchmark

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 579 untouched benchmarks
⏩ 32 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ test_feature[select-relative-sibling] 42.5 µs 44.8 µs -5.08%
⚡ test_feature[shadow-slot-comments] 137.9 µs 83.3 µs +65.59%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing gaborbernat:feat/atheris-driver-1014 (6726cb0) with main (af31f20)

Open in CodSpeed

Footnotes

  1. 32 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@gaborbernat
gaborbernat merged commit 202b878 into tox-dev:main Oct 8, 2026
55 of 63 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a coverage-guided atheris driver over the whole public API

1 participant