Skip to content

[sancov] Introduce optional callback for stack-depth tracking #138323

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions clang/docs/SanitizerCoverage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,50 @@ Users need to implement a single function to capture the CF table at startup:
// the collected control flow.
}

Tracing Stack Depth
===================

With ``-fsanitize-coverage=stack-depth`` the compiler will track how much
stack space has been used for a function call chain. Leaf functions are
not included in this tracing.

The maximum depth of a function call graph is stored in the thread-local
``__sancov_lowest_stack`` variable. Instrumentation is inserted in every
non-leaf function to check the frame pointer against this variable,
and if it is lower, store the current frame pointer. This effectively
inserts the following:

.. code-block:: c++

extern thread_local uintptr_t __sancov_lowest_stack;

uintptr_t stack = (uintptr_t)__builtin_frame_address(0);
if (stack < __sancov_lowest_stack)
__sancov_lowest_stack = stack;

If ``-fsanitize-coverage-stack-depth-callback-min=N`` (where
``N > 0``) is also used, the tracking is delegated to a callback,
``__sanitizer_cov_stack_depth``, instead of adding instrumentation to
update ``__sancov_lowest_stack``. The ``N`` of the argument is used
to determine which functions to instrument. Only functions estimated
to be using ``N`` bytes or more of stack space will be instrumented to
call the tracing callback. In the case of a dynamically sized stack,
the callback is unconditionally added.

The callback takes no arguments and is responsible for determining
the stack usage and doing any needed comparisons and storage. A roughly
equivalent implementation of ``__sancov_lowest_stack`` using the callback
would look like this:

.. code-block:: c++

void __sanitizer_cov_stack_depth(void) {
uintptr_t stack = (uintptr_t)__builtin_frame_address(0);

if (stack < __sancov_lowest_stack)
__sancov_lowest_stack = stack;
}

Gated Trace Callbacks
=====================

