[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