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

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

Add two SanitizerCoverage modes that report the values flowing into and
out of instrumented functions. trace-args emits one call per IR argument
at function entry; trace-ret emits one call before every return:

  void __sanitizer_cov_trace_args(u64 tag, u64 val, const u64 *offsets);
  void __sanitizer_cov_trace_ret(u64 size, u64 val, const u64 *offsets);

`tag` is (arg_idx << 32) | size, where arg_idx is the position in the IR
parameter list. If `offsets` is null, `val` is the value itself,
zero-extended to 64 bits. Otherwise `val` is the address of `size`
bytes, and `offsets` points to a constant table: offsets[0] is the
number N of {byte offset, byte size} pairs that follow, each at most
8 bytes. This form reports byval/byref/inalloca/preallocated arguments
and sret buffers, described by the elements of their IR type, and
pointers that are noundef and dereferenceable(N) or
dereferenceable_or_null(N), snapshotted up to
-sanitizer-coverage-trace-pointee-bytes (default 64). A size of 0 means
nothing was reported.

The modes are enabled through SanitizerCoverageOptions::TraceArgs and
TraceRet or, for opt, -sanitizer-coverage-trace-args and
-sanitizer-coverage-trace-ret. Like the other modes, they need a
coverage level. They do not imply trace-pc-guard.

What is emitted depends only on the IR. Debug information gives the
calls a location and nothing else, so -g does not change the
instrumentation. The pass creates no allocas. The calls are notail and
nomerge, because a runtime identifies an observation by its return
address, as with trace-cmp.

Assisted-by: Kiro CLI (Claude Opus 5.5)
Signed-off-by: Yunseong Kim <[email protected]>
---
 .../llvm/Transforms/Utils/Instrumentation.h   |   2 +
 .../Instrumentation/InstrumentationOptions.td |   6 +
 .../Instrumentation/SanitizerCoverage.cpp     | 350 +++++++++++++++++-
 .../trace-args-debug-invariance.ll            | 137 +++++++
 .../SanitizerCoverage/trace-args.ll           | 279 ++++++++++++++
 .../SanitizerCoverage/trace-ret.ll            | 148 ++++++++
 6 files changed, 921 insertions(+), 1 deletion(-)
 create mode 100644 
llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args.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 97a853dcb65326..bc2530a5114c7a 100644
--- a/llvm/include/llvm/Transforms/Utils/Instrumentation.h
+++ b/llvm/include/llvm/Transforms/Utils/Instrumentation.h
@@ -164,6 +164,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/InstrumentationOptions.td 
b/llvm/lib/Transforms/Instrumentation/InstrumentationOptions.td
index 44a2c46ed3ec43..d569a0c52d286a 100644
--- a/llvm/lib/Transforms/Instrumentation/InstrumentationOptions.td
+++ b/llvm/lib/Transforms/Instrumentation/InstrumentationOptions.td
@@ -750,6 +750,8 @@ defm : BoolField<"sanitizer-coverage-stack-depth", "false",
   "Max stack depth tracing">;
 defm : ValueField<"sanitizer-coverage-stack-depth-callback-min", "int", "0",
   "Max stack depth tracing should use callback and only when stack depth more 
than specified">;
+defm : BoolField<"sanitizer-coverage-trace-args", "false",
+  "Tracing of function arguments">;
 defm : BoolField<"sanitizer-coverage-trace-compares", "false",
   "Tracing of CMP and similar instructions">;
 defm : BoolField<"sanitizer-coverage-trace-divs", "false",
@@ -764,6 +766,10 @@ defm : BoolField<"sanitizer-coverage-trace-pc-entry-exit", 
"false",
   "PC tracing with separate entry/exit callbacks">;
 defm : BoolField<"sanitizer-coverage-trace-pc-guard", "false",
   "PC tracing with a guard">;
+defm : ValueField<"sanitizer-coverage-trace-pointee-bytes", "unsigned", "64",
+  "With trace-args and trace-ret, read at most this many bytes behind a 
pointer the IR says is dereferenceable (0: report the pointer only)">;
+defm : BoolField<"sanitizer-coverage-trace-ret", "false",
+  "Tracing of return values">;
 defm : BoolField<"sanitizer-coverage-trace-stores", "false",
   "Tracing of store instructions">;
 
diff --git a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp 
b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
index c375041e73d45e..9af71442e73dc6 100644
--- a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
+++ b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
@@ -16,9 +16,11 @@
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/Analysis/GlobalsModRef.h"
 #include "llvm/Analysis/PostDominators.h"
+#include "llvm/Analysis/ValueTracking.h"
 #include "llvm/IR/Constant.h"
 #include "llvm/IR/Constants.h"
 #include "llvm/IR/DataLayout.h"
+#include "llvm/IR/DebugInfoMetadata.h"
 #include "llvm/IR/Dominators.h"
 #include "llvm/IR/EHPersonalities.h"
 #include "llvm/IR/Function.h"
@@ -31,6 +33,7 @@
 #include "llvm/IR/Module.h"
 #include "llvm/IR/Type.h"
 #include "llvm/IR/ValueSymbolTable.h"
+#include "llvm/Support/MathExtras.h"
 #include "llvm/Support/SpecialCaseList.h"
 #include "llvm/Support/VirtualFileSystem.h"
 #include "llvm/TargetParser/Triple.h"
@@ -46,6 +49,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";
@@ -140,10 +145,13 @@ SanitizerCoverageOptions overrideFromCL(const 
InstrumentationOptions &Opts,
                Opts.sanitizer_coverage_stack_depth_callback_min);
   Options.TraceLoads |= Opts.sanitizer_coverage_trace_loads;
   Options.TraceStores |= Opts.sanitizer_coverage_trace_stores;
+  Options.TraceArgs |= Opts.sanitizer_coverage_trace_args;
+  Options.TraceRet |= Opts.sanitizer_coverage_trace_ret;
   Options.GatedCallbacks |= Opts.sanitizer_coverage_gated_trace_callbacks;
   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 |= Opts.sanitizer_coverage_control_flow;
   return Options;
