https://github.com/yskzalloc updated https://github.com/llvm/llvm-project/pull/201410
>From a48c22ae39ce7be294c21b008c59fb96685050bb Mon Sep 17 00:00:00 2001 From: Yunseong Kim <[email protected]> Date: Sun, 7 Jun 2026 20:10:32 +0200 Subject: [PATCH 1/6] [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 5f3baf771ff96..6dcb2baf7968e 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 97d68877467fd..04351f1156315 100644 --- a/clang/include/clang/Basic/CodeGenOptions.h +++ b/clang/include/clang/Basic/CodeGenOptions.h @@ -663,7 +663,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 4fc9f4d4c3472..e2f9c4223d421 100644 --- a/clang/include/clang/Options/Options.td +++ b/clang/include/clang/Options/Options.td @@ -8119,6 +8119,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 bf77e748ebd0c..0bbbfefbc7acb 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 { @@ -1042,6 +1044,7 @@ SanitizerArgs::SanitizerArgs(const ToolChain &TC, int InstrumentationTypes = CoverageTracePC | CoverageTracePCEntryExit | CoverageTracePCGuard | CoverageInline8bitCounters | CoverageTraceLoads | CoverageTraceStores | + CoverageTraceArgs | CoverageTraceRet | CoverageInlineBoolFlag | CoverageControlFlow; if ((CoverageFeatures & InsertionPointTypes) && !(CoverageFeatures & InstrumentationTypes) && DiagnoseErrors) { @@ -1053,9 +1056,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) @@ -1416,6 +1420,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) @@ -1803,6 +1809,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 8772358424a871da585c407025ff8ce2fbd0d747 Mon Sep 17 00:00:00 2001 From: Yunseong Kim <[email protected]> Date: Sun, 7 Jun 2026 20:10:38 +0200 Subject: [PATCH 2/6] [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 b13b4233937db1970f6ad666b168d165316ab2b9 Mon Sep 17 00:00:00 2001 From: Yunseong Kim <[email protected]> Date: Sun, 7 Jun 2026 20:10:45 +0200 Subject: [PATCH 3/6] [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 e89ec35f5553e6e6328b0def3e46143fe9a5d679 Mon Sep 17 00:00:00 2001 From: Yunseong Kim <[email protected]> Date: Wed, 24 Jun 2026 19:46:26 +0200 Subject: [PATCH 4/6] [SanitizerCoverage] fix arg index misalignment from ABI lowering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous implementation iterated F.args() sequentially and indexed into DISubroutineType::TypeArray, assuming a 1:1 correspondence between IR-level and source-level arguments. This broke in three cases: 1. sret (large struct return): Clang/rustc inserts a hidden sret ptr as the first IR argument, shifting all subsequent arg indices. 2. Struct decomposition: Small structs may be split into multiple IR arguments (e.g., struct {int x; int y} → i32 %s.0, i32 %s.1), causing one source param to map to N IR args. 3. C++ this pointer: Hidden first argument with no source-level entry in the type array. Fix: Use DILocalVariable debug records (with getArg()) to map IR values back to source-level parameters. DILocalVariable::getArg() is stable across all ABI transformations because it is set by the frontend before any lowering. For struct decomposition, detect multiple IR values sharing the same DILocalVariable (via DW_OP_LLVM_fragment expressions) and reassemble them into a stack slot matching the source layout. Falls back to the original TypeArray method when debug info is absent. Tested: - C: make_big(int, int) → struct big (sret skipped, 2 correct traces) - C: use_small(struct small, int) (coerced i64 → reassembled, 2 traces) - Rust: same patterns via rustc (sret, struct split, pointer args) Signed-off-by: Yunseong Kim <[email protected]> --- .../Instrumentation/SanitizerCoverage.cpp | 248 ++++++++++++++---- .../SanitizerCoverage/trace-args-abi.ll | 86 ++++++ 2 files changed, 289 insertions(+), 45 deletions(-) create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll diff --git a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp index b6d9d6d7b6f33..fdf44c2b4d93c 100644 --- a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp +++ b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp @@ -20,6 +20,7 @@ #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" @@ -1321,9 +1322,9 @@ static DIType *stripDITypedefs(DIType *Ty) { 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. +// 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) @@ -1331,13 +1332,19 @@ getStructFieldOffsets(DIType *Ty, Module &M, const DataLayout &DL) { 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}; + 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); - 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}; @@ -1400,55 +1407,206 @@ void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) { // 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; + // 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; + + // Pass 1: direct argument debug records (batched scan) + SmallVector<DbgVariableRecord *, 16> AllDVRs; 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]; + 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; + SA.Fragments.push_back({&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 + auto &SA = SrcArgs[SrcIdx]; + SA.Var = Var; + SA.Ty = Var->getType(); + uint64_t FragBitOff = 0; + if (auto Frag = DVar->getExpression()->getFragmentInfo()) + FragBitOff = Frag->OffsetInBits; + SA.Fragments.push_back({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->getType()) { + auto TypeArray = SP->getType()->getTypeArray(); + 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 { + AllocaInst *Alloca = IRB.CreateAlloca(Arg.getType()); + IRB.CreateStore(&Arg, Alloca); + ArgPtr = Alloca; + } + unsigned ArgByteSize = Arg.getType()->isPointerTy() + ? DL->getPointerSize() + : DL->getTypeStoreSize(Arg.getType()); + 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); - auto [OffsetsGV, NumFields] = getStructFieldOffsets(ArgDIType, M, *DL); + IRBuilder<> TraceIRB(EntryBB.getTerminator()); + + for (unsigned SrcIdx : SortedKeys) { + auto &SA = SrcArgs[SrcIdx]; + auto [OffsetsGV, NumFields] = getStructFieldOffsets(SA.Ty, M, *DL); Value *ArgPtr; - if (Arg.getType()->isPointerTy()) { - ArgPtr = &Arg; + 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 { + AllocaInst *Alloca = IRB.CreateAlloca(V->getType()); + TraceIRB.CreateStore(V, Alloca); + ArgPtr = Alloca; + ArgByteSize = DL->getTypeStoreSize(V->getType()); + } } 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; + // 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; } - // 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; + Value *OffsetsPtr = Constant::getNullValue(PtrTy); if (OffsetsGV) { Value *Indices[] = {ConstantInt::get(Int64Ty, 0), ConstantInt::get(Int64Ty, 1)}; - OffsetsPtr = - IRB.CreateInBoundsGEP(OffsetsGV->getValueType(), OffsetsGV, Indices); - } else { - OffsetsPtr = Constant::getNullValue(PtrTy); + OffsetsPtr = TraceIRB.CreateInBoundsGEP(OffsetsGV->getValueType(), + OffsetsGV, Indices); } - Value *NF = ConstantInt::get(Int32Ty, NumFields); - Value *ArgIdxVal = ConstantInt::get(Int32Ty, ArgIdx); - Value *ArgSizeVal = ConstantInt::get(Int32Ty, ArgByteSize); + // 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)}); + } - IRB.CreateCall(SanCovTraceArgsFunc, - {PC, ArgIdxVal, ArgSizeVal, ArgPtr, OffsetsPtr, NF}); - ArgIdx++; + // 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->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)}); + } + } } } 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..f1dc56476abda --- /dev/null +++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll @@ -0,0 +1,86 @@ +; 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) +; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @make_big to i64), i32 0, i32 4, ptr %{{.*}}, ptr null, i32 0) +; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @make_big to i64), i32 1, i32 4, ptr %{{.*}}, ptr null, i32 0) +; 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) >From c60c0383306161894c16955bbdfbcc8093115b02 Mon Sep 17 00:00:00 2001 From: Yunseong Kim <[email protected]> Date: Tue, 7 Jul 2026 16:06:08 +0200 Subject: [PATCH 5/6] [SanitizerCoverage] Fix -g verifier failure and capture sret returns Two defects in the trace-args/trace-ret instrumentation, both surfacing in exactly the -g configuration the feature requires: 1. Missing !dbg on argument trace calls (verifier failure under -g/LTO). InjectTraceForArgs emitted the per-argument callbacks through a plain IRBuilder anchored at the entry-block terminator. In a function that carries debug info, a call to a non-intrinsic callee with no !dbg location fails the verifier ("inlinable function call ... requires a !dbg location") once inlining/LTO runs. The argument path is the only place that did this: the fallback (no-debug-info) path and the return path both already go through InstrumentationIRBuilder, which stamps a synthetic location. Switch the argument path to InstrumentationIRBuilder as well so the callbacks inherit a location. 2. Indirect (sret) returns were dropped. 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. InjectTraceForRet saw a valueless ReturnInst and emitted a null trace, so the return value was lost -- asymmetric with the argument path, which deliberately skips the same sret pointer as a non-source argument. When the return is void and the function has an sret argument, trace that buffer instead, using the source struct's field offsets (already the return DIType) and the full struct store size. Also de-duplicate fragments by (value, bit-offset) when collecting argument debug records. The same #dbg_value can be reached more than once (via the argument in pass 1 and again while scanning the block in pass 2, or duplicated by an earlier pass); without de-duplication a single scalar argument with a repeated record would look like a multi-fragment struct and be needlessly reassembled. Tests: - trace-args-abi.ll: assert the argument trace calls carry a !dbg location. - trace-ret.ll: add func_ret_sret, verifying the sret buffer is traced as the return value with the struct's field offsets and 24-byte size. Signed-off-by: Yunseong Kim <[email protected]> --- .../Instrumentation/SanitizerCoverage.cpp | 44 +++++++++++++++++-- .../SanitizerCoverage/trace-args-abi.ll | 6 ++- .../SanitizerCoverage/trace-ret.ll | 24 ++++++++++ 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp index fdf44c2b4d93c..df96c8141a55a 100644 --- a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp +++ b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp @@ -1422,6 +1422,19 @@ void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) { }; 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()) { @@ -1438,7 +1451,7 @@ void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) { uint64_t FragBitOff = 0; if (auto Frag = DVR->getExpression()->getFragmentInfo()) FragBitOff = Frag->OffsetInBits; - SA.Fragments.push_back({&Arg, FragBitOff}); + addFragment(SA, &Arg, FragBitOff); } } @@ -1467,7 +1480,7 @@ void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) { uint64_t FragBitOff = 0; if (auto Frag = DVar->getExpression()->getFragmentInfo()) FragBitOff = Frag->OffsetInBits; - SA.Fragments.push_back({V, FragBitOff}); + addFragment(SA, V, FragBitOff); } } @@ -1520,7 +1533,11 @@ void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) { SortedKeys.push_back(K); llvm::sort(SortedKeys); - IRBuilder<> TraceIRB(EntryBB.getTerminator()); + // 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]; @@ -1623,6 +1640,20 @@ void ModuleSanitizerCoverage::InjectTraceForRet(Function &F) { 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); @@ -1645,6 +1676,13 @@ void ModuleSanitizerCoverage::InjectTraceForRet(Function &F) { 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); } diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll index f1dc56476abda..5086176c3a363 100644 --- a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll +++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll @@ -21,8 +21,10 @@ entry: } ; CHECK-LABEL: define void @make_big(ptr sret(%struct.big) %0, i32 %1, i32 %2) -; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @make_big to i64), i32 0, i32 4, ptr %{{.*}}, ptr null, i32 0) -; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @make_big to i64), i32 1, i32 4, ptr %{{.*}}, ptr null, i32 0) +; 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 diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll index 3e2fbdcb3c1fd..f5fd01776acc2 100644 --- a/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll +++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll @@ -7,6 +7,7 @@ target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80: 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: @@ -26,6 +27,18 @@ entry: ; 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} @@ -57,3 +70,14 @@ attributes #0 = { nounwind sanitize_address } !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} >From 00b912945c2e75fc23e1e74a89f17a619a98c5b4 Mon Sep 17 00:00:00 2001 From: Yunseong Kim <[email protected]> Date: Sun, 7 Jun 2026 20:10:50 +0200 Subject: [PATCH 6/6] [SanitizerCoverage] Document trace-args/trace-ret modes Add documentation for the new coverage modes to SanitizerCoverage.rst, including the callback signatures and their intended use by the Linux kernel KCOV dataflow subsystem. The description follows the ABI-correct implementation: arg_idx is the source-level parameter index (mapped via DILocalVariable::getArg(), so it is stable across ABI lowering and does not count hidden sret/this pointers), ABI-decomposed structs are reassembled into a single stack slot in source layout, and an indirect (sret) struct return is reported via its caller-provided buffer rather than dropped. Signed-off-by: Yunseong Kim <[email protected]> --- clang/docs/SanitizerCoverage.rst | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/clang/docs/SanitizerCoverage.rst b/clang/docs/SanitizerCoverage.rst index c01863adebb2d..a9456f8bf681e 100644 --- a/clang/docs/SanitizerCoverage.rst +++ b/clang/docs/SanitizerCoverage.rst @@ -348,6 +348,64 @@ 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. + +``arg_idx`` is the *source-level* parameter index, not the IR argument position. +The two diverge whenever the ABI rewrites the argument list, and the reported index +follows the source. Specifically, the pass maps IR values back to source parameters +through ``DILocalVariable::getArg()``, which the frontend assigns before ABI lowering: + +* Hidden ABI-inserted pointers (a struct-return ``sret`` pointer, a C++ ``this`` + that has no source entry) carry no ``arg`` number and are not counted. +* A by-value struct that the ABI coerces or splits into several IR arguments is + reassembled from its ``DW_OP_LLVM_fragment`` pieces into a single stack slot in + source layout, so one source parameter yields one callback rather than N. + +For return values, a by-value struct that is lowered to an indirect (``sret``) +return produces an IR function that returns ``void``; the ``trace-ret`` callback +then reports the caller-provided ``sret`` buffer as the return value, so indirect +returns are captured rather than dropped. + +Both flags imply edge coverage when used alone. + +.. code-block:: c++ + + // Called at function entry, once per source-level argument. + // pc: address of the instrumented function + // arg_idx: zero-based source-level argument index (ABI-stable; hidden + // sret/this pointers are not counted) + // arg_size: size of the argument in bytes + // ptr: pointer to the argument value (stack-spilled for scalars, reassembled + // into a stack slot for ABI-decomposed structs) + // offsets: array of [byte_offset, byte_size] pairs for struct fields (null if not a struct) + // num_fields: number of struct fields (0 if not a struct) + 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; the sret + // buffer for indirect struct returns; null for void) + // offsets: array of [byte_offset, byte_size] pairs for struct fields (null if not a struct) + // num_fields: number of struct fields (0 if not a struct) + void __sanitizer_cov_trace_ret(uint64_t pc, uint32_t ret_size, + void *ptr, uint64_t *offsets, uint32_t num_fields); + + Tracing control flow ==================== _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
