https://github.com/yskzalloc created 
https://github.com/llvm/llvm-project/pull/218254

Add driver and frontend support for the `trace-args`/`trace-ret` 
SanitizerCoverage modes, plus user-facing documentation.

This is PR 2 of a 3-PR stack. the diff shows only the clang changes:

1. Depends on #201410 (llvm/): the instrumentation pass and IR tests.
2. This PR (clang/): driver/frontend wiring and documentation
3. Follow-up (compiler-rt/): weak default callbacks and a libFuzzer 
value-profile consumer

### Changes

With this PR the modes added in PR 1 become usable as:

```
clang -fsanitize-coverage=trace-args,trace-ret ...
```

- `CodeGenOptions.def` / `CodeGenOptions.h`: `SanitizeCoverageTraceArgs` / 
`SanitizeCoverageTraceRet` codegen flags
- `Options.td` / `SanitizerArgs.cpp`: parse `trace-args` and `trace-ret` in 
`-fsanitize-coverage=`
- `BackendUtil.cpp`: forward the flags to `SanitizerCoverageOptions` 
(`TraceArgs` / `TraceRet`)
- `docs/SanitizerCoverage.md`: a new "Tracking function arguments and return 
values" section documenting the callback signatures and null-safety contract, 
the ABI behavior (source-level `arg_idx` via `DILocalVariable::getArg()`, 
struct reassembly, `sret` return capture), debug-info handling 
(`DICompositeType` field layout with `-g`, opaque-scalar fallback without; 
`-O1+` needed for meaningful argument values), and the x86 base-pointer 
spill-skip

The "Userspace runtime" documentation subsection lands with the compiler-rt PR, 
alongside the runtime it describes.

### Testing

- `clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c`: the CodeGen 
pipeline emits `__sanitizer_cov_trace_args` / `__sanitizer_cov_trace_ret` calls
- `clang/test/Driver/fsanitize-coverage.c`: driver acceptance of the new values
- Passes `check-clang` and `clang-format`

>From 4fd86f38d499b215dec9959b6688d3ff7884677d Mon Sep 17 00:00:00 2001
From: Yunseong Kim <[email protected]>
Date: Sun, 23 Aug 2026 12:27:09 +0200
Subject: [PATCH 1/2] [SanitizerCoverage] Add trace-args and trace-ret
 instrumentation modes

Add two new SanitizerCoverage modes, enabled via SanitizerCoverageOptions
(TraceArgs/TraceRet) and, for opt, the -sanitizer-coverage-trace-args /
-sanitizer-coverage-trace-ret flags:

  __sanitizer_cov_trace_args(u64 pc, u32 arg_idx, u32 arg_size,
                             ptr val, ptr offsets, u32 num_fields)
  __sanitizer_cov_trace_ret(u64 pc, u32 ret_size,
                            ptr val, ptr offsets, u32 num_fields)

