Skip to content

Commit e00002d

Browse files
JDevliegherehyjorc1
authored andcommitted
[JSC] Wasm debugger should resume breakpoints by dispatching the displaced opcode
https://bugs.webkit.org/show_bug.cgi?id=323845 rdar://187149541 Reviewed by Yijia Huang. A breakpoint patches the first byte of an instruction with `unreachable` so IPInt traps into the debugger. Resuming re-read the byte at PC and dispatched whatever it found, which is only correct while the patch happens to be gone. That holds when LLDB clears a site to step over it, and nowhere else: any resume with the patch still in place dispatches the patch itself and traps again at the same PC forever. Capture the displaced opcode when the trap is handled and return it to the interpreter, which dispatches that opcode instead of re-reading PC. The saved byte is what the program was going to run, so the resume is correct whether or not the patch is still there. A site on a real `unreachable` has no displaced opcode to replay: the instruction and the patch are the same byte. Report the breakpoint, then fall through to the trap path, so the program's own trap is still reported and propagates instead of being swallowed by the resume. Stepping off such a site has no successor to break on either, so the step resumes and lands on the trap. Key breakpoints on the physical PC rather than the virtual address. The patch is a property of a byte in module bytecode, which every instance of a module shares, and the PC is what the trap path has in hand. A user site and an internal step target can now name the same byte with the displaced opcode saved once between them; the patch is reference counted and restored when the last of the two goes away. Breakpoint holds a RefPtr to the ModuleInformation owning the bytecode it patched. Also stop crashing on a z0 for an address that holds no site. LLDB clears a site to step over it and can send z0 for a byte the server no longer breaks on, which used to hit a RELEASE_ASSERT. Reply OK, which is what a client that already dropped its own site expects. Test: BreakpointOnUnreachableTestCase and StepOffUnreachableTestCase cover the two ways a site on a real `unreachable` resumes, testPatchLifetime covers the reference counting and the byte-level restore. * JSTests/wasm/debugger/tests/tests.py: (BreakpointOnUnreachableTestCase): (BreakpointOnUnreachableTestCase.execute): (StepOffUnreachableTestCase): (StepOffUnreachableTestCase.execute): * Source/JavaScriptCore/llint/InPlaceInterpreter.asm: * Source/JavaScriptCore/llint/InPlaceInterpreter64.asm: * Source/JavaScriptCore/wasm/WasmIPIntSlowPaths.cpp: (JSC::IPInt::WASM_IPINT_EXTERN_CPP_DECL): * Source/JavaScriptCore/wasm/debugger/WasmBreakpointManager.cpp: (JSC::Wasm::BreakpointManager::hasOneTimeBreakpoints): (JSC::Wasm::BreakpointManager::ensurePatched): (JSC::Wasm::BreakpointManager::releasePatchIfUnused): (JSC::Wasm::BreakpointManager::setStepBreakpoint): (JSC::Wasm::BreakpointManager::setBreakpointAt): (JSC::Wasm::BreakpointManager::removeBreakpointAt): (JSC::Wasm::BreakpointManager::trapActionFor): (JSC::Wasm::BreakpointManager::clearAllOneTimeBreakpoints): (JSC::Wasm::BreakpointManager::clearAllBreakpoints): (JSC::Wasm::BreakpointManager::hasBreakpoints): Deleted. (JSC::Wasm::BreakpointManager::setBreakpoint): Deleted. (JSC::Wasm::BreakpointManager::findBreakpoint): Deleted. (JSC::Wasm::BreakpointManager::removeBreakpointImpl): Deleted. (JSC::Wasm::BreakpointManager::removeBreakpoint): Deleted. * Source/JavaScriptCore/wasm/debugger/WasmBreakpointManager.h: * Source/JavaScriptCore/wasm/debugger/WasmDebugServerUtilities.cpp: (JSC::Wasm::getWasmReturnPC): * Source/JavaScriptCore/wasm/debugger/WasmDebugServerUtilities.h: (JSC::Wasm::Breakpoint::create): (JSC::Wasm::Breakpoint::Breakpoint): (JSC::Wasm::Breakpoint::dump const): (JSC::Wasm::WasmReturnSite::operator bool const): * Source/JavaScriptCore/wasm/debugger/WasmExecutionHandler.cpp: (JSC::Wasm::ExecutionHandler::handleDebuggerTrapIfNeeded): (JSC::Wasm::ExecutionHandler::step): (JSC::Wasm::ExecutionHandler::stepAtBytecode): (JSC::Wasm::ExecutionHandler::setStepIntoBreakpointForCall): (JSC::Wasm::ExecutionHandler::setStepIntoBreakpointForThrow): (JSC::Wasm::ExecutionHandler::setStepBreakpointAtEntry): (JSC::Wasm::ExecutionHandler::requireModuleAddress): (JSC::Wasm::ExecutionHandler::setBreakpoint): (JSC::Wasm::ExecutionHandler::removeBreakpoint): (JSC::Wasm::ExecutionHandler::setBreakpointAtEntry): Deleted. (JSC::Wasm::ExecutionHandler::setBreakpointAtPC): Deleted. (JSC::Wasm::ExecutionHandler::hasBreakpoints const): Deleted. * Source/JavaScriptCore/wasm/debugger/WasmExecutionHandler.h: * Source/JavaScriptCore/wasm/debugger/tests/ExecutionHandlerTest.cpp: (ExecutionHandlerTest::entryAddress): (ExecutionHandlerTest::setBreakpointsAtAllFunctionEntries): (ExecutionHandlerTest::testBreakpointContinueCycles): (ExecutionHandlerTest::testBreakpointSingleStepping): (ExecutionHandlerTest::testPatchLifetime): (ExecutionHandlerTest::runTests): Canonical link: https://commits.webkit.org/320918@main
1 parent 9b09294 commit e00002d

