Skip to content

Commit 2f3246a

Browse files
committed
[JSC][WASM][Debugger] Make the WASM debugger stop at unreachable traps
https://bugs.webkit.org/show_bug.cgi?id=310858 rdar://173465318 Reviewed by Keith Miller. The WASM debugger did not intercept unreachable traps — they propagated as JS exceptions with no debugger notification, making it impossible to catch crash-style faults in WASM programs from LLDB. This patch teaches the WASM debugger to intercept unreachable traps and report them as debugger stops, allowing LLDB to inspect the call stack and locals at the point of the fault. A Swift WASM test package and a minimal hand-crafted WASM test are added to cover the feature. Tests: * JSTests/wasm/debugger/tests/tests.py: (SwiftWasmCrashTestCase): (WasmUnreachableFaultTestCase): Canonical link: https://commits.webkit.org/310104@main
1 parent ec80755 commit 2f3246a

20 files changed

Lines changed: 304 additions & 59 deletions

‎JSTests/wasm/debugger/resources/swift-wasm/build.sh‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ if [ -d "$SCRIPT_DIR/$FOLDER" ]; then
3333
# Build Swift WASM with Swiftly https://www.swift.org/documentation/articles/wasm-getting-started.html
3434
echo "Building Swift WebAssembly $FOLDER..."
3535
cd "$SCRIPT_DIR/$FOLDER"
36-
"$SWIFT" build --swift-sdk "$SDK"
36+
"$SWIFT" build --swift-sdk "$SDK" --configuration release
3737

3838
# Move compiled WASM beside main.js
3939
echo "Moving $FOLDER.wasm..."
40-
mv ".build/wasm32-unknown-wasip1/debug/$FOLDER.wasm" "./$FOLDER.wasm"
40+
mv ".build/wasm32-unknown-wasip1/release/$FOLDER.wasm" "./$FOLDER.wasm"
4141

