diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6d99779..754dbc09 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,10 @@ jobs: - uses: actions/checkout@v2 with: submodules: 'true' + - name: Enable ASan+UBSan Sanitizers + if: matrix.build-type == 'Debug' + run: | + echo "SANITIZER_FLAG=-DPEPARSE_USE_SANITIZER=Address,Undefined" >> $GITHUB_ENV - name: build env: CC: ${{ matrix.compiler.CC }} @@ -55,6 +59,7 @@ jobs: -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \ -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} \ -DPEPARSE_ENABLE_TESTING=ON \ + ${SANITIZER_FLAG} \ .. cmake --build . - name: test diff --git a/README.md b/README.md index 336f97ac..b93a568a 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,20 @@ You can build the (catch2-based) tests by adding `-DPEPARSE_ENABLE_TESTING=ON` d To run the full test suite with the [Corkami test suite](https://github.com/corkami/pocs/tree/master/PE), you must clone the submodule with `git submodule update --init`. +## Building with Sanitizers + +If you are familiar with C++ sanitizers and any specific development environment requirements for them (compiler, instrumented standard library, etc.), you can choose to compile with any of the following sanitizers: `Address`, `HWAddress`, `Undefined`, `Memory`, `MemoryWithOrigins`, `Leak`, `Address,Undefined`. + +For example, to compile with both `Address` and `Undefined` sanitizers, use the following (recommended for development and testing, and tested in CI): + +```bash +mkdir build-san +cd build-san + +cmake -DCMAKE_BUILD_TYPE=Debug -DPEPARSE_ENABLE_TESTING=ON -DPEPARSE_USE_SANITIZER=Address,Undefined .. +cmake --build . +``` + ## Using the library Once the library is installed, linking to it is easy! Add the following lines in your CMake project: diff --git a/cmake/compilation_flags.cmake b/cmake/compilation_flags.cmake index b8562095..395f1b5c 100644 --- a/cmake/compilation_flags.cmake +++ b/cmake/compilation_flags.cmake @@ -2,6 +2,9 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +include("cmake/sanitizers.cmake") +process_sanitizer(PEPARSE) + if (MSVC) list(APPEND DEFAULT_CXX_FLAGS /W4 /analyze) diff --git a/cmake/sanitizers.cmake b/cmake/sanitizers.cmake new file mode 100644 index 00000000..a86e336e --- /dev/null +++ b/cmake/sanitizers.cmake @@ -0,0 +1,173 @@ +# Enable C/C++ sanitizers in your CMake project by `include`ing this file +# somewhere in your CMakeLists.txt. +# +# Example: +# +# include("cmake/sanitizers.cmake") +# process_sanitizer(MY_PROJECT) +# +# Example CMake configuration usage for `MY_PROJECT`: +# +# cmake -Bbuild-asan-ubsan -H. -DMY_PROJECT_USE_SANITIZER=Address,Undefined +# +# where "MY_PROJECT" can by any arbitrary text that will be prepended to some +# expected CMake variables--see below. +# +# This file expects the following variables to be set during CMake +# configuration, where the prefix is set by the caller of `process_sanitizer`: +# +# - *_USE_SANITIZER +# - A string value that is one of the following: +# - Address +# - HWAddress +# - Memory +# - MemoryWithOrigins +# - Undefined +# - Thread +# - DataFlow +# - Leak +# - Address,Undefined +# +# - *_OPTIMIZE_SANITIZED_BUILDS +# - A boolean value to set whether a higher optimization is used in debug +# builds +# +# - *_BLACKLIST_FILE +# - A filepath to a sanitizer blacklist file. + + +function(append value) + foreach(variable ${ARGN}) + set(${variable} + "${${variable}} ${value}" + PARENT_SCOPE) + endforeach(variable) +endfunction() + + +macro(append_common_sanitizer_flags prefix) + if (NOT MSVC) + # Append -fno-omit-frame-pointer and turn on debug info to get better + # stack traces. + append("-fno-omit-frame-pointer" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + append("-fno-optimize-sibling-calls" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + append("-gline-tables-only" CMAKE_C_FLAGS_RELEASE CMAKE_CXX_FLAGS_RELEASE + CMAKE_C_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_MINSIZE_REL) + # Use -O1 even in debug mode, otherwise sanitizers slowdown is too large. + if (${prefix}_OPTIMIZE_SANITIZED_BUILDS) + message(STATUS "Optimizing sanitized Debug build") + append("-O1" CMAKE_C_FLAGS_DEBUG CMAKE_CXX_FLAGS_DEBUG) + endif() + else() + # Keep frame pointers around. + append("/Oy-" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + # Always ask the linker to produce symbols with asan. + append("/Zi" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + # Workaround for incompatible (warning-producing) default CMake flag + # https://docs.microsoft.com/en-us/cpp/sanitizers/asan-known-issues + # https://gitlab.kitware.com/cmake/cmake/-/issues/19084 + string(REPLACE "/RTC1" "" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") + append("/debug" CMAKE_EXE_LINKER_FLAGS CMAKE_MODULE_LINKER_FLAGS CMAKE_SHARED_LINKER_FLAGS) + endif() +endmacro() + + +# Main logic +macro(process_sanitizer prefix) + + # Add options for the project to use sanitizers + option(${prefix}_USE_SANITIZER "Enable building with sanitizer support. Options are: Address, HWAddress, Memory, MemoryWithOrigins, Undefined, Thread, DataFlow, Leak, 'Address,Undefined'" false) + if (UNIX) + option(${prefix}_OPTIMIZE_SANITIZED_BUILDS "Optimize builds that use sanitization" false) + option(${prefix}_BLACKLIST_FILE "Path to blacklist file for sanitizers" "") + option(${prefix}_USE_SANITIZE_COVERAGE "Set for libFuzzer-required instrumentation, no linking." false) + endif() + + if (${prefix}_USE_SANITIZER) + if(UNIX) + + if(${prefix}_USE_SANITIZER STREQUAL "Address") + message(STATUS "Building with Address sanitizer") + append_common_sanitizer_flags(${prefix}) + append("-fsanitize=address" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + + elseif(${prefix}_USE_SANITIZER STREQUAL "HWAddress") + message(STATUS "Building with Address sanitizer") + append_common_sanitizer_flags(${prefix}) + append("-fsanitize=hwaddress" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + + elseif(${prefix}_USE_SANITIZER MATCHES "Memory(WithOrigins)?") + append_common_sanitizer_flags(${prefix}) + append("-fsanitize=memory" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + if(${prefix}_USE_SANITIZER STREQUAL "MemoryWithOrigins") + message(STATUS "Building with MemoryWithOrigins sanitizer") + append("-fsanitize-memory-track-origins" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + else() + message(STATUS "Building with Memory sanitizer") + endif() + + elseif(${prefix}_USE_SANITIZER STREQUAL "Undefined") + message(STATUS "Building with Undefined sanitizer") + append_common_sanitizer_flags(${prefix}) + append("-fsanitize=undefined" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + # Execution error on undefined detection. Could be optional to add this + append("-fno-sanitize-recover=all" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + + elseif(${prefix}_USE_SANITIZER STREQUAL "Thread") + message(STATUS "Building with Thread sanitizer") + append_common_sanitizer_flags(${prefix}) + append("-fsanitize=thread" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + + elseif(${prefix}_USE_SANITIZER STREQUAL "DataFlow") + message(STATUS "Building with DataFlow sanitizer") + append("-fsanitize=dataflow" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + + elseif(${prefix}_USE_SANITIZER STREQUAL "Leak") + message(STATUS "Building with Leak sanitizer") + append_common_sanitizer_flags(${prefix}) + append("-fsanitize=leak" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + + elseif(${prefix}_USE_SANITIZER STREQUAL "Address,Undefined" + OR ${prefix}_USE_SANITIZER STREQUAL "Undefined,Address") + message(STATUS "Building with Address, Undefined sanitizers") + append_common_sanitizer_flags(${prefix}) + append("-fsanitize=address,undefined" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + # Execution error on undefined detection. Could be optional to add this + append("-fno-sanitize-recover=all" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + + else() + message( + FATAL_ERROR "Unsupported value of ${prefix}_USE_SANITIZER: '${${prefix}_USE_SANITIZER}'") + endif() + elseif(MSVC) + if(${prefix}_USE_SANITIZER STREQUAL "Address") + message(STATUS "Building with Address sanitizer") + append_common_sanitizer_flags(${prefix}) + append("/fsanitize=address" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + else() + message(FATAL_ERROR "This sanitizer is not yet supported in the MSVC environment: '${${prefix}_USE_SANITIZER}'") + endif() + else() + message(FATAL_ERROR "${prefix}_USE_SANITIZER is not supported on this platform.") + endif() + + # If specified, use a blacklist file + if (EXISTS "${${prefix}_BLACKLIST_FILE}") + message(STATUS "Using sanitizer blacklist file: ${${prefix}_BLACKLIST_FILE}") + append("-fsanitize-blacklist=${${prefix}_BLACKLIST_FILE}" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + endif() + + # Set a once non-default option for more detection + if (${prefix}_USE_SANITIZER MATCHES "(Undefined,)?Address(,Undefined)?") + if (UNIX) + append("-fsanitize-address-use-after-scope" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + endif() + endif() + + # Set for libFuzzer-required instrumentation, no linking. + if (${prefix}_USE_SANITIZE_COVERAGE) + message(STATUS "Setting up sanitizer for coverage support with 'fuzzer-no-link'") + append("-fsanitize=fuzzer-no-link" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + endif() + endif() +endmacro()