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

Yunseong Kim via llvm-commits llvm-commits at lists.llvm.org
Sun Aug 23 06:31:11 PDT 2026


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

>From 4fd86f38d499b215dec9959b6688d3ff7884677d Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Sun, 23 Aug 2026 12:27:09 +0200
Subject: [PATCH] [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 <yunseong.kim at est.tech>
---
 .../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}



More information about the llvm-commits mailing list