McRogueFace/tests/fuzz/README.md

90 lines
3.9 KiB
Markdown
Raw Normal View History

Add native libFuzzer fuzz harness for Python API, addresses #283 Pivots away from atheris (which lacks Python 3.14 support) to a single libFuzzer-linked executable that embeds CPython, registers mcrfpy, and dispatches each iteration to a Python fuzz_one_input(data: bytes) function loaded from tests/fuzz/fuzz_<target>.py by MCRF_FUZZ_TARGET env var. libFuzzer instruments the C++ engine code where all #258-#278 bugs live; Python drives the fuzzing logic via an in-house ByteStream replacement for atheris.FuzzedDataProvider. Python-level exceptions are caught; only ASan/UBSan signal real bugs. CMake - MCRF_FUZZER=ON builds mcrfpy_fuzz from all src/*.cpp except main.cpp plus tests/fuzz/fuzz_common.cpp, linked with -fsanitize=fuzzer,address, undefined. Asset+lib post-build copy added so the embedded interpreter finds its stdlib and default_font/default_texture load. Makefile - fuzz-build builds only mcrfpy_fuzz (fast iterate) - fuzz loops over six targets setting MCRF_FUZZ_TARGET for each - fuzz-long TARGET=x SECONDS=n for deep manual runs - fuzz-repro TARGET=x CRASH=path for crash reproduction - Shared ASAN_OPTIONS / PYTHONHOME env via FUZZ_ENV define tests/fuzz - fuzz_common.cpp: LLVMFuzzerInitialize bootstraps Python, imports target, resolves fuzz_one_input. LLVMFuzzerTestOneInput wraps bytes as PyBytes, calls target, swallows Python errors. - fuzz_common.py: ByteStream byte consumer + safe_reset() + EXPECTED_EXCEPTIONS - Six target stubs (grid_entity, property_types, anim_timer_scene, maps_procgen, fov, pathfinding_behavior) to be fleshed out in follow-up - README with build/run/triage instructions Verified end-to-end: make fuzz-build produces build-fuzz/mcrfpy_fuzz, make fuzz FUZZ_SECONDS=3 ran all six targets (~2400-9800 exec/s each, 667-1883 coverage edges), make fuzz-repro loaded and replayed a corpus input cleanly. No crashes from the stubs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:05:04 -04:00
# McRogueFace Python API fuzzing harness (#283)
Native clang+libFuzzer+ASan harness that drives the `mcrfpy` Python API from
Python fuzz targets. libFuzzer instruments the C++ engine code (where all the
#258-#278 bugs live); Python drives the fuzzing logic through a simple byte
consumer. No atheris dependency — Python-level coverage would add nothing here
because the bugs live below the API boundary.
## Prerequisites
- `clang-18`, `clang++-18`, `lld-18` on `PATH` (Debian: `apt install clang-18 lld-18`)
- `libclang_rt.fuzzer-18-dev` (for `-fsanitize=fuzzer`) — verify with
`clang-18 -print-file-name=libclang_rt.fuzzer-x86_64.a`
- Debug CPython built per top-level CLAUDE.md (`tools/build_debug_python.sh`)
## Build
```sh
make fuzz-build
```
Produces `build-fuzz/mcrfpy_fuzz`, a single libFuzzer-linked executable. All
six fuzz targets share this binary — target selection is by env var.
## Run
```sh
make fuzz # 30s smoke on each of 6 targets
make fuzz FUZZ_SECONDS=300 # 5min each
make fuzz-long TARGET=grid_entity SECONDS=3600 # 1hr on one target
make fuzz-repro TARGET=grid_entity CRASH=tests/fuzz/crashes/grid_entity-abc123
make clean-fuzz # Wipe build-fuzz/, corpora/, crashes/
```
Corpora live under `tests/fuzz/corpora/<target>/` (gitignored — libFuzzer
grows these), crashes under `tests/fuzz/crashes/` (gitignored — triage
dir). Seed inputs committed to `tests/fuzz/seeds/<target>/` are read-only.
## Targets
| Script | Surface | Hunts |
|---|---|---|
| `fuzz_grid_entity.py` | EntityCollection append/remove/insert/extend/slice across differently-sized grids, `entity.die` during iteration | #258-#263, #273, #274 |
| `fuzz_property_types.py` | Random property get/set with type confusion on Frame/Caption/Sprite/Entity/Grid/TileLayer/ColorLayer | #267, #268, #272 |
| `fuzz_anim_timer_scene.py` | Animation + Timer state machine, Frame reparenting, scene swap in callbacks | #269, #270, #275, #277 |
| `fuzz_maps_procgen.py` | HeightMap/DiscreteMap ops and conversions, NoiseSource.sample, BSP.to_heightmap | new |
| `fuzz_fov.py` | grid.compute_fov + is_in_fov, transparent toggling | new |
| `fuzz_pathfinding_behavior.py` | DijkstraMap, grid.step, entity behavior fields | #273-adjacent |
Any target not yet implemented is a stub that still compiles and runs cleanly
`make fuzz` reports it as a no-op.
## Adding a new target
1. Add `<name>` to `FUZZ_TARGETS` in the Makefile.
2. Create `tests/fuzz/fuzz_<name>.py` defining `fuzz_one_input(data: bytes) -> None`.
3. Create `tests/fuzz/seeds/<name>/.gitkeep` so the seed dir exists.
4. Import `ByteStream` and `EXPECTED_EXCEPTIONS` from `fuzz_common`. Wrap the
fuzz body in `try: ... except EXPECTED_EXCEPTIONS: pass` so Python noise
doesn't pollute libFuzzer output — real bugs come from ASan/UBSan.
No C++ code changes are needed to add a target. The harness loads
`fuzz_<MCRF_FUZZ_TARGET>.py` by name at init time.
## Triage
A crash in `tests/fuzz/crashes/` is a file containing the exact bytes that
triggered it. Reproduce with `make fuzz-repro TARGET=<name> CRASH=<path>`.
The binary will rerun ONCE against that input and ASan will print the stack.
Useful ASan tweaks when investigating:
```sh
ASAN_OPTIONS="detect_leaks=0:symbolize=1:print_stacktrace=1" \
./build-fuzz/mcrfpy_fuzz path/to/crash_input
```
If the crash reproduces a known fixed issue (#258-#278), delete the crash file
and move on. If it's new, file a Gitea issue with the crash file attached and
apply appropriate `system:*` and `priority:*` labels per CLAUDE.md.
## CI integration
Not wired into `tests/run_tests.py`. Fuzz runs are non-deterministic and too
long for normal suite runs. Follow-up issue will add a scheduled weekly job.
## References
- Plan: `/home/john/.claude/plans/abundant-gliding-hummingbird.md`
- libFuzzer: https://llvm.org/docs/LibFuzzer.html
- Bug inventory: #279 (meta), #258-#278 (individual bugs)