4242
# Clean up build artifacts
4343
echo "Cleaning up..."
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// swift-tools-version: 6.2
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "crash-test",
6+
targets: [
7+
.executableTarget(
8+
name: "crash-test",
9+
path: "Sources/crash-test",
10+
linkerSettings: [
11+
.unsafeFlags(["-Xlinker", "--export=crash_on_zero"])
12+
]
13+
)
14+
]
15+
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
@inline(never)
2+
private func _overflowTrap(_ x: Int32) -> Int32 { return x + 1 }
3+
4+
@_cdecl("crash_on_zero")
5+
public func crashOnZero(_ value: Int32) -> Int32 {
6+
if value == 0 {
7+
let _ = _overflowTrap(Int32.max)
8+
}
9+
return value * 2
10+
}
11+
12+
@main
13+
struct CrashTest {
14+
static func main() {
15+
_ = crashOnZero
16+
}
17+
}
Binary file not shown.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
var wasm_code = read('crash-test.wasm', 'binary');
2+
var wasm_module = new WebAssembly.Module(wasm_code);
3+
var imports = {
4+
wasi_snapshot_preview1: {
5+
proc_exit: function (code) {
6+
print("Program exited with code:", code);
7+
},
8+
args_get: function () { return 0; },
9+
args_sizes_get: function () { return 0; },
10+
environ_get: function () { return 0; },
11+
environ_sizes_get: function () { return 0; },
12+
fd_write: function () { return 0; },
13+
fd_read: function () { return 0; },
14+
fd_close: function () { return 0; },
15+
fd_seek: function () { return 0; },
16+
fd_fdstat_get: function () { return 0; },
17+
fd_prestat_get: function () { return 8; },
18+
fd_prestat_dir_name: function () { return 8; },
19+
path_open: function () { return 8; },
20+
random_get: function () { return 0; },
21+
clock_time_get: function () { return 0; }
22+
}
23+
};
24+
25+
var instance = new WebAssembly.Instance(wasm_module, imports);
26+
27+
print("Available exports:", Object.keys(instance.exports));
28+
29+
let crashOnZero = instance.exports.crash_on_zero;
30+
31+
let iteration = 0;
32+
for (;;) {
33+
try {
34+
crashOnZero(0);
35+
} catch (e) {
36+
// When the debugger is not yet connected the Swift fatalError propagates as a
37+
// JS exception. Catch and retry so the process stays alive until LLDB connects.
38+
//
39+
// When the debugger IS connected, hitFault() blocks the VM before the exception
40+
// reaches here, so this catch is never taken on a debugger-intercepted fault.
41+
}
42+
iteration += 1;
43+
if (iteration % 1e5 == 0)
44+
print("iteration=", iteration);
45+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
var wasm = new Uint8Array([
2+
// [0x00] WASM header
3+
0x00, 0x61, 0x73, 0x6d, // magic: \0asm
4+
0x01, 0x00, 0x00, 0x00, // version: 1
5+
6+
// [0x08] Type section: 1 type, func () -> ()
7+
0x01, 0x04, 0x01, 0x60, 0x00, 0x00,
8+
9+
// [0x0e] Function section: 1 function, type index 0
10+
0x03, 0x02, 0x01, 0x00,
11+
12+
// [0x12] Export section: export function 0 as "crasher"
13+
0x07, 0x0b, 0x01, 0x07, 0x63, 0x72, 0x61, 0x73, 0x68, 0x65, 0x72, 0x00, 0x00,
14+
15+
// [0x1f] Code section
16+
0x0a, // section id = 10
17+
0x05, // section size = 5
18+
0x01, // 1 function body
19+
0x03, // body size = 3
20+
0x00, // 0 local declarations
21+
0x00, // [0x24] unreachable
22+
0x0b // [0x25] end
23+
]);
24+
25+
var module = new WebAssembly.Module(wasm);
26+
var instance = new WebAssembly.Instance(module);
27+
let crasher = instance.exports.crasher;
28+
29+
let iteration = 0;
30+
for (; ;) {
31+
try {
32+
crasher();
33+
} catch (e) {
34+
35+
}
36+
iteration += 1;
37+
if (iteration % 1e5 == 0)
38+
print("iteration=", iteration);
39+
}

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1766,3 +1766,64 @@ def callStackTest(self):
17661766
"frame #5: 0xc000000000000000", # JS main loop
17671767
],
17681768
)
1769+
1770+
1771+
class SwiftWasmCrashTestCase(BaseTestCase):
1772+
1773+
def __init__(self, build_config: str = None, port: int = None):
1774+
super().__init__(build_config, port)
1775+
1776+
def execute(self):
1777+
self.setup_debugging_session_or_raise("resources/swift-wasm/crash-test/main.js")
1778+
1779+
try:
1780+
for _ in range(1):
1781+
self.faultTest()
1782+
1783+
except Exception as e:
1784+
raise Exception(f"Test failed: {e}")
1785+
1786+
def faultTest(self):
1787+
for _ in range(10):
1788+
self.send_lldb_command_or_raise("c", patterns=["Process 1 stopped"])
1789+
self.send_lldb_command_or_raise("dis", patterns=["-> 0x4000000000010a43 <+1>: unreachable"]);
1790+
1791+
self.send_lldb_command_or_raise(
1792+
"bt",
1793+
patterns=[
1794+
"frame #0: 0x4000000000010a43",
1795+
"frame #1: 0x4000000000010a43",
1796+
"frame #2: 0x4000000000010a6a",
1797+
"frame #3: 0x4000000000010a46",
1798+
"frame #4: 0xc000000000000000",
1799+
]
1800+
)
1801+
1802+
1803+
class WasmUnreachableFaultTestCase(BaseTestCase):
1804+
1805+
def __init__(self, build_config: str = None, port: int = None):
1806+
super().__init__(build_config, port)
1807+
1808+
def execute(self):
1809+
self.setup_debugging_session_or_raise("resources/wasm/unreachable.js")
1810+
1811+
try:
1812+
for _ in range(1):
1813+
self.faultTest()
1814+
1815+
except Exception as e:
1816+
raise Exception(f"Test failed: {e}")
1817+
1818+
def faultTest(self):
1819+
for _ in range(10):
1820+
self.send_lldb_command_or_raise("c", patterns=["Process 1 stopped"])
1821+
self.send_lldb_command_or_raise("dis", patterns=["-> 0x4000000000000024: unreachable"]);
1822+
1823+
self.send_lldb_command_or_raise(
1824+
"bt",
1825+
patterns=[
1826+
"frame #0: 0x4000000000000024",
1827+
"frame #1: 0xc000000000000000",
1828+
]
1829+
)

