Skip to content
Closed
Show file tree
Hide file tree
Changes from 66 commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
05ea1a2
Initial work on supporting SIGUSR1
alii Dec 11, 2025
91bfb9f
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 11, 2025
f0f5d17
signal safe handler, some other review notes
alii Dec 11, 2025
3e15ddc
be clear about idempotency
alii Dec 11, 2025
5e504db
a cheap shot at windows
alii Dec 11, 2025
0a40bb5
consolidate windows/posix inspector logic with a new struct "RuntimeI…
alii Dec 11, 2025
e474a1d
get it to compile
alii Dec 11, 2025
a859227
implement process._debugProcess
alii Dec 11, 2025
997c776
split up runtime inspector tests
alii Dec 11, 2025
d1924b8
add --disable-sigusr1
alii Dec 11, 2025
6aeadbf
fix --disable-sigusr1 test for CI
alii Dec 11, 2025
1130675
fix ban-words: use bun.strings.toWPath and proper struct defaults
alii Dec 11, 2025
8752473
rm semaphore
alii Dec 12, 2025
1366c69
cleaner
alii Dec 12, 2025
c25572e
Add tests for SIGUSR1 handling with --inspect-* flags
Dec 12, 2025
a028ee9
fix(test): replace timing-based waits with condition-based waits in S…
alii Jan 5, 2026
60b7424
fix(test): replace timing-based waits with condition-based waits in W…
alii Jan 5, 2026
19fa3d3
fix(test): replace timing-based waits with condition-based waits in r…
alii Jan 5, 2026
1a70d18
docs: document inspector port limitation in RuntimeInspector
alii Jan 5, 2026
1d2becb
test: assert exit codes for debug helper processes in runtime-inspect…
alii Jan 5, 2026
93de1c3
test: improve exit code assertion clarity using toBeOneOf
alii Jan 5, 2026
d607391
Add comment documenting alignment assumption for MapViewOfFile
alii Jan 5, 2026
0ae67c7
refactor: extract configureSigusr1Handler helper function
alii Jan 5, 2026
cc6704d
Address PR review comments for runtime inspector
alii Jan 5, 2026
856eda2
fix(test): update Windows _debugProcess test to match Bun's error mes…
alii Jan 5, 2026
f188b93
fix: match Node.js error message for _debugProcess on Windows
alii Jan 5, 2026
d28affd
Skip failing test on Windows for now
Jarred-Sumner Jan 6, 2026
d662557
Merge branch 'main' into ali/siguser1
alii Jan 7, 2026
57efbd0
update test expectations, dont print tiwce
alii Jan 7, 2026
c06ef30
fix review comments
alii Jan 7, 2026
ebcfbd2
Merge branch 'main' into ali/siguser1
Jarred-Sumner Jan 8, 2026
aec9e81
Merge branch 'main' into ali/siguser1
alii Jan 9, 2026
b848ac2
use a semaphore and properly inject jsc trap
alii Jan 9, 2026
0869bd7
do a best effort uninstall case
alii Jan 9, 2026
3b67f3f
Merge branch 'main' into ali/siguser1
alii Jan 9, 2026
d9b396a
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 9, 2026
8990dfc
use VMManager StopTheWorld to interrupt infinite loops for SIGUSR1 i…
alii Jan 10, 2026
21fb83e
wip
Jarred-Sumner Jan 11, 2026
f758a5f
Upgrade WebKit to d5bd162d9ab2
Jarred-Sumner Jan 11, 2026
ef5b11c
use new WebKit version
alii Jan 12, 2026
e8f40c2
Merge branch 'main' into ali/siguser1
alii Jan 12, 2026
c83254a
Update WebKit to preview-pr-135-a6fa914b
sosukesuzuki Jan 13, 2026
e7ef32e
Update WEBKIT_VERSION
sosukesuzuki Jan 13, 2026
3466088
Fix InternalPromise exposure in ReadableStream builtins
sosukesuzuki Jan 13, 2026
75a7ee5
Merge branch 'main' into ali/siguser1
alii Jan 13, 2026
f2a6c7c
Merge remote-tracking branch 'origin/jarred/webkit-upgrade-jan-10' in…
Jan 13, 2026
9195e68
Merge branch 'main' into ali/sigusr1
alii Jan 14, 2026
098bcfa
address coderabbit & DRYify some code that claude wrote twice
alii Jan 14, 2026
1f70115
address review
alii Jan 14, 2026
6bd0cf3
fix a case where we'd have a stale jsc trap for a second `kill -USR1 …
alii Jan 14, 2026
83a78f3
fix race where a second SIGUSR1 leaves stale trap
alii Jan 14, 2026
e4cd00d
Merge branch 'main' into ali/sigusr1
alii Jan 14, 2026
88b19a8
Merge branch 'main' into ali/sigusr1
alii Jan 14, 2026
a461b72
fix: clean up semaphore if thread spawn fails
Jan 14, 2026
51e26fd
test: check stderr before exit code for better error messages
Jan 14, 2026
e3baed5
docs: add comment explaining QueuedTask payload parameter
Jan 14, 2026
4c10814
test: await stderr before checking exit code in runtime-inspector tests
Jan 14, 2026
32ead03
test: await debug2 stderr before checking exit code
Jan 14, 2026
75c127f
Merge branch 'main' into ali/sigusr1
alii Jan 15, 2026
82bf72a
Merge remote-tracking branch 'origin/main' into claude/merge-sigusr1
Jan 16, 2026
1c88d40
Remove comment
Jan 16, 2026
d780bf1
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 16, 2026
f6189c7
Revert ReadableStreamInternals.ts changes
Jan 16, 2026
f2a1350
Revert Request.zig import reordering
Jan 16, 2026
82eb3f3
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 16, 2026
37cbb0c
Skip flaky SIGUSR1 inspector tests on ASAN builds
Jan 16, 2026
3800722
Address CodeRabbit review comments
Jan 17, 2026
ad01185
Merge main into ali/sigusr1
Jan 27, 2026
084f565
Fix runtime inspector tests: timing and stream reader issues
Jan 27, 2026
d819416
Fix runtime inspector tests to wait for conditions, not time
Jan 27, 2026
cf53710
Merge branch 'main' into ali/sigusr1
alii Feb 2, 2026
5024e20
fix(sigusr1): handle EINTR in sem_wait and skip ASAN for flaky tests
Feb 2, 2026
a2ba9cb
fix: address review comments
Feb 2, 2026
b97f3f3
fix: skip ASAN for flaky inspector test and add timeout handling
Feb 3, 2026
1a6b804
fix: improve ASAN detection to check ASAN_OPTIONS env var
Feb 3, 2026
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 src/bun.js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ pub const Run = struct {
.debugger = ctx.runtime_options.debugger,
.dns_result_order = DNSResolver.Order.fromStringOrDie(ctx.runtime_options.dns_result_order),
.is_main_thread = true,
.disable_sigusr1 = ctx.runtime_options.disable_sigusr1,
},
),
.arena = arena,
Expand Down
24 changes: 24 additions & 0 deletions src/bun.js/VirtualMachine.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,8 @@ pub fn initWithModuleGraph(
vm.configureDebugger(opts.debugger);
vm.body_value_hive_allocator = Body.Value.HiveAllocator.init(bun.typedAllocator(jsc.WebCore.Body.Value));

configureSigusr1Handler(vm, opts);

return vm;
}

