Add AddressSanitizer support for clang-cl builds

Select the clang-cl-asan CMake preset (RelWithDebInfo, clang-cl,
ADDRESS_SANITIZER=ON) to instrument first-party code with AddressSanitizer.
The wiring lives in cmake/AddressSanitizer.cmake; SANITIZERS.md tells how to
add the clang-cl tools, build, and read the report.

Details:
- Add the clang-cl-asan preset so the asan build is one selection in Visual
  Studio, and a base for a CMakeUserPresets.json to inherit.
- Instrument first-party code only; the vendored libraries keep default flags.
- Use the release CRT and disable MSVC-STL container annotations, so
  instrumented and un-instrumented TUs stay compatible.
- Pass /bigobj to the TUs asan inflates past the COFF section cap.
- Link the asan runtime for clang-cl (lld-link does not infer it).
- Stub Bink into the exe: retail binkw32.dll cannot load in an asan process
  (its image base is the 32-bit shadow), so compile no-op exports instead.
- Route the asan report to gamedir/asan.report.<pid>, since every app is a
  WIN32 GUI app with no console to receive the default stderr report.
- Opt functions with 32-bit inline __asm out of instrumentation with
  cmake/asan-ignorelist.txt, one function at a time (asan reserves a register
  the asm needs). The rest of each translation unit stays instrumented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marco Antonio J. Costa
