Skip to content

Commit a15e413

Browse files
committed
[BoundsSafety][LLDB] Implement instrumentation plugin for -fbounds-safety soft traps
This patch implements an instrumentation plugin for the `-fbounds-safety` soft trap mode first implemented in #11645 (rdar://158088757). The current implementation of -fbounds-safety traps works by emitting calls to runtime functions intended to log the occurrence of a soft trap. While the user could just set a breakpoint of these functions the instrumentation plugin sets it automatically and provides several additional features: When debug info is available: * It adjusts the stop reason to be the reason for trapping. This is extracted from the artificial frame in the debug info (similar to -fbounds-safety hard traps). * It adjusts the selected frame to be the frame where the soft trap occurred. When debug info is not available: * For the `call-with-str` soft trap mode the soft trap reason is read from the first argument register. * For the `call-minimal` soft trap mode the stop reason is adjusted to note its a bounds check failure but does not give further information because none is available. * In this situation the selected frame is not adjusted because in this mode the user will be looking at assembly and adjusting the frame makes things confusing. This patch includes shell and api tests. The shell tests seemed like the best way to test behavior when debug info is missing because those tests make it easy to disable building with debug info completely. rdar://163230807
1 parent 2e8a2f7 commit a15e413

16 files changed

+726
-9
lines changed

