Atheris
Contract
| Field | Bound contract |
|---|---|
| Trigger | User needs coverage-guided fuzzing for Python code or a Python native extension using Atheris. |
| Authority | Reversible local: writes only the Atheris harness file, a corpus directory, and when required for dependency management pyproject.toml and uv.lock in the harness directory; rollback is deleting the harness file and corpus directory and restoring pyproject.toml and uv.lock to their pre-run state. No remote mutation. No source under test is mutated. |
| Side effect | Local writes: a fuzz.py (or named) harness, a corpus/ directory of seed and crash artifacts, and a transient fuzzing process. |
| Done | Atheris executes an instrumented target through a deterministic TestOneInput harness, reports coverage, and any saved crash artifact reproduces the same failure when replayed. |
Inputs
- Target: the Python function or module to fuzz, or the Python C extension to fuzz. Required.
- Target kind: pure Python, or native C extension. Required; it selects the instrumentation and build path.
- Expected exceptions: the exception types the target legitimately raises on bad input, so the harness catches them instead of crashing. Optional but recommended.
- Seed corpus: initial input files for
corpus/. Optional; Atheris can start empty. - Time/length budget:
-max_total_timeand-max_lenvalues. Optional; defaults are libFuzzer defaults. - Sanitizers: whether AddressSanitizer and/or UndefinedBehaviorSanitizer are enabled. Optional; ASan is the default for native extensions.
Procedure
Determine target kind. If the target is pure Python, follow the pure-Python path. If it is a C extension compiled from source, follow the native-extension path. Do not guess; ask the user when the kind is ambiguous. Done when: the target kind is determined as pure Python or native C extension.
Install Atheris. If the harness directory is not already a uv project (no
pyproject.tomlpresent), runuv init --bareonce. Thenuv add atheris. Verify withpython -c "import atheris; print(atheris.__version__)". Done when: Atheris is installed and importable.Write the harness (
fuzz.pyor a named file). The harness must be deterministic: norandom,time, or other nondeterministic input insideTestOneInput.- Decorate the entry point with
@atheris.instrument_func. - Define
TestOneInput(data: bytes). Return early on inputs too short to be meaningful. Call the target. Catch only the expected exception types andpass; let every other exception propagate so the fuzzer records it as a crash. - In
main(), callatheris.Setup(sys.argv, TestOneInput)thenatheris.Fuzz(). - For structured input, use
atheris.FuzzedDataProvider(data)to split onebytesinput into typed values (ConsumeUnicodeNoSurrogates,ConsumeBool,ConsumeIntInRange, etc.). Draw in a fixed order; once the buffer runs dry each remaining method returns a zero-value of its type. Do not slicedataby hand when the target takes several typed arguments, because every mutation shifts the byte offsets of everything after it. Done when: the harness is written with a deterministicTestOneInputandmain().
- Decorate the entry point with
Instrument pure-Python targets. Wrap imports of the code under test in
with atheris.instrument_imports():so coverage is collected. Do not import the target module afteratheris.Setup(). Useatheris.instrument_funcfor a single function,atheris.instrument_imports()for selected modules, oratheris.instrument_all()only when system-wide instrumentation is intended. Done when: the target is instrumented with the appropriate Atheris mechanism.Build native C extensions with instrumentation. Before installing the extension from source, export:
CC=clang CXX=clang++ CFLAGS="-fsanitize=address,fuzzer-no-link" CXXFLAGS="-fsanitize=address,fuzzer-no-link" LDSHARED="clang -shared" LDSHAREDXX="clang++ -shared"For uv-managed projects, set
no-binary = ["<pkg>"]under[tool.uv]inpyproject.tomland runuv sync --reinstall-package <pkg>so the package is built from source; a lateruv synccan otherwise silently swap in an uninstrumented wheel. Addundefinedto the sanitizer list (-fsanitize=address,undefined,fuzzer-no-link) when UBSan is requested. Done when: the native extension is built from source with sanitizer and fuzzer instrumentation flags.Configure the native-extension runtime. Set
LD_PRELOADto the Atheris sanitizer shared library:export LD_PRELOAD="$(python -c 'import atheris, os; print(os.path.join(os.path.dirname(atheris.__file__), "asan_with_fuzzer.so"))')"Set
ASAN_OPTIONS="allocator_may_return_null=1,detect_leaks=0"to suppress allocation-failure and leak noise. SetASAN_SYMBOLIZER_PATHto thellvm-symbolizerfor the installed clang when stack traces are needed. Done when:LD_PRELOADandASAN_OPTIONSare set for the native-extension runtime.Create the corpus.
mkdir corpusand add seed inputs as individual files. Runuv run python fuzz.py corpus/so libFuzzer loads and grows the corpus. Minimize a merged corpus withuv run python fuzz.py -merge=1 new_corpus/ old_corpus/. Done when: the corpus directory is created and seeded.Run the campaign.
uv run python fuzz.py corpus/with optional-max_total_time=<seconds>,-max_len=<bytes>, and-workers=N -jobs=Nfor parallel exploration. Read the output:NEW cov: Xmeans new coverage and corpus growth;ERROR: libFuzzermeans a crash was detected and a crash artifact was written. Done when: the campaign runs and produces coverage output or crash artifacts.Reproduce failures. A crash artifact (named
crash-*orleak-*) is written next to the harness. Replay it deterministically withuv run python fuzz.py <artifact>and confirm the same failure recurs. Done when: each saved artifact reproduces the same failure or is classified as nondeterministic.
Failure and recovery
- No coverage increase. Cause: poor seed corpus or target not instrumented. Recovery: add representative seeds; confirm
instrument_imports()wraps the target imports and@atheris.instrument_funcwraps the entry point. Do not declare success on a stall. - Import errors / modules imported before instrumentation. Recovery: move the target imports inside the
atheris.instrument_imports()context manager, beforeatheris.Setup(). - Segfault with no ASan output. Cause:
LD_PRELOADnot set for a native extension. Recovery: exportLD_PRELOADtoasan_with_fuzzer.soand rerun. - Build failures for a native extension. Cause: wrong compiler or missing flags. Recovery: verify
CC,CXX,CFLAGS,CXXFLAGS, and the clang version; configureno-binary = ["<pkg>"]under[tool.uv]and runuv sync --reinstall-package <pkg>. - Memory-allocation or leak noise. Recovery: set
ASAN_OPTIONS=allocator_may_return_null=1,detect_leaks=0. - Crash artifact does not reproduce. Cause: nondeterminism in the harness (randomness, time, unordered iteration over mutable state). Recovery: remove the nondeterminism so
TestOneInputis a pure function ofdata, then rerun. A non-reproducing crash is not a confirmed defect. - Partial-result rule. A campaign that finds no crash is a partial result (coverage gained, no defect proven), not proof of absence. Report coverage and corpus growth; do not claim the target is bug-free.
- Rollback. Delete the harness file and the
corpus/directory. Restorepyproject.tomlanduv.lockto their pre-run state, or if they were created by this skill, remove the added atheris entry andno-binaryconfiguration. The source under test is never modified by this skill.
Output
- A deterministic, instrumented Atheris harness file.
- A
corpus/directory of seed and discovered inputs, plus anycrash-*/leak-*artifacts. - A campaign report: coverage reached, corpus size, executions per second, and for each crash artifact a confirmed reproduction (same failure on replay) or a nondeterminism flag.
- Terminal classification per crash: confirmed defect (reproduces), nondeterministic (does not reproduce), or no crash found (partial result, not proof of absence).