Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CI: sanitizers build #251

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/sanitize.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Sanitize with ASAN, MSAN, TSAN, UBSAN

on:
pull_request:
branches:
- main
pull_request_review:
types: [submitted]

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
build_dependencies:
name: Build deps [ubuntu]
runs-on: ubuntu-latest
# if: github.event.review.state == 'APPROVED'
steps:
- uses: actions/checkout@v4

run_asan:
needs: build_dependencies
uses: ./.github/workflows/sanitizer-check.yaml
with:
sanitizer-name: 'asan'

run_msan:
needs: build_dependencies
uses: ./.github/workflows/sanitizer-check.yaml
with:
sanitizer-name: 'msan'

run_tsan:
needs: build_dependencies
uses: ./.github/workflows/sanitizer-check.yaml
with:
sanitizer-name: 'tsan'

run_ubsan:
needs: build_dependencies
uses: ./.github/workflows/sanitizer-check.yaml
with:
sanitizer-name: 'ubsan'
41 changes: 41 additions & 0 deletions .github/workflows/sanitizer-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Build and run Sanitizer

on:
workflow_call:
inputs:
sanitizer-name:
description: Sanitizer name (asan/msan/tsan/ubsan)
type: string
required: true

jobs:
build_and_run_sanitizer:
name: Build and run Sanitizer
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Debug
run: cd ${{ github.workspace }} && find .
- name: Set up dependencies
run: |
sudo apt-get update
sudo apt-get install -qy build-essential \
gdb \
curl \
python3.10 \
python3-pip \
cmake \
ninja-build \
pkg-config \
libbenchmark-dev \
libgmock-dev \
libz-dev
- name: Fetch & Build non packaged dependencies
run: |
mkdir -p deps
cd deps
../.github/workflows/docker/build_deps.sh
- name: Build NTF and dependencies with sanitizer instrumentation
run: ${{ github.workspace }}/.github/workflows/sanitizers/build_sanitizer.sh ${{ inputs.sanitizer-name }}
- name: Run unit tests under sanitizer
run: ${{ github.workspace }}/run-unittests.sh
226 changes: 226 additions & 0 deletions .github/workflows/sanitizers/build_sanitizer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
#!/usr/bin/env bash

# This script can be used to build ntf-core, and all of its transitive
# dependencies (up to and including the standard library) using:
# - Clang
# - LLVM libc++ standard library
# - A CMake toolchain file specific for instrumented build
# It is currently used to build instrumented ntf-core binaries for CI for all
# Clang sanitizers (ASAN, TSAN, MSAN, UBSAN).
#
# It performs the following:
# 1) Install clang compiler.
# 2) Download llvm-project required for libc++ instrumentation.
# 3) Download external dependencies required for instrumentation.
# 4) Build libc++ with the instrumentation specified by <LLVM Sanitizer Name>.
# 5) Build sanitizer-instrumented dependencies for NTF.
# 6) Build sanitizer-instrumented NTF unit tests.
# 7) Generate scripts to run unit tests:
# ./cmake.bld/Linux/run-unittests.sh
# This script is used as-is by CI to run unit tests under sanitizer.

set -eux

# :: Required arguments :::::::::::::::::::::::::::::::::::::::::::::::::::::::
if [ -z "${1}" ]; then
echo 'Error: Missing sanitizer name.' >&2
echo ' (Usage: build_sanitizer.sh <sanitizer-name>)' >&2
exit 1
fi

SANITIZER_NAME="${1}"

# Github's 'ubuntu-22.04' image contains a lot of preinstalled tools,
# see https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2204-Readme.md.
# Uninstall uneeded tools which cause of versions clash.
sudo apt-get purge \
llvm-14 \
clang-14 \
gcc-9 \
gcc-10 \
gcc-11 \
gcc-12

# Install prerequisites
sudo apt-get update && sudo apt-get install -qy \
lsb-release \
wget \
software-properties-common \
gnupg \
git \
curl \
jq \
ninja-build \
pkg-config

