[clang] [llvm] [sancov] Add -fsanitize-coverage=dataflow-args,dataflow-ret (PR #201410)
Yunseong Kim via cfe-commits
cfe-commits at lists.llvm.org
Mon Jun 8 00:45:44 PDT 2026
https://github.com/yskzalloc updated https://github.com/llvm/llvm-project/pull/201410
>From 8746337f192b86d625b6631c0091644f36f582eb Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Sun, 7 Jun 2026 20:10:32 +0200
Subject: [PATCH 1/4] [SanitizerCoverage] Add trace-args and trace-ret coverage
modes
Add two new sanitizer coverage modes:
-fsanitize-coverage=trace-args
-fsanitize-coverage=trace-ret
These insert calls to __sanitizer_cov_trace_args() at function entry
and __sanitizer_cov_trace_ret() before return instructions, enabling
per-task capture of function arguments and return values with automatic
struct field expansion via DICompositeType metadata.
Implementation:
- SanitizerCoverage.cpp: InjectTraceForArgs/InjectTraceForRet (~270 LOC)
- Uses DISubprogram/DICompositeType to extract struct field layouts
- Creates ConstantArray globals with FNV-1a hashed type name + offsets
- Scalar args spilled to alloca for uniform pointer interface
- Both flags imply edge coverage (level=3) when used alone
- Requires -g for struct expansion; skips functions without DISubprogram
Driver integration:
- CodeGenOptions.def: SanitizeCoverageTraceArgs/Ret flags
- SanitizerArgs.cpp: parse trace-args/trace-ret (enum 1<<20, 1<<21)
- BackendUtil.cpp: wire to SanitizerCoverageOptions
- Instrumentation.h: TraceArgs/TraceRet booleans
---
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 +-
.../llvm/Transforms/Utils/Instrumentation.h | 2 +
.../Instrumentation/SanitizerCoverage.cpp | 243 +++++++++++++++++-
7 files changed, 271 insertions(+), 5 deletions(-)
diff --git a/clang/include/clang/Basic/CodeGenOptions.def b/clang/include/clang/Basic/CodeGenOptions.def
index 6cce4ada1dfd1..0ac95b143fc84 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 e43112b4bb98b..3687731f8d629 100644
--- a/clang/include/clang/Basic/CodeGenOptions.h
+++ b/clang/include/clang/Basic/CodeGenOptions.h
@@ -655,7 +655,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 4fd892e58df86..5170d731d7613 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -8069,6 +8069,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 a46a25c4492f2..763d5a7a92e07 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -251,6 +251,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 74ebd0bf375d3..88f4440dc61f9 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 {
@@ -1043,6 +1045,7 @@ SanitizerArgs::SanitizerArgs(const ToolChain &TC,
int InstrumentationTypes = CoverageTracePC | CoverageTracePCEntryExit |
CoverageTracePCGuard | CoverageInline8bitCounters |
CoverageTraceLoads | CoverageTraceStores |
+ CoverageTraceArgs | CoverageTraceRet |
CoverageInlineBoolFlag | CoverageControlFlow;
if ((CoverageFeatures & InsertionPointTypes) &&
!(CoverageFeatures & InstrumentationTypes) && DiagnoseErrors) {
@@ -1054,9 +1057,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)
@@ -1417,6 +1421,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)
@@ -1804,6 +1810,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/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..b6d9d6d7b6f33 100644
--- a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
+++ b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
@@ -15,9 +15,11 @@
#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/Dominators.h"
#include "llvm/IR/EHPersonalities.h"
#include "llvm/IR/Function.h"
@@ -46,6 +48,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 +160,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 +239,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 +281,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 +316,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 +561,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 +796,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 +1301,209 @@ 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), 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);
+
+ // Must be a pointer to something
+ auto *PtrTy = dyn_cast_or_null<DIDerivedType>(Ty);
+ if (!PtrTy || PtrTy->getTag() != dwarf::DW_TAG_pointer_type)
+ return {nullptr, 0};
+
+ DIType *PointeeTy = stripDITypedefs(PtrTy->getBaseType());
+ auto *Composite = dyn_cast_or_null<DICompositeType>(PointeeTy);
+ 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)};
+}
+
+void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
+ DISubprogram *SP = F.getSubprogram();
+ if (!SP)
+ return;
+
+ 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);
+
+ // For each argument, emit a trace call
+ unsigned ArgIdx = 0;
+ for (auto &Arg : F.args()) {
+ // Get debug info for this argument
+ DIType *ArgDIType = nullptr;
+ if (SP->getType()) {
+ auto *SubroutineType = SP->getType();
+ auto TypeArray = SubroutineType->getTypeArray();
+ // TypeArray[0] is return type, TypeArray[1..] are params
+ if (ArgIdx + 1 < TypeArray.size())
+ ArgDIType = TypeArray[ArgIdx + 1];
+ }
+
+ auto [OffsetsGV, NumFields] = getStructFieldOffsets(ArgDIType, M, *DL);
+
+ Value *ArgPtr;
+ if (Arg.getType()->isPointerTy()) {
+ ArgPtr = &Arg;
+ } else {
+ // Spill non-pointer arg to stack so we can pass its address
+ AllocaInst *Alloca = IRB.CreateAlloca(Arg.getType());
+ IRB.CreateStore(&Arg, Alloca);
+ ArgPtr = Alloca;
+ }
+
+ // Compute arg byte size
+ unsigned ArgByteSize = Arg.getType()->isPointerTy()
+ ? DL->getPointerSize()
+ : DL->getTypeStoreSize(Arg.getType());
+
+ // OffsetsGV layout: [hash, off0, sz0, off1, sz1, ...]
+ // Pass pointer to &array[1] so kernel sees field data at offsets[0],
+ // and can read offsets[-1] for the type hash.
+ Value *OffsetsPtr;
+ if (OffsetsGV) {
+ Value *Indices[] = {ConstantInt::get(Int64Ty, 0),
+ ConstantInt::get(Int64Ty, 1)};
+ OffsetsPtr =
+ IRB.CreateInBoundsGEP(OffsetsGV->getValueType(), OffsetsGV, Indices);
+ } else {
+ OffsetsPtr = Constant::getNullValue(PtrTy);
+ }
+ Value *NF = ConstantInt::get(Int32Ty, NumFields);
+ Value *ArgIdxVal = ConstantInt::get(Int32Ty, ArgIdx);
+ Value *ArgSizeVal = ConstantInt::get(Int32Ty, ArgByteSize);
+
+ IRB.CreateCall(SanCovTraceArgsFunc,
+ {PC, ArgIdxVal, ArgSizeVal, ArgPtr, OffsetsPtr, NF});
+ ArgIdx++;
+ }
+}
+
+void ModuleSanitizerCoverage::InjectTraceForRet(Function &F) {
+ DISubprogram *SP = F.getSubprogram();
+
+ // 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);
+
+ 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()) {
+ AllocaInst *Alloca = AtExit->CreateAlloca(RetVal->getType());
+ AtExit->CreateStore(RetVal, Alloca);
+ RetPtr = Alloca;
+ RetByteSize = DL->getTypeStoreSize(RetVal->getType());
+ } 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});
+ }
+}
>From 51e0af00bb2b57c2c135034536a6ec051690bfe1 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Sun, 7 Jun 2026 20:10:38 +0200
Subject: [PATCH 2/4] [SanitizerCoverage] Add clang tests for
trace-args/trace-ret
- clang/test/Driver/fsanitize-coverage.c: verify flag parsing and
edge coverage implication
- clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c: end-to-end
C source to callback emission verification
---
.../sanitizer-coverage-trace-args-ret.c | 18 ++++++++++++++++++
clang/test/Driver/fsanitize-coverage.c | 13 +++++++++++++
2 files changed, 31 insertions(+)
create mode 100644 clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c
diff --git a/clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c b/clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c
new file mode 100644
index 0000000000000..f8114c310668f
--- /dev/null
+++ b/clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c
@@ -0,0 +1,18 @@
+// Test that -fsanitize-coverage=trace-args and trace-ret emit the expected callbacks.
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -fsanitize-coverage-trace-args -fsanitize-coverage-type=3 -debug-info-kind=limited %s -o - | FileCheck %s --check-prefix=CHECK-ARGS
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -fsanitize-coverage-trace-ret -fsanitize-coverage-type=3 -debug-info-kind=limited %s -o - | FileCheck %s --check-prefix=CHECK-RET
+
+struct Foo {
+ int a;
+ long b;
+};
+
+void takes_struct_ptr(struct Foo *f) {
+}
+
+int returns_scalar(int x) {
+ return x + 1;
+}
+
+// CHECK-ARGS: call void @__sanitizer_cov_trace_args
+// CHECK-RET: call void @__sanitizer_cov_trace_ret
diff --git a/clang/test/Driver/fsanitize-coverage.c b/clang/test/Driver/fsanitize-coverage.c
index 21e2c16bfb1b7..f6d4696f7040e 100644
--- a/clang/test/Driver/fsanitize-coverage.c
+++ b/clang/test/Driver/fsanitize-coverage.c
@@ -170,3 +170,16 @@
// CHECK-NO-SHADOWCALLSTACK-NOT: unknown argument
// CHECK-NO-SHADOWCALLSTACK-NOT: -fsanitize=shadow-call-stack
// CHECK-NO-SHADOWCALLSTACK: -fsanitize-coverage-trace-pc-guard
+
+// RUN: %clang --target=x86_64-linux-gnu -fsanitize-coverage=trace-args %s -### 2>&1 | FileCheck %s --check-prefix=CHECK_DATAFLOW_ARGS
+// CHECK_DATAFLOW_ARGS: -fsanitize-coverage-type=3
+// CHECK_DATAFLOW_ARGS: -fsanitize-coverage-trace-args
+
+// RUN: %clang --target=x86_64-linux-gnu -fsanitize-coverage=trace-ret %s -### 2>&1 | FileCheck %s --check-prefix=CHECK_DATAFLOW_RET
+// CHECK_DATAFLOW_RET: -fsanitize-coverage-type=3
+// CHECK_DATAFLOW_RET: -fsanitize-coverage-trace-ret
+
+// RUN: %clang --target=x86_64-linux-gnu -fsanitize-coverage=edge,trace-args,trace-ret %s -### 2>&1 | FileCheck %s --check-prefix=CHECK_DATAFLOW_BOTH
+// CHECK_DATAFLOW_BOTH: -fsanitize-coverage-type=3
+// CHECK_DATAFLOW_BOTH: -fsanitize-coverage-trace-args
+// CHECK_DATAFLOW_BOTH: -fsanitize-coverage-trace-ret
\ No newline at end of file
>From 1a2077db43d81a1ed0ba05c0b4bf879d95e89c62 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Sun, 7 Jun 2026 20:10:45 +0200
Subject: [PATCH 3/4] [SanitizerCoverage] Add LLVM IR tests for
trace-args/trace-ret
- trace-args.ll: verifies callback insertion for scalar and struct
pointer arguments with field offset array generation
- trace-ret.ll: verifies return value callback insertion at all
exit points via EscapeEnumerator
---
.../SanitizerCoverage/trace-args.ll | 43 ++++++++++++++
.../SanitizerCoverage/trace-ret.ll | 59 +++++++++++++++++++
2 files changed, 102 insertions(+)
create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
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.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
new file mode 100644
index 0000000000000..3e2fbdcb3c1fd
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
@@ -0,0 +1,59 @@
+; 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 }
+
+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
+
+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}
>From 7fd67456defbe63d310f763c052a6cee69347f42 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Sun, 7 Jun 2026 20:10:50 +0200
Subject: [PATCH 4/4] [SanitizerCoverage] Document trace-args/trace-ret modes
Add documentation for the new coverage modes to
SanitizerCoverage.rst including callback signatures and usage.
---
clang/docs/SanitizerCoverage.rst | 39 ++++++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/clang/docs/SanitizerCoverage.rst b/clang/docs/SanitizerCoverage.rst
index c01863adebb2d..11e59fe6166b3 100644
--- a/clang/docs/SanitizerCoverage.rst
+++ b/clang/docs/SanitizerCoverage.rst
@@ -348,6 +348,45 @@ will not be instrumented.
void __sanitizer_cov_store16(__int128 *addr);
+Tracking function arguments and return values
+==============================================
+
+With ``-fsanitize-coverage=trace-args`` and ``-fsanitize-coverage=trace-ret``
+the compiler will insert callbacks at function entry and before return instructions
+to track function arguments and return values, respectively.
+
+These flags are designed for the Linux kernel's KCOV dataflow subsystem, which uses
+the callbacks to capture struct field values for memory corruption analysis.
+
+When debug info is available (``-g``), the compiler uses ``DICompositeType`` metadata
+to extract struct field layouts (byte offset and size pairs). A FNV-1a hash of the
+struct type name is prepended to the offsets array for identification. Without ``-g``,
+the pass returns early and no callbacks are emitted.
+
+Both flags imply edge coverage when used alone.
+
+.. code-block:: c++
+
+ // Called at function entry for each argument.
+ // pc: address of the instrumented function
+ // arg_idx: zero-based argument index
+ // arg_size: size of the argument in bytes
+ // ptr: pointer to the argument value (stack-spilled for scalars)
+ // offsets: array of [byte_offset, byte_size] pairs for struct fields (null if not a struct pointer)
+ // num_fields: number of struct fields (0 if not a struct pointer)
+ void __sanitizer_cov_trace_args(uint64_t pc, uint32_t arg_idx, uint32_t arg_size,
+ void *ptr, uint64_t *offsets, uint32_t num_fields);
+
+ // Called before each return instruction.
+ // pc: address of the instrumented function
+ // ret_size: size of the return value in bytes
+ // ptr: pointer to the return value (stack-spilled for scalars, null for void)
+ // offsets: array of [byte_offset, byte_size] pairs for struct fields (null if not a struct pointer)
+ // num_fields: number of struct fields (0 if not a struct pointer)
+ void __sanitizer_cov_trace_ret(uint64_t pc, uint32_t ret_size,
+ void *ptr, uint64_t *offsets, uint32_t num_fields);
+
+
Tracing control flow
====================
More information about the cfe-commits
mailing list