11 files changed

Lines changed: 271 additions & 183 deletions

‎JSTests/wasm/debugger/tests/tests.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1550,6 +1550,35 @@ def execute(self):
15501550
)
15511551

15521552

1553+
class BreakpointOnUnreachableTestCase:
1554+
test_file = "resources/wasm/unreachable.js"
1555+
1556+
def execute(self):
1557+
# A breakpoint on unreachable reports the breakpoint first, then the trap.
1558+
self.session.cmd("b 0x4000000000000024", patterns=["Breakpoint 1"])
1559+
self.session.cmd(
1560+
"c",
1561+
patterns=["Process 1 stopped", "stop reason = breakpoint 1", "-> 0x4000000000000024: unreachable"],
1562+
)
1563+
self.session.cmd(
1564+
"c",
1565+
patterns=["Process 1 stopped", "Unreachable code should not be executed"],
1566+
)
1567+
1568+
1569+
class StepOffUnreachableTestCase:
1570+
test_file = "resources/wasm/unreachable.js"
1571+
1572+
def execute(self):
1573+
# Unreachable has no successor instruction, so a step off it lands on the trap.
1574+
self.session.cmd("b 0x4000000000000024", patterns=["Breakpoint 1"])
1575+
self.session.cmd(
1576+
"c",
1577+
patterns=["Process 1 stopped", "stop reason = breakpoint 1", "-> 0x4000000000000024: unreachable"],
1578+
)
1579+
self.session.cmd("si", patterns=["Unreachable code should not be executed"])
1580+
1581+
15531582
class WasmDivByZeroTrapTestCase:
15541583
test_file = "resources/wasm/trap-div-by-zero.js"
15551584

@@ -1756,6 +1785,8 @@ def execute(self):
17561785
WasmJsWasmJsWasmCallStackTestCase,
17571786
SwiftWasmCrashTestCase,
17581787
WasmUnreachableFaultTestCase,
1788+
BreakpointOnUnreachableTestCase,
1789+
StepOffUnreachableTestCase,
17591790
WasmDivByZeroTrapTestCase,
17601791
WasmOutOfBoundsCallIndirectTrapTestCase,
17611792
WasmStackOverflowTrapTestCase,

‎Source/JavaScriptCore/llint/InPlaceInterpreter.asm‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,14 +1162,11 @@ end
11621162

