[llvm] [SanitizerCoverage] Give calls inserted into funclets their funclet bundle (PR #226762)
via llvm-commits
llvm-commits at lists.llvm.org
Sun Sep 27 00:13:17 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-llvm-transforms
Author: Yuma Kakei / DyTect (yumasansansan)
<details>
<summary>Changes</summary>
SanitizerCoverage inserts its runtime calls (`__sanitizer_cov_trace_pc_indir`, `__sanitizer_cov_trace_cmp*`, and the rest) without a `"funclet"` operand bundle. Inside a funclet, WinEHPrepare's `removeImplausibleInstructions` treats such a call as one that does not belong to the funclet and replaces it, and the rest of its block, with `unreachable`. With `-fsanitize=fuzzer` on `x86_64-pc-windows-msvc`, a catch handler that makes a virtual call or compares an integer therefore loses its body and falls through into whatever follows the funclet, usually int3 padding, and the fuzzer stops with `STATUS_BREAKPOINT` on inputs that are handled correctly. This is the cause of #<!-- -->212404: the issue describes the symptoms, and the missing bundle is why they occur. It is also what the FIXME in `wineh.ll` describes.
The patch records the runtime calls inserted into each function and, once the function has been instrumented and its blocks split, uses `colorEHFunclets` to give each call inside a funclet the bundle of its pad, as `RuntimeCallInserter` in `AddressSanitizer.cpp` does. `InjectTraceForExits` runs afterwards and is unaffected, since its calls precede returns and resumes, which are never inside a funclet. As in ASan, a call in a block that belongs to more than one funclet is reported with `emitError`, since one bundle cannot name two pads.
Testing: the FIXME in `wineh.ll` becomes checks that the callbacks in the catch and cleanup funclets carry the funclet's token and those in the function's body carry none. The new `funclet-bundle.ll` covers the handler from the issue with the options `-fsanitize=fuzzer` uses (level 4, inline 8-bit counters, trace-compares) and with gated callbacks, where the calls sit in blocks split off inside the funclet. Both tests fail with an unpatched `opt` and pass with the patch, and all 43 tests in `llvm/test/Instrumentation/SanitizerCoverage` pass. Compiled on through `llc -mtriple=x86_64-pc-windows-msvc`, the issue's kind of handler now keeps its whole body in the funclet instead of an empty one.
Found while fuzzing a Matroska demuxer on Windows: libebml's `EbmlMaster::Read` deletes the element in a `catch (...)` before rethrowing, and the virtual destructor call disappeared along with the rethrow.
Assisted-by: Claude Code (Claude Opus 5.5)
---
Full diff: https://github.com/llvm/llvm-project/pull/226762.diff
3 Files Affected:
- (modified) llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp (+76-17)
- (added) llvm/test/Instrumentation/SanitizerCoverage/funclet-bundle.ll (+91)
- (modified) llvm/test/Instrumentation/SanitizerCoverage/wineh.ll (+17-2)
``````````diff
diff --git a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
index 22aef2a846e68..6c1ba5dce3130 100644
--- a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
+++ b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
@@ -280,6 +280,9 @@ class ModuleSanitizerCoverage {
Value *CreateFunctionLocalGateCmp(IRBuilder<> &IRB);
void InjectCoverageAtBlock(Function &F, BasicBlock &BB, size_t Idx,
Value *&FunctionGateCmp, bool IsLeafFunc);
+ CallInst *createRuntimeCall(IRBuilder<> &IRB, FunctionCallee Callee,
+ ArrayRef<Value *> Args = {});
+ void addFuncletBundles(Function &F);
Function *CreateInitCallsForSections(Module &M, const char *CtorName,
const char *InitFunctionName, Type *Ty,
const char *Section);
@@ -318,6 +321,9 @@ class ModuleSanitizerCoverage {
GlobalVariable *FunctionBoolArray; // for inline-bool-flag.
GlobalVariable *FunctionPCsArray; // for pc-table.
GlobalVariable *FunctionCFsArray; // for control flow table
+ // Runtime calls inserted into the current function, for
+ // addFuncletBundles().
+ SmallVector<CallInst *, 16> RuntimeCalls;
SmallVector<GlobalValue *, 20> GlobalsToAppendToUsed;
SmallVector<GlobalValue *, 20> GlobalsToAppendToCompilerUsed;
@@ -763,11 +769,60 @@ void ModuleSanitizerCoverage::instrumentFunction(Function &F) {
InjectTraceForDiv(F, DivTraceTargets);
InjectTraceForGep(F, GepTraceTargets);
InjectTraceForLoadsAndStores(F, Loads, Stores);
+ // Before InjectTraceForExits(), whose EscapeEnumerator may turn the calls
+ // into invokes. The calls it inserts precede returns and resumes, which are
+ // never in a funclet.
+ addFuncletBundles(F);
if (Options.TracePCEntryExit)
InjectTraceForExits(F);
}
+CallInst *ModuleSanitizerCoverage::createRuntimeCall(IRBuilder<> &IRB,
+ FunctionCallee Callee,
+ ArrayRef<Value *> Args) {
+ CallInst *CI = IRB.CreateCall(Callee, Args);
+ RuntimeCalls.push_back(CI);
+ return CI;
+}
+
+// With scoped EH (MSVC C++), a call inside a funclet must name the funclet's
+// pad in a "funclet" operand bundle. WinEHPrepare takes a call without one for
+// a call that does not belong to the funclet and replaces it, and all that
+// follows it in its block, with unreachable: a catch handler that compares an
+// integer or makes an indirect call would lose its body. Give each runtime
+// call the bundle of the funclet it is in.
+void ModuleSanitizerCoverage::addFuncletBundles(Function &F) {
+ SmallVector<CallInst *, 16> Calls;
+ std::swap(Calls, RuntimeCalls);
+ if (Calls.empty() || !F.hasPersonalityFn() ||
+ !isScopedEHPersonality(classifyEHPersonality(F.getPersonalityFn())))
+ return;
+
+ DenseMap<BasicBlock *, ColorVector> BlockColors = colorEHFunclets(F);
+ for (CallInst *CI : Calls) {
+ const ColorVector &Colors = BlockColors[CI->getParent()];
+ // Unreachable blocks have no color; they are deleted later.
+ if (Colors.empty())
+ continue;
+ // A bundle names one pad.
+ if (Colors.size() != 1) {
+ F.getContext().emitError("Instruction's BasicBlock is not monochromatic");
+ continue;
+ }
+ BasicBlock *Color = Colors.front();
+ BasicBlock::iterator Pad = Color->getFirstNonPHIIt();
+ if (Pad == Color->end() || !Pad->isEHPad())
+ continue;
+ OperandBundleDef OB("funclet", &*Pad);
+ CallBase *NewCall = CallBase::addOperandBundle(CI, LLVMContext::OB_funclet,
+ OB, CI->getIterator());
+ NewCall->copyMetadata(*CI);
+ CI->replaceAllUsesWith(NewCall);
+ CI->eraseFromParent();
+ }
+}
+
GlobalVariable *ModuleSanitizerCoverage::CreateFunctionLocalArrayInSection(
size_t NumElements, Function &F, Type *Ty, const char *Section) {
ArrayType *ArrayTy = ArrayType::get(Ty, NumElements);
@@ -901,7 +956,8 @@ void ModuleSanitizerCoverage::InjectCoverageForIndirectCalls(
Value *Callee = CB.getCalledOperand();
if (isa<InlineAsm>(Callee))
continue;
- IRB.CreateCall(SanCovTracePCIndir, IRB.CreatePointerCast(Callee, IntptrTy));
+ createRuntimeCall(IRB, SanCovTracePCIndir,
+ IRB.CreatePointerCast(Callee, IntptrTy));
}
}
@@ -945,9 +1001,9 @@ void ModuleSanitizerCoverage::InjectTraceForSwitch(
if (Options.GatedCallbacks) {
auto GateBranch = CreateGateBranch(F, FunctionGateCmp, I);
IRBuilder<> GateIRB(GateBranch);
- GateIRB.CreateCall(SanCovTraceSwitchFunction, {Cond, GV});
+ createRuntimeCall(GateIRB, SanCovTraceSwitchFunction, {Cond, GV});
} else {
- IRB.CreateCall(SanCovTraceSwitchFunction, {Cond, GV});
+ createRuntimeCall(IRB, SanCovTraceSwitchFunction, {Cond, GV});
}
}
}
@@ -967,8 +1023,8 @@ void ModuleSanitizerCoverage::InjectTraceForDiv(
if (CallbackIdx < 0)
continue;
auto Ty = Type::getIntNTy(*C, TypeSize);
- IRB.CreateCall(SanCovTraceDivFunction[CallbackIdx],
- {IRB.CreateIntCast(A1, Ty, true)});
+ createRuntimeCall(IRB, SanCovTraceDivFunction[CallbackIdx],
+ {IRB.CreateIntCast(A1, Ty, true)});
}
}
@@ -978,8 +1034,8 @@ void ModuleSanitizerCoverage::InjectTraceForGep(
InstrumentationIRBuilder IRB(GEP);
for (Use &Idx : GEP->indices())
if (!isa<ConstantInt>(Idx) && Idx->getType()->isIntegerTy())
- IRB.CreateCall(SanCovTraceGepFunction,
- {IRB.CreateIntCast(Idx, IntptrTy, true)});
+ createRuntimeCall(IRB, SanCovTraceGepFunction,
+ {IRB.CreateIntCast(Idx, IntptrTy, true)});
}
}
@@ -1000,7 +1056,7 @@ void ModuleSanitizerCoverage::InjectTraceForLoadsAndStores(
int Idx = CallbackIdx(LI->getType());
if (Idx < 0)
continue;
- IRB.CreateCall(SanCovLoadFunction[Idx], Ptr);
+ createRuntimeCall(IRB, SanCovLoadFunction[Idx], Ptr);
}
for (auto *SI : Stores) {
InstrumentationIRBuilder IRB(SI);
@@ -1008,7 +1064,7 @@ void ModuleSanitizerCoverage::InjectTraceForLoadsAndStores(
int Idx = CallbackIdx(SI->getValueOperand()->getType());
if (Idx < 0)
continue;
- IRB.CreateCall(SanCovStoreFunction[Idx], Ptr);
+ createRuntimeCall(IRB, SanCovStoreFunction[Idx], Ptr);
}
}
@@ -1057,11 +1113,13 @@ void ModuleSanitizerCoverage::InjectTraceForCmp(
if (Options.GatedCallbacks) {
auto GateBranch = CreateGateBranch(F, FunctionGateCmp, I);
IRBuilder<> GateIRB(GateBranch);
- GateIRB.CreateCall(CallbackFunc, {GateIRB.CreateIntCast(A0, Ty, true),
- GateIRB.CreateIntCast(A1, Ty, true)});
+ createRuntimeCall(GateIRB, CallbackFunc,
+ {GateIRB.CreateIntCast(A0, Ty, true),
+ GateIRB.CreateIntCast(A1, Ty, true)});
} else {
- IRB.CreateCall(CallbackFunc, {IRB.CreateIntCast(A0, Ty, true),
- IRB.CreateIntCast(A1, Ty, true)});
+ createRuntimeCall(
+ IRB, CallbackFunc,
+ {IRB.CreateIntCast(A0, Ty, true), IRB.CreateIntCast(A1, Ty, true)});
}
}
}
@@ -1090,7 +1148,7 @@ void ModuleSanitizerCoverage::InjectCoverageAtBlock(Function &F, BasicBlock &BB,
FunctionCallee Callee = IsEntryBB && Options.TracePCEntryExit
? SanCovTracePCEntry
: SanCovTracePC;
- IRB.CreateCall(Callee)
+ createRuntimeCall(IRB, Callee)
->setCannotMerge(); // gets the PC using GET_CALLER_PC.
}
if (Options.TracePCGuard) {
@@ -1100,9 +1158,10 @@ void ModuleSanitizerCoverage::InjectCoverageAtBlock(Function &F, BasicBlock &BB,
Instruction *I = &*IP;
auto GateBranch = CreateGateBranch(F, FunctionGateCmp, I);
IRBuilder<> GateIRB(GateBranch);
- GateIRB.CreateCall(SanCovTracePCGuard, GuardPtr)->setCannotMerge();
+ createRuntimeCall(GateIRB, SanCovTracePCGuard, GuardPtr)
+ ->setCannotMerge();
} else {
- IRB.CreateCall(SanCovTracePCGuard, GuardPtr)->setCannotMerge();
+ createRuntimeCall(IRB, SanCovTracePCGuard, GuardPtr)->setCannotMerge();
}
}
if (Options.Inline8bitCounters) {
@@ -1166,7 +1225,7 @@ void ModuleSanitizerCoverage::InjectCoverageAtBlock(Function &F, BasicBlock &BB,
EstimatedStackSize >= Options.StackDepthCallbackMin) {
if (InsertBefore)
IRB.SetInsertPoint(InsertBefore);
- auto Call = IRB.CreateCall(SanCovStackDepthCallback);
+ auto Call = createRuntimeCall(IRB, SanCovStackDepthCallback);
if (EntryLoc)
Call->setDebugLoc(EntryLoc);
Call->setCannotMerge();
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/funclet-bundle.ll b/llvm/test/Instrumentation/SanitizerCoverage/funclet-bundle.ll
new file mode 100644
index 0000000000000..fc559928e533d
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/funclet-bundle.ll
@@ -0,0 +1,91 @@
+; Calls that SanitizerCoverage inserts into a funclet carry the funclet's
+; "funclet" operand bundle. WinEHPrepare takes a call in a funclet without one
+; for a call that does not belong there, and replaces it and the rest of its
+; block with unreachable: the handler below would lose everything from the
+; comparison on, the destructor call and the rethrow with it.
+
+; The coverage -fsanitize=fuzzer asks for.
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=4 -sanitizer-coverage-inline-8bit-counters -sanitizer-coverage-trace-compares -S | FileCheck %s
+; With the callbacks gated, the calls are in blocks split off inside the funclet.
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=4 -sanitizer-coverage-trace-pc-guard -sanitizer-coverage-trace-compares -sanitizer-coverage-gated-trace-callbacks -S | FileCheck %s --check-prefix=GATED
+
+; Generated from this C++ source, then renamed:
+; $ clang++ --target=x86_64-pc-windows-msvc -O2 -S -emit-llvm t.cpp
+; struct Element { virtual ~Element(); };
+; void read(Element *element);
+; void read_guarded(Element *element, int &found, int limit) {
+; try {
+; read(element);
+; } catch (...) {
+; if (found > limit)
+; found = 0;
+; delete element;
+; throw;
+; }
+; }
+
+target datalayout = "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-pc-windows-msvc19.33.0"
+
+define void @"?read_guarded@@YAXPEAUElement@@AEAHH at Z"(ptr %element, ptr %found, i32 %limit) personality ptr @__CxxFrameHandler3 {
+entry:
+ invoke void @"?read@@YAXPEAUElement@@@Z"(ptr %element)
+ to label %return unwind label %catch.dispatch
+
+catch.dispatch:
+ %0 = catchswitch within none [label %catch] unwind to caller
+
+catch:
+ %1 = catchpad within %0 [ptr null, i32 64, ptr null]
+ %2 = load i32, ptr %found, align 4
+ %cmp = icmp sgt i32 %2, %limit
+ br i1 %cmp, label %if.then, label %if.end
+
+if.then:
+ store i32 0, ptr %found, align 4
+ br label %if.end
+
+if.end:
+ %isnull = icmp eq ptr %element, null
+ br i1 %isnull, label %rethrow, label %delete.notnull
+
+delete.notnull:
+ %vtable = load ptr, ptr %element, align 8
+ %dtor = load ptr, ptr %vtable, align 8
+ %call = call ptr %dtor(ptr %element, i32 1) [ "funclet"(token %1) ]
+ br label %rethrow
+
+rethrow:
+ call void @_CxxThrowException(ptr null, ptr null) [ "funclet"(token %1) ]
+ unreachable
+
+return:
+ ret void
+}
+
+; CHECK-LABEL: define void @"?read_guarded@@YAXPEAUElement@@AEAHH at Z"(
+; CHECK: catch:
+; CHECK-NEXT: %[[PAD:[0-9]+]] = catchpad within %{{[0-9]+}} [ptr null, i32 64, ptr null]
+; CHECK: call void @__sanitizer_cov_trace_cmp4(i32 %{{[0-9]+}}, i32 %limit) [ "funclet"(token %[[PAD]]) ]
+; CHECK-NEXT: %cmp = icmp sgt i32 %{{[0-9]+}}, %limit
+; CHECK: delete.notnull:
+; CHECK: call void @__sanitizer_cov_trace_pc_indir(i64 %{{[0-9]+}}) [ "funclet"(token %[[PAD]]) ]
+; CHECK-NEXT: %call = call ptr %dtor(ptr %element, i32 1) [ "funclet"(token %[[PAD]]) ]
+; CHECK: rethrow:
+; CHECK: call void @_CxxThrowException(ptr null, ptr null) [ "funclet"(token %[[PAD]]) ]
+
+; Outside the funclet, no bundle.
+; GATED-LABEL: define void @"?read_guarded@@YAXPEAUElement@@AEAHH at Z"(
+; GATED: call void @__sanitizer_cov_trace_pc_guard(ptr @__sancov_gen_) #{{[0-9]+}}{{$}}
+; GATED: catch:
+; GATED-NEXT: %[[PAD:[0-9]+]] = catchpad within %{{[0-9]+}} [ptr null, i32 64, ptr null]
+; GATED: call void @__sanitizer_cov_trace_cmp4(i32 %{{[0-9]+}}, i32 %limit) [ "funclet"(token %[[PAD]]) ]
+; GATED: call void @__sanitizer_cov_trace_pc_guard({{.*}}) #{{[0-9]+}} [ "funclet"(token %[[PAD]]) ]
+; GATED: call void @__sanitizer_cov_trace_pc_indir(i64 %{{[0-9]+}}) [ "funclet"(token %[[PAD]]) ]
+; GATED-NEXT: %call = call ptr %dtor(ptr %element, i32 1) [ "funclet"(token %[[PAD]]) ]
+; GATED: return:
+; GATED: call void @__sanitizer_cov_trace_pc_guard({{.*}}) #{{[0-9]+}}{{$}}
+
+declare void @"?read@@YAXPEAUElement@@@Z"(ptr)
+declare i32 @__CxxFrameHandler3(...)
+declare void @_CxxThrowException(ptr, ptr)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/wineh.ll b/llvm/test/Instrumentation/SanitizerCoverage/wineh.ll
index 82ce3ad1a2ec3..a3e719366069c 100644
--- a/llvm/test/Instrumentation/SanitizerCoverage/wineh.ll
+++ b/llvm/test/Instrumentation/SanitizerCoverage/wineh.ll
@@ -17,12 +17,27 @@
; return 0;
; }
-; FIXME: We need to do more than this. In particular, __sanitizer_cov callbacks
-; in funclets need token bundles.
+; The callbacks in the catch and cleanup funclets carry the funclet's token;
+; the ones in the function's body carry none.
; CHECK-LABEL: define i32 @"\01?f@@YAHXZ"()
+; CHECK: entry:
+; CHECK: call void @__sanitizer_cov_trace_pc() #{{[0-9]+}}{{$}}
; CHECK: catch.dispatch:
; CHECK-NEXT: catchswitch within none [label %catch3, label %catch] unwind label %ehcleanup
+; CHECK: catch3:
+; CHECK-NEXT: %[[CATCH3:[0-9]+]] = catchpad within
+; CHECK-NEXT: call void @__sanitizer_cov_trace_pc() #{{[0-9]+}} [ "funclet"(token %[[CATCH3]]) ]
+; CHECK: invoke.cont4:
+; CHECK-NEXT: call void @__sanitizer_cov_trace_pc() #{{[0-9]+}} [ "funclet"(token %[[CATCH3]]) ]
+; CHECK: catch:
+; CHECK-NEXT: %[[CATCH:[0-9]+]] = catchpad within
+; CHECK-NEXT: call void @__sanitizer_cov_trace_pc() #{{[0-9]+}} [ "funclet"(token %[[CATCH]]) ]
+; CHECK: invoke.cont2:
+; CHECK-NEXT: call void @__sanitizer_cov_trace_pc() #{{[0-9]+}} [ "funclet"(token %[[CATCH]]) ]
+; CHECK: ehcleanup:
+; CHECK-NEXT: %[[CLEANUP:[0-9]+]] = cleanuppad within none []
+; CHECK-NEXT: call void @__sanitizer_cov_trace_pc() #{{[0-9]+}} [ "funclet"(token %[[CLEANUP]]) ]
; ModuleID = 't.cpp'
source_filename = "t.cpp"
``````````
</details>
https://github.com/llvm/llvm-project/pull/226762
More information about the llvm-commits
mailing list