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
5 changes: 0 additions & 5 deletions src/jsc/VM.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ unsafe extern "C" {
safe fn JSC__VM__runGC(vm: &VM, sync: bool) -> usize;
safe fn JSC__VM__heapSize(vm: &VM) -> usize;
safe fn JSC__VM__collectAsync(vm: &VM);
safe fn JSC__VM__setExecutionForbidden(vm: &VM, forbidden: bool);
safe fn JSC__VM__setExecutionTimeLimit(vm: &VM, timeout: f64);
safe fn JSC__VM__clearExecutionTimeLimit(vm: &VM);
safe fn JSC__VM__executionForbidden(vm: &VM) -> bool;
Expand Down Expand Up @@ -130,10 +129,6 @@ impl VM {
JSC__VM__collectAsync(self)
}

pub fn set_execution_forbidden(&self, forbidden: bool) {
JSC__VM__setExecutionForbidden(self, forbidden)
}

pub fn set_execution_time_limit(&self, timeout: f64) {
JSC__VM__setExecutionTimeLimit(self, timeout)
}
Expand Down
24 changes: 20 additions & 4 deletions src/jsc/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
#include "NodeValidator.h"
#include "NodeModuleModule.h"
#include "JSX509Certificate.h"
#include "vm/SigintWatcher.h"

#include "AsyncContextFrame.h"
#include "ErrorCode.h"
Expand Down Expand Up @@ -1568,7 +1569,12 @@ static void onDidChangeListeners(EventEmitter& eventEmitter, const Identifier& e
sigaddset(&action.sa_mask, signalNumber);
action.sa_flags = SA_RESTART;

sigaction(signalNumber, &action, nullptr);
// The SIGINT watcher (`node:vm` breakOnSigint, the REPL) holds the
// disposition while the code doing this runs, so hand it the action
// rather than installing over its handler and disarming it.
if (signalNumber != SIGINT || !Bun::SigintWatcher::get().deferSigintDisposition(action)) {
sigaction(signalNumber, &action, nullptr);
}
#else
signal_handle.handle = Bun__UVSignalHandle__init(
eventEmitter.scriptExecutionContext()->jsGlobalObject(),
Expand All @@ -1585,9 +1591,19 @@ static void onDidChangeListeners(EventEmitter& eventEmitter, const Identifier& e
if (signalToContextIdsMap->find(signalNumber) != signalToContextIdsMap->end() && eventEmitter.listenerCount(eventName) == 0) {

#if !OS(WINDOWS)
if (void (*oldHandler)(int) = signal(signalNumber, SIG_DFL); oldHandler != forwardSignal) {
// Don't uninstall the old handler if it's not the one we installed.
signal(signalNumber, oldHandler);
struct sigaction action;
memset(&action, 0, sizeof(struct sigaction));
action.sa_handler = SIG_DFL;
sigemptyset(&action.sa_mask);

// Same as above: while the watcher is armed, the default we want
// back takes effect when it disarms, not now. Without this its
// handler is what `signal()` below would find and reinstate.
if (signalNumber != SIGINT || !Bun::SigintWatcher::get().deferSigintDisposition(action)) {
if (void (*oldHandler)(int) = signal(signalNumber, SIG_DFL); oldHandler != forwardSignal) {
// Don't uninstall the old handler if it's not the one we installed.
signal(signalNumber, oldHandler);
}
}
#else
SignalHandleValue signal_handle = signalToContextIdsMap->get(signalNumber);
Expand Down
14 changes: 11 additions & 3 deletions src/jsc/bindings/NodeVMModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -245,16 +245,24 @@ JSValue NodeVMModule::evaluate(JSGlobalObject* globalObject, uint32_t timeout, b
// so the exception-check validator is satisfied before the TOP scope.
std::ignore = scope.exception();
if (vm.hasTerminationRequest() || vm.hasPendingTerminationException()) {
// An enclosing scope asked for the termination; only it can classify it.
// Returning is load-bearing: falling through would store the singleton
// TerminationException, whose re-throw later trips `VM::setException`.
if (!getSigintReceived() && timeout == 0) {
JSC::throwException(globalObject, scope, vm.ensureTerminationException());
return {};
}
Comment thread
claude[bot] marked this conversation as resolved.
// Despite the name this *clears* the queue, so scope it to the terminated
// context. `nodeVmGlobalObject` is null when there is no context (nothing
// to clear); passing `globalObject` would discard the main queue.
vm.drainMicrotasksForGlobalObject(nodeVmGlobalObject);
DECLARE_TOP_EXCEPTION_SCOPE(vm).clearException();
vm.clearHasTerminationRequest();
if (getSigintReceived()) {
setSigintReceived(false);
throwError(globalObject, scope, ErrorCode::ERR_SCRIPT_EXECUTION_INTERRUPTED, "Script execution was interrupted by `SIGINT`"_s);
} else if (timeout != 0) {
throwError(globalObject, scope, ErrorCode::ERR_SCRIPT_EXECUTION_TIMEOUT, makeString("Script execution timed out after "_s, timeout, "ms"_s));
} else {
RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("vm.SourceTextModule evaluation terminated due neither to SIGINT nor to timeout");
throwError(globalObject, scope, ErrorCode::ERR_SCRIPT_EXECUTION_TIMEOUT, makeString("Script execution timed out after "_s, timeout, "ms"_s));
}
} else {
setSigintReceived(false);
Expand Down
50 changes: 30 additions & 20 deletions src/jsc/bindings/NodeVMScript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -281,28 +281,36 @@ void NodeVMScript::destroy(JSCell* cell)
static_cast<NodeVMScript*>(cell)->NodeVMScript::~NodeVMScript();
}

static bool checkForTermination(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope, NodeVMScript* script, std::optional<double> timeout)
static bool checkForTermination(JSC::VM& vm, JSC::JSGlobalObject* globalObject, NodeVMGlobalObject* contextGlobalObject, JSC::ThrowScope& scope, NodeVMScript* script, std::optional<double> timeout)
{
if (vm.hasTerminationRequest()) {
vm.drainMicrotasksForGlobalObject(globalObject);
// The termination may have fired inside an afterEvaluate microtask
// checkpoint, leaving the termination exception pending; clear it so
// the ERR_SCRIPT_EXECUTION_* error below replaces it.
if (vm.hasPendingTerminationException())
DECLARE_TOP_EXCEPTION_SCOPE(vm).clearException();
vm.clearHasTerminationRequest();
if (script->getSigintReceived()) {
script->setSigintReceived(false);
throwError(globalObject, scope, ErrorCode::ERR_SCRIPT_EXECUTION_INTERRUPTED, "Script execution was interrupted by `SIGINT`"_s);
} else if (timeout) {
throwError(globalObject, scope, ErrorCode::ERR_SCRIPT_EXECUTION_TIMEOUT, makeString("Script execution timed out after "_s, *timeout, "ms"_s));
} else {
RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("vm.Script terminated due neither to SIGINT nor to timeout");
}
if (!vm.hasTerminationRequest())
return false;

// Neither this script's own SIGINT nor its own timeout, so an enclosing scope
// asked for the termination. Only that scope can classify it: re-raise and let
// its `checkForTermination` (or `Bun__REPL__evaluate`) report.
if (!script->getSigintReceived() && !timeout) {
JSC::throwException(globalObject, scope, vm.ensureTerminationException());
return true;
}

return false;
// Despite the name this *clears* the queue, so scope it to the terminated
// context. `runInThisContext` has none of its own (null, nothing to clear);
// passing `globalObject` there would discard the caller's microtasks.
vm.drainMicrotasksForGlobalObject(contextGlobalObject);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// The termination may have fired inside an afterEvaluate microtask
// checkpoint, leaving the termination exception pending; clear it so
// the ERR_SCRIPT_EXECUTION_* error below replaces it.
if (vm.hasPendingTerminationException())
DECLARE_TOP_EXCEPTION_SCOPE(vm).clearException();
vm.clearHasTerminationRequest();
if (script->getSigintReceived()) {
script->setSigintReceived(false);
throwError(globalObject, scope, ErrorCode::ERR_SCRIPT_EXECUTION_INTERRUPTED, "Script execution was interrupted by `SIGINT`"_s);
} else {
throwError(globalObject, scope, ErrorCode::ERR_SCRIPT_EXECUTION_TIMEOUT, makeString("Script execution timed out after "_s, *timeout, "ms"_s));
}
return true;
Comment thread
robobun marked this conversation as resolved.
}

void setupWatchdog(VM& vm, double timeout, double* oldTimeout, double* newTimeout)
Expand Down Expand Up @@ -383,7 +391,7 @@ static JSC::EncodedJSValue runInContext(NodeVMGlobalObject* globalObject, NodeVM
vm.watchdog()->setTimeLimit(WTF::Seconds::fromMilliseconds(*oldLimit));
}

if (checkForTermination(vm, globalObject, scope, script, newLimit)) {
if (checkForTermination(vm, globalObject, globalObject, scope, script, newLimit)) {
return {};
}

Expand Down Expand Up @@ -448,7 +456,9 @@ JSC_DEFINE_HOST_FUNCTION(scriptRunInThisContext, (JSGlobalObject * globalObject,
vm.watchdog()->setTimeLimit(WTF::Seconds::fromMilliseconds(*oldLimit));
}

if (checkForTermination(vm, globalObject, scope, script, newLimit)) {
// `runInThisContext` evaluates in the caller's global, so there is no
// contextified global whose microtask queue the termination may clear.
if (checkForTermination(vm, globalObject, nullptr, scope, script, newLimit)) {
return {};
}

Expand Down
90 changes: 85 additions & 5 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
#include "JavaScriptCore/ErrorType.h"
#include "JavaScriptCore/TopExceptionScope.h"
#include "JavaScriptCore/Exception.h"
#include "JavaScriptCore/VMTraps.h"
#include "ErrorCode+List.h"
#include "ErrorCode.h"
#include "JavaScriptCore/ThrowScope.h"
#include "../vm/SigintWatcher.h"

#include "JavaScriptCore/JSCast.h"
#include "JavaScriptCore/JSType.h"
Expand Down Expand Up @@ -4829,11 +4831,6 @@ bool JSC__VM__hasTerminationRequest(JSC::VM* vm)
return vm->hasTerminationRequest();
}

void JSC__VM__setExecutionForbidden(JSC::VM* arg0, bool arg1)
{
(*arg0).setExecutionForbidden();
}

// These may be called concurrently from another thread.
void JSC__VM__notifyNeedTermination(JSC::VM* arg0)
{
Expand Down Expand Up @@ -6266,6 +6263,81 @@ CPP_DECL [[ZIG_EXPORT(nothrow)]] unsigned int Bun__CallFrame__getLineNumber(JSC:
return lineColumn.line;
}

// Armed around one REPL evaluation. The receiver flag is the only durable record
// of the signal: JSC clears the trap bit and `hasTerminationRequest()` by the time
// the outermost VM entry scope has unwound.
namespace {
class ReplSigintScope final : public Bun::SigintReceiver {
public:
explicit ReplSigintScope(JSC::JSGlobalObject* globalObject)
: m_holder(Bun::SigintWatcher::hold(globalObject, this))
{
}

private:
Comment thread
claude[bot] marked this conversation as resolved.
Bun::SigintWatcher::GlobalObjectHolder m_holder;
};
}
Comment thread
claude[bot] marked this conversation as resolved.

// Drops whatever termination state a SIGINT left behind so the VM can run
// JavaScript again.
static void replClearTermination(JSC::VM& vm)
{
vm.traps().clearTrap(JSC::VMTraps::NeedTermination);
if (vm.hasPendingTerminationException()) {
DECLARE_TOP_EXCEPTION_SCOPE(vm).clearException();
}
vm.clearHasTerminationRequest();
}

// Arms SIGINT watching for `globalObject`: a SIGINT then raises a JSC termination
// trap that unwinds synchronous JS, the way node's `breakOnSigint` does. Pass the
// returned scope to `Bun__REPL__disarmSigint`.
extern "C" void* Bun__REPL__armSigint(JSC::JSGlobalObject* globalObject)
{
auto& vm = JSC::getVM(globalObject);
// Allocate the termination exception up front: the trap handler runs at a
// point where allocating one is not allowed.
vm.ensureTerminationException();
return new ReplSigintScope(globalObject);
}

extern "C" void Bun__REPL__disarmSigint(JSC::JSGlobalObject* globalObject, void* scope)
{
// Tears down the watcher thread, so no further signal can reach us.
delete static_cast<ReplSigintScope*>(scope);

auto& vm = JSC::getVM(globalObject);
// A signal that raced the disarm leaves a trap bit nobody will service;
// the next evaluation would terminate the instant it entered the VM.
if (vm.traps().hasTrapBit(JSC::VMTraps::NeedTermination)) [[unlikely]] {
replClearTermination(vm);
}
}

extern "C" bool Bun__REPL__sigintRequested(void* scope)
{
return scope && static_cast<ReplSigintScope*>(scope)->getSigintReceived();
}

// Clears the SIGINT termination state and returns the error to report, or an
// empty JSValue when no interrupt is pending.
extern "C" JSC::EncodedJSValue Bun__REPL__takeSigintError(JSC::JSGlobalObject* globalObject, void* scope)
{
if (!Bun__REPL__sigintRequested(scope)) {
return JSC::JSValue::encode({});
}

auto& vm = JSC::getVM(globalObject);
static_cast<ReplSigintScope*>(scope)->setSigintReceived(false);
replClearTermination(vm);

JSC::JSObject* error = Bun::createError(globalObject, Bun::ErrorCode::ERR_SCRIPT_EXECUTION_INTERRUPTED,
"Script execution was interrupted by `SIGINT`"_s);
globalObject->putDirect(vm, JSC::Identifier::fromString(vm, "_error"_s), error);
return JSC::JSValue::encode(error);
}

// REPL evaluation function - evaluates JavaScript code in the global scope
// Returns the result value, or undefined if an exception was thrown
// If an exception is thrown, the exception value is stored in *exception
Expand Down Expand Up @@ -6296,6 +6368,14 @@ extern "C" JSC::EncodedJSValue Bun__REPL__evaluate(
WTF::NakedPtr<JSC::Exception> evalException;
JSC::JSValue result = JSC::evaluate(globalObject, sourceCode, globalObject->globalThis(), evalException);

// SIGINT unwound the script, and `evalException` is the internal
// TerminatedExecutionError. The caller reports
// ERR_SCRIPT_EXECUTION_INTERRUPTED via `Bun__REPL__takeSigintError` instead.
if (evalException && vm.isTerminationException(evalException.get())) [[unlikely]] {
*exception = JSC::JSValue::encode(JSC::jsUndefined());
return JSC::JSValue::encode(JSC::jsUndefined());
}

if (evalException) {
*exception = JSC::JSValue::encode(evalException->value());
// Set _error on the globalObject directly (not globalThis proxy)
Expand Down
1 change: 0 additions & 1 deletion src/jsc/bindings/headers.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions src/jsc/bindings/vm/SigintReceiver.h
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
#pragma once

#include <atomic>

namespace Bun {

// `m_sigintReceived` is written by the SigintWatcher thread and read by the VM
// thread, so it has to be atomic.
class SigintReceiver {
public:
SigintReceiver() = default;

void setSigintReceived(bool value = true)
{
m_sigintReceived = value;
m_sigintReceived.store(value, std::memory_order_relaxed);
}

bool getSigintReceived()
bool getSigintReceived() const
{
return m_sigintReceived;
return m_sigintReceived.load(std::memory_order_relaxed);
}

protected:
bool m_sigintReceived = false;
std::atomic<bool> m_sigintReceived = false;
};

} // namespace Bun
Loading
Loading