llvm / llvm/llvm-project

[SanitizerCoverage][WinEH] `-fsanitize=fuzzer` emits an empty catch funclet on Windows when the handler contains an indirect/virtual call

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

Description

# [SanitizerCoverage][WinEH] `-fsanitize=fuzzer` emits an empty catch funclet on Windows when the handler contains an indirect/virtual call

## Summary

On the `x86_64-pc-windows-msvc` target, compiling with `-fsanitize=fuzzer`
(SanitizerCoverage) produces a **catch handler funclet whose body is completely
dropped** when the handler performs an indirect/virtual call (for example
`ex.what()` on a caught `std::exception`/`std::runtime_error`, or any virtual
method on a caught polymorphic exception).

The generated funclet contains only a prologue followed immediately by
`.Lfunc_end0` — no handler code and no return. At runtime the MSVC C++ EH
machinery (`__CxxFrameHandler3` → `CxxCallCatchBlock` → `_CallSettingFrame`)
enters this empty funclet, falls off the end, and executes the `int 3`
(`llvm.trap`/unreachable) that follows the `_CxxThrowException` call, terminating
the process with `STATUS_BREAKPOINT` (`0x80000003`).

Because libFuzzer implies `-fsanitize=fuzzer`, this makes **any fuzz target whose
exception handler inspects the caught exception (e.g. logs `e.what()`) crash**,
even though the handler is correct and matches. It manifests as spurious
"crashes"/false positives.

## Reproducer

`fuzz_749.cpp`:

```cpp
#include
#include
#include
#include

extern "C" int LLVMFuzzerTestOneInput(const uint8_t*, size_t) {
try {
throw std::runtime_error("boom");
} catch (const std::runtime_error& ex) {
printf("caught: %s\n", ex.what()); // virtual what() -> funclet body dropped
fflush(stdout);
}
return 0;
}
```

Build and run:

```
clang++ --target=x86_64-pc-windows-msvc -std=c++23 -O0 -g -fsanitize=fuzzer fuzz_749.cpp -o fuzz_749.exe
fuzz_749.exe -runs=1
```

## Expected

The handler runs, prints `caught: boom`, and the fuzzer completes cleanly.

## Actual

The process aborts with exit code `0xC0000005` (with `-handle_winexcept=0`) or
`0x80000003` (STATUS_BREAKPOINT), printing nothing from the handler. No libFuzzer
crash report is produced; the handler simply never executes.

## Root cause (from emitted assembly)

Same source, compiled with `-S -O0 -g`.

**With `-fsanitize=fuzzer`** — the catch funclet is empty:

```asm
"?catch$2@?0?LLVMFuzzerTestOneInput@4HA":
.seh_proc "?catch$2@?0?LLVMFuzzerTestOneInput@4HA"
.seh_handler __CxxFrameHandler3, @unwind, @except
.LBB0_2:
movq %rdx, 16(%rsp)
pushq %rbp
subq $32, %rsp
leaq 96(%rdx), %rbp
.seh_endprologue
.Lfunc_end0: ; <-- body dropped: no what() call, no return
```

**Without instrumentation** — the funclet is complete:

```asm
"?catch$1@?0?LLVMFuzzerTestOneInput@4HA":
...prologue...
.seh_endprologue
movq -8(%rbp), %rcx ; ex
movq (%rcx), %rax ; vptr
movq 8(%rax), %rax ; vtable slot for what()
callq *%rax ; virtual ex.what()
...printf, ret...
```

The main body's trailing `int 3` (unreachable after the `noreturn`
`_CxxThrowException`) is where control ends up once the empty funclet returns
into the runtime and the frame is corrupted.

### Runtime backtrace (cdb)

```
eh_fuzz_vcall!LLVMFuzzerTestOneInput+0x7f: cc int 3
...
eh_fuzz_vcall!_CallSettingFrame+0x20
eh_fuzz_vcall!__FrameHandler3::CxxCallCatchBlock+0xed
```

## Characterization

- Reproduces at `-O0`, `-O1`, `-O2`, `-O3` (deterministic; not an optimizer issue).
- Triggered by the fuzzer/SanitizerCoverage instrumentation itself: **every**
`-fsanitize-coverage=` override under `-fsanitize=fuzzer` still empties
the funclet (tested `func`, `trace-pc-guard`, `inline-8bit-counters`,
`inline-8bit-counters,pc-table`, `inline-8bit-counters,trace-cmp,pc-table`,
and with/without `stack-depth`).
- Affects any polymorphic caught exception with a virtual call in the handler —
not specific to the CRT `std::exception`. A user-defined `Base`/`Derived`
thrown and caught by base reference, calling a virtual method in the handler,
crashes identically.
- Does **not** occur without the virtual/indirect call: `catch` that only does
scalar work, `throw int` / `catch (int)`, or a non-polymorphic caught type
all run correctly under `-fsanitize=fuzzer`.
- Present in both **clang 20** and **clang 22.1.8**.
- Per-function `__attribute__((no_sanitize("coverage")))` on the enclosing
function, or a `-fsanitize-coverage-ignorelist` entry, avoids the drop
(at the cost of coverage for that function).

## Not related to

- **ASan issue google/sanitizers#749 / PR llvm/llvm-project#159618** — that is a
distinct `-fsanitize=address` bug in `AddressSanitizer.cpp`
(`isInterestingAlloca` instrumenting the *catch parameter* alloca). That fix is
present in clang 22.1.8 and the `#749` repro runs cleanly under
`-fsanitize=address`. The bug reported here reproduces with **ASan and UBSan
disabled** (`-fsanitize=fuzzer` alone) and is a SanitizerCoverage/WinEH
funclet-outlining codegen defect — the handler body is removed at compile time.
- **oss-fuzz#2328** — a Linux libc++abi build-config / uncaught-exception issue
where the app handler still executes; not a codegen bug, and structurally
impossible on Windows funclets.

## Environment

- clang version 22.1.8 (https://github.com/llvm/llvm-project ca7933e47d3a3451d81e72ac174dcb5aa28b59d1)
- Target: `x86_64-pc-windows-msvc`
- Also reproduces with clang 20.
- Linker: lld-link. Language standard: C++23 (also reproduces at earlier standards).

## Suggested area

SanitizerCoverage instrumentation interacting with `WinEHPrepare` funclet
outlining: the coverage instrumentation inserted into the catch block appears to
cause the handler body to be dropped from the outlined funclet. SanitizerCoverage
should either skip instrumenting inside WinEH catchpad funclets, or preserve the
funclet body when instrumenting handlers that contain indirect calls.

Contributor guide

Open the contributing guide

Research direction

Start with the fuzz_749.cpp reproducer and the clang++ command, then compare its -S assembly with and without -fsanitize=fuzzer on x86_64-pc-windows-msvc. Trace the interaction between SanitizerCoverage instrumentation and WinEHPrepare, using the emitted catch funclet as the primary check; done means the handler body is preserved and the reproducer prints “caught: boom” and completes cleanly.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
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.