Skip to content

build: instrument Rust under --config=asan - #7443

Open
danlapid wants to merge 3 commits into
mainfrom
dlapid/rust-asan
Open

danlapid wants to merge 3 commits into
mainfrom
dlapid/rust-asan

Conversation

@danlapid

@danlapid danlapid commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

--config=asan compiled and linked C++ with -fsanitize=address but built Rust without -Zsanitizer=address, so Rust code ran uninstrumented. Nightly rustc also refuses to link sanitizer-instrumented crates against an uninstrumented standard library, so adding the flag alone is not enough. ASan needs a toolchain like the one TSan already has.

Changes

  • build/rust/sanitizer_toolchain.bzl replaces tsan_toolchain.bzl. sanitizer_rust_toolchain(name, sanitizer, host) builds the toolchain for either sanitizer, with the per-host settings in one table. //build/rust now defines asan_toolchain_{linux_x86_64,linux_aarch64,macos_aarch64}; the TSan toolchain names are unchanged.
  • .bazelrc: asan registers the ASan toolchains and adds -Zsanitizer=address. -Zexternal-clangrt moves to sanitizer-common, so Rust uses Clang's runtime under both sanitizers.
  • rust.MODULE.bazel: exposes rust_nightly_linux_aarch64 for the ci-linux-arm-asan config.
  • macOS: Apple Clang's ASan runtime exports __asan_version_mismatch_check_apple_clang_<version> instead of upstream LLVM's __asan_version_mismatch_check_v8, which rustc references. The macOS ASan toolchain passes -Cllvm-args=-asan-guard-against-version-mismatch=false. build_std gets a rustflags input so the standard library is built with the same flag.
  • New test //src/rust/asan:heap_buffer_overflow checks that an out-of-bounds read in Rust code is reported. The read runs in a child process because __asan_on_error can't be overridden from the executable when the runtime is a dylib (macOS).
  • Removes the dead ASan test in cxx-integration-test, which was gated on a feature nothing set and imported a crate that doesn't exist. Also updates the ASan note in src/rust/cxx/AGENTS.md.

Testing

On macOS arm64 with --config=asan:

  • Passed: //src/rust/asan:heap_buffer_overflow, //src/rust/cxx/tests:test, //src/rust/cxx-integration-test:all, //src/rust/tsan:tokio_test, //src/rust/tsan:clone_fd.
  • Negative check: with -Cllvm-args=-asan-instrument-reads=false, the new test fails with "did not detect", so it really checks that Rust code is instrumented.

Not run locally on Linux; the x64 ASan CI job covers that.

Second commit: UBSan in --config=asan

ASan and TSan can't be built into the same binary, but UBSan can share a build with either. So --config=asan now uses -fsanitize=address,undefined for C++, and tests stop at the first UBSan report (UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1). rustc has no UBSan, so this covers C++ only.

Testing on macOS arm64:

  • All 91 tests in //src/rust/..., //src/workerd/util/... and //src/workerd/jsg/... that don't depend on V8 pass, with no UBSan reports.
  • A throwaway signed-overflow probe prints the UBSan report and aborts, confirming UBSan is active.
  • V8-dependent tests can't run under ASan on macOS 26. mksnapshot binds __asan_get_shadow_mapping to libsystem_sanitizers instead of Clang's runtime and segfaults in V8::Initialize. This happens with or without UBSan.

The x64 ASan CI job is the first real UBSan check for V8 and the rest of workerd. If it reports issues in third-party code, we can turn off specific checks or exclude those files.

🤖 Generated with Claude Code

--config=asan compiled and linked C++ with -fsanitize=address but built Rust
without -Zsanitizer=address, so Rust code ran uninstrumented. Nightly rustc
also refuses to link sanitizer-instrumented crates against an uninstrumented
standard library, so the flag alone is not enough.

- Generalize the TSan Rust toolchain macro into sanitizer_rust_toolchain
  (build/rust/sanitizer_toolchain.bzl) and define ASan toolchains, each with an
  ASan-built standard library, for Linux x86_64, Linux aarch64 (the
  ci-linux-arm-asan config) and macOS arm64.
- --config=asan registers them and adds -Zsanitizer=address;
  -Zexternal-clangrt moves to sanitizer-common so Rust shares Clang's runtime
  under both sanitizers.
- macOS: Apple Clang's ASan runtime exports
  __asan_version_mismatch_check_apple_clang_<version> rather than upstream
  LLVM's __asan_version_mismatch_check_v8 that rustc references, so the macOS
  ASan toolchain disables the version guard. build_std gains a rustflags input
  so the standard library is built with the same flags.
- Add //src/rust/asan:heap_buffer_overflow, which checks that an out-of-bounds
  read in Rust code is reported. It runs the read in a child process because
  __asan_on_error cannot be overridden from the executable when the runtime is
  a dylib.
- Remove the dead ASan test in cxx-integration-test (gated on a feature nothing
  set, importing a crate that does not exist) and update src/rust/cxx/AGENTS.md.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@danlapid
danlapid requested review from a team as code owners September 20, 2026 18:16
@ask-bonk

ask-bonk Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

@danlapid Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

UndefinedBehaviorSanitizer, unlike ASan and TSan, can share a build with
either, so enable it for C++ in the existing ASan configuration instead of
adding a CI lane. Tests halt on the first report with a stack trace.

It instruments C++ only: rustc has no UBSan.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Enabling UndefinedBehaviorSanitizer in --config=asan surfaced four reports
that fail jsg-test, buffer-nodejs-test, string-decoder-test and server-test:

- src/rust/jsg/ffi.c++: the typed-array constructors memcpy() into a
  zero-length backing store, whose Data() is null. Skip the copy when the
  array is empty.
- src/workerd/api/node/buffer.c++: the UTF-16 paths of writeInto() and
  toStringImpl() copied through kj::ArrayPtr<uint16_t> views of byte buffers
  that can sit at odd offsets, so the element-wise copy did misaligned
  uint16_t loads and stores. Copy through asBytes() instead; the aligned
  temporary each path already used now carries the data both ways.
- capnp's lexer memcpy()s from a null source for an empty backtick-quoted
  line. capnp-cpp is a github_tarball dependency, which update-deps.py cannot
  attach patches to, so --config=asan drops the nonnull-attribute check for
  that one file until the fix lands upstream.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant