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 <[email protected]> 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 <[email protected]> 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 <[email protected]> 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 <[email protected]> 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 ==================== _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
