diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c31b519..388eac2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,8 +2,7 @@ stages: - Analysis - Targeted Code Coverage - All Code Coverage - - Fedora - - openSUSE + - Sanitizers variables: CTEST_OUTPUT_ON_FAILURE: "1" @@ -427,10 +426,10 @@ Windows/All CC Clang Shared: - ninja -C build ccov-report - ninja -C build ccov-all-report -# Fedora +# Sanitizers -.fedora_success_template: &fedora_success_template - stage: Fedora +.linux_success_template: &linux_success_template + stage: Sanitizers image: stabletec/build-core:fedora parallel: matrix: @@ -444,8 +443,8 @@ Windows/All CC Clang Shared: - ninja -C build - ninja -C build test -.fedora_failure_template: &fedora_failure_template - stage: Fedora +.linux_failure_template: &linux_failure_template + stage: Sanitizers image: stabletec/build-core:fedora parallel: matrix: @@ -459,37 +458,183 @@ Windows/All CC Clang Shared: - ninja -C build - "! ninja -C build test" -Fedora/Static Analysis: +Linux/Static Analysis: variables: CC: clang CXX: clang++ CMAKE_OPTIONS: -DCLANG_TIDY=ON -DCPPCHECK=ON - <<: *fedora_success_template + <<: *linux_success_template -Fedora/ThreadSanitizer: +Linux/GCC/ThreadSanitizer: + variables: + CMAKE_OPTIONS: -D EXAMPLE_USE_SANITIZER=thread + <<: *linux_failure_template + +Linux/Clang/ThreadSanitizer: + variables: + CC: clang + CXX: clang++ + CMAKE_OPTIONS: -D EXAMPLE_USE_SANITIZER=thread + <<: *linux_failure_template + +Linux/GCC/AddressSanitizer: + variables: + CMAKE_OPTIONS: -D EXAMPLE_USE_SANITIZER=address + <<: *linux_failure_template + +Linux/Clang/AddressSanitizer: + variables: + CC: clang + CXX: clang++ + CMAKE_OPTIONS: -D EXAMPLE_USE_SANITIZER=address + <<: *linux_failure_template + +Linux/GCC/LeakSanitizer: + variables: + CMAKE_OPTIONS: -D EXAMPLE_USE_SANITIZER=leak + <<: *linux_failure_template + +Linux/Clang/LeakSanitizer: + variables: + CC: clang + CXX: clang++ + CMAKE_OPTIONS: -D EXAMPLE_USE_SANITIZER=leak + <<: *linux_failure_template + +Linux/Clang/MemorySanitizer: + variables: + CC: clang + CXX: clang++ + CMAKE_OPTIONS: -D EXAMPLE_USE_SANITIZER=memory + <<: *linux_failure_template + +Linux/GCC/UndefinedSanitizer: + variables: + CMAKE_OPTIONS: -D EXAMPLE_USE_SANITIZER=undefined + <<: *linux_success_template + +Linux/Clang/UndefinedSanitizer: + variables: + CC: clang + CXX: clang++ + CMAKE_OPTIONS: -D EXAMPLE_USE_SANITIZER=undefined + <<: *linux_success_template + +Linux/Clang/ThreadSanitizer (LEGACY): variables: CC: clang CXX: clang++ - CMAKE_OPTIONS: -DUSE_SANITIZER=Thread - <<: *fedora_failure_template + CMAKE_OPTIONS: -D USE_SANITIZER=thread + <<: *linux_failure_template -Fedora/AddressSanitizer: +Linux/Clang/AddressSanitizer (LEGACY): variables: CC: clang CXX: clang++ - CMAKE_OPTIONS: -DUSE_SANITIZER=Address - <<: *fedora_failure_template + CMAKE_OPTIONS: -D USE_SANITIZER=address + <<: *linux_failure_template -Fedora/LeakSanitizer: +Linux/Clang/LeakSanitizer (LEGACY): variables: CC: clang CXX: clang++ - CMAKE_OPTIONS: -DUSE_SANITIZER=Leak - <<: *fedora_failure_template + CMAKE_OPTIONS: -D USE_SANITIZER=leak + <<: *linux_failure_template -Fedora/UndefinedSanitizer: +Linux/Clang/MemorySanitizer (LEGACY): variables: CC: clang CXX: clang++ - CMAKE_OPTIONS: -DUSE_SANITIZER=Undefined - <<: *fedora_success_template + CMAKE_OPTIONS: -D USE_SANITIZER=memory + <<: *linux_failure_template + +Linux/Clang/UndefinedSanitizer (LEGACY): + variables: + CC: clang + CXX: clang++ + CMAKE_OPTIONS: -D USE_SANITIZER=undefined + <<: *linux_success_template + +.macos_success_template: &macos_success_template + stage: Sanitizers + parallel: + matrix: + - ARCH: [arm64] + tags: + - macos + - ${ARCH} + script: + - cmake example/all -B build -GNinja -DCMAKE_BUILD_TYPE=Release ${CMAKE_OPTIONS} + - ninja -C build + - ninja -C build test + +.macos_failure_template: &macos_failure_template + stage: Sanitizers + parallel: + matrix: + - ARCH: [arm64] + tags: + - macos + - ${ARCH} + script: + - cmake example/all -B build -GNinja -DCMAKE_BUILD_TYPE=Release ${CMAKE_OPTIONS} + - ninja -C build + - "! ninja -C build test" + +macOS/AppleClang/ThreadSanitizer: + variables: + CMAKE_OPTIONS: -D EXAMPLE_USE_SANITIZER=thread + <<: *macos_failure_template + +macOS/Clang/ThreadSanitizer: + variables: + CC: clang + CXX: clang++ + CMAKE_OPTIONS: -D EXAMPLE_USE_SANITIZER=thread + <<: *macos_failure_template + +macOS/AppleClang/AddressSanitizer: + variables: + CMAKE_OPTIONS: -D EXAMPLE_USE_SANITIZER=address + <<: *macos_failure_template + +macOS/Clang/AddressSanitizer: + variables: + CC: clang + CXX: clang++ + CMAKE_OPTIONS: -D EXAMPLE_USE_SANITIZER=address + <<: *macos_failure_template + +macOS/Clang/LeakSanitizer: + variables: + CC: clang + CXX: clang++ + CMAKE_OPTIONS: -D EXAMPLE_USE_SANITIZER=leak + <<: *macos_failure_template + +macOS/AppleClang/UndefinedSanitizer: + variables: + CMAKE_OPTIONS: -D EXAMPLE_USE_SANITIZER=undefined + <<: *macos_success_template + +macOS/Clang/UndefinedSanitizer: + variables: + CC: clang + CXX: clang++ + CMAKE_OPTIONS: -D EXAMPLE_USE_SANITIZER=undefined + <<: *macos_success_template + +Windows/MSVC/Address Sanitizer: + image: stabletec/build-core:windows-2019 + stage: Sanitizers + parallel: + matrix: + - ARCH: [amd64] + tags: + - container + - windows + - ${ARCH} + script: + - cmake example/all/ -B build -GNinja -D EXAMPLE_USE_SANITIZER=address + - ninja -C build + - ninja -C build test ; if ($? -ne 0) { return 0 } else { return 1 } diff --git a/README.md b/README.md index 3356ee2..eea9631 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,17 @@ This is a collection of quite useful scripts that expand the possibilities for b - [C++ Standards `c++-standards.cmake`](#c-standards-c-standardscmake) - [Sanitizer Builds `sanitizers.cmake`](#sanitizer-builds-sanitizerscmake) + - [Usage](#usage) - [Code Coverage `code-coverage.cmake`](#code-coverage-code-coveragecmake) - [Added Targets](#added-targets) - - [Usage](#usage) + - [Usage](#usage-1) - [Example 1 - All targets instrumented](#example-1---all-targets-instrumented) - [1a - Via global command](#1a---via-global-command) - [1b - Via target commands](#1b---via-target-commands) - [Example 2: Target instrumented, but with regex pattern of files to be excluded from report](#example-2-target-instrumented-but-with-regex-pattern-of-files-to-be-excluded-from-report) - [Example 3: Target added to the 'ccov' and 'ccov-all' targets](#example-3-target-added-to-the-ccov-and-ccov-all-targets) - [AFL Fuzzing Instrumentation `afl-fuzzing.cmake`](#afl-fuzzing-instrumentation-afl-fuzzingcmake) - - [Usage](#usage-1) + - [Usage](#usage-2) - [Compiler Options `compiler-options.cmake`](#compiler-options-compiler-optionscmake) - [Dependency Graph `dependency-graph.cmake`](#dependency-graph-dependency-graphcmake) - [Required Arguments](#required-arguments) @@ -88,16 +89,55 @@ A quick rundown of the tools available, and what they do: - [MemorySanitizer](https://clang.llvm.org/docs/MemorySanitizer.html) detects uninitialized reads. - [Control Flow Integrity](https://clang.llvm.org/docs/ControlFlowIntegrity.html) is designed to detect certain forms of undefined behaviour that can potentially allow attackers to subvert the program's control flow. -These are used by declaring the `USE_SANITIZER` CMake variable as string containing any of: -- Address -- Memory -- MemoryWithOrigins -- Undefined -- Thread -- Leak -- CFI +### Usage + +The most basic way to enable sanitizers is to simply call `add_sanitizer_support` with the desired sanitizer names, whereupon it will check for the availability and compatability of the combined flags and apply to following compile targets: +```cmake +# apply address and leak sanitizers +add_sanitizer_support(address leak) +# future targets will be compiled with '-fsanitize=address -fsanitize=leak' +``` + +Compile options on a per-sanitizer basis can be accomplished by calling `set_sanitizer_options` before with the name of the sanitizer and desired compile options: +```cmake +# set custom options that will be applies with that specific sanitizer +set_sanitizer_options(address -fno-omit-frame-pointer) + +add_sanitizer_support(address leak) +# future targets will be compiled with '-fsanitize=address -fno-omit-frame-pointer -fsanitize=leak' +``` + +Per-sanitizer compile options can also be set by setting the named `SANITIZER_${SANITIZER_NAME}_OPTIONS` variable before, either in script or via the command line. +```cmake +# CMake called from command line as `cmake -S . -B build -D SANITIZER_ADDRESS_OPTION='-fno-omit-frame-pointer'` + +add_sanitizer_support(address) +# future targets will be compiled with '-fsanitize=address -fno-omit-frame-pointer' +# despite no call to `set_sanitizer_options` +``` + +To prevent custom sanitizer options from external source being overwritten, the `DEFAULT` option can be used, so that the flags are only used if none have been set previously: +```cmake +# command line has options set via command-line: `cmake -S . -B build -D SANITIZER_ADDRESS_OPTION='-fno-omit-frame-pointer'` -Multiple values are allowed, e.g. `-DUSE_SANITIZER=Address,Leak` but some sanitizers cannot be combined together, e.g.`-DUSE_SANITIZER=Address,Memory` will result in configuration error. The delimeter character is not required and `-DUSE_SANITIZER=AddressLeak` would work as well. +# attempt to set custom options that will not apply since the variable already exists +# and `DEFAULT` option is passed in. +set_sanitizer_options(address DEFAULT -some-other-flag) + +add_sanitizer_support(address) +# future targets will be compiled with '-fsanitize=address -fno-omit-frame-pointer' +``` + +Different sets of options used with the sanitizer can be accomplished by defining the sanitizer serparately with the call to `set_sanitizer_option`: +```cmake +# Despite both using the 'memory' sanitizer, which specific set of flags can be chosen +# when calling `add_sanitizer_support` with either 'memory' or 'memorywithorigins' +set_sanitizer_options(memory DEFAULT -fno-omit-frame-pointer) +set_sanitizer_options(memorywithorigins DEFAULT + SANITIZER memory + -fno-omit-frame-pointer + -fsanitize-memory-track-origins) +``` ## Code Coverage [`code-coverage.cmake`](code-coverage.cmake) diff --git a/c++-standards.cmake b/c++-standards.cmake index 070cad0..012289b 100644 --- a/c++-standards.cmake +++ b/c++-standards.cmake @@ -1,5 +1,5 @@ # -# Copyright (C) 2018-2021 by George Cave - gcave@stablecoder.ca +# Copyright (C) 2018-2024 by George Cave - gcave@stablecoder.ca # # Licensed under the Apache License, Version 2.0 (the "License"); you may not # use this file except in compliance with the License. You may obtain a copy of @@ -21,8 +21,8 @@ macro(cxx_11) if(MSVC_VERSION GREATER_EQUAL "1900" AND CMAKE_VERSION LESS 3.10) include(CheckCXXCompilerFlag) - check_cxx_compiler_flag("/std:c++11" _cpp_latest_flag_supported) - if(_cpp_latest_flag_supported) + check_cxx_compiler_flag("/std:c++11" _cpp_11_flag_supported) + if(_cpp_11_flag_supported) add_compile_options("/std:c++11") endif() endif() @@ -36,8 +36,8 @@ macro(cxx_14) if(MSVC_VERSION GREATER_EQUAL "1900" AND CMAKE_VERSION LESS 3.10) include(CheckCXXCompilerFlag) - check_cxx_compiler_flag("/std:c++14" _cpp_latest_flag_supported) - if(_cpp_latest_flag_supported) + check_cxx_compiler_flag("/std:c++14" _cpp_14_flag_supported) + if(_cpp_14_flag_supported) add_compile_options("/std:c++14") endif() endif() @@ -51,8 +51,8 @@ macro(cxx_17) if(MSVC_VERSION GREATER_EQUAL "1900" AND CMAKE_VERSION LESS 3.10) include(CheckCXXCompilerFlag) - check_cxx_compiler_flag("/std:c++17" _cpp_latest_flag_supported) - if(_cpp_latest_flag_supported) + check_cxx_compiler_flag("/std:c++17" _cpp_17_flag_supported) + if(_cpp_17_flag_supported) add_compile_options("/std:c++17") endif() endif() diff --git a/example/all/CMakeLists.txt b/example/all/CMakeLists.txt index ef8db46..1bfadb0 100644 --- a/example/all/CMakeLists.txt +++ b/example/all/CMakeLists.txt @@ -7,7 +7,6 @@ set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/../..;") include(c++-standards) include(code-coverage) include(formatting) -include(sanitizers) include(tools) include(dependency-graph) @@ -15,21 +14,39 @@ include(dependency-graph) cxx_11() # Tools -file(GLOB_RECURSE FFILES *.[hc] *.[hc]pp) -clang_format(format ${FFILES}) +if(UNIX AND NOT APPLE) + file(GLOB_RECURSE FFILES *.[hc] *.[hc]pp) + clang_format(format ${FFILES}) -cmake_format(cmake-format ${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt - CMakeLists.txt) + cmake_format(cmake-format ${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt + CMakeLists.txt) -clang_tidy(-format-style=file -checks=* -header-filter='${CMAKE_SOURCE_DIR}/*') -include_what_you_use(-Xiwyu) -cppcheck( - --enable=warning,performance,portability,missingInclude - --template="[{severity}][{id}] {message} {callstack} \(On {file}:{line}\)" - --suppress=missingIncludeSystem --quiet --verbose --force) + clang_tidy(-format-style=file -checks=* + -header-filter='${CMAKE_SOURCE_DIR}/*') + include_what_you_use(-Xiwyu) + cppcheck( + --enable=warning,performance,portability,missingInclude + --template="[{severity}][{id}] {message} {callstack} \(On {file}:{line}\)" + --suppress=missingIncludeSystem --quiet --verbose --force) +endif() enable_testing() +# Sanitizers +include(sanitizers) + +set_sanitizer_options(address DEFAULT) +set_sanitizer_options(leak DEFAULT) +set_sanitizer_options(memory DEFAULT) +set_sanitizer_options(memorywithorigins DEFAULT SANITIZER memory + -fsanitize-memory-track-origins) +set_sanitizer_options(undefined DEFAULT) +set_sanitizer_options(thread DEFAULT) + +if(EXAMPLE_USE_SANITIZER) + add_sanitizer_support(${EXAMPLE_USE_SANITIZER}) +endif() + # Fails with ThreadSanitizer add_executable(tsanFail ../src/tsan_fail.cpp) target_code_coverage(tsanFail AUTO ALL) @@ -44,7 +61,7 @@ target_code_coverage(lsanFail AUTO ALL) add_test(lsan lsanFail) # Fails with AddressSanitizer -if(USE_SANITIZER MATCHES "[Aa]ddress") +if(EXAMPLE_USE_SANITIZER MATCHES "([Aa]ddress)") add_executable(asanFail ../src/asan_fail.cpp) target_code_coverage(asanFail AUTO ALL) add_test(asan asanFail) diff --git a/sanitizers.cmake b/sanitizers.cmake index b5244a1..2a9fc66 100644 --- a/sanitizers.cmake +++ b/sanitizers.cmake @@ -1,5 +1,5 @@ # -# Copyright (C) 2018-2022 by George Cave - gcave@stablecoder.ca +# Copyright (C) 2018-2024 by George Cave - gcave@stablecoder.ca # # Licensed under the Apache License, Version 2.0 (the "License"); you may not # use this file except in compliance with the License. You may obtain a copy of @@ -13,22 +13,82 @@ # License for the specific language governing permissions and limitations under # the License. -include(CheckCXXSourceCompiles) +# USAGE: + +# The most basic way to enable sanitizers is to simply call +# `add_sanitizer_support` with the desired sanitizer names, whereupon it will +# check for the availability and compatability of the combined flags and apply +# to following compile targets: +# ~~~ +# # apply address and leak sanitizers +# add_sanitizer_support(address leak) +# # future targets will be compiled with '-fsanitize=address -fsanitize=leak' +# ~~~ +# +# Compile options on a per-sanitizer basis can be accomplished by calling +# `set_sanitizer_options` before with the name of the sanitizer and desired +# compile options: +# ~~~ +# # set custom options that will be applies with that specific sanitizer +# set_sanitizer_options(address -fno-omit-frame-pointer) +# +# add_sanitizer_support(address leak) +# # future targets will be compiled with '-fsanitize=address -fno-omit-frame-pointer -fsanitize=leak' +# ~~~ +# +# Per-sanitizer compile options can also be set by setting the named +# `SANITIZER_${SANITIZER_NAME}_OPTIONS` variable before, either in script or via +# the command line. +# ~~~ +# # CMake called from command line as `cmake -S . -B build -D SANITIZER_ADDRESS_OPTION='-fno-omit-frame-pointer'` +# +# add_sanitizer_support(address) +# # future targets will be compiled with '-fsanitize=address -fno-omit-frame-pointer' +# # despite no call to `set_sanitizer_options` +# ~~~ +# +# To prevent custom sanitizer options from an external source being overwritten, +# the `DEFAULT` option can be used, so that the flags are only used if none have +# been set previously: +# ~~~ +# # command line has options set via command-line: `cmake -S . -B build -D SANITIZER_ADDRESS_OPTION='-fno-omit-frame-pointer'` +# +# # attempt to set custom options that will not apply since the variable already exists +# # and `DEFAULT` option is passed in. +# set_sanitizer_options(address DEFAULT -some-other-flag) +# +# add_sanitizer_support(address) +# # future targets will be compiled with '-fsanitize=address -fno-omit-frame-pointer' +# ~~~ +# +# Different sets of options used with the sanitizer can be accomplished by +# defining the sanitizer serparately with the call to `set_sanitizer_option`: +# ~~~ +# set_sanitizer_options(memory DEFAULT -fno-omit-frame-pointer) +# +# set_sanitizer_options(memorywithorigins DEFAULT +# SANITIZER memory +# -fno-omit-frame-pointer +# -fsanitize-memory-track-origins) +# +# # Despite both using the 'memory' sanitizer, which specific set of flags can be chosen +# # when calling `add_sanitizer_support` with either 'memory' or 'memorywithorigins' +# ~~~ -set(USE_SANITIZER - "" - CACHE - STRING - "Compile with a sanitizer. Options are: Address, Memory, MemoryWithOrigins, Undefined, Thread, Leak, 'Address;Undefined', CFI" -) +# LEGACY USAGE: -function(append value) - foreach(variable ${ARGN}) - set(${variable} - "${${variable}} ${value}" - PARENT_SCOPE) - endforeach(variable) -endfunction() +# Previous versions had a strict set of options that could be used via having a +# set CMake variable either in the script or from the command line: +# ~~~ +# # this can also be set via command line as `-D USE_SANITIZER='address leak'` +# set(USE_SANITIZER address leak) +# ~~~ +# This is now deprecated to be removed in a future version, but should still be +# functional until then, either by defining `USE_SANITIZER` or +# `SANITIZER_ENABLE_LEGACY_SUPPORT` before including the script file. + +include(CheckCXXCompilerFlag) +include(CheckCXXSourceCompiles) function(append_quoteless value) foreach(variable ${ARGN}) @@ -38,186 +98,264 @@ function(append_quoteless value) endforeach(variable) endfunction() -function(test_san_flags return_var flags) +function(test_san_flags RETURN_VAR LINK_OPTIONS) set(QUIET_BACKUP ${CMAKE_REQUIRED_QUIET}) set(CMAKE_REQUIRED_QUIET TRUE) - unset(${return_var} CACHE) - set(FLAGS_BACKUP ${CMAKE_REQUIRED_FLAGS}) - set(CMAKE_REQUIRED_FLAGS "${flags}") - check_cxx_source_compiles("int main() { return 0; }" ${return_var}) - set(CMAKE_REQUIRED_FLAGS "${FLAGS_BACKUP}") + unset(${RETURN_VAR} CACHE) + + # backup test flags + set(OPTION_FLAGS_BACKUP ${CMAKE_REQUIRED_FLAGS}) + set(LINK_FLAGS_BACKUP ${CMAKE_REQUIRED_LINK_OPTIONS}) + + # set link options + unset(CMAKE_REQUIRED_LINK_OPTIONS) + foreach(ARG ${${LINK_OPTIONS}}) + set(CMAKE_REQUIRED_LINK_OPTIONS ${CMAKE_REQUIRED_LINK_OPTIONS};${ARG}) + endforeach() + + # set compile options + unset(CMAKE_REQUIRED_FLAGS) + unset(test_san_flags_OPTION_TEST CACHE) + foreach(ARG ${ARGN}) + if(WIN32) + unset(test_san_flags_OPTION_TEST CACHE) + check_cxx_compiler_flag(${ARG} test_san_flags_OPTION_TEST) + if(NOT test_san_flags_OPTION_TEST) + break() + endif() + endif() + set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} ${ARG}") + endforeach() + + # actually test if compilation can occur with given compiler/link options + if(NOT DEFINED test_san_flags_OPTION_TEST OR test_san_flags_OPTION_TEST) + check_cxx_source_compiles("int main() { return 0; }" ${RETURN_VAR}) + endif() + + # reset backed-up flags + set(CMAKE_REQUIRED_LINK_OPTIONS "${LINK_FLAGS_BACKUP}") + set(CMAKE_REQUIRED_FLAGS "${OPTION_FLAGS_BACKUP}") + set(CMAKE_REQUIRED_QUIET "${QUIET_BACKUP}") endfunction() -if(USE_SANITIZER) - append("-fno-omit-frame-pointer" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) +# Adds/sets compile flags for a given sanitizer, and checks for +# compatability/availability with the current compiler. +# +# Each time the compile options for a sanitizer is modified, the availability +# will be re-checked and cached. +# +# After the check, the compile options will be stored in the CMake cache as the +# `SANITIZER_${SANITIZER_NAME}_OPTIONS` name which will be available as a +# modifiable CMake string. +# +# ~~~ +# Required: +# SANITIZER_NAME - Name of the sanitizer. When selected, this name also doubles +# as the name given to the compiler in the form of +# `-fsanitize=` in lower-case lettering. +# +# Optional: +# DEFAULT - Passed compile flags will only be applied if there is no currently defined +# `SANITIZER_${SANITIZER_NAME}_OPTIONS` variable +# SANITIZER - If defined, this replaces the SANITIZER_NAME for the compile/link +# in the form of `-fsanitize=` +# +# Additional parameters are added as compile flags +function(set_sanitizer_options SANITIZER_NAME) + # Argument parsing + set(options DEFAULT) + set(single_value_keywords SANITIZER) + set(multi_value_keywords) + cmake_parse_arguments( + set_sanitizer_options "${options}" "${single_value_keywords}" + "${multi_value_keywords}" ${ARGN}) + + string(TOUPPER ${SANITIZER_NAME} UPPER_SANITIZER_NAME) + string(TOLOWER ${SANITIZER_NAME} LOWER_SANITIZER_NAME) - unset(SANITIZER_SELECTED_FLAGS) + unset(USED_SANITIZER_OPTION) + if(NOT set_sanitizer_options_SANITIZER) + set(set_sanitizer_options_SANITIZER ${LOWER_SANITIZER_NAME}) + endif() - if(UNIX) + # if `DEFAULT` is specified, only apply new arguments if there is no previous + # cache + if(set_sanitizer_options_DEFAULT + AND DEFINED SANITIZER_${UPPER_SANITIZER_NAME}_OPTIONS) + return() + endif() - if(uppercase_CMAKE_BUILD_TYPE STREQUAL "DEBUG") - append("-O1" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + # check if the cache does not match what we have here, update the cache and + # check for availability + if(NOT DEFINED SANITIZER_${UPPER_SANITIZER_NAME}_OPTIONS + OR NOT (ARGN STREQUAL SANITIZER_${UPPER_SANITIZER_NAME}_OPTIONS)) + # don't overwrite options set via non-legacy path + if(DEFINED SANITIZER_${UPPER_SANITIZER_NAME}_OPTIONS + AND SANITIZER_LEGACY_SUPPORT) + # @todo remove when legacy support is + return() endif() - if(USE_SANITIZER MATCHES "([Aa]ddress)") - # Optional: -fno-optimize-sibling-calls -fsanitize-address-use-after-scope - message(STATUS "Testing with Address sanitizer") - set(SANITIZER_ADDR_FLAG "-fsanitize=address") - test_san_flags(SANITIZER_ADDR_AVAILABLE ${SANITIZER_ADDR_FLAG}) - if(SANITIZER_ADDR_AVAILABLE) - message(STATUS " Building with Address sanitizer") - append("${SANITIZER_ADDR_FLAG}" SANITIZER_SELECTED_FLAGS) + # set as the new cache + set(SANITIZER_${UPPER_SANITIZER_NAME}_SANITIZER + ${set_sanitizer_options_SANITIZER} + CACHE INTERNAL "" FORCE) + set(SANITIZER_${UPPER_SANITIZER_NAME}_OPTIONS + "${set_sanitizer_options_UNPARSED_ARGUMENTS}" + CACHE STRING "${LOWER_SANITIZER_NAME} sanitizer compile options" FORCE) - if(AFL) - append_quoteless(AFL_USE_ASAN=1 CMAKE_C_COMPILER_LAUNCHER - CMAKE_CXX_COMPILER_LAUNCHER) - endif() - else() - message( - FATAL_ERROR - "Address sanitizer not available for ${CMAKE_CXX_COMPILER}") - endif() - endif() + # check if sanitizer is available + message(CHECK_START + "Checking if '${LOWER_SANITIZER_NAME}' sanitizer is available") - if(USE_SANITIZER MATCHES "([Mm]emory([Ww]ith[Oo]rigins)?)") - # Optional: -fno-optimize-sibling-calls -fsanitize-memory-track-origins=2 - set(SANITIZER_MEM_FLAG "-fsanitize=memory") - if(USE_SANITIZER MATCHES "([Mm]emory[Ww]ith[Oo]rigins)") - message(STATUS "Testing with MemoryWithOrigins sanitizer") - append("-fsanitize-memory-track-origins" SANITIZER_MEM_FLAG) - else() - message(STATUS "Testing with Memory sanitizer") - endif() - test_san_flags(SANITIZER_MEM_AVAILABLE ${SANITIZER_MEM_FLAG}) - if(SANITIZER_MEM_AVAILABLE) - if(USE_SANITIZER MATCHES "([Mm]emory[Ww]ith[Oo]rigins)") - message(STATUS " Building with MemoryWithOrigins sanitizer") - else() - message(STATUS " Building with Memory sanitizer") - endif() - append("${SANITIZER_MEM_FLAG}" SANITIZER_SELECTED_FLAGS) + # check if the compile option combination can compile + unset(SANITIZER_${UPPER_SANITIZER_NAME}_AVAILABLE CACHE) + set(set_sanitizer_options_LINK_OPTIONS + -fsanitize=${SANITIZER_${UPPER_SANITIZER_NAME}_SANITIZER}) + test_san_flags( + SANITIZER_${UPPER_SANITIZER_NAME}_AVAILABLE + set_sanitizer_options_LINK_OPTIONS + -fsanitize=${SANITIZER_${UPPER_SANITIZER_NAME}_SANITIZER};${set_sanitizer_options_UNPARSED_ARGUMENTS} + ) - if(AFL) - append_quoteless(AFL_USE_MSAN=1 CMAKE_C_COMPILER_LAUNCHER - CMAKE_CXX_COMPILER_LAUNCHER) - endif() - else() - message( - FATAL_ERROR - "Memory [With Origins] sanitizer not available for ${CMAKE_CXX_COMPILER}" - ) - endif() - endif() + if(SANITIZER_${UPPER_SANITIZER_NAME}_AVAILABLE) + message(CHECK_PASS "available") - if(USE_SANITIZER MATCHES "([Uu]ndefined)") - message(STATUS "Testing with Undefined Behaviour sanitizer") - set(SANITIZER_UB_FLAG "-fsanitize=undefined") - if(EXISTS "${BLACKLIST_FILE}") - append("-fsanitize-blacklist=${BLACKLIST_FILE}" SANITIZER_UB_FLAG) + # add sanitizer to available options + if(NOT DEFINED SANITIZERS_AVAILABLE_LIST) + set(SANITIZERS_AVAILABLE_LIST + "${LOWER_SANITIZER_NAME}" + CACHE INTERNAL "" FORCE) + elseif(NOT SANITIZERS_AVAILABLE_LIST MATCHES "(${LOWER_SANITIZER_NAME})") + set(SANITIZERS_AVAILABLE_LIST + "${SANITIZERS_AVAILABLE_LIST};${LOWER_SANITIZER_NAME}" + CACHE INTERNAL "" FORCE) endif() - test_san_flags(SANITIZER_UB_AVAILABLE ${SANITIZER_UB_FLAG}) - if(SANITIZER_UB_AVAILABLE) - message(STATUS " Building with Undefined Behaviour sanitizer") - append("${SANITIZER_UB_FLAG}" SANITIZER_SELECTED_FLAGS) + else() + message(CHECK_FAIL "not available") - if(AFL) - append_quoteless(AFL_USE_UBSAN=1 CMAKE_C_COMPILER_LAUNCHER - CMAKE_CXX_COMPILER_LAUNCHER) - endif() - else() - message( - FATAL_ERROR - "Undefined Behaviour sanitizer not available for ${CMAKE_CXX_COMPILER}" - ) + # remove from available list if it is not available + if(SANITIZERS_AVAILABLE_LIST MATCHES "(${LOWER_SANITIZER_NAME})") + string(REPLACE "${LOWER_SANITIZER_NAME};" "" REPLACED_STRING + SANITIZERS_AVAILABLE_LIST) + string(REPLACE ";${LOWER_SANITIZER_NAME}" "" REPLACED_STRING2 + REPLACED_STRING) + string(REPLACE "${LOWER_SANITIZER_NAME}" "" REPLACED_STRING3 + REPLACED_STRING2) + set(SANITIZERS_AVAILABLE_LIST + "${REPLACED_STRING3}" + CACHE INTERNAL "" FORCE) endif() endif() + endif() +endfunction() - if(USE_SANITIZER MATCHES "([Tt]hread)") - message(STATUS "Testing with Thread sanitizer") - set(SANITIZER_THREAD_FLAG "-fsanitize=thread") - test_san_flags(SANITIZER_THREAD_AVAILABLE ${SANITIZER_THREAD_FLAG}) - if(SANITIZER_THREAD_AVAILABLE) - message(STATUS " Building with Thread sanitizer") - append("${SANITIZER_THREAD_FLAG}" SANITIZER_SELECTED_FLAGS) +# Adds the given sanitizer compile options together, checks the combined +# compatability and adds the compile_options and link_options +# ~~~ +# Required: +# All given parameters are either sanitizer/options set previously via +# `set_sanitizer_options` or are created/checked dynamically with no +# options, in the form of `-fsanitize=${SANITIZER_NAME}` in lower case. +function(add_sanitizer_support) + unset(SANITIZER_COMPILE_OPTIONS_SELECTED) + unset(SANITIZER_SELECTED_LINK_OPTIONS) - if(AFL) - append_quoteless(AFL_USE_TSAN=1 CMAKE_C_COMPILER_LAUNCHER - CMAKE_CXX_COMPILER_LAUNCHER) - endif() - else() - message( - FATAL_ERROR "Thread sanitizer not available for ${CMAKE_CXX_COMPILER}" - ) - endif() + # iterate selected sanitizers, check availability of each + foreach(SELECTED_SANITIZER ${ARGN}) + string(TOUPPER ${SELECTED_SANITIZER} UPPER_SANITIZER_NAME) + string(TOLOWER ${SELECTED_SANITIZER} LOWER_SANITIZER_NAME) + + # if the sanitizer is not yet known/checked, check it now + if(NOT DEFINED SANITIZER_${UPPER_SANITIZER_NAME}_AVAILABLE) + set_sanitizer_options(${LOWER_SANITIZER_NAME}) endif() - if(USE_SANITIZER MATCHES "([Ll]eak)") - message(STATUS "Testing with Leak sanitizer") - set(SANITIZER_LEAK_FLAG "-fsanitize=leak") - test_san_flags(SANITIZER_LEAK_AVAILABLE ${SANITIZER_LEAK_FLAG}) - if(SANITIZER_LEAK_AVAILABLE) - message(STATUS " Building with Leak sanitizer") - append("${SANITIZER_LEAK_FLAG}" SANITIZER_SELECTED_FLAGS) + if(SANITIZER_${UPPER_SANITIZER_NAME}_AVAILABLE) + # sanitizer is available, add the flags to the selection + set(SANITIZER_COMPILE_OPTIONS_SELECTED + ${SANITIZER_COMPILE_OPTIONS_SELECTED} + -fsanitize=${SANITIZER_${UPPER_SANITIZER_NAME}_SANITIZER} + ${SANITIZER_${UPPER_SANITIZER_NAME}_OPTIONS}) - if(AFL) + set(SANITIZER_SELECTED_LINK_OPTIONS + ${SANITIZER_SELECTED_LINK_OPTIONS} + -fsanitize=${SANITIZER_${UPPER_SANITIZER_NAME}_SANITIZER}) + + # special for AFL + if(AFL) + if(${SANITIZER_${UPPER_SANITIZER_NAME}_SANITIZER} MATCHES "address") + append_quoteless(AFL_USE_ASAN=1 CMAKE_C_COMPILER_LAUNCHER + CMAKE_CXX_COMPILER_LAUNCHER) + elseif(${SANITIZER_${UPPER_SANITIZER_NAME}_SANITIZER} MATCHES "leak") append_quoteless(AFL_USE_LSAN=1 CMAKE_C_COMPILER_LAUNCHER CMAKE_CXX_COMPILER_LAUNCHER) - endif() - else() - message( - FATAL_ERROR "Thread sanitizer not available for ${CMAKE_CXX_COMPILER}" - ) - endif() - endif() - - if(USE_SANITIZER MATCHES "([Cc][Ff][Ii])") - message(STATUS "Testing with Control Flow Integrity(CFI) sanitizer") - set(SANITIZER_CFI_FLAG "-fsanitize=cfi") - test_san_flags(SANITIZER_CFI_AVAILABLE ${SANITIZER_CFI_FLAG}) - if(SANITIZER_CFI_AVAILABLE) - message(STATUS " Building with Control Flow Integrity(CFI) sanitizer") - append("${SANITIZER_LEAK_FLAG}" SANITIZER_SELECTED_FLAGS) - - if(AFL) + elseif(${SANITIZER_${UPPER_SANITIZER_NAME}_SANITIZER} MATCHES "memory") + append_quoteless(AFL_USE_MSAN=1 CMAKE_C_COMPILER_LAUNCHER + CMAKE_CXX_COMPILER_LAUNCHER) + elseif(${SANITIZER_${UPPER_SANITIZER_NAME}_SANITIZER} MATCHES + "undefined") + append_quoteless(AFL_USE_UBSAN=1 CMAKE_C_COMPILER_LAUNCHER + CMAKE_CXX_COMPILER_LAUNCHER) + elseif(${SANITIZER_${UPPER_SANITIZER_NAME}_SANITIZER} MATCHES "cfi") append_quoteless(AFL_USE_CFISAN=1 CMAKE_C_COMPILER_LAUNCHER CMAKE_CXX_COMPILER_LAUNCHER) endif() - else() - message( - FATAL_ERROR - "Control Flow Integrity(CFI) sanitizer not available for ${CMAKE_CXX_COMPILER}" - ) endif() - endif() - - message(STATUS "Sanitizer flags: ${SANITIZER_SELECTED_FLAGS}") - test_san_flags(SANITIZER_SELECTED_COMPATIBLE ${SANITIZER_SELECTED_FLAGS}) - if(SANITIZER_SELECTED_COMPATIBLE) - message(STATUS " Building with ${SANITIZER_SELECTED_FLAGS}") - append("${SANITIZER_SELECTED_FLAGS}" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) else() message( - FATAL_ERROR - " Sanitizer flags ${SANITIZER_SELECTED_FLAGS} are not compatible.") + SEND_ERROR "'${LOWER_SANITIZER_NAME}' sanitizer set not available") endif() - elseif(MSVC) - if(USE_SANITIZER MATCHES "([Aa]ddress)") - message(STATUS "Building with Address sanitizer") - append("-fsanitize=address" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + endforeach() - if(AFL) - append_quoteless(AFL_USE_ASAN=1 CMAKE_C_COMPILER_LAUNCHER - CMAKE_CXX_COMPILER_LAUNCHER) - endif() - else() - message( - FATAL_ERROR - "This sanitizer not yet supported in the MSVC environment: ${USE_SANITIZER}" - ) - endif() + # check if all selected sanitizer options/flags are compatible together + if(NOT DEFINED SANITIZER_COMPILE_OPTIONS_GLOBAL_CACHE + OR NOT SANITIZER_COMPILE_OPTIONS_SELECTED STREQUAL + SANITIZER_COMPILE_OPTIONS_GLOBAL_CACHE) + # set of flags needs to be tested for compatability + unset(SANITIZER_SELECTED_OPTIONS_AVAILABLE CACHE) + test_san_flags( + SANITIZER_SELECTED_OPTIONS_AVAILABLE SANITIZER_SELECTED_LINK_OPTIONS + ${SANITIZER_COMPILE_OPTIONS_SELECTED}) + + # whatever the result, cache it to reduce repeating test + set(SANITIZER_COMPILE_OPTIONS_GLOBAL_CACHE + ${SANITIZER_COMPILE_OPTIONS_SELECTED} + CACHE INTERNAL "") + endif() + + if(SANITIZER_SELECTED_OPTIONS_AVAILABLE) + # sanitizer selection is compatible, apply it + add_compile_options(${SANITIZER_COMPILE_OPTIONS_SELECTED}) + add_link_options(${SANITIZER_SELECTED_LINK_OPTIONS}) else() - message(FATAL_ERROR "USE_SANITIZER is not supported on this platform.") + message(FATAL_ERROR "Selected sanitizer options not compatible: ${ARGN}") + endif() +endfunction() + +if(SANITIZER_ENABLE_LEGACY_SUPPORT OR USE_SANITIZER) + set(SANITIZER_LEGACY_SUPPORT ON) + + set_sanitizer_options(address DEFAULT -fno-omit-frame-pointer) + set_sanitizer_options(leak DEFAULT -fno-omit-frame-pointer) + set_sanitizer_options(memory DEFAULT -fno-omit-frame-pointer) + set_sanitizer_options(memorywithorigins DEFAULT SANITIZER memory + -fno-omit-frame-pointer -fsanitize-memory-track-origins) + set_sanitizer_options(undefined DEFAULT -fno-omit-frame-pointer) + set_sanitizer_options(thread DEFAULT -fno-omit-frame-pointer) + set_sanitizer_options(cfi DEFAULT -fno-omit-frame-pointer) + + set(USE_SANITIZER + "" + CACHE + STRING + "(DEPRECATED) Compile with sanitizers. Available sanitizers are: ${SANITIZERS_AVAILABLE_LIST}" + ) + + if(USE_SANITIZER) + add_sanitizer_support(${USE_SANITIZER}) endif() + unset(SANITIZER_LEGACY_SUPPORT) endif()