[clang] [compiler-rt] [llvm] [sancov] Add -fsanitize-coverage=trace-args,trace-ret (PR #201410)

Yunseong Kim via cfe-commits cfe-commits at lists.llvm.org
Sun Jul 12 12:26:54 PDT 2026


https://github.com/yskzalloc updated https://github.com/llvm/llvm-project/pull/201410

>From 3032cbec09b6dcdd6e946f68b14c305ea5eb9fce Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Sun, 7 Jun 2026 20:10:32 +0200
Subject: [PATCH 1/8] [SanitizerCoverage] Add trace-args and trace-ret coverage
 modes

Add two new sanitizer coverage modes:
  -fsanitize-coverage=trace-args
  -fsanitize-coverage=trace-ret

These insert calls to __sanitizer_cov_trace_args() at function entry
and __sanitizer_cov_trace_ret() before return instructions, enabling
per-task capture of function arguments and return values with automatic
struct field expansion via DICompositeType metadata.

Implementation:
- SanitizerCoverage.cpp: InjectTraceForArgs/InjectTraceForRet (~270 LOC)
- Uses DISubprogram/DICompositeType to extract struct field layouts
- Creates ConstantArray globals with FNV-1a hashed type name + offsets
- Scalar args spilled to alloca for uniform pointer interface
- Both flags imply edge coverage (level=3) when used alone
- Requires -g for struct expansion; skips functions without DISubprogram

Driver integration:
- CodeGenOptions.def: SanitizeCoverageTraceArgs/Ret flags
- SanitizerArgs.cpp: parse trace-args/trace-ret (enum 1<<20, 1<<21)
- BackendUtil.cpp: wire to SanitizerCoverageOptions
- Instrumentation.h: TraceArgs/TraceRet booleans

Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
 clang/include/clang/Basic/CodeGenOptions.def  |   2 +
 clang/include/clang/Basic/CodeGenOptions.h    |   3 +-
 clang/include/clang/Options/Options.td        |  10 +
 clang/lib/CodeGen/BackendUtil.cpp             |   2 +
 clang/lib/Driver/SanitizerArgs.cpp            |  14 +-
 .../llvm/Transforms/Utils/Instrumentation.h   |   2 +
 .../Instrumentation/SanitizerCoverage.cpp     | 243 +++++++++++++++++-
 7 files changed, 271 insertions(+), 5 deletions(-)

diff --git a/clang/include/clang/Basic/CodeGenOptions.def b/clang/include/clang/Basic/CodeGenOptions.def
index 7e54e75752f39..2f0d280c976a3 100644
--- a/clang/include/clang/Basic/CodeGenOptions.def
+++ b/clang/include/clang/Basic/CodeGenOptions.def
@@ -328,6 +328,8 @@ CODEGENOPT(SanitizeCoverageStackDepth, 1, 0, Benign) ///< Enable max stack depth
 VALUE_CODEGENOPT(SanitizeCoverageStackDepthCallbackMin , 32, 0, Benign) ///< Enable stack depth tracing callbacks.
 CODEGENOPT(SanitizeCoverageTraceLoads, 1, 0, Benign) ///< Enable tracing of loads.
 CODEGENOPT(SanitizeCoverageTraceStores, 1, 0, Benign) ///< Enable tracing of stores.
+CODEGENOPT(SanitizeCoverageTraceArgs, 1, 0, Benign) ///< Enable tracing of function args.
+CODEGENOPT(SanitizeCoverageTraceRet, 1, 0, Benign) ///< Enable tracing of return values.
 CODEGENOPT(SanitizeBinaryMetadataCovered, 1, 0, Benign) ///< Emit PCs for covered functions.
 CODEGENOPT(SanitizeBinaryMetadataAtomics, 1, 0, Benign) ///< Emit PCs for atomic operations.
 CODEGENOPT(SanitizeBinaryMetadataUAR, 1, 0, Benign) ///< Emit PCs for start of functions
diff --git a/clang/include/clang/Basic/CodeGenOptions.h b/clang/include/clang/Basic/CodeGenOptions.h
index c12434135a198..861f9088d4a5d 100644
--- a/clang/include/clang/Basic/CodeGenOptions.h
+++ b/clang/include/clang/Basic/CodeGenOptions.h
@@ -676,7 +676,8 @@ class CodeGenOptions : public CodeGenOptionsBase {
   bool hasSanitizeCoverage() const {
     return SanitizeCoverageType || SanitizeCoverageIndirectCalls ||
            SanitizeCoverageTraceCmp || SanitizeCoverageTraceLoads ||
-           SanitizeCoverageTraceStores || SanitizeCoverageControlFlow;
+           SanitizeCoverageTraceStores || SanitizeCoverageControlFlow ||
+           SanitizeCoverageTraceArgs || SanitizeCoverageTraceRet;
   }
 
   // Check if any one of SanitizeBinaryMetadata* is enabled.
diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td
index 77c93db0079d5..4aaab11973ca5 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -8165,6 +8165,16 @@ def fsanitize_coverage_trace_stores
       Group<fsan_cov_Group>,
       HelpText<"Enable tracing of stores">,
       MarshallingInfoFlag<CodeGenOpts<"SanitizeCoverageTraceStores">>;
+def fsanitize_coverage_trace_args
+    : Flag<["-"], "fsanitize-coverage-trace-args">,
+      Group<fsan_cov_Group>,
+      HelpText<"Enable dataflow tracing of function arguments">,
+      MarshallingInfoFlag<CodeGenOpts<"SanitizeCoverageTraceArgs">>;
+def fsanitize_coverage_trace_ret
+    : Flag<["-"], "fsanitize-coverage-trace-ret">,
+      Group<fsan_cov_Group>,
+      HelpText<"Enable dataflow tracing of return values">,
+      MarshallingInfoFlag<CodeGenOpts<"SanitizeCoverageTraceRet">>;
 def fexperimental_sanitize_metadata_EQ_covered
     : Flag<["-"], "fexperimental-sanitize-metadata=covered">,
       HelpText<"Emit PCs for code covered with binary analysis sanitizers">,
diff --git a/clang/lib/CodeGen/BackendUtil.cpp b/clang/lib/CodeGen/BackendUtil.cpp
index 2b755fa916e55..4ba6cdf256145 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -262,6 +262,8 @@ getSancovOptsFromCGOpts(const CodeGenOptions &CGOpts) {
   Opts.StackDepthCallbackMin = CGOpts.SanitizeCoverageStackDepthCallbackMin;
   Opts.TraceLoads = CGOpts.SanitizeCoverageTraceLoads;
   Opts.TraceStores = CGOpts.SanitizeCoverageTraceStores;
+  Opts.TraceArgs = CGOpts.SanitizeCoverageTraceArgs;
+  Opts.TraceRet = CGOpts.SanitizeCoverageTraceRet;
   Opts.CollectControlFlow = CGOpts.SanitizeCoverageControlFlow;
   return Opts;
 }
diff --git a/clang/lib/Driver/SanitizerArgs.cpp b/clang/lib/Driver/SanitizerArgs.cpp
index e813efc89073d..f6304787e9150 100644
--- a/clang/lib/Driver/SanitizerArgs.cpp
+++ b/clang/lib/Driver/SanitizerArgs.cpp
@@ -109,6 +109,8 @@ enum CoverageFeature {
   CoverageTraceStores = 1 << 17,
   CoverageControlFlow = 1 << 18,
   CoverageTracePCEntryExit = 1 << 19,
+  CoverageTraceArgs = 1 << 20,
+  CoverageTraceRet = 1 << 21,
 };
 
 enum BinaryMetadataFeature {
@@ -1050,6 +1052,7 @@ SanitizerArgs::SanitizerArgs(const ToolChain &TC,
   int InstrumentationTypes = CoverageTracePC | CoverageTracePCEntryExit |
                              CoverageTracePCGuard | CoverageInline8bitCounters |
                              CoverageTraceLoads | CoverageTraceStores |
+                             CoverageTraceArgs | CoverageTraceRet |
                              CoverageInlineBoolFlag | CoverageControlFlow;
   if ((CoverageFeatures & InsertionPointTypes) &&
       !(CoverageFeatures & InstrumentationTypes) && DiagnoseErrors) {
@@ -1061,9 +1064,10 @@ SanitizerArgs::SanitizerArgs(const ToolChain &TC,
 
   // trace-pc w/o func/bb/edge implies edge.
   if (!(CoverageFeatures & InsertionPointTypes)) {
-    if (CoverageFeatures & (CoverageTracePC | CoverageTracePCEntryExit |
-                            CoverageTracePCGuard | CoverageInline8bitCounters |
-                            CoverageInlineBoolFlag | CoverageControlFlow))
+    if (CoverageFeatures &
+        (CoverageTracePC | CoverageTracePCEntryExit | CoverageTracePCGuard |
+         CoverageInline8bitCounters | CoverageInlineBoolFlag |
+         CoverageControlFlow | CoverageTraceArgs | CoverageTraceRet))
       CoverageFeatures |= CoverageEdge;
 
     if (CoverageFeatures & CoverageStackDepth)
@@ -1424,6 +1428,8 @@ void SanitizerArgs::addArgs(const ToolChain &TC, const llvm::opt::ArgList &Args,
       std::make_pair(CoverageStackDepth, "-fsanitize-coverage-stack-depth"),
       std::make_pair(CoverageTraceLoads, "-fsanitize-coverage-trace-loads"),
       std::make_pair(CoverageTraceStores, "-fsanitize-coverage-trace-stores"),
+      std::make_pair(CoverageTraceArgs, "-fsanitize-coverage-trace-args"),
+      std::make_pair(CoverageTraceRet, "-fsanitize-coverage-trace-ret"),
       std::make_pair(CoverageControlFlow, "-fsanitize-coverage-control-flow")};
   for (auto F : CoverageFlags) {
     if (CoverageFeatures & F.first)
@@ -1811,6 +1817,8 @@ int parseCoverageFeatures(const Driver &D, const llvm::opt::Arg *A,
                 .Case("stack-depth", CoverageStackDepth)
                 .Case("trace-loads", CoverageTraceLoads)
                 .Case("trace-stores", CoverageTraceStores)
+                .Case("trace-args", CoverageTraceArgs)
+                .Case("trace-ret", CoverageTraceRet)
                 .Case("control-flow", CoverageControlFlow)
                 .Default(0);
     if (F == 0 && DiagnoseErrors)
diff --git a/llvm/include/llvm/Transforms/Utils/Instrumentation.h b/llvm/include/llvm/Transforms/Utils/Instrumentation.h
index 95a985ba3f0c4..8a4324175b075 100644
--- a/llvm/include/llvm/Transforms/Utils/Instrumentation.h
+++ b/llvm/include/llvm/Transforms/Utils/Instrumentation.h
@@ -163,6 +163,8 @@ struct SanitizerCoverageOptions {
   bool StackDepth = false;
   bool TraceLoads = false;
   bool TraceStores = false;
+  bool TraceArgs = false;
+  bool TraceRet = false;
   bool CollectControlFlow = false;
   bool GatedCallbacks = false;
   int StackDepthCallbackMin = 0;
diff --git a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
index df9675a02824e..b6d9d6d7b6f33 100644
--- a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
+++ b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
@@ -15,9 +15,11 @@
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/Analysis/GlobalsModRef.h"
 #include "llvm/Analysis/PostDominators.h"
+#include "llvm/BinaryFormat/Dwarf.h"
 #include "llvm/IR/Constant.h"
 #include "llvm/IR/Constants.h"
 #include "llvm/IR/DataLayout.h"
+#include "llvm/IR/DebugInfoMetadata.h"
 #include "llvm/IR/Dominators.h"
 #include "llvm/IR/EHPersonalities.h"
 #include "llvm/IR/Function.h"
@@ -46,6 +48,8 @@ const char SanCovTracePCIndirName[] = "__sanitizer_cov_trace_pc_indir";
 const char SanCovTracePCName[] = "__sanitizer_cov_trace_pc";
 const char SanCovTracePCEntryName[] = "__sanitizer_cov_trace_pc_entry";
 const char SanCovTracePCExitName[] = "__sanitizer_cov_trace_pc_exit";
+const char SanCovTraceArgsName[] = "__sanitizer_cov_trace_args";
+const char SanCovTraceRetName[] = "__sanitizer_cov_trace_ret";
 const char SanCovTraceCmp1[] = "__sanitizer_cov_trace_cmp1";
 const char SanCovTraceCmp2[] = "__sanitizer_cov_trace_cmp2";
 const char SanCovTraceCmp4[] = "__sanitizer_cov_trace_cmp4";
@@ -156,6 +160,15 @@ static cl::opt<bool> ClGEPTracing("sanitizer-coverage-trace-geps",
                                   cl::desc("Tracing of GEP instructions"),
                                   cl::Hidden);
 
+static cl::opt<bool>
+    ClTraceArgs("sanitizer-coverage-trace-args",
+                   cl::desc("Dataflow tracing of function arguments"),
+                   cl::Hidden);
+
+static cl::opt<bool>
+    ClTraceRet("sanitizer-coverage-trace-ret",
+                  cl::desc("Dataflow tracing of return values"), cl::Hidden);
+
 static cl::opt<bool>
     ClPruneBlocks("sanitizer-coverage-prune-blocks",
                   cl::desc("Reduce the number of instrumented blocks"),
@@ -226,10 +239,13 @@ SanitizerCoverageOptions OverrideFromCL(SanitizerCoverageOptions Options) {
                                            ClStackDepthCallbackMin.getValue());
   Options.TraceLoads |= ClLoadTracing;
   Options.TraceStores |= ClStoreTracing;
+  Options.TraceArgs |= ClTraceArgs;
+  Options.TraceRet |= ClTraceRet;
   Options.GatedCallbacks |= ClGatedCallbacks;
   if (!Options.TracePCGuard && !Options.TracePC && !Options.TracePCEntryExit &&
       !Options.Inline8bitCounters && !Options.StackDepth &&
-      !Options.InlineBoolFlag && !Options.TraceLoads && !Options.TraceStores)
+      !Options.InlineBoolFlag && !Options.TraceLoads && !Options.TraceStores &&
+      !Options.TraceArgs && !Options.TraceRet)
     Options.TracePCGuard = true; // TracePCGuard is default.
   Options.CollectControlFlow |= ClCollectCF;
   return Options;
@@ -265,6 +281,8 @@ class ModuleSanitizerCoverage {
   void InjectTraceForLoadsAndStores(Function &F, ArrayRef<LoadInst *> Loads,
                                     ArrayRef<StoreInst *> Stores);
   void InjectTraceForExits(Function &F);
+  void InjectTraceForArgs(Function &F);
+  void InjectTraceForRet(Function &F);
   void InjectTraceForSwitch(Function &F,
                             ArrayRef<Instruction *> SwitchTraceTargets,
                             Value *&FunctionGateCmp);
@@ -298,6 +316,7 @@ class ModuleSanitizerCoverage {
   FunctionCallee SanCovTracePCIndir;
   FunctionCallee SanCovTracePC, SanCovTracePCGuard;
   FunctionCallee SanCovTracePCEntry, SanCovTracePCExit;
+  FunctionCallee SanCovTraceArgsFunc, SanCovTraceRetFunc;
   std::array<FunctionCallee, 4> SanCovTraceCmpFunction;
   std::array<FunctionCallee, 4> SanCovTraceConstCmpFunction;
   std::array<FunctionCallee, 5> SanCovLoadFunction;
@@ -542,6 +561,16 @@ bool ModuleSanitizerCoverage::instrumentModule() {
   SanCovTracePCGuard =
       M.getOrInsertFunction(SanCovTracePCGuardName, VoidTy, PtrTy);
 
+  // __sanitizer_cov_trace_args(i64 pc, i32 arg_idx, i32 arg_size, ptr arg, ptr
+  // offsets, i32 num_fields)
+  SanCovTraceArgsFunc =
+      M.getOrInsertFunction(SanCovTraceArgsName, VoidTy, Int64Ty, Int32Ty,
+                            Int32Ty, PtrTy, PtrTy, Int32Ty);
+  // __sanitizer_cov_trace_ret(i64 pc, i32 ret_size, ptr ret_val, ptr offsets,
+  // i32 num_fields)
+  SanCovTraceRetFunc = M.getOrInsertFunction(
+      SanCovTraceRetName, VoidTy, Int64Ty, Int32Ty, PtrTy, PtrTy, Int32Ty);
+
   SanCovStackDepthCallback =
       M.getOrInsertFunction(SanCovStackDepthCallbackName, VoidTy);
 
@@ -767,6 +796,12 @@ void ModuleSanitizerCoverage::instrumentFunction(Function &F) {
 
   if (Options.TracePCEntryExit)
     InjectTraceForExits(F);
+
+  if (Options.TraceArgs)
+    InjectTraceForArgs(F);
+
+  if (Options.TraceRet)
+    InjectTraceForRet(F);
 }
 
 GlobalVariable *ModuleSanitizerCoverage::CreateFunctionLocalArrayInSection(
@@ -1266,3 +1301,209 @@ void ModuleSanitizerCoverage::createFunctionControlFlow(Function &F) {
       ConstantArray::get(ArrayType::get(PtrTy, CFs.size()), CFs));
   FunctionCFsArray->setConstant(true);
 }
+
+// Helper: Given a DIType, resolve typedefs/qualifiers to the underlying type.
+static DIType *stripDITypedefs(DIType *Ty) {
+  while (Ty) {
+    if (auto *Derived = dyn_cast<DIDerivedType>(Ty)) {
+      unsigned Tag = Derived->getTag();
+      if (Tag == dwarf::DW_TAG_typedef || Tag == dwarf::DW_TAG_const_type ||
+          Tag == dwarf::DW_TAG_volatile_type ||
+          Tag == dwarf::DW_TAG_restrict_type) {
+        Ty = Derived->getBaseType();
+        continue;
+      }
+      // pointer type - stop
+      break;
+    }
+    break;
+  }
+  return Ty;
+}
+
+// Helper: If Ty is a pointer to a struct (DICompositeType), collect byte
+// offsets of all scalar members. Returns the offsets array global and
+// num_fields.
+static std::pair<GlobalVariable *, unsigned>
+getStructFieldOffsets(DIType *Ty, Module &M, const DataLayout &DL) {
+  if (!Ty)
+    return {nullptr, 0};
+
+  Ty = stripDITypedefs(Ty);
+
+  // Must be a pointer to something
+  auto *PtrTy = dyn_cast_or_null<DIDerivedType>(Ty);
+  if (!PtrTy || PtrTy->getTag() != dwarf::DW_TAG_pointer_type)
+    return {nullptr, 0};
+
+  DIType *PointeeTy = stripDITypedefs(PtrTy->getBaseType());
+  auto *Composite = dyn_cast_or_null<DICompositeType>(PointeeTy);
+  if (!Composite || Composite->getTag() != dwarf::DW_TAG_structure_type)
+    return {nullptr, 0};
+
+  SmallVector<uint64_t, 16> Offsets;
+  for (auto *Element : Composite->getElements()) {
+    auto *Member = dyn_cast<DIDerivedType>(Element);
+    if (!Member || Member->getTag() != dwarf::DW_TAG_member)
+      continue;
+    uint64_t OffsetBits = Member->getOffsetInBits();
+    uint64_t SizeBits = Member->getSizeInBits();
+    if (SizeBits == 0)
+      continue;
+    // Record byte offset and size in bytes as pairs: [offset, size]
+    Offsets.push_back(OffsetBits / 8);
+    Offsets.push_back(SizeBits / 8);
+  }
+
+  if (Offsets.empty())
+    return {nullptr, 0};
+
+  // Enhance #4: Compute type name hash from struct name
+  uint64_t TypeHash = 0;
+  if (auto Name = Composite->getName(); !Name.empty()) {
+    // Simple FNV-1a hash of the struct name
+    TypeHash = 0xcbf29ce484222325ULL;
+    for (char C : Name) {
+      TypeHash ^= (uint64_t)(unsigned char)C;
+      TypeHash *= 0x100000001b3ULL;
+    }
+  }
+
+  // Layout: [type_hash, off0, sz0, off1, sz1, ...]
+  // We pass &array[1] as the offsets pointer, so kernel can read array[0] as
+  // hash
+  LLVMContext &C = M.getContext();
+  Type *I64Ty = Type::getInt64Ty(C);
+  SmallVector<Constant *, 16> OffsetConstants;
+  OffsetConstants.push_back(
+      ConstantInt::get(I64Ty, TypeHash)); // index 0 = hash
+  for (uint64_t V : Offsets)
+    OffsetConstants.push_back(ConstantInt::get(I64Ty, V));
+
+  ArrayType *ArrTy = ArrayType::get(I64Ty, OffsetConstants.size());
+  auto *GV = new GlobalVariable(M, ArrTy, true, GlobalVariable::PrivateLinkage,
+                                ConstantArray::get(ArrTy, OffsetConstants),
+                                "__sancov_offsets_");
+  GV->setUnnamedAddr(GlobalValue::UnnamedAddr::Global);
+  return {GV, (unsigned)(Offsets.size() / 2)};
+}
+
+void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
+  DISubprogram *SP = F.getSubprogram();
+  if (!SP)
+    return;
+
+  BasicBlock &EntryBB = F.getEntryBlock();
+  Instruction *InsertPt = &*EntryBB.getFirstInsertionPt();
+  InstrumentationIRBuilder IRB(InsertPt);
+
+  // Get PC as the function address cast to i64
+  Value *PC = IRB.CreatePtrToInt(&F, Int64Ty);
+
+  // For each argument, emit a trace call
+  unsigned ArgIdx = 0;
+  for (auto &Arg : F.args()) {
+    // Get debug info for this argument
+    DIType *ArgDIType = nullptr;
+    if (SP->getType()) {
+      auto *SubroutineType = SP->getType();
+      auto TypeArray = SubroutineType->getTypeArray();
+      // TypeArray[0] is return type, TypeArray[1..] are params
+      if (ArgIdx + 1 < TypeArray.size())
+        ArgDIType = TypeArray[ArgIdx + 1];
+    }
+
+    auto [OffsetsGV, NumFields] = getStructFieldOffsets(ArgDIType, M, *DL);
+
+    Value *ArgPtr;
+    if (Arg.getType()->isPointerTy()) {
+      ArgPtr = &Arg;
+    } else {
+      // Spill non-pointer arg to stack so we can pass its address
+      AllocaInst *Alloca = IRB.CreateAlloca(Arg.getType());
+      IRB.CreateStore(&Arg, Alloca);
+      ArgPtr = Alloca;
+    }
+
+    // Compute arg byte size
+    unsigned ArgByteSize = Arg.getType()->isPointerTy()
+                               ? DL->getPointerSize()
+                               : DL->getTypeStoreSize(Arg.getType());
+
+    // OffsetsGV layout: [hash, off0, sz0, off1, sz1, ...]
+    // Pass pointer to &array[1] so kernel sees field data at offsets[0],
+    // and can read offsets[-1] for the type hash.
+    Value *OffsetsPtr;
+    if (OffsetsGV) {
+      Value *Indices[] = {ConstantInt::get(Int64Ty, 0),
+                          ConstantInt::get(Int64Ty, 1)};
+      OffsetsPtr =
+          IRB.CreateInBoundsGEP(OffsetsGV->getValueType(), OffsetsGV, Indices);
+    } else {
+      OffsetsPtr = Constant::getNullValue(PtrTy);
+    }
+    Value *NF = ConstantInt::get(Int32Ty, NumFields);
+    Value *ArgIdxVal = ConstantInt::get(Int32Ty, ArgIdx);
+    Value *ArgSizeVal = ConstantInt::get(Int32Ty, ArgByteSize);
+
+    IRB.CreateCall(SanCovTraceArgsFunc,
+                   {PC, ArgIdxVal, ArgSizeVal, ArgPtr, OffsetsPtr, NF});
+    ArgIdx++;
+  }
+}
+
+void ModuleSanitizerCoverage::InjectTraceForRet(Function &F) {
+  DISubprogram *SP = F.getSubprogram();
+
+  // Get return type debug info
+  DIType *RetDIType = nullptr;
+  if (SP && SP->getType()) {
+    auto TypeArray = SP->getType()->getTypeArray();
+    if (TypeArray.size() > 0)
+      RetDIType = TypeArray[0];
+  }
+
+  auto [OffsetsGV, NumFields] = getStructFieldOffsets(RetDIType, M, *DL);
+
+  EscapeEnumerator EE(F, "sancov_trace_ret");
+  while (IRBuilder<> *AtExit = EE.Next()) {
+    InstrumentationIRBuilder::ensureDebugInfo(*AtExit, F);
+
+    Value *PC = AtExit->CreatePtrToInt(&F, Int64Ty);
+
+    // Get the return value
+    auto *RI = dyn_cast<ReturnInst>(AtExit->GetInsertPoint());
+    Value *RetVal = nullptr;
+    if (RI)
+      RetVal = RI->getReturnValue();
+
+    Value *RetPtr;
+    unsigned RetByteSize = 0;
+    if (RetVal && RetVal->getType()->isPointerTy()) {
+      RetPtr = RetVal;
+      RetByteSize = DL->getPointerSize();
+    } else if (RetVal && !RetVal->getType()->isVoidTy()) {
+      AllocaInst *Alloca = AtExit->CreateAlloca(RetVal->getType());
+      AtExit->CreateStore(RetVal, Alloca);
+      RetPtr = Alloca;
+      RetByteSize = DL->getTypeStoreSize(RetVal->getType());
+    } else {
+      RetPtr = Constant::getNullValue(PtrTy);
+    }
+
+    Value *OffsetsPtr;
+    if (OffsetsGV) {
+      Value *Indices[] = {ConstantInt::get(Int64Ty, 0),
+                          ConstantInt::get(Int64Ty, 1)};
+      OffsetsPtr = AtExit->CreateInBoundsGEP(OffsetsGV->getValueType(),
+                                             OffsetsGV, Indices);
+    } else {
+      OffsetsPtr = Constant::getNullValue(PtrTy);
+    }
+    Value *NF = ConstantInt::get(Int32Ty, NumFields);
+    Value *RetSizeVal = ConstantInt::get(Int32Ty, RetByteSize);
+
+    AtExit->CreateCall(SanCovTraceRetFunc,
+                       {PC, RetSizeVal, RetPtr, OffsetsPtr, NF});
+  }
+}

>From 5f654d1828664e4fc8982c414690c8ed7289b750 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Sun, 7 Jun 2026 20:10:38 +0200
Subject: [PATCH 2/8] [SanitizerCoverage] Add clang tests for
 trace-args/trace-ret

- clang/test/Driver/fsanitize-coverage.c: verify flag parsing and
  edge coverage implication
- clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c: end-to-end
  C source to callback emission verification

Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
 .../sanitizer-coverage-trace-args-ret.c        | 18 ++++++++++++++++++
 clang/test/Driver/fsanitize-coverage.c         | 13 +++++++++++++
 2 files changed, 31 insertions(+)
 create mode 100644 clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c

diff --git a/clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c b/clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c
new file mode 100644
index 0000000000000..f8114c310668f
--- /dev/null
+++ b/clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c
@@ -0,0 +1,18 @@
+// Test that -fsanitize-coverage=trace-args and trace-ret emit the expected callbacks.
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -fsanitize-coverage-trace-args -fsanitize-coverage-type=3 -debug-info-kind=limited %s -o - | FileCheck %s --check-prefix=CHECK-ARGS
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -fsanitize-coverage-trace-ret -fsanitize-coverage-type=3 -debug-info-kind=limited %s -o - | FileCheck %s --check-prefix=CHECK-RET
+
+struct Foo {
+  int a;
+  long b;
+};
+
+void takes_struct_ptr(struct Foo *f) {
+}
+
+int returns_scalar(int x) {
+  return x + 1;
+}
+
+// CHECK-ARGS: call void @__sanitizer_cov_trace_args
+// CHECK-RET: call void @__sanitizer_cov_trace_ret
diff --git a/clang/test/Driver/fsanitize-coverage.c b/clang/test/Driver/fsanitize-coverage.c
index 21e2c16bfb1b7..f6d4696f7040e 100644
--- a/clang/test/Driver/fsanitize-coverage.c
+++ b/clang/test/Driver/fsanitize-coverage.c
@@ -170,3 +170,16 @@
 // CHECK-NO-SHADOWCALLSTACK-NOT: unknown argument
 // CHECK-NO-SHADOWCALLSTACK-NOT: -fsanitize=shadow-call-stack
 // CHECK-NO-SHADOWCALLSTACK: -fsanitize-coverage-trace-pc-guard
+
+// RUN: %clang --target=x86_64-linux-gnu -fsanitize-coverage=trace-args %s -### 2>&1 | FileCheck %s --check-prefix=CHECK_DATAFLOW_ARGS
+// CHECK_DATAFLOW_ARGS: -fsanitize-coverage-type=3
+// CHECK_DATAFLOW_ARGS: -fsanitize-coverage-trace-args
+
+// RUN: %clang --target=x86_64-linux-gnu -fsanitize-coverage=trace-ret %s -### 2>&1 | FileCheck %s --check-prefix=CHECK_DATAFLOW_RET
+// CHECK_DATAFLOW_RET: -fsanitize-coverage-type=3
+// CHECK_DATAFLOW_RET: -fsanitize-coverage-trace-ret
+
+// RUN: %clang --target=x86_64-linux-gnu -fsanitize-coverage=edge,trace-args,trace-ret %s -### 2>&1 | FileCheck %s --check-prefix=CHECK_DATAFLOW_BOTH
+// CHECK_DATAFLOW_BOTH: -fsanitize-coverage-type=3
+// CHECK_DATAFLOW_BOTH: -fsanitize-coverage-trace-args
+// CHECK_DATAFLOW_BOTH: -fsanitize-coverage-trace-ret
\ No newline at end of file

>From a5998bbad7bfcb45d9542a3c9ce0827035e561a4 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Sun, 7 Jun 2026 20:10:45 +0200
Subject: [PATCH 3/8] [SanitizerCoverage] Add LLVM IR tests for
 trace-args/trace-ret

- trace-args.ll: verifies callback insertion for scalar and struct
  pointer arguments with field offset array generation
- trace-ret.ll: verifies return value callback insertion at all
  exit points via EscapeEnumerator

Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
 .../SanitizerCoverage/trace-args.ll           | 43 ++++++++++++++
 .../SanitizerCoverage/trace-ret.ll            | 59 +++++++++++++++++++
 2 files changed, 102 insertions(+)
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll

diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
new file mode 100644
index 0000000000000..d6c91be2c5c26
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
@@ -0,0 +1,43 @@
+; Test sanitizer coverage trace-args instrumentation.
+; Verifies that __sanitizer_cov_trace_args is called for struct pointer and scalar args.
+
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-args -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+%struct.MyStruct = type { i32, i64 }
+
+define void @func_with_args(ptr %s, i32 %x) #0 !dbg !8 {
+entry:
+  ret void
+}
+
+; CHECK: define void @func_with_args(ptr %s, i32 %x)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @func_with_args to i64), i32 0, i32 8, ptr %s, ptr getelementptr inbounds ([5 x i64], ptr @__sancov_offsets_{{.*}}, i64 0, i64 1), i32 2)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @func_with_args to i64), i32 1, i32 4, ptr %{{.*}}, ptr null, i32 0)
+; CHECK: ret void
+
+attributes #0 = { nounwind sanitize_address }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, isOptimized: false, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/tmp")
+!2 = !{}
+!3 = !{i32 2, !"Dwarf Version", i32 4}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+
+; struct MyStruct { int a; long b; }
+!5 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!6 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+!7 = !DICompositeType(tag: DW_TAG_structure_type, name: "MyStruct", size: 128, elements: !14)
+!8 = distinct !DISubprogram(name: "func_with_args", scope: !1, file: !1, line: 5, type: !9, unit: !0, retainedNodes: !2)
+!9 = !DISubroutineType(types: !10)
+; types: [ret=void, arg0=ptr to MyStruct, arg1=int]
+!10 = !{null, !11, !5}
+!11 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !7, size: 64)
+!12 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !7, file: !1, baseType: !5, size: 32, offset: 0)
+!13 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !7, file: !1, baseType: !6, size: 64, offset: 64)
+!14 = !{!12, !13}
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
new file mode 100644
index 0000000000000..3e2fbdcb3c1fd
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
@@ -0,0 +1,59 @@
+; Test sanitizer coverage trace-ret instrumentation.
+; Verifies that __sanitizer_cov_trace_ret is called for struct pointer and scalar returns.
+
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-ret -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+%struct.MyStruct = type { i32, i64 }
+
+define ptr @func_ret_struct_ptr(ptr %s) #0 !dbg !8 {
+entry:
+  ret ptr %s
+}
+
+; CHECK: define ptr @func_ret_struct_ptr(ptr %s)
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @func_ret_struct_ptr to i64), i32 8, ptr %s, ptr getelementptr inbounds ([5 x i64], ptr @__sancov_offsets_{{.*}}, i64 0, i64 1), i32 2)
+; CHECK: ret ptr %s
+
+define i32 @func_ret_scalar(i32 %x) #0 !dbg !15 {
+entry:
+  ret i32 %x
+}
+
+; CHECK: define i32 @func_ret_scalar(i32 %x)
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @func_ret_scalar to i64), i32 4, ptr %{{.*}}, ptr null, i32 0)
+; CHECK: ret i32 %x
+
+attributes #0 = { nounwind sanitize_address }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, isOptimized: false, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/tmp")
+!2 = !{}
+!3 = !{i32 2, !"Dwarf Version", i32 4}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+
+; struct MyStruct { int a; long b; }
+!5 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!6 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+!7 = !DICompositeType(tag: DW_TAG_structure_type, name: "MyStruct", size: 128, elements: !14)
+
+; func_ret_struct_ptr returns ptr to MyStruct
+!8 = distinct !DISubprogram(name: "func_ret_struct_ptr", scope: !1, file: !1, line: 5, type: !9, unit: !0, retainedNodes: !2)
+!9 = !DISubroutineType(types: !10)
+; types: [ret=ptr to MyStruct, arg0=ptr to MyStruct]
+!10 = !{!11, !11}
+!11 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !7, size: 64)
+!12 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !7, file: !1, baseType: !5, size: 32, offset: 0)
+!13 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !7, file: !1, baseType: !6, size: 64, offset: 64)
+!14 = !{!12, !13}
+
+; func_ret_scalar returns i32
+!15 = distinct !DISubprogram(name: "func_ret_scalar", scope: !1, file: !1, line: 10, type: !16, unit: !0, retainedNodes: !2)
+!16 = !DISubroutineType(types: !17)
+; types: [ret=int, arg0=int]
+!17 = !{!5, !5}

