Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions .github/workflows/sanitizers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Sanitizers and memory checkers for detecting threading issues and memory leaks
# TSan and ASan run on every PR and push to master
# Valgrind runs only on releases, manual dispatch, or when PR has "run-valgrind" label

name: Sanitizers

on:
workflow_dispatch:
pull_request:
push:
branches:
- master
release:
types: [published]

jobs:
thread-sanitizer:
name: "Thread Sanitizer"
runs-on: ubuntu-latest

env:
VCPKG_DEFAULT_TRIPLET: x64-linux-mixed
CC: clang-18
CXX: clang++-18

steps:
- uses: actions/checkout@v6

- uses: actions/setup-python@v6
with:
python-version: '3.12'

- name: Install Clang
run: |
sudo apt-get update
sudo apt-get install -y clang-18 llvm-18

- name: Download and unpack ROMs
run: ./scripts/download_unpack_roms.sh

- name: Build with Thread Sanitizer
run: |
pip install --verbose -e .[test] \
--config-settings=cmake.args="-DENABLE_SANITIZER=thread"

- name: Run tests with Thread Sanitizer
env:
TSAN_OPTIONS: "second_deadlock_stack=1 history_size=7 halt_on_error=1"
run: |
# Find and preload the TSan runtime library
CLANG_VERSION=$(clang-18 --version | grep -oP 'clang version \K[0-9]+')
TSAN_LIB=$(find /usr/lib/llvm-${CLANG_VERSION}/lib/clang -name "libclang_rt.tsan-x86_64.so" 2>/dev/null | head -1)
echo "Preloading TSan runtime: $TSAN_LIB"

echo "Running vector environment tests (most likely to have threading issues)..."
LD_PRELOAD="$TSAN_LIB" python -m pytest tests/python/test_atari_vector_env.py -v -x

echo "Running all tests..."
LD_PRELOAD="$TSAN_LIB" python -m pytest -v

address-sanitizer:
name: "Address Sanitizer + UBSan"
runs-on: ubuntu-latest

env:
VCPKG_DEFAULT_TRIPLET: x64-linux-mixed
CC: clang-18
CXX: clang++-18

steps:
- uses: actions/checkout@v6

- uses: actions/setup-python@v6
with:
python-version: '3.12'

- name: Install Clang
run: |
sudo apt-get update
sudo apt-get install -y clang-18 llvm-18

- name: Download and unpack ROMs
run: ./scripts/download_unpack_roms.sh

- name: Build with Address Sanitizer
run: |
pip install --verbose -e .[test] \
--config-settings=cmake.args="-DENABLE_SANITIZER=address"

- name: Run tests with Address Sanitizer
env:
ASAN_OPTIONS: "detect_leaks=1:check_initialization_order=1:halt_on_error=1"
UBSAN_OPTIONS: "print_stacktrace=1:halt_on_error=1"
run: |
# Find and preload the ASan runtime library
CLANG_VERSION=$(clang-18 --version | grep -oP 'clang version \K[0-9]+')
ASAN_LIB=$(find /usr/lib/llvm-${CLANG_VERSION}/lib/clang -name "libclang_rt.asan-x86_64.so" 2>/dev/null | head -1)
echo "Preloading ASan runtime: $ASAN_LIB"

echo "Running vector environment tests..."
LD_PRELOAD="$ASAN_LIB" python -m pytest tests/python/test_atari_vector_env.py -v -x

echo "Running all tests..."
LD_PRELOAD="$ASAN_LIB" python -m pytest -v

valgrind:
name: "Valgrind (Memcheck + Helgrind)"
runs-on: ubuntu-latest
if: >
github.event_name == 'release' ||
github.event_name == 'workflow_dispatch' ||
contains(github.event.pull_request.labels.*.name, 'run-valgrind')

env:
VCPKG_DEFAULT_TRIPLET: x64-linux-mixed

steps:
- uses: actions/checkout@v6

- uses: actions/setup-python@v6
with:
python-version: '3.12'

- name: Install Valgrind
run: |
sudo apt-get update
sudo apt-get install -y valgrind

- name: Download and unpack ROMs
run: ./scripts/download_unpack_roms.sh

- name: Build with debug symbols
run: |
pip install --verbose -e .[test] \
--config-settings=cmake.args="-DCMAKE_BUILD_TYPE=RelWithDebInfo"