# Install prerequisites for LLVM: latest cmake version, Ubuntu apt repository contains cmake version 3.22.1
wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null \
| gpg --dearmor - \
| sudo tee /etc/apt/trusted.gpg.d/kitware.gpg >/dev/null
sudo apt-add-repository -y "deb https://apt.kitware.com/ubuntu/ $(lsb_release -cs) main"
sudo apt-get install -qy cmake

# Install LLVM
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
LLVM_VERSION=17
sudo ./llvm.sh ${LLVM_VERSION} all

# Create version-agnostic pointers to required LLVM binaries.
sudo ln -sf /usr/bin/clang-${LLVM_VERSION} /usr/bin/clang
sudo ln -sf /usr/bin/clang++-${LLVM_VERSION} /usr/bin/clang++
sudo ln -sf /usr/bin/llvm-symbolizer-${LLVM_VERSION} /usr/bin/llvm-symbolizer

# Set some initial constants
PARALLELISM=8

DIR_ROOT="${PWD}"
DIR_SCRIPTS="${DIR_ROOT}/.github/workflows/sanitizers"
DIR_EXTERNAL="${DIR_ROOT}/deps"
DIR_SRCS_EXT="${DIR_EXTERNAL}/srcs"
DIR_BUILD_EXT="${DIR_EXTERNAL}/cmake.bld"

DIR_SRC_NTF="${DIR_ROOT}"
DIR_BUILD_NTF="${DIR_SRC_NTF}/cmake.bld/Linux"

# Parse sanitizers config
cfgquery() {
jq "${1}" "${DIR_SCRIPTS}/sanitizers.json" --raw-output
}
LLVM_SANITIZER_NAME="$(cfgquery ."${SANITIZER_NAME}".llvm_sanitizer_name)"
# Check if llvm specific cmake options are present for the given sanitizer
LLVM_SPECIFIC_CMAKE_OPTIONS="$(cfgquery ."${SANITIZER_NAME}".llvm_specific_cmake_options)"
if [[ "$LLVM_SPECIFIC_CMAKE_OPTIONS" == null ]]; then LLVM_SPECIFIC_CMAKE_OPTIONS=""; fi

checkoutGitRepo() {
local repo=$1
local ref=$2
local repoDir=$3
echo "Checking out ${repo} at ${ref}"

local repoPath="${DIR_SRCS_EXT}/${repoDir}"

git clone -b "${ref}" "${repo}" \
--depth 1 --single-branch --no-tags -c advice.detachedHead=false "${repoPath}"
}
github_url() { echo "https://github.com/$1.git"; }

# Download external dependencies
mkdir -p "${DIR_SRCS_EXT}"

# Download LLVM
LLVM_TAG="llvmorg-17.0.6"
curl -SL "https://github.com/llvm/llvm-project/archive/refs/tags/${LLVM_TAG}.tar.gz" \
| tar -xzC "${DIR_SRCS_EXT}"
mv "${DIR_SRCS_EXT}/llvm-project-${LLVM_TAG}" "${DIR_SRCS_EXT}/llvm-project"

# Download google-benchmark
GOOGLE_BENCHMARK_TAG="v1.8.4"
checkoutGitRepo "$(github_url google/benchmark)" "${GOOGLE_BENCHMARK_TAG}" "google-benchmark"

# Download googletest
GOOGLETEST_TAG="v1.14.0"
checkoutGitRepo "$(github_url google/googletest)" "${GOOGLETEST_TAG}" "googletest"

# Download zlib
ZLIB_TAG="v1.3.1"
checkoutGitRepo "$(github_url madler/zlib)" "${ZLIB_TAG}" "zlib"

# Build libc++ with required instrumentation
#
# The extent to which all dependencies to be compiled with sanitizer-support
# varies by sanitizer. MemorySanitizer is especially unforgiving: Failing to
# link against an instrumented standard library will yield many false
# positives. Concensus is that compiling libc++ with `-fsanitize=memory` is a
# significantly easier endeavor than doing the same with libstdc++ (the gcc
# standard library).
#
# We therefore opt to use libc++ here, just to ensure maximum flexibility. We
# follow build instructions from https://libcxx.llvm.org/BuildingLibcxx.html
LIBCXX_SRC_PATH="${DIR_SRCS_EXT}/llvm-project/runtimes"
LIBCXX_BUILD_PATH="${LIBCXX_SRC_PATH}/cmake.bld"

