Sanitizers for the D test runner
Whether — and how — sparkles:test-runner can drive a memory or threading sanitizer over a D unittest suite and surface each finding against the test that produced it. The survey grounds the LLVM compiler-rt stack (asan/lsan/tsan/msan/ubsan/hwasan/gwp_asan) and the Valgrind family on the D toolchain (LDC/GDC/DMD, druntime, dub), surveys how the field's runners attribute a finding per test, maps the story onto macOS/Windows and the hardware-assisted frontier, audits today's runner, and lands in a capability matrix and a milestoned --sanitize/--valgrind/--isolate proposal. Every page is grounded twice over: claims carry pinned repo@commit path:line locators or recorded experiments, and locally-demonstrable behavior is backed by runnable probes CI compiles and runs.
Last reviewed: July 11, 2026
This survey answers thirteen questions:
- Can LDC actually sanitize D — which
-fsanitize=kinds, and which runtime really links? → d-toolchain - Which of the three D compilers reaches which tool? → d-toolchain
- What does D's garbage collector hide from every memory tool? → the GC blind spot · asan · d-toolchain
- Is UndefinedBehaviorSanitizer reachable from D at all? → ubsan
- What does AddressSanitizer catch, and what are its D-specific blind spots? → asan
- Can a test runner attribute a race/leak/fault to the test that caused it? → runner-integrations + tsan
- What deterministically livelocks under a sanitizer, and why? → tsan
- Is MemorySanitizer usable for D? → d-toolchain § MSan
- What is the no-recompile path a DMD-built binary can use? → valgrind
- How does the field's tooling surface a sanitizer finding per test? → runner-integrations
- What happens today when a sparkles test crashes mid-run? → baseline
- What changes when the runner leaves Linux for macOS or Windows? → macos-windows
- What is the hardware-assisted / post-ASan story for D (HWASan, MTE, GWP-ASan, RTSan, TySan)? → hardware-assisted
Where does sparkles stand against all of it, and what should it build? → baseline + the delta table → proposal.
The seven concerns
Every tool in the tree is analyzed against the same seven-concern spine, in fixed order — and where a concern does not apply (UBSan has no instrumentation to control; MTE has no runner-facing option surface), the page says so, because the absence of a capability is itself a finding:
- Defect classes and blind spots — what it catches, and what it structurally cannot (the GC memory blind spot, redzone limits, definedness vs addressability).
- Instrumentation model and recompile scope — the instrumentation locus and whether user code, the whole instrumented world, or nothing must be rebuilt.
- D and druntime interaction — the GC, fibers, and the signal-based stop-the-world scan against a tool built for
malloc/freeC. - Runtime control and report capture — halt vs recover, the weak-hook surface, and Valgrind's client requests.
- Symbolization and suppressions — who demangles D (almost nobody), and the two suppression formats.
- Test-runner integration semantics — the three attribution designs (windowing / process-per-test / wrapper-and-parse).
- Platform, toolchain, and overhead — LDC/GDC/DMD reach, off-Linux columns, and the measured cost.
Master catalog
| Subject | What it is | Concern focus | Verification bed | Link |
|---|---|---|---|---|
| Concepts | the tree's shared vocabulary | all | tool sources | concepts.md |
| AddressSanitizer + LSan | the memory-error workhorse (shadow + redzones) | 1, 3, 4 | x86_64-linux hw | asan.md |
| UBSan | a documented absence — unreachable from D | 2 | source + one GDC hw datum | ubsan.md |
| ThreadSanitizer | the happens-before data-race detector | 3, 6 | x86_64-linux hw | tsan.md |
| The D toolchain | LDC/GDC/DMD × druntime × dub; owns the MSan story | 2, 3, 7 | x86_64-linux hw | d-toolchain.md |
| Valgrind | memcheck/helgrind/DRD — no recompile, DMD's path | 1, 2, 6 | x86_64-linux hw (LDC + DMD) | valgrind.md |
| Runner integrations | how Go/Rust/Swift/Zig/CTest/Bazel/pytest attribute | 6 | source; one Go hw battery | runner-integrations.md |
| macOS & Windows | the two off-Linux columns | 7 | source/lit; one aarch64-darwin datum | macos-windows.md |
| Hardware-assisted | HWASan · MTE · GWP-ASan · RTSan · TySan | 1, 2, 7 | x86_64-linux hw (3 of 5); MTE source/lit | hardware-assisted.md |
| sparkles baseline | today's runner, as observed under audit | audit target | in-repo (private clone) | sparkles-baseline.md |
| Integration proposal | milestoned --sanitize/--valgrind/--isolate | design | — | integration-proposal.md |
| Comparison | capability matrix · trade-offs · delta table | synthesis | — | comparison.md |
Taxonomies
By instrumentation locus
The axis that decides which D compiler can reach a tool at all (concepts § instrumentation locus).
| Locus | Tools | D-reachability | Pages |
|---|---|---|---|
| LLVM IR pass | ASan, LSan, TSan, MSan, HWASan, RTSan | inherited by LDC once the flag is plumbed | asan, tsan, d-toolchain, hardware-assisted |
| clang CodeGen only | UBSan, TySan | unreachable — no IR pass for a non-clang frontend | ubsan, hardware-assisted |
| Dynamic binary translation | Valgrind memcheck/helgrind/DRD, Dr. Memory | every compiler incl. DMD, no recompile | valgrind, macos-windows |
| Hardware tag check | Arm MTE | a deployment, needs MTE silicon | hardware-assisted |
| Sampling allocator | GWP-ASan | C-heap only; a production, not test, tool | hardware-assisted |
By defect class
Which tool catches what — and the D caveat on each.
| Defect class | Caught by | Not caught by / D caveat |
|---|---|---|
| Heap/stack/global overflow, use-after-free | ASan, HWASan, memcheck, GWP-ASan (sampled) | all blind to GC-pool memory; memcheck has no redzones |
| Stack-use-after-return | ASan (fake stack) — the flagship fiber catch | runtime-gated by detect_stack_use_after_return |
| Uninitialized-value read (definedness) | memcheck, MSan | ASan structurally cannot; MSan needs an instrumented world |
| Data race | TSan, helgrind/DRD | helgrind/DRD blind to core.atomic/SpinLock; a serialized schedule hides it |
| Memory leak | LSan, memcheck | GC-referenced malloc = LSan false positive; a dropped GC block is invisible |
Undefined behaviour (shifts, int.min/-1) | UBSan (C/C++ only) | unreachable from every D compiler |
| Real-time-safety violation | RTSan | LDC blocked on an LLVM ≥ 20 bump |
| Strict-aliasing (TBAA) | TySan | never for D — no !tbaa, no aliasing rules |
By toolchain
| Compiler | Reaches | Via |
|---|---|---|
| LDC | ASan, LSan, TSan, MSan (-conf=/tarball), Valgrind | -fsanitize= IR passes + the gcc-runtime fallback; Valgrind no-recompile |
| GDC 11.5 | ASan (--param asan-globals=0), LSan, TSan, Valgrind; UBSan check-empty | GCC libsanitizer + a -B/-L workaround |
| DMD | Valgrind only | no -fsanitize at all — DBI is its sole path |
By verification level
| Level | What carries it |
|---|---|
[hw-verified: x86_64-linux] | ASan/LSan/TSan/MSan(-conf=)/HWASan(aliasing)/GWP-ASan/RTSan/TySan catches, the dub-channel + druntime experiments, and the eleven probes — the primary bed |
[hw-verified: aarch64-darwin] | one datum: the recon Apple-clang ASan smoke test; the D-on-darwin runs were blocked (rerun kit staged) |
[source-verified] | LDC/GDC/DMD flag sets, compiler-rt/Valgrind/dub source, the Windows link branches, the MTE kernel ABI |
[literature] | vendor docs (MS Learn, Apple MIE, Android), the overhead headline figures, the saved papers |
IMPORTANT
The honest boundary: the x86_64-linux box is the only full bed. macOS is mechanism-verified, not run-verified — the mac-bsn transcripts were blocked and one Apple-clang C datum stands in; Windows has no hardware at all; and no MTE silicon is in reach (the project's only aarch64 box, an Apple M4, has no MTE). Every such gap is tagged in place and carried into comparison § open questions.
Milestones
When the field's tools landed. Coarse dates are [literature]; the Valgrind design papers are [source-verified] (archived locally).
| When | What |
|---|---|
| 2002 | Valgrind's first public release (DBI memory checking) [literature] |
| 2007 | Valgrind's design papers — the framework at PLDI 2007, "How to Shadow Every Byte of Memory" at VEE 2007 [source-verified] |
| 2011 | AddressSanitizer lands in LLVM (paper USENIX ATC 2012) [literature] |
| 2011–2012 | ThreadSanitizer v2 (the v3 shadow rewrite follows in 2021) [literature] |
| ~2013 | LeakSanitizer ships alongside ASan [literature] |
| 2015 | MemorySanitizer paper at CGO 2015 [literature] |
| 2018 | Arm MTE announced (arXiv 1802.09517) [literature] |
| 2019 | HWASan becomes Android 10's production memory-safety tool; GWP-ASan enters production fleets [literature] |
| 2021 | MSVC /fsanitize=address reaches GA in Visual Studio 16.9 [literature] |
| 2023-11 | first MTE handset: Google Pixel 8 / Tensor G3; the GWP-ASan paper (arXiv 2311.09394) [literature] |
| 2024 | first MTE datacenter CPU: AmpereOne [literature] |
| 2025-03 | RTSan and experimental TySan ship in LLVM/Clang 20 [literature] |
| 2025-09 | Apple Memory Integrity Enforcement (EMTE) debuts on the A19 / iPhone 17 — no M-series [literature] |
The runnable probes
Standalone dub single-file programs under examples/; CI compiles and runs each, and every one SKIP:s cleanly on a host or toolchain that lacks the capability (an uninstrumented or DMD build). Environment: Linux 6.18.26, AMD Ryzen 9 7940HX, LDC 1.41.0, against GCC 15.2's libasan/libtsan via LDC's gcc link fallback.
| Probe | Demonstrates | Backs |
|---|---|---|
| asan-heap-uaf.d | heap use-after-free caught, self-symbolized, exit 1 | asan concern 1 |
| asan-stack-uar.d | stack-use-after-return + the detect_stack_use_after_return gate | asan concern 1 |
| asan-global-overflow.d | global-buffer-overflow via .ptr past a __gshared array | asan concern 1 |
| asan-report-capture.d | log_path routing + report callback (the attribution material) | asan concern 4 |
| lsan-gc-interplay.d | LSan × GC four quadrants; detect_leaks gates the manual check | asan § LSan |
| tsan-data-race.d | race caught, core.atomic silent; halt_on_error=false, exit 66 | tsan concerns 3/4 |
| gc-uaf-blindspot.d | GC-pool UAF invisible while the malloc twin is caught | d-toolchain § GC blind spot |
| fiber-asan.d | fiber stack-use-after-return (the event-horizon bug), stock druntime | d-toolchain § fibers |
| valgrind-memcheck-catch.d | no-recompile XML + exit-code pipeline (DMD too) | valgrind concern 4 |
| valgrind-client-requests.d | etc.valgrind driving memcheck's A/V bits from D | valgrind concern 3 |
| valgrind-attribution.d | VALGRIND_PRINTF marker-window per-test attribution | valgrind § runner integration |
All eleven are platforms "linux" by design (macos-windows explains why a darwin port is a new file, not a platforms toggle); the ci helper ⊘-skips them on the macOS CI row. Run them with:
dub run :ci -- -x --files 'docs/research/sanitizers/examples/*.d'Quick navigation
- "I'm designing the sparkles
--sanitizemode" — concepts → d-toolchain → tsan/asan → runner-integrations → baseline → proposal; the comparison matrix + delta ties it together, and valgrind before the--valgrindmode. - "Which tool catches what?" — comparison § matrix → asan / tsan / valgrind → vocabulary in concepts.
- "What breaks off Linux?" — macos-windows → d-toolchain (the link branches) → valgrind (dead on macOS) → comparison § open questions.
- "The hardware-assisted future" — hardware-assisted → concepts § memory tagging → proposal § M7.
- Vocabulary lookup — concepts.md.
Sources
Per-page Sources sections carry the primary references; repos are pinned by SHA there. The reads behind the survey are LLVM compiler-rt/clang/llvm at 73802c2e, Valgrind at 218cee2f (tag VALGRIND_3_26_0), LDC v1.41.0, DMD e6baf474, dub 5efed360, and the Linux kernel at e43ffb69 (v7.1-rc6) for the MTE ABI; the runner-integration survey adds Go, Rust/cargo-nextest, SwiftPM, Zig, googletest, CMake, and pytest-valgrind at their pinned SHAs. The sanitizer papers (ASan, TSan, MSan, the two Valgrind papers, GWP-ASan, and the MTE whitepaper) are archived locally under papers/sanitizers/. The survey's own experimental evidence is the eleven runnable probes plus the recon mac-bsn Apple-clang smoke test, with every experiment's environment recorded alongside its output.