clang-cl sanitizers on Windows — the runtime choices behind Sanitizers.cmake#
Everything here was learned by running an instrumented /MD Flutter app end to
end on Windows, not from release notes (AccelerANTgine, 2026-07-16).
cmake/Sanitizers.cmake encodes the conclusions;
this page carries the reasoning its two-line comment ceiling keeps out of the
module.
All of it is Debug-only: every sanitizer compile flag, define and runtime
library the module adds is wrapped in $<$<CONFIG:Debug>:...>, so a
multi-config generator (Visual Studio, Ninja Multi-Config) never instruments
its Release config, and single-config non-Debug builds are byte-identical with
sanitizers requested or not. The one unwrapped addition is the ASan runtime’s
link search directory (target_link_directories), which links nothing by
itself.
UBSan: trap mode when ASan is absent#
There is no /MD UBSan runtime on Windows — clang_rt.ubsan_standalone is built
/MT and failifmismatch-es against /MD builds. So the module splits by case:
UBSan together with ASan: the dynamic-CRT ASan runtime provides the UBSan handlers and prints rich reports; nothing extra to link.
UBSan alone:
-fsanitize-trap=undefined— no runtime needed; undefined behavior raises an immediate debugger break / fast-fail at the offending instruction.
This split is also why the module passes no -fsanitize= flags at link for
clang-cl: lld-link does not understand them, trap mode needs no runtime, and
the ASan runtime is linked explicitly (next section).
Microsoft’s ASan runtime, not LLVM’s#
Runtime choice matters for a full app, not just standalone test exes. LLVM’s
clang_rt.asan_dynamic loads after ucrtbase in the dependency graph, so
allocations made during CRT/COM startup are unhooked; when combase/ole32
later free them through ASan’s interceptors, LLVM’s runtime aborts with an
unsuppressible bad-free and the app never renders a frame. Microsoft’s ASan
runtime (shipped with VS BuildTools) tracks Windows heap ownership correctly
and passes those foreign frees through, so the instrumented app runs clean.
Both toolchains name the import lib and thunk identically
(clang_rt.asan_dynamic-x86_64.lib,
clang_rt.asan_dynamic_runtime_thunk-x86_64.lib) and clang’s instrumentation
interface is a subset of Microsoft’s runtime exports, so the module keeps
clang’s instrumentation and simply points the link search at Microsoft’s
lib/x64. Selection order:
$ENV{VCToolsInstallDir}/lib/x64— set by vcvars / VS developer shells.A glob over
C:/Program Files*/Microsoft Visual Studio/*/BuildTools/VC/Tools/MSVC/*/lib/x64.Fallback with a WARNING: LLVM’s own
clang_rtdir from--print-resource-dir— fine for standalone test/fuzz exes, but a full app aborts on startup as described above.
Run-time staging is the consumer’s job. The exe links against the import
lib; the matching clang_rt.asan_dynamic-x86_64.dll from the same
VC\Tools\MSVC\<ver> must sit next to the app exe (or on PATH) at launch. Each
consumer’s app-launch script stages it — the module cannot, because it never
knows where the final exe lands.
What the hub stages, and the helpers it offers. Both pick Microsoft’s runtime first, like the link:
A Debug build through
Invoke-CmakeConfigureAndBuild(WindowsCMake.Common) copies the sanitizer DLLs into the build root before the build and into itsbin\Debug\(orbin\) after it. It also puts their directory on PATH while it builds, so instrumented build-time tools start.Get-AsanRuntimeDll -RuntimeFlavor Msvc(WindowsTesting.Common) returns Microsoft’s DLL for a launch script to copy.
Test runs through Invoke-ManualTestExecutable or Invoke-CtestDiscoveredTests
get the runtime on PATH and ASAN_OPTIONS scoped by Invoke-WithAsanOptions.
It puts the caller’s options first and restores the old value afterwards. The
option values stay with the caller; the test default is
log_path=logs/asan.log:report_globals=1. Since 2026-09-24 an empty
-Options runs the block with ASAN_OPTIONS untouched, so
Invoke-WithRuntimePath -AsanOptions '' opts a full-app run out of that
default.