>From 7354ce1e7de115362b9977060a939b511ca1f6c3 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Wed, 24 Jun 2026 19:46:26 +0200
Subject: [PATCH 4/8] [SanitizerCoverage] fix arg index misalignment from ABI
 lowering
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The previous implementation iterated F.args() sequentially and indexed
into DISubroutineType::TypeArray, assuming a 1:1 correspondence between
IR-level and source-level arguments. This broke in three cases:

1. sret (large struct return): Clang/rustc inserts a hidden sret ptr
   as the first IR argument, shifting all subsequent arg indices.

2. Struct decomposition: Small structs may be split into multiple IR
   arguments (e.g., struct {int x; int y} → i32 %s.0, i32 %s.1),
   causing one source param to map to N IR args.

3. C++ this pointer: Hidden first argument with no source-level entry
   in the type array.

Fix: Use DILocalVariable debug records (with getArg()) to map IR values
back to source-level parameters. DILocalVariable::getArg() is stable
across all ABI transformations because it is set by the frontend before
any lowering.

For struct decomposition, detect multiple IR values sharing the same
DILocalVariable (via DW_OP_LLVM_fragment expressions) and reassemble
them into a stack slot matching the source layout.

Falls back to the original TypeArray method when debug info is absent.

Tested:
- C: make_big(int, int) → struct big (sret skipped, 2 correct traces)
- C: use_small(struct small, int) (coerced i64 → reassembled, 2 traces)
- Rust: same patterns via rustc (sret, struct split, pointer args)

Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
 .../Instrumentation/SanitizerCoverage.cpp     | 248 ++++++++++++++----
 .../SanitizerCoverage/trace-args-abi.ll       |  86 ++++++
 2 files changed, 289 insertions(+), 45 deletions(-)
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll

