CI memory safety tests

This commit is contained in:
John McCardle 2026-03-07 21:53:19 -05:00
commit 4df3687045
5 changed files with 398 additions and 3 deletions

33
sanitizers/asan.supp Normal file
View 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
View 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*

View 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*
}