Extends the five existing targets to cover the remaining gaps from #312 without new files: - property_types Line/Circle/Arc setters, Scene.children collection ops (index/count/find/insert/slice/pop), module functions find/find_all/bresenham/lock. Benchmark triplet excluded (end_benchmark writes a file per call). - grid_entity grid.at / [x,y] / entities_in_radius / center_camera / hovered_cell, and GridPoint named-layer __getattr__/ __setattr__. - pathfinding_behavior Grid.find_path + full AStarPath (peek/__len__/__bool__/ iteration) that path_from didn't reach. - fov ColorLayer perspective (apply/update/clear_perspective) and draw_fov. - maps_procgen ColorLayer/TileLayer apply_threshold/apply_ranges/ apply_gradient from HeightMap sources. The full instrumented campaign surfaced five new bugs, filed as #321 (HIGH ColorLayer.draw_fov bad-free), #322 (WangSet.terrain_enum error-pending abort), #323/#324/#325 (float->int UB in pitch_shift/hsl_shift/Vector). Per decision, this issue delivers fuzz coverage only; the bugs are tracked separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv |
||
|---|---|---|
| .. | ||
| seeds | ||
| .gitignore | ||
| fuzz_anim_timer_scene.py | ||
| fuzz_audio_dsp.py | ||
| fuzz_common.cpp | ||
| fuzz_common.py | ||
| fuzz_fov.py | ||
| fuzz_grid_entity.py | ||
| fuzz_import_parsers.py | ||
| fuzz_maps_procgen.py | ||
| fuzz_pathfinding_behavior.py | ||
| fuzz_property_types.py | ||
| fuzz_shader_bindings.py | ||
| fuzz_texture_factory.py | ||
| README.md | ||
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-18onPATH(Debian:apt install clang-18 lld-18)libclang_rt.fuzzer-18-dev(for-fsanitize=fuzzer) — verify withclang-18 -print-file-name=libclang_rt.fuzzer-x86_64.a- Debug CPython built per top-level CLAUDE.md (
tools/build_debug_python.sh)
Build
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
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, ColorLayer/TileLayer apply_threshold/apply_ranges/apply_gradient |
new |
fuzz_fov.py |
grid.compute_fov + is_in_fov, transparent toggling, ColorLayer perspective (apply_perspective/update_perspective/clear_perspective/draw_fov) |
new |
fuzz_pathfinding_behavior.py |
DijkstraMap, grid.step, entity behavior fields, Grid.find_path + full AStarPath (peek/len/bool/iter) |
#273-adjacent |
fuzz_audio_dsp.py |
SoundBuffer DSP chain: from_samples/tone/sfxr, concat/mix, pitch_shift/lp/hp/echo/reverb/distortion/bit_crush/gain/normalize/reverse/slice/sfxr_mutate | #312 |
fuzz_import_parsers.py |
Tiled/LDtk external file parsers (TileSetFile/TileMapFile/LdtkProject) via temp-file mutation of real fixtures |
#312 |
fuzz_texture_factory.py |
Texture.from_bytes/composite/hsl_shift byte-ingestion + pixel transforms |
#312 |
fuzz_shader_bindings.py |
Shader uniform binding lifetime: uniforms[], PropertyBinding/CallableBinding, target destroyed mid-flight (#270/#271/#277 pattern) |
#312 |
Tier C surfaces from #312 are folded into existing targets rather than new
files: Line/Circle/Arc, Scene.children collection ops, and find/find_all/
bresenham/lock live in fuzz_property_types.py; grid spatial queries +
GridPoint dynamic attrs in fuzz_grid_entity.py. (The benchmark triplet is
deliberately excluded — end_benchmark() writes a file per call.)
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
- Add
<name>toFUZZ_TARGETSin the Makefile. - Create
tests/fuzz/fuzz_<name>.pydefiningfuzz_one_input(data: bytes) -> None. - Create
tests/fuzz/seeds/<name>/.gitkeepso the seed dir exists. - Import
ByteStreamandEXPECTED_EXCEPTIONSfromfuzz_common. Wrap the fuzz body intry: ... except EXPECTED_EXCEPTIONS: passso 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:
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)