[clang] [compiler-rt] [llvm] [compiler-rt][SanitizerCoverage] Add LibFuzzer support for trace-args/trace-ret (PR #227612)
Yunseong Kim via llvm-commits
llvm-commits at lists.llvm.org
Wed Sep 30 02:09:18 PDT 2026
https://github.com/yskzalloc created https://github.com/llvm/llvm-project/pull/227612
Fold every reported value into the value-profile map through TracePC::HandleDataflow. The fold binds the value to the site it was observed at the function, the argument or return position, and the field within a struct - so a value never seen at that site before becomes a new value-profile feature under -use_value_profile: an input that reaches a function with a new argument value counts as new coverage and is kept in the corpus. This is the userspace equivalent of what kcov-dataflow records.
A value is folded directly when num_fields is zero, which is the common case and touches no memory at all; only a non-zero num_fields makes the runtime read fields out of the object `val` points at, and at most 32 of them, to bound the work done on every call. A size of zero is the value-unavailable case and is ignored. Returns are folded under a location of their own, because they share a pc with the arguments of the same function.
This is PR 4 of a 5-PR stack. the diff shows `compiler-rt/lib/sanitizer_common/`:
1. Depends on (llvm/): the instrumentation pass and IR tests
2. Depends on (clang/): `-fsanitize-coverage=trace-args,trace-ret` driver/frontend support and documentation https://github.com/llvm/llvm-project/pull/218254
3. Depends on (compiler-rt/lib/sanitizer_common/): Add a runtime for trace-args/trace-ret https://github.com/llvm/llvm-project/pull/218265
4. This PR (compiler-rt/lib/fuzzer/): Add libFuzzer support as a value-profile consumer
5. Follow-up (clang/docs/SanitizerCoverage.md): Documentation update
>From 92883bfc4b9d2307f8659cc8dd81d87ade5acee3 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 1/4] [SanitizerCoverage] Add trace-args and trace-ret
instrumentation modes
Add two SanitizerCoverage modes that report the values flowing in and out
of instrumented functions, selected through SanitizerCoverageOptions
(TraceArgs/TraceRet) and, for opt, -sanitizer-coverage-trace-args and
-sanitizer-coverage-trace-ret. trace-args emits a call per source-level
parameter in the entry block, trace-ret one before every return. When
used alone, either mode defaults the SanitizerCoverage level to edge.
void __sanitizer_cov_trace_args(u64 pc, u32 arg_idx, u32 size, u64 val,
u64 *offsets, u32 num_fields);
void __sanitizer_cov_trace_ret (u64 pc, u32 size, u64 val,
u64 *offsets, u32 num_fields);
`pc` is the address of the instrumented function. `num_fields` says what
`val` holds:
- 0: `val` is the value itself, its low `size` bytes, zero-extended. A
pointer is reported as the address it holds, a floating-point value as
its bit pattern, and a value wider than 64 bits by its low half. This
is nearly everything, and it needs no memory of its own.
- non-zero: `val` is the address of a `size` byte object, and `offsets`
points at a constant table of `num_fields` {byte offset, byte size}
pairs describing its fields, so a consumer can read them out of it.
Only an object that already lives in memory is reported this way: a
pointer to a struct, a by-value struct the ABI passes indirectly, the
buffer of an indirect struct return. The table is shared by every value
of the same struct type.
`size == 0` means there was nothing to report: a parameter the optimizer
removed, a void return, a value of a type that cannot be widened.
The consumers are the Linux kernel's KCOV dataflow subsystem and libFuzzer
value profiling, which use the values as coverage: a call reaching a
function with an argument or a struct field value never seen there before
is new coverage, which drives a fuzzer towards the state a bug needs
rather than merely towards the code containing it.
Reporting the value rather than an address to read it from is what keeps
both modes target-independent. The pass never creates memory to report a
value from, so nothing escapes to the stack and no frame is realigned on
the instrumentation's account. Spilling instead would not work
everywhere: the slot escapes, because its address is handed to the
callback, so (K)ASan gives it a redzone and 32-byte alignment; that
realigns the frame, which makes the backend reserve a base pointer. On
x86-64 the base pointer is RBX, and a function whose inline assembly also
claims RBX - a cpuid with an "=b" operand, an rdtsc clobbering it - then
fails to compile with "Interference usage of base pointer/frame pointer".
trace-no-spill.ll pins down that such a function is instrumented like any
other.
Two consequences worth stating:
- A pointer whose pointee has no known field layout - void *, int * - is
reported as the pointer value rather than as an address to read
through. Reporting the pointee of an arbitrary pointer is a read the
instrumentation has no business making.
- A struct the ABI passed or returned in registers has no address, so it
is reported as one call per register piece, all carrying the same
arg_idx. Its field values are still observed, they are just not split
along source field boundaries; a struct passed indirectly still is.
`arg_idx` is the source-level parameter index, recovered by mapping IR
values back through DILocalVariable::getArg(). The frontend assigns those
numbers before ABI lowering, so they keep naming the same source
parameter after the ABI has rewritten the signature: a hidden sret or
`this` pointer is not counted, and a parameter the optimizer removed
entirely is still reported, with size 0, so a consumer sees the full
parameter list. A struct return lowered to an indirect return leaves an
IR function returning void; trace-ret reports the caller-provided sret
buffer rather than dropping the return.
Debug info is not required: without usable debug records the pass reports
the IR arguments positionally, which keeps both modes usable on optimized
or -g-less code at the price of exposing the ABI's view of the arguments.
Points worth noting, all from instrumenting a KASAN kernel with both
modes enabled:
- The calls carry a synthetic !dbg location, from
InstrumentationIRBuilder. Without one the verifier rejects a function
built with -g once inlining runs ("inlinable function call ...
requires a !dbg location").
- Debug records are exempt from SSA dominance, so a record may name a
value defined after the trace call. Reporting it produced IR failing
"Instruction does not dominate all uses" and, with the verifier
disabled as kernel builds do, crashed RegisterCoalescer. Such a
parameter is reported with size 0, as is a parameter interprocedural
optimization proved dead and described as poison.
- Only records belonging to the instrumented function may drive
trace-args. An inlined callee's parameters are numbered too and its
records can name our arguments -- kmalloc(size, flags) inlined into
f(ptr, size) leaves #dbg_value(%size, "size", arg: 1) behind -- which
would otherwise be reported as the caller's first parameter.
- A return forwarding a musttail call is left alone: the call has to stay
adjacent to the return, so there is nowhere to put the trace call.
Tests cover both callbacks, source-level parameter numbering across sret
insertion and aggregate splitting, field offset tables and their reuse,
structs reported in pieces, indirect struct returns, the reported
parameter list staying complete, the debug-record filters above, the
no-debug-info fallback, and that no configuration emits an alloca.
Clang driver support (-fsanitize-coverage=trace-args,trace-ret) and the
compiler-rt runtime land in follow-up patches.
Assisted-by: Kiro CLI (Claude Opus 5)
Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
.../llvm/Transforms/Utils/Instrumentation.h | 2 +
.../Instrumentation/SanitizerCoverage.cpp | 535 +++++++++++++++++-
.../SanitizerCoverage/trace-args-abi.ll | 127 +++++
.../SanitizerCoverage/trace-args-dominance.ll | 67 +++
.../SanitizerCoverage/trace-args-inlined.ll | 52 ++
.../SanitizerCoverage/trace-args-no-debug.ll | 35 ++
.../SanitizerCoverage/trace-args.ll | 130 +++++
.../SanitizerCoverage/trace-no-spill.ll | 67 +++
.../SanitizerCoverage/trace-ret.ll | 138 +++++
9 files changed, 1152 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-inlined.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-no-spill.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..a5dca75050378 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/DebugProgramInstruction.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/InstIterator.h"
#include "llvm/IR/IntrinsicInst.h"
#include "llvm/IR/Intrinsics.h"
#include "llvm/IR/LLVMContext.h"
@@ -31,6 +35,7 @@
#include "llvm/IR/Type.h"
#include "llvm/IR/ValueSymbolTable.h"
#include "llvm/Support/CommandLine.h"
+#include "llvm/Support/MathExtras.h"
#include "llvm/Support/SpecialCaseList.h"
#include "llvm/Support/VirtualFileSystem.h"
#include "llvm/TargetParser/Triple.h"
@@ -46,6 +51,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 +163,14 @@ 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("Tracing of function arguments"),
+ cl::Hidden);
+
+static cl::opt<bool> ClTraceRet("sanitizer-coverage-trace-ret",
+ cl::desc("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);
@@ -290,6 +310,17 @@ class ModuleSanitizerCoverage {
std::string getSectionStart(const std::string &Section) const;
std::string getSectionEnd(const std::string &Section) const;
+ /// The `offsets` and `num_fields` arguments of the trace-args/trace-ret
+ /// callbacks: a constant table holding one {byte offset, byte size} pair per
+ /// field of a struct, and the size of that struct. Table is null when the
+ /// field layout is unknown, in which case NumFields and ObjectSize are 0.
+ struct FieldOffsets {
+ Constant *Table = nullptr;
+ unsigned NumFields = 0;
+ uint64_t ObjectSize = 0;
+ };
+ FieldOffsets getFieldOffsets(DIType *Ty);
+
Module &M;
DomTreeCallback DTCallback;
PostDomTreeCallback PDTCallback;
@@ -298,6 +329,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;
@@ -321,6 +353,11 @@ class ModuleSanitizerCoverage {
SmallVector<GlobalValue *, 20> GlobalsToAppendToUsed;
SmallVector<GlobalValue *, 20> GlobalsToAppendToCompilerUsed;
+ /// Field offset tables are shared by every value of the same struct type: a
+ /// type used by hundreds of functions must not emit hundreds of identical
+ /// tables.
+ DenseMap<const DICompositeType *, FieldOffsets> FieldOffsetsCache;
+
SanitizerCoverageOptions Options;
const SpecialCaseList *Allowlist;
@@ -542,6 +579,18 @@ 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 pc, u32 arg_idx, u32 size, u64 val,
+ // u64 *offsets, u32 num_fields)
+ SanCovTraceArgsFunc =
+ M.getOrInsertFunction(SanCovTraceArgsName, VoidTy, Int64Ty, Int32Ty,
+ Int32Ty, Int64Ty, PtrTy, Int32Ty);
+ // void __sanitizer_cov_trace_ret(u64 pc, u32 size, u64 val,
+ // u64 *offsets, u32 num_fields)
+ SanCovTraceRetFunc = M.getOrInsertFunction(
+ SanCovTraceRetName, VoidTy, Int64Ty, Int32Ty, Int64Ty, PtrTy, Int32Ty);
+
SanCovStackDepthCallback =
M.getOrInsertFunction(SanCovStackDepthCallbackName, VoidTy);
@@ -767,6 +816,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 +1321,481 @@ void ModuleSanitizerCoverage::createFunctionControlFlow(Function &F) {
ConstantArray::get(ArrayType::get(PtrTy, CFs.size()), CFs));
FunctionCFsArray->setConstant(true);
}
+
+//===----------------------------------------------------------------------===//
+// Argument and return value tracing.
+//===----------------------------------------------------------------------===//
+//
+// -sanitizer-coverage-trace-args reports every source-level parameter of an
+// instrumented function on entry, -sanitizer-coverage-trace-ret reports the
+// value of every return:
+//
+// void __sanitizer_cov_trace_args(u64 pc, u32 arg_idx, u32 size, u64 val,
+// u64 *offsets, u32 num_fields);
+// void __sanitizer_cov_trace_ret (u64 pc, u32 size, u64 val,
+// u64 *offsets, u32 num_fields);
+//
+// `pc` is the address of the instrumented function. What `val` holds depends on
+// `num_fields`:
+//
+// - `num_fields == 0`: `val` is the value itself, the low `size` bytes of it,
+// zero-extended. Integers, pointers and floating-point values are reported
+// this way, which is nearly everything, and needs no memory of its own.
+//
+// - `num_fields != 0`: `val` is the address of a `size` byte object and
+// `offsets` points at a constant table of `num_fields` {byte offset, byte
+// size} pairs describing its fields, so that a consumer can read them out of
+// it. Only an object that already lives in memory is reported this way: a
+// pointer to a struct, a by-value struct the ABI passes indirectly, or the
+// buffer of an indirect struct return. The pass never creates memory to
+// report a value from, so no stack slot escapes and no frame is realigned on
+// its account.
+//
+// `size == 0` means the pass had nothing to report - a parameter the optimizer
+// removed, a value of a type it cannot widen, a void return.
+//
+// `arg_idx` is the zero-based *source-level* parameter index. Argument values
+// are recovered from debug records rather than from the IR argument list,
+// because the two disagree as soon as the ABI rewrites the signature; see
+// collectSourceParams(). A function without debug records falls back to the IR
+// argument list, which keeps both modes usable on code built without -g, at
+// the price of exposing the ABI's view of the arguments.
+
+/// Peel the typedefs and qualifiers off \p Ty to reach the type they decorate.
+static DIType *stripTypedefsAndQualifiers(DIType *Ty) {
+ while (auto *Derived = dyn_cast_or_null<DIDerivedType>(Ty)) {
+ switch (Derived->getTag()) {
+ case dwarf::DW_TAG_typedef:
+ case dwarf::DW_TAG_const_type:
+ case dwarf::DW_TAG_volatile_type:
+ case dwarf::DW_TAG_restrict_type:
+ case dwarf::DW_TAG_atomic_type:
+ case dwarf::DW_TAG_immutable_type:
+ Ty = Derived->getBaseType();
+ continue;
+ default:
+ return Ty;
+ }
+ }
+ return Ty;
+}
+
+/// Return the aggregate whose fields describe \p Ty: either \p Ty itself, for
+/// a by-value struct, or its pointee, for a pointer to one. Returns null for
+/// any other type.
+static DICompositeType *getTracedStructType(DIType *Ty) {
+ Ty = stripTypedefsAndQualifiers(Ty);
+ if (auto *Derived = dyn_cast_or_null<DIDerivedType>(Ty))
+ if (Derived->getTag() == dwarf::DW_TAG_pointer_type)
+ Ty = stripTypedefsAndQualifiers(Derived->getBaseType());
+
+ auto *Composite = dyn_cast_or_null<DICompositeType>(Ty);
+ if (!Composite)
+ return nullptr;
+ switch (Composite->getTag()) {
+ case dwarf::DW_TAG_structure_type:
+ case dwarf::DW_TAG_class_type:
+ return Composite;
+ default:
+ return nullptr;
+ }
+}
+
+/// Number of parameters \p SP declares in source, or 0 if that is unknown.
+static unsigned getNumDeclaredParams(DISubprogram *SP) {
+ if (!SP || !SP->getType())
+ return 0;
+ // The type array is {return type, parameter types...}.
+ unsigned Size = SP->getType()->getTypeArray().size();
+ return Size ? Size - 1 : 0;
+}
+
+/// Declared type of \p SP's \p Idx-th parameter, numbered from 1 as DWARF
+/// numbers parameters, or null if it is unknown.
+static DIType *getDeclaredParamType(DISubprogram *SP, unsigned Idx) {
+ if (Idx == 0 || Idx > getNumDeclaredParams(SP))
+ return nullptr;
+ return SP->getType()->getTypeArray()[Idx];
+}
+
+/// Declared return type of \p SP, or null if it is unknown or void.
+static DIType *getDeclaredReturnType(DISubprogram *SP) {
+ if (!SP || !SP->getType() || SP->getType()->getTypeArray().empty())
+ return nullptr;
+ return SP->getType()->getTypeArray()[0];
+}
+
+ModuleSanitizerCoverage::FieldOffsets
+ModuleSanitizerCoverage::getFieldOffsets(DIType *Ty) {
+ DICompositeType *Composite = getTracedStructType(Ty);
+ if (!Composite)
+ return {};
+ if (auto It = FieldOffsetsCache.find(Composite);
+ It != FieldOffsetsCache.end())
+ return It->second;
+
+ SmallVector<Constant *, 16> Pairs;
+ for (DINode *Element : Composite->getElements()) {
+ auto *Member = dyn_cast<DIDerivedType>(Element);
+ if (!Member || Member->getTag() != dwarf::DW_TAG_member ||
+ Member->isStaticMember())
+ continue;
+ uint64_t SizeInBits = Member->getSizeInBits();
+ if (!SizeInBits)
+ continue; // A flexible array member or an empty base class.
+ // The callbacks describe fields in bytes, so widen a bitfield to the bytes
+ // that hold it instead of reporting a zero-sized field.
+ uint64_t FirstByte = Member->getOffsetInBits() / 8;
+ uint64_t EndByte = divideCeil(Member->getOffsetInBits() + SizeInBits, 8);
+ Pairs.push_back(ConstantInt::get(Int64Ty, FirstByte));
+ Pairs.push_back(ConstantInt::get(Int64Ty, EndByte - FirstByte));
+ }
+
+ FieldOffsets Offsets;
+ if (!Pairs.empty()) {
+ 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);
+ Offsets = {Table, static_cast<unsigned>(Pairs.size() / 2),
+ divideCeil(Composite->getSizeInBits(), 8)};
+ }
+ FieldOffsetsCache[Composite] = Offsets;
+ return Offsets;
+}
+
+namespace {
+
+/// One of the IR values that hold a source-level object, together with its bit
+/// offset in that object. A scalar has a single piece at offset 0.
+using ValuePiece = std::pair<Value *, uint64_t>;
+
+/// A source-level parameter of a function and the entry-block values that hold
+/// it. The ABI may coerce one parameter into several values, each described by
+/// a DW_OP_LLVM_fragment debug record.
+struct SourceParam {
+ DIType *Ty = nullptr;
+ SmallVector<ValuePiece, 2> Pieces;
+ /// Whether Pieces holds fragments of the parameter rather than the whole of
+ /// it. A parameter can lose a fragment (see collectSourceParams), so the
+ /// number of pieces alone does not answer this.
+ bool Fragmented = false;
+ /// Whether Pieces holds incoming Arguments rather than derived values.
+ bool FromArguments = false;
+
+ /// Record that \p V holds this parameter's bits from \p BitOffset on.
+ ///
+ /// A record naming an incoming Argument describes the parameter as it was
+ /// passed, so it wins over records naming values derived from it. Exact
+ /// duplicates are dropped: a repeated record would otherwise make a scalar
+ /// look like a multi-piece aggregate and be needlessly reassembled.
+ void addPiece(Value *V, uint64_t BitOffset, bool IsFragment) {
+ bool IsArgument = isa<Argument>(V);
+ if (IsArgument && !FromArguments) {
+ Pieces.clear();
+ Fragmented = false;
+ FromArguments = true;
+ } else if (!IsArgument && FromArguments) {
+ return;
+ }
+ if (is_contained(Pieces, ValuePiece(V, BitOffset)))
+ return;
+ Pieces.emplace_back(V, BitOffset);
+ Fragmented |= IsFragment;
+ }
+};
+
+using SourceParamMap = SmallDenseMap<unsigned, SourceParam, 8>;
+
+} // namespace
+
+/// Map the source-level parameters of \p F to the values that hold them on
+/// entry, keyed by the DWARF parameter number (counted from 1).
+///
+/// The frontend numbers parameters, in DILocalVariable::getArg(), before ABI
+/// lowering, so the number keeps naming the same source parameter even when
+/// the ABI inserts a hidden argument or splits an aggregate across several.
+/// Debug records are the only link back to that numbering, which is why they,
+/// and not the IR argument list, drive trace-args.
+///
+/// A parameter is left out of \p Params when it has no location the trace call
+/// can use; the caller reports those with a null value pointer.
+static void collectSourceParams(Function &F, SourceParamMap &Params) {
+ BasicBlock &EntryBB = F.getEntryBlock();
+ DISubprogram *SP = F.getSubprogram();
+
+ for (Instruction &I : instructions(F)) {
+ for (DbgVariableRecord &DVR : filterDbgVars(I.getDbgRecordRange())) {
+ DILocalVariable *Var = DVR.getVariable();
+ if (!Var || !Var->getArg())
+ continue;
+ // Only this function's own parameters are numbered for us. An inlined
+ // callee's parameters are numbered too, and its records may name our
+ // Arguments: kmalloc(size, flags) inlined into f(ptr, size) leaves
+ // #dbg_value(%size, "size", arg: 1) behind, which would otherwise be
+ // taken as a description of f's first parameter.
+ if (DVR.getDebugLoc() && DVR.getDebugLoc().getInlinedAt())
+ continue;
+ if (SP && Var->getScope() && Var->getScope()->getSubprogram() != SP)
+ continue;
+ // A #dbg_declare names the parameter's storage rather than its incoming
+ // value. That storage is written by the prologue, which follows the trace
+ // call, so reading it here would report whatever the stack held.
+ if (DVR.isDbgDeclare())
+ continue;
+
+ // Only a location that is the value itself can be reported. An
+ // expression that computes the value - a field shifted out of a wider
+ // register, an offset applied to a pointer - cannot be replayed into the
+ // spill slot, and storing the value it starts from would write the wrong
+ // bytes and overrun the piece.
+ DIExpression *Expr = DVR.getExpression();
+ std::optional<DIExpression::FragmentInfo> Fragment =
+ Expr->getFragmentInfo();
+ if (Expr->getNumElements() != (Fragment ? 3u : 0u))
+ continue;
+
+ Value *V = DVR.getValue();
+ if (!V)
+ continue;
+ // A parameter that interprocedural optimization proved dead is described
+ // as poison, and callers pass poison for it: there is nothing to report.
+ if (isa<UndefValue>(V))
+ continue;
+ // Debug records are exempt from SSA dominance, so a record may name a
+ // value that is not available where the trace call goes. Reporting it
+ // would produce IR failing the verifier with "Instruction does not
+ // dominate all uses".
+ if (auto *Def = dyn_cast<Instruction>(V))
+ if (Def->getParent() != &EntryBB || Def->isTerminator())
+ continue;
+
+ SourceParam &Param = Params[Var->getArg()];
+ Param.Ty = Var->getType();
+ Param.addPiece(V, Fragment ? Fragment->OffsetInBits : 0,
+ Fragment.has_value());
+ }
+ }
+}
+
+/// Widen \p V to the 64-bit value the callbacks take: a pointer is reported as
+/// the address it holds, a floating-point value as its bit pattern, and a value
+/// wider than 64 bits by its low half. Returns the value together with the
+/// number of bytes of it that are meaningful, or {nullptr, 0} for a type that
+/// cannot be widened - a vector, or an aggregate the ABI passes in registers.
+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();
+
+ // ptrtoint widens or narrows to the requested type, so this also holds for a
+ // target whose pointers are narrower than 64 bits.
+ if (Ty->isPointerTy())
+ return {IRB.CreatePtrToInt(V, Int64Ty), DL.getPointerSize()};
+
+ if (Ty->isFloatingPointTy()) {
+ unsigned Bits = Ty->getPrimitiveSizeInBits();
+ V = IRB.CreateBitCast(V, IRB.getIntNTy(Bits));
+ Ty = V->getType();
+ }
+ if (!Ty->isIntegerTy())
+ return {nullptr, 0};
+
+ uint64_t Size = divideCeil(Ty->getIntegerBitWidth(), 8);
+ if (Size > sizeof(uint64_t)) {
+ // A value that does not fit is reported by its low bytes rather than not at
+ // all: they are what a comparison against it looks at first.
+ V = IRB.CreateTrunc(V, Int64Ty);
+ Size = sizeof(uint64_t);
+ } else {
+ V = IRB.CreateZExt(V, Int64Ty);
+ }
+ return {V, Size};
+}
+
+/// Widen \p V for the callbacks, splitting a value that the ABI passed as a
+/// first-class aggregate into its elements: those live in separate registers
+/// and share no address, so each is reported on its own. Appends at least one
+/// entry, {nullptr, 0} for a value that cannot be reported at all.
+static void
+getReportedValues(IRBuilderBase &IRB, Value *V,
+ SmallVectorImpl<std::pair<Value *, uint64_t>> &Out) {
+ size_t First = Out.size();
+ Type *Ty = V->getType();
+ unsigned NumElements = Ty->isStructTy() ? Ty->getStructNumElements()
+ : Ty->isArrayTy() ? Ty->getArrayNumElements()
+ : 0;
+ if (NumElements)
+ for (unsigned I = 0; I != NumElements; ++I)
+ Out.push_back(getReportedValue(IRB, IRB.CreateExtractValue(V, I)));
+ else
+ Out.push_back(getReportedValue(IRB, V));
+
+ if (Out.size() == First)
+ Out.emplace_back(nullptr, 0);
+}
+
+void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
+ DISubprogram *SP = F.getSubprogram();
+ BasicBlock &EntryBB = F.getEntryBlock();
+
+ SourceParamMap Params;
+ collectSourceParams(F, Params);
+
+ // Trace as early as the reported values allow: at the first insertion point
+ // of the entry block, pushed down past the definition of every value that is
+ // reported so that it dominates the call.
+ BasicBlock::iterator IP = EntryBB.getFirstInsertionPt();
+ for (const auto &[Idx, Param] : Params)
+ for (const auto &[V, BitOffset] : Param.Pieces)
+ if (auto *Def = dyn_cast<Instruction>(V); Def && !Def->comesBefore(&*IP))
+ IP = std::next(Def->getIterator());
+
+ // InstrumentationIRBuilder gives the calls a synthetic !dbg location. A
+ // plain IRBuilder would leave them without one, which fails the verifier
+ // ("inlinable function call ... requires a !dbg location") once a function
+ // built with -g is inlined.
+ InstrumentationIRBuilder IRB(&*IP);
+ Value *PC = IRB.CreatePtrToInt(&F, Int64Ty);
+
+ auto trace = [&](unsigned Idx, Value *Val, uint64_t Size,
+ FieldOffsets Offsets) {
+ IRB.CreateCall(
+ SanCovTraceArgsFunc,
+ {PC, ConstantInt::get(Int32Ty, Idx - 1),
+ ConstantInt::get(Int32Ty, Size),
+ Val ? Val : ConstantInt::get(Int64Ty, 0),
+ Offsets.Table ? Offsets.Table : ConstantPointerNull::get(PtrTy),
+ ConstantInt::get(Int32Ty, Offsets.NumFields)});
+ };
+
+ // Report \p V as the value of the \p Idx-th parameter, declared as \p Ty.
+ // A pointer to a struct whose fields are known is reported as the address of
+ // that struct, so that a consumer can read the fields out of it; every other
+ // value is reported as the value itself. \p WholeObject says whether \p V is
+ // the entire parameter, which a fragment of a struct the ABI split across
+ // registers is not - such a fragment may well be a pointer, but it is not the
+ // address of the struct its type describes.
+ auto traceValue = [&](unsigned Idx, Value *V, DIType *Ty, bool WholeObject) {
+ // Ask for the field table only on the path that uses it: building one for a
+ // struct that ends up reported by value would leave an unreferenced global
+ // behind in every object file.
+ if (WholeObject && V->getType()->isPointerTy())
+ if (FieldOffsets Offsets = getFieldOffsets(Ty); Offsets.Table) {
+ trace(Idx, IRB.CreatePtrToInt(V, Int64Ty), Offsets.ObjectSize, Offsets);
+ return;
+ }
+ SmallVector<std::pair<Value *, uint64_t>, 2> Values;
+ getReportedValues(IRB, V, Values);
+ for (auto [Val, Size] : Values)
+ trace(Idx, Val, Size, {});
+ };
+
+ // Without debug records there is nothing to map the IR arguments back to, so
+ // report them positionally and let the declared parameter types, if there
+ // are any, describe their fields. ABI lowering is visible to the consumer in
+ // this mode: a coerced aggregate is reported as the values it was coerced
+ // into, and the indices of the parameters after it shift accordingly.
+ if (Params.empty()) {
+ unsigned Idx = 1;
+ for (Argument &Arg : F.args()) {
+ // A struct-return pointer has no source-level counterpart.
+ if (Arg.hasStructRetAttr())
+ continue;
+ traceValue(Idx, &Arg, getDeclaredParamType(SP, Idx),
+ /*WholeObject=*/true);
+ ++Idx;
+ }
+ return;
+ }
+
+ // One call per source-level parameter, in source order, except that a
+ // parameter the ABI split across registers is reported once per piece: the
+ // pieces have no common address to report them from. A parameter with no
+ // usable location is still reported, with size 0, so that a consumer sees
+ // every parameter the function declares.
+ unsigned NumParams = getNumDeclaredParams(SP);
+ for (const auto &[Idx, Param] : Params)
+ NumParams = std::max(NumParams, Idx);
+
+ for (unsigned Idx = 1; Idx <= NumParams; ++Idx) {
+ auto It = Params.find(Idx);
+ if (It == Params.end() || It->second.Pieces.empty()) {
+ trace(Idx, nullptr, 0, {});
+ continue;
+ }
+ const SourceParam &Param = It->second;
+ bool WholeObject = Param.Pieces.size() == 1 && !Param.Fragmented;
+ for (const auto &[V, BitOffset] : Param.Pieces)
+ traceValue(Idx, V, Param.Ty, WholeObject);
+ }
+}
+
+void ModuleSanitizerCoverage::InjectTraceForRet(Function &F) {
+ DIType *RetTy = getDeclaredReturnType(F.getSubprogram());
+
+ // A struct returned by value may be lowered to an indirect return: the IR
+ // function returns void and writes the result through a hidden struct-return
+ // pointer. Report that buffer, so an indirect return is not dropped. This
+ // mirrors the argument side, which skips the same pointer.
+ Argument *SRetArg = nullptr;
+ for (Argument &Arg : F.args())
+ if (Arg.hasStructRetAttr()) {
+ SRetArg = &Arg;
+ break;
+ }
+
+ 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);
+ Value *PC = IRB.CreatePtrToInt(&F, Int64Ty);
+
+ auto trace = [&](Value *Val, uint64_t Size, FieldOffsets Offsets) {
+ IRB.CreateCall(
+ SanCovTraceRetFunc,
+ {PC, ConstantInt::get(Int32Ty, Size),
+ Val ? Val : ConstantInt::get(Int64Ty, 0),
+ Offsets.Table ? Offsets.Table : ConstantPointerNull::get(PtrTy),
+ ConstantInt::get(Int32Ty, Offsets.NumFields)});
+ };
+
+ Value *RetVal = RI->getReturnValue();
+ // The value is reported by address when it is one: a pointer whose pointee
+ // fields are known, or the buffer of an indirect struct return. A struct
+ // returned in registers has no address, so it is reported as its elements.
+ if (RetVal ? RetVal->getType()->isPointerTy() : SRetArg != nullptr) {
+ Value *Object = RetVal ? RetVal : SRetArg;
+ if (FieldOffsets Offsets = getFieldOffsets(RetTy); Offsets.Table) {
+ trace(IRB.CreatePtrToInt(Object, Int64Ty), Offsets.ObjectSize, Offsets);
+ continue;
+ }
+ // An indirect return whose field layout is unknown is a struct in memory,
+ // not a value that fits in a register, so there is nothing to report.
+ if (!RetVal) {
+ trace(nullptr, 0, {});
+ continue;
+ }
+ }
+
+ if (!RetVal) {
+ trace(nullptr, 0, {}); // A void return.
+ continue;
+ }
+ SmallVector<std::pair<Value *, uint64_t>, 2> Values;
+ getReportedValues(IRB, RetVal, Values);
+ for (auto [Val, Size] : Values)
+ trace(Val, Size, {});
+ }
+}
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..eac49ee3bd1a3
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll
@@ -0,0 +1,127 @@
+; Argument tracing across ABI lowering. A by-value struct the ABI passed in
+; registers has no address, so it is reported as one call per register piece,
+; all carrying the same source-level parameter index. A struct the ABI passed
+; indirectly does have an address, and is reported through it with its field
+; offset table.
+;
+; 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 }
+
+; Only struct big needs a field table: it is the only one reported by address.
+; CHECK: @__sancov_offsets_ = private unnamed_addr constant [10 x i64] [i64 0, i64 8, i64 8, i64 8, i64 16, i64 8, i64 24, i64 8, i64 32, i64 8]
+
+; int use_pair(struct pair p, int z) with struct pair { int x; int y; } coerced
+; into a single i64 and split back into two i32 fragments. Both pieces are
+; reported under index 0, and `z` keeps index 1.
+define i32 @use_pair(i64 %0, i32 %1) !dbg !13 {
+entry:
+ %2 = trunc i64 %0 to i32
+ %3 = lshr i64 %0, 32
+ %4 = trunc nuw i64 %3 to i32
+ #dbg_value(i32 %2, !17, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !19)
+ #dbg_value(i32 %4, !17, !DIExpression(DW_OP_LLVM_fragment, 32, 32), !19)
+ #dbg_value(i32 %1, !18, !DIExpression(), !19)
+ %5 = add i32 %2, %4
+ ret i32 %5
+}
+; Nothing is spilled, so no field table is built for struct pair either.
+; CHECK-LABEL: define i32 @use_pair(
+; CHECK-NOT: alloca
+; CHECK: %[[LO:[0-9]+]] = zext i32 %2 to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_pair to i64), i32 0, i32 4, i64 %[[LO]], ptr null, i32 0)
+; CHECK: %[[HI:[0-9]+]] = zext i32 %4 to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_pair to i64), i32 0, i32 4, i64 %[[HI]], ptr null, i32 0)
+; CHECK: %[[Z:[0-9]+]] = zext i32 %1 to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_pair to i64), i32 1, i32 4, i64 %[[Z]], ptr null, i32 0)
+
+; A fragment whose location is an expression that *computes* the value - here a
+; field shifted out of a wider register - is not a value that can be reported,
+; so it is dropped while the fragments that are values are still reported.
+;
+; The dropped fragment is also defined before the one that follows it, so this
+; doubles as a check that the calls are placed after every value they report:
+; opt runs the verifier, which rejects a use that does not dominate.
+define i32 @use_trio([2 x i64] %0) !dbg !30 {
+entry:
+ %1 = extractvalue [2 x i64] %0, 0
+ %2 = trunc i64 %1 to i32
+ %3 = extractvalue [2 x i64] %0, 1
+ #dbg_value(i32 %2, !34, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !35)
+ #dbg_value(i64 %1, !34, !DIExpression(DW_OP_constu, 32, DW_OP_shr, DW_OP_LLVM_convert, 64, DW_ATE_unsigned, DW_OP_LLVM_convert, 32, DW_ATE_unsigned, DW_OP_stack_value, DW_OP_LLVM_fragment, 32, 32), !35)
+ #dbg_value(i64 %3, !34, !DIExpression(DW_OP_LLVM_fragment, 64, 64), !35)
+ ret i32 %2
+}
+; CHECK-LABEL: define i32 @use_trio(
+; CHECK: %[[A:[0-9]+]] = zext i32 %2 to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_trio to i64), i32 0, i32 4, i64 %[[A]], ptr null, i32 0)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_trio to i64), i32 0, i32 8, i64 %3, ptr null, i32 0)
+; CHECK-NOT: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_trio to i64), i32 1
+
+; void use_big(struct big b) passed indirectly: the parameter is a pointer to
+; the caller's copy, described by the struct's own type, so it is reported as
+; the address of that copy with its five fields.
+define void @use_big(ptr byval(%struct.big) align 8 %0) !dbg !20 {
+entry:
+ #dbg_value(ptr %0, !24, !DIExpression(), !25)
+ ret void
+}
+; CHECK-LABEL: define void @use_big(
+; CHECK: %[[ADDR:[0-9]+]] = ptrtoint ptr %0 to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_big to i64), i32 0, i32 40, i64 %[[ADDR]], ptr @__sancov_offsets_, i32 5)
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!1, !2}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !3, isOptimized: true, emissionKind: FullDebug)
+!1 = !{i32 2, !"Dwarf Version", i32 5}
+!2 = !{i32 2, !"Debug Info Version", i32 3}
+!3 = !DIFile(filename: "abi.c", directory: "/")
+!4 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!5 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+
+; struct pair { int x; int y; }
+!7 = !DICompositeType(tag: DW_TAG_structure_type, name: "pair", file: !3, size: 64, elements: !8)
+!8 = !{!9, !10}
+!9 = !DIDerivedType(tag: DW_TAG_member, name: "x", scope: !7, file: !3, baseType: !4, size: 32)
+!10 = !DIDerivedType(tag: DW_TAG_member, name: "y", scope: !7, file: !3, baseType: !4, size: 32, offset: 32)
+
+; struct big { long a, b, c, d, e; }
+!11 = !DICompositeType(tag: DW_TAG_structure_type, name: "big", file: !3, size: 320, elements: !12)
+!12 = !{!41, !42, !43, !44, !45}
+
+!13 = distinct !DISubprogram(name: "use_pair", scope: !3, file: !3, line: 1, type: !14, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !16)
+!14 = !DISubroutineType(types: !15)
+!15 = !{!4, !7, !4}
+!16 = !{!17, !18}
+!17 = !DILocalVariable(name: "p", arg: 1, scope: !13, file: !3, line: 1, type: !7)
+!18 = !DILocalVariable(name: "z", arg: 2, scope: !13, file: !3, line: 1, type: !4)
+!19 = !DILocation(line: 1, column: 1, scope: !13)
+
+!20 = distinct !DISubprogram(name: "use_big", scope: !3, file: !3, line: 6, type: !21, scopeLine: 6, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !23)
+!21 = !DISubroutineType(types: !22)
+!22 = !{null, !11}
+!23 = !{!24}
+!24 = !DILocalVariable(name: "b", arg: 1, scope: !20, file: !3, line: 6, type: !11)
+!25 = !DILocation(line: 6, column: 1, scope: !20)
+
+; struct trio { int a; int b; long c; }
+!29 = !DICompositeType(tag: DW_TAG_structure_type, name: "trio", file: !3, size: 128, elements: !39)
+!30 = distinct !DISubprogram(name: "use_trio", scope: !3, file: !3, line: 11, type: !31, scopeLine: 11, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !33)
+!31 = !DISubroutineType(types: !32)
+!32 = !{!4, !29}
+!33 = !{!34}
+!34 = !DILocalVariable(name: "t", arg: 1, scope: !30, file: !3, line: 11, type: !29)
+!35 = !DILocation(line: 11, column: 1, scope: !30)
+!39 = !{!36, !37, !38}
+!36 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !29, file: !3, baseType: !4, size: 32)
+!37 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !29, file: !3, baseType: !4, size: 32, offset: 32)
+!38 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !29, file: !3, baseType: !5, size: 64, offset: 64)
+!41 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !11, file: !3, baseType: !5, size: 64)
+!42 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !11, file: !3, baseType: !5, size: 64, offset: 64)
+!43 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !11, file: !3, baseType: !5, size: 64, offset: 128)
+!44 = !DIDerivedType(tag: DW_TAG_member, name: "d", scope: !11, file: !3, baseType: !5, size: 64, offset: 192)
+!45 = !DIDerivedType(tag: DW_TAG_member, name: "e", scope: !11, file: !3, baseType: !5, size: 64, offset: 256)
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..3e5c8a87a4d95
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-dominance.ll
@@ -0,0 +1,67 @@
+; A debug record may name a value that the trace call cannot use: debug records
+; are exempt from SSA dominance, so the value may be defined after the call, and
+; a parameter that interprocedural optimization proved dead is described as
+; poison. Neither can be reported, and using the first would produce IR that
+; fails the verifier with "Instruction does not dominate all uses". Such
+; parameters are reported with size 0.
+;
+; 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=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"
+
+; The only location of `a` is %later, defined in a block that the entry block
+; branches to. `b` is a plain pointer argument and is reported normally.
+define void @location_does_not_dominate(ptr %a, ptr %b) !dbg !6 {
+entry:
+ #dbg_value(ptr %later, !10, !DIExpression(), !12)
+ #dbg_value(ptr %b, !11, !DIExpression(), !12)
+ br label %bb, !dbg !12
+bb:
+ %later = getelementptr i8, ptr %a, i64 128, !dbg !12
+ ret void, !dbg !12
+}
+; CHECK-LABEL: define void @location_does_not_dominate(
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @location_does_not_dominate to i64), i32 0, i32 0, i64 0, ptr null, i32 0)
+; CHECK: %[[B:[0-9]+]] = ptrtoint ptr %b to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @location_does_not_dominate to i64), i32 1, i32 8, i64 %[[B]], ptr null, i32 0)
+
+; `a` is dead and described as poison; `b` is live.
+define void @poison_location(ptr %b) !dbg !13 {
+entry:
+ #dbg_value(ptr poison, !15, !DIExpression(), !17)
+ #dbg_value(ptr %b, !16, !DIExpression(), !17)
+ ret void
+}
+; CHECK-LABEL: define void @poison_location(
+; CHECK-NOT: ptrtoint ptr poison
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @poison_location to i64), i32 0, i32 0, i64 0, ptr null, i32 0)
+; CHECK: %[[PB:[0-9]+]] = ptrtoint ptr %b to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @poison_location to i64), i32 1, i32 8, i64 %[[PB]], ptr null, i32 0)
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!1, !2}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !3, isOptimized: true, emissionKind: FullDebug)
+!1 = !{i32 2, !"Dwarf Version", i32 5}
+!2 = !{i32 2, !"Debug Info Version", i32 3}
+!3 = !DIFile(filename: "dominance.c", directory: "/")
+!4 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!5 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !4, size: 64)
+
+!6 = distinct !DISubprogram(name: "location_does_not_dominate", scope: !3, file: !3, line: 1, type: !7, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !9)
+!7 = !DISubroutineType(types: !8)
+!8 = !{null, !5, !5}
+!9 = !{!10, !11}
+!10 = !DILocalVariable(name: "a", arg: 1, scope: !6, file: !3, line: 1, type: !5)
+!11 = !DILocalVariable(name: "b", arg: 2, scope: !6, file: !3, line: 1, type: !5)
+!12 = !DILocation(line: 1, column: 1, scope: !6)
+
+!13 = distinct !DISubprogram(name: "poison_location", scope: !3, file: !3, line: 6, type: !7, scopeLine: 6, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !14)
+!14 = !{!15, !16}
+!15 = !DILocalVariable(name: "a", arg: 1, scope: !13, file: !3, line: 6, type: !5)
+!16 = !DILocalVariable(name: "b", arg: 2, scope: !13, file: !3, line: 6, type: !5)
+!17 = !DILocation(line: 6, column: 1, scope: !13)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-inlined.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-inlined.ll
new file mode 100644
index 0000000000000..f1dc642b0f79a
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-inlined.ll
@@ -0,0 +1,52 @@
+; An inlined callee's parameters are numbered just like the caller's, and the
+; records describing them may name the caller's own arguments. Only records
+; belonging to the function being instrumented may drive trace-args, otherwise
+; an inlined callee's first parameter would be reported as the caller's.
+;
+; 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"
+
+; void caller(long size) calling an inlined callee(long n): the inlined
+; parameter `n` is also arg 1 and is described by a record naming %size, but it
+; belongs to @callee, so only @caller's own record for `size` is used.
+define void @caller(i64 %size, ptr %p) !dbg !6 {
+entry:
+ #dbg_value(i64 %size, !14, !DIExpression(), !16)
+ #dbg_value(ptr %p, !15, !DIExpression(), !16)
+ #dbg_value(i64 %size, !12, !DIExpression(), !17)
+ ret void
+}
+; Both of @caller's parameters are reported from their own records. Nothing is
+; reported for the inlined `n`.
+; CHECK-LABEL: define void @caller(
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @caller to i64), i32 0, i32 8, i64 %size, ptr null, i32 0)
+; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @caller to i64), i32 1, i32 8, i64 %[[P]], ptr null, i32 0)
+; CHECK-NOT: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @caller to i64), i32 2
+
+declare void @callee(i64)
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!1, !2}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !3, isOptimized: true, emissionKind: FullDebug)
+!1 = !{i32 2, !"Dwarf Version", i32 5}
+!2 = !{i32 2, !"Debug Info Version", i32 3}
+!3 = !DIFile(filename: "inlined.c", directory: "/")
+!4 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+!5 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !4, size: 64)
+
+!6 = distinct !DISubprogram(name: "caller", scope: !3, file: !3, line: 10, type: !7, scopeLine: 10, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !13)
+!7 = !DISubroutineType(types: !8)
+!8 = !{null, !4, !5}
+!9 = distinct !DISubprogram(name: "callee", scope: !3, file: !3, line: 1, type: !10, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !11)
+!10 = !DISubroutineType(types: !{null, !4})
+!11 = !{!12}
+!12 = !DILocalVariable(name: "n", arg: 1, scope: !9, file: !3, line: 1, type: !4)
+!13 = !{!14, !15}
+!14 = !DILocalVariable(name: "size", arg: 1, scope: !6, file: !3, line: 10, type: !4)
+!15 = !DILocalVariable(name: "p", arg: 2, scope: !6, file: !3, line: 10, type: !5)
+!16 = !DILocation(line: 10, column: 1, scope: !6)
+!17 = !DILocation(line: 1, column: 1, scope: !9, inlinedAt: !16)
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..80f50e58aedb4
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-debug.ll
@@ -0,0 +1,35 @@
+; Argument tracing without debug info: there is nothing to map the IR arguments
+; back to, so they are reported positionally and without field offset tables.
+; This keeps trace-args usable on code built without -g; the ABI's view of the
+; arguments is then what a consumer sees.
+;
+; 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=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.S = type { i64, i64, i64 }
+
+define void @no_debug(ptr %p, i32 %x) {
+entry:
+ ret void
+}
+; CHECK-LABEL: define void @no_debug(
+; With no type to describe its pointee, a pointer is reported as its own value.
+; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @no_debug to i64), i32 0, i32 8, i64 %[[P]], ptr null, i32 0)
+; CHECK: %[[X:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @no_debug to i64), i32 1, i32 4, i64 %[[X]], ptr null, i32 0)
+
+; A struct-return pointer is ABI-inserted and has no source-level counterpart,
+; so it is skipped here as well and %x keeps index 0.
+define void @no_debug_sret(ptr sret(%struct.S) %0, i32 %x) {
+entry:
+ ret void
+}
+; CHECK-LABEL: define void @no_debug_sret(
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @no_debug_sret to i64), i32 0, i32 4, i64 %{{[0-9]+}}, ptr null, i32 0)
+; CHECK-NOT: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @no_debug_sret to i64), i32 1
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
new file mode 100644
index 0000000000000..2af4899bad284
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
@@ -0,0 +1,130 @@
+; Argument tracing: one __sanitizer_cov_trace_args call per source-level
+; parameter. A scalar is reported as its value, a pointer to a struct as the
+; address of that struct together with its field offset table, and hidden ABI
+; arguments are left out.
+;
+; 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.S = type { i32, i64 }
+
+; struct S { int a; long b; } is described by one {byte offset, byte size} pair
+; per field. The table is shared by every value of that type, so both functions
+; below referring to struct S must produce exactly one.
+; CHECK: @__sancov_offsets_ = private unnamed_addr constant [4 x i64] [i64 0, i64 4, i64 8, i64 8]
+; CHECK-NOT: = private unnamed_addr constant [{{.*}} x i64]
+
+; void two_params(struct S *s, int x)
+define void @two_params(ptr %s, i32 %x) !dbg !11 {
+entry:
+ #dbg_value(ptr %s, !15, !DIExpression(), !17)
+ #dbg_value(i32 %x, !16, !DIExpression(), !17)
+ ret void
+}
+; The pointer is reported as the address of the 16-byte struct it points at, so
+; that a consumer can read the two fields out of it. Nothing is spilled.
+; CHECK-LABEL: define void @two_params(
+; CHECK-NOT: alloca
+; CHECK: %[[ADDR:[0-9]+]] = ptrtoint ptr %s to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @two_params to i64), i32 0, i32 16, i64 %[[ADDR]], ptr @__sancov_offsets_, i32 2)
+; The scalar is reported as its own value, widened to 64 bits.
+; CHECK: %[[X:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @two_params to i64), i32 1, i32 4, i64 %[[X]], ptr null, i32 0)
+
+; struct S sret_and_scalar(int x), returning the struct through a hidden
+; struct-return pointer: that pointer is not a source-level parameter, so `x`
+; keeps source index 0 rather than becoming the IR argument index 1.
+define void @sret_and_scalar(ptr sret(%struct.S) %0, i32 %1) !dbg !18 {
+entry:
+ #dbg_value(i32 %1, !22, !DIExpression(), !23)
+ ret void
+}
+; CHECK-LABEL: define void @sret_and_scalar(
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @sret_and_scalar to i64), i32 0, i32 4, i64 %{{[0-9]+}}, ptr null, i32 0)
+; CHECK-NOT: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @sret_and_scalar to i64), i32 1
+
+; void dead_param(struct S *s, int x) with `x` optimized away: it has no debug
+; record left, but the subprogram still declares it, so it is reported with
+; size 0 to keep the parameter list complete.
+define void @dead_param(ptr %s) !dbg !24 {
+entry:
+ #dbg_value(ptr %s, !28, !DIExpression(), !29)
+ ret void
+}
+; CHECK-LABEL: define void @dead_param(
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @dead_param to i64), i32 0, i32 16, i64 %{{[0-9]+}}, ptr @__sancov_offsets_, i32 2)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @dead_param to i64), i32 1, i32 0, i64 0, ptr null, i32 0)
+
+; A pointer whose pointee has no field layout - void *, int * - is reported as
+; the pointer value itself rather than as an address to read through.
+define void @opaque_pointer(ptr %p) !dbg !30 {
+entry:
+ #dbg_value(ptr %p, !32, !DIExpression(), !33)
+ ret void
+}
+; CHECK-LABEL: define void @opaque_pointer(
+; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @opaque_pointer to i64), i32 0, i32 8, i64 %[[P]], ptr null, i32 0)
+
+; A floating-point parameter is reported as its bit pattern.
+define void @floating(double %d) !dbg !34 {
+entry:
+ #dbg_value(double %d, !36, !DIExpression(), !37)
+ ret void
+}
+; CHECK-LABEL: define void @floating(
+; CHECK: %[[BITS:[0-9]+]] = bitcast double %d to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @floating to i64), i32 0, i32 8, i64 %[[BITS]], ptr null, i32 0)
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!1, !2}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !3, isOptimized: true, emissionKind: FullDebug)
+!1 = !{i32 2, !"Dwarf Version", i32 5}
+!2 = !{i32 2, !"Debug Info Version", i32 3}
+!3 = !DIFile(filename: "args.c", directory: "/")
+!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: !3, size: 128, elements: !7)
+!7 = !{!8, !9}
+!8 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !6, file: !3, baseType: !4, size: 32)
+!9 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !6, file: !3, baseType: !5, size: 64, offset: 64)
+!10 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !6, size: 64)
+
+!11 = distinct !DISubprogram(name: "two_params", scope: !3, file: !3, line: 1, type: !12, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !14)
+!12 = !DISubroutineType(types: !13)
+!13 = !{null, !10, !4}
+!14 = !{!15, !16}
+!15 = !DILocalVariable(name: "s", arg: 1, scope: !11, file: !3, line: 1, type: !10)
+!16 = !DILocalVariable(name: "x", arg: 2, scope: !11, file: !3, line: 1, type: !4)
+!17 = !DILocation(line: 1, column: 1, scope: !11)
+
+!18 = distinct !DISubprogram(name: "sret_and_scalar", scope: !3, file: !3, line: 5, type: !19, scopeLine: 5, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !21)
+!19 = !DISubroutineType(types: !20)
+!20 = !{!6, !4}
+!21 = !{!22}
+!22 = !DILocalVariable(name: "x", arg: 1, scope: !18, file: !3, line: 5, type: !4)
+!23 = !DILocation(line: 5, column: 1, scope: !18)
+
+!24 = distinct !DISubprogram(name: "dead_param", scope: !3, file: !3, line: 9, type: !25, scopeLine: 9, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !26)
+!25 = !DISubroutineType(types: !13)
+!26 = !{!28}
+!28 = !DILocalVariable(name: "s", arg: 1, scope: !24, file: !3, line: 9, type: !10)
+!29 = !DILocation(line: 9, column: 1, scope: !24)
+
+!30 = distinct !DISubprogram(name: "opaque_pointer", scope: !3, file: !3, line: 13, type: !38, scopeLine: 13, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !31)
+!31 = !{!32}
+!32 = !DILocalVariable(name: "p", arg: 1, scope: !30, file: !3, line: 13, type: !39)
+!33 = !DILocation(line: 13, column: 1, scope: !30)
+
+!34 = distinct !DISubprogram(name: "floating", scope: !3, file: !3, line: 17, type: !40, scopeLine: 17, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !35)
+!35 = !{!36}
+!36 = !DILocalVariable(name: "d", arg: 1, scope: !34, file: !3, line: 17, type: !42)
+!37 = !DILocation(line: 17, column: 1, scope: !34)
+
+!38 = !DISubroutineType(types: !{null, !39})
+!39 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !4, size: 64)
+!40 = !DISubroutineType(types: !{null, !42})
+!42 = !DIBasicType(name: "double", size: 64, encoding: DW_ATE_float)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-no-spill.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-no-spill.ll
new file mode 100644
index 0000000000000..395efab36dd77
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-no-spill.ll
@@ -0,0 +1,67 @@
+; Neither mode ever creates memory to report a value from, on any target: a
+; value that is not already in memory is reported as the value itself. Nothing
+; escapes to the stack, so no frame is realigned and no base pointer is
+; reserved on the instrumentation's account.
+;
+; This is what makes the modes portable. A function whose inline assembly claims
+; the x86-64 base pointer (rbx) is the case that would otherwise fail to
+; compile - a redzoned, realigned spill slot needs the very register the
+; assembly uses - and it now needs no special handling: it is instrumented
+; exactly like the function below it.
+;
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-args -sanitizer-coverage-trace-ret -S | FileCheck %s
+
+; CHECK-NOT: alloca
+
+; An rdtsc-style clobber of rbx.
+define i32 @clobbers_rbx(i32 %x) #0 !dbg !6 {
+entry:
+ call void asm sideeffect "nop", "~{rbx},~{dirflag},~{fpsr},~{flags}"() #0, !dbg !9
+ ret i32 %x, !dbg !9
+}
+; CHECK-LABEL: define i32 @clobbers_rbx(
+; CHECK: %[[A:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @clobbers_rbx to i64), i32 0, i32 4, i64 %[[A]], ptr null, i32 0)
+; CHECK: %[[R:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @clobbers_rbx to i64), i32 4, i64 %[[R]], ptr null, i32 0)
+; CHECK: ret i32 %x
+
+; A cpuid-style "=b" output operand names the same register.
+define i32 @uses_b_constraint(i32 %x) #0 !dbg !10 {
+entry:
+ %0 = call i32 asm "cpuid", "=b,0"(i32 %x) #0, !dbg !11
+ ret i32 %0, !dbg !11
+}
+; CHECK-LABEL: define i32 @uses_b_constraint(
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @uses_b_constraint to i64), i32 0, i32 4, i64 %{{[0-9]+}}, ptr null, i32 0)
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @uses_b_constraint to i64), i32 4, i64 %{{[0-9]+}}, ptr null, i32 0)
+
+; The same function without such inline assembly, instrumented identically.
+define i32 @plain(i32 %x) #0 !dbg !12 {
+entry:
+ ret i32 %x, !dbg !13
+}
+; CHECK-LABEL: define i32 @plain(
+; CHECK: %[[PA:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @plain to i64), i32 0, i32 4, i64 %[[PA]], ptr null, i32 0)
+; CHECK: %[[PR:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @plain to i64), i32 4, i64 %[[PR]], ptr null, i32 0)
+
+attributes #0 = { nounwind sanitize_address }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!1, !2}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !3, isOptimized: true, emissionKind: FullDebug)
+!1 = !{i32 2, !"Dwarf Version", i32 5}
+!2 = !{i32 2, !"Debug Info Version", i32 3}
+!3 = !DIFile(filename: "no-spill.c", directory: "/")
+!4 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!5 = !DISubroutineType(types: !{!4, !4})
+!6 = distinct !DISubprogram(name: "clobbers_rbx", scope: !3, file: !3, line: 1, type: !5, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !20)
+!9 = !DILocation(line: 1, column: 1, scope: !6)
+!10 = distinct !DISubprogram(name: "uses_b_constraint", scope: !3, file: !3, line: 5, type: !5, scopeLine: 5, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !20)
+!11 = !DILocation(line: 5, column: 1, scope: !10)
+!12 = distinct !DISubprogram(name: "plain", scope: !3, file: !3, line: 9, type: !5, scopeLine: 9, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !20)
+!13 = !DILocation(line: 9, column: 1, scope: !12)
+!20 = !{}
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
new file mode 100644
index 0000000000000..3e1223457d727
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
@@ -0,0 +1,138 @@
+; Return value tracing: one __sanitizer_cov_trace_ret call before every return.
+; A scalar is reported as its value, a pointer to a struct as the address of
+; that struct together with its field offset table, and a struct returned
+; indirectly through the caller's buffer.
+;
+; 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.S = type { i32, i64 }
+%struct.Big = type { i64, i64, i64 }
+
+; CHECK-NOT: alloca
+
+; A pointer return whose pointee fields are known is reported as the address of
+; the object, so a consumer can read the fields out of it.
+define ptr @ret_struct_ptr(ptr %s) !dbg !13 {
+entry:
+ ret ptr %s
+}
+; CHECK-LABEL: define ptr @ret_struct_ptr(
+; CHECK: %[[ADDR:[0-9]+]] = ptrtoint ptr %s to i64
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_struct_ptr to i64), i32 16, i64 %[[ADDR]], ptr @__sancov_offsets_, i32 2)
+; CHECK: ret ptr %s
+
+; A scalar return is reported as its value, once per return.
+define i32 @ret_scalar(i1 %c, i32 %x) !dbg !16 {
+entry:
+ br i1 %c, label %yes, label %no
+yes:
+ ret i32 %x
+no:
+ ret i32 0
+}
+; CHECK-LABEL: define i32 @ret_scalar(
+; CHECK: %[[X:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_scalar to i64), i32 4, i64 %[[X]], ptr null, i32 0)
+; CHECK: ret i32 %x
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_scalar to i64), i32 4, i64 0, ptr null, i32 0)
+; CHECK: ret i32 0
+
+; A struct returned by value may be lowered to an indirect return: the IR
+; function returns void and writes the result through a hidden struct-return
+; pointer. That buffer is reported as the return value, with the size and the
+; fields of the source struct, so the return is not dropped.
+define void @ret_sret(ptr sret(%struct.Big) %0) !dbg !19 {
+entry:
+ ret void
+}
+; CHECK-LABEL: define void @ret_sret(
+; CHECK: %[[BUF:[0-9]+]] = ptrtoint ptr %0 to i64
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_sret to i64), i32 24, i64 %[[BUF]], ptr @__sancov_offsets_.1, i32 3)
+; CHECK: ret void
+
+; A struct small enough to come back in registers has no address, so it is
+; reported as one call per register, the same way the argument side reports a
+; struct the ABI split across registers.
+define { i64, i64 } @ret_in_registers(i64 %a, i64 %b) !dbg !30 {
+entry:
+ %0 = insertvalue { i64, i64 } poison, i64 %a, 0
+ %1 = insertvalue { i64, i64 } %0, i64 %b, 1
+ ret { i64, i64 } %1
+}
+; CHECK-LABEL: define { i64, i64 } @ret_in_registers(
+; CHECK: %[[LO:[0-9]+]] = extractvalue { i64, i64 } %1, 0
+; CHECK: %[[HI:[0-9]+]] = extractvalue { i64, i64 } %1, 1
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_in_registers to i64), i32 8, i64 %[[LO]], ptr null, i32 0)
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_in_registers to i64), i32 8, i64 %[[HI]], ptr null, i32 0)
+
+; A void return has nothing to report.
+define void @ret_void() !dbg !22 {
+entry:
+ ret void
+}
+; CHECK-LABEL: define void @ret_void(
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_void to i64), i32 0, i64 0, ptr null, i32 0)
+; CHECK: ret void
+
+; A musttail call has to stay adjacent to the return that forwards it, so there
+; is nowhere to put the call and the return is left alone.
+declare i32 @tail_callee(i32)
+define i32 @ret_musttail(i32 %x) !dbg !24 {
+entry:
+ %r = musttail call i32 @tail_callee(i32 %x)
+ ret i32 %r
+}
+; CHECK-LABEL: define i32 @ret_musttail(
+; CHECK-NOT: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_musttail to i64)
+; CHECK: ret i32 %r
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!1, !2}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !3, isOptimized: true, emissionKind: FullDebug)
+!1 = !{i32 2, !"Dwarf Version", i32 5}
+!2 = !{i32 2, !"Debug Info Version", i32 3}
+!3 = !DIFile(filename: "ret.c", directory: "/")
+!4 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!5 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+
+; struct S { int a; long b; }
+!6 = !DICompositeType(tag: DW_TAG_structure_type, name: "S", file: !3, size: 128, elements: !7)
+!7 = !{!8, !9}
+!8 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !6, file: !3, baseType: !4, size: 32)
+!9 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !6, file: !3, baseType: !5, size: 64, offset: 64)
+!10 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !6, size: 64)
+
+; struct Big { long a; long b; long c; }
+!11 = !DICompositeType(tag: DW_TAG_structure_type, name: "Big", file: !3, size: 192, elements: !12)
+!12 = !{!27, !28, !29}
+
+!13 = distinct !DISubprogram(name: "ret_struct_ptr", scope: !3, file: !3, line: 1, type: !14, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !31)
+!14 = !DISubroutineType(types: !15)
+!15 = !{!10, !10}
+
+!16 = distinct !DISubprogram(name: "ret_scalar", scope: !3, file: !3, line: 5, type: !17, scopeLine: 5, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !31)
+!17 = !DISubroutineType(types: !18)
+!18 = !{!4, !4, !4}
+
+!19 = distinct !DISubprogram(name: "ret_sret", scope: !3, file: !3, line: 9, type: !20, scopeLine: 9, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !31)
+!20 = !DISubroutineType(types: !21)
+!21 = !{!11}
+
+!22 = distinct !DISubprogram(name: "ret_void", scope: !3, file: !3, line: 13, type: !23, scopeLine: 13, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !31)
+!23 = !DISubroutineType(types: !{null})
+
+!24 = distinct !DISubprogram(name: "ret_musttail", scope: !3, file: !3, line: 17, type: !25, scopeLine: 17, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !31)
+!25 = !DISubroutineType(types: !26)
+!26 = !{!4, !4}
+
+!27 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !11, file: !3, baseType: !5, size: 64)
+!28 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !11, file: !3, baseType: !5, size: 64, offset: 64)
+!29 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !11, file: !3, baseType: !5, size: 64, offset: 128)
+
+!30 = distinct !DISubprogram(name: "ret_in_registers", scope: !3, file: !3, line: 21, type: !32, scopeLine: 21, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !31)
+!31 = !{}
+!32 = !DISubroutineType(types: !{!6, !5, !5})
>From 0c231e7232d28b15e461058a918dd9a4ee1bb84a Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Sun, 27 Sep 2026 21:14:06 +0200
Subject: [PATCH 2/4] [Clang][SanitizerCoverage] Add
-fsanitize-coverage=trace-args,trace-ret
Expose the trace-args and trace-ret SanitizerCoverage modes added in the
previous patch to the driver and the frontend:
- CodeGenOptions.def: SanitizeCoverageTraceArgs/TraceRet, and
hasSanitizeCoverage() accounting for them, so either flag on its own
enables the pass.
- Options.td: the cc1 flags -fsanitize-coverage-trace-args and
-fsanitize-coverage-trace-ret.
- SanitizerArgs.cpp: accept trace-args/trace-ret in
-fsanitize-coverage=, count both as instrumentation types so that they
satisfy the "requires an instrumentation type" check, forward the cc1
flags, and let either imply edge coverage when used alone.
- BackendUtil.cpp: forward both into SanitizerCoverageOptions.
Tests cover the driver accepting the new -fsanitize-coverage= values and
forwarding the cc1 flags, and the CodeGen pipeline emitting each callback
only for the flag that asks for it, with a struct pointer parameter
reported as the address of its pointee and its field offset table.
SanitizerCoverage.md documents both flags in a later patch in this
series, once the compiler-rt side that the documentation refers to is in
place.
Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
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 | 38 +++++++++++++++++++
clang/test/Driver/fsanitize-coverage.c | 13 +++++++
7 files changed, 78 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 2a04538677005..574f7a6bf8c25 100644
--- a/clang/include/clang/Basic/CodeGenOptions.def
+++ b/clang/include/clang/Basic/CodeGenOptions.def
@@ -328,6 +328,8 @@ CODEGENOPT(SanitizeCoverageStackDepth, 1, 0, Benign) ///< Enable max stack depth
VALUE_CODEGENOPT(SanitizeCoverageStackDepthCallbackMin , 32, 0, Benign) ///< Enable stack depth tracing callbacks.
CODEGENOPT(SanitizeCoverageTraceLoads, 1, 0, Benign) ///< Enable tracing of loads.
CODEGENOPT(SanitizeCoverageTraceStores, 1, 0, Benign) ///< Enable tracing of stores.
+CODEGENOPT(SanitizeCoverageTraceArgs, 1, 0, Benign) ///< Enable tracing of function args.
+CODEGENOPT(SanitizeCoverageTraceRet, 1, 0, Benign) ///< Enable tracing of return values.
CODEGENOPT(SanitizeBinaryMetadataCovered, 1, 0, Benign) ///< Emit PCs for covered functions.
CODEGENOPT(SanitizeBinaryMetadataAtomics, 1, 0, Benign) ///< Emit PCs for atomic operations.
CODEGENOPT(SanitizeBinaryMetadataUAR, 1, 0, Benign) ///< Emit PCs for start of functions
diff --git a/clang/include/clang/Basic/CodeGenOptions.h b/clang/include/clang/Basic/CodeGenOptions.h
index 17f367bc02607..b805e069a109b 100644
--- a/clang/include/clang/Basic/CodeGenOptions.h
+++ b/clang/include/clang/Basic/CodeGenOptions.h
@@ -686,7 +686,8 @@ class CodeGenOptions : public CodeGenOptionsBase {
bool hasSanitizeCoverage() const {
return SanitizeCoverageType || SanitizeCoverageIndirectCalls ||
SanitizeCoverageTraceCmp || SanitizeCoverageTraceLoads ||
- SanitizeCoverageTraceStores || SanitizeCoverageControlFlow;
+ SanitizeCoverageTraceStores || SanitizeCoverageControlFlow ||
+ SanitizeCoverageTraceArgs || SanitizeCoverageTraceRet;
}
// Check if any one of SanitizeBinaryMetadata* is enabled.
diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td
index e991dba448033..0a97f5c9d342a 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -8345,6 +8345,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 c09a8f7c0d679..f221a2a51d82a 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -259,6 +259,8 @@ getSancovOptsFromCGOpts(const CodeGenOptions &CGOpts) {
Opts.StackDepthCallbackMin = CGOpts.SanitizeCoverageStackDepthCallbackMin;
Opts.TraceLoads = CGOpts.SanitizeCoverageTraceLoads;
Opts.TraceStores = CGOpts.SanitizeCoverageTraceStores;
+ Opts.TraceArgs = CGOpts.SanitizeCoverageTraceArgs;
+ Opts.TraceRet = CGOpts.SanitizeCoverageTraceRet;
Opts.CollectControlFlow = CGOpts.SanitizeCoverageControlFlow;
return Opts;
}
diff --git a/clang/lib/Driver/SanitizerArgs.cpp b/clang/lib/Driver/SanitizerArgs.cpp
index 778cde8285aaf..1ea3975c8db03 100644
--- a/clang/lib/Driver/SanitizerArgs.cpp
+++ b/clang/lib/Driver/SanitizerArgs.cpp
@@ -109,6 +109,8 @@ enum CoverageFeature {
CoverageTraceStores = 1 << 17,
CoverageControlFlow = 1 << 18,
CoverageTracePCEntryExit = 1 << 19,
+ CoverageTraceArgs = 1 << 20,
+ CoverageTraceRet = 1 << 21,
};
enum BinaryMetadataFeature {
@@ -1100,6 +1102,7 @@ SanitizerArgs::SanitizerArgs(const ToolChain &TC,
int InstrumentationTypes = CoverageTracePC | CoverageTracePCEntryExit |
CoverageTracePCGuard | CoverageInline8bitCounters |
CoverageTraceLoads | CoverageTraceStores |
+ CoverageTraceArgs | CoverageTraceRet |
CoverageInlineBoolFlag | CoverageControlFlow;
if ((CoverageFeatures & InsertionPointTypes) &&
!(CoverageFeatures & InstrumentationTypes) && DiagnoseErrors) {
@@ -1111,9 +1114,10 @@ SanitizerArgs::SanitizerArgs(const ToolChain &TC,
// trace-pc w/o func/bb/edge implies edge.
if (!(CoverageFeatures & InsertionPointTypes)) {
- if (CoverageFeatures & (CoverageTracePC | CoverageTracePCEntryExit |
- CoverageTracePCGuard | CoverageInline8bitCounters |
- CoverageInlineBoolFlag | CoverageControlFlow))
+ if (CoverageFeatures &
+ (CoverageTracePC | CoverageTracePCEntryExit | CoverageTracePCGuard |
+ CoverageInline8bitCounters | CoverageInlineBoolFlag |
+ CoverageControlFlow | CoverageTraceArgs | CoverageTraceRet))
CoverageFeatures |= CoverageEdge;
if (CoverageFeatures & CoverageStackDepth)
@@ -1474,6 +1478,8 @@ void SanitizerArgs::addArgs(const ToolChain &TC, const llvm::opt::ArgList &Args,
std::make_pair(CoverageStackDepth, "-fsanitize-coverage-stack-depth"),
std::make_pair(CoverageTraceLoads, "-fsanitize-coverage-trace-loads"),
std::make_pair(CoverageTraceStores, "-fsanitize-coverage-trace-stores"),
+ std::make_pair(CoverageTraceArgs, "-fsanitize-coverage-trace-args"),
+ std::make_pair(CoverageTraceRet, "-fsanitize-coverage-trace-ret"),
std::make_pair(CoverageControlFlow, "-fsanitize-coverage-control-flow")};
for (auto F : CoverageFlags) {
if (CoverageFeatures & F.first)
@@ -1864,6 +1870,8 @@ int parseCoverageFeatures(const Driver &D, const llvm::opt::Arg *A,
.Case("stack-depth", CoverageStackDepth)
.Case("trace-loads", CoverageTraceLoads)
.Case("trace-stores", CoverageTraceStores)
+ .Case("trace-args", CoverageTraceArgs)
+ .Case("trace-ret", CoverageTraceRet)
.Case("control-flow", CoverageControlFlow)
.Default(0);
if (F == 0 && DiagnoseErrors)
diff --git a/clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c b/clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c
new file mode 100644
index 0000000000000..58482a40035d5
--- /dev/null
+++ b/clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c
@@ -0,0 +1,38 @@
+// Check that -fsanitize-coverage-trace-args and -fsanitize-coverage-trace-ret
+// reach the SanitizerCoverage pass and emit their callbacks.
+//
+// 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 -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; }
+
+// The struct pointer parameter is reported as the address of the 16-byte struct
+// it points at, with the field layout of that struct. Nothing is spilled.
+// ARGS: @__sancov_offsets_ = private unnamed_addr constant [4 x i64] [i64 0, i64 4, i64 8, i64 8]
+// ARGS-LABEL: define {{.*}} @takes_struct_ptr(
+// ARGS-NOT: alloca
+// ARGS: %[[ADDR:.*]] = ptrtoint ptr %f to i64
+// ARGS: call void @__sanitizer_cov_trace_args({{.*}}, i32 0, i32 16, i64 %[[ADDR]], ptr @__sancov_offsets_, i32 2)
+// ARGS-LABEL: define {{.*}} @returns_scalar(
+// ARGS: call void @__sanitizer_cov_trace_args(
+// ARGS-NOT: call void @__sanitizer_cov_trace_ret(
+
+// RET-LABEL: define {{.*}} @returns_scalar(
+// RET: call void @__sanitizer_cov_trace_ret(
+// 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 21e2c16bfb1b7..9a0c74bc3dda9 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 afc1d858b4c03ea1e9f6ef445091f8bdd98795f0 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Sun, 27 Sep 2026 21:18:19 +0200
Subject: [PATCH 3/4] [compiler-rt][SanitizerCoverage] Add a runtime for
trace-args/trace-ret
The trace-args and trace-ret callbacks so far only had a kernel consumer,
the KCOV dataflow subsystem. Make both flags usable from userspace
C/C++/Rust programs:
- sanitizer_common: weak, empty default definitions of
__sanitizer_cov_trace_args and __sanitizer_cov_trace_ret, so that a
program built with -fsanitize-coverage=trace-args,trace-ret links
without a runtime consuming the values, plus the internal-interface
declarations describing the two contracts and the weak-function
interface entries.
A value is folded directly when num_fields is zero, which is the common
case and touches no memory at all; only a non-zero num_fields makes the
runtime read fields out of the object `val` points at, and at most 32 of
them, to bound the work done on every call. A size of zero is the
value-unavailable case and is ignored. Returns are folded under a
location of their own, because they share a pc with the arguments of the
same function.
- Test: an end-to-end sanitizer_common test in which a user definition
overrides the weak defaults and checks the reported values. It builds at
-O2 because arguments are read in the entry block, before the -O0
prologue stores them to their stack slots.
Assisted-by: Kiro CLI (Claude Opus 5)
Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
.../sanitizer_coverage_interface.inc | 2 +
.../sanitizer_coverage_libcdep_new.cpp | 7 +++
.../sanitizer_interface_internal.h | 15 ++++++
.../sanitizer_coverage_trace_args.cpp | 50 +++++++++++++++++++
4 files changed, 74 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 9d36a40270d5a..ff7d971bfe51e 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 506659a58c45e..cdb1f389ea79f 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp
@@ -256,6 +256,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 c424ab1cecf94..45e37bfe88473 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h
@@ -115,6 +115,21 @@ 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 a function's parameters on entry
+// and its return values.
+// __sanitizer_cov_trace_args(u64 pc, u32 arg_idx, u32 size, u64 val,
+// u64 *offsets, u32 num_fields)
+// __sanitizer_cov_trace_ret (u64 pc, u32 size, u64 val,
+// u64 *offsets, u32 num_fields)
+// `val` is the reported value itself, or, when num_fields is non-zero, the
+// address of an object whose fields the num_fields {byte offset, byte size}
+// pairs in `offsets` describe. A size of zero means nothing was reported: a
+// parameter the optimizer removed, a void return, a value the instrumentation
+// could not report.
+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 0000000000000..d909e1d245d97
--- /dev/null
+++ b/compiler-rt/test/sanitizer_common/TestCases/sanitizer_coverage_trace_args.cpp
@@ -0,0 +1,50 @@
+// 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.
+//
+// Arguments are reported in the entry block, before the -O0 prologue stores
+// them to their stack slots, so the values are only meaningful from -O1 on.
+//
+// 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
+
+#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 size of zero means the instrumentation had
+// nothing to report, and a non-zero num_fields would mean `val` is an address
+// rather than a value.
+void __sanitizer_cov_trace_args(uint64_t pc, uint32_t arg_idx, uint32_t size,
+ uint64_t val, uint64_t *offsets,
+ uint32_t num_fields) {
+ if (size == sizeof(int) && !num_fields)
+ fprintf(stderr, "ARG idx=%u val=%d\n", arg_idx, (int)val);
+}
+
+void __sanitizer_cov_trace_ret(uint64_t pc, uint32_t size, uint64_t val,
+ uint64_t *offsets, uint32_t num_fields) {
+ if (size == sizeof(int) && !num_fields)
+ 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
>From 44b57bb9b1bcaa65b5590488ba34f3a2dfb1c6f5 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Sun, 27 Sep 2026 22:39:56 +0200
Subject: [PATCH 4/4] [compiler-rt][SanitizerCoverage] Add LibFuzzer support
for trace-args/trace-ret
Fold every reported value into the value-profile map through
TracePC::HandleDataflow. The fold binds the value to the site it was
observed at the function, the argument or return position, and the field
within a struct - so a value never seen at that site before becomes a new
value-profile feature under -use_value_profile: an input that reaches a
function with a new argument value counts as new coverage and is kept in
the corpus. This is the userspace equivalent of what kcov-dataflow records.
A value is folded directly when num_fields is zero, which is the common
case and touches no memory at all; only a non-zero num_fields makes the
runtime read fields out of the object `val` points at, and at most 32 of
them, to bound the work done on every call. A size of zero is the
value-unavailable case and is ignored. Returns are folded under a
location of their own, because they share a pc with the arguments of the
same function.
Assisted-by: Kiro CLI (Claude Opus 5)
Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
compiler-rt/lib/fuzzer/FuzzerTracePC.cpp | 66 ++++++++++++++++++++++++
compiler-rt/lib/fuzzer/FuzzerTracePC.h | 7 +++
2 files changed, 73 insertions(+)
diff --git a/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp b/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp
index d439ec2f0982d..3bd7e6b76040e 100644
--- a/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp
+++ b/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp
@@ -384,6 +384,48 @@ void TracePC::HandleCmp(uintptr_t PC, T Arg1, T Arg2) {
ValueProfileMap.AddValue(PC * 128 + 64 + AbsoluteDistance);
}
+// A struct with more fields than this contributes only its first ones, to bound
+// the work done on every call.
+static const uint32_t kMaxDataflowFields = 32;
+
+ATTRIBUTE_NO_SANITIZE_ALL
+void TracePC::HandleDataflow(uintptr_t PC, uint32_t Loc, uint32_t Size,
+ uint64_t Val, const uint64_t *Offsets,
+ uint32_t NumFields) {
+ // A parameter the optimizer removed, or one the instrumentation could not
+ // report, carries no value to observe.
+ if (!Size)
+ return;
+ // Bind the value to the site it was observed at - the function, the argument
+ // or return position, and the field - so that the same value seen elsewhere
+ // stays a distinct observation, then fold it into the value profile: a value
+ // never seen at that site before becomes a new feature. Only
+ // -use_value_profile consumes this.
+ auto Fold = [&](uint32_t Field, uint64_t V) {
+ ValueProfileMap.AddValueModPrime((PC * 3 + Loc) ^ (Field * 0x9E3779B1u) ^
+ V);
+ };
+
+ if (!NumFields || !Offsets) {
+ Fold(0, Val);
+ return;
+ }
+ // Val is the address of an object and Offsets holds {byte offset, byte size}
+ // pairs, one per field of it. Field numbers start at one to stay distinct
+ // from a whole value.
+ const uint8_t *Object =
+ reinterpret_cast<const uint8_t *>(static_cast<uintptr_t>(Val));
+ for (uint32_t I = 0; I < NumFields && I < kMaxDataflowFields; I++) {
+ uint64_t Bytes = Offsets[I * 2 + 1];
+ if (Bytes > sizeof(uint64_t))
+ Bytes = sizeof(uint64_t);
+ uint64_t Field = 0;
+ __builtin_memcpy(&Field, Object + Offsets[I * 2],
+ static_cast<size_t>(Bytes));
+ Fold(I + 1, Field);
+ }
+}
+
ATTRIBUTE_NO_SANITIZE_MEMORY
static size_t InternalStrnlen(const char *S, size_t MaxLen) {
size_t Len = 0;
@@ -478,6 +520,30 @@ void __sanitizer_cov_trace_cmp8(uint64_t Arg1, uint64_t Arg2) {
fuzzer::TPC.HandleCmp(PC, Arg1, Arg2);
}
+// Argument and return value tracing (-fsanitize-coverage=trace-args,trace-ret).
+// Unlike the cmp callbacks these receive the PC from the instrumentation - the
+// address of the instrumented function, not of the call site - and fold the
+// observed value into the value profile.
+ATTRIBUTE_INTERFACE
+ATTRIBUTE_NO_SANITIZE_ALL
+void __sanitizer_cov_trace_args(uint64_t PC, uint32_t ArgIdx, uint32_t Size,
+ uint64_t Val, const uint64_t *Offsets,
+ uint32_t NumFields) {
+ fuzzer::TPC.HandleDataflow(static_cast<uintptr_t>(PC), ArgIdx, Size, Val,
+ Offsets, NumFields);
+}
+
+ATTRIBUTE_INTERFACE
+ATTRIBUTE_NO_SANITIZE_ALL
+void __sanitizer_cov_trace_ret(uint64_t PC, uint32_t Size, uint64_t Val,
+ const uint64_t *Offsets, uint32_t NumFields) {
+ // Returns share the PC with the arguments of the same function, so they need
+ // a location of their own; no function has this many parameters.
+ const uint32_t kReturnLoc = 0xFFFF;
+ fuzzer::TPC.HandleDataflow(static_cast<uintptr_t>(PC), kReturnLoc, Size, Val,
+ Offsets, NumFields);
+}
+
ATTRIBUTE_INTERFACE
ATTRIBUTE_NO_SANITIZE_ALL
ATTRIBUTE_TARGET_POPCNT
diff --git a/compiler-rt/lib/fuzzer/FuzzerTracePC.h b/compiler-rt/lib/fuzzer/FuzzerTracePC.h
index af1f9d81e9509..e09b07992c164 100644
--- a/compiler-rt/lib/fuzzer/FuzzerTracePC.h
+++ b/compiler-rt/lib/fuzzer/FuzzerTracePC.h
@@ -73,6 +73,13 @@ class TracePC {
void HandlePCsInit(const uintptr_t *Start, const uintptr_t *Stop);
void HandleCallerCallee(uintptr_t Caller, uintptr_t Callee);
template <class T> void HandleCmp(uintptr_t PC, T Arg1, T Arg2);
+ // Fold an argument or return value observed at \p PC into the value profile
+ // (trace-args / trace-ret). Loc tells apart the arguments and the return of
+ // the same function. Val is the value itself, or, when NumFields is non-zero,
+ // the address of an object whose fields the NumFields {byte offset, byte
+ // size} pairs in Offsets describe.
+ void HandleDataflow(uintptr_t PC, uint32_t Loc, uint32_t Size, uint64_t Val,
+ const uint64_t *Offsets, uint32_t NumFields);
size_t GetTotalPCCoverage();
void SetUseCounters(bool UC) { UseCounters = UC; }
void SetUseValueProfileMask(uint32_t VPMask) { UseValueProfileMask = VPMask; }
More information about the llvm-commits
mailing list