@@ -180,6 +188,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);
@@ -205,6 +215,36 @@ class ModuleSanitizerCoverage {
   std::string getSectionStart(const std::string &Section) const;
   std::string getSectionEnd(const std::string &Section) const;
 
+  /// The `offsets` argument of the trace-args/trace-ret callbacks for an
+  /// object in memory: a constant table whose first element is the number of
+  /// fields, followed by [byte offset, byte size] pairs covering the elements
+  /// of its IR type, and the store size of that type. Table is null for an
+  /// object of no fixed size, in which case ObjectSize is 0.
+  struct FieldOffsets {
+    Constant *Table = nullptr;
+    uint64_t ObjectSize = 0;
+  };
+  FieldOffsets getObjectOffsets(Type *Ty);
+  /// Create one trace-args/trace-ret call, which is never a tail call and
+  /// cannot be merged.
+  void emitTraceCall(IRBuilderBase &IRB, FunctionCallee Callee,
+                     std::optional<uint32_t> ArgIdx, uint64_t Size, Value *Val,
+                     Value *Table = nullptr);
+  /// Emit the trace-args/trace-ret call for \p V through \p IRB: one call per
+  /// element of a first-class aggregate, and one for anything else.
+  void traceValue(IRBuilderBase &IRB, FunctionCallee Callee,
+                  std::optional<uint32_t> ArgIdx, Value *V);
+  void traceObject(IRBuilderBase &IRB, FunctionCallee Callee,
+                   std::optional<uint32_t> ArgIdx, Value *Addr, Type *Ty);
+  /// Report pointer \p Ptr together with the first \p Bytes bytes it points
+  /// to, which the IR says are dereferenceable.
+  void tracePointee(IRBuilderBase &IRB, FunctionCallee Callee,
+                    std::optional<uint32_t> ArgIdx, Value *Ptr, uint64_t 
Bytes);
+  /// How many bytes behind \p Ptr to report, given that the IR says
+  /// \p Dereferenceable of them may be read: none if \p Ptr is not a pointer
+  /// in the default address space.
+  uint64_t getPointeeBytes(Value *Ptr, uint64_t Dereferenceable) const;
+
   const InstrumentationOptions &Opts;
   Module &M;
   DomTreeCallback DTCallback;
@@ -214,6 +254,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;
@@ -237,6 +278,11 @@ class ModuleSanitizerCoverage {
   SmallVector<GlobalValue *, 20> GlobalsToAppendToUsed;
   SmallVector<GlobalValue *, 20> GlobalsToAppendToCompilerUsed;
 
+  /// Object tables are shared by every object of the same IR type: a type
+  /// passed by hundreds of functions must not emit hundreds of identical
+  /// tables.
+  DenseMap<Type *, FieldOffsets> ObjectOffsetsCache;
+
   SanitizerCoverageOptions Options;
 
   const SpecialCaseList *Allowlist;
@@ -458,6 +504,15 @@ bool ModuleSanitizerCoverage::instrumentModule() {
   SanCovTracePCGuard =
       M.getOrInsertFunction(SanCovTracePCGuardName, VoidTy, PtrTy);
 
+  // See the "Argument and return value tracing" section below for the meaning
+  // of the arguments.
+  // void __sanitizer_cov_trace_args(u64 tag, u64 val, const u64 *offsets)
+  SanCovTraceArgsFunc = M.getOrInsertFunction(SanCovTraceArgsName, VoidTy,
+                                              Int64Ty, Int64Ty, PtrTy);
+  // void __sanitizer_cov_trace_ret(u64 size, u64 val, const u64 *offsets)
+  SanCovTraceRetFunc = M.getOrInsertFunction(SanCovTraceRetName, VoidTy,
+                                             Int64Ty, Int64Ty, PtrTy);
+
   SanCovStackDepthCallback =
       M.getOrInsertFunction(SanCovStackDepthCallbackName, VoidTy);
 
@@ -672,6 +727,11 @@ void ModuleSanitizerCoverage::instrumentFunction(Function 
&F) {
   if (Options.CollectControlFlow)
     createFunctionControlFlow(F);
 
+  // Before InjectCoverage, which then puts its entry-block instrumentation
+  // ahead of the argument calls: allocas; pc_entry; trace_args.
+  if (Options.TraceArgs)
+    InjectTraceForArgs(F);
+
   Value *FunctionGateCmp = nullptr;
   InjectCoverage(F, BlocksToInstrument, FunctionGateCmp, IsLeafFunc);
   InjectCoverageForIndirectCalls(F, IndirCalls);
@@ -681,6 +741,10 @@ void ModuleSanitizerCoverage::instrumentFunction(Function 
&F) {
   InjectTraceForGep(F, GepTraceTargets);
   InjectTraceForLoadsAndStores(F, Loads, Stores);
 
+  // Before InjectTraceForExits, so that a return reads trace_ret; pc_exit; 
ret.
+  if (Options.TraceRet)
+    InjectTraceForRet(F);
+
   if (Options.TracePCEntryExit)
     InjectTraceForExits(F);
 }
@@ -1182,3 +1246,287 @@ void 
ModuleSanitizerCoverage::createFunctionControlFlow(Function &F) {
       ConstantArray::get(ArrayType::get(PtrTy, CFs.size()), CFs));
   FunctionCFsArray->setConstant(true);
 }
+
+//===----------------------------------------------------------------------===//
+// Argument and return value tracing.
+// See clang/docs/SanitizerCoverage.md for the full design and callback
+// contract:
+//   void __sanitizer_cov_trace_args(u64 tag, u64 val, const u64 *offsets);
+//   void __sanitizer_cov_trace_ret (u64 size, u64 val, const u64 *offsets);
+//===----------------------------------------------------------------------===//
+
+ModuleSanitizerCoverage::FieldOffsets
+ModuleSanitizerCoverage::getObjectOffsets(Type *Ty) {
+  // trace-args packs the size into the low 32 bits of its tag.
+  if (!Ty || !Ty->isSized() || DL->getTypeStoreSize(Ty).isScalable() ||
+      DL->getTypeStoreSize(Ty).getFixedValue() > UINT32_MAX)
+    return {};
+  auto [It, Inserted] = ObjectOffsetsCache.try_emplace(Ty);
+  if (!Inserted)
+    return It->second;
+
+  // A struct is described by its elements, recursively; anything else as one
+  // field, or in 8-byte pieces if it is wider, so that a runtime reading every
+  // field into a 64-bit word reads all of the object. Element 0 is the number
+  // of pairs, filled in below.
+  SmallVector<Constant *, 17> Pairs = {nullptr};
+  auto AddField = [&](uint64_t Offset, uint64_t Bytes) {
+    for (uint64_t Piece = 0; Piece < Bytes; Piece += sizeof(uint64_t)) {
+      Pairs.push_back(ConstantInt::get(Int64Ty, Offset + Piece));
+      Pairs.push_back(ConstantInt::get(
+          Int64Ty, std::min<uint64_t>(Bytes - Piece, sizeof(uint64_t))));
+    }
+  };
+  auto AddType = [&](auto &Self, Type *T, uint64_t Offset) -> void {
+    if (auto *STy = dyn_cast<StructType>(T)) {
+      const StructLayout *Layout = DL->getStructLayout(STy);
+      for (unsigned I = 0, E = STy->getNumElements(); I != E; ++I)
+        Self(Self, STy->getElementType(I),
+             Offset + Layout->getElementOffset(I).getFixedValue());
+      return;
+    }
+    AddField(Offset, DL->getTypeStoreSize(T).getFixedValue());
+  };
+  uint64_t Size = DL->getTypeStoreSize(Ty).getFixedValue();
+  AddType(AddType, Ty, 0);
+
+  if (Pairs.size() > 1) {
+    Pairs[0] = ConstantInt::get(Int64Ty, Pairs.size() / 2);
+    ArrayType *TableTy = ArrayType::get(Int64Ty, Pairs.size());
+    auto *Table = new GlobalVariable(
+        M, TableTy, /*isConstant=*/true, GlobalVariable::PrivateLinkage,
+        ConstantArray::get(TableTy, Pairs), "__sancov_offsets_");
+    Table->setUnnamedAddr(GlobalValue::UnnamedAddr::Global);
+    It->second = {Table, Size};
+  }
+  return It->second;
+}
+
+/// Widen \p V to the 64-bit value the callbacks take: a pointer is reported as
+/// the address it holds, a floating-point value or a vector as its bit 
pattern,
+/// and a value wider than 64 bits by its low 64 bits. Returns the value
+/// together with the number of bytes of it that are meaningful, or
+/// {nullptr, 0} for a type that has no fixed-size bit pattern, such as a
+/// scalable vector.
+static std::pair<Value *, uint64_t> getReportedValue(IRBuilderBase &IRB,
+                                                     Value *V) {
+  const DataLayout &DL = IRB.GetInsertBlock()->getDataLayout();
+  Type *Ty = V->getType();
+  IntegerType *Int64Ty = IRB.getInt64Ty();
+
+  // The callback receives bits, not poison: an argument callers pass nothing
+  // for (dead argument elimination passes poison), or the padding of a struct
+  // coerced into an integer, is whatever it happens to be. This also keeps
+  // MemorySanitizer from reporting the instrumentation's own use of it.
+  if (!isGuaranteedNotToBeUndefOrPoison(V))
+    V = IRB.CreateFreeze(V);
+
+  // ptrtoint widens or narrows to the requested type, so this also holds for a
+  // target, or an address space, whose pointers are narrower than 64 bits.
+  if (Ty->isPointerTy())
+    return {IRB.CreatePtrToInt(V, Int64Ty),
+            std::min<uint64_t>(DL.getPointerSize(Ty->getPointerAddressSpace()),
+                               sizeof(uint64_t))};
+
+  // A vector of pointers has no bit pattern of its own; its addresses do.
+  if (auto *VTy = dyn_cast<FixedVectorType>(Ty);
+      VTy && VTy->getElementType()->isPointerTy()) {
+    V = IRB.CreatePtrToInt(
+        V, VectorType::get(DL.getIntPtrType(VTy->getElementType()),
+                           VTy->getElementCount()));
+    Ty = V->getType();
+  }
+  if (Ty->isFloatingPointTy() || isa<FixedVectorType>(Ty)) {
+    TypeSize Bits = Ty->getPrimitiveSizeInBits();
+    if (Bits.isScalable() || !Bits.getFixedValue())
+      return {nullptr, 0};
+    V = IRB.CreateBitCast(V, IRB.getIntNTy(Bits.getFixedValue()));
+    Ty = V->getType();
+  }
+  if (!Ty->isIntegerTy())
+    return {nullptr, 0};
+
+  uint64_t Size = divideCeil(Ty->getIntegerBitWidth(), 8);
+  if (Size > sizeof(uint64_t))
+    return {IRB.CreateTrunc(V, Int64Ty), sizeof(uint64_t)};
+  return {IRB.CreateZExt(V, Int64Ty), Size};
+}
+
+void ModuleSanitizerCoverage::emitTraceCall(IRBuilderBase &IRB,
+                                            FunctionCallee Callee,
+                                            std::optional<uint32_t> ArgIdx,
+                                            uint64_t Size, Value *Val,
+                                            Value *Table) {
+  uint64_t Tag = ArgIdx ? ((uint64_t)*ArgIdx << 32) | Size : Size;
+  Value *ValArg =
+      Val ? (Val->getType()->isPointerTy() ? IRB.CreatePtrToInt(Val, Int64Ty)
+                                           : Val)
+          : ConstantInt::get(Int64Ty, 0);
+  Value *TableArg = Table ? Table : ConstantPointerNull::get(PtrTy);
+
+  CallInst *CI = IRB.CreateCall(
+      Callee, {ConstantInt::get(Int64Ty, Tag), ValArg, TableArg});
+  // An observation is identified by its call site, which the runtime reads as
+  // its own return address. A tail call would hand the callback the caller's
+  // return address instead, and a trace-ret call of a void function is in tail
+  // position. Merging identical calls would conflate distinct return sites.
+  CI->setTailCallKind(CallInst::TCK_NoTail);
+  CI->setCannotMerge();
+}
+
+void ModuleSanitizerCoverage::traceValue(IRBuilderBase &IRB,
+                                         FunctionCallee Callee,
+                                         std::optional<uint32_t> ArgIdx,
+                                         Value *V) {
+  // A first-class aggregate lives in registers and has no address, so each of
+  // its elements is reported on its own.
+  Type *Ty = V->getType();
+  unsigned NumElements = Ty->isStructTy()  ? Ty->getStructNumElements()
+                         : Ty->isArrayTy() ? Ty->getArrayNumElements()
+                                           : 0;
+  if (NumElements) {
+    for (unsigned I = 0; I != NumElements; ++I)
+      traceValue(IRB, Callee, ArgIdx, IRB.CreateExtractValue(V, I));
+    return;
+  }
+
+  std::pair<Value *, uint64_t> Reported = getReportedValue(IRB, V);
+  emitTraceCall(IRB, Callee, ArgIdx, Reported.second, Reported.first);
+}
+
+void ModuleSanitizerCoverage::traceObject(IRBuilderBase &IRB,
+                                          FunctionCallee Callee,
+                                          std::optional<uint32_t> ArgIdx,
+                                          Value *Addr, Type *Ty) {
+  FieldOffsets Offsets = getObjectOffsets(Ty);
+  emitTraceCall(IRB, Callee, ArgIdx, Offsets.ObjectSize,
+                Offsets.Table ? Addr : nullptr, Offsets.Table);
+}
+
+uint64_t
+ModuleSanitizerCoverage::getPointeeBytes(Value *Ptr,
+                                         uint64_t Dereferenceable) const {
+  // A pointer of another address space may not be readable through the
+  // generic address the runtime gets.
+  if (!Ptr->getType()->isPointerTy() ||
+      Ptr->getType()->getPointerAddressSpace() != 0)
+    return 0;
+  return std::min<uint64_t>(Dereferenceable,
+                            Opts.sanitizer_coverage_trace_pointee_bytes);
+}
+
+void ModuleSanitizerCoverage::tracePointee(IRBuilderBase &IRB,
+                                           FunctionCallee Callee,
+                                           std::optional<uint32_t> ArgIdx,
+                                           Value *Ptr, uint64_t Bytes) {
+  // The IR says nothing about the pointee but how many bytes of it may be
+  // read, so they are read as bytes: the table of a byte array that long.
+  FieldOffsets Offsets =
+      getObjectOffsets(ArrayType::get(IRB.getInt8Ty(), Bytes));
+  emitTraceCall(IRB, Callee, ArgIdx, Offsets.ObjectSize, Ptr, Offsets.Table);
+}
+
+void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
+  // Static allocas stay first in the entry block. This runs before
+  // InjectCoverage, whose entry-block instrumentation (pc_entry, trace_pc,
+  // the guard or counter) is then inserted ahead of these calls.
+  BasicBlock &EntryBB = F.getEntryBlock();
+  BasicBlock::iterator IP =
+      PrepareToSplitEntryBlock(EntryBB, EntryBB.getFirstInsertionPt());
+
+  // InstrumentationIRBuilder gives the calls a !dbg location, which the
+  // verifier requires of a call in a function with debug information once it
+  // can be inlined. The location is the function's own opening line rather
+  // than the one of the instruction the calls precede, which may belong to an
+  // inlined callee: the arguments are the function's own. Only the location
+  // depends on debug information, never the calls.
+  InstrumentationIRBuilder IRB(&*IP);
+  if (DISubprogram *SP = F.getSubprogram())
+    IRB.SetCurrentDebugLocation(
+        DILocation::get(SP->getContext(), SP->getScopeLine(), 0, SP));
+
+  for (Argument &Arg : F.args()) {
+    // A struct-return pointer is where the return value goes, which trace-ret
+    // reports; the error slot of a Swift throwing function may only be loaded,
+    // stored, or passed on as a swifterror argument. Neither is observed, and
+    // neither renumbers the arguments that are: arg_idx stays the IR position.
+    if (Arg.hasStructRetAttr() || Arg.hasSwiftErrorAttr())
+      continue;
+    // Callers may pass anything for an argument the function does not use: a
+    // value is still reported, but an object is not read.
+    bool Unused = Arg.use_empty();
+    uint32_t ArgIdx = Arg.getArgNo();
+    // An argument the ABI says points at a copy of the parameter in memory is
+    // that object, not a value.
+    // An object the function does not use may be passed as a poison pointer,
+    // by dead argument elimination in its callers: it is not handed to the
+    // runtime to read.
+    if (Type *MemTy = Arg.getPointeeInMemoryValueType()) {
+      traceObject(IRB, SanCovTraceArgsFunc, ArgIdx, Unused ? nullptr : &Arg,
+                  Unused ? nullptr : MemTy);
+    } else if (uint64_t Bytes =
+                   // Argument::getDereferenceableBytes() asserts on a
+                   // non-pointer. Without noundef the pointer may be poison,
+                   // which no freeze makes safe to read through.
+               Unused || !Arg.getType()->isPointerTy() ||
+                       !Arg.hasAttribute(Attribute::NoUndef)
+                   ? 0
+                   : getPointeeBytes(
+                         &Arg, std::max(Arg.getDereferenceableBytes(),
+                                        Arg.getDereferenceableOrNullBytes()))) 
{
+      // What the pointer points to, as far as the IR says it may be read: at
+      // the top of the entry block nothing can have freed it yet.
+      tracePointee(IRB, SanCovTraceArgsFunc, ArgIdx, &Arg, Bytes);
+    } else {
+      traceValue(IRB, SanCovTraceArgsFunc, ArgIdx, &Arg);
+    }
+  }
+}
+
+void ModuleSanitizerCoverage::InjectTraceForRet(Function &F) {
+  // An indirect return leaves an IR function returning void and writes the
+  // returned object through a hidden struct-return pointer; that buffer is
+  // what is reported, so an indirect return is not dropped.
+  Argument *SRetArg = nullptr;
+  for (Argument &Arg : F.args())
+    if (Arg.hasStructRetAttr()) {
+      SRetArg = &Arg;
+      break;
+    }
+
+  // A function that never writes its sret buffer may be passed a poison one.
+  bool SRetUnused = SRetArg && SRetArg->use_empty();
+  // A returned pointer the IR says may be read from is reported with what it
+  // points to: the return attribute holds where the function returns.
+  uint64_t RetPointeeBytes =
+      F.hasRetAttribute(Attribute::NoUndef)
+          ? std::max(F.getAttributes().getRetDereferenceableBytes(),
+                     F.getAttributes().getRetDereferenceableOrNullBytes())
+          : 0;
+  for (BasicBlock &BB : F) {
+    // Only a return carries a value to report; unwinding leaves the function
+    // without one.
+    auto *RI = dyn_cast<ReturnInst>(BB.getTerminator());
+    if (!RI)
+      continue;
+    // A musttail call has to stay adjacent to the return that forwards it, so
+    // there is nowhere to put the call.
+    if (auto *CI = dyn_cast_or_null<CallInst>(RI->getPrevNode());
+        CI && CI->isMustTailCall())
+      continue;
+
+    InstrumentationIRBuilder IRB(RI);
+    if (SRetArg) {
+      traceObject(IRB, SanCovTraceRetFunc, std::nullopt,
+                  SRetUnused ? nullptr : SRetArg,
+                  SRetUnused ? nullptr : SRetArg->getParamStructRetType());
+    } else if (Value *RetVal = RI->getReturnValue()) {
+      if (uint64_t Bytes = getPointeeBytes(RetVal, RetPointeeBytes))
+        tracePointee(IRB, SanCovTraceRetFunc, std::nullopt, RetVal, Bytes);
+      else
+        traceValue(IRB, SanCovTraceRetFunc, std::nullopt, RetVal);
+    } else {
+      emitTraceCall(IRB, SanCovTraceRetFunc, std::nullopt, 0, nullptr); // void
+    }
+  }
+}
diff --git 
a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance.ll 
b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance.ll
new file mode 100644
index 00000000000000..3183cbfaca86f0
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance.ll
@@ -0,0 +1,137 @@
+; Debug information may enrich how an observation is interpreted, but must 
never
+; decide what is observed: instrumenting this module and then stripping its 
debug
+; information gives exactly the module that stripping first and instrumenting
+; then gives.
+;
+; The input carries everything that could tempt the pass: source-level
+; parameter records that disagree with the IR argument list (an sret pointer, a
+; struct coerced into one register and split into two fragments, a first-class
+; aggregate one of whose fragments was optimized out), a struct pointer with a
+; known field layout, a #dbg_declare of a byval argument, an unoptimized
+; function that stores its arguments to declared allocas, and an inlined callee
+; with parameters of its own.
+;
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 
-sanitizer-coverage-trace-pc-guard -sanitizer-coverage-trace-args 
-sanitizer-coverage-trace-ret \
+; RUN:   | opt -strip-debug -S -o %t.instrumented-then-stripped.ll
+; RUN: opt < %s -strip-debug \
+; RUN:   | opt -passes='module(sancov-module)' -sanitizer-coverage-level=3 
-sanitizer-coverage-trace-pc-guard -sanitizer-coverage-trace-args 
-sanitizer-coverage-trace-ret -S -o %t.stripped-then-instrumented.ll
+; RUN: diff %t.instrumented-then-stripped.ll %t.stripped-then-instrumented.ll
+
+; Debug information still gives the calls a location: the function's own
+; opening line, even where the entry block starts with an inlined callee.
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=1 
-sanitizer-coverage-trace-args -S | FileCheck %s
+; CHECK-LABEL: define i32 @caller(
+; CHECK: call void @__sanitizer_cov_trace_args(i64 8, i64 %n, ptr null) 
#{{[0-9]+}}, !dbg ![[ENTRY:[0-9]+]]
+; CHECK-NOT: call void @__sanitizer_cov_trace_args
+; CHECK: ret i32
+; CHECK-DAG: ![[ENTRY]] = !DILocation(line: 10, scope: ![[CALLER:[0-9]+]])
+; CHECK-DAG: ![[CALLER]] = distinct !DISubprogram(name: "caller"
+
+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.S = type { i64, i64, i64 }
+
+; struct S make(struct pair p, struct S *q) with struct pair { int a, b; }
+define void @make(ptr sret(%struct.S) %r, i64 noundef %p, ptr noundef %q) !dbg 
!10 {
+entry:
+  %a = trunc i64 %p to i32
+  %hi = lshr i64 %p, 32
+  %b = trunc i64 %hi to i32
+    #dbg_value(i32 %a, !13, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !15)
+    #dbg_value(i32 %b, !13, !DIExpression(DW_OP_LLVM_fragment, 32, 32), !15)
+    #dbg_value(ptr %q, !14, !DIExpression(), !15)
+  store i64 %p, ptr %r, !dbg !15
+  ret void, !dbg !15
+}
+
+; long use(struct S s), passed in memory and described by a #dbg_declare.
+define i64 @use(ptr byval(%struct.S) %s) !dbg !20 {
+entry:
+    #dbg_declare(ptr %s, !21, !DIExpression(), !22)
+  %v = load i64, ptr %s, !dbg !22
+  ret i64 %v, !dbg !22
+}
+
+; long caller(long n) { return leaf(n * 2); } with leaf(long m) inlined.
+define i32 @caller(i64 noundef %n) !dbg !30 {
+entry:
+  %m = shl i64 %n, 1, !dbg !34
+    #dbg_value(i64 %n, !31, !DIExpression(), !33)
+    #dbg_value(i64 %m, !35, !DIExpression(), !34)
+  %t = trunc i64 %m to i32, !dbg !34
+  ret i32 %t, !dbg !33
+}
+
+; long split(struct mix m, int z), with m passed as [2 x i64] (AArch64).
+define i64 @split([2 x i64] noundef %m, i32 noundef %z) !dbg !50 {
+entry:
+  %l = extractvalue [2 x i64] %m, 1
+    #dbg_value(i64 poison, !51, !DIExpression(DW_OP_LLVM_fragment, 0, 64), !53)
+    #dbg_value(i64 %l, !51, !DIExpression(DW_OP_LLVM_fragment, 64, 64), !53)
+    #dbg_value(i32 %z, !52, !DIExpression(), !53)
+  %zl = sext i32 %z to i64
+  %r = add i64 %l, %zl
+  ret i64 %r, !dbg !53
+}
+
+; The same at -O0, where the arguments are stored to declared allocas.
+define i64 @split_o0([2 x i64] noundef %m, i32 noundef %z) !dbg !60 {
+entry:
+  %m.addr = alloca [2 x i64], align 8
+  %z.addr = alloca i32, align 4
+  store [2 x i64] %m, ptr %m.addr, align 8
+    #dbg_declare(ptr %m.addr, !61, !DIExpression(), !63)
+  store i32 %z, ptr %z.addr, align 4
+    #dbg_declare(ptr %z.addr, !62, !DIExpression(), !63)
+  %p = getelementptr inbounds i8, ptr %m.addr, i64 8
+  %l = load i64, ptr %p, align 8, !dbg !63
+  ret i64 %l, !dbg !63
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !1, isOptimized: 
true, emissionKind: FullDebug)
+!1 = !DIFile(filename: "invariance.c", directory: "/")
+!2 = !{i32 7, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!5 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+!6 = !DICompositeType(tag: DW_TAG_structure_type, name: "S", file: !1, size: 
192, elements: !7)
+!7 = !{!40, !41, !42}
+!8 = !DICompositeType(tag: DW_TAG_structure_type, name: "pair", file: !1, 
size: 64, elements: !{})
+!9 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !6, size: 64)
+!10 = distinct !DISubprogram(name: "make", scope: !1, file: !1, line: 1, type: 
!11, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
+!11 = !DISubroutineType(types: !12)
+!12 = !{!6, !8, !9}
+!13 = !DILocalVariable(name: "p", arg: 1, scope: !10, file: !1, line: 1, type: 
!8)
+!14 = !DILocalVariable(name: "q", arg: 2, scope: !10, file: !1, line: 1, type: 
!9)
+!15 = !DILocation(line: 1, column: 1, scope: !10)
+!20 = distinct !DISubprogram(name: "use", scope: !1, file: !1, line: 5, type: 
!23, scopeLine: 5, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
+!21 = !DILocalVariable(name: "s", arg: 1, scope: !20, file: !1, line: 5, type: 
!6)
+!22 = !DILocation(line: 5, column: 1, scope: !20)
+!23 = !DISubroutineType(types: !24)
+!24 = !{!5, !6}
+!30 = distinct !DISubprogram(name: "caller", scope: !1, file: !1, line: 10, 
type: !36, scopeLine: 10, spFlags: DISPFlagDefinition | DISPFlagOptimized, 
unit: !0)
+!31 = !DILocalVariable(name: "n", arg: 1, scope: !30, file: !1, line: 10, 
type: !5)
+!32 = distinct !DISubprogram(name: "leaf", scope: !1, file: !1, line: 8, type: 
!36, scopeLine: 8, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition | 
DISPFlagOptimized, unit: !0)
+!33 = !DILocation(line: 10, column: 1, scope: !30)
+!34 = !DILocation(line: 8, column: 1, scope: !32, inlinedAt: !33)
+!35 = !DILocalVariable(name: "m", arg: 1, scope: !32, file: !1, line: 8, type: 
!5)
+!36 = !DISubroutineType(types: !37)
+!37 = !{!5, !5}
+!40 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !6, file: !1, 
baseType: !5, size: 64)
+!41 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !6, file: !1, 
baseType: !5, size: 64, offset: 64)
+!42 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !6, file: !1, 
baseType: !5, size: 64, offset: 128)
+!43 = !DICompositeType(tag: DW_TAG_structure_type, name: "mix", file: !1, 
size: 128, elements: !{})
+!44 = !DISubroutineType(types: !45)
+!45 = !{!5, !43, !4}
+!50 = distinct !DISubprogram(name: "split", scope: !1, file: !1, line: 20, 
type: !44, scopeLine: 20, spFlags: DISPFlagDefinition | DISPFlagOptimized, 
unit: !0)
+!51 = !DILocalVariable(name: "m", arg: 1, scope: !50, file: !1, line: 20, 
type: !43)
+!52 = !DILocalVariable(name: "z", arg: 2, scope: !50, file: !1, line: 20, 
type: !4)
+!53 = !DILocation(line: 20, column: 1, scope: !50)
+!60 = distinct !DISubprogram(name: "split_o0", scope: !1, file: !1, line: 30, 
type: !44, scopeLine: 30, spFlags: DISPFlagDefinition, unit: !0)
+!61 = !DILocalVariable(name: "m", arg: 1, scope: !60, file: !1, line: 30, 
type: !43)
+!62 = !DILocalVariable(name: "z", arg: 2, scope: !60, file: !1, line: 30, 
type: !4)
+!63 = !DILocation(line: 30, column: 1, scope: !60)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll 
b/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
new file mode 100644
index 00000000000000..f6e42b40b54194
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
@@ -0,0 +1,279 @@
+; Argument tracing: one __sanitizer_cov_trace_args call per IR argument, in the
+; entry block, decided by the IR alone. arg_idx is the position of the argument
+; in the IR parameter list; a struct-return pointer and a Swift error slot are
+; not observed but keep their positions.
+;
+; A value that fits in a register is reported as the value; an argument the ABI
+; says points at a copy of the parameter in memory is reported as that object,
+; with a table of the elements of its IR type, none wider than 8 bytes. A
+; pointer the IR says is noundef and dereferenceable is reported with a raw 
snapshot of the
+; bytes it points to, as many as the attribute says and
+; -sanitizer-coverage-trace-pointee-bytes allows.
+;
+; An argument the function does not use is passed whatever its callers pass -
+; poison, after dead argument elimination - so an object is not read through
+; it; the functions below use theirs, and @unused checks the rule.
+;
+; opt runs the verifier, so a passing run also proves the emitted IR is
+; well-formed.
+;
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=1 
-sanitizer-coverage-trace-args -S | FileCheck %s
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=1 
-sanitizer-coverage-trace-args \
+; RUN:   -sanitizer-coverage-trace-pointee-bytes=0 -S | FileCheck %s 
--check-prefix=NOPTEE
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=1 
-sanitizer-coverage-trace-args \
+; RUN:   -sanitizer-coverage-trace-ret -sanitizer-coverage-trace-pc-entry-exit 
-S | FileCheck %s --check-prefix=ENTRY
+
+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"
+
+declare void @use(...)
+
+%struct.T = type { i32, i8, i64 }
+
+; One table per IR type, shared by every object of that type, and one for an
+; object that is not a struct.
+; CHECK: @__sancov_offsets_ = private unnamed_addr constant [7 x i64] [i64 3, 
i64 0, i64 4, i64 4, i64 1, i64 8, i64 8]
+; CHECK: @__sancov_offsets_.1 = private unnamed_addr constant [3 x i64] [i64 
1, i64 0, i64 8]
+; A pointee snapshot is a byte array: 16, 64 and 12 bytes, in 8-byte pieces.
+; CHECK: @__sancov_offsets_.2 = private unnamed_addr constant [5 x i64] [i64 
2, i64 0, i64 8, i64 8, i64 8]
+; CHECK: @__sancov_offsets_.3 = private unnamed_addr constant [17 x i64] [i64 
8, i64 0, i64 8, i64 8, i64 8, i64 16, i64 8, i64 24, i64 8, i64 32, i64 8, i64 
40, i64 8, i64 48, i64 8, i64 56, i64 8]
+; CHECK: @__sancov_offsets_.4 = private unnamed_addr constant [5 x i64] [i64 
2, i64 0, i64 8, i64 8, i64 4]
+; An opaque [32 x i8] object, in 8-byte pieces; a struct by the elements of its
+; nested structs, and an element wider than 8 bytes in pieces.
+; CHECK: @__sancov_offsets_.5 = private unnamed_addr constant [9 x i64] [i64 
4, i64 0, i64 8, i64 8, i64 8, i64 16, i64 8, i64 24, i64 8]
+; CHECK: @__sancov_offsets_.6 = private unnamed_addr constant [11 x i64] [i64 
5, i64 0, i64 4, i64 8, i64 1, i64 16, i64 8, i64 24, i64 8, i64 32, i64 4]
+; CHECK: @__sancov_offsets_.7 = private unnamed_addr constant [7 x i64] [i64 
3, i64 0, i64 8, i64 8, i64 8, i64 16, i64 1]
+; CHECK-NOT: @__sancov_offsets_
+
+; Scalars and pointers are reported as their value, zero-extended to 64 bits.
+define void @scalars(i1 noundef %b, i8 noundef %c, i32 noundef %i, i64 noundef 
%l, ptr noundef %p) {
+entry:
+  call void (...) @use(i1 %b, i8 %c, i32 %i, i64 %l, ptr %p)
+  ret void
+}
+; CHECK-LABEL: define void @scalars(
+; CHECK-NOT: alloca
+; CHECK: %[[B:[0-9]+]] = zext i1 %b to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 1, i64 %[[B]], ptr null)
+; CHECK: %[[C:[0-9]+]] = zext i8 %c to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967297, i64 %[[C]], ptr 
null)
+; CHECK: %[[I:[0-9]+]] = zext i32 %i to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 8589934596, i64 %[[I]], ptr 
null)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 12884901896, i64 %l, ptr 
null)
+; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 17179869192, i64 %[[P]], 
ptr null)
+
+; A floating-point value or a vector is reported as its bit pattern, a value
+; wider than 64 bits by its low 8 bytes, a pointer with the width of its 
address
+; space, and a value with no fixed-size bit pattern with size 0.
+define void @wide(float noundef %f, double noundef %d, i128 noundef %w, ptr 
addrspace(270) noundef %p32, <4 x i32> noundef %v, <2 x float> noundef %v2, <2 
x ptr> noundef %vp, <vscale x 2 x i64> noundef %sv) {
+entry:
+  call void (...) @use(float %f, double %d, i128 %w, ptr addrspace(270) %p32, 
<4 x i32> %v, <2 x float> %v2, <2 x ptr> %vp, <vscale x 2 x i64> %sv)
+  ret void
+}
+; CHECK-LABEL: define void @wide(
+; CHECK: %[[F:[0-9]+]] = bitcast float %f to i32
+; CHECK: %[[FZ:[0-9]+]] = zext i32 %[[F]] to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 4, i64 %[[FZ]], ptr null)
+; CHECK: %[[D:[0-9]+]] = bitcast double %d to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967304, i64 %[[D]], ptr 
null)
+; CHECK: %[[W:[0-9]+]] = trunc i128 %w to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 8589934600, i64 %[[W]], ptr 
null)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 12884901892,
+; CHECK: %[[V:[0-9]+]] = bitcast <4 x i32> %v to i128
+; CHECK: %[[VT:[0-9]+]] = trunc i128 %[[V]] to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 17179869192, i64 %[[VT]], 
ptr null)
+; CHECK: %[[V2:[0-9]+]] = bitcast <2 x float> %v2 to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 21474836488, i64 %[[V2]], 
ptr null)
+; CHECK: %[[VP:[0-9]+]] = ptrtoint <2 x ptr> %vp to <2 x i64>
+; CHECK: %[[VPB:[0-9]+]] = bitcast <2 x i64> %[[VP]] to i128
+; CHECK: %[[VPT:[0-9]+]] = trunc i128 %[[VPB]] to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 25769803784, i64 %[[VPT]], 
ptr null)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 30064771072, i64 0, ptr 
null)
+
+; A first-class aggregate lives in registers and has no address: each element
+; is reported on its own, under the same arg_idx.
+define void @aggregate({ i32, i64 } noundef %s, [2 x i8] noundef %a) {
+entry:
+  call void (...) @use({ i32, i64 } %s, [2 x i8] %a)
+  ret void
+}
+; CHECK-LABEL: define void @aggregate(
+; CHECK: call void @__sanitizer_cov_trace_args(i64 4,
+; CHECK: call void @__sanitizer_cov_trace_args(i64 8,
+; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967297,
+; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967297,
+
+; A nested first-class aggregate unwinds recursively; each leaf element is
+; reported under the same arg_idx.
+define void @nested_aggregate({ i64, { i32, i32 } } noundef %s) {
+entry:
+  call void (...) @use({ i64, { i32, i32 } } %s)
+  ret void
+}
+; CHECK-LABEL: define void @nested_aggregate(
+; CHECK: %[[E0:[0-9]+]] = extractvalue { i64, { i32, i32 } } %s, 0
+; CHECK: call void @__sanitizer_cov_trace_args(i64 8, i64 %[[E0]], ptr null)
+; CHECK: %[[E1:[0-9]+]] = extractvalue { i64, { i32, i32 } } %s, 1
+; CHECK: %[[E1_0:[0-9]+]] = extractvalue { i32, i32 } %[[E1]], 0
+; CHECK: %[[E1_0_EXT:[0-9]+]] = zext i32 %[[E1_0]] to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 4, i64 %[[E1_0_EXT]], ptr 
null)
+; CHECK: %[[E1_1:[0-9]+]] = extractvalue { i32, i32 } %[[E1]], 1
+; CHECK: %[[E1_1_EXT:[0-9]+]] = zext i32 %[[E1_1]] to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 4, i64 %[[E1_1_EXT]], ptr 
null)
+
+; byval, inalloca, preallocated and byref arguments are the address of a copy
+; of the parameter: they are reported as that object, described by the IR type.
+define void @objects(ptr byval(%struct.T) %a, ptr preallocated(i64) %b, ptr 
byref(%struct.T) %c) {
+entry:
+  call void (...) @use(ptr %a, ptr %b, ptr %c)
+  ret void
+}
+; CHECK-LABEL: define void @objects(
+; CHECK: %[[A:[0-9]+]] = ptrtoint ptr %a to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 16, i64 %[[A]], ptr 
@__sancov_offsets_)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967304, i64 
%{{[0-9]+}}, ptr @__sancov_offsets_.1)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 8589934608, i64 
%{{[0-9]+}}, ptr @__sancov_offsets_)
+
+; inalloca has to be the last parameter.
+define void @inalloca(i32 noundef %x, ptr inalloca(%struct.T) %a) {
+entry:
+  call void (...) @use(i32 %x, ptr %a)
+  ret void
+}
+; CHECK-LABEL: define void @inalloca(
+; CHECK: call void @__sanitizer_cov_trace_args(i64 4,
+; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967312, i64 
%{{[0-9]+}}, ptr @__sancov_offsets_)
+
+; A struct-return pointer is where the return value goes, which trace-ret
+; reports. It is not observed, but %x keeps its IR position, 1.
+define void @sret(ptr sret(%struct.T) %r, i32 noundef %x) {
+entry:
+  call void (...) @use(ptr %r, i32 %x)
+  ret void
+}
+; CHECK-LABEL: define void @sret(
+; CHECK-NOT: ptrtoint ptr %r
+; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967300,
+; CHECK-NOT: call void @__sanitizer_cov_trace_args(
+
+; The error slot of a Swift throwing function may only be loaded, stored, or
+; passed on as a swifterror argument: it is skipped, and %x keeps index 2.
+define swiftcc void @swifterror(ptr noundef swiftself %self, ptr swifterror 
%err, i64 noundef %x) {
+entry:
+  call void (...) @use(ptr %self, i64 %x)
+  ret void
+}
+; CHECK-LABEL: define swiftcc void @swifterror(
+; CHECK-NOT: ptrtoint ptr %err
+; CHECK: call void @__sanitizer_cov_trace_args(i64 8,
+; CHECK: call void @__sanitizer_cov_trace_args(i64 8589934600, i64 %x, ptr 
null)
+; CHECK-NOT: call void @__sanitizer_cov_trace_args(
+
+; The named arguments of a variadic function are reported; the variadic ones
+; have no IR argument.
+define void @varargs(i32 noundef %n, ...) {
+entry:
+  call void (...) @use(i32 %n)
+  ret void
+}
+; CHECK-LABEL: define void @varargs(
+; CHECK: call void @__sanitizer_cov_trace_args(i64 4,
+; CHECK-NOT: call void @__sanitizer_cov_trace_args(
+
+; The calls are first in the entry block and are notail: the runtime identifies
+; an observation by its call's return address.
+define i32 @notail(i32 noundef %x) {
+entry:
+  ret i32 %x
+}
+; CHECK-LABEL: define i32 @notail(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: %[[X:[0-9]+]] = zext i32 %x to i64
+; CHECK-NEXT: notail call void @__sanitizer_cov_trace_args(i64 4, i64 %[[X]], 
ptr null)
+; CHECK-NEXT: ret i32 %x
+
+; An unused argument may be passed anything: an unused value is still
+; reported and frozen, but an unused object is not read, as its pointer may
+; be poison. A value that may be poison - here
+; because none of the arguments is noundef - is frozen, so that the callback
+; receives bits.
+define void @unused(i32 %x, ptr byval(%struct.T) %o, { i32, i64 } %s) {
+entry:
+  ret void
+}
+; CHECK-LABEL: define void @unused(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: %[[XF:[0-9]+]] = freeze i32 %x
+; CHECK-NEXT: %[[X:[0-9]+]] = zext i32 %[[XF]] to i64
+; CHECK-NEXT: notail call void @__sanitizer_cov_trace_args(i64 4, i64 %[[X]], 
ptr null)
+; CHECK-NEXT: notail call void @__sanitizer_cov_trace_args(i64 4294967296, i64 
0, ptr null)
+; CHECK:      notail call void @__sanitizer_cov_trace_args(i64 8589934596,
+; CHECK:      notail call void @__sanitizer_cov_trace_args(i64 8589934600,
+; CHECK-NEXT: ret void
+
+; A pointer the IR says is noundef and dereferenceable(N) is reported with the
+; first N bytes it points to, at most -sanitizer-coverage-trace-pointee-bytes 
(64
+; by default): `val` stays the pointer, `size` is the number of bytes and the
+; table covers them. dereferenceable_or_null may be null, which the runtime
+; checks. A pointer with no such attribute, one without noundef, one in another
+; address space, and one the function does not use are reported by value only.
+define void @pointee(ptr noundef nonnull align 8 dereferenceable(16) %p, ptr 
noundef dereferenceable_or_null(100) %q, ptr noundef %raw, ptr addrspace(1) 
dereferenceable(16) %as1, ptr noundef dereferenceable(12) %r, ptr 
dereferenceable(8) %no_noundef, ptr dereferenceable(8) %dead) {
+entry:
+  call void (...) @use(ptr %p, ptr %q, ptr %raw, ptr addrspace(1) %as1, ptr 
%r, ptr %no_noundef)
+  ret void
+}
+; CHECK-LABEL: define void @pointee(
+; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 16, i64 %[[P]], ptr 
@__sancov_offsets_.2)
+; CHECK: %[[Q:[0-9]+]] = ptrtoint ptr %q to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967360, i64 %[[Q]], ptr 
@__sancov_offsets_.3)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 8589934600, i64 
%{{[0-9]+}}, ptr null)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 12884901896, i64 
%{{[0-9]+}}, ptr null)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 17179869196, i64 
%{{[0-9]+}}, ptr @__sancov_offsets_.4)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 21474836488, i64 
%{{[0-9]+}}, ptr null)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 25769803784, i64 
%{{[0-9]+}}, ptr null)
+; NOPTEE-LABEL: define void @pointee(
+; NOPTEE-COUNT-7: call void @__sanitizer_cov_trace_args(i64 {{[0-9]+}}, i64 
%{{[0-9]+}}, ptr null)
+
+; An object whose IR type is an opaque byte array, as rustc passes and returns
+; one, is read in 8-byte pieces rather than by its first 8 bytes only; so is an
+; element wider than 8 bytes, and a nested struct is described by its elements.
+define void @wide_objects(ptr byval([32 x i8]) %o, ptr byval({ i32, { i8, i64 
}, [3 x i32] }) %n, ptr byval({ i128, i8 }) %w) {
+entry:
+  call void (...) @use(ptr %o, ptr %n, ptr %w)
+  ret void
+}
+; CHECK-LABEL: define void @wide_objects(
+; CHECK: call void @__sanitizer_cov_trace_args(i64 32, i64 %{{[0-9]+}}, ptr 
@__sancov_offsets_.5)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967336, i64 
%{{[0-9]+}}, ptr @__sancov_offsets_.6)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 8589934624, i64 
%{{[0-9]+}}, ptr @__sancov_offsets_.7)
+
+; Static allocas remain first in the entry block. With trace-pc-entry-exit,
+; pc_entry precedes trace_args at the entry and trace_ret precedes pc_exit at a
+; return. nounwind keeps trace-pc-entry-exit from turning the calls into
+; invokes.
+define void @entry_alloca(i32 noundef %x) nounwind {
+entry:
+  %a = alloca i32
+  call void (...) @use(ptr %a, i32 %x)
+  ret void
+}
+; CHECK-LABEL: define void @entry_alloca(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: %a = alloca i32
+; CHECK-NEXT: %[[X:[0-9]+]] = zext i32 %x to i64
+; CHECK-NEXT: notail call void @__sanitizer_cov_trace_args(i64 4, i64 %[[X]], 
ptr null)
+; CHECK-NEXT: call void (...) @use(ptr %a, i32 %x)
+; CHECK-NEXT: ret void
+
+; ENTRY-LABEL: define void @entry_alloca(
+; ENTRY-NEXT: entry:
+; ENTRY-NEXT: %a = alloca i32
+; ENTRY-NEXT: call void @__sanitizer_cov_trace_pc_entry()
+; ENTRY-NEXT: %[[X:[0-9]+]] = zext i32 %x to i64
+; ENTRY-NEXT: notail call void @__sanitizer_cov_trace_args(i64 4, i64 %[[X]], 
ptr null)
+; ENTRY-NEXT: call void (...) @use(ptr %a, i32 %x)
+; ENTRY-NEXT: notail call void @__sanitizer_cov_trace_ret(i64 0, i64 0, ptr 
null)
+; ENTRY-NEXT: notail call void @__sanitizer_cov_trace_pc_exit()
+; ENTRY-NEXT: ret void
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll 
b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
new file mode 100644
index 00000000000000..a0d52dec283f82
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
@@ -0,0 +1,148 @@
+; Return value tracing: one __sanitizer_cov_trace_ret call before every return,
+; decided by the IR alone. A value that fits in a register is reported as the
+; value, a first-class aggregate as its elements, and an indirect (sret) return
+; as the buffer the returned object was written to.
+;
+; opt runs the verifier, so a passing run also proves the emitted IR is
+; well-formed.
+;
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=1 
-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.S = type { i64, i64, i64 }
+
+; CHECK: @__sancov_offsets_ = private unnamed_addr constant [5 x i64] [i64 2, 
i64 0, i64 8, i64 8, i64 4]
+; CHECK: @__sancov_offsets_.1 = private unnamed_addr constant [7 x i64] [i64 
3, i64 0, i64 8, i64 8, i64 8, i64 16, i64 8]
+; CHECK: @__sancov_offsets_.2 = private unnamed_addr constant [9 x i64] [i64 
4, i64 0, i64 8, i64 8, i64 8, i64 16, i64 8, i64 24, i64 8]
+; CHECK-NOT: @__sancov_offsets_
+
+define i32 @scalar(i32 noundef %x) {
+entry:
+  ret i32 %x
+}
+; CHECK-LABEL: define i32 @scalar(
+; CHECK: %[[X:[0-9]+]] = zext i32 %x to i64
+; CHECK-NEXT: call void @__sanitizer_cov_trace_ret(i64 4, i64 %[[X]], ptr null)
+; CHECK-NEXT: ret i32 %x
+
+; A pointer is reported as the address it holds.
+define ptr @pointer(ptr noundef %p) {
+entry:
+  ret ptr %p
+}
+; CHECK-LABEL: define ptr @pointer(
+; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 8, i64 %[[P]], ptr null)
+
+; A pointer the return attributes say is dereferenceable at the return is
+; reported with the bytes it points to, as rustc returns a reference: `val` is
+; the pointer, `size` the 12 bytes read.
+define noundef nonnull align 4 dereferenceable(12) ptr @reference(ptr noundef 
%p) {
+entry:
+  ret ptr %p
+}
+; CHECK-LABEL: define noundef nonnull align 4 dereferenceable(12) ptr 
@reference(
+; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64
+; CHECK-NEXT: call void @__sanitizer_cov_trace_ret(i64 12, i64 %[[P]], ptr 
@__sancov_offsets_)
+; CHECK-NEXT: ret ptr %p
+
+; Every return is reported, each with its own value.
+define i64 @multi(i1 noundef %c, i64 noundef %x) {
+entry:
+  br i1 %c, label %a, label %b
+a:
+  ret i64 %x
+b:
+  ret i64 7
+}
+; CHECK-LABEL: define i64 @multi(
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 8, i64 %x, ptr null)
+; CHECK-NEXT: ret i64 %x
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 8, i64 7, ptr null)
+; CHECK-NEXT: ret i64 7
+
+; A struct returned in registers is reported as its elements.
+define { i64, double } @aggregate(i64 noundef %a, double noundef %b) {
+entry:
+  %0 = insertvalue { i64, double } poison, i64 %a, 0
+  %1 = insertvalue { i64, double } %0, double %b, 1
+  ret { i64, double } %1
+}
+; CHECK-LABEL: define { i64, double } @aggregate(
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 8,
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 8,
+; CHECK-NEXT: ret { i64, double }
+
+; An indirect return is reported as the buffer the returned object was written
+; to, described by the elements of its IR type.
+define void @indirect(ptr sret(%struct.S) %r, i64 noundef %x) {
+entry:
+  store i64 %x, ptr %r
+  ret void
+}
+; CHECK-LABEL: define void @indirect(
+; CHECK: %[[R:[0-9]+]] = ptrtoint ptr %r to i64
+; CHECK-NEXT: call void @__sanitizer_cov_trace_ret(i64 24, i64 %[[R]], ptr 
@__sancov_offsets_.1)
+; CHECK-NEXT: ret void
+
+; rustc types the sret buffer of a Rust-ABI function as an opaque byte array:
+; it is read in 8-byte pieces, all 32 bytes of it.
+define void @opaque_indirect(ptr sret([32 x i8]) %r, i64 noundef %x) {
+entry:
+  store i64 %x, ptr %r
+  ret void
+}
+; CHECK-LABEL: define void @opaque_indirect(
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 32, i64 %{{[0-9]+}}, ptr 
@__sancov_offsets_.2)
+
+; A void return has nothing to report, and says so with size 0. The call is in
+; tail position, but must not become a tail call: the runtime identifies an
+; observation by its call's return address.
+define void @void() {
+entry:
+  ret void
+}
+; CHECK-LABEL: define void @void(
+; CHECK: notail call void @__sanitizer_cov_trace_ret(i64 0, i64 0, ptr null)
+; CHECK-NEXT: ret void
+
+; A vector is reported as its bit pattern.
+define <2 x i32> @vector(<2 x i32> noundef %v) {
+entry:
+  ret <2 x i32> %v
+}
+; CHECK-LABEL: define <2 x i32> @vector(
+; CHECK: %[[V:[0-9]+]] = bitcast <2 x i32> %v to i64
+; CHECK-NEXT: notail call void @__sanitizer_cov_trace_ret(i64 8, i64 %[[V]], 
ptr null)
+
+; A musttail call has to stay adjacent to the return that forwards it, so that
+; return is left alone.
+define i64 @forward(i64 noundef %x) {
+entry:
+  %r = musttail call i64 @forward(i64 %x)
+  ret i64 %r
+}
+; CHECK-LABEL: define i64 @forward(
+; CHECK-NOT: call void @__sanitizer_cov_trace_ret
+; CHECK: musttail call i64 @forward
+; CHECK-NEXT: ret i64 %r
+
+; Unwinding leaves the function without a value.
+declare void @may_throw()
+declare i32 @__gxx_personality_v0(...)
+define i32 @unwinds(i32 noundef %x) personality ptr @__gxx_personality_v0 {
+entry:
+  invoke void @may_throw() to label %ok unwind label %lp
+ok:
+  ret i32 %x
+lp:
+  %l = landingpad { ptr, i32 } cleanup
+  resume { ptr, i32 } %l
+}
+; CHECK-LABEL: define i32 @unwinds(
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 4,
+; CHECK-NEXT: ret i32 %x
+; CHECK-NOT: call void @__sanitizer_cov_trace_ret
+; CHECK: resume

>From a9eeae8a1335252391b48733f8a4ba7aa0d0a0fb Mon Sep 17 00:00:00 2001
From: Yunseong Kim <[email protected]>
Date: Sun, 27 Sep 2026 21:14:06 +0200
Subject: [PATCH 2/3] [Clang][SanitizerCoverage] Add
 -fsanitize-coverage=trace-args,trace-ret

Accept trace-args and trace-ret in -fsanitize-coverage= and forward
them to SanitizerCoverageOptions::TraceArgs/TraceRet through the new
cc1 flags -fsanitize-coverage-trace-args and
-fsanitize-coverage-trace-ret.

Like the other instrumentation types, they satisfy the "requires an
instrumentation type" check and, without a coverage level, select the
edge level. That level alone adds no block callbacks, so
-fsanitize-coverage=trace-args emits argument tracing only.

hasSanitizeCoverage() now accounts for both flags, so either one on its
own runs the pass.

Assisted-by: Kiro CLI (Claude Opus 5.5)
Signed-off-by: Yunseong Kim <[email protected]>
---
 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       | 43 +++++++++++++++++++
 clang/test/Driver/fsanitize-coverage.c        | 13 ++++++
 7 files changed, 83 insertions(+), 4 deletions(-)
 create mode 100644 clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c

diff --git a/clang/include/clang/Basic/CodeGenOptions.def 
b/clang/include/clang/Basic/CodeGenOptions.def
index c8ad0eb3f302e3..277fc5f69841e1 100644
--- a/clang/include/clang/Basic/CodeGenOptions.def
+++ b/clang/include/clang/Basic/CodeGenOptions.def
@@ -330,6 +330,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 5e969a4309bfbd..c2ef8ded0cca65 100644
--- a/clang/include/clang/Basic/CodeGenOptions.h
+++ b/clang/include/clang/Basic/CodeGenOptions.h
@@ -730,7 +730,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 4419e3cf6f98c5..0014c45244bce2 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -8428,6 +8428,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 fe1454026f569d..783b6634786ce2 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -269,6 +269,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 a1bfa90e5dceb2..4b189a0f143292 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)
@@ -1477,6 +1481,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)
@@ -1867,6 +1873,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 00000000000000..ad880544b0b388
--- /dev/null
+++ b/clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c
@@ -0,0 +1,43 @@
+// Check that -fsanitize-coverage-trace-args and -fsanitize-coverage-trace-ret
+// reach the SanitizerCoverage pass and emit their callbacks, and that debug
+// information does not change what they emit.
+//
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -O1 
-debug-info-kind=limited \
+// RUN:     -fsanitize-coverage-type=3 -fsanitize-coverage-trace-args %s -o - \
+// RUN:   | FileCheck %s --check-prefix=ARGS
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -O1 \
+// RUN:     -fsanitize-coverage-type=3 -fsanitize-coverage-trace-args %s -o - \
+// RUN:   | FileCheck %s --check-prefix=ARGS
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -O1 
-debug-info-kind=limited \
+// RUN:     -fsanitize-coverage-type=3 -fsanitize-coverage-trace-ret %s -o - \
+// RUN:   | FileCheck %s --check-prefix=RET
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -O1 
-debug-info-kind=limited \
+// RUN:     -fsanitize-coverage-type=3 %s -o - | FileCheck %s 
--check-prefix=NONE
+
+struct Foo {
+  int a;
+  long b;
+};
+
+void takes_struct_ptr(struct Foo *f) {}
+
+int returns_scalar(int x) { return x + 1; }
+
+// A pointer is an IR value like any other: it is reported as the address it
+// holds, and the struct it points at is not read. Whether debug information
+// describes that struct makes no difference. Nothing is spilled.
+// ARGS-NOT: @__sancov_offsets_
+// ARGS-LABEL: define {{.*}} @takes_struct_ptr(
+// ARGS-NOT: alloca
+// ARGS: %[[ADDR:.*]] = ptrtoint ptr %f to i64
+// ARGS: call void @__sanitizer_cov_trace_args(i64 8, i64 %[[ADDR]], ptr null)
+// ARGS-LABEL: define {{.*}} @returns_scalar(
+// ARGS: call void @__sanitizer_cov_trace_args(i64 4,
+// ARGS-NOT: call void @__sanitizer_cov_trace_ret(
+
+// RET-LABEL: define {{.*}} @returns_scalar(
+// RET: call void @__sanitizer_cov_trace_ret(i64 4,
+// RET-NOT: call void @__sanitizer_cov_trace_args(
+
+// NONE-NOT: call void @__sanitizer_cov_trace_args(
+// NONE-NOT: call void @__sanitizer_cov_trace_ret(
diff --git a/clang/test/Driver/fsanitize-coverage.c 
b/clang/test/Driver/fsanitize-coverage.c
index 21e2c16bfb1b74..9a0c74bc3dda92 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-TRACE-ARGS
+// CHECK-TRACE-ARGS: -fsanitize-coverage-type=3
+// CHECK-TRACE-ARGS: -fsanitize-coverage-trace-args
+
+// RUN: %clang --target=x86_64-linux-gnu -fsanitize-coverage=trace-ret %s -### 
2>&1 | FileCheck %s --check-prefix=CHECK-TRACE-RET
+// CHECK-TRACE-RET: -fsanitize-coverage-type=3
+// CHECK-TRACE-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-TRACE-ARGS-RET
+// CHECK-TRACE-ARGS-RET: -fsanitize-coverage-type=3
+// CHECK-TRACE-ARGS-RET: -fsanitize-coverage-trace-args
+// CHECK-TRACE-ARGS-RET: -fsanitize-coverage-trace-ret

>From 7fb57e6e5fb887b17422ab0d43387d28952392f0 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <[email protected]>
Date: Sun, 27 Sep 2026 21:18:19 +0200
Subject: [PATCH 3/3] [compiler-rt][SanitizerCoverage] Add weak
 trace-args/trace-ret callbacks

Add weak, empty definitions of __sanitizer_cov_trace_args and
__sanitizer_cov_trace_ret to sanitizer_common, and declare them in
sanitizer_interface_internal.h with their contract. A program built with
-fsanitize-coverage=trace-args,trace-ret then links against a sanitizer
runtime without supplying the callbacks itself. A strong definition
overrides the defaults.

The new test overrides both callbacks and checks the reported argument
and return values at -O2 -g and at -O0.

Assisted-by: Kiro CLI (Claude Opus 5.5)
Signed-off-by: Yunseong Kim <[email protected]>
---
 .../sanitizer_coverage_interface.inc          |  2 +
 .../sanitizer_coverage_libcdep_new.cpp        |  7 +++
 .../sanitizer_interface_internal.h            | 27 ++++++++++
 .../sanitizer_coverage_trace_args.cpp         | 53 +++++++++++++++++++
 4 files changed, 89 insertions(+)
 create mode 100644 
compiler-rt/test/sanitizer_common/TestCases/sanitizer_coverage_trace_args.cpp

diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_coverage_interface.inc 
b/compiler-rt/lib/sanitizer_common/sanitizer_coverage_interface.inc
index 9d36a40270d5a2..ff7d971bfe51e3 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_coverage_interface.inc
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_coverage_interface.inc
@@ -38,6 +38,8 @@ INTERFACE_WEAK_FUNCTION(__sanitizer_cov_store4)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_store8)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_store16)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_trace_switch)
+INTERFACE_WEAK_FUNCTION(__sanitizer_cov_trace_args)
+INTERFACE_WEAK_FUNCTION(__sanitizer_cov_trace_ret)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_8bit_counters_init)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_bool_flag_init)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_pcs_init)
diff --git 
a/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp 
b/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp
index 2f5b6f68afa413..963585c06dbd09 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp
@@ -257,6 +257,13 @@ SANITIZER_INTERFACE_WEAK_DEF(void, 
__sanitizer_cov_trace_const_cmp2, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_const_cmp4, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_const_cmp8, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_switch, void) {}
+// Argument and return value tracing (trace-args / trace-ret). Weak, empty
+// defaults so that a program built with -fsanitize-coverage=trace-args and
+// trace-ret links without a runtime consuming the values; libFuzzer, or the
+// user, provides a strong definition. Such a definition must tolerate a size 
of
+// zero, which the instrumentation passes for a value it cannot report.
+SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_args, void) {}
+SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_ret, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_div4, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_div8, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_gep, void) {}
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h 
b/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h
index c424ab1cecf94f..7bfe40a792abea 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h
@@ -115,6 +115,33 @@ SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
 __sanitizer_cov_trace_const_cmp8();
 SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
 __sanitizer_cov_trace_switch();
+// Argument and return value tracing: observe the IR arguments of a function on
+// entry and its return values. What is observed is decided by the LLVM IR
+// alone; debug information never changes it.
+//   __sanitizer_cov_trace_args(u64 tag, u64 val, const u64 *offsets)
+//   __sanitizer_cov_trace_ret (u64 size, u64 val, const u64 *offsets)
+// `tag` is (arg_idx << 32) | size. `arg_idx` is the position of the argument
+// in the IR parameter list, not the source-language parameter number: one
+// source parameter may be several IR arguments, a hidden struct-return pointer
+// takes a position, and the elements of a first-class aggregate are reported
+// one call each under the same index.
+// `val` is the reported value itself - a pointer as the address it holds - if
+// `offsets` is null. Otherwise it is the address of `size` bytes, and
+// offsets[0] is the number N of {byte offset, byte size} pairs that follow it,
+// none wider than 8 bytes, which say what to read there: an object the ABI
+// passed or returned in memory, described by its IR-type elements, or the
+// bytes behind a noundef pointer the IR says are dereferenceable, as many as
+// that says and -sanitizer-coverage-trace-pointee-bytes allows. Such a pointer
+// may be null if the IR said dereferenceable_or_null. A size of zero means
+// nothing was reported: a void return, a value with no fixed-size bit pattern,
+// or an object the function does not use, whose address may be poison.
+// Neither callback is passed the function it speaks for. A runtime that needs
+// it takes its own return address, less one so that the address lies within 
the
+// call, which identifies the observation.
+SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
+__sanitizer_cov_trace_args();
+SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
+__sanitizer_cov_trace_ret();
 SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
 __sanitizer_cov_trace_div4();
 SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
diff --git 
a/compiler-rt/test/sanitizer_common/TestCases/sanitizer_coverage_trace_args.cpp 
b/compiler-rt/test/sanitizer_common/TestCases/sanitizer_coverage_trace_args.cpp
new file mode 100644
index 00000000000000..7b874bc2d77a03
--- /dev/null
+++ 
b/compiler-rt/test/sanitizer_common/TestCases/sanitizer_coverage_trace_args.cpp
@@ -0,0 +1,53 @@
+// Check that -fsanitize-coverage=trace-args,trace-ret calls the two callbacks
+// with the observed argument and return values, and that a user definition
+// overrides compiler-rt's weak defaults.
+//
+// The arguments are the IR arguments, reported on entry, so the values are
+// right at any optimization level, with or without debug information.
+//
+// REQUIRES: has_sancovcc
+// UNSUPPORTED: i386-darwin
+// RUN: %clangxx -O2 -g 
-fsanitize-coverage=trace-pc-guard,trace-args,trace-ret %s -o %t
+// RUN: %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx -O0 -fsanitize-coverage=trace-pc-guard,trace-args,trace-ret 
%s -o %t.O0
+// RUN: %run %t.O0 2>&1 | FileCheck %s
+
+#include <cstdint>
+#include <cstdio>
+
+extern "C" {
+// Stubs so the program links without a sanitizer runtime; these are weak in
+// compiler-rt, so these definitions win.
+void __sanitizer_cov_trace_pc_guard(uint32_t *) {}
+void __sanitizer_cov_trace_pc_guard_init(uint32_t *, uint32_t *) {}
+
+// The consumers under test. A null `offsets` means `val` is the value; a size
+// of zero means the instrumentation had nothing to report. Neither callback is
+// passed the function it speaks for; a consumer that wants it takes its own
+// return address.
+void __sanitizer_cov_trace_args(uint64_t tag, uint64_t val,
+                                const uint64_t *offsets) {
+  uint32_t arg_idx = tag >> 32, size = (uint32_t)tag;
+  if (size == sizeof(int) && !offsets)
+    fprintf(stderr, "ARG idx=%u val=%d\n", arg_idx, (int)val);
+}
+
+void __sanitizer_cov_trace_ret(uint64_t size, uint64_t val,
+                               const uint64_t *offsets) {
+  if (size == sizeof(int) && !offsets)
+    fprintf(stderr, "RET val=%d\n", (int)val);
+}
+}
+
+__attribute__((noinline)) int add(int a, int b) { return a + b; }
+
+int main() {
+  volatile int r = add(41, 2);
+  fprintf(stderr, "r=%d\n", (int)r);
+  return 0;
+}
+
+// CHECK-DAG: ARG idx=0 val=41
+// CHECK-DAG: ARG idx=1 val=2
+// CHECK: RET val=43
+// CHECK: r=43

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

Reply via email to