cmake -B "${LIBCXX_BUILD_PATH}" \
-S "${LIBCXX_SRC_PATH}" \
-DCMAKE_BUILD_TYPE="Debug" \
-DCMAKE_C_COMPILER="clang" \
-DCMAKE_CXX_COMPILER="clang++" \
-DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi;libunwind" \
-DLLVM_USE_SANITIZER="${LLVM_SANITIZER_NAME}" \
"${LLVM_SPECIFIC_CMAKE_OPTIONS}"

cmake --build "${LIBCXX_BUILD_PATH}" -j${PARALLELISM} --target cxx cxxabi unwind generate-cxx-headers

# Variables read by our custom CMake toolchain used to build everything else.
export LIBCXX_BUILD_PATH="${LIBCXX_BUILD_PATH}"
export DIR_SRC_NTF="${DIR_SRC_NTF}"
export DIR_SCRIPTS="${DIR_SCRIPTS}"

TOOLCHAIN_PATH="${DIR_SCRIPTS}/clang-libcxx-sanitizer.cmake"
export SANITIZER_NAME="${SANITIZER_NAME}"
export CC="clang"
export CXX="clang++"
export CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES="/usr/include;/usr/include/clang/${LLVM_VERSION}/include"
export BBS_BUILD_SYSTEM="ON"
PATH="$PATH:$(realpath "${DIR_SRCS_EXT}"/bde-tools/bin)"
export PATH

# Build BDE + NTF
pushd "${DIR_SRCS_EXT}/bde"
eval "$(bbs_build_env -u dbg_64_safe_cpp20 -b "${DIR_BUILD_EXT}/bde")"
bbs_build configure --toolchain "${TOOLCHAIN_PATH}"
bbs_build build -j${PARALLELISM}
bbs_build --install=/opt/bb --prefix=/ install
popd

# TODO The deprecated flag "-fcoroutines-ts" has been removed in clang
# 17.0.1, but NTF is still using it. We manually change this flag until
# the fix in issue 175307231 is resolved.
sed -i 's/fcoroutines-ts/fcoroutines/g' 'repository.cmake'

./configure --keep \
--output "${DIR_BUILD_NTF}" \
--without-warnings-as-errors \
--without-usage-examples \
--ufid 'dbg_64_safe_cpp20' \
--toolchain "${TOOLCHAIN_PATH}" \
--from-continuous-integration
make -j${PARALLELISM} build_test

# Create testing script
envcfgquery() {
# Parses the '<build-name>.environment' object from 'sanitizers.json',
# and outputs a string of whitespace-separated 'VAR=VAL' pairs intended to
# be used to set the environment for a command.
# e.g. 'asan' -> 'ASAN_OPTIONS="foo=bar:baz=baf" LSAN_OPTIONS="abc=fgh"'
#
cfgquery " \
.${1}.environment | \
to_entries | \
map(\"\(.key)=\\\"\(.value | \
to_entries | \
map(\"\(.key)=\(.value)\") | \
join(\":\"))\\\"\") | \
join(\" \")" |
sed "s|%%SRC%%|$(realpath "${DIR_SRC_NTF}")|g" |
sed "s|%%ROOT%%|$(realpath "${DIR_ROOT}")|g"
}

mkscript() {
local cmd=${1}
local outfile=${2}

echo '#!/usr/bin/env bash' > "${outfile}"
echo "${cmd}" >> "${outfile}"
chmod +x "${outfile}"
}

SANITIZER_ENV="$(envcfgquery "${SANITIZER_NAME}")"

# 'run-env.sh' runs a command with environment required of the sanitizer.
mkscript "${SANITIZER_ENV} \${@}" "${DIR_ROOT}/run-env.sh"

# 'run-unittests.sh' runs all instrumented unit-tests.
CMD="cd $(realpath "${DIR_ROOT}") "
CMD+="&& ./run-env.sh make test "
mkscript "${CMD}" "${DIR_ROOT}/run-unittests.sh"
Loading
Loading