Expand Down
1 change: 1 addition & 0 deletions clang/include/clang/Basic/CodeGenOptions.def
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ CODEGENOPT(SanitizeCoveragePCTable, 1, 0) ///< Create a PC Table.
CODEGENOPT(SanitizeCoverageControlFlow, 1, 0) ///< Collect control flow
CODEGENOPT(SanitizeCoverageNoPrune, 1, 0) ///< Disable coverage pruning.
CODEGENOPT(SanitizeCoverageStackDepth, 1, 0) ///< Enable max stack depth tracing
VALUE_CODEGENOPT(SanitizeCoverageStackDepthCallbackMin , 32, 0) ///< Enable stack depth tracing callbacks.
CODEGENOPT(SanitizeCoverageTraceLoads, 1, 0) ///< Enable tracing of loads.
CODEGENOPT(SanitizeCoverageTraceStores, 1, 0) ///< Enable tracing of stores.
CODEGENOPT(SanitizeBinaryMetadataCovered, 1, 0) ///< Emit PCs for covered functions.
Expand Down
7 changes: 7 additions & 0 deletions clang/include/clang/Driver/Options.td
Original file line number Diff line number Diff line change
Expand Up @@ -2361,6 +2361,13 @@ def fsanitize_coverage_ignorelist : Joined<["-"], "fsanitize-coverage-ignorelist
HelpText<"Disable sanitizer coverage instrumentation for modules and functions "
"that match the provided special case list, even the allowed ones">,
MarshallingInfoStringVector<CodeGenOpts<"SanitizeCoverageIgnorelistFiles">>;
def fsanitize_coverage_stack_depth_callback_min_EQ
: Joined<["-"], "fsanitize-coverage-stack-depth-callback-min=">,
Group<f_clang_Group>,
MetaVarName<"<M>">,
HelpText<"Use callback for max stack depth tracing with minimum stack "
"depth M">,
MarshallingInfoInt<CodeGenOpts<"SanitizeCoverageStackDepthCallbackMin">>;
def fexperimental_sanitize_metadata_EQ : CommaJoined<["-"], "fexperimental-sanitize-metadata=">,
Group<f_Group>,
HelpText<"Specify the type of metadata to emit for binary analysis sanitizers">;
Expand Down
1 change: 1 addition & 0 deletions clang/include/clang/Driver/SanitizerArgs.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class SanitizerArgs {
std::vector<std::string> CoverageIgnorelistFiles;
std::vector<std::string> BinaryMetadataIgnorelistFiles;
int CoverageFeatures = 0;
int CoverageStackDepthCallbackMin = 0;
int BinaryMetadataFeatures = 0;
int OverflowPatternExclusions = 0;
int MsanTrackOrigins = 0;
Expand Down
1 change: 1 addition & 0 deletions clang/lib/CodeGen/BackendUtil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ getSancovOptsFromCGOpts(const CodeGenOptions &CGOpts) {
Opts.InlineBoolFlag = CGOpts.SanitizeCoverageInlineBoolFlag;
Opts.PCTable = CGOpts.SanitizeCoveragePCTable;
Opts.StackDepth = CGOpts.SanitizeCoverageStackDepth;
Opts.StackDepthCallbackMin = CGOpts.SanitizeCoverageStackDepthCallbackMin;
Opts.TraceLoads = CGOpts.SanitizeCoverageTraceLoads;
Opts.TraceStores = CGOpts.SanitizeCoverageTraceStores;
Opts.CollectControlFlow = CGOpts.SanitizeCoverageControlFlow;
Expand Down
16 changes: 16 additions & 0 deletions clang/lib/Driver/SanitizerArgs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,17 @@ SanitizerArgs::SanitizerArgs(const ToolChain &TC,
options::OPT_fno_sanitize_ignorelist,
clang::diag::err_drv_malformed_sanitizer_ignorelist, DiagnoseErrors);

// Verify that -fsanitize-coverage-stack-depth-callback-min is >= 0.
if (Arg *A = Args.getLastArg(
options::OPT_fsanitize_coverage_stack_depth_callback_min_EQ)) {
StringRef S = A->getValue();
if (S.getAsInteger(0, CoverageStackDepthCallbackMin) ||
CoverageStackDepthCallbackMin < 0) {
if (DiagnoseErrors)
D.Diag(clang::diag::err_drv_invalid_value) << A->getAsString(Args) << S;
}
}

// Parse -f[no-]sanitize-memory-track-origins[=level] options.
if (AllAddedKinds & SanitizerKind::Memory) {
if (Arg *A =
Expand Down Expand Up @@ -1269,6 +1280,11 @@ void SanitizerArgs::addArgs(const ToolChain &TC, const llvm::opt::ArgList &Args,
addSpecialCaseListOpt(Args, CmdArgs, "-fsanitize-coverage-ignorelist=",
CoverageIgnorelistFiles);

if (CoverageStackDepthCallbackMin)
CmdArgs.push_back(
Args.MakeArgString("-fsanitize-coverage-stack-depth-callback-min=" +
Twine(CoverageStackDepthCallbackMin)));

if (!GPUSanitize) {
// Translate available BinaryMetadataFeatures to corresponding clang-cc1
// flags. Does not depend on any other sanitizers. Unsupported on GPUs.
Expand Down
14 changes: 14 additions & 0 deletions clang/test/Driver/fsanitize-coverage.c
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@
// CHECK-STACK-DEPTH-PC-GUARD: -fsanitize-coverage-trace-pc-guard
// CHECK-STACK-DEPTH-PC-GUARD: -fsanitize-coverage-stack-depth

// RUN: %clang --target=x86_64-linux-gnu \
// RUN: -fsanitize-coverage-stack-depth-callback-min=100 %s -### 2>&1 | \
// RUN: FileCheck %s --check-prefix=CHECK-STACK-DEPTH-CALLBACK
// RUN: %clang --target=x86_64-linux-gnu \
// RUN: -fsanitize-coverage-stack-depth-callback-min=0 %s -### 2>&1 | \
// RUN: FileCheck %s --check-prefix=CHECK-STACK-DEPTH-CALLBACK-ZERO
// RUN: not %clang --target=x86_64-linux-gnu \
// RUN: -fsanitize-coverage-stack-depth-callback-min=-10 %s -### 2>&1 | \
// RUN: FileCheck %s --check-prefix=CHECK-STACK-DEPTH-CALLBACK-NEGATIVE
// CHECK-STACK-DEPTH-CALLBACK-NOT: error:
// CHECK-STACK-DEPTH-CALLBACK: -fsanitize-coverage-stack-depth-callback-min=100
// CHECK-STACK-DEPTH-CALLBACK-ZERO-NOT: -fsanitize-coverage-stack-depth-callback-min=0
// CHECK-STACK-DEPTH-CALLBACK-NEGATIVE: error: invalid value '-10' in '-fsanitize-coverage-stack-depth-callback-min=-10'

// RUN: %clang --target=x86_64-linux-gnu -fsanitize=address -fsanitize-coverage=trace-cmp,indirect-calls %s -### 2>&1 | FileCheck %s --check-prefix=CHECK-NO-TYPE-NECESSARY
// CHECK-NO-TYPE-NECESSARY-NOT: error:
// CHECK-NO-TYPE-NECESSARY: -fsanitize-coverage-indirect-calls
Expand Down
1 change: 1 addition & 0 deletions llvm/include/llvm/Transforms/Utils/Instrumentation.h
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ struct SanitizerCoverageOptions {
bool TraceStores = false;
bool CollectControlFlow = false;
bool GatedCallbacks = false;
int StackDepthCallbackMin = 0;

SanitizerCoverageOptions() = default;
};
Expand Down
86 changes: 71 additions & 15 deletions llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const char SanCovPCsSectionName[] = "sancov_pcs";
const char SanCovCFsSectionName[] = "sancov_cfs";
const char SanCovCallbackGateSectionName[] = "sancov_gate";

const char SanCovStackDepthCallbackName[] = "__sanitizer_cov_stack_depth";
const char SanCovLowestStackName[] = "__sancov_lowest_stack";
const char SanCovCallbackGateName[] = "__sancov_should_track";

Expand Down Expand Up @@ -152,6 +153,12 @@ static cl::opt<bool> ClStackDepth("sanitizer-coverage-stack-depth",
cl::desc("max stack depth tracing"),
cl::Hidden);

static cl::opt<int> ClStackDepthCallbackMin(
"sanitizer-coverage-stack-depth-callback-min",
cl::desc("max stack depth tracing should use callback and only when "
"stack depth more than specified"),
cl::Hidden);

static cl::opt<bool>
ClCollectCF("sanitizer-coverage-control-flow",
cl::desc("collect control flow for each function"), cl::Hidden);
Expand Down Expand Up @@ -202,6 +209,8 @@ SanitizerCoverageOptions OverrideFromCL(SanitizerCoverageOptions Options) {
Options.PCTable |= ClCreatePCTable;
Options.NoPrune |= !ClPruneBlocks;
Options.StackDepth |= ClStackDepth;
Options.StackDepthCallbackMin = std::max(Options.StackDepthCallbackMin,
ClStackDepthCallbackMin.getValue());
Options.TraceLoads |= ClLoadTracing;
Options.TraceStores |= ClStoreTracing;
Options.GatedCallbacks |= ClGatedCallbacks;
Expand Down Expand Up @@ -271,6 +280,7 @@ class ModuleSanitizerCoverage {
DomTreeCallback DTCallback;
PostDomTreeCallback PDTCallback;

FunctionCallee SanCovStackDepthCallback;
FunctionCallee SanCovTracePCIndir;
FunctionCallee SanCovTracePC, SanCovTracePCGuard;
std::array<FunctionCallee, 4> SanCovTraceCmpFunction;
Expand Down Expand Up @@ -514,6 +524,9 @@ bool ModuleSanitizerCoverage::instrumentModule() {
SanCovTracePCGuard =
M.getOrInsertFunction(SanCovTracePCGuardName, VoidTy, PtrTy);

SanCovStackDepthCallback =
M.getOrInsertFunction(SanCovStackDepthCallbackName, VoidTy);

for (auto &F : M)
instrumentFunction(F);

Expand Down Expand Up @@ -1078,22 +1091,65 @@ void ModuleSanitizerCoverage::InjectCoverageAtBlock(Function &F, BasicBlock &BB,
Store->setNoSanitizeMetadata();
}
if (Options.StackDepth && IsEntryBB && !IsLeafFunc) {
// Check stack depth. If it's the deepest so far, record it.
Module *M = F.getParent();
auto FrameAddrPtr = IRB.CreateIntrinsic(
Intrinsic::frameaddress,
IRB.getPtrTy(M->getDataLayout().getAllocaAddrSpace()),
{Constant::getNullValue(Int32Ty)});
auto FrameAddrInt = IRB.CreatePtrToInt(FrameAddrPtr, IntptrTy);
auto LowestStack = IRB.CreateLoad(IntptrTy, SanCovLowestStack);
auto IsStackLower = IRB.CreateICmpULT(FrameAddrInt, LowestStack);
auto ThenTerm = SplitBlockAndInsertIfThen(
IsStackLower, &*IP, false,
MDBuilder(IRB.getContext()).createUnlikelyBranchWeights());
IRBuilder<> ThenIRB(ThenTerm);
auto Store = ThenIRB.CreateStore(FrameAddrInt, SanCovLowestStack);
LowestStack->setNoSanitizeMetadata();
Store->setNoSanitizeMetadata();
const DataLayout &DL = M->getDataLayout();

if (Options.StackDepthCallbackMin) {
// In callback mode, only add call when stack depth reaches minimum.
uint32_t EstimatedStackSize = 0;
// If dynamic alloca found, always add call.
bool HasDynamicAlloc = false;
// Find an insertion point after last "alloca".
llvm::Instruction *InsertBefore = nullptr;

// Examine all allocas in the basic block. since we're too early
// to have results from Intrinsic::frameaddress, we have to manually
// estimate the stack size.
for (auto &I : BB) {
if (auto *AI = dyn_cast<AllocaInst>(&I)) {
// Move potential insertion point past the "alloca".
InsertBefore = AI->getNextNode();

// Make an estimate on the stack usage.
if (AI->isStaticAlloca()) {
uint32_t Bytes = DL.getTypeAllocSize(AI->getAllocatedType());
if (AI->isArrayAllocation()) {
if (const ConstantInt *arraySize =
dyn_cast<ConstantInt>(AI->getArraySize())) {
Bytes *= arraySize->getZExtValue();
} else {
HasDynamicAlloc = true;
}
}
EstimatedStackSize += Bytes;
} else {
HasDynamicAlloc = true;
}
}
}

if (HasDynamicAlloc ||
EstimatedStackSize >= Options.StackDepthCallbackMin) {
if (InsertBefore)
IRB.SetInsertPoint(InsertBefore);
IRB.CreateCall(SanCovStackDepthCallback)->setCannotMerge();
}
} else {
// Check stack depth. If it's the deepest so far, record it.
auto FrameAddrPtr = IRB.CreateIntrinsic(
Intrinsic::frameaddress, IRB.getPtrTy(DL.getAllocaAddrSpace()),
{Constant::getNullValue(Int32Ty)});
auto FrameAddrInt = IRB.CreatePtrToInt(FrameAddrPtr, IntptrTy);
auto LowestStack = IRB.CreateLoad(IntptrTy, SanCovLowestStack);
auto IsStackLower = IRB.CreateICmpULT(FrameAddrInt, LowestStack);
auto ThenTerm = SplitBlockAndInsertIfThen(
IsStackLower, &*IP, false,
MDBuilder(IRB.getContext()).createUnlikelyBranchWeights());
IRBuilder<> ThenIRB(ThenTerm);
auto Store = ThenIRB.CreateStore(FrameAddrInt, SanCovLowestStack);
LowestStack->setNoSanitizeMetadata();
Store->setNoSanitizeMetadata();
}
}
}

Expand Down
Loading
Loading