lldb/include/lldb/lldb-enumerations.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,7 @@ enum InstrumentationRuntimeType {
538538
eInstrumentationRuntimeTypeMainThreadChecker = 0x0003,
539539
eInstrumentationRuntimeTypeSwiftRuntimeReporting = 0x0004,
540540
eInstrumentationRuntimeTypeLibsanitizersAsan = 0x0005,
541+
eInstrumentationRuntimeTypeBoundsSafety = 0x0006,
541542
eNumInstrumentationRuntimeTypes
542543
};
543544

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
add_lldb_library(lldbPluginInstrumentationRuntimeBoundsSafety PLUGIN
2+
InstrumentationRuntimeBoundsSafety.cpp
3+
4+
LINK_LIBS
5+
lldbBreakpoint
6+
lldbCore
7+
lldbSymbol
8+
lldbTarget
9+
lldbPluginInstrumentationRuntimeUtility
10+
11+
CLANG_LIBS
12+
clangCodeGen
13+
)
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
//===-- InstrumentationRuntimeBoundsSafety.cpp -----------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "InstrumentationRuntimeBoundsSafety.h"
10+
11+
#include "Plugins/Process/Utility/HistoryThread.h"
12+
#include "lldb/Breakpoint/StoppointCallbackContext.h"
13+
#include "lldb/Core/Module.h"
14+
#include "lldb/Core/PluginManager.h"
15+
#include "lldb/Symbol/Block.h"
16+
#include "lldb/Symbol/Symbol.h"
17+
#include "lldb/Symbol/SymbolContext.h"
18+
#include "lldb/Symbol/Variable.h"
19+
#include "lldb/Symbol/VariableList.h"
20+
#include "lldb/Target/InstrumentationRuntimeStopInfo.h"
21+
#include "lldb/Target/RegisterContext.h"
22+
#include "lldb/Target/SectionLoadList.h"
23+
#include "lldb/Target/StopInfo.h"
24+
#include "lldb/Target/Target.h"
25+
#include "lldb/Target/Thread.h"
26+
#include "lldb/Utility/RegisterValue.h"
27+
#include "lldb/Utility/RegularExpression.h"
28+
#include "clang/CodeGen/ModuleBuilder.h"
29+
30+
#include <memory>
31+
32+
using namespace lldb;
33+
using namespace lldb_private;
34+
35+
LLDB_PLUGIN_DEFINE(InstrumentationRuntimeBoundsSafety)
36+
37+
#define BOUNDS_SAFETY_SOFT_TRAP_MINIMAL "__bounds_safety_soft_trap"
38+
#define BOUNDS_SAFETY_SOFT_TRAP_S "__bounds_safety_soft_trap_s"
39+
40+
std::vector<std::string> &getBoundsSafetySoftTrapRuntimeFuncs() {
41+
static std::vector<std::string> Funcs = {BOUNDS_SAFETY_SOFT_TRAP_MINIMAL,
42+
BOUNDS_SAFETY_SOFT_TRAP_S};
43+
44+
return Funcs;
45+
}
46+
47+
#define SOFT_TRAP_CATEGORY_PREFIX "Soft "
48+
#define SOFT_TRAP_FALLBACK_CATEGORY \
49+
SOFT_TRAP_CATEGORY_PREFIX "Bounds check failed"
50+
51+
class InstrumentationBoundsSafetyStopInfo : public StopInfo {
52+
public:
53+
~InstrumentationBoundsSafetyStopInfo() override = default;
54+
55+
lldb::StopReason GetStopReason() const override {
56+
return lldb::eStopReasonInstrumentation;
57+
}
58+
59+
std::optional<uint32_t>
60+
GetSuggestedStackFrameIndex(bool inlined_stack) override {
61+
return m_value;
62+
}
63+
64+
const char *GetDescription() override { return m_description.c_str(); }
65+
66+
bool DoShouldNotify(Event *event_ptr) override { return true; }
67+
68+
static lldb::StopInfoSP
69+
CreateInstrumentationBoundsSafetyStopInfo(Thread &thread) {
70+
return StopInfoSP(new InstrumentationBoundsSafetyStopInfo(thread));
71+
}
72+
73+
private:
74+
std::pair<std::string, std::optional<uint32_t>>
75+
ComputeStopReasonAndSuggestedStackFrameWithDebugInfo(
76+
lldb::StackFrameSP parent_sf) {
77+
// First try to use debug info to understand the reason for trapping. The
78+
// call stack will look something like this:
79+
//
80+
// ```
81+
// frame #0: `__bounds_safety_soft_trap_s(reason="")
82+
// frame #1: `__clang_trap_msg$Bounds check failed$<reason>'
83+
// frame #2: `bad_read(index=10)
84+
// ```
85+
// ....
86+
const auto *TrapReasonFuncName = parent_sf->GetFunctionName();
87+
88+
auto MaybeTrapReason =
89+
clang::CodeGen::DemangleTrapReasonInDebugInfo(TrapReasonFuncName);
90+
if (!MaybeTrapReason.has_value())
91+
return {};
92+
auto category = MaybeTrapReason.value().first;
93+
auto message = MaybeTrapReason.value().second;
94+
95+
// TODO: Clang should probably be changed to emit the "Soft " prefix itself
96+
std::string stop_reason;
97+
llvm::raw_string_ostream ss(stop_reason);
98+
ss << SOFT_TRAP_CATEGORY_PREFIX;
99+
if (category.empty())
100+
ss << "<empty category>";
101+
else
102+
ss << category;
103+
if (!message.empty()) {
104+
ss << ": " << message;
105+
}
106+
// Use computed stop-reason and assume the parent of `parent_sf` is the
107+
// the place in the user's code where the call to the soft trap runtime
108+
// originated.
109+
return std::make_pair(stop_reason, parent_sf->GetFrameIndex() + 1);
110+
}
111+
112+
std::pair<std::optional<std::string>, std::optional<uint32_t>>
113+
ComputeStopReasonAndSuggestedStackFrameWithoutDebugInfo(ThreadSP thread_sp) {
114+
auto softtrap_sf = thread_sp->GetStackFrameAtIndex(0);
115+
if (!softtrap_sf)
116+
return {};
117+
llvm::StringRef trap_reason_func_name = softtrap_sf->GetFunctionName();
118+
119+
if (trap_reason_func_name == BOUNDS_SAFETY_SOFT_TRAP_MINIMAL) {
120+
// This function has no arguments so there's no additional information
121+
// that would allow us to identify the trap reason.
122+
//
123+
// Use the fallback stop reason and the current frame.
124+
// While we "could" set the suggested frame to our parent (where the
125+
// bounds check failed), doing this leads to very misleading output in
126+
// LLDB. E.g.:
127+
//
128+
// ```
129+
// 0x100003b40 <+104>: bl 0x100003d64 ; __bounds_safety_soft_trap
130+
// -> 0x100003b44 <+108>: b 0x100003b48 ; <+112>
131+
// ```
132+
//
133+
// This makes it look we stopped after finishing the call to
134+
// `__bounds_safety_soft_trap` but actually we are in the middle of the
135+
// call. To avoid this confusion just use the current frame.
136+
return {};
137+
}
138+
139+
// BOUNDS_SAFETY_SOFT_TRAP_S has one argument which is a pointer to a string
140+
// describing the trap or a nullptr.
141+
if (trap_reason_func_name != BOUNDS_SAFETY_SOFT_TRAP_S)
142+
return {};
143+
144+
auto rc = thread_sp->GetRegisterContext();
145+
if (!rc)
146+
return {};
147+
148+
// Don't try for architectures where examining the first register won't
149+
// work.
150+
auto process = thread_sp->GetProcess();
151+
if (!process)
152+
return {};
153+
switch (process->GetTarget().GetArchitecture().GetCore()) {
154+
case ArchSpec::eCore_x86_32_i386:
155+
case ArchSpec::eCore_x86_32_i486:
156+
case ArchSpec::eCore_x86_32_i486sx:
157+
case ArchSpec::eCore_x86_32_i686:
158+
// Technically some x86 calling conventions do use a register for
159+
// passing the first argument but let's ignore that for now.
160+
return {};
161+
default: {
162+
}
163+
};
164+
165+
// Examine the register for the first argument
166+
auto *arg0_info = rc->GetRegisterInfo(
167+
lldb::RegisterKind::eRegisterKindGeneric, LLDB_REGNUM_GENERIC_ARG1);
168+
if (!arg0_info)
169+
return {};
170+
RegisterValue reg_value;
171+
if (!rc->ReadRegister(arg0_info, reg_value))
172+
return {};
173+
uint64_t reg_value_as_int = reg_value.GetAsUInt64(UINT64_MAX);
174+
if (reg_value_as_int == UINT64_MAX || reg_value_as_int == 0)
175+
return {};
176+
177+
// The first argument to the call is a pointer to a global C string
178+
// containing the trap reason.
179+
std::string out_string;
180+
Status error_status;
181+
thread_sp->GetProcess()->ReadCStringFromMemory(reg_value_as_int, out_string,
182+
error_status);
183+
if (error_status.Fail())
184+
return {};
185+
std::string stop_reason;
186+
llvm::raw_string_ostream SS(stop_reason);
187+
SS << SOFT_TRAP_FALLBACK_CATEGORY;
188+
if (!stop_reason.empty()) {
189+
SS << ": " << out_string;
190+
}
191+
// Use the current frame as the suggested frame for the same reason as for
192+
// `BOUNDS_SAFETY_SOFT_TRAP_MINIMAL`.
193+
return {stop_reason, 0};
194+
}
195+
196+
std::pair<std::optional<std::string>, std::optional<uint32_t>>
197+
ComputeStopReasonAndSuggestedStackFrame() {
198+
199+
ThreadSP thread_sp = GetThread();
200+
if (!thread_sp)
201+
return {};
202+
203+
auto parent_sf = thread_sp->GetStackFrameAtIndex(1);
204+
if (!parent_sf)
205+
return {};
206+
207+
if (parent_sf->HasDebugInformation()) {
208+
return ComputeStopReasonAndSuggestedStackFrameWithDebugInfo(parent_sf);
209+
}
210+
211+
// If the debug info is missing we can still get some information
212+
// from the parameter in the soft trap runtime call.
213+
return ComputeStopReasonAndSuggestedStackFrameWithoutDebugInfo(thread_sp);
214+
}
215+
216+
InstrumentationBoundsSafetyStopInfo(Thread &thread) : StopInfo(thread, 0) {
217+
// No additional data describing the reason for stopping
218+
m_extended_info = nullptr;
219+
m_description = SOFT_TRAP_FALLBACK_CATEGORY;
220+
221+
auto [Description, MaybeSuggestedStackIndex] =
222+
ComputeStopReasonAndSuggestedStackFrame();
223+
if (Description)
224+
m_description = Description.value();
225+
if (MaybeSuggestedStackIndex)
226+
m_value = MaybeSuggestedStackIndex.value();
227+
}
228+
};
229+
230+
InstrumentationRuntimeBoundsSafety::~InstrumentationRuntimeBoundsSafety() {
231+
Deactivate();
232+
}
233+
234+
lldb::InstrumentationRuntimeSP
235+
InstrumentationRuntimeBoundsSafety::CreateInstance(
236+
const lldb::ProcessSP &process_sp) {
237+
return InstrumentationRuntimeSP(
238+
new InstrumentationRuntimeBoundsSafety(process_sp));
239+
}
240+
241+
void InstrumentationRuntimeBoundsSafety::Initialize() {
242+
PluginManager::RegisterPlugin(GetPluginNameStatic(),
243+
"BoundsSafety instrumentation runtime plugin.",
244+
CreateInstance, GetTypeStatic);
245+
}
246+
247+
void InstrumentationRuntimeBoundsSafety::Terminate() {
248+
PluginManager::UnregisterPlugin(CreateInstance);
249+
}
250+
251+
lldb::InstrumentationRuntimeType
252+
InstrumentationRuntimeBoundsSafety::GetTypeStatic() {
253+
return lldb::eInstrumentationRuntimeTypeBoundsSafety;
254+
}
255+
256+
const RegularExpression &
257+
InstrumentationRuntimeBoundsSafety::GetPatternForRuntimeLibrary() {
258+
static RegularExpression regex;
259+
return regex;
260+
}
261+
262+
bool InstrumentationRuntimeBoundsSafety::CheckIfRuntimeIsValid(
263+
const lldb::ModuleSP module_sp) {
264+
265+
for (const auto &SoftTrapFunc : getBoundsSafetySoftTrapRuntimeFuncs()) {
266+
ConstString test_sym(SoftTrapFunc);
267+
268+
if (module_sp->FindFirstSymbolWithNameAndType(test_sym,
269+
lldb::eSymbolTypeAny))
270+
return true;
271+
}
272+
return false;
273+
}
274+
275+
bool InstrumentationRuntimeBoundsSafety::NotifyBreakpointHit(
276+
void *baton, StoppointCallbackContext *context, user_id_t break_id,
277+
user_id_t break_loc_id) {
278+
assert(baton && "null baton");
279+
if (!baton)
280+
return false; ///< false => resume execution.
281+
282+
InstrumentationRuntimeBoundsSafety *const instance =
283+
static_cast<InstrumentationRuntimeBoundsSafety *>(baton);
284+
285+
ProcessSP process_sp = instance->GetProcessSP();
286+
ThreadSP thread_sp = context->exe_ctx_ref.GetThreadSP();
287+
if (!process_sp || !thread_sp ||
288+
process_sp != context->exe_ctx_ref.GetProcessSP())
289+
return false;
290+
291+
if (process_sp->GetModIDRef().IsLastResumeForUserExpression())
292+
return false;
293+
294+
thread_sp->SetStopInfo(
295+
InstrumentationBoundsSafetyStopInfo::
296+
CreateInstrumentationBoundsSafetyStopInfo(*thread_sp));
297+
return true;
298+
}
299+
300+
void InstrumentationRuntimeBoundsSafety::Activate() {
301+
if (IsActive())
302+
return;
303+
304+
ProcessSP process_sp = GetProcessSP();
305+
if (!process_sp)
306+
return;
307+
308+
auto breakpoint = process_sp->GetTarget().CreateBreakpoint(
309+
/*containingModules=*/nullptr,
310+
/*containingSourceFiles=*/nullptr, getBoundsSafetySoftTrapRuntimeFuncs(),
311+
eFunctionNameTypeFull, eLanguageTypeUnknown,
312+
/*m_offset=*/0,
313+
/*skip_prologue*/ eLazyBoolNo,
314+
/*internal=*/true,
315+
/*request_hardware*/ false);
316+
317+
if (!breakpoint)
318+
return;
319+
320+
// Note: When `sync=true` the suggested stackframe is completely ignored. So
321+
// we use `sync=false`. Is that a bug?
322+
breakpoint->SetCallback(
323+
InstrumentationRuntimeBoundsSafety::NotifyBreakpointHit, this,
324+
/*sync=*/false);
325+
breakpoint->SetBreakpointKind("bounds-safety-soft-trap");
326+
SetBreakpointID(breakpoint->GetID());
327+
SetActive(true);
328+
}
329+
330+
void InstrumentationRuntimeBoundsSafety::Deactivate() {
331+
SetActive(false);
332+
if (ProcessSP process_sp = GetProcessSP())
333+
process_sp->GetTarget().RemoveBreakpointByID(GetBreakpointID());
334+
335+
SetBreakpointID(LLDB_INVALID_BREAK_ID);
336+
}

0 commit comments

Comments
 (0)