diff --git a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
index b6d9d6d7b6f33..fdf44c2b4d93c 100644
--- a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
+++ b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
@@ -20,6 +20,7 @@
 #include "llvm/IR/Constants.h"
 #include "llvm/IR/DataLayout.h"
 #include "llvm/IR/DebugInfoMetadata.h"
+#include "llvm/IR/DebugInfo.h"
 #include "llvm/IR/Dominators.h"
 #include "llvm/IR/EHPersonalities.h"
 #include "llvm/IR/Function.h"
@@ -1321,9 +1322,9 @@ static DIType *stripDITypedefs(DIType *Ty) {
   return Ty;
 }
 
-// Helper: If Ty is a pointer to a struct (DICompositeType), collect byte
-// offsets of all scalar members. Returns the offsets array global and
-// num_fields.
+// Helper: If Ty is a pointer to a struct (DICompositeType) or a struct
+// directly, collect byte offsets of all scalar members. Returns the offsets
+// array global and num_fields.
 static std::pair<GlobalVariable *, unsigned>
 getStructFieldOffsets(DIType *Ty, Module &M, const DataLayout &DL) {
   if (!Ty)
@@ -1331,13 +1332,19 @@ getStructFieldOffsets(DIType *Ty, Module &M, const DataLayout &DL) {
 
   Ty = stripDITypedefs(Ty);
 
-  // Must be a pointer to something
-  auto *PtrTy = dyn_cast_or_null<DIDerivedType>(Ty);
-  if (!PtrTy || PtrTy->getTag() != dwarf::DW_TAG_pointer_type)
-    return {nullptr, 0};
+  DICompositeType *Composite = nullptr;
+
+  // Case 1: pointer to struct
+  if (auto *PtrTy = dyn_cast_or_null<DIDerivedType>(Ty)) {
+    if (PtrTy->getTag() == dwarf::DW_TAG_pointer_type) {
+      DIType *PointeeTy = stripDITypedefs(PtrTy->getBaseType());
+      Composite = dyn_cast_or_null<DICompositeType>(PointeeTy);
+    }
+  }
+  // Case 2: direct struct type (for reassembled by-value args)
+  if (!Composite)
+    Composite = dyn_cast_or_null<DICompositeType>(Ty);
 
-  DIType *PointeeTy = stripDITypedefs(PtrTy->getBaseType());
-  auto *Composite = dyn_cast_or_null<DICompositeType>(PointeeTy);
   if (!Composite || Composite->getTag() != dwarf::DW_TAG_structure_type)
     return {nullptr, 0};
 
@@ -1400,55 +1407,206 @@ void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
   // Get PC as the function address cast to i64
   Value *PC = IRB.CreatePtrToInt(&F, Int64Ty);
 
-  // For each argument, emit a trace call
-  unsigned ArgIdx = 0;
+  // Build source-level argument map from debug variable records.
+  // DILocalVariable::getArg() gives the 1-based source parameter number,
+  // which is ABI-stable: unaffected by sret insertion or struct decomposition.
+  //
+  // We scan ALL debug records in the entry block because ABI lowering may
+  // decompose one source argument into derived values (e.g., trunc of a
+  // coerced i64 into two i32 fragments). findDbgValues on the Argument
+  // alone would miss those derived values.
+  struct SourceArg {
+    DILocalVariable *Var = nullptr;
+    DIType *Ty = nullptr;
+    SmallVector<std::pair<Value *, uint64_t>, 2> Fragments; // {val, bit_off}
+  };
+  DenseMap<unsigned, SourceArg> SrcArgs;
+
+  // Pass 1: direct argument debug records (batched scan)
+  SmallVector<DbgVariableRecord *, 16> AllDVRs;
   for (auto &Arg : F.args()) {
-    // Get debug info for this argument
-    DIType *ArgDIType = nullptr;
-    if (SP->getType()) {
-      auto *SubroutineType = SP->getType();
-      auto TypeArray = SubroutineType->getTypeArray();
-      // TypeArray[0] is return type, TypeArray[1..] are params
-      if (ArgIdx + 1 < TypeArray.size())
-        ArgDIType = TypeArray[ArgIdx + 1];
+    AllDVRs.clear();
+    findDbgValues(&Arg, AllDVRs);
+    for (auto *DVR : AllDVRs) {
+      DILocalVariable *Var = DVR->getVariable();
+      if (!Var || !Var->getArg())
+        continue;
+      unsigned SrcIdx = Var->getArg();
+      auto &SA = SrcArgs[SrcIdx];
+      SA.Var = Var;
+      SA.Ty = Var->getType();
+      uint64_t FragBitOff = 0;
+      if (auto Frag = DVR->getExpression()->getFragmentInfo())
+        FragBitOff = Frag->OffsetInBits;
+      SA.Fragments.push_back({&Arg, FragBitOff});
+    }
+  }
+
+  // Pass 2: scan entry block for debug records on derived values
+  // (handles struct coercion where debug info points at trunc/extract, not arg)
+  for (auto &I : EntryBB) {
+    for (auto &DVR : I.getDbgRecordRange()) {
+      auto *DVar = dyn_cast<DbgVariableRecord>(&DVR);
+      if (!DVar)
+        continue;
+      DILocalVariable *Var = DVar->getVariable();
+      if (!Var || !Var->getArg())
+        continue;
+      unsigned SrcIdx = Var->getArg();
+      // Skip if pass 1 already found a direct argument reference for this param
+      auto It = SrcArgs.find(SrcIdx);
+      if (It != SrcArgs.end() && !It->second.Fragments.empty() &&
+          isa<Argument>(It->second.Fragments[0].first))
+        continue;
+      Value *V = DVar->getValue();
+      if (!V || isa<Argument>(V))
+        continue; // Direct args handled in pass 1
+      auto &SA = SrcArgs[SrcIdx];
+      SA.Var = Var;
+      SA.Ty = Var->getType();
+      uint64_t FragBitOff = 0;
+      if (auto Frag = DVar->getExpression()->getFragmentInfo())
+        FragBitOff = Frag->OffsetInBits;
+      SA.Fragments.push_back({V, FragBitOff});
+    }
+  }
+
+  // Fallback: if no debug records found (compiled without -g or stripped),
+  // use the original TypeArray-based indexing.
+  if (SrcArgs.empty()) {
+    unsigned ArgIdx = 0;
+    for (auto &Arg : F.args()) {
+      // Skip ABI-inserted hidden args that don't correspond to source params
+      if (Arg.hasStructRetAttr())
+        continue;
+      DIType *ArgDIType = nullptr;
+      if (SP->getType()) {
+        auto TypeArray = SP->getType()->getTypeArray();
+        if (ArgIdx + 1 < TypeArray.size())
+          ArgDIType = TypeArray[ArgIdx + 1];
+      }
+      auto [OffsetsGV, NumFields] = getStructFieldOffsets(ArgDIType, M, *DL);
+      Value *ArgPtr;
+      if (Arg.getType()->isPointerTy()) {
+        ArgPtr = &Arg;
+      } else {
+        AllocaInst *Alloca = IRB.CreateAlloca(Arg.getType());
+        IRB.CreateStore(&Arg, Alloca);
+        ArgPtr = Alloca;
+      }
+      unsigned ArgByteSize = Arg.getType()->isPointerTy()
+                                 ? DL->getPointerSize()
+                                 : DL->getTypeStoreSize(Arg.getType());
+      Value *OffsetsPtr = Constant::getNullValue(PtrTy);
+      if (OffsetsGV) {
+        Value *Indices[] = {ConstantInt::get(Int64Ty, 0),
+                            ConstantInt::get(Int64Ty, 1)};
+        OffsetsPtr = IRB.CreateInBoundsGEP(OffsetsGV->getValueType(),
+                                           OffsetsGV, Indices);
+      }
+      IRB.CreateCall(SanCovTraceArgsFunc,
+                     {PC, ConstantInt::get(Int32Ty, ArgIdx),
+                      ConstantInt::get(Int32Ty, ArgByteSize), ArgPtr,
+                      OffsetsPtr, ConstantInt::get(Int32Ty, NumFields)});
+      ArgIdx++;
     }
+    return;
+  }
+
+  // Emit one trace call per source-level argument, sorted by source position.
+  // Place trace calls before the entry block terminator so all values dominate.
+  SmallVector<unsigned, 8> SortedKeys;
+  for (auto &[K, _] : SrcArgs)
+    SortedKeys.push_back(K);
+  llvm::sort(SortedKeys);
 
-    auto [OffsetsGV, NumFields] = getStructFieldOffsets(ArgDIType, M, *DL);
+  IRBuilder<> TraceIRB(EntryBB.getTerminator());
+
+  for (unsigned SrcIdx : SortedKeys) {
+    auto &SA = SrcArgs[SrcIdx];
+    auto [OffsetsGV, NumFields] = getStructFieldOffsets(SA.Ty, M, *DL);
 
     Value *ArgPtr;
-    if (Arg.getType()->isPointerTy()) {
-      ArgPtr = &Arg;
+    unsigned ArgByteSize;
+
+    if (SA.Fragments.size() == 1) {
+      // Single IR arg for this source param (common case: pointers, scalars)
+      Value *V = SA.Fragments[0].first;
+      if (V->getType()->isPointerTy()) {
+        ArgPtr = V;
+        ArgByteSize = DL->getPointerSize();
+      } else {
+        AllocaInst *Alloca = IRB.CreateAlloca(V->getType());
+        TraceIRB.CreateStore(V, Alloca);
+        ArgPtr = Alloca;
+        ArgByteSize = DL->getTypeStoreSize(V->getType());
+      }
     } else {
-      // Spill non-pointer arg to stack so we can pass its address
-      AllocaInst *Alloca = IRB.CreateAlloca(Arg.getType());
-      IRB.CreateStore(&Arg, Alloca);
-      ArgPtr = Alloca;
+      // Multiple IR args for one source param (ABI struct decomposition).
+      // Reassemble fragments into a stack slot matching the source layout.
+      unsigned TotalBits = 0;
+      for (auto &[V, BitOff] : SA.Fragments) {
+        unsigned End = BitOff + DL->getTypeSizeInBits(V->getType());
+        if (End > TotalBits)
+          TotalBits = End;
+      }
+      unsigned TotalBytes = (TotalBits + 7) / 8;
+      AllocaInst *Slot =
+          IRB.CreateAlloca(ArrayType::get(IRB.getInt8Ty(), TotalBytes));
+      Slot->setAlignment(Align(8));
+      TraceIRB.CreateMemSet(Slot, TraceIRB.getInt8(0), TotalBytes,
+                            Slot->getAlign());
+      for (auto &[V, BitOff] : SA.Fragments) {
+        unsigned ByteOff = BitOff / 8;
+        Value *Ptr = TraceIRB.CreateGEP(TraceIRB.getInt8Ty(), Slot,
+                                        ConstantInt::get(Int32Ty, ByteOff));
+        TraceIRB.CreateAlignedStore(V, Ptr, Align(1));
+      }
+      ArgPtr = Slot;
+      ArgByteSize = TotalBytes;
     }
 
-    // Compute arg byte size
-    unsigned ArgByteSize = Arg.getType()->isPointerTy()
-                               ? DL->getPointerSize()
-                               : DL->getTypeStoreSize(Arg.getType());
-
-    // OffsetsGV layout: [hash, off0, sz0, off1, sz1, ...]
-    // Pass pointer to &array[1] so kernel sees field data at offsets[0],
-    // and can read offsets[-1] for the type hash.
-    Value *OffsetsPtr;
+    Value *OffsetsPtr = Constant::getNullValue(PtrTy);
     if (OffsetsGV) {
       Value *Indices[] = {ConstantInt::get(Int64Ty, 0),
                           ConstantInt::get(Int64Ty, 1)};
-      OffsetsPtr =
-          IRB.CreateInBoundsGEP(OffsetsGV->getValueType(), OffsetsGV, Indices);
-    } else {
-      OffsetsPtr = Constant::getNullValue(PtrTy);
+      OffsetsPtr = TraceIRB.CreateInBoundsGEP(OffsetsGV->getValueType(),
+                                              OffsetsGV, Indices);
     }
-    Value *NF = ConstantInt::get(Int32Ty, NumFields);
-    Value *ArgIdxVal = ConstantInt::get(Int32Ty, ArgIdx);
-    Value *ArgSizeVal = ConstantInt::get(Int32Ty, ArgByteSize);
+    // Report 0-based source arg index
+    TraceIRB.CreateCall(SanCovTraceArgsFunc,
+                        {PC, ConstantInt::get(Int32Ty, SrcIdx - 1),
+                         ConstantInt::get(Int32Ty, ArgByteSize), ArgPtr,
+                         OffsetsPtr, ConstantInt::get(Int32Ty, NumFields)});
+  }
 
-    IRB.CreateCall(SanCovTraceArgsFunc,
-                   {PC, ArgIdxVal, ArgSizeVal, ArgPtr, OffsetsPtr, NF});
-    ArgIdx++;
+  // Dead-arg fallback: if the DISubroutineType indicates more source params
+  // than we found via debug records (e.g., arg optimized away entirely at -O2),
+  // emit a null-pointer trace so consumers know the argument existed.
+  if (SP->getType()) {
+    auto TypeArray = SP->getType()->getTypeArray();
+    unsigned NumSrcParams = TypeArray.size() > 0 ? TypeArray.size() - 1 : 0;
+    for (unsigned I = 1; I <= NumSrcParams; ++I) {
+      if (!SrcArgs.count(I)) {
+        // This source param had no debug record — likely optimized away
+        DIType *ArgDIType = TypeArray[I];
+        auto [OffsetsGV, NumFields] = getStructFieldOffsets(ArgDIType, M, *DL);
+        Value *OffsetsPtr = Constant::getNullValue(PtrTy);
+        if (OffsetsGV) {
+          Value *Indices[] = {ConstantInt::get(Int64Ty, 0),
+                              ConstantInt::get(Int64Ty, 1)};
+          OffsetsPtr = TraceIRB.CreateInBoundsGEP(OffsetsGV->getValueType(),
+                                                  OffsetsGV, Indices);
+        }
+        // Pass null pointer — kernel will record 0xBADADD85 for all fields
+        TraceIRB.CreateCall(
+            SanCovTraceArgsFunc,
+            {PC, ConstantInt::get(Int32Ty, I - 1),
+             ConstantInt::get(Int32Ty, 0),
+             Constant::getNullValue(PtrTy), OffsetsPtr,
+             ConstantInt::get(Int32Ty, NumFields)});
+      }
+    }
   }
 }
 
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll
new file mode 100644
index 0000000000000..f1dc56476abda
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll
@@ -0,0 +1,86 @@
+; Test trace-args handles ABI-inserted hidden arguments correctly.
+; Verifies:
+; 1. sret hidden arg is skipped (not traced)
+; 2. Struct coercion fragments are reassembled
+; 3. Normal pointer arg with struct offsets still works
+
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-args -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+%struct.big = type { i64, i64, i64, i64, i64 }
+%struct.small = type { i32, i32 }
+
+; Function with sret: source has 2 params (x, y), IR has 3 args (sret, x, y)
+define void @make_big(ptr sret(%struct.big) %0, i32 %1, i32 %2) #0 !dbg !20 {
+entry:
+    #dbg_value(i32 %1, !30, !DIExpression(), !32)
+    #dbg_value(i32 %2, !31, !DIExpression(), !32)
+  ret void
+}
+
+; CHECK-LABEL: define void @make_big(ptr sret(%struct.big) %0, i32 %1, i32 %2)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @make_big to i64), i32 0, i32 4, ptr %{{.*}}, ptr null, i32 0)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @make_big to i64), i32 1, i32 4, ptr %{{.*}}, ptr null, i32 0)
+; CHECK-NOT: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @make_big to i64), i32 2
+; CHECK: ret void
+
+; Function with struct coercion: source has 2 params (s, z), IR has 2 args (i64, i32)
+; but debug info says s is split into fragments at bit offsets 0 and 32
+define i32 @use_small(i64 %0, i32 %1) #0 !dbg !40 {
+entry:
+  %3 = trunc i64 %0 to i32
+  %4 = lshr i64 %0, 32
+  %5 = trunc nuw i64 %4 to i32
+    #dbg_value(i32 %3, !50, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !52)
+    #dbg_value(i32 %5, !50, !DIExpression(DW_OP_LLVM_fragment, 32, 32), !52)
+    #dbg_value(i32 %1, !51, !DIExpression(), !52)
+  %6 = add i32 %1, %3
+  %7 = add i32 %6, %5
+  ret i32 %7
+}
+
+; CHECK-LABEL: define i32 @use_small(i64 %0, i32 %1)
+; Two trace calls: arg 0 = reassembled struct (8 bytes) with field offsets, arg 1 = z (4 bytes)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_small to i64), i32 0, i32 8, ptr %{{.*}}, ptr getelementptr inbounds {{.*}}, i32 2)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_small to i64), i32 1, i32 4, ptr %{{.*}}, ptr null, i32 0)
+; CHECK: ret i32
+
+attributes #0 = { nounwind }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, isOptimized: true, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test_abi.c", directory: "/tmp")
+!2 = !{}
+!3 = !{i32 2, !"Dwarf Version", i32 4}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+
+; Types
+!5 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!6 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+
+; make_big debug info
+!20 = distinct !DISubprogram(name: "make_big", scope: !1, file: !1, line: 3, type: !21, scopeLine: 3, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !29)
+!21 = !DISubroutineType(types: !22)
+!22 = !{!23, !5, !5}  ; returns struct big, params: int, int
+!23 = !DICompositeType(tag: DW_TAG_structure_type, name: "big", size: 320, elements: !2)
+!29 = !{!30, !31}
+!30 = !DILocalVariable(name: "x", arg: 1, scope: !20, file: !1, line: 3, type: !5)
+!31 = !DILocalVariable(name: "y", arg: 2, scope: !20, file: !1, line: 3, type: !5)
+!32 = !DILocation(line: 3, scope: !20)
+
+; use_small debug info
+!40 = distinct !DISubprogram(name: "use_small", scope: !1, file: !1, line: 8, type: !41, scopeLine: 8, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !49)
+!41 = !DISubroutineType(types: !42)
+!42 = !{!5, !43, !5}  ; returns int, params: struct small, int
+!43 = !DICompositeType(tag: DW_TAG_structure_type, name: "small", size: 64, elements: !44)
+!44 = !{!45, !46}
+!45 = !DIDerivedType(tag: DW_TAG_member, name: "x", scope: !43, baseType: !5, size: 32, offset: 0)
+!46 = !DIDerivedType(tag: DW_TAG_member, name: "y", scope: !43, baseType: !5, size: 32, offset: 32)
+!49 = !{!50, !51}
+!50 = !DILocalVariable(name: "s", arg: 1, scope: !40, file: !1, line: 8, type: !43)
+!51 = !DILocalVariable(name: "z", arg: 2, scope: !40, file: !1, line: 8, type: !5)
+!52 = !DILocation(line: 8, scope: !40)

