Add build plumbing for libFuzzer+ASan fuzz build, addresses #283
- CMakeLists MCRF_FUZZER option (clang-only, -fsanitize=fuzzer-no-link) - Makefile fuzz-build/fuzz/fuzz-long/fuzz-repro/clean-fuzz targets - CommandLineParser -- passthrough after --exec for forwarding libFuzzer argv - McRFPy_API: forward script_args to sys.argv in --exec mode so atheris.Setup() sees libFuzzer flags; set sys.argv[0] to the exec script path to match Python script-mode conventions - .gitignore build-fuzz/ and corpora/crashes dirs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6bf5c451a3
commit
136d2a2a25
5 changed files with 93 additions and 1 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -49,3 +49,7 @@ dist/
|
|||
!CLAUDE.md
|
||||
!README.md
|
||||
!tests/
|
||||
|
||||
# Fuzzing build artifacts and runtime data (build-fuzz matched by build* above)
|
||||
tests/fuzz/corpora/
|
||||
tests/fuzz/crashes/
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ option(MCRF_SANITIZE_THREAD "Build with ThreadSanitizer" OFF)
|
|||
option(MCRF_DEBUG_PYTHON "Link against debug CPython from __lib_debug/" OFF)
|
||||
option(MCRF_FREE_THREADED_PYTHON "Link against free-threaded CPython (python3.14t)" OFF)
|
||||
option(MCRF_WASM_DEBUG "Build WASM with DWARF debug info and source maps" OFF)
|
||||
option(MCRF_FUZZER "Build with libFuzzer coverage instrumentation for atheris" OFF)
|
||||
|
||||
# Validate mutually exclusive sanitizers
|
||||
if(MCRF_SANITIZE_ADDRESS AND MCRF_SANITIZE_THREAD)
|
||||
|
|
@ -333,6 +334,15 @@ if(MCRF_SANITIZE_THREAD)
|
|||
-fsanitize=thread)
|
||||
endif()
|
||||
|
||||
if(MCRF_FUZZER)
|
||||
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
message(FATAL_ERROR "MCRF_FUZZER=ON requires Clang. Invoke with CC=clang-18 CXX=clang++-18.")
|
||||
endif()
|
||||
message(STATUS "libFuzzer coverage instrumentation enabled (atheris harness)")
|
||||
target_compile_options(mcrogueface PRIVATE -fsanitize=fuzzer-no-link)
|
||||
target_link_options(mcrogueface PRIVATE -fsanitize=fuzzer-no-link)
|
||||
endif()
|
||||
|
||||
# Enable Py_DEBUG when linking against debug CPython (matches pydebug ABI)
|
||||
if(MCRF_DEBUG_PYTHON OR MCRF_FREE_THREADED_PYTHON)
|
||||
target_compile_definitions(mcrogueface PRIVATE Py_DEBUG)
|
||||
|
|
|
|||
57
Makefile
57
Makefile
|
|
@ -114,6 +114,61 @@ asan-test: asan
|
|||
UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1" \
|
||||
python3 run_tests.py -v --sanitizer
|
||||
|
||||
# Fuzzing targets (clang + ASan + libFuzzer + atheris)
|
||||
FUZZ_TARGETS := grid_entity property_types anim_timer_scene maps_procgen fov pathfinding_behavior
|
||||
FUZZ_SECONDS ?= 30
|
||||
|
||||
fuzz-build:
|
||||
@echo "Building McRogueFace with libFuzzer + ASan (clang-18)..."
|
||||
@mkdir -p build-fuzz
|
||||
@cd build-fuzz && CC=clang-18 CXX=clang++-18 cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DMCRF_DEBUG_PYTHON=ON \
|
||||
-DMCRF_SANITIZE_ADDRESS=ON \
|
||||
-DMCRF_SANITIZE_UNDEFINED=ON \
|
||||
-DMCRF_FUZZER=ON \
|
||||
-DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=lld && make -j$(JOBS)
|
||||
@if [ ! -f __lib/Python/Lib/site-packages/atheris/__init__.py ]; then \
|
||||
echo "NOTE: atheris not installed. Run tools/install_atheris.sh"; \
|
||||
fi
|
||||
@echo "Fuzz build complete! Output: build-fuzz/mcrogueface"
|
||||
|
||||
fuzz: fuzz-build
|
||||
@for t in $(FUZZ_TARGETS); do \
|
||||
if [ ! -f tests/fuzz/fuzz_$$t.py ]; then \
|
||||
echo "SKIP: tests/fuzz/fuzz_$$t.py does not exist yet"; \
|
||||
continue; \
|
||||
fi; \
|
||||
echo "=== fuzzing $$t for $(FUZZ_SECONDS)s ==="; \
|
||||
mkdir -p tests/fuzz/corpora/$$t tests/fuzz/crashes; \
|
||||
( cd build-fuzz && MCRF_LIB_DIR=../__lib_debug PYTHONMALLOC=malloc \
|
||||
ASAN_OPTIONS="detect_leaks=0:halt_on_error=1:abort_on_error=1" \
|
||||
./mcrogueface --headless --exec ../tests/fuzz/fuzz_$$t.py -- \
|
||||
-max_total_time=$(FUZZ_SECONDS) \
|
||||
-artifact_prefix=../tests/fuzz/crashes/$$t- \
|
||||
../tests/fuzz/corpora/$$t ../tests/fuzz/seeds/$$t ) || exit 1; \
|
||||
done
|
||||
|
||||
fuzz-long: fuzz-build
|
||||
@test -n "$(TARGET)" || (echo "Usage: make fuzz-long TARGET=<name> SECONDS=<n>"; exit 1)
|
||||
@mkdir -p tests/fuzz/corpora/$(TARGET) tests/fuzz/crashes
|
||||
@( cd build-fuzz && MCRF_LIB_DIR=../__lib_debug PYTHONMALLOC=malloc \
|
||||
ASAN_OPTIONS="detect_leaks=0:halt_on_error=1:abort_on_error=1" \
|
||||
./mcrogueface --headless --exec ../tests/fuzz/fuzz_$(TARGET).py -- \
|
||||
-max_total_time=$(or $(SECONDS),3600) \
|
||||
-artifact_prefix=../tests/fuzz/crashes/$(TARGET)- \
|
||||
../tests/fuzz/corpora/$(TARGET) ../tests/fuzz/seeds/$(TARGET) )
|
||||
|
||||
fuzz-repro:
|
||||
@test -n "$(TARGET)" || (echo "Usage: make fuzz-repro TARGET=<name> CRASH=<path>"; exit 1)
|
||||
@test -n "$(CRASH)" || (echo "Usage: make fuzz-repro TARGET=<name> CRASH=<path>"; exit 1)
|
||||
@( cd build-fuzz && MCRF_LIB_DIR=../__lib_debug PYTHONMALLOC=malloc \
|
||||
./mcrogueface --headless --exec ../tests/fuzz/fuzz_$(TARGET).py -- ../$(CRASH) )
|
||||
|
||||
clean-fuzz:
|
||||
@echo "Cleaning fuzz build and corpora..."
|
||||
@rm -rf build-fuzz tests/fuzz/corpora tests/fuzz/crashes
|
||||
|
||||
tsan:
|
||||
@echo "Building McRogueFace with TSan + free-threaded Python..."
|
||||
@echo "NOTE: Requires free-threaded debug Python built with:"
|
||||
|
|
@ -169,7 +224,7 @@ analyze:
|
|||
|
||||
clean-debug:
|
||||
@echo "Cleaning debug/sanitizer builds..."
|
||||
@rm -rf build-debug build-asan build-tsan
|
||||
@rm -rf build-debug build-asan build-tsan build-fuzz
|
||||
|
||||
# Packaging targets using tools/package.sh
|
||||
package-windows-light: windows
|
||||
|
|
|
|||
|
|
@ -121,6 +121,19 @@ CommandLineParser::ParseResult CommandLineParser::parse(McRogueFaceConfig& confi
|
|||
config.exec_scripts.push_back(argv[current_arg]);
|
||||
config.python_mode = true;
|
||||
current_arg++;
|
||||
// Look for `--` passthrough marker: everything after it becomes script_args
|
||||
// (forwarded to sys.argv so fuzzers/libFuzzer can receive their flags).
|
||||
int scan = current_arg;
|
||||
while (scan < argc) {
|
||||
if (std::string(argv[scan]) == "--") {
|
||||
for (int i = scan + 1; i < argc; i++) {
|
||||
config.script_args.push_back(argv[i]);
|
||||
}
|
||||
current_arg = argc;
|
||||
break;
|
||||
}
|
||||
scan++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1027,6 +1027,16 @@ PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config)
|
|||
std::wstring warg(arg.begin(), arg.end());
|
||||
argv_storage.push_back(warg);
|
||||
}
|
||||
} else if (!config.exec_scripts.empty()) {
|
||||
// --exec mode: argv[0] = last exec script path so sys.argv[0] matches the running
|
||||
// script, mirroring normal Python script-mode behavior. Any script_args populated
|
||||
// via the `--` passthrough marker (see CommandLineParser) become argv[1:] so
|
||||
// fuzzers/libFuzzer can receive their flags through atheris.Setup(sys.argv, ...).
|
||||
argv_storage.push_back(config.exec_scripts.back().wstring());
|
||||
for (const auto& arg : config.script_args) {
|
||||
std::wstring warg(arg.begin(), arg.end());
|
||||
argv_storage.push_back(warg);
|
||||
}
|
||||
} else {
|
||||
// Interactive mode or no script: argv[0] = ""
|
||||
argv_storage.push_back(L"");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue