Skip to content
Merged
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
42 changes: 28 additions & 14 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions build/deps/rust.MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
9 changes: 4 additions & 5 deletions build/platforms/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
)
106 changes: 36 additions & 70 deletions build/rust/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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__"],
)
8 changes: 5 additions & 3 deletions build/rust/build_std.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
40 changes: 28 additions & 12 deletions build/rust/build_std.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -63,19 +68,28 @@ 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
# what Cargo itself uses to exercise -Zbuild-std against a separate source tree.
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" \
Expand All @@ -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
Expand Down
87 changes: 87 additions & 0 deletions build/rust/tsan_toolchain.bzl
Original file line number Diff line number Diff line change
@@ -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 `<name>_rustc_lib`, `<name>_std` (the TSan-built standard library),
`<name>_impl` (the rust_toolchain) and `<name>` (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",
)
4 changes: 2 additions & 2 deletions src/rust/tsan-test/verify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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_<platform>_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
Expand Down
Loading