trace-args inserts a callback at function entry for every source-level
argument; trace-ret inserts one before every return. Consumers (the
Linux kernel's KCOV dataflow subsystem, libFuzzer value profiling) use
them to capture argument/return values with automatic struct field
expansion. Both modes imply edge coverage when used alone.

With debug info, struct field layouts ([byte_offset, byte_size] pairs)
are extracted from DICompositeType and emitted as a constant global
prefixed with an FNV-1a hash of the type name; scalar values are
spilled to an alloca so the callback uniformly takes a pointer.

arg_idx is the source-level parameter index: IR values are mapped back
to source parameters via DILocalVariable::getArg(), which the frontend
assigns before ABI lowering, so the index is stable when the ABI
rewrites the argument list. Hidden sret/this pointers are not counted,
and a by-value struct split into several IR arguments is reassembled
from its DW_OP_LLVM_fragment pieces into one stack slot in source
layout. A struct return lowered to an indirect (sret) return is
reported through the caller-provided sret buffer rather than dropped.
If the DISubroutineType names more source parameters than the debug
records yield (argument optimized away), a null-pointer trace is
emitted so consumers still see the argument's existence.

Debug info is not required: without a DISubprogram the pass falls back
to tracing each IR argument (and the return value) as an opaque scalar
(offsets=null, num_fields=0), which keeps the modes usable for
optimized userspace and Rust builds; only the field breakdown is lost.

Robustness, from instrumenting a whole KASAN kernel with both modes:

- Trace calls are emitted through InstrumentationIRBuilder so they
  carry a !dbg location; a plain builder tripped the verifier
  ("inlinable function call ... requires a !dbg location") under -g
  once inlining/LTO ran.

- A #dbg_value may reference an instruction defined after the entry
  block (debug records are exempt from SSA dominance); using it as the
  trace pointer produced IR failing "Instruction does not dominate all
  uses" and crashed RegisterCoalescer. Such values are skipped and the
  parameter is traced as a null pointer.

- Argument fragments are de-duplicated by (value, bit-offset) so a
  repeated debug record does not make a scalar look like a
  multi-fragment struct.

- On x86-64, the escaping spill allocas get ASan redzones and force
  frame realignment, which needs a base pointer (RBX); in functions
  whose inline asm also clobbers RBX (CPUID "=b", RDTSC) the backend
  rejects this ("Interference usage of base pointer/frame pointer").
  functionInlineAsmUsesBasePointerX86() detects those functions and
  skips the spill, passing a null value pointer instead.

Tests cover basic operation of both callbacks, ABI index remapping and
struct reassembly, sret return capture, !dbg presence, the no-debug
fallback, the dominance guard, and the base-pointer skip.

Clang driver support (-fsanitize-coverage=trace-args,trace-ret) and the
compiler-rt runtime land in follow-up patches.

Signed-off-by: Yunseong Kim <[email protected]>
---
 .../llvm/Transforms/Utils/Instrumentation.h   |   2 +
 .../Instrumentation/SanitizerCoverage.cpp     | 503 +++++++++++++++++-
 .../SanitizerCoverage/trace-args-abi.ll       |  88 +++
 .../SanitizerCoverage/trace-args-dominance.ll |  50 ++
 .../SanitizerCoverage/trace-args-no-debug.ll  |  21 +
 .../SanitizerCoverage/trace-args.ll           |  43 ++
 .../trace-ret-basepointer.ll                  |  51 ++
 .../SanitizerCoverage/trace-ret.ll            |  83 +++
 8 files changed, 840 insertions(+), 1 deletion(-)
 create mode 100644 
llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll
 create mode 100644 
llvm/test/Instrumentation/SanitizerCoverage/trace-args-dominance.ll
 create mode 100644 
llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-debug.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
 create mode 100644 
llvm/test/Instrumentation/SanitizerCoverage/trace-ret-basepointer.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll

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..e55b13d2cf077 100644
--- a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
+++ b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
@@ -15,14 +15,18 @@
 #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/DebugInfo.h"
 #include "llvm/IR/Dominators.h"
 #include "llvm/IR/EHPersonalities.h"
 #include "llvm/IR/Function.h"
 #include "llvm/IR/GlobalVariable.h"
 #include "llvm/IR/IRBuilder.h"
+#include "llvm/IR/InlineAsm.h"
 #include "llvm/IR/IntrinsicInst.h"
 #include "llvm/IR/Intrinsics.h"
 #include "llvm/IR/LLVMContext.h"
@@ -46,6 +50,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 +162,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 +241,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 +283,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 +318,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 +563,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 +798,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 +1303,467 @@ 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) 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)
+    return {nullptr, 0};
+
+  Ty = stripDITypedefs(Ty);
+
+  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);
+
+  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)};
+}
+
+// x86 uses RBX as the frame base pointer for a realigned stack. A function 
whose
+// inline asm reads/writes/clobbers RBX cannot also use it as the base pointer 
-- the
+// backend rejects it ("Interference usage of base pointer/frame pointer"). The
+// trace-args/trace-ret spill allocas below escape (their address is passed to 
the
+// trace call), so ASAN redzones them, forcing 32-byte frame realignment => a 
base
+// pointer. In a function that already ties up RBX in inline asm (e.g. CPUID's 
"=b",
+// RDTSC's "~{rbx}") that is a hard error. Detect it and skip the escaping 
spill for
+// such functions (the arg/ret is traced as a null pointer instead). The 
base-pointer
+// register is spelled {rbx}/{ebx}/{bx} (and byte views {bl}/{bh}) in 
constraint codes.
+static bool functionInlineAsmUsesBasePointerX86(const Function &F) {
+  for (const BasicBlock &BB : F)
+    for (const Instruction &I : BB) {
+      const auto *CB = dyn_cast<CallBase>(&I);
+      if (!CB || !CB->isInlineAsm())
+        continue;
+      const auto *IA = dyn_cast<InlineAsm>(CB->getCalledOperand());
+      if (!IA)
+        continue;
+      for (const InlineAsm::ConstraintInfo &CI : IA->ParseConstraints())
+        for (StringRef Code : CI.Codes)
+          if (Code == "{rbx}" || Code == "{ebx}" || Code == "{bx}" ||
+              Code == "{bl}" || Code == "{bh}")
+            return true;
+    }
+  return false;
+}
+
+void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
+  // SP may be null: a function compiled WITHOUT debug info (-g) carries no
+  // #dbg_value records, so the source-argument map below stays empty and the
+  // SrcArgs.empty() fallback traces each IR argument directly (no source-level
+  // struct-field offsets). This lets trace-args work on userspace / optimized
+  // code built without -g, not just debug kernels.
+  DISubprogram *SP = F.getSubprogram();
+  // Only guards x86; on other targets there is no RBX base-pointer 
interference.
+  const bool SkipSpill = TargetTriple.getArch() == Triple::x86_64 &&
+                         functionInlineAsmUsesBasePointerX86(F);
+
+  BasicBlock &EntryBB = F.getEntryBlock();
+  Instruction *InsertPt = &*EntryBB.getFirstInsertionPt();
+  InstrumentationIRBuilder IRB(InsertPt);
+
+  // Get PC as the function address cast to i64
+  Value *PC = IRB.CreatePtrToInt(&F, Int64Ty);
+
+  // 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;
+
+  // 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()) {
+    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;
+      addFragment(SA, &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
+      // A #dbg_value may point at a value that does NOT dominate the 
entry-block
+      // terminator where the trace call is inserted (debug records are exempt 
from
+      // SSA dominance). Using such a value as ArgPtr emits IR that fails the 
verifier
+      // ("Instruction does not dominate all uses") and, with 
-disable-llvm-verifier
+      // (kernel builds), reaches codegen and crashes 
RegisterCoalescer::reMaterializeDef.
+      // The insertion point is the entry terminator, so a value defined in 
the entry
+      // block dominates it; anything else (a later-block instruction, e.g. a 
field
+      // getelementptr) does not -> skip it and let the dead-arg fallback emit 
a null
+      // trace for this param.
+      if (auto *VI = dyn_cast<Instruction>(V))
+        if (VI->getParent() != &EntryBB)
+          continue;
+      auto &SA = SrcArgs[SrcIdx];
+      SA.Var = Var;
+      SA.Ty = Var->getType();
+      uint64_t FragBitOff = 0;
+      if (auto Frag = DVar->getExpression()->getFragmentInfo())
+        FragBitOff = Frag->OffsetInBits;
+      addFragment(SA, 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 && 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;
+      bool Spilled = false;
+      if (Arg.getType()->isPointerTy()) {
+        ArgPtr = &Arg;
+      } else if (SkipSpill) {
+        ArgPtr = Constant::getNullValue(PtrTy); // base-pointer asm: no 
escaping spill
+      } else {
+        AllocaInst *Alloca = IRB.CreateAlloca(Arg.getType());
+        IRB.CreateStore(&Arg, Alloca);
+        ArgPtr = Alloca;
+        Spilled = true;
+      }
+      unsigned ArgByteSize =
+          Arg.getType()->isPointerTy() ? DL->getPointerSize()
+          : Spilled                    ? DL->getTypeStoreSize(Arg.getType())
+                                       : 0;
+      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);
+
+  // 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];
+    auto [OffsetsGV, NumFields] = getStructFieldOffsets(SA.Ty, M, *DL);
+
+    Value *ArgPtr;
+    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 if (SkipSpill) {
+        ArgPtr = Constant::getNullValue(PtrTy); // base-pointer asm: no 
escaping spill
+        ArgByteSize = 0;
+      } else {
+        AllocaInst *Alloca = IRB.CreateAlloca(V->getType());
+        TraceIRB.CreateStore(V, Alloca);
+        ArgPtr = Alloca;
+        ArgByteSize = DL->getTypeStoreSize(V->getType());
+      }
+    } else if (SkipSpill) {
+      ArgPtr = Constant::getNullValue(PtrTy); // base-pointer asm: no escaping 
spill
+      ArgByteSize = 0;
+    } else {
+      // Multiple IR args for one source param (ABI struct decomposition).
+      // Reassemble fragments into a stack slot matching the source layout.
+      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;
+    }
+
+    Value *OffsetsPtr = Constant::getNullValue(PtrTy);
+    if (OffsetsGV) {
+      Value *Indices[] = {ConstantInt::get(Int64Ty, 0),
+                          ConstantInt::get(Int64Ty, 1)};
+      OffsetsPtr = TraceIRB.CreateInBoundsGEP(OffsetsGV->getValueType(),
+                                              OffsetsGV, Indices);
+    }
+    // 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)});
+  }
+
+  // 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 && 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)});
+      }
+    }
+  }
+}
+
+void ModuleSanitizerCoverage::InjectTraceForRet(Function &F) {
+  DISubprogram *SP = F.getSubprogram();
+
+  // On x86, skip the escaping return-value spill in functions whose inline 
asm ties up
+  // the base pointer (RBX): the spill alloca is ASAN-redzoned -> stack 
realignment ->
+  // base pointer, which the backend rejects against the asm's RBX use 
("Interference
+  // usage of base pointer/frame pointer"). Such returns are traced as a null 
pointer.
+  const bool SkipSpill = TargetTriple.getArch() == Triple::x86_64 &&
+                         functionInlineAsmUsesBasePointerX86(F);
+
+  // Get return type debug info
+  DIType *RetDIType = nullptr;
+  if (SP && SP->getType()) {
+    auto TypeArray = SP->getType()->getTypeArray();
+    if (TypeArray.size() > 0)
+      RetDIType = TypeArray[0];
+  }
+
+  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);
+
+    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() && !SkipSpill) {
+      AllocaInst *Alloca = AtExit->CreateAlloca(RetVal->getType());
+      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);
+    }
+
+    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});
+  }
+}
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..5086176c3a363
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll
@@ -0,0 +1,88 @@
+; 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)
+; 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
+
+; 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)
diff --git 
a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-dominance.ll 
b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-dominance.ll
new file mode 100644
index 0000000000000..d3dfb4db8b788
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-dominance.ll
@@ -0,0 +1,50 @@
+; Regression test: trace-args must NOT emit a non-dominating use. The 
entry-block #dbg_value
+; scan may find an argument whose debug location is a value defined LATER in 
the function
+; (debug records are exempt from SSA dominance). Using such a value as the 
trace pointer -
+; the trace call is inserted at the entry-block terminator - produces IR that 
fails the
+; verifier ("Instruction does not dominate all uses") and, with 
-disable-llvm-verifier
+; (kernel builds), crashes RegisterCoalescer::reMaterializeDef at codegen. 
Such an argument
+; is traced as a null pointer instead. opt runs the verifier, so a successful 
run of this
+; test already proves the emitted IR is well-formed (it would error before 
this fix).
+
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 
-sanitizer-coverage-trace-args -S | FileCheck %s
+
+target datalayout = 
"e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+; %arg1's only debug location is %later, a GEP defined in a later block (does 
NOT dominate
+; the entry terminator). %arg2 is a normal directly-located pointer argument.
+define void @arg_loc_not_dominating(ptr %arg1, ptr %arg2) #0 !dbg !8 {
+entry:
+  #dbg_value(ptr %arg2, !13, !DIExpression(), !15)
+  #dbg_value(ptr %later, !12, !DIExpression(), !15)
+  br label %bb, !dbg !15
+bb:
+  %later = getelementptr i8, ptr %arg1, i64 128, !dbg !15
+  ret void, !dbg !15
+}
+; CHECK-LABEL: define void @arg_loc_not_dominating(ptr %arg1, ptr %arg2)
+; arg1 (index 0): its debug location did not dominate -> traced as a null 
pointer, size 0.
+; CHECK-DAG: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr 
@arg_loc_not_dominating to i64), i32 0, i32 0, ptr null, ptr {{.*}}, i32 {{.*}})
+; arg2 (index 1): a normal pointer arg, traced directly.
+; CHECK-DAG: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr 
@arg_loc_not_dominating to i64), i32 1, i32 {{[0-9]+}}, ptr %arg2, ptr {{.*}}, 
i32 {{.*}})
+
+attributes #0 = { nounwind sanitize_address }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, isOptimized: true, 
emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/tmp")
+!2 = !{}
+!3 = !{i32 2, !"Dwarf Version", i32 4}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!6 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !5, size: 64)
+!8 = distinct !DISubprogram(name: "arg_loc_not_dominating", scope: !1, file: 
!1, line: 1, type: !9, unit: !0, retainedNodes: !11)
+!9 = !DISubroutineType(types: !10)
+!10 = !{null, !6, !6}
+!11 = !{!12, !13}
+!12 = !DILocalVariable(name: "arg1", arg: 1, scope: !8, file: !1, line: 1, 
type: !6)
+!13 = !DILocalVariable(name: "arg2", arg: 2, scope: !8, file: !1, line: 1, 
type: !6)
+!15 = !DILocation(line: 1, column: 1, scope: !8)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-debug.ll 
b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-debug.ll
new file mode 100644
index 0000000000000..81f516508cf1b
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-debug.ll
@@ -0,0 +1,21 @@
+; Test that trace-args works WITHOUT debug info (-g). A function with no
+; DISubprogram carries no #dbg_value records, so the source-argument map stays
+; empty and the pass falls back to tracing each IR argument directly (no
+; source-level struct-field offsets). This keeps trace-args usable on userspace
+; / optimized code built without -g, not only debug kernels. opt runs the
+; verifier, so a successful run also proves the emitted IR is well-formed.
+
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 
-sanitizer-coverage-trace-args -S | FileCheck %s
+
+target datalayout = 
"e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+define void @no_debug(ptr %p, i32 %x) {
+entry:
+  ret void
+}
+; CHECK-LABEL: define void @no_debug(ptr %p, i32 %x)
+; pointer arg 0 is traced directly (no spill, no field offsets):
+; CHECK-DAG: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @no_debug 
to i64), i32 0, i32 8, ptr %p, ptr null, i32 0)
+; scalar arg 1 is spilled to a stack slot then traced:
+; CHECK-DAG: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @no_debug 
to i64), i32 1, i32 4, ptr %{{.*}}, ptr null, i32 0)
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-basepointer.ll 
b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret-basepointer.ll
new file mode 100644
index 0000000000000..48839d8d77246
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret-basepointer.ll
@@ -0,0 +1,51 @@
+; Regression test: trace-ret must NOT create an escaping return-value spill 
alloca in a
+; function whose inline asm uses/clobbers the x86 base pointer (RBX). The 
spill alloca's
+; address is passed to __sanitizer_cov_trace_ret, so it escapes; under KASAN 
it is redzoned,
+; forcing 32-byte frame realignment -> a base pointer (RBX), which the X86 
backend then
+; rejects against the asm's RBX use with "Interference usage of base 
pointer/frame pointer".
+; For such functions the return is traced as a null pointer instead.
+
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 
-sanitizer-coverage-trace-ret -S | FileCheck %s
+
+target datalayout = 
"e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+; Scalar return in a function whose inline asm clobbers rbx (cf. CPUID "=b", 
RDTSC "~{rbx}").
+define i32 @ret_scalar_clobbers_rbx(i32 %x) #0 !dbg !8 {
+entry:
+  call void asm sideeffect "nop", "~{rbx},~{dirflag},~{fpsr},~{flags}"() #0, 
!dbg !12
+  ret i32 %x, !dbg !12
+}
+; CHECK-LABEL: define i32 @ret_scalar_clobbers_rbx(i32 %x)
+; CHECK-NOT:   alloca
+; CHECK:       call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr 
@ret_scalar_clobbers_rbx to i64), i32 0, ptr null, ptr null, i32 0)
+; CHECK:       ret i32 %x
+
+; Control: the same scalar return WITHOUT base-pointer asm still spills and 
traces normally.
+define i32 @ret_scalar_plain(i32 %x) #0 !dbg !13 {
+entry:
+  ret i32 %x, !dbg !14
+}
+; CHECK-LABEL: define i32 @ret_scalar_plain(i32 %x)
+; CHECK:       %[[SLOT:.*]] = alloca i32
+; CHECK:       store i32 %x, ptr %[[SLOT]]
+; CHECK:       call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr 
@ret_scalar_plain to i64), i32 4, ptr %[[SLOT]], ptr null, i32 0)
+; CHECK:       ret i32 %x
+
+attributes #0 = { nounwind sanitize_address }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, isOptimized: true, 
emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/tmp")
+!2 = !{}
+!3 = !{i32 2, !"Dwarf Version", i32 4}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!8 = distinct !DISubprogram(name: "ret_scalar_clobbers_rbx", scope: !1, file: 
!1, line: 1, type: !9, unit: !0, retainedNodes: !2)
+!9 = !DISubroutineType(types: !10)
+!10 = !{!5, !5}
+!12 = !DILocation(line: 1, column: 1, scope: !8)
+!13 = distinct !DISubprogram(name: "ret_scalar_plain", scope: !1, file: !1, 
line: 2, type: !9, unit: !0, retainedNodes: !2)
+!14 = !DILocation(line: 2, column: 1, scope: !13)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll 
b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
new file mode 100644
index 0000000000000..f5fd01776acc2
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
@@ -0,0 +1,83 @@
+; 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 }
+%struct.Big = type { i64, i64, 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
+
+; 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}
+!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}
+
+; 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 c44dd13e3201ebc672d43bba235e198ce7c8b952 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <[email protected]>
Date: Sun, 23 Aug 2026 12:27:52 +0200
Subject: [PATCH 2/2] [Clang][SanitizerCoverage] Add and document
 -fsanitize-coverage=trace-args,trace-ret