11631163
op(wasm_ipint_check_debugger_hook_and_throw_trap, macro ()
11641164
handleDebuggerTrapIfNeeded()
1165-
# r0 == 0 i.e. DebuggerTrapStatus::ResolvedByDebugger i.e. this was purely a debugger trap / breakpoint,
1166-
# and has been handled. We should continue executing because it's not a Wasm trap.
1167-
# r0 == 1 i.e. DebuggerTrapStatus::NotResolvedByDebugger i.e. this was a fatal Wasm trap. We should
1168-
# throw it to terminate Wasm execution.
1169-
btpz r0, .continue
1165+
# r0 is the displaced opcode to resume with, or Unreachable (0) to throw.
1166+
btpz r0, .throwTrap
1167+
dispatchIPIntOpcode(r0)
1168+
.throwTrap:
11701169
jmp _wasm_throw_from_slow_path_trampoline
1171-
.continue:
1172-
nextIPIntInstruction()
11731170
end)
11741171

11751172
op(wasm_throw_from_slow_path_trampoline, macro ()

‎Source/JavaScriptCore/llint/InPlaceInterpreter64.asm‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,23 +64,26 @@ end
6464

6565
# Tail-call bytecode dispatch
6666

67-
macro nextIPIntInstruction()
68-
loadb [PC], t0
67+
macro dispatchIPIntOpcode(opcode)
6968
if ARM64 or ARM64E
70-
# x0 = opcode
7169
pcrtoaddr ipint_dispatch_base, t7
72-
addlshiftp t7, t0, (constexpr (WTF::fastLog2(JSC::IPInt::alignIPInt))), t0
70+
addlshiftp t7, opcode, (constexpr (WTF::fastLog2(JSC::IPInt::alignIPInt))), t0
7371
jmp t0
7472
elsif X86_64
7573
pcrtoaddr ipint_dispatch_base, t1
76-
lshiftq (constexpr (WTF::fastLog2(JSC::IPInt::alignIPInt))), t0
77-
addq t1, t0
78-
jmp t0
74+
lshiftq (constexpr (WTF::fastLog2(JSC::IPInt::alignIPInt))), opcode
75+
addq t1, opcode
76+
jmp opcode
7977
else
8078
error
8179
end
8280
end
8381

82+
macro nextIPIntInstruction()
83+
loadb [PC], t0
84+
dispatchIPIntOpcode(t0)
85+
end
86+
8487
# Stack operations
8588
# Every value on the stack is always 16 bytes! This makes life easy.
8689

‎Source/JavaScriptCore/wasm/WasmIPIntSlowPaths.cpp‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1506,10 +1506,8 @@ static UNUSED_FUNCTION void displayWasmDebugState(JSWebAssemblyInstance* instanc
15061506

15071507
WASM_IPINT_EXTERN_CPP_DECL(handle_debugger_trap_if_needed, CallFrame* callFrame, Register* sp)
15081508
{
1509-
// By default, the trap is a fatal Wasm trap and must propagate (shouldThrow = true).
1510-
// If the debugger is connected and determines this was solely a debugger trap (e.g. a
1511-
// breakpoint on unreachable), it sets shouldThrow = false and execution resumes.
1512-
bool shouldThrow = true;
1509+
// Unreachable propagates the trap; otherwise resumes with the displaced opcode.
1510+
Wasm::OpType resumeOpcode = Wasm::OpType::Unreachable;
15131511
#if ENABLE(WEBASSEMBLY_DEBUGGER)
15141512
if (Options::enableWasmDebugger()) [[unlikely]] {
15151513
Wasm::DebugServer& debugServer = Wasm::DebugServer::singleton();
@@ -1521,16 +1519,15 @@ WASM_IPINT_EXTERN_CPP_DECL(handle_debugger_trap_if_needed, CallFrame* callFrame,
15211519
auto exceptionType = static_cast<Wasm::ExceptionType>(callFrame->argumentCountIncludingThis());
15221520
if (Options::verboseWasmDebugger() && exceptionType == Wasm::ExceptionType::Unreachable)
15231521
displayWasmDebugState(instance, callee, callFrame, stack);
1524-
auto trapStatus = debugServer.execution().handleDebuggerTrapIfNeeded(callFrame, instance, callee, pc, mc, stack, exceptionType);
1525-
shouldThrow = trapStatus == Wasm::DebuggerTrapStatus::NotResolvedByDebugger;
1522+
resumeOpcode = debugServer.execution().handleDebuggerTrapIfNeeded(callFrame, instance, callee, pc, mc, stack, exceptionType);
15261523
}
15271524
}
15281525
#else
15291526
UNUSED_PARAM(instance);
15301527
UNUSED_PARAM(callFrame);
15311528
UNUSED_PARAM(sp);
15321529
#endif
1533-
IPINT_RETURN(static_cast<EncodedJSValue>(static_cast<int32_t>(shouldThrow)));
1530+
IPINT_RETURN(static_cast<EncodedJSValue>(static_cast<uint32_t>(resumeOpcode)));
15341531
}
15351532

15361533
} } // namespace JSC::IPInt

‎Source/JavaScriptCore/wasm/debugger/WasmBreakpointManager.cpp‎

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -44,58 +44,90 @@ BreakpointManager::~BreakpointManager()
4444
clearAllBreakpoints();
4545
}
4646

47-
bool BreakpointManager::hasBreakpoints()
47+
bool BreakpointManager::hasOneTimeBreakpoints()
4848
{
4949
Locker locker { m_lock };
50-
return !m_breakpoints.isEmpty();
50+
return !m_oneTimeBreakpoints.isEmpty();
5151
}
5252

53-
bool BreakpointManager::hasOneTimeBreakpoints()
53+
Breakpoint& BreakpointManager::ensurePatched(const ModuleInformation& owner, uint8_t* pc)
5454
{
55-
Locker locker { m_lock };
56-
return !m_oneTimeBreakpoints.isEmpty();
55+
RELEASE_ASSERT(pc);
56+
if (auto it = m_breakpoints.find(pc); it != m_breakpoints.end()) {
57+
dataLogLnIf(Options::verboseWasmDebugger(), "[BreakpointManager] Reusing the patch at ", RawPointer(pc));
58+
return it->value;
59+
}
60+
61+
Ref<Breakpoint> breakpoint = Breakpoint::create(owner, pc);
62+
breakpoint->patchBreakpoint();
63+
dataLogLnIf(Options::verboseWasmDebugger(), "[BreakpointManager] Patched ", breakpoint);
64+
return m_breakpoints.set(pc, WTF::move(breakpoint)).iterator->value;
5765
}
5866

59-
void BreakpointManager::setBreakpoint(VirtualAddress address, Ref<Breakpoint>&& breakpoint)
67+
void BreakpointManager::releasePatchIfUnused(uint8_t* pc)
68+
{
69+
auto it = m_breakpoints.find(pc);
70+
RELEASE_ASSERT(it != m_breakpoints.end());
71+
if (it->value->hasSite || m_oneTimeBreakpoints.contains(pc))
72+
return;
73+
74+
dataLogLnIf(Options::verboseWasmDebugger(), "[BreakpointManager] Restoring ", it->value);
75+
it->value->restorePatch();
76+
m_breakpoints.remove(it);
77+
}
78+
79+
void BreakpointManager::setStepBreakpoint(const ModuleInformation& owner, uint8_t* pc)
6080
{
6181
Locker locker { m_lock };
62-
breakpoint->patchBreakpoint();
63-
dataLogLnIf(Options::verboseWasmDebugger(), "[BreakpointManager] setBreakpoint ", breakpoint, " at moduleAddress:", address);
64-
if (breakpoint->isOneTimeBreakpoint())
65-
m_oneTimeBreakpoints.add(address);
66-
m_breakpoints.set(address, WTF::move(breakpoint));
82+
ensurePatched(owner, pc);
83+
m_oneTimeBreakpoints.add(pc);
6784
}
6885

69-
RefPtr<Breakpoint> BreakpointManager::findBreakpoint(VirtualAddress address)
86+
void BreakpointManager::setBreakpointAt(VirtualAddress address, const ModuleInformation& owner, uint8_t* pc)
7087
{
7188
Locker locker { m_lock };
72-
return m_breakpoints.get(address);
89+
// Re-arming an existing site is a no-op.
90+
auto result = m_addressToPC.add(address, pc);
91+
if (!result.isNewEntry) {
92+
RELEASE_ASSERT(result.iterator->value == pc);
93+
return;
94+
}
95+
ensurePatched(owner, pc).hasSite = true;
7396
}
7497

