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

>From 8746337f192b86d625b6631c0091644f36f582eb Mon Sep 17 00:00:00 2001
From: Yunseong Kim <[email protected]>
Date: Sun, 7 Jun 2026 20:10:32 +0200
Subject: [PATCH 1/4] [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 6cce4ada1dfd1..0ac95b143fc84 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 e43112b4bb98b..3687731f8d629 100644
--- a/clang/include/clang/Basic/CodeGenOptions.h
+++ b/clang/include/clang/Basic/CodeGenOptions.h
@@ -655,7 +655,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 4fd892e58df86..5170d731d7613 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -8069,6 +8069,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 74ebd0bf375d3..88f4440dc61f9 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 {
@@ -1043,6 +1045,7 @@ SanitizerArgs::SanitizerArgs(const ToolChain &TC,
   int InstrumentationTypes = CoverageTracePC | CoverageTracePCEntryExit |
                              CoverageTracePCGuard | CoverageInline8bitCounters 
|
                              CoverageTraceLoads | CoverageTraceStores |
+                             CoverageTraceArgs | CoverageTraceRet |
                              CoverageInlineBoolFlag | CoverageControlFlow;
   if ((CoverageFeatures & InsertionPointTypes) &&
       !(CoverageFeatures & InstrumentationTypes) && DiagnoseErrors) {
@@ -1054,9 +1057,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)
@@ -1417,6 +1421,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)
@@ -1804,6 +1810,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 51e0af00bb2b57c2c135034536a6ec051690bfe1 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <[email protected]>
Date: Sun, 7 Jun 2026 20:10:38 +0200
Subject: [PATCH 2/4] [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 1a2077db43d81a1ed0ba05c0b4bf879d95e89c62 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <[email protected]>
Date: Sun, 7 Jun 2026 20:10:45 +0200
Subject: [PATCH 3/4] [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 7fd67456defbe63d310f763c052a6cee69347f42 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <[email protected]>
Date: Sun, 7 Jun 2026 20:10:50 +0200
Subject: [PATCH 4/4] [SanitizerCoverage] Document trace-args/trace-ret modes

Add documentation for the new coverage modes to
SanitizerCoverage.rst including callback signatures and usage.
---
 clang/docs/SanitizerCoverage.rst | 39 ++++++++++++++++++++++++++++++++
 1 file changed, 39 insertions(+)

diff --git a/clang/docs/SanitizerCoverage.rst b/clang/docs/SanitizerCoverage.rst
index c01863adebb2d..11e59fe6166b3 100644
--- a/clang/docs/SanitizerCoverage.rst
+++ b/clang/docs/SanitizerCoverage.rst
@@ -348,6 +348,45 @@ 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.
+
+Both flags imply edge coverage when used alone.
+
+.. code-block:: c++
+
+  // Called at function entry for each argument.
+  // pc: address of the instrumented function
+  // arg_idx: zero-based argument index
+  // arg_size: size of the argument in bytes
+  // ptr: pointer to the argument value (stack-spilled for scalars)
+  // offsets: array of [byte_offset, byte_size] pairs for struct fields (null 
if not a struct pointer)
+  // num_fields: number of struct fields (0 if not a struct pointer)
+  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, null for 
void)
+  // offsets: array of [byte_offset, byte_size] pairs for struct fields (null 
if not a struct pointer)
+  // num_fields: number of struct fields (0 if not a struct pointer)
+  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