>From c1c9f138623d8c53669e85a1363de1631f284a12 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Tue, 7 Jul 2026 16:06:08 +0200
Subject: [PATCH 5/8] [SanitizerCoverage] Fix -g verifier failure and capture
 sret returns

Two defects in the trace-args/trace-ret instrumentation, both surfacing
in exactly the -g configuration the feature requires:

1. Missing !dbg on argument trace calls (verifier failure under -g/LTO).
   InjectTraceForArgs emitted the per-argument callbacks through a plain
   IRBuilder anchored at the entry-block terminator. In a function that
   carries debug info, a call to a non-intrinsic callee with no !dbg
   location fails the verifier ("inlinable function call ... requires a
   !dbg location") once inlining/LTO runs. The argument path is the only
   place that did this: the fallback (no-debug-info) path and the return
   path both already go through InstrumentationIRBuilder, which stamps a
   synthetic location. Switch the argument path to InstrumentationIRBuilder
   as well so the callbacks inherit a location.

2. Indirect (sret) returns were dropped. A struct returned by value may be
   lowered to an indirect return: the IR function returns void and writes
   the result into a caller-provided buffer passed as a hidden sret pointer.
   InjectTraceForRet saw a valueless ReturnInst and emitted a null trace, so
   the return value was lost -- asymmetric with the argument path, which
   deliberately skips the same sret pointer as a non-source argument. When
   the return is void and the function has an sret argument, trace that
   buffer instead, using the source struct's field offsets (already the
   return DIType) and the full struct store size.

Also de-duplicate fragments by (value, bit-offset) when collecting argument
debug records. The same #dbg_value can be reached more than once (via the
argument in pass 1 and again while scanning the block in pass 2, or
duplicated by an earlier pass); without de-duplication a single scalar
argument with a repeated record would look like a multi-fragment struct and
be needlessly reassembled.

Tests:
- trace-args-abi.ll: assert the argument trace calls carry a !dbg location.
- trace-ret.ll: add func_ret_sret, verifying the sret buffer is traced as
  the return value with the struct's field offsets and 24-byte size.

Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
 .../Instrumentation/SanitizerCoverage.cpp     | 44 +++++++++++++++++--
 .../SanitizerCoverage/trace-args-abi.ll       |  6 ++-
 .../SanitizerCoverage/trace-ret.ll            | 24 ++++++++++
 3 files changed, 69 insertions(+), 5 deletions(-)

diff --git a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
index fdf44c2b4d93c..df96c8141a55a 100644
--- a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
+++ b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
@@ -1422,6 +1422,19 @@ void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
   };
   DenseMap<unsigned, SourceArg> SrcArgs;
 
+  // Record a fragment for a source parameter, ignoring exact (value, offset)
+  // duplicates. The same #dbg_value can be reached more than once (found via
+  // the argument in pass 1 and again while walking the block in pass 2, or
+  // simply duplicated by an earlier pass); without this a single scalar arg
+  // with a repeated record would look like a multi-fragment struct and be
+  // needlessly reassembled.
+  auto addFragment = [](SourceArg &SA, Value *V, uint64_t BitOff) {
+    for (const auto &Existing : SA.Fragments)
+      if (Existing.first == V && Existing.second == BitOff)
+        return;
+    SA.Fragments.push_back({V, BitOff});
+  };
+
   // Pass 1: direct argument debug records (batched scan)
   SmallVector<DbgVariableRecord *, 16> AllDVRs;
   for (auto &Arg : F.args()) {
@@ -1438,7 +1451,7 @@ void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
       uint64_t FragBitOff = 0;
       if (auto Frag = DVR->getExpression()->getFragmentInfo())
         FragBitOff = Frag->OffsetInBits;
-      SA.Fragments.push_back({&Arg, FragBitOff});
+      addFragment(SA, &Arg, FragBitOff);
     }
   }
 
@@ -1467,7 +1480,7 @@ void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
       uint64_t FragBitOff = 0;
       if (auto Frag = DVar->getExpression()->getFragmentInfo())
         FragBitOff = Frag->OffsetInBits;
-      SA.Fragments.push_back({V, FragBitOff});
+      addFragment(SA, V, FragBitOff);
     }
   }
 
@@ -1520,7 +1533,11 @@ void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
     SortedKeys.push_back(K);
   llvm::sort(SortedKeys);
 
-  IRBuilder<> TraceIRB(EntryBB.getTerminator());
+  // Use InstrumentationIRBuilder so the inserted calls inherit a synthetic
+  // !dbg location. In a function that carries debug info (which this pass
+  // requires), a plain IRBuilder would emit callee-bearing calls with no
+  // location and trip the verifier under -g and LTO.
+  InstrumentationIRBuilder TraceIRB(EntryBB.getTerminator());
 
   for (unsigned SrcIdx : SortedKeys) {
     auto &SA = SrcArgs[SrcIdx];
@@ -1623,6 +1640,20 @@ void ModuleSanitizerCoverage::InjectTraceForRet(Function &F) {
 
   auto [OffsetsGV, NumFields] = getStructFieldOffsets(RetDIType, M, *DL);
 
+  // A struct returned by value may be lowered to an indirect return: the IR
+  // function returns void and writes the result into a caller-provided buffer
+  // passed as a hidden `sret` pointer. In that case the ReturnInst carries no
+  // value, so trace the sret buffer instead. This keeps the return path
+  // symmetric with the argument path, which deliberately skips the same sret
+  // pointer as a non-source argument.
+  Argument *SRetArg = nullptr;
+  for (Argument &A : F.args()) {
+    if (A.hasStructRetAttr()) {
+      SRetArg = &A;
+      break;
+    }
+  }
+
   EscapeEnumerator EE(F, "sancov_trace_ret");
   while (IRBuilder<> *AtExit = EE.Next()) {
     InstrumentationIRBuilder::ensureDebugInfo(*AtExit, F);
@@ -1645,6 +1676,13 @@ void ModuleSanitizerCoverage::InjectTraceForRet(Function &F) {
       AtExit->CreateStore(RetVal, Alloca);
       RetPtr = Alloca;
       RetByteSize = DL->getTypeStoreSize(RetVal->getType());
+    } else if (SRetArg) {
+      // Indirect (sret) return: the value lives in the caller-provided buffer.
+      RetPtr = SRetArg;
+      if (Type *ElemTy = SRetArg->getParamStructRetType())
+        RetByteSize = DL->getTypeStoreSize(ElemTy);
+      else
+        RetByteSize = DL->getPointerSize();
     } else {
       RetPtr = Constant::getNullValue(PtrTy);
     }
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll
index f1dc56476abda..5086176c3a363 100644
--- a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll
@@ -21,8 +21,10 @@ entry:
 }
 
 ; CHECK-LABEL: define void @make_big(ptr sret(%struct.big) %0, i32 %1, i32 %2)
-; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @make_big to i64), i32 0, i32 4, ptr %{{.*}}, ptr null, i32 0)
-; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @make_big to i64), i32 1, i32 4, ptr %{{.*}}, ptr null, i32 0)
+; Trace calls must carry a !dbg location: this function has debug info, so a
+; call without one would fail the verifier under -g/LTO.
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @make_big to i64), i32 0, i32 4, ptr %{{.*}}, ptr null, i32 0), !dbg !{{[0-9]+}}
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @make_big to i64), i32 1, i32 4, ptr %{{.*}}, ptr null, i32 0), !dbg !{{[0-9]+}}
 ; CHECK-NOT: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @make_big to i64), i32 2
 ; CHECK: ret void
 
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
index 3e2fbdcb3c1fd..f5fd01776acc2 100644
--- a/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
@@ -7,6 +7,7 @@ target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:
 target triple = "x86_64-unknown-linux-gnu"
 
 %struct.MyStruct = type { i32, i64 }
+%struct.Big = type { i64, i64, i64 }
 
 define ptr @func_ret_struct_ptr(ptr %s) #0 !dbg !8 {
 entry:
@@ -26,6 +27,18 @@ entry:
 ; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @func_ret_scalar to i64), i32 4, ptr %{{.*}}, ptr null, i32 0)
 ; CHECK: ret i32 %x
 
