Skip to content

Commit 5598f94

Browse files
committed
Allow sanitizers to be enabled in CMakeLists
1 parent 5e69749 commit 5598f94

3 files changed

Lines changed: 100 additions & 9 deletions

File tree

ASAN.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Address Sanitizer (ASAN) Guide
2+
3+
This project supports Address Sanitizer (ASAN) to help detect memory corruption,
4+
use-after-free, and buffer overflows in the C/C++ NetHack engine and its Python
5+
extensions.
6+
7+
## Enabling ASAN
8+
9+
ASAN is integrated into the CMake build system and can be enabled by editing
10+
`pyproject.toml`.
11+
12+
### Current Configuration
13+
14+
```toml
15+
[tool.scikit-build]
16+
cmake.build-type = "Release"
17+
cmake.args = ["-DHACKDIR=nle/nethackdir", "-DPYTHON_PACKAGE_NAME=nle"]
18+
```
19+
20+
To enable ASAN, add the cmake argument `-DENABLE_ASAN=On` and switch
21+
`cmake.build-type` to `Debug`.
22+
23+
## Running Tests with ASAN
24+
25+
Because the Python interpreter itself is not built with ASAN, you must preload
26+
the ASAN runtime library when running tests.
27+
28+
```bash
29+
LD_PRELOAD=$(gcc -print-file-name=libasan.so):$(gcc -print-file-name=libstdc++.so) \
30+
ASAN_OPTIONS=detect_leaks=0 \
31+
uv run pytest
32+
```
33+
34+
_Note: Preloading `libstdc++.so` may be necessary on some platforms (like
35+
aarch64 Linux) to avoid crashes when C++ exceptions are thrown._
36+
37+
### Why `detect_leaks=0`?
38+
39+
We disable the LeakSanitizer (`detect_leaks=0`) for several reasons:
40+
41+
1. Python Shutdown: CPython does not free all memory at exit (e.g., global
42+
singletons, interned strings). This is intentional for performance but is
43+
flagged as a "leak" by ASAN.
44+
2. Pytest State: `pytest` keeps tracebacks, local variables, and fixture data in
45+
memory until the end of the session to generate reports.
46+
3. Standard Interpreter: Since we are running a sanitized C extension inside a
47+
non-sanitized Python interpreter, the leak detector cannot accurately track
48+
the ownership boundary between the two.
49+
50+
Disabling leak detection still allows ASAN to catch critical memory corruption
51+
errors (Buffer Overflows, Use-After-Free, etc.) as they happen.
52+
53+
## Other Sanitizers
54+
55+
The build system also supports:
56+
57+
- Thread Sanitizer (TSAN): Use `-DENABLE_TSAN=ON`.
58+
- Undefined Behavior Sanitizer (UBSAN): Use `-DENABLE_UBSAN=ON`.
59+
60+
To use these, update `pyproject.toml` accordingly and preload the corresponding
61+
library (e.g., `libtsan.so`).

CMakeLists.txt

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,28 @@ if(CMAKE_BUILD_TYPE MATCHES Debug)
1919
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g")
2020
set(CMAKE_XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT "dwarf-with-dsym")
2121

22-
if(0)
23-
# address sanitizer.
24-
set(CMAKE_CXX_FLAGS_DEBUG
25-
"${CMAKE_CXX_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address")
26-
set(CMAKE_C_FLAGS_DEBUG
27-
"${CMAKE_C_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address")
28-
set(CMAKE_LINKER_FLAGS_DEBUG
29-
"${CMAKE_LINKER_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address"
30-
)
22+
option(ENABLE_ASAN "Enable Address Sanitizer" OFF)
23+
option(ENABLE_TSAN "Enable Thread Sanitizer" OFF)
24+
option(ENABLE_UBSAN "Enable Undefined Behavior Sanitizer" OFF)
25+
26+
if(ENABLE_ASAN)
27+
message(STATUS "Enabling Address Sanitizer")
28+
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
29+
add_link_options(-fsanitize=address)
3130
endif()
31+
32+
if(ENABLE_TSAN)
33+
message(STATUS "Enabling Thread Sanitizer")
34+
add_compile_options(-fsanitize=thread)
35+
add_link_options(-fsanitize=thread)
36+
endif()
37+
38+
if(ENABLE_UBSAN)
39+
message(STATUS "Enabling Undefined Behavior Sanitizer")
40+
add_compile_options(-fsanitize=undefined)
41+
add_link_options(-fsanitize=undefined)
42+
endif()
43+
3244
if(MSVC)
3345
add_compile_options(/W4)
3446
else()

nle/tests/test_system.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# Copyright (c) Facebook, Inc. and its affiliates.
2+
3+
import ctypes
4+
import functools
25
import multiprocessing as mp
36
import queue
47
import random
@@ -19,17 +22,32 @@ def new_env_one_step():
1922
return terminated
2023

2124

25+
@functools.cache
26+
def is_asan():
27+
"""Checks if the process is running with ASAN.
28+
29+
See if the __asan_init symbol is present in the current process.
30+
"""
31+
32+
current_process = ctypes.CDLL(None)
33+
return hasattr(current_process, "__asan_init")
34+
35+
2236
@pytest.mark.parametrize(
2337
"ctx", [mp.get_context(m) for m in START_METHODS], ids=START_METHODS
2438
)
2539
class TestEnvSubprocess:
2640
def test_env_in_subprocess(self, ctx):
41+
if ctx.get_start_method() == "spawn" and is_asan():
42+
pytest.skip("ASAN crashes on spawn on this environment")
2743
p = ctx.Process(target=new_env_one_step)
2844
p.start()
2945
p.join()
3046
assert p.exitcode == 0
3147

3248
def test_env_before_and_in_subprocess(self, ctx):
49+
if ctx.get_start_method() == "spawn" and is_asan():
50+
pytest.skip("ASAN crashes on spawn on this environment")
3351
new_env_one_step()
3452
p = ctx.Process(target=new_env_one_step)
3553
p.start()

0 commit comments

Comments
 (0)