mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-08 02:53:25 -08:00
Adds -DENABLE_SANITIZERS, off by default, building with AddressSanitizer and UndefinedBehaviorSanitizer. The bulk of the work is in the three ways the obvious implementation silently does the wrong thing. Probe the flags on the link step, not just the compile step. check_cxx_compiler_flag() only compiles, so every sanitizer flag comes back unsupported, because linking an ASan object without -fsanitize=address leaves __asan_init undefined. The result is not a build failure but a build that appears instrumented and is not, which is worse. Use check_cxx_source_compiles() with CMAKE_REQUIRED_LINK_OPTIONS instead, and treat an unsupported sanitizer pair as fatal since the cosmetic flags are allowed to degrade. Opt vendored sources out per source, through a cockatrice_disable_sanitizers_for() helper rather than a list of set_source_files_properties() calls at the root. The helper has to exist because CMake resolves a source-file property against the directory scope its target was declared in: setting the property from the root with absolute paths is a no-op, silently, which is why the call sites sit next to the add_library and add_executable that pull the vendored code in. SFMT, minizip, the LZMA SDK and Qxt SMTP are excluded. oracle/src/qt-json is in the history but not in the tree, removed in #7214. Also mark those sources SKIP_PRECOMPILE_HEADERS. The PCH is built with the sanitizer flags, so it defines __SANITIZE_ADDRESS__; a translation unit compiled with -fno-sanitize=all does not and GCC then refuses the PCH, which is a fatal -Werror=invalid-pch in the servatrice and oracle builds. -fno-sanitize=vptr is not optional. Qt constructs QObject subclasses into raw storage and writes the vptr afterwards, so the vptr check reports on essentially every QObject instantiation, and it has no value here because nothing outside moc-generated code inherits QObject. Verified on the resulting build: our own translation units carry ASan and UBSan calls, SFMT.o carries neither, and the full suite of 464 cases passes clean under ASAN_OPTIONS=detect_leaks=0:abort_on_error=1 with UBSAN_OPTIONS=halt_on_error=0. The test suite needs two adjustments to make this option usable at all, both of which land here because they are consequences of COCKATRICE_SANITIZERS existing rather than independent changes. DISCOVERY_TIMEOUT rises from 60s to 300s under sanitizers. An instrumented binary takes 2.8s to answer --gtest_list_tests against 0.04s uninstrumented, and discovery runs several binaries at once behind a -j build. One target is excluded via a NO_SANITIZE flag, server_developer_role_test. Reaching google::protobuf::TextFormat::Printer's destructor aborts on an absl assertion inside system protobuf 33.4 whenever the consumer translation unit is built without NDEBUG, which a Debug sanitizer build is. This reproduces with no Cockatrice code at all, just "TextFormat::Printer p; p.PrintToString(msg, &out);" built with -fsanitize=address, so it is upstream rather than ours. It is a flag and not a fourth label because "skip this in the sanitizer leg" has to compose with a test's group rather than compete for the single label slot, and because a flag excludes by not registering the cases at all, which cannot rot the way a label silently can. The target still builds and still runs on the legs without sanitizers.
94 lines
4.2 KiB
CMake
94 lines
4.2 KiB
CMake
# AddressSanitizer + UndefinedBehaviorSanitizer opt-in, plus per-source opt-out
|
|
# Input: ENABLE_SANITIZERS
|
|
# Defines: cockatrice_disable_sanitizers_for
|
|
#
|
|
# Included once from the root CMakeLists.txt, immediately after the per-compiler flag
|
|
# block, so add_compile_options() reaches every library and executable defined below.
|
|
|
|
include(CheckCXXSourceCompiles)
|
|
|
|
# Opt a set of vendored sources out of instrumentation.
|
|
#
|
|
# cockatrice_disable_sanitizers_for(<source> ...)
|
|
#
|
|
# Sanitizer findings inside vendored code are not actionable for us, and letting them
|
|
# through buries our own bugs in noise until everyone learns to skip the job.
|
|
#
|
|
# The sources must be named relative to the directory that compiles them. CMake resolves a
|
|
# source-file property against the directory scope the target was declared in, so calling
|
|
# this from the root with absolute paths silently does nothing. That is why the call sites
|
|
# live next to the add_library/add_executable that pull the vendored code in.
|
|
#
|
|
# SKIP_PRECOMPILE_HEADERS is not optional. The precompiled header is built with the
|
|
# sanitizer flags, so it defines __SANITIZE_ADDRESS__; a translation unit compiled with
|
|
# -fno-sanitize=all does not, and GCC then refuses the PCH with
|
|
# "not used because __SANITIZE_ADDRESS__ not defined", which is a fatal -Werror=invalid-pch
|
|
# in the servatrice and oracle builds.
|
|
function(cockatrice_disable_sanitizers_for)
|
|
if(NOT ENABLE_SANITIZERS)
|
|
return()
|
|
endif()
|
|
set_source_files_properties(${ARGN} PROPERTIES COMPILE_OPTIONS "-fno-sanitize=all" SKIP_PRECOMPILE_HEADERS ON)
|
|
endfunction()
|
|
|
|
if(ENABLE_SANITIZERS)
|
|
# Probe a flag the way it will actually be used: on the compile step AND the link step.
|
|
# check_cxx_compiler_flag() only probes the compile step, which makes every sanitizer
|
|
# flag look unsupported because linking an ASan object without -fsanitize=address leaves
|
|
# __asan_init undefined. That is a silent misconfiguration rather than an error, so the
|
|
# probe has to link.
|
|
function(cockatrice_probe_flag FLAG VARNAME)
|
|
set(CMAKE_REQUIRED_FLAGS "${FLAG}")
|
|
set(CMAKE_REQUIRED_LINK_OPTIONS "${FLAG}")
|
|
check_cxx_source_compiles("int main() { return 0; }" ${VARNAME})
|
|
unset(CMAKE_REQUIRED_FLAGS)
|
|
unset(CMAKE_REQUIRED_LINK_OPTIONS)
|
|
endfunction()
|
|
|
|
set(SANITIZER_FLAGS
|
|
-fsanitize=address,undefined
|
|
-fno-omit-frame-pointer
|
|
# Qt builds QObject subclasses into raw storage and writes the vptr afterwards, which
|
|
# the vptr check reports on essentially every QObject instantiation. The check has no
|
|
# value for us because nothing outside moc-generated code inherits QObject, so it is
|
|
# off outright rather than suppressed piecemeal.
|
|
-fno-sanitize=vptr
|
|
)
|
|
|
|
# MSVC has no ASan/UBSan equivalent, and clang-cl would need /fsanitize=address plus a
|
|
# matching runtime link, which is a separate piece of work.
|
|
if(MSVC)
|
|
message(FATAL_ERROR "ENABLE_SANITIZERS is not supported with MSVC")
|
|
endif()
|
|
|
|
# Probe rather than assume, so an older toolchain gets the sanitizers it can support
|
|
# instead of failing the build on a flag it does not recognize.
|
|
set(SANITIZER_SUPPORTED_FLAGS)
|
|
foreach(FLAG ${SANITIZER_FLAGS})
|
|
string(MAKE_C_IDENTIFIER "COCKATRICE_HAS${FLAG}" _FLAG_VAR)
|
|
cockatrice_probe_flag("${FLAG}" ${_FLAG_VAR})
|
|
if(${_FLAG_VAR})
|
|
list(APPEND SANITIZER_SUPPORTED_FLAGS ${FLAG})
|
|
else()
|
|
message(STATUS "Sanitizers: ${FLAG} unsupported by ${CMAKE_CXX_COMPILER_ID}, skipping")
|
|
endif()
|
|
endforeach()
|
|
|
|
# The whole point of the option is the sanitizers, so an unsupported pair is fatal even
|
|
# though the cosmetic flags above are allowed to degrade.
|
|
if(NOT "-fsanitize=address,undefined" IN_LIST SANITIZER_SUPPORTED_FLAGS)
|
|
message(FATAL_ERROR "ENABLE_SANITIZERS requires -fsanitize=address,undefined, which "
|
|
"${CMAKE_CXX_COMPILER_ID} does not accept"
|
|
)
|
|
endif()
|
|
|
|
add_compile_options(${SANITIZER_SUPPORTED_FLAGS})
|
|
add_link_options(-fsanitize=address,undefined)
|
|
|
|
# Recorded so the test tree can label its entries and a plain `make` at the same time
|
|
# cannot quietly disagree with what CI ran.
|
|
set(COCKATRICE_SANITIZERS
|
|
ON
|
|
CACHE INTERNAL "AddressSanitizer and UBSan are enabled"
|
|
)
|
|
endif()
|