2026-08-20 23:22:38 -03:00
committed by majcosta
co-authored by Claude Opus 4.8
parent 69764e4459
commit 1bed0047ea
7 changed files with 365 additions and 10 deletions
+50
View File
@@ -0,0 +1,50 @@
# AddressSanitizer wiring, kept in one place. Include this early: it declares
# the option and the _asan generator expression that the CRT selection in the
# root CMakeLists depends on. The two functions are ordered hook points the
# root calls at the right moments:
# ja2_asan_instrument_first_party() — after the vendored add_subdirectory()s
# (they must keep default flags), before our own targets are defined.
# ja2_asan_link_binkw32_stub(<exe>) — per executable, links the Bink stubs in.
option(ADDRESS_SANITIZER "Enable AddressSanitizer for Debug and RelWithDebInfo" OFF)
# asan only on the debuggable configs, shared by the flag and the CRT choice
set(_asan "$<AND:$<BOOL:${ADDRESS_SANITIZER}>,$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>>")
if(ADDRESS_SANITIZER)
message(STATUS "AddressSanitizer ENABLED for Debug and RelWithDebInfo (first-party code only)")
message(STATUS " Bink stubbed into the exe; no binkw32.dll import, so the asan exe runs from gamedir untouched")
# keep MSVC-STL container annotations uniform across un-instrumented TUs
add_compile_definitions("$<${_asan}:_DISABLE_STL_ANNOTATION>")
# retail binkw32.dll cannot load in an asan process (image base 0x30000000 is
# the 32-bit shadow). __RADINEXE__ makes bink.h declare the Bink functions as
# in-exe calls instead of dllimports (sgp/RAD.H), so ja2_asan_link_binkw32_stub
# can satisfy them from sgp/binkw32_stub.c and the exe imports no binkw32.dll.
# Configure-time, not per-config: it must stay in lockstep with dropping the
# binkw32 import library from the link (see root CMakeLists).
add_compile_definitions(__RADINEXE__)
endif()
function(ja2_asan_instrument_first_party)
# instrument first-party code; /bigobj for the TUs asan inflates past the COFF
# section cap (LanguageStrings.cpp). The ignorelist opts individual functions
# with 32-bit inline __asm out of instrumentation (see cmake/asan-ignorelist.txt).
# Empty when the option is off.
add_compile_options(
"$<${_asan}:-fsanitize=address;-fsanitize-ignorelist=${CMAKE_SOURCE_DIR}/cmake/asan-ignorelist.txt;/bigobj>")
# clang-cl/lld-link do not infer the asan runtime; /WHOLEARCHIVE is invalid for llvm-lib
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
add_link_options(
"$<$<AND:${_asan},$<STREQUAL:$<TARGET_PROPERTY:TYPE>,EXECUTABLE>>:clang_rt.asan_dynamic-i386.lib;/WHOLEARCHIVE:clang_rt.asan_static_runtime_thunk-i386.lib>")
endif()
endfunction()
function(ja2_asan_link_binkw32_stub exe)
# Compile the no-op Bink exports into the exe. With __RADINEXE__ above the game
# calls them directly, so this replaces the binkw32.dll the retail import
# library would have pulled in.
if(ADDRESS_SANITIZER)
target_sources(${exe} PRIVATE "${CMAKE_SOURCE_DIR}/sgp/binkw32_stub.c")
endif()
endfunction()
+104
View File
@@ -0,0 +1,104 @@
# AddressSanitizer ignorelist. Functions with 32-bit inline __asm blocks:
# asan's shadow-base register reservation starves a register the asm needs,
# so instrumenting them fails to compile. Excluded from instrumentation only
# (still linked and called); the rest of each translation unit stays checked.
# Patterns end in @@ so each matches one clang-cl (MSVC) mangled name exactly,
# never a longer-named sibling (e.g. Blt16BPPTo16BPP vs Blt16BPPTo16BPPTrans).
# Regenerate: grep the *.cpp for __asm / _asm and list the enclosing functions.
[address]
# sgp/vobject_blitters.cpp
fun:*BlitZRect@@*
fun:*Blt16BPPBufferPixelateRectWithColor@@*
fun:*Blt16BPPBufferShadowRect@@*
fun:*Blt16BPPBufferShadowRectAlternateTable@@*
fun:*Blt16BPPDataTo16BPPBufferTransZClip@@*
fun:*Blt16BPPDataTo16BPPBufferTransparentClip@@*
fun:*Blt16BPPTo16BPP@@*
fun:*Blt16BPPTo16BPPMirror@@*
fun:*Blt8BPPDataSubTo16BPPBuffer@@*
fun:*Blt8BPPDataTo16BPPBuffer@@*
fun:*Blt8BPPDataTo16BPPBufferFullTransparent@@*
fun:*Blt8BPPDataTo16BPPBufferHalf@@*
fun:*Blt8BPPDataTo16BPPBufferHalfRect@@*
fun:*Blt8BPPDataTo16BPPBufferIntensity@@*
fun:*Blt8BPPDataTo16BPPBufferIntensityClip@@*
fun:*Blt8BPPDataTo16BPPBufferIntensityZ@@*
fun:*Blt8BPPDataTo16BPPBufferIntensityZClip@@*
fun:*Blt8BPPDataTo16BPPBufferIntensityZNB@@*
fun:*Blt8BPPDataTo16BPPBufferIntensityZNBClip@@*
fun:*Blt8BPPDataTo16BPPBufferMask@@*
fun:*Blt8BPPDataTo16BPPBufferMonoShadow@@*
fun:*Blt8BPPDataTo16BPPBufferMonoShadowClip@@*
fun:*Blt8BPPDataTo16BPPBufferOutline@@*
fun:*Blt8BPPDataTo16BPPBufferOutlineClip@@*
fun:*Blt8BPPDataTo16BPPBufferOutlineShadow@@*
fun:*Blt8BPPDataTo16BPPBufferOutlineShadowClip@@*
fun:*Blt8BPPDataTo16BPPBufferOutlineZ@@*
fun:*Blt8BPPDataTo16BPPBufferOutlineZClip@@*
fun:*Blt8BPPDataTo16BPPBufferOutlineZNB@@*
fun:*Blt8BPPDataTo16BPPBufferOutlineZPixelateObscured@@*
fun:*Blt8BPPDataTo16BPPBufferOutlineZPixelateObscuredClip@@*
fun:*Blt8BPPDataTo16BPPBufferShadow@@*
fun:*Blt8BPPDataTo16BPPBufferShadowClip@@*
fun:*Blt8BPPDataTo16BPPBufferShadowZ@@*
fun:*Blt8BPPDataTo16BPPBufferShadowZClip@@*
fun:*Blt8BPPDataTo16BPPBufferShadowZNB@@*
fun:*Blt8BPPDataTo16BPPBufferShadowZNBClip@@*
fun:*Blt8BPPDataTo16BPPBufferTransMirror@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadow@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadowAlpha@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadowBelowOrEqualZNBClip@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadowClip@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadowClipAlpha@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadowZ@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadowZAlpha@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadowZClip@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadowZClipAlpha@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadowZNB@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadowZNBAlpha@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadowZNBClip@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadowZNBClipAlpha@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadowZNBObscured@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadowZNBObscuredAlpha@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadowZNBObscuredClip@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadowZNBObscuredClipAlpha@@*
fun:*Blt8BPPDataTo16BPPBufferTransShadowZNBObscuredTest@@*
fun:*Blt8BPPDataTo16BPPBufferTransZ@@*
fun:*Blt8BPPDataTo16BPPBufferTransZClip@@*
fun:*Blt8BPPDataTo16BPPBufferTransZClipPixelate@@*
fun:*Blt8BPPDataTo16BPPBufferTransZClipPixelateObscured@@*
fun:*Blt8BPPDataTo16BPPBufferTransZClipTranslucent@@*
fun:*Blt8BPPDataTo16BPPBufferTransZNB@@*
fun:*Blt8BPPDataTo16BPPBufferTransZNBClip@@*
fun:*Blt8BPPDataTo16BPPBufferTransZNBClipColor@@*
fun:*Blt8BPPDataTo16BPPBufferTransZNBClipPixelate@@*
fun:*Blt8BPPDataTo16BPPBufferTransZNBClipTranslucent@@*
fun:*Blt8BPPDataTo16BPPBufferTransZNBColor@@*
fun:*Blt8BPPDataTo16BPPBufferTransZNBPixelate@@*
fun:*Blt8BPPDataTo16BPPBufferTransZNBTranslucent@@*
fun:*Blt8BPPDataTo16BPPBufferTransZPixelate@@*
fun:*Blt8BPPDataTo16BPPBufferTransZPixelateObscured@@*
fun:*Blt8BPPDataTo16BPPBufferTransZTranslucent@@*
fun:*Blt8BPPDataTo16BPPBufferTransparent@@*
fun:*Blt8BPPDataTo16BPPBufferTransparentClip@@*
fun:*Blt8BPPTo8BPP@@*
fun:*FillRect16BPP@@*
fun:*blendWithAlpha@@*
# sgp/shading.cpp
fun:*FindIndecies@@*
# TileEngine/renderworld.cpp
fun:*Blt8BPPDataTo16BPPBufferTransInvZ@@*
fun:*Blt8BPPDataTo16BPPBufferTransZIncClip@@*
fun:*Blt8BPPDataTo16BPPBufferTransZIncClipZSameZBurnsThrough@@*
fun:*Blt8BPPDataTo16BPPBufferTransZIncObscureClip@@*
fun:*Blt8BPPDataTo16BPPBufferTransZTransShadowIncClip@@*
fun:*Blt8BPPDataTo16BPPBufferTransZTransShadowIncClipAlpha@@*
fun:*Blt8BPPDataTo16BPPBufferTransZTransShadowIncObscureClip@@*
fun:*Blt8BPPDataTo16BPPBufferTransZTransShadowIncObscureClipAlpha@@*
fun:*IsTileRedundent@@*
fun:*Zero8BPPDataTo16BPPBufferTransparent@@*
# TileEngine/Interactive Tiles.cpp
fun:*CheckVideoObjectScreenCoordinateInData@@*
# Ja2/profiler.cpp
fun:*getCPUCount@PerfManager@@*