https://github.com/yskzalloc updated https://github.com/llvm/llvm-project/pull/227614
>From cca75b98d5e908d6fd69f3e21fff18ce3155241b Mon Sep 17 00:00:00 2001 From: Yunseong Kim <[email protected]> Date: Sun, 23 Aug 2026 12:27:09 +0200 Subject: [PATCH 1/4] [SanitizerCoverage] Add trace-args and trace-ret instrumentation Add two SanitizerCoverage modes that report the values flowing into and out of instrumented functions. trace-args emits one call per IR argument at function entry; trace-ret emits one call before every return: void __sanitizer_cov_trace_args(u64 tag, u64 val, const u64 *offsets); void __sanitizer_cov_trace_ret(u64 size, u64 val, const u64 *offsets); `tag` is (arg_idx << 32) | size, where arg_idx is the position in the IR parameter list. If `offsets` is null, `val` is the value itself, zero-extended to 64 bits. Otherwise `val` is the address of `size` bytes, and `offsets` points to a constant table: offsets[0] is the number N of {byte offset, byte size} pairs that follow, each at most 8 bytes. This form reports byval/byref/inalloca/preallocated arguments and sret buffers, described by the elements of their IR type, and pointers that are noundef and dereferenceable(N) or dereferenceable_or_null(N), snapshotted up to -sanitizer-coverage-trace-pointee-bytes (default 64). A size of 0 means nothing was reported. The modes are enabled through SanitizerCoverageOptions::TraceArgs and TraceRet or, for opt, -sanitizer-coverage-trace-args and -sanitizer-coverage-trace-ret. Like the other modes, they need a coverage level. They do not imply trace-pc-guard. What is emitted depends only on the IR. Debug information gives the calls a location and nothing else, so -g does not change the instrumentation. The pass creates no allocas. The calls are notail and nomerge, because a runtime identifies an observation by its return address, as with trace-cmp. Assisted-by: Kiro CLI (Claude Opus 5.5) Signed-off-by: Yunseong Kim <[email protected]> --- .../llvm/Transforms/Utils/Instrumentation.h | 2 + .../Instrumentation/InstrumentationOptions.td | 6 + .../Instrumentation/SanitizerCoverage.cpp | 350 +++++++++++++++++- .../trace-args-debug-invariance.ll | 137 +++++++ .../SanitizerCoverage/trace-args.ll | 279 ++++++++++++++ .../SanitizerCoverage/trace-ret.ll | 148 ++++++++ 6 files changed, 921 insertions(+), 1 deletion(-) create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance.ll create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args.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 97a853dcb65326..bc2530a5114c7a 100644 --- a/llvm/include/llvm/Transforms/Utils/Instrumentation.h +++ b/llvm/include/llvm/Transforms/Utils/Instrumentation.h @@ -164,6 +164,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/InstrumentationOptions.td b/llvm/lib/Transforms/Instrumentation/InstrumentationOptions.td index 44a2c46ed3ec43..d569a0c52d286a 100644 --- a/llvm/lib/Transforms/Instrumentation/InstrumentationOptions.td +++ b/llvm/lib/Transforms/Instrumentation/InstrumentationOptions.td @@ -750,6 +750,8 @@ defm : BoolField<"sanitizer-coverage-stack-depth", "false", "Max stack depth tracing">; defm : ValueField<"sanitizer-coverage-stack-depth-callback-min", "int", "0", "Max stack depth tracing should use callback and only when stack depth more than specified">; +defm : BoolField<"sanitizer-coverage-trace-args", "false", + "Tracing of function arguments">; defm : BoolField<"sanitizer-coverage-trace-compares", "false", "Tracing of CMP and similar instructions">; defm : BoolField<"sanitizer-coverage-trace-divs", "false", @@ -764,6 +766,10 @@ defm : BoolField<"sanitizer-coverage-trace-pc-entry-exit", "false", "PC tracing with separate entry/exit callbacks">; defm : BoolField<"sanitizer-coverage-trace-pc-guard", "false", "PC tracing with a guard">; +defm : ValueField<"sanitizer-coverage-trace-pointee-bytes", "unsigned", "64", + "With trace-args and trace-ret, read at most this many bytes behind a pointer the IR says is dereferenceable (0: report the pointer only)">; +defm : BoolField<"sanitizer-coverage-trace-ret", "false", + "Tracing of return values">; defm : BoolField<"sanitizer-coverage-trace-stores", "false", "Tracing of store instructions">; diff --git a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp index c375041e73d45e..9af71442e73dc6 100644 --- a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp +++ b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp @@ -16,9 +16,11 @@ #include "llvm/ADT/SmallVector.h" #include "llvm/Analysis/GlobalsModRef.h" #include "llvm/Analysis/PostDominators.h" +#include "llvm/Analysis/ValueTracking.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" @@ -31,6 +33,7 @@ #include "llvm/IR/Module.h" #include "llvm/IR/Type.h" #include "llvm/IR/ValueSymbolTable.h" +#include "llvm/Support/MathExtras.h" #include "llvm/Support/SpecialCaseList.h" #include "llvm/Support/VirtualFileSystem.h" #include "llvm/TargetParser/Triple.h" @@ -46,6 +49,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"; @@ -140,10 +145,13 @@ SanitizerCoverageOptions overrideFromCL(const InstrumentationOptions &Opts, Opts.sanitizer_coverage_stack_depth_callback_min); Options.TraceLoads |= Opts.sanitizer_coverage_trace_loads; Options.TraceStores |= Opts.sanitizer_coverage_trace_stores; + Options.TraceArgs |= Opts.sanitizer_coverage_trace_args; + Options.TraceRet |= Opts.sanitizer_coverage_trace_ret; Options.GatedCallbacks |= Opts.sanitizer_coverage_gated_trace_callbacks; 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 |= Opts.sanitizer_coverage_control_flow; return Options; @@ -180,6 +188,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); @@ -205,6 +215,36 @@ class ModuleSanitizerCoverage { std::string getSectionStart(const std::string &Section) const; std::string getSectionEnd(const std::string &Section) const; + /// The `offsets` argument of the trace-args/trace-ret callbacks for an + /// object in memory: a constant table whose first element is the number of + /// fields, followed by [byte offset, byte size] pairs covering the elements + /// of its IR type, and the store size of that type. Table is null for an + /// object of no fixed size, in which case ObjectSize is 0. + struct FieldOffsets { + Constant *Table = nullptr; + uint64_t ObjectSize = 0; + }; + FieldOffsets getObjectOffsets(Type *Ty); + /// Create one trace-args/trace-ret call, which is never a tail call and + /// cannot be merged. + void emitTraceCall(IRBuilderBase &IRB, FunctionCallee Callee, + std::optional<uint32_t> ArgIdx, uint64_t Size, Value *Val, + Value *Table = nullptr); + /// Emit the trace-args/trace-ret call for \p V through \p IRB: one call per + /// element of a first-class aggregate, and one for anything else. + void traceValue(IRBuilderBase &IRB, FunctionCallee Callee, + std::optional<uint32_t> ArgIdx, Value *V); + void traceObject(IRBuilderBase &IRB, FunctionCallee Callee, + std::optional<uint32_t> ArgIdx, Value *Addr, Type *Ty); + /// Report pointer \p Ptr together with the first \p Bytes bytes it points + /// to, which the IR says are dereferenceable. + void tracePointee(IRBuilderBase &IRB, FunctionCallee Callee, + std::optional<uint32_t> ArgIdx, Value *Ptr, uint64_t Bytes); + /// How many bytes behind \p Ptr to report, given that the IR says + /// \p Dereferenceable of them may be read: none if \p Ptr is not a pointer + /// in the default address space. + uint64_t getPointeeBytes(Value *Ptr, uint64_t Dereferenceable) const; + const InstrumentationOptions &Opts; Module &M; DomTreeCallback DTCallback; @@ -214,6 +254,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; @@ -237,6 +278,11 @@ class ModuleSanitizerCoverage { SmallVector<GlobalValue *, 20> GlobalsToAppendToUsed; SmallVector<GlobalValue *, 20> GlobalsToAppendToCompilerUsed; + /// Object tables are shared by every object of the same IR type: a type + /// passed by hundreds of functions must not emit hundreds of identical + /// tables. + DenseMap<Type *, FieldOffsets> ObjectOffsetsCache; + SanitizerCoverageOptions Options; const SpecialCaseList *Allowlist; @@ -458,6 +504,15 @@ bool ModuleSanitizerCoverage::instrumentModule() { SanCovTracePCGuard = M.getOrInsertFunction(SanCovTracePCGuardName, VoidTy, PtrTy); + // See the "Argument and return value tracing" section below for the meaning + // of the arguments. + // void __sanitizer_cov_trace_args(u64 tag, u64 val, const u64 *offsets) + SanCovTraceArgsFunc = M.getOrInsertFunction(SanCovTraceArgsName, VoidTy, + Int64Ty, Int64Ty, PtrTy); + // void __sanitizer_cov_trace_ret(u64 size, u64 val, const u64 *offsets) + SanCovTraceRetFunc = M.getOrInsertFunction(SanCovTraceRetName, VoidTy, + Int64Ty, Int64Ty, PtrTy); + SanCovStackDepthCallback = M.getOrInsertFunction(SanCovStackDepthCallbackName, VoidTy); @@ -672,6 +727,11 @@ void ModuleSanitizerCoverage::instrumentFunction(Function &F) { if (Options.CollectControlFlow) createFunctionControlFlow(F); + // Before InjectCoverage, which then puts its entry-block instrumentation + // ahead of the argument calls: allocas; pc_entry; trace_args. + if (Options.TraceArgs) + InjectTraceForArgs(F); + Value *FunctionGateCmp = nullptr; InjectCoverage(F, BlocksToInstrument, FunctionGateCmp, IsLeafFunc); InjectCoverageForIndirectCalls(F, IndirCalls); @@ -681,6 +741,10 @@ void ModuleSanitizerCoverage::instrumentFunction(Function &F) { InjectTraceForGep(F, GepTraceTargets); InjectTraceForLoadsAndStores(F, Loads, Stores); + // Before InjectTraceForExits, so that a return reads trace_ret; pc_exit; ret. + if (Options.TraceRet) + InjectTraceForRet(F); + if (Options.TracePCEntryExit) InjectTraceForExits(F); } @@ -1182,3 +1246,287 @@ void ModuleSanitizerCoverage::createFunctionControlFlow(Function &F) { ConstantArray::get(ArrayType::get(PtrTy, CFs.size()), CFs)); FunctionCFsArray->setConstant(true); } + +//===----------------------------------------------------------------------===// +// Argument and return value tracing. +// See clang/docs/SanitizerCoverage.md for the full design and callback +// contract: +// void __sanitizer_cov_trace_args(u64 tag, u64 val, const u64 *offsets); +// void __sanitizer_cov_trace_ret (u64 size, u64 val, const u64 *offsets); +//===----------------------------------------------------------------------===// + +ModuleSanitizerCoverage::FieldOffsets +ModuleSanitizerCoverage::getObjectOffsets(Type *Ty) { + // trace-args packs the size into the low 32 bits of its tag. + if (!Ty || !Ty->isSized() || DL->getTypeStoreSize(Ty).isScalable() || + DL->getTypeStoreSize(Ty).getFixedValue() > UINT32_MAX) + return {}; + auto [It, Inserted] = ObjectOffsetsCache.try_emplace(Ty); + if (!Inserted) + return It->second; + + // A struct is described by its elements, recursively; anything else as one + // field, or in 8-byte pieces if it is wider, so that a runtime reading every + // field into a 64-bit word reads all of the object. Element 0 is the number + // of pairs, filled in below. + SmallVector<Constant *, 17> Pairs = {nullptr}; + auto AddField = [&](uint64_t Offset, uint64_t Bytes) { + for (uint64_t Piece = 0; Piece < Bytes; Piece += sizeof(uint64_t)) { + Pairs.push_back(ConstantInt::get(Int64Ty, Offset + Piece)); + Pairs.push_back(ConstantInt::get( + Int64Ty, std::min<uint64_t>(Bytes - Piece, sizeof(uint64_t)))); + } + }; + auto AddType = [&](auto &Self, Type *T, uint64_t Offset) -> void { + if (auto *STy = dyn_cast<StructType>(T)) { + const StructLayout *Layout = DL->getStructLayout(STy); + for (unsigned I = 0, E = STy->getNumElements(); I != E; ++I) + Self(Self, STy->getElementType(I), + Offset + Layout->getElementOffset(I).getFixedValue()); + return; + } + AddField(Offset, DL->getTypeStoreSize(T).getFixedValue()); + }; + uint64_t Size = DL->getTypeStoreSize(Ty).getFixedValue(); + AddType(AddType, Ty, 0); + + if (Pairs.size() > 1) { + Pairs[0] = ConstantInt::get(Int64Ty, Pairs.size() / 2); + ArrayType *TableTy = ArrayType::get(Int64Ty, Pairs.size()); + auto *Table = new GlobalVariable( + M, TableTy, /*isConstant=*/true, GlobalVariable::PrivateLinkage, + ConstantArray::get(TableTy, Pairs), "__sancov_offsets_"); + Table->setUnnamedAddr(GlobalValue::UnnamedAddr::Global); + It->second = {Table, Size}; + } + return It->second; +} + +/// Widen \p V to the 64-bit value the callbacks take: a pointer is reported as +/// the address it holds, a floating-point value or a vector as its bit pattern, +/// and a value wider than 64 bits by its low 64 bits. Returns the value +/// together with the number of bytes of it that are meaningful, or +/// {nullptr, 0} for a type that has no fixed-size bit pattern, such as a +/// scalable vector. +static std::pair<Value *, uint64_t> getReportedValue(IRBuilderBase &IRB, + Value *V) { + const DataLayout &DL = IRB.GetInsertBlock()->getDataLayout(); + Type *Ty = V->getType(); + IntegerType *Int64Ty = IRB.getInt64Ty(); + + // The callback receives bits, not poison: an argument callers pass nothing + // for (dead argument elimination passes poison), or the padding of a struct + // coerced into an integer, is whatever it happens to be. This also keeps + // MemorySanitizer from reporting the instrumentation's own use of it. + if (!isGuaranteedNotToBeUndefOrPoison(V)) + V = IRB.CreateFreeze(V); + + // ptrtoint widens or narrows to the requested type, so this also holds for a + // target, or an address space, whose pointers are narrower than 64 bits. + if (Ty->isPointerTy()) + return {IRB.CreatePtrToInt(V, Int64Ty), + std::min<uint64_t>(DL.getPointerSize(Ty->getPointerAddressSpace()), + sizeof(uint64_t))}; + + // A vector of pointers has no bit pattern of its own; its addresses do. + if (auto *VTy = dyn_cast<FixedVectorType>(Ty); + VTy && VTy->getElementType()->isPointerTy()) { + V = IRB.CreatePtrToInt( + V, VectorType::get(DL.getIntPtrType(VTy->getElementType()), + VTy->getElementCount())); + Ty = V->getType(); + } + if (Ty->isFloatingPointTy() || isa<FixedVectorType>(Ty)) { + TypeSize Bits = Ty->getPrimitiveSizeInBits(); + if (Bits.isScalable() || !Bits.getFixedValue()) + return {nullptr, 0}; + V = IRB.CreateBitCast(V, IRB.getIntNTy(Bits.getFixedValue())); + Ty = V->getType(); + } + if (!Ty->isIntegerTy()) + return {nullptr, 0}; + + uint64_t Size = divideCeil(Ty->getIntegerBitWidth(), 8); + if (Size > sizeof(uint64_t)) + return {IRB.CreateTrunc(V, Int64Ty), sizeof(uint64_t)}; + return {IRB.CreateZExt(V, Int64Ty), Size}; +} + +void ModuleSanitizerCoverage::emitTraceCall(IRBuilderBase &IRB, + FunctionCallee Callee, + std::optional<uint32_t> ArgIdx, + uint64_t Size, Value *Val, + Value *Table) { + uint64_t Tag = ArgIdx ? ((uint64_t)*ArgIdx << 32) | Size : Size; + Value *ValArg = + Val ? (Val->getType()->isPointerTy() ? IRB.CreatePtrToInt(Val, Int64Ty) + : Val) + : ConstantInt::get(Int64Ty, 0); + Value *TableArg = Table ? Table : ConstantPointerNull::get(PtrTy); + + CallInst *CI = IRB.CreateCall( + Callee, {ConstantInt::get(Int64Ty, Tag), ValArg, TableArg}); + // An observation is identified by its call site, which the runtime reads as + // its own return address. A tail call would hand the callback the caller's + // return address instead, and a trace-ret call of a void function is in tail + // position. Merging identical calls would conflate distinct return sites. + CI->setTailCallKind(CallInst::TCK_NoTail); + CI->setCannotMerge(); +} + +void ModuleSanitizerCoverage::traceValue(IRBuilderBase &IRB, + FunctionCallee Callee, + std::optional<uint32_t> ArgIdx, + Value *V) { + // A first-class aggregate lives in registers and has no address, so each of + // its elements is reported on its own. + Type *Ty = V->getType(); + unsigned NumElements = Ty->isStructTy() ? Ty->getStructNumElements() + : Ty->isArrayTy() ? Ty->getArrayNumElements() + : 0; + if (NumElements) { + for (unsigned I = 0; I != NumElements; ++I) + traceValue(IRB, Callee, ArgIdx, IRB.CreateExtractValue(V, I)); + return; + } + + std::pair<Value *, uint64_t> Reported = getReportedValue(IRB, V); + emitTraceCall(IRB, Callee, ArgIdx, Reported.second, Reported.first); +} + +void ModuleSanitizerCoverage::traceObject(IRBuilderBase &IRB, + FunctionCallee Callee, + std::optional<uint32_t> ArgIdx, + Value *Addr, Type *Ty) { + FieldOffsets Offsets = getObjectOffsets(Ty); + emitTraceCall(IRB, Callee, ArgIdx, Offsets.ObjectSize, + Offsets.Table ? Addr : nullptr, Offsets.Table); +} + +uint64_t +ModuleSanitizerCoverage::getPointeeBytes(Value *Ptr, + uint64_t Dereferenceable) const { + // A pointer of another address space may not be readable through the + // generic address the runtime gets. + if (!Ptr->getType()->isPointerTy() || + Ptr->getType()->getPointerAddressSpace() != 0) + return 0; + return std::min<uint64_t>(Dereferenceable, + Opts.sanitizer_coverage_trace_pointee_bytes); +} + +void ModuleSanitizerCoverage::tracePointee(IRBuilderBase &IRB, + FunctionCallee Callee, + std::optional<uint32_t> ArgIdx, + Value *Ptr, uint64_t Bytes) { + // The IR says nothing about the pointee but how many bytes of it may be + // read, so they are read as bytes: the table of a byte array that long. + FieldOffsets Offsets = + getObjectOffsets(ArrayType::get(IRB.getInt8Ty(), Bytes)); + emitTraceCall(IRB, Callee, ArgIdx, Offsets.ObjectSize, Ptr, Offsets.Table); +} + +void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) { + // Static allocas stay first in the entry block. This runs before + // InjectCoverage, whose entry-block instrumentation (pc_entry, trace_pc, + // the guard or counter) is then inserted ahead of these calls. + BasicBlock &EntryBB = F.getEntryBlock(); + BasicBlock::iterator IP = + PrepareToSplitEntryBlock(EntryBB, EntryBB.getFirstInsertionPt()); + + // InstrumentationIRBuilder gives the calls a !dbg location, which the + // verifier requires of a call in a function with debug information once it + // can be inlined. The location is the function's own opening line rather + // than the one of the instruction the calls precede, which may belong to an + // inlined callee: the arguments are the function's own. Only the location + // depends on debug information, never the calls. + InstrumentationIRBuilder IRB(&*IP); + if (DISubprogram *SP = F.getSubprogram()) + IRB.SetCurrentDebugLocation( + DILocation::get(SP->getContext(), SP->getScopeLine(), 0, SP)); + + for (Argument &Arg : F.args()) { + // A struct-return pointer is where the return value goes, which trace-ret + // reports; the error slot of a Swift throwing function may only be loaded, + // stored, or passed on as a swifterror argument. Neither is observed, and + // neither renumbers the arguments that are: arg_idx stays the IR position. + if (Arg.hasStructRetAttr() || Arg.hasSwiftErrorAttr()) + continue; + // Callers may pass anything for an argument the function does not use: a + // value is still reported, but an object is not read. + bool Unused = Arg.use_empty(); + uint32_t ArgIdx = Arg.getArgNo(); + // An argument the ABI says points at a copy of the parameter in memory is + // that object, not a value. + // An object the function does not use may be passed as a poison pointer, + // by dead argument elimination in its callers: it is not handed to the + // runtime to read. + if (Type *MemTy = Arg.getPointeeInMemoryValueType()) { + traceObject(IRB, SanCovTraceArgsFunc, ArgIdx, Unused ? nullptr : &Arg, + Unused ? nullptr : MemTy); + } else if (uint64_t Bytes = + // Argument::getDereferenceableBytes() asserts on a + // non-pointer. Without noundef the pointer may be poison, + // which no freeze makes safe to read through. + Unused || !Arg.getType()->isPointerTy() || + !Arg.hasAttribute(Attribute::NoUndef) + ? 0 + : getPointeeBytes( + &Arg, std::max(Arg.getDereferenceableBytes(), + Arg.getDereferenceableOrNullBytes()))) { + // What the pointer points to, as far as the IR says it may be read: at + // the top of the entry block nothing can have freed it yet. + tracePointee(IRB, SanCovTraceArgsFunc, ArgIdx, &Arg, Bytes); + } else { + traceValue(IRB, SanCovTraceArgsFunc, ArgIdx, &Arg); + } + } +} + +void ModuleSanitizerCoverage::InjectTraceForRet(Function &F) { + // An indirect return leaves an IR function returning void and writes the + // returned object through a hidden struct-return pointer; that buffer is + // what is reported, so an indirect return is not dropped. + Argument *SRetArg = nullptr; + for (Argument &Arg : F.args()) + if (Arg.hasStructRetAttr()) { + SRetArg = &Arg; + break; + } + + // A function that never writes its sret buffer may be passed a poison one. + bool SRetUnused = SRetArg && SRetArg->use_empty(); + // A returned pointer the IR says may be read from is reported with what it + // points to: the return attribute holds where the function returns. + uint64_t RetPointeeBytes = + F.hasRetAttribute(Attribute::NoUndef) + ? std::max(F.getAttributes().getRetDereferenceableBytes(), + F.getAttributes().getRetDereferenceableOrNullBytes()) + : 0; + for (BasicBlock &BB : F) { + // Only a return carries a value to report; unwinding leaves the function + // without one. + auto *RI = dyn_cast<ReturnInst>(BB.getTerminator()); + if (!RI) + continue; + // A musttail call has to stay adjacent to the return that forwards it, so + // there is nowhere to put the call. + if (auto *CI = dyn_cast_or_null<CallInst>(RI->getPrevNode()); + CI && CI->isMustTailCall()) + continue; + + InstrumentationIRBuilder IRB(RI); + if (SRetArg) { + traceObject(IRB, SanCovTraceRetFunc, std::nullopt, + SRetUnused ? nullptr : SRetArg, + SRetUnused ? nullptr : SRetArg->getParamStructRetType()); + } else if (Value *RetVal = RI->getReturnValue()) { + if (uint64_t Bytes = getPointeeBytes(RetVal, RetPointeeBytes)) + tracePointee(IRB, SanCovTraceRetFunc, std::nullopt, RetVal, Bytes); + else + traceValue(IRB, SanCovTraceRetFunc, std::nullopt, RetVal); + } else { + emitTraceCall(IRB, SanCovTraceRetFunc, std::nullopt, 0, nullptr); // void + } + } +} diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance.ll new file mode 100644 index 00000000000000..3183cbfaca86f0 --- /dev/null +++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance.ll @@ -0,0 +1,137 @@ +; Debug information may enrich how an observation is interpreted, but must never +; decide what is observed: instrumenting this module and then stripping its debug +; information gives exactly the module that stripping first and instrumenting +; then gives. +; +; The input carries everything that could tempt the pass: source-level +; parameter records that disagree with the IR argument list (an sret pointer, a +; struct coerced into one register and split into two fragments, a first-class +; aggregate one of whose fragments was optimized out), a struct pointer with a +; known field layout, a #dbg_declare of a byval argument, an unoptimized +; function that stores its arguments to declared allocas, and an inlined callee +; with parameters of its own. +; +; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-pc-guard -sanitizer-coverage-trace-args -sanitizer-coverage-trace-ret \ +; RUN: | opt -strip-debug -S -o %t.instrumented-then-stripped.ll +; RUN: opt < %s -strip-debug \ +; RUN: | opt -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-pc-guard -sanitizer-coverage-trace-args -sanitizer-coverage-trace-ret -S -o %t.stripped-then-instrumented.ll +; RUN: diff %t.instrumented-then-stripped.ll %t.stripped-then-instrumented.ll + +; Debug information still gives the calls a location: the function's own +; opening line, even where the entry block starts with an inlined callee. +; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=1 -sanitizer-coverage-trace-args -S | FileCheck %s +; CHECK-LABEL: define i32 @caller( +; CHECK: call void @__sanitizer_cov_trace_args(i64 8, i64 %n, ptr null) #{{[0-9]+}}, !dbg ![[ENTRY:[0-9]+]] +; CHECK-NOT: call void @__sanitizer_cov_trace_args +; CHECK: ret i32 +; CHECK-DAG: ![[ENTRY]] = !DILocation(line: 10, scope: ![[CALLER:[0-9]+]]) +; CHECK-DAG: ![[CALLER]] = distinct !DISubprogram(name: "caller" + +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.S = type { i64, i64, i64 } + +; struct S make(struct pair p, struct S *q) with struct pair { int a, b; } +define void @make(ptr sret(%struct.S) %r, i64 noundef %p, ptr noundef %q) !dbg !10 { +entry: + %a = trunc i64 %p to i32 + %hi = lshr i64 %p, 32 + %b = trunc i64 %hi to i32 + #dbg_value(i32 %a, !13, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !15) + #dbg_value(i32 %b, !13, !DIExpression(DW_OP_LLVM_fragment, 32, 32), !15) + #dbg_value(ptr %q, !14, !DIExpression(), !15) + store i64 %p, ptr %r, !dbg !15 + ret void, !dbg !15 +} + +; long use(struct S s), passed in memory and described by a #dbg_declare. +define i64 @use(ptr byval(%struct.S) %s) !dbg !20 { +entry: + #dbg_declare(ptr %s, !21, !DIExpression(), !22) + %v = load i64, ptr %s, !dbg !22 + ret i64 %v, !dbg !22 +} + +; long caller(long n) { return leaf(n * 2); } with leaf(long m) inlined. +define i32 @caller(i64 noundef %n) !dbg !30 { +entry: + %m = shl i64 %n, 1, !dbg !34 + #dbg_value(i64 %n, !31, !DIExpression(), !33) + #dbg_value(i64 %m, !35, !DIExpression(), !34) + %t = trunc i64 %m to i32, !dbg !34 + ret i32 %t, !dbg !33 +} + +; long split(struct mix m, int z), with m passed as [2 x i64] (AArch64). +define i64 @split([2 x i64] noundef %m, i32 noundef %z) !dbg !50 { +entry: + %l = extractvalue [2 x i64] %m, 1 + #dbg_value(i64 poison, !51, !DIExpression(DW_OP_LLVM_fragment, 0, 64), !53) + #dbg_value(i64 %l, !51, !DIExpression(DW_OP_LLVM_fragment, 64, 64), !53) + #dbg_value(i32 %z, !52, !DIExpression(), !53) + %zl = sext i32 %z to i64 + %r = add i64 %l, %zl + ret i64 %r, !dbg !53 +} + +; The same at -O0, where the arguments are stored to declared allocas. +define i64 @split_o0([2 x i64] noundef %m, i32 noundef %z) !dbg !60 { +entry: + %m.addr = alloca [2 x i64], align 8 + %z.addr = alloca i32, align 4 + store [2 x i64] %m, ptr %m.addr, align 8 + #dbg_declare(ptr %m.addr, !61, !DIExpression(), !63) + store i32 %z, ptr %z.addr, align 4 + #dbg_declare(ptr %z.addr, !62, !DIExpression(), !63) + %p = getelementptr inbounds i8, ptr %m.addr, i64 8 + %l = load i64, ptr %p, align 8, !dbg !63 + ret i64 %l, !dbg !63 +} + +!llvm.dbg.cu = !{!0} +!llvm.module.flags = !{!2, !3} + +!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !1, isOptimized: true, emissionKind: FullDebug) +!1 = !DIFile(filename: "invariance.c", directory: "/") +!2 = !{i32 7, !"Dwarf Version", i32 5} +!3 = !{i32 2, !"Debug Info Version", i32 3} +!4 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed) +!5 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed) +!6 = !DICompositeType(tag: DW_TAG_structure_type, name: "S", file: !1, size: 192, elements: !7) +!7 = !{!40, !41, !42} +!8 = !DICompositeType(tag: DW_TAG_structure_type, name: "pair", file: !1, size: 64, elements: !{}) +!9 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !6, size: 64) +!10 = distinct !DISubprogram(name: "make", scope: !1, file: !1, line: 1, type: !11, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0) +!11 = !DISubroutineType(types: !12) +!12 = !{!6, !8, !9} +!13 = !DILocalVariable(name: "p", arg: 1, scope: !10, file: !1, line: 1, type: !8) +!14 = !DILocalVariable(name: "q", arg: 2, scope: !10, file: !1, line: 1, type: !9) +!15 = !DILocation(line: 1, column: 1, scope: !10) +!20 = distinct !DISubprogram(name: "use", scope: !1, file: !1, line: 5, type: !23, scopeLine: 5, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0) +!21 = !DILocalVariable(name: "s", arg: 1, scope: !20, file: !1, line: 5, type: !6) +!22 = !DILocation(line: 5, column: 1, scope: !20) +!23 = !DISubroutineType(types: !24) +!24 = !{!5, !6} +!30 = distinct !DISubprogram(name: "caller", scope: !1, file: !1, line: 10, type: !36, scopeLine: 10, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0) +!31 = !DILocalVariable(name: "n", arg: 1, scope: !30, file: !1, line: 10, type: !5) +!32 = distinct !DISubprogram(name: "leaf", scope: !1, file: !1, line: 8, type: !36, scopeLine: 8, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition | DISPFlagOptimized, unit: !0) +!33 = !DILocation(line: 10, column: 1, scope: !30) +!34 = !DILocation(line: 8, column: 1, scope: !32, inlinedAt: !33) +!35 = !DILocalVariable(name: "m", arg: 1, scope: !32, file: !1, line: 8, type: !5) +!36 = !DISubroutineType(types: !37) +!37 = !{!5, !5} +!40 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !6, file: !1, baseType: !5, size: 64) +!41 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !6, file: !1, baseType: !5, size: 64, offset: 64) +!42 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !6, file: !1, baseType: !5, size: 64, offset: 128) +!43 = !DICompositeType(tag: DW_TAG_structure_type, name: "mix", file: !1, size: 128, elements: !{}) +!44 = !DISubroutineType(types: !45) +!45 = !{!5, !43, !4} +!50 = distinct !DISubprogram(name: "split", scope: !1, file: !1, line: 20, type: !44, scopeLine: 20, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0) +!51 = !DILocalVariable(name: "m", arg: 1, scope: !50, file: !1, line: 20, type: !43) +!52 = !DILocalVariable(name: "z", arg: 2, scope: !50, file: !1, line: 20, type: !4) +!53 = !DILocation(line: 20, column: 1, scope: !50) +!60 = distinct !DISubprogram(name: "split_o0", scope: !1, file: !1, line: 30, type: !44, scopeLine: 30, spFlags: DISPFlagDefinition, unit: !0) +!61 = !DILocalVariable(name: "m", arg: 1, scope: !60, file: !1, line: 30, type: !43) +!62 = !DILocalVariable(name: "z", arg: 2, scope: !60, file: !1, line: 30, type: !4) +!63 = !DILocation(line: 30, column: 1, scope: !60) diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll new file mode 100644 index 00000000000000..f6e42b40b54194 --- /dev/null +++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll @@ -0,0 +1,279 @@ +; Argument tracing: one __sanitizer_cov_trace_args call per IR argument, in the +; entry block, decided by the IR alone. arg_idx is the position of the argument +; in the IR parameter list; a struct-return pointer and a Swift error slot are +; not observed but keep their positions. +; +; A value that fits in a register is reported as the value; an argument the ABI +; says points at a copy of the parameter in memory is reported as that object, +; with a table of the elements of its IR type, none wider than 8 bytes. A +; pointer the IR says is noundef and dereferenceable is reported with a raw snapshot of the +; bytes it points to, as many as the attribute says and +; -sanitizer-coverage-trace-pointee-bytes allows. +; +; An argument the function does not use is passed whatever its callers pass - +; poison, after dead argument elimination - so an object is not read through +; it; the functions below use theirs, and @unused checks the rule. +; +; opt runs the verifier, so a passing run also proves the emitted IR is +; well-formed. +; +; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=1 -sanitizer-coverage-trace-args -S | FileCheck %s +; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=1 -sanitizer-coverage-trace-args \ +; RUN: -sanitizer-coverage-trace-pointee-bytes=0 -S | FileCheck %s --check-prefix=NOPTEE +; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=1 -sanitizer-coverage-trace-args \ +; RUN: -sanitizer-coverage-trace-ret -sanitizer-coverage-trace-pc-entry-exit -S | FileCheck %s --check-prefix=ENTRY + +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" + +declare void @use(...) + +%struct.T = type { i32, i8, i64 } + +; One table per IR type, shared by every object of that type, and one for an +; object that is not a struct. +; CHECK: @__sancov_offsets_ = private unnamed_addr constant [7 x i64] [i64 3, i64 0, i64 4, i64 4, i64 1, i64 8, i64 8] +; CHECK: @__sancov_offsets_.1 = private unnamed_addr constant [3 x i64] [i64 1, i64 0, i64 8] +; A pointee snapshot is a byte array: 16, 64 and 12 bytes, in 8-byte pieces. +; CHECK: @__sancov_offsets_.2 = private unnamed_addr constant [5 x i64] [i64 2, i64 0, i64 8, i64 8, i64 8] +; CHECK: @__sancov_offsets_.3 = private unnamed_addr constant [17 x i64] [i64 8, i64 0, i64 8, i64 8, i64 8, i64 16, i64 8, i64 24, i64 8, i64 32, i64 8, i64 40, i64 8, i64 48, i64 8, i64 56, i64 8] +; CHECK: @__sancov_offsets_.4 = private unnamed_addr constant [5 x i64] [i64 2, i64 0, i64 8, i64 8, i64 4] +; An opaque [32 x i8] object, in 8-byte pieces; a struct by the elements of its +; nested structs, and an element wider than 8 bytes in pieces. +; CHECK: @__sancov_offsets_.5 = private unnamed_addr constant [9 x i64] [i64 4, i64 0, i64 8, i64 8, i64 8, i64 16, i64 8, i64 24, i64 8] +; CHECK: @__sancov_offsets_.6 = private unnamed_addr constant [11 x i64] [i64 5, i64 0, i64 4, i64 8, i64 1, i64 16, i64 8, i64 24, i64 8, i64 32, i64 4] +; CHECK: @__sancov_offsets_.7 = private unnamed_addr constant [7 x i64] [i64 3, i64 0, i64 8, i64 8, i64 8, i64 16, i64 1] +; CHECK-NOT: @__sancov_offsets_ + +; Scalars and pointers are reported as their value, zero-extended to 64 bits. +define void @scalars(i1 noundef %b, i8 noundef %c, i32 noundef %i, i64 noundef %l, ptr noundef %p) { +entry: + call void (...) @use(i1 %b, i8 %c, i32 %i, i64 %l, ptr %p) + ret void +} +; CHECK-LABEL: define void @scalars( +; CHECK-NOT: alloca +; CHECK: %[[B:[0-9]+]] = zext i1 %b to i64 +; CHECK: call void @__sanitizer_cov_trace_args(i64 1, i64 %[[B]], ptr null) +; CHECK: %[[C:[0-9]+]] = zext i8 %c to i64 +; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967297, i64 %[[C]], ptr null) +; CHECK: %[[I:[0-9]+]] = zext i32 %i to i64 +; CHECK: call void @__sanitizer_cov_trace_args(i64 8589934596, i64 %[[I]], ptr null) +; CHECK: call void @__sanitizer_cov_trace_args(i64 12884901896, i64 %l, ptr null) +; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64 +; CHECK: call void @__sanitizer_cov_trace_args(i64 17179869192, i64 %[[P]], ptr null) + +; A floating-point value or a vector is reported as its bit pattern, a value +; wider than 64 bits by its low 8 bytes, a pointer with the width of its address +; space, and a value with no fixed-size bit pattern with size 0. +define void @wide(float noundef %f, double noundef %d, i128 noundef %w, ptr addrspace(270) noundef %p32, <4 x i32> noundef %v, <2 x float> noundef %v2, <2 x ptr> noundef %vp, <vscale x 2 x i64> noundef %sv) { +entry: + call void (...) @use(float %f, double %d, i128 %w, ptr addrspace(270) %p32, <4 x i32> %v, <2 x float> %v2, <2 x ptr> %vp, <vscale x 2 x i64> %sv) + ret void +} +; CHECK-LABEL: define void @wide( +; CHECK: %[[F:[0-9]+]] = bitcast float %f to i32 +; CHECK: %[[FZ:[0-9]+]] = zext i32 %[[F]] to i64 +; CHECK: call void @__sanitizer_cov_trace_args(i64 4, i64 %[[FZ]], ptr null) +; CHECK: %[[D:[0-9]+]] = bitcast double %d to i64 +; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967304, i64 %[[D]], ptr null) +; CHECK: %[[W:[0-9]+]] = trunc i128 %w to i64 +; CHECK: call void @__sanitizer_cov_trace_args(i64 8589934600, i64 %[[W]], ptr null) +; CHECK: call void @__sanitizer_cov_trace_args(i64 12884901892, +; CHECK: %[[V:[0-9]+]] = bitcast <4 x i32> %v to i128 +; CHECK: %[[VT:[0-9]+]] = trunc i128 %[[V]] to i64 +; CHECK: call void @__sanitizer_cov_trace_args(i64 17179869192, i64 %[[VT]], ptr null) +; CHECK: %[[V2:[0-9]+]] = bitcast <2 x float> %v2 to i64 +; CHECK: call void @__sanitizer_cov_trace_args(i64 21474836488, i64 %[[V2]], ptr null) +; CHECK: %[[VP:[0-9]+]] = ptrtoint <2 x ptr> %vp to <2 x i64> +; CHECK: %[[VPB:[0-9]+]] = bitcast <2 x i64> %[[VP]] to i128 +; CHECK: %[[VPT:[0-9]+]] = trunc i128 %[[VPB]] to i64 +; CHECK: call void @__sanitizer_cov_trace_args(i64 25769803784, i64 %[[VPT]], ptr null) +; CHECK: call void @__sanitizer_cov_trace_args(i64 30064771072, i64 0, ptr null) + +; A first-class aggregate lives in registers and has no address: each element +; is reported on its own, under the same arg_idx. +define void @aggregate({ i32, i64 } noundef %s, [2 x i8] noundef %a) { +entry: + call void (...) @use({ i32, i64 } %s, [2 x i8] %a) + ret void +} +; CHECK-LABEL: define void @aggregate( +; CHECK: call void @__sanitizer_cov_trace_args(i64 4, +; CHECK: call void @__sanitizer_cov_trace_args(i64 8, +; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967297, +; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967297, + +; A nested first-class aggregate unwinds recursively; each leaf element is +; reported under the same arg_idx. +define void @nested_aggregate({ i64, { i32, i32 } } noundef %s) { +entry: + call void (...) @use({ i64, { i32, i32 } } %s) + ret void +} +; CHECK-LABEL: define void @nested_aggregate( +; CHECK: %[[E0:[0-9]+]] = extractvalue { i64, { i32, i32 } } %s, 0 +; CHECK: call void @__sanitizer_cov_trace_args(i64 8, i64 %[[E0]], ptr null) +; CHECK: %[[E1:[0-9]+]] = extractvalue { i64, { i32, i32 } } %s, 1 +; CHECK: %[[E1_0:[0-9]+]] = extractvalue { i32, i32 } %[[E1]], 0 +; CHECK: %[[E1_0_EXT:[0-9]+]] = zext i32 %[[E1_0]] to i64 +; CHECK: call void @__sanitizer_cov_trace_args(i64 4, i64 %[[E1_0_EXT]], ptr null) +; CHECK: %[[E1_1:[0-9]+]] = extractvalue { i32, i32 } %[[E1]], 1 +; CHECK: %[[E1_1_EXT:[0-9]+]] = zext i32 %[[E1_1]] to i64 +; CHECK: call void @__sanitizer_cov_trace_args(i64 4, i64 %[[E1_1_EXT]], ptr null) + +; byval, inalloca, preallocated and byref arguments are the address of a copy +; of the parameter: they are reported as that object, described by the IR type. +define void @objects(ptr byval(%struct.T) %a, ptr preallocated(i64) %b, ptr byref(%struct.T) %c) { +entry: + call void (...) @use(ptr %a, ptr %b, ptr %c) + ret void +} +; CHECK-LABEL: define void @objects( +; CHECK: %[[A:[0-9]+]] = ptrtoint ptr %a to i64 +; CHECK: call void @__sanitizer_cov_trace_args(i64 16, i64 %[[A]], ptr @__sancov_offsets_) +; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967304, i64 %{{[0-9]+}}, ptr @__sancov_offsets_.1) +; CHECK: call void @__sanitizer_cov_trace_args(i64 8589934608, i64 %{{[0-9]+}}, ptr @__sancov_offsets_) + +; inalloca has to be the last parameter. +define void @inalloca(i32 noundef %x, ptr inalloca(%struct.T) %a) { +entry: + call void (...) @use(i32 %x, ptr %a) + ret void +} +; CHECK-LABEL: define void @inalloca( +; CHECK: call void @__sanitizer_cov_trace_args(i64 4, +; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967312, i64 %{{[0-9]+}}, ptr @__sancov_offsets_) + +; A struct-return pointer is where the return value goes, which trace-ret +; reports. It is not observed, but %x keeps its IR position, 1. +define void @sret(ptr sret(%struct.T) %r, i32 noundef %x) { +entry: + call void (...) @use(ptr %r, i32 %x) + ret void +} +; CHECK-LABEL: define void @sret( +; CHECK-NOT: ptrtoint ptr %r +; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967300, +; CHECK-NOT: call void @__sanitizer_cov_trace_args( + +; The error slot of a Swift throwing function may only be loaded, stored, or +; passed on as a swifterror argument: it is skipped, and %x keeps index 2. +define swiftcc void @swifterror(ptr noundef swiftself %self, ptr swifterror %err, i64 noundef %x) { +entry: + call void (...) @use(ptr %self, i64 %x) + ret void +} +; CHECK-LABEL: define swiftcc void @swifterror( +; CHECK-NOT: ptrtoint ptr %err +; CHECK: call void @__sanitizer_cov_trace_args(i64 8, +; CHECK: call void @__sanitizer_cov_trace_args(i64 8589934600, i64 %x, ptr null) +; CHECK-NOT: call void @__sanitizer_cov_trace_args( + +; The named arguments of a variadic function are reported; the variadic ones +; have no IR argument. +define void @varargs(i32 noundef %n, ...) { +entry: + call void (...) @use(i32 %n) + ret void +} +; CHECK-LABEL: define void @varargs( +; CHECK: call void @__sanitizer_cov_trace_args(i64 4, +; CHECK-NOT: call void @__sanitizer_cov_trace_args( + +; The calls are first in the entry block and are notail: the runtime identifies +; an observation by its call's return address. +define i32 @notail(i32 noundef %x) { +entry: + ret i32 %x +} +; CHECK-LABEL: define i32 @notail( +; CHECK-NEXT: entry: +; CHECK-NEXT: %[[X:[0-9]+]] = zext i32 %x to i64 +; CHECK-NEXT: notail call void @__sanitizer_cov_trace_args(i64 4, i64 %[[X]], ptr null) +; CHECK-NEXT: ret i32 %x + +; An unused argument may be passed anything: an unused value is still +; reported and frozen, but an unused object is not read, as its pointer may +; be poison. A value that may be poison - here +; because none of the arguments is noundef - is frozen, so that the callback +; receives bits. +define void @unused(i32 %x, ptr byval(%struct.T) %o, { i32, i64 } %s) { +entry: + ret void +} +; CHECK-LABEL: define void @unused( +; CHECK-NEXT: entry: +; CHECK-NEXT: %[[XF:[0-9]+]] = freeze i32 %x +; CHECK-NEXT: %[[X:[0-9]+]] = zext i32 %[[XF]] to i64 +; CHECK-NEXT: notail call void @__sanitizer_cov_trace_args(i64 4, i64 %[[X]], ptr null) +; CHECK-NEXT: notail call void @__sanitizer_cov_trace_args(i64 4294967296, i64 0, ptr null) +; CHECK: notail call void @__sanitizer_cov_trace_args(i64 8589934596, +; CHECK: notail call void @__sanitizer_cov_trace_args(i64 8589934600, +; CHECK-NEXT: ret void + +; A pointer the IR says is noundef and dereferenceable(N) is reported with the +; first N bytes it points to, at most -sanitizer-coverage-trace-pointee-bytes (64 +; by default): `val` stays the pointer, `size` is the number of bytes and the +; table covers them. dereferenceable_or_null may be null, which the runtime +; checks. A pointer with no such attribute, one without noundef, one in another +; address space, and one the function does not use are reported by value only. +define void @pointee(ptr noundef nonnull align 8 dereferenceable(16) %p, ptr noundef dereferenceable_or_null(100) %q, ptr noundef %raw, ptr addrspace(1) dereferenceable(16) %as1, ptr noundef dereferenceable(12) %r, ptr dereferenceable(8) %no_noundef, ptr dereferenceable(8) %dead) { +entry: + call void (...) @use(ptr %p, ptr %q, ptr %raw, ptr addrspace(1) %as1, ptr %r, ptr %no_noundef) + ret void +} +; CHECK-LABEL: define void @pointee( +; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64 +; CHECK: call void @__sanitizer_cov_trace_args(i64 16, i64 %[[P]], ptr @__sancov_offsets_.2) +; CHECK: %[[Q:[0-9]+]] = ptrtoint ptr %q to i64 +; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967360, i64 %[[Q]], ptr @__sancov_offsets_.3) +; CHECK: call void @__sanitizer_cov_trace_args(i64 8589934600, i64 %{{[0-9]+}}, ptr null) +; CHECK: call void @__sanitizer_cov_trace_args(i64 12884901896, i64 %{{[0-9]+}}, ptr null) +; CHECK: call void @__sanitizer_cov_trace_args(i64 17179869196, i64 %{{[0-9]+}}, ptr @__sancov_offsets_.4) +; CHECK: call void @__sanitizer_cov_trace_args(i64 21474836488, i64 %{{[0-9]+}}, ptr null) +; CHECK: call void @__sanitizer_cov_trace_args(i64 25769803784, i64 %{{[0-9]+}}, ptr null) +; NOPTEE-LABEL: define void @pointee( +; NOPTEE-COUNT-7: call void @__sanitizer_cov_trace_args(i64 {{[0-9]+}}, i64 %{{[0-9]+}}, ptr null) + +; An object whose IR type is an opaque byte array, as rustc passes and returns +; one, is read in 8-byte pieces rather than by its first 8 bytes only; so is an +; element wider than 8 bytes, and a nested struct is described by its elements. +define void @wide_objects(ptr byval([32 x i8]) %o, ptr byval({ i32, { i8, i64 }, [3 x i32] }) %n, ptr byval({ i128, i8 }) %w) { +entry: + call void (...) @use(ptr %o, ptr %n, ptr %w) + ret void +} +; CHECK-LABEL: define void @wide_objects( +; CHECK: call void @__sanitizer_cov_trace_args(i64 32, i64 %{{[0-9]+}}, ptr @__sancov_offsets_.5) +; CHECK: call void @__sanitizer_cov_trace_args(i64 4294967336, i64 %{{[0-9]+}}, ptr @__sancov_offsets_.6) +; CHECK: call void @__sanitizer_cov_trace_args(i64 8589934624, i64 %{{[0-9]+}}, ptr @__sancov_offsets_.7) + +; Static allocas remain first in the entry block. With trace-pc-entry-exit, +; pc_entry precedes trace_args at the entry and trace_ret precedes pc_exit at a +; return. nounwind keeps trace-pc-entry-exit from turning the calls into +; invokes. +define void @entry_alloca(i32 noundef %x) nounwind { +entry: + %a = alloca i32 + call void (...) @use(ptr %a, i32 %x) + ret void +} +; CHECK-LABEL: define void @entry_alloca( +; CHECK-NEXT: entry: +; CHECK-NEXT: %a = alloca i32 +; CHECK-NEXT: %[[X:[0-9]+]] = zext i32 %x to i64 +; CHECK-NEXT: notail call void @__sanitizer_cov_trace_args(i64 4, i64 %[[X]], ptr null) +; CHECK-NEXT: call void (...) @use(ptr %a, i32 %x) +; CHECK-NEXT: ret void + +; ENTRY-LABEL: define void @entry_alloca( +; ENTRY-NEXT: entry: +; ENTRY-NEXT: %a = alloca i32 +; ENTRY-NEXT: call void @__sanitizer_cov_trace_pc_entry() +; ENTRY-NEXT: %[[X:[0-9]+]] = zext i32 %x to i64 +; ENTRY-NEXT: notail call void @__sanitizer_cov_trace_args(i64 4, i64 %[[X]], ptr null) +; ENTRY-NEXT: call void (...) @use(ptr %a, i32 %x) +; ENTRY-NEXT: notail call void @__sanitizer_cov_trace_ret(i64 0, i64 0, ptr null) +; ENTRY-NEXT: notail call void @__sanitizer_cov_trace_pc_exit() +; ENTRY-NEXT: ret void diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll new file mode 100644 index 00000000000000..a0d52dec283f82 --- /dev/null +++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll @@ -0,0 +1,148 @@ +; Return value tracing: one __sanitizer_cov_trace_ret call before every return, +; decided by the IR alone. A value that fits in a register is reported as the +; value, a first-class aggregate as its elements, and an indirect (sret) return +; as the buffer the returned object was written to. +; +; opt runs the verifier, so a passing run also proves the emitted IR is +; well-formed. +; +; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=1 -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.S = type { i64, i64, i64 } + +; CHECK: @__sancov_offsets_ = private unnamed_addr constant [5 x i64] [i64 2, i64 0, i64 8, i64 8, i64 4] +; CHECK: @__sancov_offsets_.1 = private unnamed_addr constant [7 x i64] [i64 3, i64 0, i64 8, i64 8, i64 8, i64 16, i64 8] +; CHECK: @__sancov_offsets_.2 = private unnamed_addr constant [9 x i64] [i64 4, i64 0, i64 8, i64 8, i64 8, i64 16, i64 8, i64 24, i64 8] +; CHECK-NOT: @__sancov_offsets_ + +define i32 @scalar(i32 noundef %x) { +entry: + ret i32 %x +} +; CHECK-LABEL: define i32 @scalar( +; CHECK: %[[X:[0-9]+]] = zext i32 %x to i64 +; CHECK-NEXT: call void @__sanitizer_cov_trace_ret(i64 4, i64 %[[X]], ptr null) +; CHECK-NEXT: ret i32 %x + +; A pointer is reported as the address it holds. +define ptr @pointer(ptr noundef %p) { +entry: + ret ptr %p +} +; CHECK-LABEL: define ptr @pointer( +; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64 +; CHECK: call void @__sanitizer_cov_trace_ret(i64 8, i64 %[[P]], ptr null) + +; A pointer the return attributes say is dereferenceable at the return is +; reported with the bytes it points to, as rustc returns a reference: `val` is +; the pointer, `size` the 12 bytes read. +define noundef nonnull align 4 dereferenceable(12) ptr @reference(ptr noundef %p) { +entry: + ret ptr %p +} +; CHECK-LABEL: define noundef nonnull align 4 dereferenceable(12) ptr @reference( +; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64 +; CHECK-NEXT: call void @__sanitizer_cov_trace_ret(i64 12, i64 %[[P]], ptr @__sancov_offsets_) +; CHECK-NEXT: ret ptr %p + +; Every return is reported, each with its own value. +define i64 @multi(i1 noundef %c, i64 noundef %x) { +entry: + br i1 %c, label %a, label %b +a: + ret i64 %x +b: + ret i64 7 +} +; CHECK-LABEL: define i64 @multi( +; CHECK: call void @__sanitizer_cov_trace_ret(i64 8, i64 %x, ptr null) +; CHECK-NEXT: ret i64 %x +; CHECK: call void @__sanitizer_cov_trace_ret(i64 8, i64 7, ptr null) +; CHECK-NEXT: ret i64 7 + +; A struct returned in registers is reported as its elements. +define { i64, double } @aggregate(i64 noundef %a, double noundef %b) { +entry: + %0 = insertvalue { i64, double } poison, i64 %a, 0 + %1 = insertvalue { i64, double } %0, double %b, 1 + ret { i64, double } %1 +} +; CHECK-LABEL: define { i64, double } @aggregate( +; CHECK: call void @__sanitizer_cov_trace_ret(i64 8, +; CHECK: call void @__sanitizer_cov_trace_ret(i64 8, +; CHECK-NEXT: ret { i64, double } + +; An indirect return is reported as the buffer the returned object was written +; to, described by the elements of its IR type. +define void @indirect(ptr sret(%struct.S) %r, i64 noundef %x) { +entry: + store i64 %x, ptr %r + ret void +} +; CHECK-LABEL: define void @indirect( +; CHECK: %[[R:[0-9]+]] = ptrtoint ptr %r to i64 +; CHECK-NEXT: call void @__sanitizer_cov_trace_ret(i64 24, i64 %[[R]], ptr @__sancov_offsets_.1) +; CHECK-NEXT: ret void + +; rustc types the sret buffer of a Rust-ABI function as an opaque byte array: +; it is read in 8-byte pieces, all 32 bytes of it. +define void @opaque_indirect(ptr sret([32 x i8]) %r, i64 noundef %x) { +entry: + store i64 %x, ptr %r + ret void +} +; CHECK-LABEL: define void @opaque_indirect( +; CHECK: call void @__sanitizer_cov_trace_ret(i64 32, i64 %{{[0-9]+}}, ptr @__sancov_offsets_.2) + +; A void return has nothing to report, and says so with size 0. The call is in +; tail position, but must not become a tail call: the runtime identifies an +; observation by its call's return address. +define void @void() { +entry: + ret void +} +; CHECK-LABEL: define void @void( +; CHECK: notail call void @__sanitizer_cov_trace_ret(i64 0, i64 0, ptr null) +; CHECK-NEXT: ret void + +; A vector is reported as its bit pattern. +define <2 x i32> @vector(<2 x i32> noundef %v) { +entry: + ret <2 x i32> %v +} +; CHECK-LABEL: define <2 x i32> @vector( +; CHECK: %[[V:[0-9]+]] = bitcast <2 x i32> %v to i64 +; CHECK-NEXT: notail call void @__sanitizer_cov_trace_ret(i64 8, i64 %[[V]], ptr null) + +; A musttail call has to stay adjacent to the return that forwards it, so that +; return is left alone. +define i64 @forward(i64 noundef %x) { +entry: + %r = musttail call i64 @forward(i64 %x) + ret i64 %r +} +; CHECK-LABEL: define i64 @forward( +; CHECK-NOT: call void @__sanitizer_cov_trace_ret +; CHECK: musttail call i64 @forward +; CHECK-NEXT: ret i64 %r + +; Unwinding leaves the function without a value. +declare void @may_throw() +declare i32 @__gxx_personality_v0(...) +define i32 @unwinds(i32 noundef %x) personality ptr @__gxx_personality_v0 { +entry: + invoke void @may_throw() to label %ok unwind label %lp +ok: + ret i32 %x +lp: + %l = landingpad { ptr, i32 } cleanup + resume { ptr, i32 } %l +} +; CHECK-LABEL: define i32 @unwinds( +; CHECK: call void @__sanitizer_cov_trace_ret(i64 4, +; CHECK-NEXT: ret i32 %x +; CHECK-NOT: call void @__sanitizer_cov_trace_ret +; CHECK: resume >From a9eeae8a1335252391b48733f8a4ba7aa0d0a0fb Mon Sep 17 00:00:00 2001 From: Yunseong Kim <[email protected]> Date: Sun, 27 Sep 2026 21:14:06 +0200 Subject: [PATCH 2/4] [Clang][SanitizerCoverage] Add -fsanitize-coverage=trace-args,trace-ret Accept trace-args and trace-ret in -fsanitize-coverage= and forward them to SanitizerCoverageOptions::TraceArgs/TraceRet through the new cc1 flags -fsanitize-coverage-trace-args and -fsanitize-coverage-trace-ret. Like the other instrumentation types, they satisfy the "requires an instrumentation type" check and, without a coverage level, select the edge level. That level alone adds no block callbacks, so -fsanitize-coverage=trace-args emits argument tracing only. hasSanitizeCoverage() now accounts for both flags, so either one on its own runs the pass. Assisted-by: Kiro CLI (Claude Opus 5.5) Signed-off-by: Yunseong Kim <[email protected]> --- 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 | 43 +++++++++++++++++++ clang/test/Driver/fsanitize-coverage.c | 13 ++++++ 7 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c diff --git a/clang/include/clang/Basic/CodeGenOptions.def b/clang/include/clang/Basic/CodeGenOptions.def index c8ad0eb3f302e3..277fc5f69841e1 100644 --- a/clang/include/clang/Basic/CodeGenOptions.def +++ b/clang/include/clang/Basic/CodeGenOptions.def @@ -330,6 +330,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 5e969a4309bfbd..c2ef8ded0cca65 100644 --- a/clang/include/clang/Basic/CodeGenOptions.h +++ b/clang/include/clang/Basic/CodeGenOptions.h @@ -730,7 +730,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 4419e3cf6f98c5..0014c45244bce2 100644 --- a/clang/include/clang/Options/Options.td +++ b/clang/include/clang/Options/Options.td @@ -8428,6 +8428,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 fe1454026f569d..783b6634786ce2 100644 --- a/clang/lib/CodeGen/BackendUtil.cpp +++ b/clang/lib/CodeGen/BackendUtil.cpp @@ -269,6 +269,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 a1bfa90e5dceb2..4b189a0f143292 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) @@ -1477,6 +1481,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) @@ -1867,6 +1873,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 00000000000000..ad880544b0b388 --- /dev/null +++ b/clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c @@ -0,0 +1,43 @@ +// Check that -fsanitize-coverage-trace-args and -fsanitize-coverage-trace-ret +// reach the SanitizerCoverage pass and emit their callbacks, and that debug +// information does not change what they emit. +// +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -O1 -debug-info-kind=limited \ +// RUN: -fsanitize-coverage-type=3 -fsanitize-coverage-trace-args %s -o - \ +// RUN: | FileCheck %s --check-prefix=ARGS +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -O1 \ +// RUN: -fsanitize-coverage-type=3 -fsanitize-coverage-trace-args %s -o - \ +// RUN: | FileCheck %s --check-prefix=ARGS +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -O1 -debug-info-kind=limited \ +// RUN: -fsanitize-coverage-type=3 -fsanitize-coverage-trace-ret %s -o - \ +// RUN: | FileCheck %s --check-prefix=RET +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -O1 -debug-info-kind=limited \ +// RUN: -fsanitize-coverage-type=3 %s -o - | FileCheck %s --check-prefix=NONE + +struct Foo { + int a; + long b; +}; + +void takes_struct_ptr(struct Foo *f) {} + +int returns_scalar(int x) { return x + 1; } + +// A pointer is an IR value like any other: it is reported as the address it +// holds, and the struct it points at is not read. Whether debug information +// describes that struct makes no difference. Nothing is spilled. +// ARGS-NOT: @__sancov_offsets_ +// ARGS-LABEL: define {{.*}} @takes_struct_ptr( +// ARGS-NOT: alloca +// ARGS: %[[ADDR:.*]] = ptrtoint ptr %f to i64 +// ARGS: call void @__sanitizer_cov_trace_args(i64 8, i64 %[[ADDR]], ptr null) +// ARGS-LABEL: define {{.*}} @returns_scalar( +// ARGS: call void @__sanitizer_cov_trace_args(i64 4, +// ARGS-NOT: call void @__sanitizer_cov_trace_ret( + +// RET-LABEL: define {{.*}} @returns_scalar( +// RET: call void @__sanitizer_cov_trace_ret(i64 4, +// RET-NOT: call void @__sanitizer_cov_trace_args( + +// NONE-NOT: call void @__sanitizer_cov_trace_args( +// NONE-NOT: call void @__sanitizer_cov_trace_ret( diff --git a/clang/test/Driver/fsanitize-coverage.c b/clang/test/Driver/fsanitize-coverage.c index 21e2c16bfb1b74..9a0c74bc3dda92 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-TRACE-ARGS +// CHECK-TRACE-ARGS: -fsanitize-coverage-type=3 +// CHECK-TRACE-ARGS: -fsanitize-coverage-trace-args + +// RUN: %clang --target=x86_64-linux-gnu -fsanitize-coverage=trace-ret %s -### 2>&1 | FileCheck %s --check-prefix=CHECK-TRACE-RET +// CHECK-TRACE-RET: -fsanitize-coverage-type=3 +// CHECK-TRACE-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-TRACE-ARGS-RET +// CHECK-TRACE-ARGS-RET: -fsanitize-coverage-type=3 +// CHECK-TRACE-ARGS-RET: -fsanitize-coverage-trace-args +// CHECK-TRACE-ARGS-RET: -fsanitize-coverage-trace-ret >From 7fb57e6e5fb887b17422ab0d43387d28952392f0 Mon Sep 17 00:00:00 2001 From: Yunseong Kim <[email protected]> Date: Sun, 27 Sep 2026 21:18:19 +0200 Subject: [PATCH 3/4] [compiler-rt][SanitizerCoverage] Add weak trace-args/trace-ret callbacks Add weak, empty definitions of __sanitizer_cov_trace_args and __sanitizer_cov_trace_ret to sanitizer_common, and declare them in sanitizer_interface_internal.h with their contract. A program built with -fsanitize-coverage=trace-args,trace-ret then links against a sanitizer runtime without supplying the callbacks itself. A strong definition overrides the defaults. The new test overrides both callbacks and checks the reported argument and return values at -O2 -g and at -O0. Assisted-by: Kiro CLI (Claude Opus 5.5) Signed-off-by: Yunseong Kim <[email protected]> --- .../sanitizer_coverage_interface.inc | 2 + .../sanitizer_coverage_libcdep_new.cpp | 7 +++ .../sanitizer_interface_internal.h | 27 ++++++++++ .../sanitizer_coverage_trace_args.cpp | 53 +++++++++++++++++++ 4 files changed, 89 insertions(+) create mode 100644 compiler-rt/test/sanitizer_common/TestCases/sanitizer_coverage_trace_args.cpp diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_coverage_interface.inc b/compiler-rt/lib/sanitizer_common/sanitizer_coverage_interface.inc index 9d36a40270d5a2..ff7d971bfe51e3 100644 --- a/compiler-rt/lib/sanitizer_common/sanitizer_coverage_interface.inc +++ b/compiler-rt/lib/sanitizer_common/sanitizer_coverage_interface.inc @@ -38,6 +38,8 @@ INTERFACE_WEAK_FUNCTION(__sanitizer_cov_store4) INTERFACE_WEAK_FUNCTION(__sanitizer_cov_store8) INTERFACE_WEAK_FUNCTION(__sanitizer_cov_store16) INTERFACE_WEAK_FUNCTION(__sanitizer_cov_trace_switch) +INTERFACE_WEAK_FUNCTION(__sanitizer_cov_trace_args) +INTERFACE_WEAK_FUNCTION(__sanitizer_cov_trace_ret) INTERFACE_WEAK_FUNCTION(__sanitizer_cov_8bit_counters_init) INTERFACE_WEAK_FUNCTION(__sanitizer_cov_bool_flag_init) INTERFACE_WEAK_FUNCTION(__sanitizer_cov_pcs_init) diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp b/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp index 2f5b6f68afa413..963585c06dbd09 100644 --- a/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp +++ b/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp @@ -257,6 +257,13 @@ SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_const_cmp2, void) {} SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_const_cmp4, void) {} SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_const_cmp8, void) {} SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_switch, void) {} +// Argument and return value tracing (trace-args / trace-ret). Weak, empty +// defaults so that a program built with -fsanitize-coverage=trace-args and +// trace-ret links without a runtime consuming the values; libFuzzer, or the +// user, provides a strong definition. Such a definition must tolerate a size of +// zero, which the instrumentation passes for a value it cannot report. +SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_args, void) {} +SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_ret, void) {} SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_div4, void) {} SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_div8, void) {} SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_gep, void) {} diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h b/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h index c424ab1cecf94f..7bfe40a792abea 100644 --- a/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h +++ b/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h @@ -115,6 +115,33 @@ SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void __sanitizer_cov_trace_const_cmp8(); SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void __sanitizer_cov_trace_switch(); +// Argument and return value tracing: observe the IR arguments of a function on +// entry and its return values. What is observed is decided by the LLVM IR +// alone; debug information never changes it. +// __sanitizer_cov_trace_args(u64 tag, u64 val, const u64 *offsets) +// __sanitizer_cov_trace_ret (u64 size, u64 val, const u64 *offsets) +// `tag` is (arg_idx << 32) | size. `arg_idx` is the position of the argument +// in the IR parameter list, not the source-language parameter number: one +// source parameter may be several IR arguments, a hidden struct-return pointer +// takes a position, and the elements of a first-class aggregate are reported +// one call each under the same index. +// `val` is the reported value itself - a pointer as the address it holds - if +// `offsets` is null. Otherwise it is the address of `size` bytes, and +// offsets[0] is the number N of {byte offset, byte size} pairs that follow it, +// none wider than 8 bytes, which say what to read there: an object the ABI +// passed or returned in memory, described by its IR-type elements, or the +// bytes behind a noundef pointer the IR says are dereferenceable, as many as +// that says and -sanitizer-coverage-trace-pointee-bytes allows. Such a pointer +// may be null if the IR said dereferenceable_or_null. A size of zero means +// nothing was reported: a void return, a value with no fixed-size bit pattern, +// or an object the function does not use, whose address may be poison. +// Neither callback is passed the function it speaks for. A runtime that needs +// it takes its own return address, less one so that the address lies within the +// call, which identifies the observation. +SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void +__sanitizer_cov_trace_args(); +SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void +__sanitizer_cov_trace_ret(); SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void __sanitizer_cov_trace_div4(); SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void diff --git a/compiler-rt/test/sanitizer_common/TestCases/sanitizer_coverage_trace_args.cpp b/compiler-rt/test/sanitizer_common/TestCases/sanitizer_coverage_trace_args.cpp new file mode 100644 index 00000000000000..7b874bc2d77a03 --- /dev/null +++ b/compiler-rt/test/sanitizer_common/TestCases/sanitizer_coverage_trace_args.cpp @@ -0,0 +1,53 @@ +// Check that -fsanitize-coverage=trace-args,trace-ret calls the two callbacks +// with the observed argument and return values, and that a user definition +// overrides compiler-rt's weak defaults. +// +// The arguments are the IR arguments, reported on entry, so the values are +// right at any optimization level, with or without debug information. +// +// REQUIRES: has_sancovcc +// UNSUPPORTED: i386-darwin +// RUN: %clangxx -O2 -g -fsanitize-coverage=trace-pc-guard,trace-args,trace-ret %s -o %t +// RUN: %run %t 2>&1 | FileCheck %s +// RUN: %clangxx -O0 -fsanitize-coverage=trace-pc-guard,trace-args,trace-ret %s -o %t.O0 +// RUN: %run %t.O0 2>&1 | FileCheck %s + +#include <cstdint> +#include <cstdio> + +extern "C" { +// Stubs so the program links without a sanitizer runtime; these are weak in +// compiler-rt, so these definitions win. +void __sanitizer_cov_trace_pc_guard(uint32_t *) {} +void __sanitizer_cov_trace_pc_guard_init(uint32_t *, uint32_t *) {} + +// The consumers under test. A null `offsets` means `val` is the value; a size +// of zero means the instrumentation had nothing to report. Neither callback is +// passed the function it speaks for; a consumer that wants it takes its own +// return address. +void __sanitizer_cov_trace_args(uint64_t tag, uint64_t val, + const uint64_t *offsets) { + uint32_t arg_idx = tag >> 32, size = (uint32_t)tag; + if (size == sizeof(int) && !offsets) + fprintf(stderr, "ARG idx=%u val=%d\n", arg_idx, (int)val); +} + +void __sanitizer_cov_trace_ret(uint64_t size, uint64_t val, + const uint64_t *offsets) { + if (size == sizeof(int) && !offsets) + fprintf(stderr, "RET val=%d\n", (int)val); +} +} + +__attribute__((noinline)) int add(int a, int b) { return a + b; } + +int main() { + volatile int r = add(41, 2); + fprintf(stderr, "r=%d\n", (int)r); + return 0; +} + +// CHECK-DAG: ARG idx=0 val=41 +// CHECK-DAG: ARG idx=1 val=2 +// CHECK: RET val=43 +// CHECK: r=43 >From 5707f6049d806635c2816442c01f9e2d63ec8c84 Mon Sep 17 00:00:00 2001 From: Yunseong Kim <[email protected]> Date: Thu, 15 Oct 2026 12:46:43 +0200 Subject: [PATCH 4/4] [Clang][SanitizerCoverage] Document trace-args and trace-ret Add a "Tracing function arguments and return values" section to SanitizerCoverage.md. It covers: - the callback signatures, the tag encoding, and the offsets table; - how arg_idx relates to source parameters; - which pointers have their pointee snapshotted: noundef with dereferenceable(N) or dereferenceable_or_null(N); - the supported scope. Assisted-by: Kiro CLI (Claude Opus 5.5) Signed-off-by: Yunseong Kim <[email protected]> --- clang/docs/SanitizerCoverage.md | 199 ++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/clang/docs/SanitizerCoverage.md b/clang/docs/SanitizerCoverage.md index ae074d13f5322a..741c1d6ef0a847 100644 --- a/clang/docs/SanitizerCoverage.md +++ b/clang/docs/SanitizerCoverage.md @@ -330,6 +330,205 @@ void __sanitizer_cov_store8(uint64_t *addr); void __sanitizer_cov_store16(__int128 *addr); ``` +## Tracing function arguments and return values + +With `-fsanitize-coverage=trace-args` the compiler will insert a callback for +each argument of every instrumented function, on entry, and with +`-fsanitize-coverage=trace-ret` a callback before every return. Neither flag +needs another coverage mode: used alone, they emit no block or edge callbacks. + +The design rule for both flags is: + +> Debug information may enrich the interpretation of an observation, but must +> never determine which instrumentation is emitted. + +What is observed is decided by the LLVM IR of the instrumented function alone. +A build with `-g` emits exactly the same callbacks, with the same operands, and +the same machine code, as a build without it; debug information gives the +calls a source location and nothing else. + +```c++ +// Called once per observed IR argument, or element of one, on entry. +// tag: (arg_idx << 32) | size, where arg_idx is the zero-based +// position in the IR parameter list and size is the number of +// bytes reported (0 if nothing to report) +// val: the value itself, or the address of the bytes to read +// offsets: null if `val` is a value; otherwise a constant table: +// offsets[0] is the number of pairs N, followed by N +// [byte offset, byte size] pairs, none wider than 8 bytes +void __sanitizer_cov_trace_args(uint64_t tag, uint64_t val, + const uint64_t *offsets); + +// Called before each return; once per element of a first-class aggregate +// return value. +// size: number of bytes reported, 0 if nothing to report +void __sanitizer_cov_trace_ret(uint64_t size, uint64_t val, + const uint64_t *offsets); +``` + +Neither callback is told which function it speaks for. A runtime that needs to +know takes its own return address, less one so that the address lies within the +call rather than on the instruction after it, and resolves it like any other +code address. This is the same convention as the `trace-cmp` callbacks. Every +observation has a call of its own, which is never a tail call, so that address +identifies it. + +`offsets` says what `val` holds: + +* `offsets == nullptr`: `val` **is** the value, its low `size` bytes, + zero-extended. Integers, pointers, floating-point values and vectors are + reported this way: a pointer as the address it holds, a floating-point value + or a vector as its bit pattern, and a value wider than 64 bits by its low 64 + bits. + +* `offsets != nullptr`: `val` is the **address** of `size` bytes that may be + read at the call, and `offsets` says which: `offsets[0]` pieces follow it as + [byte offset, byte size] pairs, none wider than 8 bytes, so a consumer reading + each into a 64-bit word reads all of them. That is one of + + * an object the ABI placed in memory, which the IR itself marks as one: an + argument with a `byval`, `byref`, `inalloca` or `preallocated` attribute, + or the `sret` buffer of an indirect return. `offsets` describes the + elements of its IR type, nested structs included; anything wider than + 8 bytes - an array, or the opaque `[N x i8]` rustc types every such object + as - in 8-byte pieces. + * a *pointee snapshot*: a `noundef` pointer argument the IR says is + `dereferenceable(N)` or `dereferenceable_or_null(N)` on entry, or a + pointer return value the function's return attributes say is (`noundef` + included), at the return. `val` is the pointer, and `size` is the first + min(N, `-sanitizer-coverage-trace-pointee-bytes`) bytes it points to + (64 by default; 0 reports every pointer by value), covered in 8-byte + pieces. The IR attribute alone bounds the read - the IR has no pointee + type - so the bytes are raw; a consumer cuts them into members with the + binary's debug information. A + `dereferenceable_or_null` pointer may be null, which the runtime checks. + + The callback does not say which of the two it is. Every other + pointer is reported by value, and what it points to is never read: the + instrumentation reads memory only as far as the IR proves it may. + +`size == 0` means there was nothing to report: a `void` return, a value with no +fixed-size bit pattern, such as a scalable vector, or an object the function +does not use. Callers may pass anything for an argument the function does not +use - dead argument elimination passes poison in the callers it can see - so +such an object is not handed to the runtime to read; an unused value is still +reported. A value that may be poison is frozen first, so `val` always holds +bits. + +The compiler never creates memory to report a value from, so no stack slot +escapes and no frame is realigned on the instrumentation's account. + +### IR arguments, not source parameters + +`arg_idx` is the position of the argument in the IR function's parameter list. +That is the ABI's view of the signature, and it is not guaranteed to equal the +source-language parameter number: + +* One source parameter may be several IR arguments. On x86-64, + `void f(struct mix { double d; long l; } m, int z)` is + `f(double %0, i64 %1, i32 %2)`: `m` is IR arguments 0 and 1, and `z` is IR + argument 2. +* One IR argument may be several observations. On AArch64 the same function is + `f([2 x i64] %0, i32 %1)`: the first-class aggregate `%0` is reported as two + callbacks, both with `arg_idx == 0`, and `z` is IR argument 1. +* A hidden struct-return (`sret`) pointer and a Swift error slot are not + observed, but keep their positions: in `struct big f(long x)`, lowered to + `f(ptr sret %0, i64 %1)`, `x` is reported with `arg_idx == 1`. +* A C++ `this` is an ordinary pointer argument, reported as its value - with + the `sizeof` bytes of the object it points to, which the IR says are + dereferenceable, as for a C++ reference. + +A struct the ABI passes by address without an IR attribute that says so - a +large struct on AArch64, a non-trivially-copyable C++ object - is a pointer +argument, and is reported as the pointer, with what it points to if the IR +says it is dereferenceable. + +Which pointers the IR says that of depends on the front end. Clang gives C++ +references and `this` `dereferenceable(sizeof(T))`, as it does a struct the ABI +passes by address (on AArch64, any over 16 bytes), and a C array parameter of +static size, `const struct s p[static 1]`, `dereferenceable` that far; a plain +C pointer has no such attribute, and is reported by value. rustc gives `&T` and +`&mut T` arguments `dereferenceable(size_of::<T>())`, `Option<&T>` +`dereferenceable_or_null`, and the vtable of a `&dyn Trait` its size; not a +`Box<T>`, a raw pointer, the data of a slice or trait object, a `&` to a +`!Freeze` type (`Cell`, `UnsafeCell`) or a `&mut` to a `!Unpin` one, nor any +return value. + +### Pointers: values, layouts and snapshots + +Four things about a pointer argument or return value are easily confused, and +only the first and the third are recorded: + +* The **pointer value**, the address it holds: always reported, as `val`. +* The **source-level pointee type and member layout**: what the debug + information says it points to. A consumer can name and lay out the members + from it, but debug information never causes anything to be read. +* An explicit **pointee snapshot**: bytes the runtime reads behind the pointer + at the trace point. Only an explicit IR dereferenceability contract causes + the instrumentation runtime to read behind a pointer: + `noundef` together with `dereferenceable(N)`, or with + `dereferenceable_or_null(N)` and a null check, and then the first + min(N, `-sanitizer-coverage-trace-pointee-bytes`) bytes. The + attribute gives a bounded readable range, not a type: which members those + bytes are is the decoder's business, with the debug information. +* **Complete and partial values**: a member wholly within the bytes read is a + complete value; one the byte limit cuts is partial, and one beyond it was + not read. + +What carries the contract is up to the front end (see above). An ordinary C +`T *` argument normally does not, and is a pointer value only; a Rust `&T` or +`&mut T` usually does; a Rust raw pointer, `*const T` or `*mut T`, does not, +nor does a C pointer unless declared `T p[static N]`. Nothing is inferred to +stand in for a missing contract - not from later accesses, the size of an +object, the callers, the debug information or what must execute - and a +pointer stored inside a snapshot is a value too: what it points to is not +read. The instrumentation therefore does not reconstruct every pointer +argument, nor complete structs behind pointers. As a measure of how far it +goes, on an x86-64 Linux kernel build the pointee of 0 of 23,810 C pointer +arguments has a byte recorded, and on arm64 21 of 22,606 (structs AArch64 +passes by reference); of the 756 pointer arguments of the kernel's Rust +objects, 318 (42%) do. These are the shares of pointer arguments with at +least one recorded pointee byte, not of arguments, structs or return values +reconstructed in full. + +Recording, for a pointer without a contract, the values the function itself +loads and stores through it, when it does, is possible future work, and not +part of this implementation. + +Mapping an observation to a source parameter, or to a struct field, is the +job of a consumer that has the binary's debug information; the +instrumentation does not attempt it. An inlined callee is not an IR function +of its own any more, so its parameters are not observed: arguments are those of +the IR functions that remain after the inliner ran. `-fno-inline` makes more of +them visible, at the usual cost. + +Argument values are read on entry, before the `-O0` prologue stores the +incoming arguments to their stack slots, so they are correct at any +optimization level; return values likewise. + +### Consumers + +`compiler-rt` defines both callbacks weakly and empty in `sanitizer_common`, so +a program compiled with `trace-args`/`trace-ret` links without a runtime that +consumes the values; a strong definition overrides the default. + +The Linux kernel consumes the same callbacks through the KCOV dataflow +subsystem rather than through compiler-rt. + +### Supported scope + +The two flags are validated for: + +* C and C++ compiled by Clang, and Rust compiled by a `rustc` whose LLVM + carries the pass. +* ELF targets on x86-64 and AArch64, both in userspace (a custom runtime) and + in the Linux kernel. +* Code built with or without `-g`, at any optimization level. + +Any other frontend or target gets valid instrumentation: the pass works on +LLVM IR and target-independent IR attributes only, and never emits IR the +verifier rejects - a Swift `swifterror` slot, for one, is not reported. + ## Tracing control flow With `-fsanitize-coverage=control-flow` the compiler will create a table to collect _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