75-
bool BreakpointManager::removeBreakpointImpl(VirtualAddress address)
98+
bool BreakpointManager::removeBreakpointAt(VirtualAddress address)
7699
{
77-
auto it = m_breakpoints.find(address);
100+
Locker locker { m_lock };
101+
uint8_t* pc = m_addressToPC.take(address);
102+
if (!pc)
103+
return false;
104+
105+
auto it = m_breakpoints.find(pc);
78106
RELEASE_ASSERT(it != m_breakpoints.end());
79-
dataLogLnIf(Options::verboseWasmDebugger(), "[BreakpointManager] Removing breakpoint ", it->value, " at ", address);
80-
it->value->restorePatch();
81-
m_breakpoints.remove(it);
107+
it->value->hasSite = false;
108+
releasePatchIfUnused(pc);
82109
return true;
83110
}
84111

85-
bool BreakpointManager::removeBreakpoint(VirtualAddress address)
112+
std::optional<BreakpointManager::TrapAction> BreakpointManager::trapActionFor(uint8_t* pc)
86113
{
87114
Locker locker { m_lock };
88-
bool removed = removeBreakpointImpl(address);
89-
RELEASE_ASSERT(removed);
90-
return true;
115+
auto it = m_breakpoints.find(pc);
116+
if (it == m_breakpoints.end())
117+
return std::nullopt;
118+
119+
// A breakpoint site takes precedence over a step.
120+
Breakpoint::Type stopType = it->value->hasSite ? Breakpoint::Type::Regular : Breakpoint::Type::Step;
121+
return TrapAction { static_cast<OpType>(it->value->originalBytecode), stopType };
91122
}
92123

93124
void BreakpointManager::clearAllOneTimeBreakpoints()
94125
{
95126
Locker locker { m_lock };
96-
for (VirtualAddress address : m_oneTimeBreakpoints)
97-
removeBreakpointImpl(address);
98-
m_oneTimeBreakpoints.clear();
127+
// Cleared first so releasePatchIfUnused can free unreferenced patches.
128+
auto steppedPCs = std::exchange(m_oneTimeBreakpoints, { });
129+
for (uint8_t* pc : steppedPCs)
130+
releasePatchIfUnused(pc);
99131
dataLogLnIf(Options::verboseWasmDebugger(), "[BreakpointManager] Cleared all one-time breakpoints");
100132
}
101133

@@ -105,7 +137,8 @@ void BreakpointManager::clearAllBreakpoints()
105137
for (auto& [_, breakpoint] : m_breakpoints)
106138
breakpoint->restorePatch();
107139
m_breakpoints.clear();
108-
RELEASE_ASSERT(m_oneTimeBreakpoints.isEmpty());
140+
m_oneTimeBreakpoints.clear();
141+
m_addressToPC.clear();
109142
}
110143

111144
} // namespace Wasm