Expand All @@ -1100,8 +1102,26 @@ pub const Options = struct {
/// Worker VMs are always destroyed on exit, regardless of this setting. Setting this to
/// true may expose bugs that would otherwise only occur using Workers.
destruct_main_thread_on_exit: bool = false,
/// Disable SIGUSR1 handler for runtime debugger activation (matches Node.js).
disable_sigusr1: bool = false,
};

/// Configure SIGUSR1 handling for runtime debugger activation (main thread only).
fn configureSigusr1Handler(vm: *const VirtualMachine, opts: Options) void {
if (!opts.is_main_thread) return;

if (opts.disable_sigusr1) {
// User requested --disable-sigusr1, set SIGUSR1 to default action (terminate)
jsc.EventLoop.RuntimeInspector.setDefaultSigusr1Action();
} else if (vm.debugger != null) {
// Debugger already enabled via CLI flags, ignore SIGUSR1
jsc.EventLoop.RuntimeInspector.ignoreSigusr1();
} else {
// Install RuntimeInspector signal handler for runtime activation
jsc.EventLoop.RuntimeInspector.installIfNotAlready();
}
}

pub var is_smol_mode = false;

pub fn init(opts: Options) !*VirtualMachine {
Expand Down Expand Up @@ -1201,6 +1221,8 @@ pub fn init(opts: Options) !*VirtualMachine {
vm.configureDebugger(opts.debugger);
vm.body_value_hive_allocator = Body.Value.HiveAllocator.init(bun.typedAllocator(jsc.WebCore.Body.Value));

configureSigusr1Handler(vm, opts);

return vm;
}

Expand Down Expand Up @@ -1451,6 +1473,8 @@ pub fn initBake(opts: Options) anyerror!*VirtualMachine {
vm.configureDebugger(opts.debugger);
vm.body_value_hive_allocator = Body.Value.HiveAllocator.init(bun.typedAllocator(jsc.WebCore.Body.Value));

configureSigusr1Handler(vm, opts);

return vm;
}

Expand Down
35 changes: 35 additions & 0 deletions src/bun.js/bindings/BunDebugger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#include "ZigGlobalObject.h"

#include <JavaScriptCore/InspectorFrontendChannel.h>
#include <JavaScriptCore/StopTheWorldCallback.h>
#include <JavaScriptCore/VMManager.h>
#include <JavaScriptCore/JSGlobalObjectDebuggable.h>
#include <JavaScriptCore/JSGlobalObjectDebugger.h>
#include <JavaScriptCore/Debugger.h>
Expand Down Expand Up @@ -659,3 +661,36 @@ extern "C" void Debugger__willDispatchAsyncCall(JSGlobalObject* globalObject, As
agent->willDispatchAsyncCall(getCallType(callType), callbackId);
}
}

