diff --git a/docker/check-sanitizers.sh b/docker/check-sanitizers.sh deleted file mode 100755 index 38ccaed560..0000000000 --- a/docker/check-sanitizers.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash - -# Sanity-check that the sanitizer runtimes shipped with g++/clang++ work -# end-to-end against the system loader: compile each example with both -# compilers, run it, and confirm the expected diagnostic is emitted. - -set -eo pipefail - -cpp_files_dir="${1:?usage: $0 }" - -case "$(uname -m)" in - x86_64) loader=/lib64/ld-linux-x86-64.so.2 ;; - aarch64) loader=/lib/ld-linux-aarch64.so.1 ;; - *) - echo "Unsupported arch: $(uname -m)" >&2 - exit 1 - ;; -esac - -declare -A sanitize=( - [asan]="-fsanitize=address" - [tsan]="-fsanitize=thread" - [ubsan]="-fsanitize=undefined" -) -declare -A expect=( - [asan]="heap-use-after-free" - [tsan]="data race" - [ubsan]="signed integer overflow" -) - -for compiler in g++ clang++; do - for name in asan tsan ubsan; do - bin="/tmp/${name}-${compiler}" - echo "=== Build ${name} with ${compiler} ===" - "$compiler" -std=c++20 -O1 -g ${sanitize[$name]} \ - -Wl,--dynamic-linker=$loader \ - "${cpp_files_dir}/${name}.cpp" -o "$bin" - echo "=== Run ${name}-${compiler} ===" - output=$("$bin" 2>&1) || true - echo "$output" - echo "$output" | grep -q "${expect[$name]}" || - { - echo "expected '${expect[$name]}' from $bin" - exit 1 - } - rm -f "$bin" - done -done diff --git a/docker/loader-path.sh b/docker/loader-path.sh new file mode 100755 index 0000000000..b8b9f0de51 --- /dev/null +++ b/docker/loader-path.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +case "$(uname -m)" in + x86_64) LOADER=/lib64/ld-linux-x86-64.so.2 ;; + aarch64) LOADER=/lib/ld-linux-aarch64.so.1 ;; + *) + echo "Unsupported arch: $(uname -m)" >&2 + exit 1 + ;; +esac + +echo "${LOADER}" diff --git a/docker/nix.Dockerfile b/docker/nix.Dockerfile index 690f0b76bd..3c5dbcb734 100644 --- a/docker/nix.Dockerfile +++ b/docker/nix.Dockerfile @@ -27,7 +27,9 @@ RUN mkdir /tmp/nix-store-closure && \ cp -R $(nix-store -qR result/) /tmp/nix-store-closure # Final image -FROM ${BASE_IMAGE} +FROM ${BASE_IMAGE} AS final + +ARG BASE_IMAGE # bash is not located at /bin/bash in nixos/nix, so we need to create a symlink to it. RUN if [ -d /nix ]; then \ @@ -43,25 +45,23 @@ ENTRYPOINT ["/bin/bash"] COPY --from=builder /tmp/nix-store-closure /nix/store COPY --from=builder /tmp/build/result /nix/ci-env -ENV PATH="/nix/ci-env/bin:$PATH" +ENV PATH="/nix/ci-env/bin:${PATH}" # Externally-built dynamically-linked ELF binaries hard-code the loader path -# (e.g. /lib64/ld-linux-x86-64.so.2) in their PT_INTERP header. Copy the -# loader from the Nix store to that path when the base image doesn't already -# provide one (i.e. on nixos/nix). +# (e.g. /lib64/ld-linux-x86-64.so.2) in their PT_INTERP header. Install it +# from the Nix store when the base image doesn't already provide one. +COPY docker/loader-path.sh /tmp/loader-path.sh + RUN <&2; exit 1 ;; -esac -if [ ! -e "$target" ]; then +target="$(/tmp/loader-path.sh)" + +if [ ! -e "${target}" ]; then # Use the loader from the same glibc that gcc links libc against, so # ld-linux and libc/libpthread share GLIBC_PRIVATE symbols at runtime. - src="$(dirname "$(gcc -print-file-name=libc.so.6)")/$(basename "$target")" - [ -e "$src" ] || { echo "ld-linux not found at $src" >&2; exit 1; } - mkdir -p "$(dirname "$target")" - cp "$src" "$target" + src="$(dirname "$(gcc -print-file-name=libc.so.6)")/$(basename "${target}")" + [ -e "${src}" ] || { echo "ld-linux not found at ${src}" >&2; exit 1; } + mkdir -p "$(dirname "${target}")" + cp "${src}" "${target}" fi EOF @@ -87,9 +87,16 @@ run-clang-tidy --help vim --version EOF -# Sanity-check that the sanitizer runtimes shipped with g++/clang++ work -# end-to-end against the system loader. -COPY docker/cpp_files/ /tmp/cpp_files/ -COPY docker/check-sanitizers.sh /tmp/check-sanitizers.sh +# Sanity-check that the sanitizer runtimes shipped with g++/clang++ are able to build binaries +COPY docker/test_files/cpp_sources/ /tmp/cpp_sources/ +COPY docker/test_files/compile-cpp-sources.sh /tmp/compile-cpp-sources.sh +RUN /tmp/compile-cpp-sources.sh /tmp/cpp_sources /tmp/bins -RUN grep -qi ubuntu /etc/os-release 2>/dev/null && /tmp/check-sanitizers.sh /tmp/cpp_files || true +# Sanity-check that the built binaries are able to run. +# We only support running the test binaries on Ubuntu and NixOS right now (will be fixed in the future) +# +# When build and test images will be separate, we will be to run on vanilla images. +COPY docker/test_files/run-test-binaries.sh /tmp/run-test-binaries.sh +RUN if echo "${BASE_IMAGE}" | grep -qiE '(ubuntu|nixos)'; then \ + /tmp/run-test-binaries.sh /tmp/bins; \ + fi diff --git a/docker/test_files/compile-cpp-sources.sh b/docker/test_files/compile-cpp-sources.sh new file mode 100755 index 0000000000..b4edaee2f6 --- /dev/null +++ b/docker/test_files/compile-cpp-sources.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Compile all C++ test binaries during the Docker image build. +# Each binary has the target system's ELF PT_INTERP (dynamic-linker path) +# baked in so it can run on the (potentially minimal) final BASE_IMAGE. + +set -eo pipefail + +src_dir="${1:?usage: $0 }" +dst_dir="${2:?usage: $0 }" + +loader="$(/tmp/loader-path.sh)" + +mkdir -p "${dst_dir}" + +function compile() { + local compiler="${1}" + local name="${2}" + local san_flag="${3:-}" + + local src="${src_dir}/${name}.cpp" + local binary="${dst_dir}/${name}-${compiler}" + + echo "=== Compile ${name} with ${compiler} ===" + cmd="${compiler} -std=c++23 -O1 -g \ + -pthread \ + -Wl,--dynamic-linker=${loader} \ + ${san_flag} \ + ${src} -o ${binary}" + echo "Command: ${cmd}" + eval "${cmd}" +} + +declare -A sanitize=( + [regular]="" + + [asan]="-fsanitize=address" + [tsan]="-fsanitize=thread" + [ubsan]="-fsanitize=undefined -fno-sanitize-recover=all" +) + +for name in regular asan tsan ubsan; do + san_flag="${sanitize[${name}]}" + for compiler in g++ clang++; do + compile "${compiler}" "${name}" "${san_flag}" + done +done + +echo "=== All binaries compiled ===" + +ls -la "${dst_dir}" diff --git a/docker/cpp_files/asan.cpp b/docker/test_files/cpp_sources/asan.cpp similarity index 100% rename from docker/cpp_files/asan.cpp rename to docker/test_files/cpp_sources/asan.cpp diff --git a/docker/test_files/cpp_sources/regular.cpp b/docker/test_files/cpp_sources/regular.cpp new file mode 100644 index 0000000000..637dafa1fd --- /dev/null +++ b/docker/test_files/cpp_sources/regular.cpp @@ -0,0 +1,28 @@ +#include +#include +#include +#include + +static std::mutex gMutex; + +void +worker(int id) +{ + std::lock_guard lock(gMutex); + std::cout << "Hello from thread " << id << "\n"; +} + +int +main() +{ + constexpr int kNumThreads = 10; + std::vector threads; + threads.reserve(kNumThreads); + for (int i = 0; i < kNumThreads; ++i) + threads.emplace_back(worker, i); + for (auto& t : threads) + t.join(); + + std::cout << "Hello from main thread\n"; + return 0; +} diff --git a/docker/cpp_files/tsan.cpp b/docker/test_files/cpp_sources/tsan.cpp similarity index 100% rename from docker/cpp_files/tsan.cpp rename to docker/test_files/cpp_sources/tsan.cpp diff --git a/docker/cpp_files/ubsan.cpp b/docker/test_files/cpp_sources/ubsan.cpp similarity index 100% rename from docker/cpp_files/ubsan.cpp rename to docker/test_files/cpp_sources/ubsan.cpp diff --git a/docker/test_files/run-test-binaries.sh b/docker/test_files/run-test-binaries.sh new file mode 100755 index 0000000000..6e8f0a931c --- /dev/null +++ b/docker/test_files/run-test-binaries.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Run pre-compiled sanitizer binaries and confirm each emits its expected diagnostic. +# Binaries must already exist in with the layout: +# -g++ and -clang++ for name in {regular,asan,tsan,ubsan} + +set -eo pipefail + +bins_dir="${1:?usage: $0 }" + +# Run a binary and verify its exit code and output. +# Usage: run +function run() { + local binary="${1}" + local expected_output="${2}" + local expected_rc="${3}" + + local out_file + out_file="$(mktemp)" + + echo "=== Run ${binary} ===" + local rc=0 + "${binary}" >"${out_file}" 2>&1 || rc=$? + + cat "${out_file}" + + if [ "${expected_rc}" = "nonzero" ]; then + if [ "${rc}" -eq 0 ]; then + echo "ERROR: expected non-zero exit code from ${binary}, got ${rc}" >&2 + exit 1 + fi + elif [ "${rc}" -ne "${expected_rc}" ]; then + echo "ERROR: expected exit code ${expected_rc} from ${binary}, got ${rc}" >&2 + exit 1 + fi + + grep -q "${expected_output}" "${out_file}" || + { + echo "ERROR: expected '${expected_output}' from ${binary}" >&2 + exit 1 + } + echo "OK: '${expected_output}' detected" +} + +declare -A expect=( + [regular]="Hello from main thread" + + [asan]="heap-use-after-free" + [tsan]="data race" + [ubsan]="signed integer overflow" +) + +for compiler in g++ clang++; do + for name in regular asan tsan ubsan; do + binary="${bins_dir}/${name}-${compiler}" + if [ "${name}" = "regular" ]; then + expected_rc=0 + else + expected_rc=nonzero + fi + run "${binary}" "${expect[$name]}" "${expected_rc}" + done +done