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