+; Struct returned by value, lowered to an indirect (sret) return: the IR
+; returns void, so the sret buffer (arg 0) must be traced as the return value
+; with the source struct's field offsets and full struct size (24 bytes).
+define void @func_ret_sret(ptr sret(%struct.Big) %0) #0 !dbg !18 {
+entry:
+  ret void
+}
+
+; CHECK: define void @func_ret_sret(ptr sret(%struct.Big) %0)
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @func_ret_sret to i64), i32 24, ptr %0, ptr getelementptr inbounds ([7 x i64], ptr @__sancov_offsets_{{.*}}, i64 0, i64 1), i32 3)
+; CHECK: ret void
+
 attributes #0 = { nounwind sanitize_address }
 
 !llvm.dbg.cu = !{!0}
@@ -57,3 +70,14 @@ attributes #0 = { nounwind sanitize_address }
 !16 = !DISubroutineType(types: !17)
 ; types: [ret=int, arg0=int]
 !17 = !{!5, !5}
+
+; func_ret_sret returns struct Big { long a; long b; long c; } by value
+!18 = distinct !DISubprogram(name: "func_ret_sret", scope: !1, file: !1, line: 15, type: !19, unit: !0, retainedNodes: !2)
+!19 = !DISubroutineType(types: !20)
+; types: [ret=struct Big, arg=struct Big] (arg is the source-level return, no sret entry)
+!20 = !{!21, !21}
+!21 = !DICompositeType(tag: DW_TAG_structure_type, name: "Big", size: 192, elements: !25)
+!22 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !21, file: !1, baseType: !6, size: 64, offset: 0)
+!23 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !21, file: !1, baseType: !6, size: 64, offset: 64)
+!24 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !21, file: !1, baseType: !6, size: 64, offset: 128)
+!25 = !{!22, !23, !24}

>From e76295efa14f5b4dd944695b5c40198101162ad9 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Thu, 9 Jul 2026 17:43:00 +0200
Subject: [PATCH 6/8] [SanitizerCoverage] Fix two trace-args/ret crashes under
 whole-module instrumentation

Two codegen crashes surface when compiling a full KASAN kernel with
-fsanitize-coverage=trace-args,trace-ret applied to every translation unit:

1. SSA dominance. The entry-block #dbg_value scan in InjectTraceForArgs()
   collected an argument's value from a debug record that may reference an
   instruction defined later in the function (debug records are exempt from
   SSA dominance). Using it as the trace pointer -- the trace call is inserted
   at the entry-block terminator -- produced IR that fails the verifier
   ("Instruction does not dominate all uses") and, with -disable-llvm-verifier,
   crashed RegisterCoalescer::reMaterializeDef at codegen. Skip a derived value
   not defined in the entry block; the dead-arg fallback then traces that
   parameter as a null pointer.

2. x86 base-pointer interference. The trace-args/ret spill allocas escape (their
   address is passed to the trace call), so AddressSanitizer redzones them,
   forcing 32-byte frame realignment -> a base pointer (RBX). In a function whose
   inline asm also ties up RBX (e.g. CPUID "=b" -> {bx}, RDTSC -> ~{rbx}) the X86
   backend rejects this with "Interference usage of base pointer/frame pointer".
   functionInlineAsmUsesBasePointerX86() detects such functions and skips the
   escaping spill (traced as a null pointer) at all four sites (three in
   InjectTraceForArgs, one in InjectTraceForRet).

Add regression tests trace-args-dominance.ll and trace-ret-basepointer.ll.

Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
 .../Instrumentation/SanitizerCoverage.cpp     | 69 +++++++++++++++++--
 .../SanitizerCoverage/trace-args-dominance.ll | 50 ++++++++++++++
 .../trace-ret-basepointer.ll                  | 51 ++++++++++++++
 3 files changed, 166 insertions(+), 4 deletions(-)
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args-dominance.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-ret-basepointer.ll

diff --git a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
index df96c8141a55a..fae21bf41087d 100644
--- a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
+++ b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
@@ -26,6 +26,7 @@
 #include "llvm/IR/Function.h"
 #include "llvm/IR/GlobalVariable.h"
 #include "llvm/IR/IRBuilder.h"
+#include "llvm/IR/InlineAsm.h"
 #include "llvm/IR/IntrinsicInst.h"
 #include "llvm/IR/Intrinsics.h"
 #include "llvm/IR/LLVMContext.h"
@@ -1395,10 +1396,40 @@ getStructFieldOffsets(DIType *Ty, Module &M, const DataLayout &DL) {
   return {GV, (unsigned)(Offsets.size() / 2)};
 }
 
+// x86 uses RBX as the frame base pointer for a realigned stack. A function whose
+// inline asm reads/writes/clobbers RBX cannot also use it as the base pointer -- the
+// backend rejects it ("Interference usage of base pointer/frame pointer"). The
+// trace-args/trace-ret spill allocas below escape (their address is passed to the
+// trace call), so ASAN redzones them, forcing 32-byte frame realignment => a base
+// pointer. In a function that already ties up RBX in inline asm (e.g. CPUID's "=b",
+// RDTSC's "~{rbx}") that is a hard error. Detect it and skip the escaping spill for
+// such functions (the arg/ret is traced as a null pointer instead). The base-pointer
+// register is spelled {rbx}/{ebx}/{bx} (and byte views {bl}/{bh}) in constraint codes.
+static bool functionInlineAsmUsesBasePointerX86(const Function &F) {
+  for (const BasicBlock &BB : F)
+    for (const Instruction &I : BB) {
+      const auto *CB = dyn_cast<CallBase>(&I);
+      if (!CB || !CB->isInlineAsm())
+        continue;
+      const auto *IA = dyn_cast<InlineAsm>(CB->getCalledOperand());
+      if (!IA)
+        continue;
+      for (const InlineAsm::ConstraintInfo &CI : IA->ParseConstraints())
+        for (StringRef Code : CI.Codes)
+          if (Code == "{rbx}" || Code == "{ebx}" || Code == "{bx}" ||
+              Code == "{bl}" || Code == "{bh}")
+            return true;
+    }
+  return false;
+}
+
 void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
   DISubprogram *SP = F.getSubprogram();
   if (!SP)
     return;
+  // Only guards x86; on other targets there is no RBX base-pointer interference.
+  const bool SkipSpill = TargetTriple.getArch() == Triple::x86_64 &&
+                         functionInlineAsmUsesBasePointerX86(F);
 
   BasicBlock &EntryBB = F.getEntryBlock();
   Instruction *InsertPt = &*EntryBB.getFirstInsertionPt();
@@ -1474,6 +1505,18 @@ void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
       Value *V = DVar->getValue();
       if (!V || isa<Argument>(V))
         continue; // Direct args handled in pass 1
+      // A #dbg_value may point at a value that does NOT dominate the entry-block
+      // terminator where the trace call is inserted (debug records are exempt from
+      // SSA dominance). Using such a value as ArgPtr emits IR that fails the verifier
+      // ("Instruction does not dominate all uses") and, with -disable-llvm-verifier
+      // (kernel builds), reaches codegen and crashes RegisterCoalescer::reMaterializeDef.
+      // The insertion point is the entry terminator, so a value defined in the entry
+      // block dominates it; anything else (a later-block instruction, e.g. a field
+      // getelementptr) does not -> skip it and let the dead-arg fallback emit a null
+      // trace for this param.
+      if (auto *VI = dyn_cast<Instruction>(V))
+        if (VI->getParent() != &EntryBB)
+          continue;
       auto &SA = SrcArgs[SrcIdx];
       SA.Var = Var;
       SA.Ty = Var->getType();
@@ -1500,16 +1543,21 @@ void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
       }
       auto [OffsetsGV, NumFields] = getStructFieldOffsets(ArgDIType, M, *DL);
       Value *ArgPtr;
+      bool Spilled = false;
       if (Arg.getType()->isPointerTy()) {
         ArgPtr = &Arg;
+      } else if (SkipSpill) {
+        ArgPtr = Constant::getNullValue(PtrTy); // base-pointer asm: no escaping spill
       } else {
         AllocaInst *Alloca = IRB.CreateAlloca(Arg.getType());
         IRB.CreateStore(&Arg, Alloca);
         ArgPtr = Alloca;
+        Spilled = true;
       }
-      unsigned ArgByteSize = Arg.getType()->isPointerTy()
-                                 ? DL->getPointerSize()
-                                 : DL->getTypeStoreSize(Arg.getType());
+      unsigned ArgByteSize =
+          Arg.getType()->isPointerTy() ? DL->getPointerSize()
+          : Spilled                    ? DL->getTypeStoreSize(Arg.getType())
+                                       : 0;
       Value *OffsetsPtr = Constant::getNullValue(PtrTy);
       if (OffsetsGV) {
         Value *Indices[] = {ConstantInt::get(Int64Ty, 0),
@@ -1552,12 +1600,18 @@ void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
       if (V->getType()->isPointerTy()) {
         ArgPtr = V;
         ArgByteSize = DL->getPointerSize();
+      } else if (SkipSpill) {
+        ArgPtr = Constant::getNullValue(PtrTy); // base-pointer asm: no escaping spill
+        ArgByteSize = 0;
       } else {
         AllocaInst *Alloca = IRB.CreateAlloca(V->getType());
         TraceIRB.CreateStore(V, Alloca);
         ArgPtr = Alloca;
         ArgByteSize = DL->getTypeStoreSize(V->getType());
       }
+    } else if (SkipSpill) {
+      ArgPtr = Constant::getNullValue(PtrTy); // base-pointer asm: no escaping spill
+      ArgByteSize = 0;
     } else {
       // Multiple IR args for one source param (ABI struct decomposition).
       // Reassemble fragments into a stack slot matching the source layout.
@@ -1630,6 +1684,13 @@ void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
 void ModuleSanitizerCoverage::InjectTraceForRet(Function &F) {
   DISubprogram *SP = F.getSubprogram();
 
+  // On x86, skip the escaping return-value spill in functions whose inline asm ties up
+  // the base pointer (RBX): the spill alloca is ASAN-redzoned -> stack realignment ->
+  // base pointer, which the backend rejects against the asm's RBX use ("Interference
+  // usage of base pointer/frame pointer"). Such returns are traced as a null pointer.
+  const bool SkipSpill = TargetTriple.getArch() == Triple::x86_64 &&
+                         functionInlineAsmUsesBasePointerX86(F);
+
   // Get return type debug info
   DIType *RetDIType = nullptr;
   if (SP && SP->getType()) {
@@ -1671,7 +1732,7 @@ void ModuleSanitizerCoverage::InjectTraceForRet(Function &F) {
     if (RetVal && RetVal->getType()->isPointerTy()) {
       RetPtr = RetVal;
       RetByteSize = DL->getPointerSize();
-    } else if (RetVal && !RetVal->getType()->isVoidTy()) {
+    } else if (RetVal && !RetVal->getType()->isVoidTy() && !SkipSpill) {
       AllocaInst *Alloca = AtExit->CreateAlloca(RetVal->getType());
       AtExit->CreateStore(RetVal, Alloca);
       RetPtr = Alloca;
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-dominance.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-dominance.ll
new file mode 100644
index 0000000000000..d3dfb4db8b788
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-dominance.ll
@@ -0,0 +1,50 @@
+; Regression test: trace-args must NOT emit a non-dominating use. The entry-block #dbg_value
+; scan may find an argument whose debug location is a value defined LATER in the function
+; (debug records are exempt from SSA dominance). Using such a value as the trace pointer -
+; the trace call is inserted at the entry-block terminator - produces IR that fails the
+; verifier ("Instruction does not dominate all uses") and, with -disable-llvm-verifier
+; (kernel builds), crashes RegisterCoalescer::reMaterializeDef at codegen. Such an argument
+; is traced as a null pointer instead. opt runs the verifier, so a successful run of this
+; test already proves the emitted IR is well-formed (it would error before this fix).
+
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-args -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+; %arg1's only debug location is %later, a GEP defined in a later block (does NOT dominate
+; the entry terminator). %arg2 is a normal directly-located pointer argument.
+define void @arg_loc_not_dominating(ptr %arg1, ptr %arg2) #0 !dbg !8 {
+entry:
+  #dbg_value(ptr %arg2, !13, !DIExpression(), !15)
+  #dbg_value(ptr %later, !12, !DIExpression(), !15)
+  br label %bb, !dbg !15
+bb:
+  %later = getelementptr i8, ptr %arg1, i64 128, !dbg !15
+  ret void, !dbg !15
+}
+; CHECK-LABEL: define void @arg_loc_not_dominating(ptr %arg1, ptr %arg2)
+; arg1 (index 0): its debug location did not dominate -> traced as a null pointer, size 0.
+; CHECK-DAG: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @arg_loc_not_dominating to i64), i32 0, i32 0, ptr null, ptr {{.*}}, i32 {{.*}})
+; arg2 (index 1): a normal pointer arg, traced directly.
+; CHECK-DAG: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @arg_loc_not_dominating to i64), i32 1, i32 {{[0-9]+}}, ptr %arg2, ptr {{.*}}, i32 {{.*}})
+
+attributes #0 = { nounwind sanitize_address }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, isOptimized: true, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/tmp")
+!2 = !{}
+!3 = !{i32 2, !"Dwarf Version", i32 4}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!6 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !5, size: 64)
+!8 = distinct !DISubprogram(name: "arg_loc_not_dominating", scope: !1, file: !1, line: 1, type: !9, unit: !0, retainedNodes: !11)
+!9 = !DISubroutineType(types: !10)
+!10 = !{null, !6, !6}
+!11 = !{!12, !13}
+!12 = !DILocalVariable(name: "arg1", arg: 1, scope: !8, file: !1, line: 1, type: !6)
+!13 = !DILocalVariable(name: "arg2", arg: 2, scope: !8, file: !1, line: 1, type: !6)
+!15 = !DILocation(line: 1, column: 1, scope: !8)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-ret-basepointer.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret-basepointer.ll
new file mode 100644
index 0000000000000..48839d8d77246
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret-basepointer.ll
@@ -0,0 +1,51 @@
+; Regression test: trace-ret must NOT create an escaping return-value spill alloca in a
+; function whose inline asm uses/clobbers the x86 base pointer (RBX). The spill alloca's
+; address is passed to __sanitizer_cov_trace_ret, so it escapes; under KASAN it is redzoned,
+; forcing 32-byte frame realignment -> a base pointer (RBX), which the X86 backend then
+; rejects against the asm's RBX use with "Interference usage of base pointer/frame pointer".
+; For such functions the return is traced as a null pointer instead.
+
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-ret -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+; Scalar return in a function whose inline asm clobbers rbx (cf. CPUID "=b", RDTSC "~{rbx}").
+define i32 @ret_scalar_clobbers_rbx(i32 %x) #0 !dbg !8 {
+entry:
+  call void asm sideeffect "nop", "~{rbx},~{dirflag},~{fpsr},~{flags}"() #0, !dbg !12
+  ret i32 %x, !dbg !12
+}
+; CHECK-LABEL: define i32 @ret_scalar_clobbers_rbx(i32 %x)
+; CHECK-NOT:   alloca
+; CHECK:       call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_scalar_clobbers_rbx to i64), i32 0, ptr null, ptr null, i32 0)
+; CHECK:       ret i32 %x
+
+; Control: the same scalar return WITHOUT base-pointer asm still spills and traces normally.
+define i32 @ret_scalar_plain(i32 %x) #0 !dbg !13 {
+entry:
+  ret i32 %x, !dbg !14
+}
+; CHECK-LABEL: define i32 @ret_scalar_plain(i32 %x)
+; CHECK:       %[[SLOT:.*]] = alloca i32
+; CHECK:       store i32 %x, ptr %[[SLOT]]
+; CHECK:       call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_scalar_plain to i64), i32 4, ptr %[[SLOT]], ptr null, i32 0)
+; CHECK:       ret i32 %x
+
+attributes #0 = { nounwind sanitize_address }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, isOptimized: true, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/tmp")
+!2 = !{}
+!3 = !{i32 2, !"Dwarf Version", i32 4}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!8 = distinct !DISubprogram(name: "ret_scalar_clobbers_rbx", scope: !1, file: !1, line: 1, type: !9, unit: !0, retainedNodes: !2)
+!9 = !DISubroutineType(types: !10)
+!10 = !{!5, !5}
+!12 = !DILocation(line: 1, column: 1, scope: !8)
+!13 = distinct !DISubprogram(name: "ret_scalar_plain", scope: !1, file: !1, line: 2, type: !9, unit: !0, retainedNodes: !2)
+!14 = !DILocation(line: 2, column: 1, scope: !13)

