McRogueFace/tests/fuzz
John McCardle 925699ef0b Add 4 libFuzzer targets for Tier A/B API surface; addresses #312
New targets under tests/fuzz/, wired into Makefile FUZZ_TARGETS, each with a
seed corpus (parser seeds are real fixtures prefixed with a loader selector
byte):

- fuzz_audio_dsp        SoundBuffer factories + 14 DSP effects + concat/mix.
                        Self-contained (CPU sample math, no device).
- fuzz_import_parsers   TileSetFile/TileMapFile/LdtkProject. Loaders take a
                        path, so each iteration writes mutated bytes to a temp
                        file; OSError (IOError) is swallowed as an expected
                        parse-failure outcome.
- fuzz_texture_factory  Texture.from_bytes/composite/hsl_shift byte ingestion.
                        Multiplication-overflow path documented as out of scope
                        (would OOM, not crash cleanly).
- fuzz_shader_bindings  uniforms[] + PropertyBinding/CallableBinding lifetime,
                        target Drawable destroyed mid-flight (#270/#271/#277
                        pattern). Degrades to pure binding-lifetime fuzzing if
                        shaders are unavailable.

All four signature-validated against the live mcrfpy API before running.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv
2026-06-21 16:44:47 -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 Add fuzz_fov target, addresses #283 2026-04-10 11:15:01 -04:00
fuzz_grid_entity.py Add fuzz_grid_entity target for EntityCollection bug family, addresses #283 2026-04-10 11:13:16 -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 Add fuzz_maps_procgen target for HeightMap/DiscreteMap interfaces, addresses #283 2026-04-10 11:17:52 -04:00
fuzz_pathfinding_behavior.py Add fuzz_pathfinding_behavior target, addresses #283 2026-04-10 11:18:01 -04:00
fuzz_property_types.py Add fuzz_property_types target for refcount/type-confusion bugs, addresses #283 2026-04-10 11:15:39 -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