- name: Run Memcheck (memory leaks)
run: |
valgrind \
--tool=memcheck \
--leak-check=full \
--show-leak-kinds=definite \
--errors-for-leak-kinds=definite \
--track-origins=yes \
--suppressions=.valgrind-python.supp \
--error-exitcode=1 \
python -m pytest tests/python/test_atari_vector_env.py -v
continue-on-error: true

- name: Run Helgrind (thread errors)
run: |
valgrind \
--tool=helgrind \
--suppressions=.valgrind-python.supp \
--error-exitcode=1 \
python -m pytest tests/python/test_atari_vector_env.py -v
continue-on-error: true
126 changes: 126 additions & 0 deletions .valgrind-python.supp
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Valgrind suppression file for Python and ALE
# This file suppresses known false positives from Python's memory management
# Usage: valgrind --suppressions=.valgrind-python.supp <command>

# Python's PyMalloc arena allocator intentionally pools memory
{
python_pymalloc
Memcheck:Leak
match-leak-kinds: possible,reachable
...
fun:_PyObject_Malloc
}

{
python_arena
Memcheck:Leak
match-leak-kinds: possible,reachable
...
fun:_PyArena_*
}

{
python_dict
Memcheck:Leak
match-leak-kinds: possible,reachable
...
fun:PyDict_*
}

{
python_list
Memcheck:Leak
match-leak-kinds: possible,reachable
...
fun:PyList_*
}

{
python_tuple
Memcheck:Leak
match-leak-kinds: possible,reachable
...
fun:PyTuple_*
}

{
python_unicode
Memcheck:Leak
match-leak-kinds: possible,reachable
...
fun:PyUnicode_*
}

{
python_type
Memcheck:Leak
match-leak-kinds: possible,reachable
...
fun:PyType_*
}

{
python_import
Memcheck:Leak
match-leak-kinds: possible,reachable
...
fun:PyImport_*
}

# NumPy can have similar pooling behavior
{
numpy_array
Memcheck:Leak
match-leak-kinds: possible,reachable
...
obj:*/numpy/*.so
}

# pybind11/nanobind related
{
pybind11_internals
Memcheck:Leak
match-leak-kinds: possible,reachable
...
fun:*pybind11*
}

# SDL library (if enabled)
{
sdl_init
Memcheck:Leak
match-leak-kinds: possible,reachable
...
fun:SDL_*
}

# Helgrind suppressions for known thread-safe patterns

# Python GIL is thread-safe by design
{
python_gil
Helgrind:Race
...
fun:PyGILState_*
}

{
python_threadstate
Helgrind:Race
...
fun:PyThreadState_*
}

# C++ standard library threading
{
libstdc++_thread
Helgrind:Race
...
obj:*/libstdc++.so*
}

{
libstdc++_once
Helgrind:Race
fun:__once_proxy
}
23 changes: 23 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,29 @@ if (BUILD_WASM_LIB)
list(APPEND VCPKG_MANIFEST_FEATURES "sdl")
endif()

# Sanitizer support for debugging (thread, address, undefined)
# Usage: -DENABLE_SANITIZER=thread or -DENABLE_SANITIZER=address
set(ENABLE_SANITIZER "" CACHE STRING "Enable sanitizer (thread, address, or empty to disable)")
set_property(CACHE ENABLE_SANITIZER PROPERTY STRINGS "" "thread" "address")

if(ENABLE_SANITIZER)
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT CMAKE_CXX_COMPILER_ID MATCHES "GNU")
message(WARNING "Sanitizers are best supported with Clang or GCC. Current compiler: ${CMAKE_CXX_COMPILER_ID}")
endif()

if(ENABLE_SANITIZER STREQUAL "thread")
message(STATUS "Building with Thread Sanitizer (TSan)")
add_compile_options(-fsanitize=thread -g -O1)
add_link_options(-fsanitize=thread)
elseif(ENABLE_SANITIZER STREQUAL "address")
message(STATUS "Building with Address Sanitizer (ASan) + Undefined Behavior Sanitizer (UBSan)")
add_compile_options(-fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer -g -O1)
add_link_options(-fsanitize=address -fsanitize=undefined)
else()
message(FATAL_ERROR "Invalid sanitizer '${ENABLE_SANITIZER}'. Choose 'thread' or 'address'.")
endif()
endif()

# Set cmake module path
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake ${CMAKE_MODULE_PATH})

Expand Down
Loading
Loading