diff --git a/.bazelrc b/.bazelrc index 65e2c301e1e..0e52071752f 100644 --- a/.bazelrc +++ b/.bazelrc @@ -248,24 +248,38 @@ build:asan --copt="-DV8_USE_ADDRESS_SANITIZER" build:asan --per_file_copt='external/.*v8@-DADDRESS_SANITIZER,-DLEAK_SANITIZER' # thread sanitizer (https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual) -build:tsan --config=sanitizer-common -build:tsan --copt="-fsanitize=thread" --linkopt="-fsanitize=thread" -# A statically-linked unwinder in each shared library cannot propagate exceptions across DSOs. -build:tsan --linkopt="-l:libunwind.so.8" -build:tsan --platforms=//build/platforms:sanitizer_thread_platform -build:tsan --action_env=TSAN_OPTIONS=halt_on_error=true -build:tsan --test_env=TSAN_OPTIONS=halt_on_error=true +# +# Instruments both C++ and Rust (nightly rustc with -Zsanitizer=thread plus a TSan-built standard +# library, see //build/rust). Supported hosts: Linux x86_64 (`--config=tsan`) and macOS arm64 +# (`--config=tsan-macos`). The two differ only in the Linux-specific unwinder linkopt below -- +# bazelrc has no way to make a single flag OS-conditional, hence the two entry points sharing +# `tsan-common`. +build:tsan-common --config=sanitizer-common +build:tsan-common --copt="-fsanitize=thread" --linkopt="-fsanitize=thread" +build:tsan-common --platforms=//build/platforms:sanitizer_thread_platform +build:tsan-common --action_env=TSAN_OPTIONS=halt_on_error=true +build:tsan-common --test_env=TSAN_OPTIONS=halt_on_error=true # Clang-tidy loads this shared object into an unsanitized process, which cannot resolve TSan hooks. -build:tsan --per_file_copt='tools/clang-tidy@-fno-sanitize=thread' +build:tsan-common --per_file_copt='tools/clang-tidy@-fno-sanitize=thread' # TSan significantly increases test runtime. -build:tsan --test_timeout=30,150,600,2400 +build:tsan-common --test_timeout=30,150,600,2400 # Enable V8's TSan-specific generated code and synchronization annotations. V8's Bazel build does # not currently derive these defines from the compiler's sanitizer flags. -build:tsan --copt="-DV8_IS_TSAN" -build:tsan --per_file_copt='external/.*v8@-DTHREAD_SANITIZER' -build:tsan --extra_toolchains=//build/rust:tsan_toolchain -build:tsan --@rules_rust//:extra_rustc_flag=-Zsanitizer=thread -build:tsan --@rules_rust//:extra_rustc_flag=-Zexternal-clangrt +build:tsan-common --copt="-DV8_IS_TSAN" +build:tsan-common --per_file_copt='external/.*v8@-DTHREAD_SANITIZER' +# One toolchain per host; resolution picks the one matching the (host-parented) tsan platform. +build:tsan-common --extra_toolchains=//build/rust:tsan_toolchain_linux_x86_64,//build/rust:tsan_toolchain_macos_aarch64 +build:tsan-common --@rules_rust//:extra_rustc_flag=-Zsanitizer=thread +build:tsan-common --@rules_rust//:extra_rustc_flag=-Zexternal-clangrt + +# Linux x86_64. +build:tsan --config=tsan-common +# A statically-linked unwinder in each shared library cannot propagate exceptions across DSOs. +build:tsan --linkopt="-l:libunwind.so.8" + +# macOS arm64. ld64 does not accept the GNU-ld `-l:` syntax and has no equivalent need: libunwind is +# part of libSystem and shared by every image. +build:tsan-macos --config=tsan-common # fuzzilli (https://github.com/googleprojectzero/fuzzilli/) build:fuzzilli --config=asan diff --git a/build/deps/rust.MODULE.bazel b/build/deps/rust.MODULE.bazel index 5d71e90d014..94d67b6ced2 100644 --- a/build/deps/rust.MODULE.bazel +++ b/build/deps/rust.MODULE.bazel @@ -33,6 +33,7 @@ use_repo( rust, "rust_toolchains", rust_nightly_linux_x86_64 = "rust_linux_x86_64__x86_64-unknown-linux-gnu__nightly_tools", + rust_nightly_macos_aarch64 = "rust_macos_aarch64__aarch64-apple-darwin__nightly_tools", ) register_toolchains("@rust_toolchains//:all") diff --git a/build/platforms/BUILD.bazel b/build/platforms/BUILD.bazel index 75f17c89d46..93ce5ea6576 100644 --- a/build/platforms/BUILD.bazel +++ b/build/platforms/BUILD.bazel @@ -18,11 +18,10 @@ platform( parents = ["@platforms//host"], ) +# Like the address platform, inherits the host's cpu/os: the TSan Rust toolchains in //build/rust +# are defined per host (Linux x86_64, macOS arm64) and selected by toolchain resolution. platform( name = "sanitizer_thread_platform", - constraint_values = [ - ":sanitizer_thread", - "@platforms//cpu:x86_64", - "@platforms//os:linux", - ], + constraint_values = [":sanitizer_thread"], + parents = ["@platforms//host"], ) diff --git a/build/rust/BUILD.bazel b/build/rust/BUILD.bazel index cd48a74df11..d634af938ee 100644 --- a/build/rust/BUILD.bazel +++ b/build/rust/BUILD.bazel @@ -1,84 +1,50 @@ -load("@rules_rust//rust:toolchain.bzl", "rust_toolchain") -load("//build/deps:gen/build_deps.bzl", "RUST_NIGHTLY_DATE", "RUST_NIGHTLY_VERSION") -load(":build_std.bzl", "compiler_runtime_files", "instrumented_rust_std") +load(":tsan_toolchain.bzl", "tsan_rust_toolchain") exports_files(["build_std.sh"]) -_RUST_TOOLS = "@rust_nightly_linux_x86_64" - -_TARGET_TRIPLE = "x86_64-unknown-linux-gnu" - -compiler_runtime_files( - name = "nightly_rustc_lib", - src = _RUST_TOOLS + "//:rustc_lib", - sanitizer = "tsan", - tags = ["manual"], - target_compatible_with = [ - "@platforms//cpu:x86_64", - "@platforms//os:linux", - ], -) - -instrumented_rust_std( - name = "tsan_rust_std", - rust_src = "@rust_nightly_src", - rust_tools = _RUST_TOOLS, - sanitizer = "thread", - tags = ["manual"], - target_compatible_with = ["//build/platforms:sanitizer_thread"], - target_triple = _TARGET_TRIPLE, - visibility = ["//src/rust/tsan-test:__pkg__"], -) - -rust_toolchain( - name = "tsan_toolchain_impl", - allocator_library = "@rules_rust//ffi/rs:empty", - binary_ext = "", - cargo = _RUST_TOOLS + "//:cargo", - cargo_clippy = _RUST_TOOLS + "//:cargo_clippy_bin", - channel = "nightly", - clippy_driver = _RUST_TOOLS + "//:clippy_driver_bin", - default_edition = "2024", +# One TSan toolchain per supported host. `--config=tsan` / `--config=tsan-macos` (.bazelrc) list +# all of them in --extra_toolchains; toolchain resolution picks the one matching the host. +# stdlib_linkflags mirror rules_rust's own generated toolchain for each triple. +tsan_rust_toolchain( + name = "tsan_toolchain_linux_x86_64", + cpu = "@platforms//cpu:x86_64", dylib_ext = ".so", - exec_triple = _TARGET_TRIPLE, - extra_exec_rustc_flags = [], extra_rustc_flags = ["-Ctarget-feature=+sse4.2,+pclmulqdq"], - iso_date = RUST_NIGHTLY_DATE, - linker = _RUST_TOOLS + "//:rust-lld", - linker_type = "direct", - llvm_cov = _RUST_TOOLS + "//:llvm_cov_bin", - llvm_lib = _RUST_TOOLS + "//:llvm_lib", - llvm_profdata = _RUST_TOOLS + "//:llvm_profdata_bin", - rust_doc = _RUST_TOOLS + "//:rustdoc", - rust_objcopy = _RUST_TOOLS + "//:rust-objcopy", - rust_std = ":tsan_rust_std", - rustc = _RUST_TOOLS + "//:rustc", - rustc_lib = ":nightly_rustc_lib", - rustfmt = _RUST_TOOLS + "//:rustfmt_bin", - staticlib_ext = ".a", + os = "@platforms//os:linux", + rust_tools = "@rust_nightly_linux_x86_64", stdlib_linkflags = [ "-ldl", "-lpthread", ], - tags = ["manual"], - target_compatible_with = ["//build/platforms:sanitizer_thread"], - target_triple = _TARGET_TRIPLE, - version = RUST_NIGHTLY_VERSION, + target_triple = "x86_64-unknown-linux-gnu", ) -toolchain( - name = "tsan_toolchain", - exec_compatible_with = [ - "@platforms//cpu:x86_64", - "@platforms//os:linux", +tsan_rust_toolchain( + name = "tsan_toolchain_macos_aarch64", + cpu = "@platforms//cpu:aarch64", + dylib_ext = ".dylib", + # rustc passes -nodefaultlibs to the linker driver, and unlike Linux, Darwin's clang then also + # omits the sanitizer runtime that -Zexternal-clangrt (.bazelrc) relies on it to provide. + # Letting clang add its default libraries restores libclang_rt.tsan_osx_dynamic.dylib (and + # its rpath) at every Rust-driven link, so rust_binary/rust_test targets share Clang's + # runtime with the C++ they link, exactly as on Linux. + extra_rustc_flags = ["-Cdefault-linker-libraries=yes"], + os = "@platforms//os:macos", + rust_tools = "@rust_nightly_macos_aarch64", + stdlib_linkflags = [ + "-lSystem", + "-lresolv", ], + target_triple = "aarch64-apple-darwin", +) + +# The host's TSan-instrumented standard library, for tests that inspect it (//src/rust/tsan-test). +alias( + name = "tsan_rust_std", + actual = select({ + "@platforms//os:linux": ":tsan_toolchain_linux_x86_64_std", + "@platforms//os:macos": ":tsan_toolchain_macos_aarch64_std", + }), tags = ["manual"], - target_compatible_with = [ - "@platforms//cpu:x86_64", - "@platforms//os:linux", - "//build/platforms:sanitizer_thread", - ], - target_settings = ["@rules_rust//rust/toolchain/channel:nightly"], - toolchain = ":tsan_toolchain_impl", - toolchain_type = "@rules_rust//rust:toolchain", + visibility = ["//src/rust/tsan-test:__pkg__"], ) diff --git a/build/rust/build_std.bzl b/build/rust/build_std.bzl index 8d314287ee9..25caed3d3a8 100644 --- a/build/rust/build_std.bzl +++ b/build/rust/build_std.bzl @@ -14,13 +14,15 @@ load("@rules_rust//rust:toolchain.bzl", "rust_stdlib_filegroup") # rustc_lib contains host compiler libraries plus prebuilt target metadata. A # replacement toolchain must discard that metadata while retaining the runtime -# archive needed when rustc links sanitizer-instrumented execution tools. +# library needed when rustc links sanitizer-instrumented execution tools. The +# runtime is a static archive on Linux (librustc-nightly_rt.tsan.a) and a dylib +# on macOS (librustc-nightly_rt.tsan.dylib), so match on the stem only. def _compiler_runtime_files_impl(ctx): - sanitizer_runtime = "_rt.{}.a".format(ctx.attr.sanitizer) + sanitizer_runtime = "_rt.{}.".format(ctx.attr.sanitizer) return [DefaultInfo(files = depset([ file for file in ctx.files.src - if "/lib/rustlib/" not in file.path or file.basename.endswith(sanitizer_runtime) + if "/lib/rustlib/" not in file.path or sanitizer_runtime in file.basename ]))] compiler_runtime_files = rule( diff --git a/build/rust/build_std.sh b/build/rust/build_std.sh index d501b43689b..f8a81814986 100755 --- a/build/rust/build_std.sh +++ b/build/rust/build_std.sh @@ -23,11 +23,16 @@ if [[ $# -lt 9 ]]; then exit 2 fi -rust_root=$(realpath "$1") -rust_library=$(realpath "$2") -target_dir=$(realpath -m "$3") -output_dir=$(realpath -m "$4") -work=$(realpath -m "$5") +# Portable across GNU and BSD userlands (macOS ships bash 3.2 and lacks GNU realpath -m): +# existing directories resolve via pwd -P, not-yet-existing outputs via their (existing) parent. +abs_dir() { (cd "$1" && pwd -P); } +abs_path() { printf '%s/%s\n' "$(abs_dir "$(dirname "$1")")" "$(basename "$1")"; } + +rust_root=$(abs_dir "$1") +rust_library=$(abs_dir "$2") +target_dir=$(abs_path "$3") +output_dir=$(abs_path "$4") +work=$(abs_path "$5") target_triple=$6 sanitizer=$7 shift 7 @@ -63,6 +68,13 @@ EOF export PATH="$rust_root/bin:$PATH" export RUSTC="$rust_root/bin/rustc" export CARGO_HOME="$work/cargo-home" +# Cargo also reads config.toml from every ancestor of the working directory, and Bazel's +# sandbox lives under the developer's home (~/Library/Caches/bazel on macOS, ~/.cache/bazel on +# Linux), so a developer's ~/.cargo/config.toml leaks into this action. Explicitly unset the +# settings that would redirect the compiler (an empty wrapper disables it); RUSTFLAGS and the +# linker are pinned below via the target-specific env var, which outranks config files. +export RUSTC_WRAPPER= +export RUSTC_WORKSPACE_WRAPPER= export CARGO_NET_OFFLINE=true export CARGO_TARGET_DIR="$target_dir" # Cargo has no public override for rust-src's location. This test-only hook is @@ -70,12 +82,14 @@ export CARGO_TARGET_DIR="$target_dir" export __CARGO_TESTS_ONLY_SRC_ROOT="$rust_library" # Cargo applies target-specific flags to the generated package and every -# build-std dependency. The temporary test binary may use Rust's sanitizer -# runtime; Bazel-built targets use -Zexternal-clangrt to share Clang's runtime -# with C++ at their final link. -env_name="CARGO_TARGET_${target_triple^^}_RUSTFLAGS" -env_name=${env_name//-/_} -export "$env_name=-Zsanitizer=$sanitizer -Zexternal-clangrt -Clinker=clang -Clink-arg=-fsanitize=$sanitizer" +# build-std dependency. The temporary test binary links Rust's own sanitizer +# runtime: rustc passes -nodefaultlibs to clang, and on macOS that suppresses +# clang's automatic sanitizer-runtime linking, so -Zexternal-clangrt would leave +# the binary's __tsan_* references unresolved. That flag only affects linking, +# never the rlibs we keep, so it is applied to Bazel-built targets from .bazelrc +# instead, where they share Clang's runtime with C++ at their final link. +env_name="CARGO_TARGET_$(printf '%s' "$target_triple" | tr '[:lower:]-' '[:upper:]_')_RUSTFLAGS" +export "$env_name=-Zsanitizer=$sanitizer -Clinker=clang -Clink-arg=-fsanitize=$sanitizer" "$rust_root/bin/cargo" test \ --manifest-path "$work/Cargo.toml" \ @@ -90,7 +104,9 @@ copy_unique_rlib() { local crate=$1 local destination=$2 local sources=() - mapfile -t sources < <(find "$deps" -maxdepth 1 -name "lib$crate-*.rlib" -print) + while IFS= read -r source; do + sources+=("$source") + done < <(find "$deps" -maxdepth 1 -name "lib$crate-*.rlib" -print) if [[ ${#sources[@]} -ne 1 ]]; then echo "Cargo produced ${#sources[@]} rlibs for $crate; expected exactly one" >&2 printf ' %s\n' "${sources[@]}" >&2 diff --git a/build/rust/tsan_toolchain.bzl b/build/rust/tsan_toolchain.bzl new file mode 100644 index 00000000000..80b41da6f8b --- /dev/null +++ b/build/rust/tsan_toolchain.bzl @@ -0,0 +1,87 @@ +# Copyright (c) 2026 Cloudflare, Inc. +# Licensed under the Apache 2.0 license found in the LICENSE file or at: +# https://opensource.org/licenses/Apache-2.0 + +"""Per-host ThreadSanitizer-instrumented Rust toolchains (see //build/rust:BUILD.bazel).""" + +load("@rules_rust//rust:toolchain.bzl", "rust_toolchain") +load("//build/deps:gen/build_deps.bzl", "RUST_NIGHTLY_DATE", "RUST_NIGHTLY_VERSION") +load(":build_std.bzl", "compiler_runtime_files", "instrumented_rust_std") + +def tsan_rust_toolchain( + name, + rust_tools, + target_triple, + cpu, + os, + dylib_ext, + stdlib_linkflags, + extra_rustc_flags = []): + """One host platform's ThreadSanitizer-instrumented Rust toolchain. + + Defines `_rustc_lib`, `_std` (the TSan-built standard library), + `_impl` (the rust_toolchain) and `` (the registrable toolchain), + all constrained to `cpu`/`os` plus the sanitizer_thread platform constraint. + """ + host = [cpu, os] + + compiler_runtime_files( + name = name + "_rustc_lib", + src = rust_tools + "//:rustc_lib", + sanitizer = "tsan", + tags = ["manual"], + target_compatible_with = host, + ) + + instrumented_rust_std( + name = name + "_std", + rust_src = "@rust_nightly_src", + rust_tools = rust_tools, + sanitizer = "thread", + tags = ["manual"], + target_compatible_with = host + ["//build/platforms:sanitizer_thread"], + target_triple = target_triple, + ) + + rust_toolchain( + name = name + "_impl", + allocator_library = "@rules_rust//ffi/rs:empty", + binary_ext = "", + cargo = rust_tools + "//:cargo", + cargo_clippy = rust_tools + "//:cargo_clippy_bin", + channel = "nightly", + clippy_driver = rust_tools + "//:clippy_driver_bin", + default_edition = "2024", + dylib_ext = dylib_ext, + exec_triple = target_triple, + extra_exec_rustc_flags = [], + extra_rustc_flags = extra_rustc_flags, + iso_date = RUST_NIGHTLY_DATE, + linker = rust_tools + "//:rust-lld", + linker_type = "direct", + llvm_cov = rust_tools + "//:llvm_cov_bin", + llvm_lib = rust_tools + "//:llvm_lib", + llvm_profdata = rust_tools + "//:llvm_profdata_bin", + rust_doc = rust_tools + "//:rustdoc", + rust_objcopy = rust_tools + "//:rust-objcopy", + rust_std = ":" + name + "_std", + rustc = rust_tools + "//:rustc", + rustc_lib = ":" + name + "_rustc_lib", + rustfmt = rust_tools + "//:rustfmt_bin", + staticlib_ext = ".a", + stdlib_linkflags = stdlib_linkflags, + tags = ["manual"], + target_compatible_with = host + ["//build/platforms:sanitizer_thread"], + target_triple = target_triple, + version = RUST_NIGHTLY_VERSION, + ) + + native.toolchain( + name = name, + exec_compatible_with = host, + tags = ["manual"], + target_compatible_with = host + ["//build/platforms:sanitizer_thread"], + target_settings = ["@rules_rust//rust/toolchain/channel:nightly"], + toolchain = ":" + name + "_impl", + toolchain_type = "@rules_rust//rust:toolchain", + ) diff --git a/src/rust/tsan-test/verify.sh b/src/rust/tsan-test/verify.sh index b6fdd5c2ef1..ca2524df329 100755 --- a/src/rust/tsan-test/verify.sh +++ b/src/rust/tsan-test/verify.sh @@ -8,8 +8,8 @@ set -euo pipefail readonly ROOT="${TEST_SRCDIR}/${TEST_WORKSPACE}" -stdlib=$(find "$ROOT/build/rust/tsan_rust_std_build" \ - -name libstd_std.rlib -print -quit) +# The instrumented stdlib lives under the host's tsan__std_build directory. +stdlib=$(find "$ROOT/build/rust" -name libstd_std.rlib -print -quit) if [[ -z "$stdlib" ]] || ! nm -A "$stdlib" 2>/dev/null | grep '__tsan_func_entry' >/dev/null; then echo "Rust standard library does not contain TSan instrumentation" >&2 exit 1