// StopTheWorld callback for SIGUSR1 debugger activation.
// This runs on the main thread at a safe point when VMManager::requestStopAll(JSDebugger) is called.
//
// This handles the case where JS is actively executing (including infinite loops).
// For idle VMs, RuntimeInspector::checkAndActivateInspector handles it via event loop.

extern "C" bool Bun__activateInspector();

JSC::StopTheWorldStatus Bun__jsDebuggerCallback(JSC::VM& vm, JSC::StopTheWorldEvent event)
{
using namespace JSC;

if (event != StopTheWorldEvent::VMStopped)
return STW_CONTINUE();

if (Bun__activateInspector()) {
vm.notifyNeedDebuggerBreak();
}

return STW_RESUME_ALL();
}

// Zig bindings for VMManager
extern "C" void VMManager__requestStopAll(uint32_t reason)
{
JSC::VMManager::requestStopAll(static_cast<JSC::VMManager::StopReason>(reason));
}

extern "C" void VMManager__requestResumeAll(uint32_t reason)
{
JSC::VMManager::requestResumeAll(static_cast<JSC::VMManager::StopReason>(reason));
}
78 changes: 77 additions & 1 deletion src/bun.js/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1346,6 +1346,9 @@ extern "C" bool Bun__shouldIgnoreOneDisconnectEventListener(JSC::JSGlobalObject*
extern "C" void Bun__ensureSignalHandler();
extern "C" bool Bun__isMainThreadVM();
extern "C" void Bun__onPosixSignal(int signalNumber);
#ifdef SIGUSR1
extern "C" void Bun__Sigusr1Handler__uninstall();
#endif

__attribute__((noinline)) static void forwardSignal(int signalNumber)
{
Expand Down Expand Up @@ -1504,6 +1507,14 @@ static void onDidChangeListeners(EventEmitter& eventEmitter, const Identifier& e
action.sa_flags = SA_RESTART;

sigaction(signalNumber, &action, nullptr);

#ifdef SIGUSR1
// When user adds a SIGUSR1 listener, uninstall the automatic
// inspector activation handler. User handlers take precedence.
if (signalNumber == SIGUSR1) {
Bun__Sigusr1Handler__uninstall();
}
#endif
#else
signal_handle.handle = Bun__UVSignalHandle__init(
eventEmitter.scriptExecutionContext()->jsGlobalObject(),
Expand Down Expand Up @@ -3834,6 +3845,71 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionReallyKill, (JSC::JSGlobalObject * glob
RELEASE_AND_RETURN(scope, JSValue::encode(jsNumber(result)));
}

JSC_DEFINE_HOST_FUNCTION(Process_functionDebugProcess, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject));

if (callFrame->argumentCount() < 1) {
throwVMError(globalObject, scope, "process._debugProcess requires a pid argument"_s);
return {};
}

int pid = callFrame->argument(0).toInt32(globalObject);
RETURN_IF_EXCEPTION(scope, {});

// posix we can just send SIGUSR1, on windows we map a file to `bun-debug-handler-<pid>` and send to that
#if !OS(WINDOWS)
int result = kill(pid, SIGUSR1);
if (result < 0) {
throwVMError(globalObject, scope, makeString("Failed to send SIGUSR1 to process "_s, pid, ": process may not exist or permission denied"_s));
return {};
}
#else
wchar_t mappingName[64];
swprintf(mappingName, 64, L"bun-debug-handler-%d", pid);

HANDLE hMapping = OpenFileMappingW(FILE_MAP_READ, FALSE, mappingName);
if (!hMapping) {
// Match Node.js error message for compatibility
throwVMError(globalObject, scope, "The system cannot find the file specified."_s);
return {};
}

void* pFunc = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, sizeof(void*));
if (!pFunc) {
CloseHandle(hMapping);
throwVMError(globalObject, scope, makeString("Failed to map debug handler for process "_s, pid));
return {};
}

LPTHREAD_START_ROUTINE threadProc = *reinterpret_cast<LPTHREAD_START_ROUTINE*>(pFunc);
UnmapViewOfFile(pFunc);
Comment on lines +3873 to +3876
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add null check for threadProc before use.

After reading from the mapped memory, threadProc could be null or contain invalid data if the target process's debug handler mapping was corrupted or improperly initialized. Consider validating the pointer before passing it to CreateRemoteThread.

🔧 Suggested fix
     LPTHREAD_START_ROUTINE threadProc = *reinterpret_cast<LPTHREAD_START_ROUTINE*>(pFunc);
     UnmapViewOfFile(pFunc);
     CloseHandle(hMapping);

+    if (!threadProc) {
+        throwVMError(globalObject, scope, makeString("Invalid debug handler for process "_s, pid));
+        return {};
+    }
+
     HANDLE hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, pid);
🤖 Prompt for AI Agents
In `@src/bun.js/bindings/BunProcess.cpp` around lines 3873 - 3876, Add a
null/validity check for the function pointer read from the mapped view: after
assigning LPTHREAD_START_ROUTINE threadProc =
*reinterpret_cast<LPTHREAD_START_ROUTINE*>(pFunc) and before calling
CreateRemoteThread, verify that threadProc is not null (and optionally that
pFunc was non-null) and bail out with appropriate cleanup (e.g.,
UnmapViewOfFile(pFunc) if not already done, set/return an error) if the pointer
is invalid; reference the variables threadProc and pFunc and the call to
CreateRemoteThread to locate where to insert the guard.

CloseHandle(hMapping);

HANDLE hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, pid);
if (!hProcess) {
throwVMError(globalObject, scope, makeString("Failed to open process "_s, pid, ": access denied or process not found"_s));
return {};
}

HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, threadProc, NULL, 0, NULL);
if (!hThread) {
CloseHandle(hProcess);
throwVMError(globalObject, scope, makeString("Failed to create remote thread in process "_s, pid));
return {};
}

// Wait briefly for the thread to complete because closing the handles
// immediately could terminate the remote thread before it finishes
// triggering the inspector in the target process.
WaitForSingleObject(hThread, 1000);
CloseHandle(hThread);
CloseHandle(hProcess);
#endif

return JSValue::encode(jsUndefined());
}

JSC_DEFINE_HOST_FUNCTION(Process_functionKill, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject));
Expand Down Expand Up @@ -3973,7 +4049,7 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu
/* Source for Process.lut.h
@begin processObjectTable
_debugEnd Process_stubEmptyFunction Function 0
_debugProcess Process_stubEmptyFunction Function 0
_debugProcess Process_functionDebugProcess Function 1
_eval processGetEval CustomAccessor
_fatalException Process_stubEmptyFunction Function 1
_getActiveHandles Process_stubFunctionReturningArray Function 0
Expand Down
30 changes: 30 additions & 0 deletions src/bun.js/bindings/VMManager.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/// Zig bindings for JSC::VMManager
///
/// VMManager coordinates multiple VMs (workers) and provides the StopTheWorld
/// mechanism for safely interrupting JavaScript execution at safe points.
///
/// Note: StopReason values are bitmasks (1 << bit_position), not sequential.
/// This matches the C++ enum in VMManager.h which uses:
/// enum class StopReason : StopRequestBits { None = 0, GC = 1, WasmDebugger = 2, MemoryDebugger = 4, JSDebugger = 8 }
pub const StopReason = enum(u32) {
None = 0,
GC = 1 << 0, // 1
WasmDebugger = 1 << 1, // 2
MemoryDebugger = 1 << 2, // 4
JSDebugger = 1 << 3, // 8
};

extern fn VMManager__requestStopAll(reason: StopReason) void;
extern fn VMManager__requestResumeAll(reason: StopReason) void;

/// Request all VMs to stop at their next safe point.
/// The registered StopTheWorld callback for the given reason will be called
/// on the main thread once all VMs have stopped.
pub fn requestStopAll(reason: StopReason) void {
VMManager__requestStopAll(reason);
}

/// Clear the pending stop request and resume all VMs.
pub fn requestResumeAll(reason: StopReason) void {
VMManager__requestResumeAll(reason);
}
10 changes: 10 additions & 0 deletions src/bun.js/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
#include "JavaScriptCore/StackFrame.h"
#include "JavaScriptCore/StackVisitor.h"
#include "JavaScriptCore/VM.h"
#include "JavaScriptCore/VMManager.h"
#include "AddEventListenerOptions.h"
#include "AsyncContextFrame.h"
#include "BunClientData.h"
Expand Down Expand Up @@ -266,6 +267,10 @@ extern "C" unsigned getJSCBytecodeCacheVersion()
extern "C" void Bun__REPRL__registerFuzzilliFunctions(Zig::GlobalObject*);
#endif

// StopTheWorld callback for SIGUSR1 debugger activation (defined in BunDebugger.cpp).
// Note: This is a C++ function - cannot use extern "C" because it returns std::pair.
JSC::StopTheWorldStatus Bun__jsDebuggerCallback(JSC::VM&, JSC::StopTheWorldEvent);

extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(const char* ptr, size_t length), bool evalMode)
{
static std::once_flag jsc_init_flag;
Expand All @@ -287,6 +292,11 @@ extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(c
#endif

JSC::initialize();

// Register the StopTheWorld callback for SIGUSR1 debugger activation.
// This allows us to interrupt infinite loops and activate the debugger.
JSC::VMManager::setJSDebuggerCallback(Bun__jsDebuggerCallback);

{

JSC::Options::AllowUnfinalizedAccessScope scope;
Expand Down
5 changes: 5 additions & 0 deletions src/bun.js/event_loop.zig
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ pub fn runImminentGCTimer(this: *EventLoop) void {
pub fn tickConcurrentWithCount(this: *EventLoop) usize {
this.updateCounts();

if (this.virtual_machine.is_main_thread) {
RuntimeInspector.checkAndActivateInspector();
}

if (comptime Environment.isPosix) {
if (this.signal_handler) |signal_handler| {
signal_handler.drain(this);
Expand Down Expand Up @@ -687,6 +691,7 @@ pub const DeferredTaskQueue = @import("./event_loop/DeferredTaskQueue.zig");
pub const DeferredRepeatingTask = DeferredTaskQueue.DeferredRepeatingTask;
pub const PosixSignalHandle = @import("./event_loop/PosixSignalHandle.zig");
pub const PosixSignalTask = PosixSignalHandle.PosixSignalTask;
pub const RuntimeInspector = @import("./event_loop/RuntimeInspector.zig");
pub const MiniEventLoop = @import("./event_loop/MiniEventLoop.zig");
pub const MiniVM = MiniEventLoop.MiniVM;
pub const JsVM = MiniEventLoop.JsVM;
Expand Down
Loading