McRogueFace/tests/fuzz
John McCardle 246ed886db Fold Tier C surface into existing fuzz targets; closes #312
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
2026-06-21 16:45:03 -04:00
..
seeds Add 4 libFuzzer targets for Tier A/B API surface; addresses #312 2026-06-21 16:44:47 -04:00
.gitignore Add native libFuzzer fuzz harness for Python API, addresses #283 2026-04-10 11:05:04 -04:00
fuzz_anim_timer_scene.py Add fuzz_anim_timer_scene target for lifecycle bug family, addresses #283 2026-04-10 11:16:40 -04:00
fuzz_audio_dsp.py Add 4 libFuzzer targets for Tier A/B API surface; addresses #312 2026-06-21 16:44:47 -04:00
fuzz_common.cpp Add native libFuzzer fuzz harness for Python API, addresses #283 2026-04-10 11:05:04 -04:00
fuzz_common.py Add native libFuzzer fuzz harness for Python API, addresses #283 2026-04-10 11:05:04 -04:00
fuzz_fov.py Fold Tier C surface into existing fuzz targets; closes #312 2026-06-21 16:45:03 -04:00
fuzz_grid_entity.py Fold Tier C surface into existing fuzz targets; closes #312 2026-06-21 16:45:03 -04:00
fuzz_import_parsers.py Add 4 libFuzzer targets for Tier A/B API surface; addresses #312 2026-06-21 16:44:47 -04:00
fuzz_maps_procgen.py Fold Tier C surface into existing fuzz targets; closes #312 2026-06-21 16:45:03 -04:00
fuzz_pathfinding_behavior.py Fold Tier C surface into existing fuzz targets; closes #312 2026-06-21 16:45:03 -04:00
fuzz_property_types.py Fold Tier C surface into existing fuzz targets; closes #312 2026-06-21 16:45:03 -04:00
fuzz_shader_bindings.py Add 4 libFuzzer targets for Tier A/B API surface; addresses #312 2026-06-21 16:44:47 -04:00
fuzz_texture_factory.py Add 4 libFuzzer targets for Tier A/B API surface; addresses #312 2026-06-21 16:44:47 -04:00
README.md Add 4 libFuzzer targets for Tier A/B API surface; addresses #312 2026-06-21 16:44:47 -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

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

  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:

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