Wire up driver and frontend support for the trace-args/trace-ret
SanitizerCoverage instrumentation modes added to LLVM in the previous
patch:

- CodeGenOptions.def: SanitizeCoverageTraceArgs/Ret codegen flags
- SanitizerArgs.cpp: parse trace-args/trace-ret in -fsanitize-coverage=
- BackendUtil.cpp: forward the flags to SanitizerCoverageOptions

Document the new modes in a "Tracking function arguments and return
values" section of SanitizerCoverage.md:

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

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

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

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

Tests cover driver acceptance of the new values and the CodeGen
pipeline emitting the callbacks.

Signed-off-by: Yunseong Kim <[email protected]>
---
 clang/docs/SanitizerCoverage.md               | 80 +++++++++++++++++++
 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 +++-
 .../sanitizer-coverage-trace-args-ret.c       | 18 +++++
 clang/test/Driver/fsanitize-coverage.c        | 13 +++
 8 files changed, 138 insertions(+), 4 deletions(-)
 create mode 100644 clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c

diff --git a/clang/docs/SanitizerCoverage.md b/clang/docs/SanitizerCoverage.md
index f859838616b49..e0c77f6a3465f 100644
--- a/clang/docs/SanitizerCoverage.md
+++ b/clang/docs/SanitizerCoverage.md
@@ -333,6 +333,86 @@ void __sanitizer_cov_store8(uint64_t *addr);
 void __sanitizer_cov_store16(__int128 *addr);
 ```
 
+## Tracking function arguments and return values
+
+With `-fsanitize-coverage=trace-args` and `-fsanitize-coverage=trace-ret`
+the compiler will insert callbacks at function entry and before return 
instructions
+to track function arguments and return values, respectively.
+
+These flags are designed for the Linux kernel's KCOV dataflow subsystem, which 
uses
+the callbacks to capture struct field values for memory corruption analysis.
+
+When debug info is available (`-g`), the compiler uses `DICompositeType` 
metadata
+to extract struct field layouts (byte offset and size pairs). A FNV-1a hash of 
the
+struct type name is prepended to the offsets array for identification.
+
+Debug info is *not* required. Without `-g` the pass falls back to tracing each 
IR
+argument (and the return value) directly as an opaque scalar: `offsets` is 
null and
+`num_fields` is zero, so struct fields are not decomposed, but the value 
itself is
+still reported. This keeps `trace-args`/`trace-ret` usable on optimized 
userspace
+or Rust code built without debug info; only the source-level field breakdown 
is lost.
+
+The argument value is read at the function-entry insertion point, which 
precedes the
+prologue stores of the incoming arguments to their stack slots at `-O0`. Build 
with
+optimization (`-O1` or higher) for the reported argument values to be 
meaningful;
+return values are captured correctly at any optimization level.
+
+`arg_idx` is the *source-level* parameter index, not the IR argument position.
+The two diverge whenever the ABI rewrites the argument list, and the reported 
index
+follows the source. Specifically, the pass maps IR values back to source 
parameters
+through `DILocalVariable::getArg()`, which the frontend assigns before ABI 
lowering:
+
+- Hidden ABI-inserted pointers (a struct-return `sret` pointer, a C++ `this`
+  that has no source entry) carry no `arg` number and are not counted.
+- A by-value struct that the ABI coerces or splits into several IR arguments is
+  reassembled from its `DW_OP_LLVM_fragment` pieces into a single stack slot in
+  source layout, so one source parameter yields one callback rather than N.
+
+For return values, a by-value struct that is lowered to an indirect (`sret`)
+return produces an IR function that returns `void`; the `trace-ret` callback
+then reports the caller-provided `sret` buffer as the return value, so indirect
+returns are captured rather than dropped.
+
+Both flags imply edge coverage when used alone.
+
+```c++
+// Called at function entry, once per source-level argument.
+// pc: address of the instrumented function
+// arg_idx: zero-based source-level argument index (ABI-stable; hidden
+//          sret/this pointers are not counted)
+// arg_size: size of the argument in bytes
+// ptr: pointer to the argument value (stack-spilled for scalars, reassembled
+//      into a stack slot for ABI-decomposed structs)
+// offsets: array of [byte_offset, byte_size] pairs for struct fields (null if 
not a struct)
+// num_fields: number of struct fields (0 if not a struct)
+void __sanitizer_cov_trace_args(uint64_t pc, uint32_t arg_idx, uint32_t 
arg_size,
+                                void *ptr, uint64_t *offsets, uint32_t 
num_fields);
+
+// Called before each return instruction.
+// pc: address of the instrumented function
+// ret_size: size of the return value in bytes
+// ptr: pointer to the return value (stack-spilled for scalars; the sret
+//      buffer for indirect struct returns; null for void)
+// offsets: array of [byte_offset, byte_size] pairs for struct fields (null if 
not a struct)
+// num_fields: number of struct fields (0 if not a struct)
+void __sanitizer_cov_trace_ret(uint64_t pc, uint32_t ret_size,
+                               void *ptr, uint64_t *offsets, uint32_t 
num_fields);
+```
+
+Both `ptr` and `offsets` may be null (a value the pass could not spill, a void
+return); a consumer must null-check before dereferencing.
+
+### x86 base-pointer note
+
+To report a scalar value, the pass may spill it to a stack slot and pass the 
slot
+address. On x86-64, a function whose inline assembly clobbers the base pointer
+(`rbx`/`ebx`/`bx`, e.g. a `cpuid` with an `=b` constraint, or `rdtsc`)
+cannot also take an escaping, stack-realigned spill slot without the base 
pointer and
+the realignment interfering. For those functions the pass skips the spill and 
passes a
+null `ptr` for that scalar (the `pc`/`arg_idx` are still reported); struct
+arguments already backed by an `alloca` are unaffected. This detection is
+x86-specific; other targets always spill.
+
 ## Tracing control flow
 
 With `-fsanitize-coverage=control-flow` the compiler will create a table to 
collect
diff --git a/clang/include/clang/Basic/CodeGenOptions.def 
b/clang/include/clang/Basic/CodeGenOptions.def
index bf3e61f2f036f..7950aaf050fe7 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 17f367bc02607..b805e069a109b 100644
--- a/clang/include/clang/Basic/CodeGenOptions.h
+++ b/clang/include/clang/Basic/CodeGenOptions.h
@@ -686,7 +686,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 eb5a009b5628c..588aabe9b1041 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -8318,6 +8318,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 6aa6bc1bd41e8..709eaa3b27ae9 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -265,6 +265,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 778cde8285aaf..1ea3975c8db03 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 {
@@ -1100,6 +1102,7 @@ SanitizerArgs::SanitizerArgs(const ToolChain &TC,
   int InstrumentationTypes = CoverageTracePC | CoverageTracePCEntryExit |
                              CoverageTracePCGuard | CoverageInline8bitCounters 
|
                              CoverageTraceLoads | CoverageTraceStores |
+                             CoverageTraceArgs | CoverageTraceRet |
                              CoverageInlineBoolFlag | CoverageControlFlow;
   if ((CoverageFeatures & InsertionPointTypes) &&
       !(CoverageFeatures & InstrumentationTypes) && DiagnoseErrors) {
@@ -1111,9 +1114,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)
@@ -1474,6 +1478,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)
@@ -1864,6 +1870,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/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

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

Reply via email to