>From 82267b8e0e4a4950eb701adfb14721ead64be3ae Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Thu, 9 Jul 2026 18:03:12 +0200
Subject: [PATCH 7/8] [SanitizerCoverage] Add userspace runtime for
 trace-args/trace-ret

The trace-args/trace-ret instrumentation only had a kernel (KCOV dataflow)
consumer. Make the two flags usable from userspace C/C++/Rust programs:

- compiler-rt: weak no-op default definitions of __sanitizer_cov_trace_args
  and __sanitizer_cov_trace_ret (sanitizer_common), so a program built with
  -fsanitize-coverage=trace-args,trace-ret links without any runtime; plus
  internal-interface declarations and the weak-function interface entries.

- libFuzzer: strong definitions of both callbacks that fold each observed
  argument/return value into the value-profile map via TracePC::HandleDataflow.
  The fold binds the value to its (PC, arg/ret location, struct field) site, so
  a never-before-seen value at a site becomes a new value-profile feature under
  -use_value_profile, mirroring kcov-dataflow's (pc, value) coverage.

- SanitizerCoverage: drop the hard "-g required" early return in
  InjectTraceForArgs. Without debug info the source-argument map is empty and
  the pass now falls back to tracing each IR argument directly (offsets=null,
  num_fields=0), so trace-args works on optimized/no-debug userspace and Rust
  builds; only the source-level struct-field breakdown is lost. Guard the two
  remaining SP->getType() derefs against a null subprogram.

- Tests: a no-debug IR test (trace-args-no-debug.ll) covering the fallback, and
  an end-to-end compiler-rt runtime test exercising a user override of the weak
  callbacks.

The user-facing documentation for these modes lands in the following commit.

Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
 compiler-rt/lib/fuzzer/FuzzerTracePC.cpp      | 52 +++++++++++++++++++
 compiler-rt/lib/fuzzer/FuzzerTracePC.h        |  5 ++
 .../sanitizer_coverage_interface.inc          |  2 +
 .../sanitizer_coverage_libcdep_new.cpp        |  6 +++
 .../sanitizer_interface_internal.h            | 13 +++++
 .../sanitizer_coverage_trace_args.cpp         | 48 +++++++++++++++++
 .../Instrumentation/SanitizerCoverage.cpp     | 11 ++--
 .../SanitizerCoverage/trace-args-no-debug.ll  | 21 ++++++++
 8 files changed, 154 insertions(+), 4 deletions(-)
 create mode 100644 compiler-rt/test/sanitizer_common/TestCases/sanitizer_coverage_trace_args.cpp
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-debug.ll

diff --git a/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp b/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp
index 7bd1a0870c593..9290cab8392b7 100644
--- a/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp
+++ b/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp
@@ -395,6 +395,37 @@ void TracePC::HandleCmp(uintptr_t PC, T Arg1, T Arg2) {
   ValueProfileMap.AddValue(PC * 128 + 64 + AbsoluteDistance);
 }
 
+ATTRIBUTE_NO_SANITIZE_ALL
+void TracePC::HandleDataflow(uintptr_t PC, uint32_t Loc, const void *Ptr,
+                             uint32_t Size, const uint64_t *Offsets,
+                             uint32_t NumFields) {
+  if (!Ptr)
+    return;  // argument optimized away, or skipped (base-pointer inline asm)
+  const uint8_t *Base = reinterpret_cast<const uint8_t *>(Ptr);
+  auto Load = [](const uint8_t *P, uint64_t N) -> uint64_t {
+    uint64_t V = 0;
+    if (N > sizeof(V))
+      N = sizeof(V);
+    __builtin_memcpy(&V, P, static_cast<size_t>(N));
+    return V;
+  };
+  // Bind the value to its site (PC, argument/return, field) so the same value
+  // seen at different sites stays distinct, then fold it into the value profile:
+  // a never-before-seen value at that site becomes a new feature (this mirrors
+  // kcov-dataflow's (pc, value) coverage). Consumed only under -use_value_profile.
+  auto Fold = [&](uint32_t FieldId, uint64_t V) {
+    uintptr_t Idx = (PC * 3 + Loc) ^ (FieldId * 0x9E3779B1u) ^ V;
+    ValueProfileMap.AddValueModPrime(Idx);
+  };
+  if (Offsets && NumFields) {
+    // Struct arg/return: Offsets is {off0, sz0, off1, sz1, ...}; read each field.
+    for (uint32_t i = 0; i < NumFields && i < 32; i++)
+      Fold(i + 1, Load(Base + Offsets[i * 2], Offsets[i * 2 + 1]));
+  } else if (Size) {
+    Fold(0, Load(Base, Size));
+  }
+}
+
 ATTRIBUTE_NO_SANITIZE_MEMORY
 static size_t InternalStrnlen(const char *S, size_t MaxLen) {
   size_t Len = 0;
@@ -489,6 +520,27 @@ void __sanitizer_cov_trace_cmp8(uint64_t Arg1, uint64_t Arg2) {
   fuzzer::TPC.HandleCmp(PC, Arg1, Arg2);
 }
 
+// Dataflow coverage (-fsanitize-coverage=trace-args,trace-ret). Unlike the cmp
+// callbacks, PC is passed by the instrumentation (the function address), not via
+// GET_CALLER_PC. Folds the observed argument/return value into the value profile.
+ATTRIBUTE_INTERFACE
+ATTRIBUTE_NO_SANITIZE_ALL
+void __sanitizer_cov_trace_args(uint64_t PC, uint32_t ArgIdx, uint32_t ArgSize,
+                                const void *Arg, const uint64_t *Offsets,
+                                uint32_t NumFields) {
+  fuzzer::TPC.HandleDataflow(static_cast<uintptr_t>(PC), ArgIdx, Arg, ArgSize,
+                             Offsets, NumFields);
+}
+
+ATTRIBUTE_INTERFACE
+ATTRIBUTE_NO_SANITIZE_ALL
+void __sanitizer_cov_trace_ret(uint64_t PC, uint32_t RetSize, const void *Ret,
+                               const uint64_t *Offsets, uint32_t NumFields) {
+  // 0xFFFF distinguishes the return site from arguments sharing this PC.
+  fuzzer::TPC.HandleDataflow(static_cast<uintptr_t>(PC), 0xFFFF, Ret, RetSize,
+                             Offsets, NumFields);
+}
+
 ATTRIBUTE_INTERFACE
 ATTRIBUTE_NO_SANITIZE_ALL
 ATTRIBUTE_TARGET_POPCNT
diff --git a/compiler-rt/lib/fuzzer/FuzzerTracePC.h b/compiler-rt/lib/fuzzer/FuzzerTracePC.h
index af1f9d81e9509..b466628274c6a 100644
--- a/compiler-rt/lib/fuzzer/FuzzerTracePC.h
+++ b/compiler-rt/lib/fuzzer/FuzzerTracePC.h
@@ -73,6 +73,11 @@ class TracePC {
   void HandlePCsInit(const uintptr_t *Start, const uintptr_t *Stop);
   void HandleCallerCallee(uintptr_t Caller, uintptr_t Callee);
   template <class T> void HandleCmp(uintptr_t PC, T Arg1, T Arg2);
+  // Fold a dataflow value observed at a call-argument / return site into the
+  // value profile (trace-args / trace-ret). Loc distinguishes arguments/returns
+  // that share the same PC; Offsets/NumFields (when non-null) decompose a struct.
+  void HandleDataflow(uintptr_t PC, uint32_t Loc, const void *Ptr, uint32_t Size,
+                      const uint64_t *Offsets, uint32_t NumFields);
   size_t GetTotalPCCoverage();
   void SetUseCounters(bool UC) { UseCounters = UC; }
   void SetUseValueProfileMask(uint32_t VPMask) { UseValueProfileMask = VPMask; }
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_coverage_interface.inc b/compiler-rt/lib/sanitizer_common/sanitizer_coverage_interface.inc
index 9d36a40270d5a..ff7d971bfe51e 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_coverage_interface.inc
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_coverage_interface.inc
@@ -38,6 +38,8 @@ INTERFACE_WEAK_FUNCTION(__sanitizer_cov_store4)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_store8)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_store16)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_trace_switch)
+INTERFACE_WEAK_FUNCTION(__sanitizer_cov_trace_args)
+INTERFACE_WEAK_FUNCTION(__sanitizer_cov_trace_ret)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_8bit_counters_init)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_bool_flag_init)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_pcs_init)
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp b/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp
index 506659a58c45e..5451128c31d99 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp
@@ -256,6 +256,12 @@ SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_const_cmp2, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_const_cmp4, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_const_cmp8, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_switch, void) {}
+// Dataflow coverage (trace-args / trace-ret). Weak no-op defaults so a program
+// built with -fsanitize-coverage=trace-args,trace-ret links without a consumer;
+// a fuzzer (libFuzzer) or the user redefines them. They must be null-safe: the
+// pass passes a null pointer for an argument/return it could not spill.
+SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_args, void) {}
+SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_ret, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_div4, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_div8, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_gep, void) {}
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h b/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h
index c424ab1cecf94..63f4991f2ae17 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h
@@ -115,6 +115,19 @@ SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
 __sanitizer_cov_trace_const_cmp8();
 SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
 __sanitizer_cov_trace_switch();
+// Dataflow coverage: observe a call/function argument and a return value.
+//   __sanitizer_cov_trace_args(uptr pc, u32 arg_idx, u32 arg_size, void *arg,
+//                              u64 *field_offsets, u32 num_fields)
+//   __sanitizer_cov_trace_ret (uptr pc, u32 ret_size, void *ret,
+//                              u64 *field_offsets, u32 num_fields)
+// `arg`/`ret` may be null (argument optimized away, or skipped for a function
+// whose inline asm ties up the x86 base pointer); consumers must not dereference
+// a null pointer. `field_offsets` (when non-null) has num_fields {offset,size}
+// pairs describing the fields of a struct-typed arg/return.
+SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
+__sanitizer_cov_trace_args();
+SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
+__sanitizer_cov_trace_ret();
 SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
 __sanitizer_cov_trace_div4();
 SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
diff --git a/compiler-rt/test/sanitizer_common/TestCases/sanitizer_coverage_trace_args.cpp b/compiler-rt/test/sanitizer_common/TestCases/sanitizer_coverage_trace_args.cpp
new file mode 100644
index 0000000000000..0b14469755e30
--- /dev/null
+++ b/compiler-rt/test/sanitizer_common/TestCases/sanitizer_coverage_trace_args.cpp
@@ -0,0 +1,48 @@
+// Test the dataflow-coverage callbacks (-fsanitize-coverage=trace-args,trace-ret):
+// the instrumentation invokes __sanitizer_cov_trace_args / __sanitizer_cov_trace_ret
+// with the observed argument and return values, and a user definition overrides
+// compiler-rt's weak defaults.
+
+// trace-args reads arguments at the function entry insertion point, so it needs
+// -O1+ (at -O0 the trace call precedes the prologue stores to the argument slots).
+
+// REQUIRES: has_sancovcc
+// UNSUPPORTED: i386-darwin
+// RUN: %clangxx -O2 -g -fsanitize-coverage=trace-pc-guard,trace-args,trace-ret %s -o %t
+// RUN: %run %t 2>&1 | FileCheck %s
+
+#include <cstdint>
+#include <cstdio>
+
+extern "C" {
+// Standalone stubs so the program links without a sanitizer runtime (these are
+// weak in compiler-rt; our strong definitions win).
+void __sanitizer_cov_trace_pc_guard(uint32_t *) {}
+void __sanitizer_cov_trace_pc_guard_init(uint32_t *, uint32_t *) {}
+
+// The dataflow consumers under test. arg/ret may be null (skipped/optimized-away)
+// -- do not dereference a null pointer.
+void __sanitizer_cov_trace_args(uint64_t pc, uint32_t arg_idx, uint32_t arg_size,
+                                void *arg, uint64_t *offsets, uint32_t nfields) {
+  if (arg && arg_size == sizeof(int))
+    fprintf(stderr, "ARG idx=%u val=%d\n", arg_idx, *(int *)arg);
+}
+void __sanitizer_cov_trace_ret(uint64_t pc, uint32_t ret_size, void *ret,
+                               uint64_t *offsets, uint32_t nfields) {
+  if (ret && ret_size == sizeof(int))
+    fprintf(stderr, "RET val=%d\n", *(int *)ret);
+}
+}
+
+__attribute__((noinline)) int add(int a, int b) { return a + b; }
+
+int main() {
+  volatile int r = add(41, 2);
+  fprintf(stderr, "r=%d\n", (int)r);
+  return 0;
+}
+
+// CHECK-DAG: ARG idx=0 val=41
+// CHECK-DAG: ARG idx=1 val=2
+// CHECK: RET val=43
+// CHECK: r=43
diff --git a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
index fae21bf41087d..e55b13d2cf077 100644
--- a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
+++ b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
@@ -1424,9 +1424,12 @@ static bool functionInlineAsmUsesBasePointerX86(const Function &F) {
 }
 
 void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
+  // SP may be null: a function compiled WITHOUT debug info (-g) carries no
+  // #dbg_value records, so the source-argument map below stays empty and the
+  // SrcArgs.empty() fallback traces each IR argument directly (no source-level
+  // struct-field offsets). This lets trace-args work on userspace / optimized
+  // code built without -g, not just debug kernels.
   DISubprogram *SP = F.getSubprogram();
-  if (!SP)
-    return;
   // Only guards x86; on other targets there is no RBX base-pointer interference.
   const bool SkipSpill = TargetTriple.getArch() == Triple::x86_64 &&
                          functionInlineAsmUsesBasePointerX86(F);
@@ -1536,7 +1539,7 @@ void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
       if (Arg.hasStructRetAttr())
         continue;
       DIType *ArgDIType = nullptr;