‎Source/JavaScriptCore/llint/InPlaceInterpreter64.asm‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ ipintOp(_unreachable, macro()
243243

244244
move cfr, a1
245245
move sp, a2
246-
operationCall(macro() cCall3(_ipint_extern_unreachable_breakpoint_handler) end)
246+
operationCall(macro() cCall3(_ipint_extern_unreachable_handler) end)
247247

248248
# Remove pushed values
249249
addq 4 * SlotSize, sp

‎Source/JavaScriptCore/runtime/StopTheWorldCallback.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ enum class StopTheWorldEvent : uint8_t {
4949
VMActivated,
5050
VMStopped,
5151
BreakpointHit,
52+
TrapHit,
5253
StepIntoSiteReached,
5354
};
5455

‎Source/JavaScriptCore/wasm/WasmIPIntSlowPaths.cpp‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,14 +1327,13 @@ static UNUSED_FUNCTION void displayWasmDebugState(JSWebAssemblyInstance* instanc
13271327
}
13281328
#endif
13291329

1330-
WASM_IPINT_EXTERN_CPP_DECL(unreachable_breakpoint_handler, CallFrame* callFrame, Register* sp)
1330+
WASM_IPINT_EXTERN_CPP_DECL(unreachable_handler, CallFrame* callFrame, Register* sp)
13311331
{
1332-
dataLogLnIf(Options::verboseWasmDebugger(), "[Code][unreachable] Start");
13331332
bool breakpointHandled = false;
13341333
#if ENABLE(WEBASSEMBLY_DEBUGGER)
13351334
if (Options::enableWasmDebugger()) [[unlikely]] {
13361335
Wasm::DebugServer& debugServer = Wasm::DebugServer::singleton();
1337-
if (debugServer.needToHandleBreakpoints()) {
1336+
if (debugServer.shouldHandleUnreachable()) {
13381337
uint8_t* pc = static_cast<uint8_t*>(sp[2].pointer());
13391338
uint8_t* mc = static_cast<uint8_t*>(sp[3].pointer());
13401339
IPIntLocal* pl = static_cast<IPIntLocal*>(sp[0].pointer());
@@ -1343,15 +1342,15 @@ WASM_IPINT_EXTERN_CPP_DECL(unreachable_breakpoint_handler, CallFrame* callFrame,
13431342
IPIntStackEntry* stackPointer = std::bit_cast<IPIntStackEntry*>(sp + 4);
13441343
if (Options::verboseWasmDebugger())
13451344
displayWasmDebugState(instance, callee, stackPointer, pl);
1346-
breakpointHandled = debugServer.execution().hitBreakpoint(callFrame, instance, callee, pc, mc, pl, stackPointer);
1345+
1346+
breakpointHandled = debugServer.execution().handleUnreachable(callFrame, instance, callee, pc, mc, pl, stackPointer);
13471347
}
13481348
}
13491349
#else
13501350
UNUSED_PARAM(instance);
13511351
UNUSED_PARAM(callFrame);
13521352
UNUSED_PARAM(sp);
13531353
#endif
1354-
dataLogLnIf(Options::verboseWasmDebugger(), "[Code][unreachable] Done with breakpointHandled=", breakpointHandled);
13551354
IPINT_RETURN(static_cast<EncodedJSValue>(static_cast<int32_t>(breakpointHandled)));
13561355
}
13571356

0 commit comments

Comments
 (0)