[llvm] [sancov] Add -fsanitize-coverage=trace-args, trace-ret (PR #201410)
Yunseong Kim via llvm-commits
llvm-commits at lists.llvm.org
Sun Aug 23 06:31:11 PDT 2026
https://github.com/yskzalloc updated https://github.com/llvm/llvm-project/pull/201410
>From 4fd86f38d499b215dec9959b6688d3ff7884677d Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Sun, 23 Aug 2026 12:27:09 +0200
Subject: [PATCH] [SanitizerCoverage] Add trace-args and trace-ret
instrumentation modes
Add two new SanitizerCoverage modes, enabled via SanitizerCoverageOptions
(TraceArgs/TraceRet) and, for opt, the -sanitizer-coverage-trace-args /
-sanitizer-coverage-trace-ret flags:
__sanitizer_cov_trace_args(u64 pc, u32 arg_idx, u32 arg_size,
ptr val, ptr offsets, u32 num_fields)
__sanitizer_cov_trace_ret(u64 pc, u32 ret_size,
ptr val, ptr offsets, u32 num_fields)
trace-args inserts a callback at function entry for every source-level
argument; trace-ret inserts one before every return. Consumers (the
Linux kernel's KCOV dataflow subsystem, libFuzzer value profiling) use
them to capture argument/return values with automatic struct field
expansion. Both modes imply edge coverage when used alone.
With debug info, struct field layouts ([byte_offset, byte_size] pairs)
are extracted from DICompositeType and emitted as a constant global
prefixed with an FNV-1a hash of the type name; scalar values are
spilled to an alloca so the callback uniformly takes a pointer.
arg_idx is the source-level parameter index: IR values are mapped back
to source parameters via DILocalVariable::getArg(), which the frontend
assigns before ABI lowering, so the index is stable when the ABI
rewrites the argument list. Hidden sret/this pointers are not counted,
and a by-value struct split into several IR arguments is reassembled
from its DW_OP_LLVM_fragment pieces into one stack slot in source
layout. A struct return lowered to an indirect (sret) return is
reported through the caller-provided sret buffer rather than dropped.
If the DISubroutineType names more source parameters than the debug
records yield (argument optimized away), a null-pointer trace is
emitted so consumers still see the argument's existence.
Debug info is not required: without a DISubprogram the pass falls back
to tracing each IR argument (and the return value) as an opaque scalar
(offsets=null, num_fields=0), which keeps the modes usable for
optimized userspace and Rust builds; only the field breakdown is lost.
Robustness, from instrumenting a whole KASAN kernel with both modes:
- Trace calls are emitted through InstrumentationIRBuilder so they
carry a !dbg location; a plain builder tripped the verifier
("inlinable function call ... requires a !dbg location") under -g
once inlining/LTO ran.
- A #dbg_value may reference an instruction defined after the entry
block (debug records are exempt from SSA dominance); using it as the
trace pointer produced IR failing "Instruction does not dominate all
uses" and crashed RegisterCoalescer. Such values are skipped and the
parameter is traced as a null pointer.
- Argument fragments are de-duplicated by (value, bit-offset) so a
repeated debug record does not make a scalar look like a
multi-fragment struct.
- On x86-64, the escaping spill allocas get ASan redzones and force
frame realignment, which needs a base pointer (RBX); in functions
whose inline asm also clobbers RBX (CPUID "=b", RDTSC) the backend
rejects this ("Interference usage of base pointer/frame pointer").
functionInlineAsmUsesBasePointerX86() detects those functions and
skips the spill, passing a null value pointer instead.
Tests cover basic operation of both callbacks, ABI index remapping and
struct reassembly, sret return capture, !dbg presence, the no-debug
fallback, the dominance guard, and the base-pointer skip.
Clang driver support (-fsanitize-coverage=trace-args,trace-ret) and the
compiler-rt runtime land in follow-up patches.
Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
.../llvm/Transforms/Utils/Instrumentation.h | 2 +
.../Instrumentation/SanitizerCoverage.cpp | 503 +++++++++++++++++-
.../SanitizerCoverage/trace-args-abi.ll | 88 +++
.../SanitizerCoverage/trace-args-dominance.ll | 50 ++
.../SanitizerCoverage/trace-args-no-debug.ll | 21 +
.../SanitizerCoverage/trace-args.ll | 43 ++
.../trace-ret-basepointer.ll | 51 ++
.../SanitizerCoverage/trace-ret.ll | 83 +++
8 files changed, 840 insertions(+), 1 deletion(-)
create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll
create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args-dominance.ll
create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-debug.ll
create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-ret-basepointer.ll
create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
diff --git a/llvm/include/llvm/Transforms/Utils/Instrumentation.h b/llvm/include/llvm/Transforms/Utils/Instrumentation.h
index 95a985ba3f0c4..8a4324175b075 100644
--- a/llvm/include/llvm/Transforms/Utils/Instrumentation.h
+++ b/llvm/include/llvm/Transforms/Utils/Instrumentation.h
@@ -163,6 +163,8 @@ struct SanitizerCoverageOptions {
bool StackDepth = false;
bool TraceLoads = false;
bool TraceStores = false;
+ bool TraceArgs = false;
+ bool TraceRet = false;
bool CollectControlFlow = false;
bool GatedCallbacks = false;
int StackDepthCallbackMin = 0;
diff --git a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
index df9675a02824e..e55b13d2cf077 100644
--- a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
+++ b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
@@ -15,14 +15,18 @@
#include "llvm/ADT/SmallVector.h"
#include "llvm/Analysis/GlobalsModRef.h"
#include "llvm/Analysis/PostDominators.h"
+#include "llvm/BinaryFormat/Dwarf.h"
#include "llvm/IR/Constant.h"
#include "llvm/IR/Constants.h"
#include "llvm/IR/DataLayout.h"
+#include "llvm/IR/DebugInfoMetadata.h"
+#include "llvm/IR/DebugInfo.h"
#include "llvm/IR/Dominators.h"
#include "llvm/IR/EHPersonalities.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/GlobalVariable.h"
#include "llvm/IR/IRBuilder.h"
+#include "llvm/IR/InlineAsm.h"
#include "llvm/IR/IntrinsicInst.h"
#include "llvm/IR/Intrinsics.h"
#include "llvm/IR/LLVMContext.h"
@@ -46,6 +50,8 @@ const char SanCovTracePCIndirName[] = "__sanitizer_cov_trace_pc_indir";
const char SanCovTracePCName[] = "__sanitizer_cov_trace_pc";
const char SanCovTracePCEntryName[] = "__sanitizer_cov_trace_pc_entry";
const char SanCovTracePCExitName[] = "__sanitizer_cov_trace_pc_exit";
+const char SanCovTraceArgsName[] = "__sanitizer_cov_trace_args";
+const char SanCovTraceRetName[] = "__sanitizer_cov_trace_ret";
const char SanCovTraceCmp1[] = "__sanitizer_cov_trace_cmp1";
const char SanCovTraceCmp2[] = "__sanitizer_cov_trace_cmp2";
const char SanCovTraceCmp4[] = "__sanitizer_cov_trace_cmp4";
@@ -156,6 +162,15 @@ static cl::opt<bool> ClGEPTracing("sanitizer-coverage-trace-geps",
cl::desc("Tracing of GEP instructions"),
cl::Hidden);
+static cl::opt<bool>
+ ClTraceArgs("sanitizer-coverage-trace-args",
+ cl::desc("Dataflow tracing of function arguments"),
+ cl::Hidden);
+
+static cl::opt<bool>
+ ClTraceRet("sanitizer-coverage-trace-ret",
+ cl::desc("Dataflow tracing of return values"), cl::Hidden);
+
static cl::opt<bool>
ClPruneBlocks("sanitizer-coverage-prune-blocks",
cl::desc("Reduce the number of instrumented blocks"),
@@ -226,10 +241,13 @@ SanitizerCoverageOptions OverrideFromCL(SanitizerCoverageOptions Options) {
ClStackDepthCallbackMin.getValue());
Options.TraceLoads |= ClLoadTracing;
Options.TraceStores |= ClStoreTracing;
+ Options.TraceArgs |= ClTraceArgs;
+ Options.TraceRet |= ClTraceRet;
Options.GatedCallbacks |= ClGatedCallbacks;
if (!Options.TracePCGuard && !Options.TracePC && !Options.TracePCEntryExit &&
!Options.Inline8bitCounters && !Options.StackDepth &&
- !Options.InlineBoolFlag && !Options.TraceLoads && !Options.TraceStores)
+ !Options.InlineBoolFlag && !Options.TraceLoads && !Options.TraceStores &&
+ !Options.TraceArgs && !Options.TraceRet)
Options.TracePCGuard = true; // TracePCGuard is default.
Options.CollectControlFlow |= ClCollectCF;
return Options;
@@ -265,6 +283,8 @@ class ModuleSanitizerCoverage {
void InjectTraceForLoadsAndStores(Function &F, ArrayRef<LoadInst *> Loads,
ArrayRef<StoreInst *> Stores);
void InjectTraceForExits(Function &F);
+ void InjectTraceForArgs(Function &F);
+ void InjectTraceForRet(Function &F);
void InjectTraceForSwitch(Function &F,
ArrayRef<Instruction *> SwitchTraceTargets,
Value *&FunctionGateCmp);
@@ -298,6 +318,7 @@ class ModuleSanitizerCoverage {
FunctionCallee SanCovTracePCIndir;
FunctionCallee SanCovTracePC, SanCovTracePCGuard;
FunctionCallee SanCovTracePCEntry, SanCovTracePCExit;
+ FunctionCallee SanCovTraceArgsFunc, SanCovTraceRetFunc;
std::array<FunctionCallee, 4> SanCovTraceCmpFunction;
std::array<FunctionCallee, 4> SanCovTraceConstCmpFunction;
std::array<FunctionCallee, 5> SanCovLoadFunction;
@@ -542,6 +563,16 @@ bool ModuleSanitizerCoverage::instrumentModule() {
SanCovTracePCGuard =
M.getOrInsertFunction(SanCovTracePCGuardName, VoidTy, PtrTy);
+ // __sanitizer_cov_trace_args(i64 pc, i32 arg_idx, i32 arg_size, ptr arg, ptr
+ // offsets, i32 num_fields)
+ SanCovTraceArgsFunc =
+ M.getOrInsertFunction(SanCovTraceArgsName, VoidTy, Int64Ty, Int32Ty,
+ Int32Ty, PtrTy, PtrTy, Int32Ty);
+ // __sanitizer_cov_trace_ret(i64 pc, i32 ret_size, ptr ret_val, ptr offsets,
+ // i32 num_fields)
+ SanCovTraceRetFunc = M.getOrInsertFunction(
+ SanCovTraceRetName, VoidTy, Int64Ty, Int32Ty, PtrTy, PtrTy, Int32Ty);
+
SanCovStackDepthCallback =
M.getOrInsertFunction(SanCovStackDepthCallbackName, VoidTy);
@@ -767,6 +798,12 @@ void ModuleSanitizerCoverage::instrumentFunction(Function &F) {
if (Options.TracePCEntryExit)
InjectTraceForExits(F);
+
+ if (Options.TraceArgs)
+ InjectTraceForArgs(F);
+
+ if (Options.TraceRet)
+ InjectTraceForRet(F);
}
GlobalVariable *ModuleSanitizerCoverage::CreateFunctionLocalArrayInSection(
@@ -1266,3 +1303,467 @@ void ModuleSanitizerCoverage::createFunctionControlFlow(Function &F) {
ConstantArray::get(ArrayType::get(PtrTy, CFs.size()), CFs));
FunctionCFsArray->setConstant(true);
}
+
+// Helper: Given a DIType, resolve typedefs/qualifiers to the underlying type.
+static DIType *stripDITypedefs(DIType *Ty) {
+ while (Ty) {
+ if (auto *Derived = dyn_cast<DIDerivedType>(Ty)) {
+ unsigned Tag = Derived->getTag();
+ if (Tag == dwarf::DW_TAG_typedef || Tag == dwarf::DW_TAG_const_type ||
+ Tag == dwarf::DW_TAG_volatile_type ||
+ Tag == dwarf::DW_TAG_restrict_type) {
+ Ty = Derived->getBaseType();
+ continue;
+ }
+ // pointer type - stop
+ break;
+ }
+ break;
+ }
+ return Ty;
+}
+
+// Helper: If Ty is a pointer to a struct (DICompositeType) or a struct
+// directly, collect byte offsets of all scalar members. Returns the offsets
+// array global and num_fields.
+static std::pair<GlobalVariable *, unsigned>
+getStructFieldOffsets(DIType *Ty, Module &M, const DataLayout &DL) {
+ if (!Ty)
+ return {nullptr, 0};
+
+ Ty = stripDITypedefs(Ty);
+
+ DICompositeType *Composite = nullptr;
+
+ // Case 1: pointer to struct
+ if (auto *PtrTy = dyn_cast_or_null<DIDerivedType>(Ty)) {
+ if (PtrTy->getTag() == dwarf::DW_TAG_pointer_type) {
+ DIType *PointeeTy = stripDITypedefs(PtrTy->getBaseType());
+ Composite = dyn_cast_or_null<DICompositeType>(PointeeTy);
+ }
+ }
+ // Case 2: direct struct type (for reassembled by-value args)
+ if (!Composite)
+ Composite = dyn_cast_or_null<DICompositeType>(Ty);
+
+ if (!Composite || Composite->getTag() != dwarf::DW_TAG_structure_type)
+ return {nullptr, 0};
+
+ SmallVector<uint64_t, 16> Offsets;
+ for (auto *Element : Composite->getElements()) {
+ auto *Member = dyn_cast<DIDerivedType>(Element);
+ if (!Member || Member->getTag() != dwarf::DW_TAG_member)
+ continue;
+ uint64_t OffsetBits = Member->getOffsetInBits();
+ uint64_t SizeBits = Member->getSizeInBits();
+ if (SizeBits == 0)
+ continue;
+ // Record byte offset and size in bytes as pairs: [offset, size]
+ Offsets.push_back(OffsetBits / 8);
+ Offsets.push_back(SizeBits / 8);
+ }
+
+ if (Offsets.empty())
+ return {nullptr, 0};
+
+ // Enhance #4: Compute type name hash from struct name
+ uint64_t TypeHash = 0;
+ if (auto Name = Composite->getName(); !Name.empty()) {
+ // Simple FNV-1a hash of the struct name
+ TypeHash = 0xcbf29ce484222325ULL;
+ for (char C : Name) {
+ TypeHash ^= (uint64_t)(unsigned char)C;
+ TypeHash *= 0x100000001b3ULL;
+ }
+ }
+
+ // Layout: [type_hash, off0, sz0, off1, sz1, ...]
+ // We pass &array[1] as the offsets pointer, so kernel can read array[0] as
+ // hash
+ LLVMContext &C = M.getContext();
+ Type *I64Ty = Type::getInt64Ty(C);
+ SmallVector<Constant *, 16> OffsetConstants;
+ OffsetConstants.push_back(
+ ConstantInt::get(I64Ty, TypeHash)); // index 0 = hash
+ for (uint64_t V : Offsets)
+ OffsetConstants.push_back(ConstantInt::get(I64Ty, V));
+
+ ArrayType *ArrTy = ArrayType::get(I64Ty, OffsetConstants.size());
+ auto *GV = new GlobalVariable(M, ArrTy, true, GlobalVariable::PrivateLinkage,
+ ConstantArray::get(ArrTy, OffsetConstants),
+ "__sancov_offsets_");
+ GV->setUnnamedAddr(GlobalValue::UnnamedAddr::Global);
+ return {GV, (unsigned)(Offsets.size() / 2)};
+}
+
+// x86 uses RBX as the frame base pointer for a realigned stack. A function whose
+// inline asm reads/writes/clobbers RBX cannot also use it as the base pointer -- the
+// backend rejects it ("Interference usage of base pointer/frame pointer"). The
+// trace-args/trace-ret spill allocas below escape (their address is passed to the
+// trace call), so ASAN redzones them, forcing 32-byte frame realignment => a base
+// pointer. In a function that already ties up RBX in inline asm (e.g. CPUID's "=b",
+// RDTSC's "~{rbx}") that is a hard error. Detect it and skip the escaping spill for
+// such functions (the arg/ret is traced as a null pointer instead). The base-pointer
+// register is spelled {rbx}/{ebx}/{bx} (and byte views {bl}/{bh}) in constraint codes.
+static bool functionInlineAsmUsesBasePointerX86(const Function &F) {
+ for (const BasicBlock &BB : F)
+ for (const Instruction &I : BB) {
+ const auto *CB = dyn_cast<CallBase>(&I);
+ if (!CB || !CB->isInlineAsm())
+ continue;
+ const auto *IA = dyn_cast<InlineAsm>(CB->getCalledOperand());
+ if (!IA)
+ continue;
+ for (const InlineAsm::ConstraintInfo &CI : IA->ParseConstraints())
+ for (StringRef Code : CI.Codes)
+ if (Code == "{rbx}" || Code == "{ebx}" || Code == "{bx}" ||
+ Code == "{bl}" || Code == "{bh}")
+ return true;
+ }
+ return false;
+}
+
+void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
+ // SP may be null: a function compiled WITHOUT debug info (-g) carries no
+ // #dbg_value records, so the source-argument map below stays empty and the
+ // SrcArgs.empty() fallback traces each IR argument directly (no source-level
+ // struct-field offsets). This lets trace-args work on userspace / optimized
+ // code built without -g, not just debug kernels.
+ DISubprogram *SP = F.getSubprogram();
+ // Only guards x86; on other targets there is no RBX base-pointer interference.
+ const bool SkipSpill = TargetTriple.getArch() == Triple::x86_64 &&
+ functionInlineAsmUsesBasePointerX86(F);
+
+ BasicBlock &EntryBB = F.getEntryBlock();
+ Instruction *InsertPt = &*EntryBB.getFirstInsertionPt();
+ InstrumentationIRBuilder IRB(InsertPt);
+
+ // Get PC as the function address cast to i64
+ Value *PC = IRB.CreatePtrToInt(&F, Int64Ty);
+
+ // Build source-level argument map from debug variable records.
+ // DILocalVariable::getArg() gives the 1-based source parameter number,
+ // which is ABI-stable: unaffected by sret insertion or struct decomposition.
+ //
+ // We scan ALL debug records in the entry block because ABI lowering may
+ // decompose one source argument into derived values (e.g., trunc of a
+ // coerced i64 into two i32 fragments). findDbgValues on the Argument
+ // alone would miss those derived values.
+ struct SourceArg {
+ DILocalVariable *Var = nullptr;
+ DIType *Ty = nullptr;
+ SmallVector<std::pair<Value *, uint64_t>, 2> Fragments; // {val, bit_off}
+ };
+ DenseMap<unsigned, SourceArg> SrcArgs;
+
+ // Record a fragment for a source parameter, ignoring exact (value, offset)
+ // duplicates. The same #dbg_value can be reached more than once (found via
+ // the argument in pass 1 and again while walking the block in pass 2, or
+ // simply duplicated by an earlier pass); without this a single scalar arg
+ // with a repeated record would look like a multi-fragment struct and be
+ // needlessly reassembled.
+ auto addFragment = [](SourceArg &SA, Value *V, uint64_t BitOff) {
+ for (const auto &Existing : SA.Fragments)
+ if (Existing.first == V && Existing.second == BitOff)
+ return;
+ SA.Fragments.push_back({V, BitOff});
+ };
+
+ // Pass 1: direct argument debug records (batched scan)
+ SmallVector<DbgVariableRecord *, 16> AllDVRs;
+ for (auto &Arg : F.args()) {
+ AllDVRs.clear();
+ findDbgValues(&Arg, AllDVRs);
+ for (auto *DVR : AllDVRs) {
+ DILocalVariable *Var = DVR->getVariable();
+ if (!Var || !Var->getArg())
+ continue;
+ unsigned SrcIdx = Var->getArg();
+ auto &SA = SrcArgs[SrcIdx];
+ SA.Var = Var;
+ SA.Ty = Var->getType();
+ uint64_t FragBitOff = 0;
+ if (auto Frag = DVR->getExpression()->getFragmentInfo())
+ FragBitOff = Frag->OffsetInBits;
+ addFragment(SA, &Arg, FragBitOff);
+ }
+ }
+
+ // Pass 2: scan entry block for debug records on derived values
+ // (handles struct coercion where debug info points at trunc/extract, not arg)
+ for (auto &I : EntryBB) {
+ for (auto &DVR : I.getDbgRecordRange()) {
+ auto *DVar = dyn_cast<DbgVariableRecord>(&DVR);
+ if (!DVar)
+ continue;
+ DILocalVariable *Var = DVar->getVariable();
+ if (!Var || !Var->getArg())
+ continue;
+ unsigned SrcIdx = Var->getArg();
+ // Skip if pass 1 already found a direct argument reference for this param
+ auto It = SrcArgs.find(SrcIdx);
+ if (It != SrcArgs.end() && !It->second.Fragments.empty() &&
+ isa<Argument>(It->second.Fragments[0].first))
+ continue;
+ Value *V = DVar->getValue();
+ if (!V || isa<Argument>(V))
+ continue; // Direct args handled in pass 1
+ // A #dbg_value may point at a value that does NOT dominate the entry-block
+ // terminator where the trace call is inserted (debug records are exempt from
+ // SSA dominance). Using such a value as ArgPtr emits IR that fails the verifier
+ // ("Instruction does not dominate all uses") and, with -disable-llvm-verifier
+ // (kernel builds), reaches codegen and crashes RegisterCoalescer::reMaterializeDef.
+ // The insertion point is the entry terminator, so a value defined in the entry
+ // block dominates it; anything else (a later-block instruction, e.g. a field
+ // getelementptr) does not -> skip it and let the dead-arg fallback emit a null
+ // trace for this param.
+ if (auto *VI = dyn_cast<Instruction>(V))
+ if (VI->getParent() != &EntryBB)
+ continue;
+ auto &SA = SrcArgs[SrcIdx];
+ SA.Var = Var;
+ SA.Ty = Var->getType();
+ uint64_t FragBitOff = 0;
+ if (auto Frag = DVar->getExpression()->getFragmentInfo())
+ FragBitOff = Frag->OffsetInBits;
+ addFragment(SA, V, FragBitOff);
+ }
+ }
+
+ // Fallback: if no debug records found (compiled without -g or stripped),
+ // use the original TypeArray-based indexing.
+ if (SrcArgs.empty()) {
+ unsigned ArgIdx = 0;
+ for (auto &Arg : F.args()) {
+ // Skip ABI-inserted hidden args that don't correspond to source params
+ if (Arg.hasStructRetAttr())
+ continue;
+ DIType *ArgDIType = nullptr;
+ if (SP && SP->getType()) {
+ auto TypeArray = SP->getType()->getTypeArray();
+ if (ArgIdx + 1 < TypeArray.size())
+ ArgDIType = TypeArray[ArgIdx + 1];
+ }
+ auto [OffsetsGV, NumFields] = getStructFieldOffsets(ArgDIType, M, *DL);
+ Value *ArgPtr;
+ bool Spilled = false;
+ if (Arg.getType()->isPointerTy()) {
+ ArgPtr = &Arg;
+ } else if (SkipSpill) {
+ ArgPtr = Constant::getNullValue(PtrTy); // base-pointer asm: no escaping spill
+ } else {
+ AllocaInst *Alloca = IRB.CreateAlloca(Arg.getType());
+ IRB.CreateStore(&Arg, Alloca);
+ ArgPtr = Alloca;
+ Spilled = true;
+ }
+ unsigned ArgByteSize =
+ Arg.getType()->isPointerTy() ? DL->getPointerSize()
+ : Spilled ? DL->getTypeStoreSize(Arg.getType())
+ : 0;
+ Value *OffsetsPtr = Constant::getNullValue(PtrTy);
+ if (OffsetsGV) {
+ Value *Indices[] = {ConstantInt::get(Int64Ty, 0),
+ ConstantInt::get(Int64Ty, 1)};
+ OffsetsPtr = IRB.CreateInBoundsGEP(OffsetsGV->getValueType(),
+ OffsetsGV, Indices);
+ }
+ IRB.CreateCall(SanCovTraceArgsFunc,
+ {PC, ConstantInt::get(Int32Ty, ArgIdx),
+ ConstantInt::get(Int32Ty, ArgByteSize), ArgPtr,
+ OffsetsPtr, ConstantInt::get(Int32Ty, NumFields)});
+ ArgIdx++;
+ }
+ return;
+ }
+
+ // Emit one trace call per source-level argument, sorted by source position.
+ // Place trace calls before the entry block terminator so all values dominate.
+ SmallVector<unsigned, 8> SortedKeys;
+ for (auto &[K, _] : SrcArgs)
+ SortedKeys.push_back(K);
+ llvm::sort(SortedKeys);
+
+ // Use InstrumentationIRBuilder so the inserted calls inherit a synthetic
+ // !dbg location. In a function that carries debug info (which this pass
+ // requires), a plain IRBuilder would emit callee-bearing calls with no
+ // location and trip the verifier under -g and LTO.
+ InstrumentationIRBuilder TraceIRB(EntryBB.getTerminator());
+
+ for (unsigned SrcIdx : SortedKeys) {
+ auto &SA = SrcArgs[SrcIdx];
+ auto [OffsetsGV, NumFields] = getStructFieldOffsets(SA.Ty, M, *DL);
+
+ Value *ArgPtr;
+ unsigned ArgByteSize;
+
+ if (SA.Fragments.size() == 1) {
+ // Single IR arg for this source param (common case: pointers, scalars)
+ Value *V = SA.Fragments[0].first;
+ if (V->getType()->isPointerTy()) {
+ ArgPtr = V;
+ ArgByteSize = DL->getPointerSize();
+ } else if (SkipSpill) {
+ ArgPtr = Constant::getNullValue(PtrTy); // base-pointer asm: no escaping spill
+ ArgByteSize = 0;
+ } else {
+ AllocaInst *Alloca = IRB.CreateAlloca(V->getType());
+ TraceIRB.CreateStore(V, Alloca);
+ ArgPtr = Alloca;
+ ArgByteSize = DL->getTypeStoreSize(V->getType());
+ }
+ } else if (SkipSpill) {
+ ArgPtr = Constant::getNullValue(PtrTy); // base-pointer asm: no escaping spill
+ ArgByteSize = 0;
+ } else {
+ // Multiple IR args for one source param (ABI struct decomposition).
+ // Reassemble fragments into a stack slot matching the source layout.
+ unsigned TotalBits = 0;
+ for (auto &[V, BitOff] : SA.Fragments) {
+ unsigned End = BitOff + DL->getTypeSizeInBits(V->getType());
+ if (End > TotalBits)
+ TotalBits = End;
+ }
+ unsigned TotalBytes = (TotalBits + 7) / 8;
+ AllocaInst *Slot =
+ IRB.CreateAlloca(ArrayType::get(IRB.getInt8Ty(), TotalBytes));
+ Slot->setAlignment(Align(8));
+ TraceIRB.CreateMemSet(Slot, TraceIRB.getInt8(0), TotalBytes,
+ Slot->getAlign());
+ for (auto &[V, BitOff] : SA.Fragments) {
+ unsigned ByteOff = BitOff / 8;
+ Value *Ptr = TraceIRB.CreateGEP(TraceIRB.getInt8Ty(), Slot,
+ ConstantInt::get(Int32Ty, ByteOff));
+ TraceIRB.CreateAlignedStore(V, Ptr, Align(1));
+ }
+ ArgPtr = Slot;
+ ArgByteSize = TotalBytes;
+ }
+
+ Value *OffsetsPtr = Constant::getNullValue(PtrTy);
+ if (OffsetsGV) {
+ Value *Indices[] = {ConstantInt::get(Int64Ty, 0),
+ ConstantInt::get(Int64Ty, 1)};
+ OffsetsPtr = TraceIRB.CreateInBoundsGEP(OffsetsGV->getValueType(),
+ OffsetsGV, Indices);
+ }
+ // Report 0-based source arg index
+ TraceIRB.CreateCall(SanCovTraceArgsFunc,
+ {PC, ConstantInt::get(Int32Ty, SrcIdx - 1),
+ ConstantInt::get(Int32Ty, ArgByteSize), ArgPtr,
+ OffsetsPtr, ConstantInt::get(Int32Ty, NumFields)});
+ }
+
+ // Dead-arg fallback: if the DISubroutineType indicates more source params
+ // than we found via debug records (e.g., arg optimized away entirely at -O2),
+ // emit a null-pointer trace so consumers know the argument existed.
+ if (SP && SP->getType()) {
+ auto TypeArray = SP->getType()->getTypeArray();
+ unsigned NumSrcParams = TypeArray.size() > 0 ? TypeArray.size() - 1 : 0;
+ for (unsigned I = 1; I <= NumSrcParams; ++I) {
+ if (!SrcArgs.count(I)) {
+ // This source param had no debug record — likely optimized away
+ DIType *ArgDIType = TypeArray[I];
+ auto [OffsetsGV, NumFields] = getStructFieldOffsets(ArgDIType, M, *DL);
+ Value *OffsetsPtr = Constant::getNullValue(PtrTy);
+ if (OffsetsGV) {
+ Value *Indices[] = {ConstantInt::get(Int64Ty, 0),
+ ConstantInt::get(Int64Ty, 1)};
+ OffsetsPtr = TraceIRB.CreateInBoundsGEP(OffsetsGV->getValueType(),
+ OffsetsGV, Indices);
+ }
+ // Pass null pointer — kernel will record 0xBADADD85 for all fields
+ TraceIRB.CreateCall(
+ SanCovTraceArgsFunc,
+ {PC, ConstantInt::get(Int32Ty, I - 1),
+ ConstantInt::get(Int32Ty, 0),
+ Constant::getNullValue(PtrTy), OffsetsPtr,
+ ConstantInt::get(Int32Ty, NumFields)});
+ }
+ }
+ }
+}
+
+void ModuleSanitizerCoverage::InjectTraceForRet(Function &F) {
+ DISubprogram *SP = F.getSubprogram();
+
+ // On x86, skip the escaping return-value spill in functions whose inline asm ties up
+ // the base pointer (RBX): the spill alloca is ASAN-redzoned -> stack realignment ->
+ // base pointer, which the backend rejects against the asm's RBX use ("Interference
+ // usage of base pointer/frame pointer"). Such returns are traced as a null pointer.
+ const bool SkipSpill = TargetTriple.getArch() == Triple::x86_64 &&
+ functionInlineAsmUsesBasePointerX86(F);
+
+ // Get return type debug info
+ DIType *RetDIType = nullptr;
+ if (SP && SP->getType()) {
+ auto TypeArray = SP->getType()->getTypeArray();
+ if (TypeArray.size() > 0)
+ RetDIType = TypeArray[0];
+ }
+
+ auto [OffsetsGV, NumFields] = getStructFieldOffsets(RetDIType, M, *DL);
+
+ // A struct returned by value may be lowered to an indirect return: the IR
+ // function returns void and writes the result into a caller-provided buffer
+ // passed as a hidden `sret` pointer. In that case the ReturnInst carries no
+ // value, so trace the sret buffer instead. This keeps the return path
+ // symmetric with the argument path, which deliberately skips the same sret
+ // pointer as a non-source argument.
+ Argument *SRetArg = nullptr;
+ for (Argument &A : F.args()) {
+ if (A.hasStructRetAttr()) {
+ SRetArg = &A;
+ break;
+ }
+ }
+
+ EscapeEnumerator EE(F, "sancov_trace_ret");
+ while (IRBuilder<> *AtExit = EE.Next()) {
+ InstrumentationIRBuilder::ensureDebugInfo(*AtExit, F);
+
+ Value *PC = AtExit->CreatePtrToInt(&F, Int64Ty);
+
+ // Get the return value
+ auto *RI = dyn_cast<ReturnInst>(AtExit->GetInsertPoint());
+ Value *RetVal = nullptr;
+ if (RI)
+ RetVal = RI->getReturnValue();
+
+ Value *RetPtr;
+ unsigned RetByteSize = 0;
+ if (RetVal && RetVal->getType()->isPointerTy()) {
+ RetPtr = RetVal;
+ RetByteSize = DL->getPointerSize();
+ } else if (RetVal && !RetVal->getType()->isVoidTy() && !SkipSpill) {
+ AllocaInst *Alloca = AtExit->CreateAlloca(RetVal->getType());
+ AtExit->CreateStore(RetVal, Alloca);
+ RetPtr = Alloca;
+ RetByteSize = DL->getTypeStoreSize(RetVal->getType());
+ } else if (SRetArg) {
+ // Indirect (sret) return: the value lives in the caller-provided buffer.
+ RetPtr = SRetArg;
+ if (Type *ElemTy = SRetArg->getParamStructRetType())
+ RetByteSize = DL->getTypeStoreSize(ElemTy);
+ else
+ RetByteSize = DL->getPointerSize();
+ } else {
+ RetPtr = Constant::getNullValue(PtrTy);
+ }
+
+ Value *OffsetsPtr;
+ if (OffsetsGV) {
+ Value *Indices[] = {ConstantInt::get(Int64Ty, 0),
+ ConstantInt::get(Int64Ty, 1)};
+ OffsetsPtr = AtExit->CreateInBoundsGEP(OffsetsGV->getValueType(),
+ OffsetsGV, Indices);
+ } else {
+ OffsetsPtr = Constant::getNullValue(PtrTy);
+ }
+ Value *NF = ConstantInt::get(Int32Ty, NumFields);
+ Value *RetSizeVal = ConstantInt::get(Int32Ty, RetByteSize);
+
+ AtExit->CreateCall(SanCovTraceRetFunc,
+ {PC, RetSizeVal, RetPtr, OffsetsPtr, NF});
+ }
+}
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll
new file mode 100644
index 0000000000000..5086176c3a363
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll
@@ -0,0 +1,88 @@
+; Test trace-args handles ABI-inserted hidden arguments correctly.
+; Verifies:
+; 1. sret hidden arg is skipped (not traced)
+; 2. Struct coercion fragments are reassembled
+; 3. Normal pointer arg with struct offsets still works
+
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-args -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+%struct.big = type { i64, i64, i64, i64, i64 }
+%struct.small = type { i32, i32 }
+
+; Function with sret: source has 2 params (x, y), IR has 3 args (sret, x, y)
+define void @make_big(ptr sret(%struct.big) %0, i32 %1, i32 %2) #0 !dbg !20 {
+entry:
+ #dbg_value(i32 %1, !30, !DIExpression(), !32)
+ #dbg_value(i32 %2, !31, !DIExpression(), !32)
+ ret void
+}
+
+; CHECK-LABEL: define void @make_big(ptr sret(%struct.big) %0, i32 %1, i32 %2)
+; Trace calls must carry a !dbg location: this function has debug info, so a
+; call without one would fail the verifier under -g/LTO.
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @make_big to i64), i32 0, i32 4, ptr %{{.*}}, ptr null, i32 0), !dbg !{{[0-9]+}}
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @make_big to i64), i32 1, i32 4, ptr %{{.*}}, ptr null, i32 0), !dbg !{{[0-9]+}}
+; CHECK-NOT: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @make_big to i64), i32 2
+; CHECK: ret void
+
+; Function with struct coercion: source has 2 params (s, z), IR has 2 args (i64, i32)
+; but debug info says s is split into fragments at bit offsets 0 and 32
+define i32 @use_small(i64 %0, i32 %1) #0 !dbg !40 {
+entry:
+ %3 = trunc i64 %0 to i32
+ %4 = lshr i64 %0, 32
+ %5 = trunc nuw i64 %4 to i32
+ #dbg_value(i32 %3, !50, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !52)
+ #dbg_value(i32 %5, !50, !DIExpression(DW_OP_LLVM_fragment, 32, 32), !52)
+ #dbg_value(i32 %1, !51, !DIExpression(), !52)
+ %6 = add i32 %1, %3
+ %7 = add i32 %6, %5
+ ret i32 %7
+}
+
+; CHECK-LABEL: define i32 @use_small(i64 %0, i32 %1)
+; Two trace calls: arg 0 = reassembled struct (8 bytes) with field offsets, arg 1 = z (4 bytes)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_small to i64), i32 0, i32 8, ptr %{{.*}}, ptr getelementptr inbounds {{.*}}, i32 2)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_small to i64), i32 1, i32 4, ptr %{{.*}}, ptr null, i32 0)
+; CHECK: ret i32
+
+attributes #0 = { nounwind }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, isOptimized: true, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test_abi.c", directory: "/tmp")
+!2 = !{}
+!3 = !{i32 2, !"Dwarf Version", i32 4}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+
+; Types
+!5 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!6 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+
+; make_big debug info
+!20 = distinct !DISubprogram(name: "make_big", scope: !1, file: !1, line: 3, type: !21, scopeLine: 3, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !29)
+!21 = !DISubroutineType(types: !22)
+!22 = !{!23, !5, !5} ; returns struct big, params: int, int
+!23 = !DICompositeType(tag: DW_TAG_structure_type, name: "big", size: 320, elements: !2)
+!29 = !{!30, !31}
+!30 = !DILocalVariable(name: "x", arg: 1, scope: !20, file: !1, line: 3, type: !5)
+!31 = !DILocalVariable(name: "y", arg: 2, scope: !20, file: !1, line: 3, type: !5)
+!32 = !DILocation(line: 3, scope: !20)
+
+; use_small debug info
+!40 = distinct !DISubprogram(name: "use_small", scope: !1, file: !1, line: 8, type: !41, scopeLine: 8, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !49)
+!41 = !DISubroutineType(types: !42)
+!42 = !{!5, !43, !5} ; returns int, params: struct small, int
+!43 = !DICompositeType(tag: DW_TAG_structure_type, name: "small", size: 64, elements: !44)
+!44 = !{!45, !46}
+!45 = !DIDerivedType(tag: DW_TAG_member, name: "x", scope: !43, baseType: !5, size: 32, offset: 0)
+!46 = !DIDerivedType(tag: DW_TAG_member, name: "y", scope: !43, baseType: !5, size: 32, offset: 32)
+!49 = !{!50, !51}
+!50 = !DILocalVariable(name: "s", arg: 1, scope: !40, file: !1, line: 8, type: !43)
+!51 = !DILocalVariable(name: "z", arg: 2, scope: !40, file: !1, line: 8, type: !5)
+!52 = !DILocation(line: 8, scope: !40)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-dominance.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-dominance.ll
new file mode 100644
index 0000000000000..d3dfb4db8b788
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-dominance.ll
@@ -0,0 +1,50 @@
+; Regression test: trace-args must NOT emit a non-dominating use. The entry-block #dbg_value
+; scan may find an argument whose debug location is a value defined LATER in the function
+; (debug records are exempt from SSA dominance). Using such a value as the trace pointer -
+; the trace call is inserted at the entry-block terminator - produces IR that fails the
+; verifier ("Instruction does not dominate all uses") and, with -disable-llvm-verifier
+; (kernel builds), crashes RegisterCoalescer::reMaterializeDef at codegen. Such an argument
+; is traced as a null pointer instead. opt runs the verifier, so a successful run of this
+; test already proves the emitted IR is well-formed (it would error before this fix).
+
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-args -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+; %arg1's only debug location is %later, a GEP defined in a later block (does NOT dominate
+; the entry terminator). %arg2 is a normal directly-located pointer argument.
+define void @arg_loc_not_dominating(ptr %arg1, ptr %arg2) #0 !dbg !8 {
+entry:
+ #dbg_value(ptr %arg2, !13, !DIExpression(), !15)
+ #dbg_value(ptr %later, !12, !DIExpression(), !15)
+ br label %bb, !dbg !15
+bb:
+ %later = getelementptr i8, ptr %arg1, i64 128, !dbg !15
+ ret void, !dbg !15
+}
+; CHECK-LABEL: define void @arg_loc_not_dominating(ptr %arg1, ptr %arg2)
+; arg1 (index 0): its debug location did not dominate -> traced as a null pointer, size 0.
+; CHECK-DAG: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @arg_loc_not_dominating to i64), i32 0, i32 0, ptr null, ptr {{.*}}, i32 {{.*}})
+; arg2 (index 1): a normal pointer arg, traced directly.
+; CHECK-DAG: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @arg_loc_not_dominating to i64), i32 1, i32 {{[0-9]+}}, ptr %arg2, ptr {{.*}}, i32 {{.*}})
+
+attributes #0 = { nounwind sanitize_address }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, isOptimized: true, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/tmp")
+!2 = !{}
+!3 = !{i32 2, !"Dwarf Version", i32 4}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!6 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !5, size: 64)
+!8 = distinct !DISubprogram(name: "arg_loc_not_dominating", scope: !1, file: !1, line: 1, type: !9, unit: !0, retainedNodes: !11)
+!9 = !DISubroutineType(types: !10)
+!10 = !{null, !6, !6}
+!11 = !{!12, !13}
+!12 = !DILocalVariable(name: "arg1", arg: 1, scope: !8, file: !1, line: 1, type: !6)
+!13 = !DILocalVariable(name: "arg2", arg: 2, scope: !8, file: !1, line: 1, type: !6)
+!15 = !DILocation(line: 1, column: 1, scope: !8)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-debug.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-debug.ll
new file mode 100644
index 0000000000000..81f516508cf1b
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-debug.ll
@@ -0,0 +1,21 @@
+; Test that trace-args works WITHOUT debug info (-g). A function with no
+; DISubprogram carries no #dbg_value records, so the source-argument map stays
+; empty and the pass falls back to tracing each IR argument directly (no
+; source-level struct-field offsets). This keeps trace-args usable on userspace
+; / optimized code built without -g, not only debug kernels. opt runs the
+; verifier, so a successful run also proves the emitted IR is well-formed.
+
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-args -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+define void @no_debug(ptr %p, i32 %x) {
+entry:
+ ret void
+}
+; CHECK-LABEL: define void @no_debug(ptr %p, i32 %x)
+; pointer arg 0 is traced directly (no spill, no field offsets):
+; CHECK-DAG: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @no_debug to i64), i32 0, i32 8, ptr %p, ptr null, i32 0)
+; scalar arg 1 is spilled to a stack slot then traced:
+; CHECK-DAG: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @no_debug to i64), i32 1, i32 4, ptr %{{.*}}, ptr null, i32 0)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
new file mode 100644
index 0000000000000..d6c91be2c5c26
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
@@ -0,0 +1,43 @@
+; Test sanitizer coverage trace-args instrumentation.
+; Verifies that __sanitizer_cov_trace_args is called for struct pointer and scalar args.
+
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-args -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+%struct.MyStruct = type { i32, i64 }
+
+define void @func_with_args(ptr %s, i32 %x) #0 !dbg !8 {
+entry:
+ ret void
+}
+
+; CHECK: define void @func_with_args(ptr %s, i32 %x)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @func_with_args to i64), i32 0, i32 8, ptr %s, ptr getelementptr inbounds ([5 x i64], ptr @__sancov_offsets_{{.*}}, i64 0, i64 1), i32 2)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @func_with_args to i64), i32 1, i32 4, ptr %{{.*}}, ptr null, i32 0)
+; CHECK: ret void
+
+attributes #0 = { nounwind sanitize_address }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, isOptimized: false, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/tmp")
+!2 = !{}
+!3 = !{i32 2, !"Dwarf Version", i32 4}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+
+; struct MyStruct { int a; long b; }
+!5 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!6 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+!7 = !DICompositeType(tag: DW_TAG_structure_type, name: "MyStruct", size: 128, elements: !14)
+!8 = distinct !DISubprogram(name: "func_with_args", scope: !1, file: !1, line: 5, type: !9, unit: !0, retainedNodes: !2)
+!9 = !DISubroutineType(types: !10)
+; types: [ret=void, arg0=ptr to MyStruct, arg1=int]
+!10 = !{null, !11, !5}
+!11 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !7, size: 64)
+!12 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !7, file: !1, baseType: !5, size: 32, offset: 0)
+!13 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !7, file: !1, baseType: !6, size: 64, offset: 64)
+!14 = !{!12, !13}
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-ret-basepointer.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret-basepointer.ll
new file mode 100644
index 0000000000000..48839d8d77246
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret-basepointer.ll
@@ -0,0 +1,51 @@
+; Regression test: trace-ret must NOT create an escaping return-value spill alloca in a
+; function whose inline asm uses/clobbers the x86 base pointer (RBX). The spill alloca's
+; address is passed to __sanitizer_cov_trace_ret, so it escapes; under KASAN it is redzoned,
+; forcing 32-byte frame realignment -> a base pointer (RBX), which the X86 backend then
+; rejects against the asm's RBX use with "Interference usage of base pointer/frame pointer".
+; For such functions the return is traced as a null pointer instead.
+
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-ret -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+; Scalar return in a function whose inline asm clobbers rbx (cf. CPUID "=b", RDTSC "~{rbx}").
+define i32 @ret_scalar_clobbers_rbx(i32 %x) #0 !dbg !8 {
+entry:
+ call void asm sideeffect "nop", "~{rbx},~{dirflag},~{fpsr},~{flags}"() #0, !dbg !12
+ ret i32 %x, !dbg !12
+}
+; CHECK-LABEL: define i32 @ret_scalar_clobbers_rbx(i32 %x)
+; CHECK-NOT: alloca
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_scalar_clobbers_rbx to i64), i32 0, ptr null, ptr null, i32 0)
+; CHECK: ret i32 %x
+
+; Control: the same scalar return WITHOUT base-pointer asm still spills and traces normally.
+define i32 @ret_scalar_plain(i32 %x) #0 !dbg !13 {
+entry:
+ ret i32 %x, !dbg !14
+}
+; CHECK-LABEL: define i32 @ret_scalar_plain(i32 %x)
+; CHECK: %[[SLOT:.*]] = alloca i32
+; CHECK: store i32 %x, ptr %[[SLOT]]
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_scalar_plain to i64), i32 4, ptr %[[SLOT]], ptr null, i32 0)
+; CHECK: ret i32 %x
+
+attributes #0 = { nounwind sanitize_address }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, isOptimized: true, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/tmp")
+!2 = !{}
+!3 = !{i32 2, !"Dwarf Version", i32 4}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!8 = distinct !DISubprogram(name: "ret_scalar_clobbers_rbx", scope: !1, file: !1, line: 1, type: !9, unit: !0, retainedNodes: !2)
+!9 = !DISubroutineType(types: !10)
+!10 = !{!5, !5}
+!12 = !DILocation(line: 1, column: 1, scope: !8)
+!13 = distinct !DISubprogram(name: "ret_scalar_plain", scope: !1, file: !1, line: 2, type: !9, unit: !0, retainedNodes: !2)
+!14 = !DILocation(line: 2, column: 1, scope: !13)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
new file mode 100644
index 0000000000000..f5fd01776acc2
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
@@ -0,0 +1,83 @@
+; Test sanitizer coverage trace-ret instrumentation.
+; Verifies that __sanitizer_cov_trace_ret is called for struct pointer and scalar returns.
+
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-ret -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+%struct.MyStruct = type { i32, i64 }
+%struct.Big = type { i64, i64, i64 }
+
+define ptr @func_ret_struct_ptr(ptr %s) #0 !dbg !8 {
+entry:
+ ret ptr %s
+}
+
+; CHECK: define ptr @func_ret_struct_ptr(ptr %s)
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @func_ret_struct_ptr to i64), i32 8, ptr %s, ptr getelementptr inbounds ([5 x i64], ptr @__sancov_offsets_{{.*}}, i64 0, i64 1), i32 2)
+; CHECK: ret ptr %s
+
+define i32 @func_ret_scalar(i32 %x) #0 !dbg !15 {
+entry:
+ ret i32 %x
+}
+
+; CHECK: define i32 @func_ret_scalar(i32 %x)
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @func_ret_scalar to i64), i32 4, ptr %{{.*}}, ptr null, i32 0)
+; CHECK: ret i32 %x
+
+; Struct returned by value, lowered to an indirect (sret) return: the IR
+; returns void, so the sret buffer (arg 0) must be traced as the return value
+; with the source struct's field offsets and full struct size (24 bytes).
+define void @func_ret_sret(ptr sret(%struct.Big) %0) #0 !dbg !18 {
+entry:
+ ret void
+}
+
+; CHECK: define void @func_ret_sret(ptr sret(%struct.Big) %0)
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @func_ret_sret to i64), i32 24, ptr %0, ptr getelementptr inbounds ([7 x i64], ptr @__sancov_offsets_{{.*}}, i64 0, i64 1), i32 3)
+; CHECK: ret void
+
+attributes #0 = { nounwind sanitize_address }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, isOptimized: false, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/tmp")
+!2 = !{}
+!3 = !{i32 2, !"Dwarf Version", i32 4}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+
+; struct MyStruct { int a; long b; }
+!5 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!6 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+!7 = !DICompositeType(tag: DW_TAG_structure_type, name: "MyStruct", size: 128, elements: !14)
+
+; func_ret_struct_ptr returns ptr to MyStruct
+!8 = distinct !DISubprogram(name: "func_ret_struct_ptr", scope: !1, file: !1, line: 5, type: !9, unit: !0, retainedNodes: !2)
+!9 = !DISubroutineType(types: !10)
+; types: [ret=ptr to MyStruct, arg0=ptr to MyStruct]
+!10 = !{!11, !11}
+!11 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !7, size: 64)
+!12 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !7, file: !1, baseType: !5, size: 32, offset: 0)
+!13 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !7, file: !1, baseType: !6, size: 64, offset: 64)
+!14 = !{!12, !13}
+
+; func_ret_scalar returns i32
+!15 = distinct !DISubprogram(name: "func_ret_scalar", scope: !1, file: !1, line: 10, type: !16, unit: !0, retainedNodes: !2)
+!16 = !DISubroutineType(types: !17)
+; types: [ret=int, arg0=int]
+!17 = !{!5, !5}
+
+; func_ret_sret returns struct Big { long a; long b; long c; } by value
+!18 = distinct !DISubprogram(name: "func_ret_sret", scope: !1, file: !1, line: 15, type: !19, unit: !0, retainedNodes: !2)
+!19 = !DISubroutineType(types: !20)
+; types: [ret=struct Big, arg=struct Big] (arg is the source-level return, no sret entry)
+!20 = !{!21, !21}
+!21 = !DICompositeType(tag: DW_TAG_structure_type, name: "Big", size: 192, elements: !25)
+!22 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !21, file: !1, baseType: !6, size: 64, offset: 0)
+!23 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !21, file: !1, baseType: !6, size: 64, offset: 64)
+!24 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !21, file: !1, baseType: !6, size: 64, offset: 128)
+!25 = !{!22, !23, !24}
More information about the llvm-commits
mailing list