-      if (SP->getType()) {
+      if (SP && SP->getType()) {
         auto TypeArray = SP->getType()->getTypeArray();
         if (ArgIdx + 1 < TypeArray.size())
           ArgDIType = TypeArray[ArgIdx + 1];
@@ -1654,7 +1657,7 @@ void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
   // Dead-arg fallback: if the DISubroutineType indicates more source params
   // than we found via debug records (e.g., arg optimized away entirely at -O2),
   // emit a null-pointer trace so consumers know the argument existed.
-  if (SP->getType()) {
+  if (SP && SP->getType()) {
     auto TypeArray = SP->getType()->getTypeArray();
     unsigned NumSrcParams = TypeArray.size() > 0 ? TypeArray.size() - 1 : 0;
     for (unsigned I = 1; I <= NumSrcParams; ++I) {
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-debug.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-debug.ll
new file mode 100644
index 0000000000000..81f516508cf1b
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-debug.ll
@@ -0,0 +1,21 @@
+; Test that trace-args works WITHOUT debug info (-g). A function with no
+; DISubprogram carries no #dbg_value records, so the source-argument map stays
+; empty and the pass falls back to tracing each IR argument directly (no
+; source-level struct-field offsets). This keeps trace-args usable on userspace
+; / optimized code built without -g, not only debug kernels. opt runs the
+; verifier, so a successful run also proves the emitted IR is well-formed.
+
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-args -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+define void @no_debug(ptr %p, i32 %x) {
+entry:
+  ret void
+}
+; CHECK-LABEL: define void @no_debug(ptr %p, i32 %x)
+; pointer arg 0 is traced directly (no spill, no field offsets):
+; CHECK-DAG: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @no_debug to i64), i32 0, i32 8, ptr %p, ptr null, i32 0)
+; scalar arg 1 is spilled to a stack slot then traced:
+; CHECK-DAG: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @no_debug to i64), i32 1, i32 4, ptr %{{.*}}, ptr null, i32 0)

>From af51311635537d6703fe67f53a96af7799d05bf6 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Sun, 7 Jun 2026 20:10:50 +0200
Subject: [PATCH 8/8] [SanitizerCoverage] Document trace-args/trace-ret modes

Add a "Tracking function arguments and return values" section to
SanitizerCoverage.rst covering the trace-args/trace-ret coverage modes:

- The __sanitizer_cov_trace_args / __sanitizer_cov_trace_ret callback
  signatures, and the null-safety contract (ptr/offsets may be null).

- ABI behavior: arg_idx is the source-level parameter index (mapped via
  DILocalVariable::getArg(), so it is stable across ABI lowering and does not
  count hidden sret/this pointers), ABI-decomposed structs are reassembled into
  a single stack slot in source layout, and an indirect (sret) struct return is
  reported via its caller-provided buffer rather than dropped.

- Debug-info handling: with -g the struct field layout is derived from
  DICompositeType; without -g the pass falls back to tracing each IR argument as
  an opaque scalar. Argument values require -O1+ to be meaningful (the entry
  insertion point precedes the -O0 prologue stores to the argument slots).

- The userspace runtime: compiler-rt's weak no-op defaults, the libFuzzer
  value-profile consumer (-use_value_profile), and the kernel KCOV path.

- The x86-only base-pointer spill-skip (a null ptr for scalars in functions
  whose inline asm clobbers the rbx base pointer).

Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
 clang/docs/SanitizerCoverage.rst | 745 +++++++++++++++++++++++++++++++
 1 file changed, 745 insertions(+)
 create mode 100644 clang/docs/SanitizerCoverage.rst

diff --git a/clang/docs/SanitizerCoverage.rst b/clang/docs/SanitizerCoverage.rst
new file mode 100644
index 0000000000000..b5228fc29dec8
--- /dev/null
+++ b/clang/docs/SanitizerCoverage.rst
@@ -0,0 +1,745 @@
+=================
+SanitizerCoverage
+=================
+
+.. contents::
+   :local:
+
+Introduction
+============
+
+LLVM has a simple code coverage instrumentation built in (SanitizerCoverage).
+It inserts calls to user-defined functions on function-, basic-block-, and edge- levels.
+Default implementations of those callbacks are provided and implement
+simple coverage reporting and visualization,
+however if you need *just* coverage visualization you may want to use
+:doc:`SourceBasedCodeCoverage <SourceBasedCodeCoverage>` instead.
+
+Tracing PCs with guards
+=======================
+
+With ``-fsanitize-coverage=trace-pc-guard`` the compiler will insert the following code
+on every edge:
+
+.. code-block:: none
+
+   __sanitizer_cov_trace_pc_guard(&guard_variable)
+
+Every edge will have its own `guard_variable` (uint32_t).
+
+The compiler will also insert calls to a module constructor:
+
+.. code-block:: c++
+
+   // The guards are [start, stop).
+   // This function will be called at least once per DSO and may be called
+   // more than once with the same values of start/stop.
+   __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop);
+
+With an additional ``...=trace-pc,indirect-calls`` flag
+``__sanitizer_cov_trace_pc_indirect(void *callee)`` will be inserted on every indirect call.
+
+The functions `__sanitizer_cov_trace_pc_*` should be defined by the user.
+
+Example:
+
+.. code-block:: c++
+
+  // trace-pc-guard-cb.cc
+  #include <stdint.h>
+  #include <stdio.h>
+  #include <sanitizer/coverage_interface.h>
+
+  // This callback is inserted by the compiler as a module constructor
+  // into every DSO. 'start' and 'stop' correspond to the
+  // beginning and end of the section with the guards for the entire
+  // binary (executable or DSO). The callback will be called at least
+  // once per DSO and may be called multiple times with the same parameters.
+  extern "C" void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
+                                                      uint32_t *stop) {
+    static uint64_t N;  // Counter for the guards.
+    if (start == stop || *start) return;  // Initialize only once.
+    printf("INIT: %p %p\n", start, stop);
+    for (uint32_t *x = start; x < stop; x++)
+      *x = ++N;  // Guards should start from 1.
+  }
+
+  // This callback is inserted by the compiler on every edge in the
+  // control flow (some optimizations apply).
+  // Typically, the compiler will emit the code like this:
+  //    if(*guard)
+  //      __sanitizer_cov_trace_pc_guard(guard);
+  // But for large functions it will emit a simple call:
+  //    __sanitizer_cov_trace_pc_guard(guard);
+  extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
+    if (!*guard) return;  // Duplicate the guard check.
+    // If you set *guard to 0 this code will not be called again for this edge.
+    // Now you can get the PC and do whatever you want:
+    //   store it somewhere or symbolize it and print right away.
+    // The values of `*guard` are as you set them in
+    // __sanitizer_cov_trace_pc_guard_init and so you can make them consecutive
+    // and use them to dereference an array or a bit vector.
+    void *PC = __builtin_return_address(0);
+    char PcDescr[1024];
+    // This function is a part of the sanitizer run-time.
+    // To use it, link with AddressSanitizer or other sanitizer.
+    __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
+    printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
+  }
+
+.. code-block:: c++
+
+  // trace-pc-guard-example.cc
+  void foo() { }
+  int main(int argc, char **argv) {
+    if (argc > 1) foo();
+  }
+
+.. code-block:: console
+
+  clang++ -g  -fsanitize-coverage=trace-pc-guard trace-pc-guard-example.cc -c
+  clang++ trace-pc-guard-cb.cc trace-pc-guard-example.o -fsanitize=address
+  ASAN_OPTIONS=strip_path_prefix=`pwd`/ ./a.out
+
+.. code-block:: console
+
+  INIT: 0x71bcd0 0x71bce0
+  guard: 0x71bcd4 2 PC 0x4ecd5b in main trace-pc-guard-example.cc:2
+  guard: 0x71bcd8 3 PC 0x4ecd9e in main trace-pc-guard-example.cc:3:7
+
+.. code-block:: console
+
+  ASAN_OPTIONS=strip_path_prefix=`pwd`/ ./a.out with-foo
+
+
+.. code-block:: console
+
+  INIT: 0x71bcd0 0x71bce0
+  guard: 0x71bcd4 2 PC 0x4ecd5b in main trace-pc-guard-example.cc:3
+  guard: 0x71bcdc 4 PC 0x4ecdc7 in main trace-pc-guard-example.cc:4:17
+  guard: 0x71bcd0 1 PC 0x4ecd20 in foo() trace-pc-guard-example.cc:2:14
+
+Inline 8bit-counters
+====================
+
+**Experimental, may change or disappear in future**
+
+With ``-fsanitize-coverage=inline-8bit-counters`` the compiler will insert
+inline counter increments on every edge.
+This is similar to ``-fsanitize-coverage=trace-pc-guard`` but instead of a
+callback the instrumentation simply increments a counter.
+
+Users need to implement a single function to capture the counters at startup.
+
+.. code-block:: c++
+
+  extern "C"
+  void __sanitizer_cov_8bit_counters_init(char *start, char *end) {
+    // [start,end) is the array of 8-bit counters created for the current DSO.
+    // Capture this array in order to read/modify the counters.
+  }
+
+
+Inline bool-flag
+================
+
+**Experimental, may change or disappear in future**
+
+With ``-fsanitize-coverage=inline-bool-flag`` the compiler will insert
+setting an inline boolean to true on every edge.
+This is similar to ``-fsanitize-coverage=inline-8bit-counter`` but instead of
+an increment of a counter, it just sets a boolean to true.
+
+Users need to implement a single function to capture the flags at startup.
+
+.. code-block:: c++
+
+  extern "C"
+  void __sanitizer_cov_bool_flag_init(bool *start, bool *end) {
+    // [start,end) is the array of boolean flags created for the current DSO.
+    // Capture this array in order to read/modify the flags.
+  }
+
+
+PC-Table
+========
+
+**Experimental, may change or disappear in future**
+
+**Note:** this instrumentation might be incompatible with dead code stripping
+(``-Wl,-gc-sections``) for linkers other than LLD, thus resulting in a
+significant binary size overhead. For more information, see
+`Bug 34636 <https://bugs.llvm.org/show_bug.cgi?id=34636>`_.
+
+With ``-fsanitize-coverage=pc-table`` the compiler will create a table of
+instrumented PCs. Requires either ``-fsanitize-coverage=inline-8bit-counters``,
+or ``-fsanitize-coverage=inline-bool-flag``, or ``-fsanitize-coverage=trace-pc-guard``.
+
+Users need to implement a single function to capture the PC table at startup:
+
+.. code-block:: c++
+
+  extern "C"
+  void __sanitizer_cov_pcs_init(const uintptr_t *pcs_beg,
+                                const uintptr_t *pcs_end) {
+    // [pcs_beg,pcs_end) is the array of ptr-sized integers representing
+    // pairs [PC,PCFlags] for every instrumented block in the current DSO.
+    // Capture this array in order to read the PCs and their Flags.
+    // The number of PCs and PCFlags for a given DSO is the same as the number
+    // of 8-bit counters (-fsanitize-coverage=inline-8bit-counters), or
+    // boolean flags (-fsanitize-coverage=inline=bool-flags), or trace_pc_guard
+    // callbacks (-fsanitize-coverage=trace-pc-guard).
+    // A PCFlags describes the basic block:
+    //  * bit0: 1 if the block is the function entry block, 0 otherwise.
+  }
+
+
+Tracing PCs
+===========
+
+With ``-fsanitize-coverage=trace-pc`` the compiler will insert
+``__sanitizer_cov_trace_pc()`` on every edge.
+With an additional ``...=trace-pc,indirect-calls`` flag
+``__sanitizer_cov_trace_pc_indirect(void *callee)`` will be inserted on every indirect call.
+
+With ``-fsanitize-coverage=trace-pc-entry-exit`` the compiler will insert
+``__sanitizer_cov_trace_pc_entry()`` on function entry, and insert
+``__sanitizer_cov_trace_pc_exit()`` on function return;
+``-fsanitize-coverage=trace-pc`` or ``-fsanitize-coverage=trace-pc-guard`` must
+still be passed to instrument all basic blocks.
+
+With the combination ``-fsanitize-coverage=trace-pc-entry-exit,trace-pc``,
+``__sanitizer_cov_trace_pc()`` will be omitted in the entry basic block because
+the block is already covered by ``__sanitizer_cov_trace_pc_entry()``, which can
+be used to both record that the function has been entered and record coverage of
+the entry basic block.
+However, with ``-fsanitize-coverage=trace-pc-entry-exit,trace-pc-guard``, both
+callbacks are called for the entry block because
+``__sanitizer_cov_trace_pc_entry()`` does not provide a `guard_variable`.
+
+These callbacks are not implemented in the Sanitizer run-time and should be defined
+by the user.
+This mechanism is used for fuzzing the Linux kernel
+(https://github.com/google/syzkaller).
+
+Instrumentation points
+======================
+Sanitizer Coverage offers different levels of instrumentation.
+
+* ``edge`` (default): edges are instrumented (see below).
+* ``bb``: basic blocks are instrumented.
+* ``func``: only the entry block of every function will be instrumented.
+
+Use these flags together with ``trace-pc-guard`` or ``trace-pc``,
+like this: ``-fsanitize-coverage=func,trace-pc-guard``.
+
+When ``edge`` or ``bb`` is used, some of the edges/blocks may still be left
+uninstrumented (pruned) if such instrumentation is considered redundant.
+Use ``no-prune`` (e.g. ``-fsanitize-coverage=bb,no-prune,trace-pc-guard``)
+to disable pruning. This could be useful for better coverage visualization.
+
+
+Edge coverage
+-------------
+
+Consider this code:
+
+.. code-block:: c++
+
+    void foo(int *a) {
+      if (a)
+        *a = 0;
+    }
+
+It contains 3 basic blocks, let's name them A, B, C:
+
+.. code-block:: none
+
+    A
+    |\
+    | \
+    |  B
+    | /
+    |/
+    C
+
+If blocks A, B, and C are all covered we know for certain that the edges A=>B
+and B=>C were executed, but we still don't know if the edge A=>C was executed.
+Such edges of control flow graph are called
+`critical <https://en.wikipedia.org/wiki/Control_flow_graph#Special_edges>`_.
+The edge-level coverage simply splits all critical edges by introducing new
+dummy blocks and then instruments those blocks:
+
+.. code-block:: none
+
+    A
+    |\
+    | \
+    D  B
+    | /
+    |/
+    C
+
+Tracing data flow
+=================
+
+Support for data-flow-guided fuzzing.
+With ``-fsanitize-coverage=trace-cmp`` the compiler will insert extra instrumentation
+around comparison instructions and switch statements.
+Similarly, with ``-fsanitize-coverage=trace-div`` the compiler will instrument
+integer division instructions (to capture the right argument of division)
+and with  ``-fsanitize-coverage=trace-gep`` --
+the `LLVM GEP instructions <https://llvm.org/docs/GetElementPtr.html>`_
+(to capture array indices).
+Similarly, with ``-fsanitize-coverage=trace-loads`` and ``-fsanitize-coverage=trace-stores``
+the compiler will instrument loads and stores, respectively.
+
+Currently, these flags do not work by themselves - they require one
+of ``-fsanitize-coverage={trace-pc,inline-8bit-counters,inline-bool}``
+flags to work.
+
+Unless ``no-prune`` option is provided, some of the comparison instructions
+will not be instrumented.
+
+.. code-block:: c++
+
+  // Called before a comparison instruction.
+  // Arg1 and Arg2 are arguments of the comparison.
+  void __sanitizer_cov_trace_cmp1(uint8_t Arg1, uint8_t Arg2);
+  void __sanitizer_cov_trace_cmp2(uint16_t Arg1, uint16_t Arg2);
+  void __sanitizer_cov_trace_cmp4(uint32_t Arg1, uint32_t Arg2);
+  void __sanitizer_cov_trace_cmp8(uint64_t Arg1, uint64_t Arg2);
+
+  // Called before a comparison instruction if exactly one of the arguments is constant.
+  // Arg1 and Arg2 are arguments of the comparison, Arg1 is a compile-time constant.
+  // These callbacks are emitted by -fsanitize-coverage=trace-cmp since 2017-08-11
+  void __sanitizer_cov_trace_const_cmp1(uint8_t Arg1, uint8_t Arg2);
+  void __sanitizer_cov_trace_const_cmp2(uint16_t Arg1, uint16_t Arg2);
+  void __sanitizer_cov_trace_const_cmp4(uint32_t Arg1, uint32_t Arg2);
+  void __sanitizer_cov_trace_const_cmp8(uint64_t Arg1, uint64_t Arg2);
+
+  // Called before a switch statement.
+  // Val is the switch operand.
+  // Cases[0] is the number of case constants.
+  // Cases[1] is the size of Val in bits.
+  // Cases[2:] are the case constants.
+  void __sanitizer_cov_trace_switch(uint64_t Val, uint64_t *Cases);
+
+  // Called before a division statement.
+  // Val is the second argument of division.
+  void __sanitizer_cov_trace_div4(uint32_t Val);
+  void __sanitizer_cov_trace_div8(uint64_t Val);
+
+  // Called before a GetElementPtr (GEP) instruction
+  // for every non-constant array index.
+  void __sanitizer_cov_trace_gep(uintptr_t Idx);
+
+  // Called before a load of appropriate size. Addr is the address of the load.
+  void __sanitizer_cov_load1(uint8_t *addr);
+  void __sanitizer_cov_load2(uint16_t *addr);
+  void __sanitizer_cov_load4(uint32_t *addr);
+  void __sanitizer_cov_load8(uint64_t *addr);
+  void __sanitizer_cov_load16(__int128 *addr);
+  // Called before a store of appropriate size. Addr is the address of the store.
+  void __sanitizer_cov_store1(uint8_t *addr);
+  void __sanitizer_cov_store2(uint16_t *addr);
+  void __sanitizer_cov_store4(uint32_t *addr);
+  void __sanitizer_cov_store8(uint64_t *addr);
+  void __sanitizer_cov_store16(__int128 *addr);
+
+Tracking function arguments and return values
+==============================================
+
+With ``-fsanitize-coverage=trace-args`` and ``-fsanitize-coverage=trace-ret``
+the compiler will insert callbacks at function entry and before return instructions
+to track function arguments and return values, respectively.
+
+These flags are designed for the Linux kernel's KCOV dataflow subsystem, which uses
+the callbacks to capture struct field values for memory corruption analysis.
+
+When debug info is available (``-g``), the compiler uses ``DICompositeType`` metadata
+to extract struct field layouts (byte offset and size pairs). A FNV-1a hash of the
+struct type name is prepended to the offsets array for identification.
+
+Debug info is *not* required. Without ``-g`` the pass falls back to tracing each IR
+argument (and the return value) directly as an opaque scalar: ``offsets`` is null and
+``num_fields`` is zero, so struct fields are not decomposed, but the value itself is
+still reported. This keeps ``trace-args``/``trace-ret`` usable on optimized userspace
+or Rust code built without debug info; only the source-level field breakdown is lost.
+
+The argument value is read at the function-entry insertion point, which precedes the
+prologue stores of the incoming arguments to their stack slots at ``-O0``. Build with
+optimization (``-O1`` or higher) for the reported argument values to be meaningful;
+return values are captured correctly at any optimization level.
+
+``arg_idx`` is the *source-level* parameter index, not the IR argument position.
+The two diverge whenever the ABI rewrites the argument list, and the reported index
+follows the source. Specifically, the pass maps IR values back to source parameters
+through ``DILocalVariable::getArg()``, which the frontend assigns before ABI lowering:
+
+* Hidden ABI-inserted pointers (a struct-return ``sret`` pointer, a C++ ``this``
+  that has no source entry) carry no ``arg`` number and are not counted.
+* A by-value struct that the ABI coerces or splits into several IR arguments is
+  reassembled from its ``DW_OP_LLVM_fragment`` pieces into a single stack slot in
+  source layout, so one source parameter yields one callback rather than N.
+
+For return values, a by-value struct that is lowered to an indirect (``sret``)
+return produces an IR function that returns ``void``; the ``trace-ret`` callback
+then reports the caller-provided ``sret`` buffer as the return value, so indirect
+returns are captured rather than dropped.
+
+Both flags imply edge coverage when used alone.
+
+.. code-block:: c++
+
+  // Called at function entry, once per source-level argument.
+  // pc: address of the instrumented function
+  // arg_idx: zero-based source-level argument index (ABI-stable; hidden
+  //          sret/this pointers are not counted)
+  // arg_size: size of the argument in bytes
+  // ptr: pointer to the argument value (stack-spilled for scalars, reassembled
+  //      into a stack slot for ABI-decomposed structs)
+  // offsets: array of [byte_offset, byte_size] pairs for struct fields (null if not a struct)
+  // num_fields: number of struct fields (0 if not a struct)
+  void __sanitizer_cov_trace_args(uint64_t pc, uint32_t arg_idx, uint32_t arg_size,
+                                  void *ptr, uint64_t *offsets, uint32_t num_fields);
+
+  // Called before each return instruction.
+  // pc: address of the instrumented function
+  // ret_size: size of the return value in bytes
+  // ptr: pointer to the return value (stack-spilled for scalars; the sret
+  //      buffer for indirect struct returns; null for void)
+  // offsets: array of [byte_offset, byte_size] pairs for struct fields (null if not a struct)
+  // num_fields: number of struct fields (0 if not a struct)
+  void __sanitizer_cov_trace_ret(uint64_t pc, uint32_t ret_size,
+                                 void *ptr, uint64_t *offsets, uint32_t num_fields);
+
+Both ``ptr`` and ``offsets`` may be null (a value the pass could not spill, a void
+return); a consumer must null-check before dereferencing.
+
+Userspace runtime
+-----------------
+
+``compiler-rt`` provides weak, empty default definitions of both callbacks (in
+``sanitizer_common``), so a program compiled with ``trace-args``/``trace-ret`` links
+even without any runtime consuming the data. A user runtime overrides the weak
+default with a strong definition to observe the values.
+
+libFuzzer implements the callbacks (``TracePC::HandleDataflow``) and folds each
+observed value into the value-profile map. Enable it with ``-use_value_profile=1``;
+every distinct argument/return value then contributes a value-profile feature, so a
+new argument or return value that a code path has never produced before counts as new
+coverage and is added to the corpus. The fold mixes the PC, the argument/return
+location, and the field id, so the same value seen at different sites stays distinct.
+
+Kernel builds consume the same callbacks through the KCOV dataflow subsystem instead
+of compiler-rt.
+
+x86 base-pointer note
+---------------------
+
+To report a scalar value, the pass may spill it to a stack slot and pass the slot
+address. On x86-64, a function whose inline assembly clobbers the base pointer
+(``rbx``/``ebx``/``bx``, e.g. a ``cpuid`` with an ``=b`` constraint, or ``rdtsc``)
+cannot also take an escaping, stack-realigned spill slot without the base pointer and
+the realignment interfering. For those functions the pass skips the spill and passes a
+null ``ptr`` for that scalar (the ``pc``/``arg_idx`` are still reported); struct
+arguments already backed by an ``alloca`` are unaffected. This detection is
+x86-specific; other targets always spill.
+
+Tracing control flow
+====================
+
+With ``-fsanitize-coverage=control-flow`` the compiler will create a table to collect
+control flow for each function. More specifically, for each basic block in the function,
+two lists are populated. One list for successors of the basic block and another list for
+non-intrinsic called functions.
+
+**TODO:** in the current implementation, indirect calls are not tracked
+and are only marked with special value (-1) in the list.
+
+Each table row consists of the basic block address
+followed by ``null``-ended lists of successors and callees.
+The table is encoded in a special section named ``sancov_cfs``
+
+Example:
+
+.. code-block:: c++
+
+  int foo (int x) {
+    if (x > 0)
+      bar(x);
+    else
+      x = 0;
+    return x;
+  }
+
+The code above contains 4 basic blocks, let's name them A, B, C, D:
+
+.. code-block:: none
+
+    A
+    |\
+    | \
+    B  C
+    | /
+    |/
+    D
+
+The collected control flow table is as follows:
+``A, B, C, null, null, B, D, null, @bar, null, C, D, null, null, D, null, null.``
+
+Users need to implement a single function to capture the CF table at startup:
+
+.. code-block:: c++
+
+  extern "C"
+  void __sanitizer_cov_cfs_init(const uintptr_t *cfs_beg,
+                                const uintptr_t *cfs_end) {
+    // [cfs_beg,cfs_end) is the array of ptr-sized integers representing
+    // the collected control flow.
+  }
+
+Tracing Stack Depth
+===================
+
+With ``-fsanitize-coverage=stack-depth`` the compiler will track how much
+stack space has been used for a function call chain. Leaf functions are
+not included in this tracing.
+
+The maximum depth of a function call graph is stored in the thread-local
+``__sancov_lowest_stack`` variable. Instrumentation is inserted in every
+non-leaf function to check the frame pointer against this variable,
+and if it is lower, store the current frame pointer. This effectively
+inserts the following:
+
+.. code-block:: c++
+
+  extern thread_local uintptr_t __sancov_lowest_stack;
+
+  uintptr_t stack = (uintptr_t)__builtin_frame_address(0);
+  if (stack < __sancov_lowest_stack)
+    __sancov_lowest_stack = stack;
+
+If ``-fsanitize-coverage-stack-depth-callback-min=N`` (where
+``N > 0``) is also used, the tracking is delegated to a callback,
+``__sanitizer_cov_stack_depth``, instead of adding instrumentation to
+update ``__sancov_lowest_stack``. The ``N`` of the argument is used
+to determine which functions to instrument. Only functions estimated
+to be using ``N`` bytes or more of stack space will be instrumented to
+call the tracing callback. In the case of a dynamically sized stack,
+the callback is unconditionally added.
+
+The callback takes no arguments and is responsible for determining
+the stack usage and doing any needed comparisons and storage. A roughly
+equivalent implementation of ``__sancov_lowest_stack`` using the callback
+would look like this:
+
+.. code-block:: c++
+
+  void __sanitizer_cov_stack_depth(void) {
+    uintptr_t stack = (uintptr_t)__builtin_frame_address(0);
+
+    if (stack < __sancov_lowest_stack)
+      __sancov_lowest_stack = stack;
+  }
+
+Gated Trace Callbacks
+=====================
+
+Gate the invocation of the tracing callbacks with
+``-sanitizer-coverage-gated-trace-callbacks``.
+
+When this option is enabled, the instrumentation will not call into the
+runtime-provided callbacks for tracing, thus only incurring in a trivial
+branch without going through a function call.
+
+It is up to the runtime to toggle the value of the global variable in order to
+enable tracing.
+
+This option is only supported for trace-pc-guard and trace-cmp.
+
+Disabling instrumentation with ``__attribute__((no_sanitize("coverage")))``
+===========================================================================
+
+It is possible to disable coverage instrumentation for select functions via the
+function attribute ``__attribute__((no_sanitize("coverage")))``. Because this
+attribute may not be supported by other compilers, it is recommended to use it
+together with ``__has_feature(coverage_sanitizer)``.
+
+Disabling instrumentation without source modification
+=====================================================
+
+It is sometimes useful to tell SanitizerCoverage to instrument only a subset of the
+functions in your target without modifying source files.
+With ``-fsanitize-coverage-allowlist=allowlist.txt``
+and ``-fsanitize-coverage-ignorelist=blocklist.txt``,
+you can specify such a subset through the combination of an allowlist and a blocklist.
+
+SanitizerCoverage will only instrument functions that satisfy two conditions.
+First, the function should belong to a source file with a path that is both allowlisted
+and not blocklisted.
+Second, the function should have a mangled name that is both allowlisted and not blocklisted.
+
+The allowlist and blocklist format is similar to that of the sanitizer blocklist format.
+The default allowlist will match every source file and every function.
+The default blocklist will match no source file and no function.
+
+A common use case is to have the allowlist list folders or source files for which you want
+instrumentation and allow all function names, while the blocklist will opt out some specific
+files or functions that the allowlist loosely allowed.
+
+Here is an example allowlist:
+
+.. code-block:: none
+
+  # Enable instrumentation for a whole folder
+  src:bar/*
+  # Enable instrumentation for a specific source file
+  src:foo/a.cpp
+  # Enable instrumentation for all functions in those files
+  fun:*
+
+And an example blocklist:
+
+.. code-block:: none
+
+  # Disable instrumentation for a specific source file that the allowlist allowed
+  src:bar/b.cpp
+  # Disable instrumentation for a specific function that the allowlist allowed
+  fun:*myFunc*
+
+The use of ``*`` wildcards above is required because function names are matched after mangling.
+Without the wildcards, one would have to write the whole mangled name.
+
+Be careful that the paths of source files are matched exactly as they are provided on the clang
+command line.
+For example, the allowlist above would include file ``bar/b.cpp`` if the path was provided
+exactly like this, but would it would fail to include it with other ways to refer to the same
+file such as ``./bar/b.cpp``, or ``bar\b.cpp`` on Windows.
+So, please make sure to always double check that your lists are correctly applied.
+
+Default implementation
+======================
+
+The sanitizer run-time (AddressSanitizer, MemorySanitizer, etc) provide a
+default implementations of some of the coverage callbacks.
+You may use this implementation to dump the coverage on disk at the process
+exit.
+
+Example:
+
+.. code-block:: console
+
+    % cat -n cov.cc
+         1  #include <stdio.h>
+         2  __attribute__((noinline))
+         3  void foo() { printf("foo\n"); }
+         4
+         5  int main(int argc, char **argv) {
+         6    if (argc == 2)
+         7      foo();
+         8    printf("main\n");
+         9  }
+    % clang++ -g cov.cc -fsanitize=address -fsanitize-coverage=trace-pc-guard
+    % ASAN_OPTIONS=coverage=1 ./a.out; wc -c *.sancov
+    main
+    SanitizerCoverage: ./a.out.7312.sancov 2 PCs written
+    24 a.out.7312.sancov
+    % ASAN_OPTIONS=coverage=1 ./a.out foo ; wc -c *.sancov
+    foo
+    main
+    SanitizerCoverage: ./a.out.7316.sancov 3 PCs written
+    24 a.out.7312.sancov
+    32 a.out.7316.sancov
+
+Every time you run an executable instrumented with SanitizerCoverage
+one ``*.sancov`` file is created during the process shutdown.
+If the executable is dynamically linked against instrumented DSOs,
+one ``*.sancov`` file will be also created for every DSO.
+
+Sancov data format
+------------------
+
+The format of ``*.sancov`` files is very simple: the first 8 bytes is the magic,
+one of ``0xC0BFFFFFFFFFFF64`` and ``0xC0BFFFFFFFFFFF32``. The last byte of the
+magic defines the size of the following offsets. The rest of the data is the
+offsets in the corresponding binary/DSO that were executed during the run.
+
+Sancov Tool
+-----------
+
+A simple ``sancov`` tool is provided to process coverage files.
+The tool is part of LLVM project and is currently supported only on Linux.
+It can handle symbolization tasks autonomously without any extra support
+from the environment. You need to pass .sancov files (named
+``<module_name>.<pid>.sancov`` and paths to all corresponding binary elf files.
+Sancov matches these files using module names and binaries file names.
+
+.. code-block:: console
+
+    USAGE: sancov [options] <action> <binary files...> <.sancov files...> <.symcov files...>
+
+    Action (required):
+      -covered-functions     Print all covered funcions.
+      -diff                  Compute difference between two sancov files (A - B) and write to the new output sancov file
+      -html-report           REMOVED. Use -symbolize & coverage-report-server.py.
+      -merge                 Merges reports.
+      -not-covered-functions Print all not covered funcions.
+      -print-coverage-pcs    Print coverage instrumentation points addresses.
+      -print-coverage-stats  Print coverage statistics.
+      -print                 Print coverage addresses
+      -symbolize             Produces a symbolized JSON report from binary report.
+      -union                 Compute union of multiple sancov files and write to the new output sancov file
+
+    Generic Options:
+      -help    Display this help
+      -h       Alias for --help
+      -version Display the version
+      -v       Alias for --version
+
+    OPTIONS:
+      -demangle=0          Alias for --no-demangle
+      -demangle            Demangle function names
+      -ignorelist=<string> Ignorelist file (sanitizer ignorelist format)
+      -no-demangle         Do not demangle function names
+      -no-skip-dead-files  List dead source files in reports
+      -output=<string>     Output file for diff and union actions
+      -skip-dead-files=0   Alias for --no-skip-dead-files
+      -skip-dead-files     Do not list dead source files in reports
+      -strip_path_prefix=<string>
+                          Strip this prefix from files paths in reports
+      -use_default_ignorelist=0
+                          Alias for --no-use_default_ignore_list
+
+
+Coverage Reports
+----------------
+
+**Experimental**
+
+``.sancov`` files do not contain enough information to generate a source-level
+coverage report. The missing information is contained
+in debug info of the binary. Thus the ``.sancov`` has to be symbolized
+to produce a ``.symcov`` file first:
+
+.. code-block:: console
+
+    sancov -symbolize my_program.123.sancov my_program > my_program.123.symcov
+
+The ``.symcov`` file can be browsed overlaid over the source code by
+running ``tools/sancov/coverage-report-server.py`` script that will start
+an HTTP server.
+
+Output directory
+----------------
+
+By default, .sancov files are created in the current working directory.
+This can be changed with ``ASAN_OPTIONS=coverage_dir=/path``:
+
+.. code-block:: console
+
+    % ASAN_OPTIONS="coverage=1:coverage_dir=/tmp/cov" ./a.out foo
+    % ls -l /tmp/cov/*sancov
+    -rw-r----- 1 kcc eng 4 Nov 27 12:21 a.out.22673.sancov
+    -rw-r----- 1 kcc eng 8 Nov 27 12:21 a.out.22679.sancov



More information about the cfe-commits mailing list