Skip to content
Open
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
1 change: 1 addition & 0 deletions lldb/include/lldb/lldb-enumerations.h
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ enum InstrumentationRuntimeType {
eInstrumentationRuntimeTypeMainThreadChecker = 0x0003,
eInstrumentationRuntimeTypeSwiftRuntimeReporting = 0x0004,
eInstrumentationRuntimeTypeLibsanitizersAsan = 0x0005,
eInstrumentationRuntimeTypeBoundsSafety = 0x0006,
eNumInstrumentationRuntimeTypes
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
add_lldb_library(lldbPluginInstrumentationRuntimeBoundsSafety PLUGIN
InstrumentationRuntimeBoundsSafety.cpp

LINK_LIBS
lldbBreakpoint
lldbCore
lldbSymbol
lldbTarget
lldbPluginInstrumentationRuntimeUtility

CLANG_LIBS
clangCodeGen
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
//===-- InstrumentationRuntimeBoundsSafety.cpp -----------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "InstrumentationRuntimeBoundsSafety.h"

#include "Plugins/Process/Utility/HistoryThread.h"
#include "lldb/Breakpoint/StoppointCallbackContext.h"
#include "lldb/Core/Module.h"
#include "lldb/Core/PluginManager.h"
#include "lldb/Symbol/Block.h"
#include "lldb/Symbol/Symbol.h"
#include "lldb/Symbol/SymbolContext.h"
#include "lldb/Symbol/Variable.h"
#include "lldb/Symbol/VariableList.h"
#include "lldb/Target/InstrumentationRuntimeStopInfo.h"
#include "lldb/Target/RegisterContext.h"
#include "lldb/Target/SectionLoadList.h"
#include "lldb/Target/StopInfo.h"
#include "lldb/Target/Target.h"
#include "lldb/Target/Thread.h"
#include "lldb/Utility/RegisterValue.h"
#include "lldb/Utility/RegularExpression.h"
#include "clang/CodeGen/ModuleBuilder.h"

#include <memory>

using namespace lldb;
using namespace lldb_private;

LLDB_PLUGIN_DEFINE(InstrumentationRuntimeBoundsSafety)

#define BOUNDS_SAFETY_SOFT_TRAP_MINIMAL "__bounds_safety_soft_trap"
#define BOUNDS_SAFETY_SOFT_TRAP_S "__bounds_safety_soft_trap_s"

std::vector<std::string> &getBoundsSafetySoftTrapRuntimeFuncs() {
static std::vector<std::string> Funcs = {BOUNDS_SAFETY_SOFT_TRAP_MINIMAL,
BOUNDS_SAFETY_SOFT_TRAP_S};

return Funcs;
}

#define SOFT_TRAP_CATEGORY_PREFIX "Soft "
#define SOFT_TRAP_FALLBACK_CATEGORY \
SOFT_TRAP_CATEGORY_PREFIX "Bounds check failed"

class InstrumentationBoundsSafetyStopInfo : public StopInfo {
public:
~InstrumentationBoundsSafetyStopInfo() override = default;

lldb::StopReason GetStopReason() const override {
return lldb::eStopReasonInstrumentation;
}

std::optional<uint32_t>
GetSuggestedStackFrameIndex(bool inlined_stack) override {
return m_value;
}

const char *GetDescription() override { return m_description.c_str(); }

bool DoShouldNotify(Event *event_ptr) override { return true; }

static lldb::StopInfoSP
CreateInstrumentationBoundsSafetyStopInfo(Thread &thread) {
return StopInfoSP(new InstrumentationBoundsSafetyStopInfo(thread));
}

private:
std::pair<std::string, std::optional<uint32_t>>
ComputeStopReasonAndSuggestedStackFrameWithDebugInfo(
lldb::StackFrameSP parent_sf) {
// First try to use debug info to understand the reason for trapping. The
// call stack will look something like this:
//
// ```
// frame #0: `__bounds_safety_soft_trap_s(reason="")
// frame #1: `__clang_trap_msg$Bounds check failed$<reason>'
// frame #2: `bad_read(index=10)
// ```
// ....
const auto *TrapReasonFuncName = parent_sf->GetFunctionName();

auto MaybeTrapReason =
clang::CodeGen::DemangleTrapReasonInDebugInfo(TrapReasonFuncName);
if (!MaybeTrapReason.has_value())
return {};
auto category = MaybeTrapReason.value().first;
auto message = MaybeTrapReason.value().second;

// TODO: Clang should probably be changed to emit the "Soft " prefix itself
std::string stop_reason;
llvm::raw_string_ostream ss(stop_reason);
ss << SOFT_TRAP_CATEGORY_PREFIX;
if (category.empty())
ss << "<empty category>";
else
ss << category;
if (!message.empty()) {
ss << ": " << message;
}
// Use computed stop-reason and assume the parent of `parent_sf` is the
// the place in the user's code where the call to the soft trap runtime
// originated.
return std::make_pair(stop_reason, parent_sf->GetFrameIndex() + 1);
}

std::pair<std::optional<std::string>, std::optional<uint32_t>>
ComputeStopReasonAndSuggestedStackFrameWithoutDebugInfo(ThreadSP thread_sp) {
auto softtrap_sf = thread_sp->GetStackFrameAtIndex(0);
if (!softtrap_sf)
return {};
llvm::StringRef trap_reason_func_name = softtrap_sf->GetFunctionName();

if (trap_reason_func_name == BOUNDS_SAFETY_SOFT_TRAP_MINIMAL) {
// This function has no arguments so there's no additional information
// that would allow us to identify the trap reason.
//
// Use the fallback stop reason and the current frame.
// While we "could" set the suggested frame to our parent (where the
// bounds check failed), doing this leads to very misleading output in
// LLDB. E.g.:
//
// ```
// 0x100003b40 <+104>: bl 0x100003d64 ; __bounds_safety_soft_trap
// -> 0x100003b44 <+108>: b 0x100003b48 ; <+112>
// ```
//
// This makes it look we stopped after finishing the call to
// `__bounds_safety_soft_trap` but actually we are in the middle of the
// call. To avoid this confusion just use the current frame.
return {};
}

// BOUNDS_SAFETY_SOFT_TRAP_S has one argument which is a pointer to a string
// describing the trap or a nullptr.
if (trap_reason_func_name != BOUNDS_SAFETY_SOFT_TRAP_S)
return {};

auto rc = thread_sp->GetRegisterContext();
if (!rc)
return {};

// Don't try for architectures where examining the first register won't
// work.
auto process = thread_sp->GetProcess();
if (!process)
return {};
switch (process->GetTarget().GetArchitecture().GetCore()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure you need to do this for this commit, but it would be clearer if the architecture told you whether it uses registers for argument passing, than having to hard code it here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can file a github issue for this. Should this go to the upstream LLVM issue tracker or the Swift issue tracker?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

llvm issue tracker, this is architecture and not swift specific.

case ArchSpec::eCore_x86_32_i386:
case ArchSpec::eCore_x86_32_i486:
case ArchSpec::eCore_x86_32_i486sx:
case ArchSpec::eCore_x86_32_i686:
// Technically some x86 calling conventions do use a register for
// passing the first argument but let's ignore that for now.
return {};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth explaining here that you can't get the information for this architecture? Otherwise the person receiving this report might waste time trying to figure out why this wasn't working in their particular case?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If LLDB has a clean way of communicating this to the user I'm willing to use it (e.g. some kind of diagnostic system). At the moment is seems like the only thing I can do is shoehorn the failure into the trap reason message and I think that would be confusing. E.g.:

stop reason = Soft Bounds check failed: Cannot determine trap reason on x86 architecture when debug info is missing

The Cannot determ... part of the message is supposed to be read as the specific reason for trapping normally. Reusing it here to explain why the trap reason couldn't be determined seems a bit confusing.

Ideally what is something like:

stop reason = Soft Bounds check failed
note: Cannot determine trap reason on x86 architecture when debug info is missing

but I don't think anything like this exists today.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can write warning messages directly by writing to the stream returned by Debugger::GetAsyncErrorStream, or you can post a warning event using Debugger::ReportWarning. I think posting a warning event is currently in favor.

default: {
}
};

// Examine the register for the first argument
auto *arg0_info = rc->GetRegisterInfo(
lldb::RegisterKind::eRegisterKindGeneric, LLDB_REGNUM_GENERIC_ARG1);
if (!arg0_info)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Along this process there are a bunch of places where it could fail. It might help your sanity later on if you logged where the failure occurs. Then when this is failing on a machine or for a process you can't get access to you have more of a chance of figuring out what went wrong.

Copy link
Author

@delcypher delcypher Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What channel should I use? Presumably you mean logging code like this (first thing I found when greping for LLDB_LOG) ?

Log *log = GetLog(LLDBLog::API);
LLDB_LOGF(log,
            "SBDebugger(%p)::CreateTarget (filename=\"%s\", triple=%s, "
            "platform_name=%s, add_dependent_modules=%u, error=%s) => "
            "SBTarget(%p)",
            static_cast<void *>(m_opaque_sp.get()), filename, target_triple,
            platform_name, add_dependent_modules, sb_error.GetCString(),
            static_cast<void *>(target_sp.get()));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL, there isn't an instrumentation runtime log channel, which there really should be. So maybe better to do this as a follow-on.

return {};
RegisterValue reg_value;
if (!rc->ReadRegister(arg0_info, reg_value))
return {};
uint64_t reg_value_as_int = reg_value.GetAsUInt64(UINT64_MAX);
if (reg_value_as_int == UINT64_MAX || reg_value_as_int == 0)
return {};

// The first argument to the call is a pointer to a global C string
// containing the trap reason.
std::string out_string;
Status error_status;
thread_sp->GetProcess()->ReadCStringFromMemory(reg_value_as_int, out_string,
error_status);
if (error_status.Fail())
return {};
std::string stop_reason;
llvm::raw_string_ostream SS(stop_reason);
SS << SOFT_TRAP_FALLBACK_CATEGORY;
if (!stop_reason.empty()) {
SS << ": " << out_string;
}
// Use the current frame as the suggested frame for the same reason as for
// `BOUNDS_SAFETY_SOFT_TRAP_MINIMAL`.
return {stop_reason, 0};
}

std::pair<std::optional<std::string>, std::optional<uint32_t>>
ComputeStopReasonAndSuggestedStackFrame() {

ThreadSP thread_sp = GetThread();
if (!thread_sp)
return {};

auto parent_sf = thread_sp->GetStackFrameAtIndex(1);
if (!parent_sf)
return {};

if (parent_sf->HasDebugInformation()) {
return ComputeStopReasonAndSuggestedStackFrameWithDebugInfo(parent_sf);
}

// If the debug info is missing we can still get some information
// from the parameter in the soft trap runtime call.
return ComputeStopReasonAndSuggestedStackFrameWithoutDebugInfo(thread_sp);
}

InstrumentationBoundsSafetyStopInfo(Thread &thread) : StopInfo(thread, 0) {
// No additional data describing the reason for stopping
m_extended_info = nullptr;
m_description = SOFT_TRAP_FALLBACK_CATEGORY;

auto [Description, MaybeSuggestedStackIndex] =
ComputeStopReasonAndSuggestedStackFrame();
if (Description)
m_description = Description.value();
if (MaybeSuggestedStackIndex)
m_value = MaybeSuggestedStackIndex.value();
}
};

InstrumentationRuntimeBoundsSafety::~InstrumentationRuntimeBoundsSafety() {
Deactivate();
}

lldb::InstrumentationRuntimeSP
InstrumentationRuntimeBoundsSafety::CreateInstance(
const lldb::ProcessSP &process_sp) {
return InstrumentationRuntimeSP(
new InstrumentationRuntimeBoundsSafety(process_sp));
}

void InstrumentationRuntimeBoundsSafety::Initialize() {
PluginManager::RegisterPlugin(GetPluginNameStatic(),
"BoundsSafety instrumentation runtime plugin.",
CreateInstance, GetTypeStatic);
}

void InstrumentationRuntimeBoundsSafety::Terminate() {
PluginManager::UnregisterPlugin(CreateInstance);
}

lldb::InstrumentationRuntimeType
InstrumentationRuntimeBoundsSafety::GetTypeStatic() {
return lldb::eInstrumentationRuntimeTypeBoundsSafety;
}

const RegularExpression &
Copy link

@jimingham jimingham Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Julian just added a InstrumentationRuntime::MatchAllModules. You still need to override this function but if you also override MatchAllModules to return true, we won't waste time pointlessly trying an empty regex against all the libraries.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The override for MatchAllModules is declared in the header for InstrumentationRuntimeBoundsSafety

InstrumentationRuntimeBoundsSafety::GetPatternForRuntimeLibrary() {
static RegularExpression regex;
return regex;
}

bool InstrumentationRuntimeBoundsSafety::CheckIfRuntimeIsValid(
const lldb::ModuleSP module_sp) {

for (const auto &SoftTrapFunc : getBoundsSafetySoftTrapRuntimeFuncs()) {
ConstString test_sym(SoftTrapFunc);

if (module_sp->FindFirstSymbolWithNameAndType(test_sym,
lldb::eSymbolTypeAny))
return true;
}
return false;
}

bool InstrumentationRuntimeBoundsSafety::NotifyBreakpointHit(
void *baton, StoppointCallbackContext *context, user_id_t break_id,
user_id_t break_loc_id) {
assert(baton && "null baton");
if (!baton)
return false; ///< false => resume execution.

InstrumentationRuntimeBoundsSafety *const instance =
static_cast<InstrumentationRuntimeBoundsSafety *>(baton);

ProcessSP process_sp = instance->GetProcessSP();
ThreadSP thread_sp = context->exe_ctx_ref.GetThreadSP();
if (!process_sp || !thread_sp ||
process_sp != context->exe_ctx_ref.GetProcessSP())
return false;

if (process_sp->GetModIDRef().IsLastResumeForUserExpression())
return false;

thread_sp->SetStopInfo(
InstrumentationBoundsSafetyStopInfo::
CreateInstrumentationBoundsSafetyStopInfo(*thread_sp));
return true;
}

void InstrumentationRuntimeBoundsSafety::Activate() {
if (IsActive())
return;

ProcessSP process_sp = GetProcessSP();
if (!process_sp)
return;

auto breakpoint = process_sp->GetTarget().CreateBreakpoint(
/*containingModules=*/nullptr,
/*containingSourceFiles=*/nullptr, getBoundsSafetySoftTrapRuntimeFuncs(),
eFunctionNameTypeFull, eLanguageTypeUnknown,
/*m_offset=*/0,
/*skip_prologue*/ eLazyBoolNo,
/*internal=*/true,
/*request_hardware*/ false);

// TODO: Once we have an instrumentation log handle we should log these
// failures (rdar://164920875).
if (!breakpoint)
return;
if (!breakpoint->HasResolvedLocations()) {
assert(0 && "breakpoint has no resolved locations");
process_sp->GetTarget().RemoveBreakpointByID(breakpoint->GetID());
return;
}

// Note: When `sync=true` the suggested stackframe is completely ignored. So
// we use `sync=false`. Is that a bug?
breakpoint->SetCallback(
InstrumentationRuntimeBoundsSafety::NotifyBreakpointHit, this,
/*sync=*/false);
breakpoint->SetBreakpointKind("bounds-safety-soft-trap");
SetBreakpointID(breakpoint->GetID());
SetActive(true);
}

void InstrumentationRuntimeBoundsSafety::Deactivate() {
SetActive(false);
if (ProcessSP process_sp = GetProcessSP())
process_sp->GetTarget().RemoveBreakpointByID(GetBreakpointID());

SetBreakpointID(LLDB_INVALID_BREAK_ID);
}
Loading