‎Source/JavaScriptCore/wasm/debugger/WasmBreakpointManager.h‎

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
#include "WasmDebugServerUtilities.h"
3131
#include "WasmVirtualAddress.h"
32+
#include <optional>
3233
#include <wtf/Forward.h>
3334
#include <wtf/HashMap.h>
3435
#include <wtf/HashSet.h>
@@ -39,28 +40,39 @@
3940
namespace JSC {
4041
namespace Wasm {
4142

43+
// A bytecode patch persists while either a breakpoint site or single-step references it.
4244
class JS_EXPORT_PRIVATE BreakpointManager {
4345
WTF_MAKE_TZONE_ALLOCATED(BreakpointManager);
4446

4547
public:
4648
BreakpointManager() = default;
4749
~BreakpointManager();
4850

49-
bool hasBreakpoints();
51+
struct TrapAction {
52+
OpType displacedOpcode { OpType::Unreachable };
53+
Breakpoint::Type stopType { Breakpoint::Type::Regular };
54+
};
55+
5056
bool hasOneTimeBreakpoints();
5157

52-
RefPtr<Breakpoint> findBreakpoint(VirtualAddress);
53-
void setBreakpoint(VirtualAddress, Ref<Breakpoint>&&);
54-
bool removeBreakpoint(VirtualAddress);
58+
std::optional<TrapAction> trapActionFor(uint8_t* pc);
59+
60+
void setStepBreakpoint(const ModuleInformation& owner, uint8_t* pc);
5561
void clearAllOneTimeBreakpoints();
62+
63+
void setBreakpointAt(VirtualAddress, const ModuleInformation& owner, uint8_t* pc);
64+
bool removeBreakpointAt(VirtualAddress);
65+
5666
void clearAllBreakpoints();
5767

5868
private:
59-
bool removeBreakpointImpl(VirtualAddress) WTF_REQUIRES_LOCK(m_lock);
69+
Breakpoint& ensurePatched(const ModuleInformation& owner, uint8_t* pc) WTF_REQUIRES_LOCK(m_lock);
70+
void releasePatchIfUnused(uint8_t* pc) WTF_REQUIRES_LOCK(m_lock);
6071

6172
mutable Lock m_lock;
62-
UncheckedKeyHashMap<VirtualAddress, Ref<Breakpoint>> m_breakpoints WTF_GUARDED_BY_LOCK(m_lock);
63-
UncheckedKeyHashSet<VirtualAddress> m_oneTimeBreakpoints WTF_GUARDED_BY_LOCK(m_lock);
73+
UncheckedKeyHashMap<uint8_t*, Ref<Breakpoint>> m_breakpoints WTF_GUARDED_BY_LOCK(m_lock);
74+
UncheckedKeyHashSet<uint8_t*> m_oneTimeBreakpoints WTF_GUARDED_BY_LOCK(m_lock);
75+
UncheckedKeyHashMap<VirtualAddress, uint8_t*> m_addressToPC WTF_GUARDED_BY_LOCK(m_lock);
6476
};
6577

6678
} // namespace Wasm

‎Source/JavaScriptCore/wasm/debugger/WasmDebugServerUtilities.cpp‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -154,28 +154,25 @@ static const uint8_t* savedWasmToJSPC(CallFrame* wasmToJSFrame)
154154
return WTF::unalignedLoad<const uint8_t*>(reinterpret_cast<const uint8_t*>(wasmToJSFrame) + Wasm::WasmToJSIPIntReturnPCSlot);
155155
}
156156

157-
bool getWasmReturnPC(CallFrame* currentFrame, uint8_t*& returnPC, VirtualAddress& virtualReturnPC)
157+
WasmReturnSite getWasmReturnPC(CallFrame* currentFrame)
158158
{
159159
// Safe to use the non-EntryFrame overload: IPInt WASM frames are always entered via a
160160
// JSToWasm trampoline (a normal CallFrame), never directly from C++, so no EntryFrame
161161
// boundary can appear immediately above an IPInt frame.
162162
CallFrame* callerFrame = currentFrame->callerFrame();
163163

164164
if (!callerFrame->callee().isNativeCallee())
165-
return false;
165+
return { };
166166

167167
RefPtr caller = callerFrame->callee().asNativeCallee();
168168
if (caller->category() != NativeCallee::Category::Wasm)
169-
return false;
169+
return { };
170170

171171
RefPtr wasmCaller = uncheckedDowncast<const Wasm::Callee>(caller.get());
172172
if (wasmCaller->compilationMode() != Wasm::CompilationMode::IPIntMode)
173-
return false;
173+
return { };
174174

175-
RefPtr ipintCaller = uncheckedDowncast<const Wasm::IPIntCallee>(wasmCaller.get());
176-
returnPC = const_cast<uint8_t*>(savedWasmToWasmPC(currentFrame));
177-
virtualReturnPC = VirtualAddress::toVirtual(callerFrame->wasmInstance(), ipintCaller->functionIndex(), returnPC);
178-
return true;
175+
return { const_cast<uint8_t*>(savedWasmToWasmPC(currentFrame)), callerFrame->wasmInstance() };
179176
}
180177

181178

0 commit comments

Comments
 (0)