llvm / llvm/llvm-project

[asan][windows] Shadow-fault VEH can be displaced from head of chain, causing recursive AV when displacer reads thread_local on fresh threads

Open
#197,179 0 comments 0 reactions 0 assignees View on GitHub
compiler-rt:asan platform:windows
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

**Component**: compiler-rt — ASAN runtime, Windows
(`compiler-rt/lib/asan/asan_win.cc`)

## Summary

ASAN-Windows registers a vectored exception handler (VEH) that commits
shadow pages on demand via `VirtualAlloc(MEM_COMMIT)`. It's registered
with `AddVectoredExceptionHandler(First=TRUE, …)`. Any later caller
registering with `First=TRUE` displaces ASAN to position 2 in the
chain. If the displacer's handler itself faults on the AV path before
returning, ASAN's now-second handler never runs and the process dies
in a recursive AV.

This is reachable in any process that:
1. Spawns threads outside `kernel32!CreateThread` (e.g., Windows OS
thread-pool workers via `NtCreateWorkerFactory` / `nt!PspCreateThread`,
which bypass ASAN's `_asan_wrap_CreateThread` interceptor and never
get their stack shadow committed eagerly), **and**
2. Has at least one ASAN-instrumented TLS callback registered in
`.CRT$XL?` (very common — e.g., BoringSSL's
`bssl::thread_local_destructor`), **and**
3. Has another VEH ahead of ASAN's whose handler is unsafe on the AV
path.

The most prominent example of (3) is V8's WASM-trap VEH: its first
action is reading a `thread_local`, which on a brand-new thread
triggers `__dyn_tls_init` → runs ASAN-instrumented dynamic
initializers → second shadow AV inside V8's handler → recursion → die.

## Environment

- Windows 11 / Server 2025 (build 26100)
- clang `-fsanitize=address`
- compiler-rt's `clang_rt.asan_dynamic-x86_64.dll`

## Forensic evidence

At crash time:
- Thread start address is `ntdll!TppWorkerThread` (OS thread-pool
worker — kernel-created, bypasses kernel32).
- VEH chain has exactly two entries (`ntdll!LdrpVectorHandlerList`):
position 1 is the displacer's handler, position 2 is
`clang_rt.asan_dynamic-x86_64.dll`.
- The faulting access is in `_asan_memset` inside an ASAN-instrumented
dynamic initializer invoked by `__dyn_tls_init` from the
position-1 handler.

Compiler-rt's shadow-commit VEH is correct and would resolve the fault
if it ran, but it sits at position 2 and never gets the chance.

## Why it matters

The crash rate scales with how often kernel-created threads spawn
before the process's shadow has been incidentally warmed up by ASAN's
eager-commit interceptors on the heap and on `CreateThread`-tracked
threads. Faster CPUs / busier processes raise the rate.

## Suggested fixes (in compiler-rt)

1. At ASAN runtime init, save the handle returned by ASAN's own initial
`AddVectoredExceptionHandler` call (already done internally — just
expose it to the interceptor).

2. Replace `AddVectoredExceptionHandler` with a wrapper:

```c
PVOID __asan_intercepted_AddVectoredExceptionHandler(
ULONG first, PVECTORED_EXCEPTION_HANDLER handler) {
PVOID h = REAL(AddVectoredExceptionHandler)(first, handler);
if (first /* caller asked for head */ && asan_veh_was_head) {
// Caller has just displaced us. Re-register our handler at
// head to restore ordering. Drop the old handle.
REAL(RemoveVectoredExceptionHandler)(asan_veh_handle);
asan_veh_handle = REAL(AddVectoredExceptionHandler)(
/*First=*/TRUE, asan_shadow_fault_handler);
}
return h;
}

Synchronization via a lightweight mutex; reentrancy guard to avoid
re-entering during init.

This is invisible to embedders. Programs that register their own VEHs
on Windows ASAN get the right behavior without any code changes,
without having to know about ASAN's presence, and without coordinating
with ASAN through any new API.

## Workaround for downstream users

Register your own VEH with `First=FALSE` on Windows ASAN. This is not
discoverable without dump-level forensics.

Contributor guide

Open the contributing guide

Research direction

Start in compiler-rt/lib/asan/asan_win.cc and trace ASAN runtime initialization, the shadow-fault VEH, and the AddVectoredExceptionHandler interception path. Reproduce or inspect the Windows thread-pool and TLS-callback scenario described in the issue; done means ASAN's handler remains first when another caller registers a First=TRUE VEH and still resolves the shadow fault without recursive failure.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers, operating-systems
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.