CI memory safety tests
This commit is contained in:
parent
cdae3b3ac9
commit
4df3687045
5 changed files with 398 additions and 3 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -28,6 +28,7 @@ CMakeFiles/
|
||||||
Makefile
|
Makefile
|
||||||
*.zip
|
*.zip
|
||||||
__lib/
|
__lib/
|
||||||
|
__lib_debug/
|
||||||
__lib_windows/
|
__lib_windows/
|
||||||
build-windows/
|
build-windows/
|
||||||
build_windows/
|
build_windows/
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,27 @@ option(MCRF_PLAYGROUND "Build with minimal playground scripts instead of full ga
|
||||||
# Game shell mode - fullscreen canvas, no REPL chrome (for itch.io / standalone web games)
|
# Game shell mode - fullscreen canvas, no REPL chrome (for itch.io / standalone web games)
|
||||||
option(MCRF_GAME_SHELL "Use minimal game-only HTML shell (no REPL)" OFF)
|
option(MCRF_GAME_SHELL "Use minimal game-only HTML shell (no REPL)" OFF)
|
||||||
|
|
||||||
|
# Debug/sanitizer build options
|
||||||
|
option(MCRF_SANITIZE_ADDRESS "Build with AddressSanitizer" OFF)
|
||||||
|
option(MCRF_SANITIZE_UNDEFINED "Build with UBSan" OFF)
|
||||||
|
option(MCRF_SANITIZE_THREAD "Build with ThreadSanitizer" OFF)
|
||||||
|
option(MCRF_DEBUG_PYTHON "Link against debug CPython from __lib_debug/" OFF)
|
||||||
|
|
||||||
|
# Validate mutually exclusive sanitizers
|
||||||
|
if(MCRF_SANITIZE_ADDRESS AND MCRF_SANITIZE_THREAD)
|
||||||
|
message(FATAL_ERROR "ASan and TSan are mutually exclusive. Use one or the other.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Validate debug Python library exists when requested
|
||||||
|
if(MCRF_DEBUG_PYTHON)
|
||||||
|
if(NOT EXISTS "${CMAKE_SOURCE_DIR}/__lib_debug/libpython3.14.so.1.0")
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"__lib_debug/libpython3.14.so.1.0 not found.\n"
|
||||||
|
"Build it first: tools/build_debug_python.sh")
|
||||||
|
endif()
|
||||||
|
message(STATUS "Using debug CPython from __lib_debug/")
|
||||||
|
endif()
|
||||||
|
|
||||||
# Emscripten builds: use SDL2 if specified, otherwise fall back to headless
|
# Emscripten builds: use SDL2 if specified, otherwise fall back to headless
|
||||||
if(EMSCRIPTEN)
|
if(EMSCRIPTEN)
|
||||||
if(MCRF_SDL2)
|
if(MCRF_SDL2)
|
||||||
|
|
@ -189,6 +210,9 @@ elseif(MCRF_HEADLESS)
|
||||||
python3.14
|
python3.14
|
||||||
m dl util pthread)
|
m dl util pthread)
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
||||||
|
if(MCRF_DEBUG_PYTHON)
|
||||||
|
link_directories(${CMAKE_SOURCE_DIR}/__lib_debug)
|
||||||
|
endif()
|
||||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||||
endif()
|
endif()
|
||||||
elseif(MCRF_CROSS_WINDOWS)
|
elseif(MCRF_CROSS_WINDOWS)
|
||||||
|
|
@ -245,6 +269,9 @@ else()
|
||||||
m dl util pthread
|
m dl util pthread
|
||||||
${OPENGL_LIBRARIES})
|
${OPENGL_LIBRARIES})
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
||||||
|
if(MCRF_DEBUG_PYTHON)
|
||||||
|
link_directories(${CMAKE_SOURCE_DIR}/__lib_debug)
|
||||||
|
endif()
|
||||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
@ -256,6 +283,36 @@ add_executable(mcrogueface ${SOURCES})
|
||||||
# Our SDL2 backend is separate from libtcod's SDL3 renderer
|
# Our SDL2 backend is separate from libtcod's SDL3 renderer
|
||||||
target_compile_definitions(mcrogueface PRIVATE NO_SDL)
|
target_compile_definitions(mcrogueface PRIVATE NO_SDL)
|
||||||
|
|
||||||
|
# Sanitizer instrumentation — applied to mcrogueface target only (not imported libs)
|
||||||
|
if(MCRF_SANITIZE_ADDRESS)
|
||||||
|
message(STATUS "AddressSanitizer enabled")
|
||||||
|
target_compile_options(mcrogueface PRIVATE
|
||||||
|
-fsanitize=address -fno-omit-frame-pointer -g -O1)
|
||||||
|
target_link_options(mcrogueface PRIVATE
|
||||||
|
-fsanitize=address)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(MCRF_SANITIZE_UNDEFINED)
|
||||||
|
message(STATUS "UndefinedBehaviorSanitizer enabled")
|
||||||
|
target_compile_options(mcrogueface PRIVATE
|
||||||
|
-fsanitize=undefined -fno-sanitize=function,vptr -g -O1)
|
||||||
|
target_link_options(mcrogueface PRIVATE
|
||||||
|
-fsanitize=undefined -fno-sanitize=function,vptr)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(MCRF_SANITIZE_THREAD)
|
||||||
|
message(STATUS "ThreadSanitizer enabled")
|
||||||
|
target_compile_options(mcrogueface PRIVATE
|
||||||
|
-fsanitize=thread -g -O1)
|
||||||
|
target_link_options(mcrogueface PRIVATE
|
||||||
|
-fsanitize=thread)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Enable Py_DEBUG when linking against debug CPython (matches pydebug ABI)
|
||||||
|
if(MCRF_DEBUG_PYTHON)
|
||||||
|
target_compile_definitions(mcrogueface PRIVATE Py_DEBUG)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Define MCRF_HEADLESS for headless builds (excludes SFML/ImGui code)
|
# Define MCRF_HEADLESS for headless builds (excludes SFML/ImGui code)
|
||||||
if(MCRF_HEADLESS)
|
if(MCRF_HEADLESS)
|
||||||
target_compile_definitions(mcrogueface PRIVATE MCRF_HEADLESS)
|
target_compile_definitions(mcrogueface PRIVATE MCRF_HEADLESS)
|
||||||
|
|
@ -377,9 +434,19 @@ add_custom_command(TARGET mcrogueface POST_BUILD
|
||||||
${CMAKE_SOURCE_DIR}/src/scripts $<TARGET_FILE_DIR:mcrogueface>/scripts)
|
${CMAKE_SOURCE_DIR}/src/scripts $<TARGET_FILE_DIR:mcrogueface>/scripts)
|
||||||
|
|
||||||
# Copy Python standard library to build directory
|
# Copy Python standard library to build directory
|
||||||
|
if(MCRF_DEBUG_PYTHON)
|
||||||
|
# Copy all libs first (SFML, libtcod, Python stdlib), then overwrite with debug Python
|
||||||
|
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>/lib
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy
|
||||||
|
${CMAKE_SOURCE_DIR}/__lib_debug/libpython3.14.so.1.0
|
||||||
|
$<TARGET_FILE_DIR:mcrogueface>/lib/libpython3.14.so.1.0)
|
||||||
|
else()
|
||||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>/lib)
|
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>/lib)
|
||||||
|
endif()
|
||||||
|
|
||||||
# On Windows, copy DLLs to executable directory
|
# On Windows, copy DLLs to executable directory
|
||||||
if(MCRF_CROSS_WINDOWS)
|
if(MCRF_CROSS_WINDOWS)
|
||||||
|
|
|
||||||
33
sanitizers/asan.supp
Normal file
33
sanitizers/asan.supp
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# McRogueFace AddressSanitizer Suppression File
|
||||||
|
#
|
||||||
|
# Minimal — most CPython false positives are handled by:
|
||||||
|
# - PYTHONMALLOC=malloc (bypasses pymalloc)
|
||||||
|
# - detect_leaks=0 (CPython has intentional lifetime leaks)
|
||||||
|
#
|
||||||
|
# Usage (via ASAN_OPTIONS or LSAN_OPTIONS):
|
||||||
|
# LSAN_OPTIONS="suppressions=sanitizers/asan.supp"
|
||||||
|
#
|
||||||
|
# Format: one suppression per line, prefix with "leak:" for leak suppressions
|
||||||
|
# See https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer
|
||||||
|
|
||||||
|
# CPython interned strings — intentionally never freed
|
||||||
|
leak:_PyUnicode_InternInPlace
|
||||||
|
leak:PyUnicode_InternFromString
|
||||||
|
|
||||||
|
# CPython type objects — intentionally immortal
|
||||||
|
leak:PyType_Ready
|
||||||
|
leak:_PyType_Ready
|
||||||
|
|
||||||
|
# CPython small int cache ([-5, 256]) — allocated once, never freed
|
||||||
|
leak:_PyLong_Init
|
||||||
|
|
||||||
|
# CPython GIL — allocated once per interpreter
|
||||||
|
leak:PyThread_allocate_lock
|
||||||
|
|
||||||
|
# CPython import system caches
|
||||||
|
leak:PyImport_ImportModule
|
||||||
|
leak:_PyImport_FindExtensionObject
|
||||||
|
|
||||||
|
# dlopen — loaded shared libraries are intentionally kept resident
|
||||||
|
leak:dlopen
|
||||||
|
leak:_dl_open
|
||||||
25
sanitizers/ubsan.supp
Normal file
25
sanitizers/ubsan.supp
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# McRogueFace UndefinedBehaviorSanitizer Suppression File
|
||||||
|
#
|
||||||
|
# CPython uses function pointer casts extensively (PyCFunction signatures
|
||||||
|
# don't match the actual function types). These are "safe" in practice
|
||||||
|
# on all ABIs CPython targets, but UBSan flags them.
|
||||||
|
#
|
||||||
|
# The primary defense is -fno-sanitize=function,vptr in compile flags.
|
||||||
|
# This file is a backup for any that slip through.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# UBSAN_OPTIONS="suppressions=sanitizers/ubsan.supp"
|
||||||
|
#
|
||||||
|
# Format: type:source_pattern
|
||||||
|
|
||||||
|
# CPython function pointer cast patterns
|
||||||
|
function:modules/cpython/*
|
||||||
|
function:deps/cpython/*
|
||||||
|
|
||||||
|
# CPython object header type punning (PyObject* -> specific types)
|
||||||
|
alignment:modules/cpython/*
|
||||||
|
alignment:deps/cpython/*
|
||||||
|
|
||||||
|
# libtcod internal casts (C library with void* patterns)
|
||||||
|
function:*libtcod*
|
||||||
|
function:*tcod*
|
||||||
269
sanitizers/valgrind-mcrf.supp
Normal file
269
sanitizers/valgrind-mcrf.supp
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
#
|
||||||
|
# McRogueFace Valgrind Suppression File
|
||||||
|
#
|
||||||
|
# Adapted from CPython's Misc/valgrind-python.supp with _PyObject_Free
|
||||||
|
# and _PyObject_Realloc entries uncommented and updated for 64-bit.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# valgrind --tool=memcheck --suppressions=sanitizers/valgrind-mcrf.supp \
|
||||||
|
# --leak-check=full --error-exitcode=42 ./build-debug/mcrogueface ...
|
||||||
|
#
|
||||||
|
# NOTE: For best results, run with PYTHONMALLOC=malloc so that all Python
|
||||||
|
# allocations go through system malloc and are fully visible to Valgrind.
|
||||||
|
# When using PYTHONMALLOC=malloc, the pymalloc suppressions below are not
|
||||||
|
# needed, but they're kept for cases where you want to run without it.
|
||||||
|
#
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# CPython pymalloc internals (address_in_range)
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
{
|
||||||
|
ADDRESS_IN_RANGE/Invalid read of size 4
|
||||||
|
Memcheck:Addr4
|
||||||
|
fun:address_in_range
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
ADDRESS_IN_RANGE/Invalid read of size 4
|
||||||
|
Memcheck:Value4
|
||||||
|
fun:address_in_range
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
ADDRESS_IN_RANGE/Invalid read of size 8 (x86_64)
|
||||||
|
Memcheck:Value8
|
||||||
|
fun:address_in_range
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
ADDRESS_IN_RANGE/Conditional jump depends on uninitialised value
|
||||||
|
Memcheck:Cond
|
||||||
|
fun:address_in_range
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# _PyObject_Free — pymalloc's free; reads pool headers that look
|
||||||
|
# uninitialised to Valgrind. Updated from Addr4/Value4 to Addr8/Value8
|
||||||
|
# for 64-bit systems.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
{
|
||||||
|
_PyObject_Free/Invalid read of size 4
|
||||||
|
Memcheck:Addr4
|
||||||
|
fun:_PyObject_Free
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
_PyObject_Free/Invalid read of size 4
|
||||||
|
Memcheck:Value4
|
||||||
|
fun:_PyObject_Free
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
_PyObject_Free/Use of uninitialised value of size 8
|
||||||
|
Memcheck:Addr8
|
||||||
|
fun:_PyObject_Free
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
_PyObject_Free/Use of uninitialised value of size 8
|
||||||
|
Memcheck:Value8
|
||||||
|
fun:_PyObject_Free
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
_PyObject_Free/Conditional jump depends on uninitialised value
|
||||||
|
Memcheck:Cond
|
||||||
|
fun:_PyObject_Free
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# _PyObject_Realloc — same pymalloc pool-header reads as _PyObject_Free
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
{
|
||||||
|
_PyObject_Realloc/Invalid read of size 4
|
||||||
|
Memcheck:Addr4
|
||||||
|
fun:_PyObject_Realloc
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
_PyObject_Realloc/Invalid read of size 4
|
||||||
|
Memcheck:Value4
|
||||||
|
fun:_PyObject_Realloc
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
_PyObject_Realloc/Use of uninitialised value of size 8
|
||||||
|
Memcheck:Addr8
|
||||||
|
fun:_PyObject_Realloc
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
_PyObject_Realloc/Use of uninitialised value of size 8
|
||||||
|
Memcheck:Value8
|
||||||
|
fun:_PyObject_Realloc
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
_PyObject_Realloc/Conditional jump depends on uninitialised value
|
||||||
|
Memcheck:Cond
|
||||||
|
fun:_PyObject_Realloc
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# CPython intentional leaks — interned strings, type objects, small int
|
||||||
|
# cache, and other objects that live for the entire process lifetime
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
{
|
||||||
|
Suppress leaking the GIL after a fork
|
||||||
|
Memcheck:Leak
|
||||||
|
fun:malloc
|
||||||
|
fun:PyThread_allocate_lock
|
||||||
|
fun:PyEval_ReInitThreads
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Suppress leaking the autoTLSkey
|
||||||
|
Memcheck:Leak
|
||||||
|
fun:malloc
|
||||||
|
fun:PyThread_create_key
|
||||||
|
fun:_PyGILState_Init
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Handle pthread leak (possibly leaked)
|
||||||
|
Memcheck:Leak
|
||||||
|
fun:calloc
|
||||||
|
fun:allocate_dtv
|
||||||
|
fun:_dl_allocate_tls_storage
|
||||||
|
fun:_dl_allocate_tls
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Handle pthread leak (possibly leaked)
|
||||||
|
Memcheck:Leak
|
||||||
|
fun:memalign
|
||||||
|
fun:_dl_allocate_tls_storage
|
||||||
|
fun:_dl_allocate_tls
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# dlopen internals — these leak by design (loaded libraries stay resident)
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
{
|
||||||
|
dlopen without dlclose (strdup via cache lookup)
|
||||||
|
Memcheck:Leak
|
||||||
|
fun:malloc
|
||||||
|
fun:malloc
|
||||||
|
fun:strdup
|
||||||
|
fun:_dl_load_cache_lookup
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
dlopen without dlclose (strdup via map object)
|
||||||
|
Memcheck:Leak
|
||||||
|
fun:malloc
|
||||||
|
fun:malloc
|
||||||
|
fun:strdup
|
||||||
|
fun:_dl_map_object
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
dlopen without dlclose (new object via malloc)
|
||||||
|
Memcheck:Leak
|
||||||
|
fun:malloc
|
||||||
|
fun:*
|
||||||
|
fun:_dl_new_object
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
dlopen without dlclose (new object via calloc)
|
||||||
|
Memcheck:Leak
|
||||||
|
fun:calloc
|
||||||
|
fun:*
|
||||||
|
fun:_dl_new_object
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
dlopen without dlclose (check map versions)
|
||||||
|
Memcheck:Leak
|
||||||
|
fun:calloc
|
||||||
|
fun:*
|
||||||
|
fun:_dl_check_map_versions
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# CPython false positives
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
{
|
||||||
|
bpo-38118: Valgrind false alarm on GCC builtin strcmp
|
||||||
|
Memcheck:Cond
|
||||||
|
fun:PyUnicode_Decode
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Uninitialised byte(s) false alarm (bpo-35561)
|
||||||
|
Memcheck:Param
|
||||||
|
epoll_ctl(event)
|
||||||
|
fun:epoll_ctl
|
||||||
|
fun:pyepoll_internal_ctl
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
wcscmp false positive in command line parsing
|
||||||
|
Memcheck:Addr8
|
||||||
|
fun:wcscmp
|
||||||
|
fun:_PyOS_GetOpt
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# CPython 3.14 specific — type slot lookups can read union members that
|
||||||
|
# Valgrind thinks are uninitialised (they're valid via different union paths)
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
{
|
||||||
|
CPython type slot union reads
|
||||||
|
Memcheck:Cond
|
||||||
|
fun:_Py_type_getattro
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
CPython type slot union reads (value variant)
|
||||||
|
Memcheck:Value8
|
||||||
|
fun:_Py_type_getattro
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# SFML / OpenGL driver internals — graphics drivers have their own allocators
|
||||||
|
# that produce false positives. Only suppress known-safe patterns.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
{
|
||||||
|
Mesa/OpenGL driver init leaks
|
||||||
|
Memcheck:Leak
|
||||||
|
...
|
||||||
|
obj:*/dri/*_dri.so
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
X11 display connection leaks
|
||||||
|
Memcheck:Leak
|
||||||
|
...
|
||||||
|
fun:XOpenDisplay
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
SFML font/texture init
|
||||||
|
Memcheck:Leak
|
||||||
|
...
|
||||||
|
fun:*sf*Font*
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue