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

>From a48c22ae39ce7be294c21b008c59fb96685050bb Mon Sep 17 00:00:00 2001
From: Yunseong Kim <[email protected]>
Date: Sun, 7 Jun 2026 20:10:32 +0200
Subject: [PATCH 1/6] [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
---
 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 5f3baf771ff96..6dcb2baf7968e 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 97d68877467fd..04351f1156315 100644
--- a/clang/include/clang/Basic/CodeGenOptions.h
+++ b/clang/include/clang/Basic/CodeGenOptions.h
@@ -663,7 +663,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 4fc9f4d4c3472..e2f9c4223d421 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -8119,6 +8119,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 a46a25c4492f2..763d5a7a92e07 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -251,6 +251,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 bf77e748ebd0c..0bbbfefbc7acb 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 {
@@ -1042,6 +1044,7 @@ SanitizerArgs::SanitizerArgs(const ToolChain &TC,
   int InstrumentationTypes = CoverageTracePC | CoverageTracePCEntryExit |
                              CoverageTracePCGuard | CoverageInline8bitCounters 
|
                              CoverageTraceLoads | CoverageTraceStores |
+                             CoverageTraceArgs | CoverageTraceRet |
                              CoverageInlineBoolFlag | CoverageControlFlow;
   if ((CoverageFeatures & InsertionPointTypes) &&
       !(CoverageFeatures & InstrumentationTypes) && DiagnoseErrors) {
@@ -1053,9 +1056,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)
@@ -1416,6 +1420,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)
@@ -1803,6 +1809,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 8772358424a871da585c407025ff8ce2fbd0d747 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <[email protected]>
Date: Sun, 7 Jun 2026 20:10:38 +0200
Subject: [PATCH 2/6] [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
---
 .../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 b13b4233937db1970f6ad666b168d165316ab2b9 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <[email protected]>
Date: Sun, 7 Jun 2026 20:10:45 +0200
Subject: [PATCH 3/6] [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
---
 .../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 e89ec35f5553e6e6328b0def3e46143fe9a5d679 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <[email protected]>
Date: Wed, 24 Jun 2026 19:46:26 +0200
Subject: [PATCH 4/6] [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 <[email protected]>
---
 .../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 c60c0383306161894c16955bbdfbcc8093115b02 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <[email protected]>
Date: Tue, 7 Jul 2026 16:06:08 +0200
Subject: [PATCH 5/6] [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 <[email protected]>
---
 .../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 00b912945c2e75fc23e1e74a89f17a619a98c5b4 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <[email protected]>
Date: Sun, 7 Jun 2026 20:10:50 +0200
Subject: [PATCH 6/6] [SanitizerCoverage] Document trace-args/trace-ret modes

Add documentation for the new coverage modes to SanitizerCoverage.rst,
including the callback signatures and their intended use by the Linux
kernel KCOV dataflow subsystem.

The description follows the ABI-correct implementation: 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.

Signed-off-by: Yunseong Kim <[email protected]>
---
 clang/docs/SanitizerCoverage.rst | 58 ++++++++++++++++++++++++++++++++
 1 file changed, 58 insertions(+)

diff --git a/clang/docs/SanitizerCoverage.rst b/clang/docs/SanitizerCoverage.rst
index c01863adebb2d..a9456f8bf681e 100644
--- a/clang/docs/SanitizerCoverage.rst
+++ b/clang/docs/SanitizerCoverage.rst
@@ -348,6 +348,64 @@ will not be instrumented.
   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. Without 
``-g``,
+the pass returns early and no callbacks are emitted.
+
+``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);
+
+
 Tracing control flow
 ====================
 

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to