Compare commits

...

7 Commits

Author SHA1 Message Date
Ben Grant
8bf1863877 Add Bun test 2025-05-20 15:37:04 -07:00
Ben Grant
fb11a3cada Add Node test 2025-05-20 14:59:20 -07:00
Ben Grant
8f8c6c3811 Make many things unsupported in workers 2025-05-20 14:59:13 -07:00
Ben Grant
f671455a5e Throw error setting process.umask in Worker 2025-05-20 14:56:59 -07:00
Ben Grant
15a16d4e79 Add Zig::GlobalObject::worker() 2025-05-20 14:56:22 -07:00
Ben Grant
cb7a8d9b7c Add ERR_WORKER_UNSUPPORTED_OPERATION 2025-05-20 14:55:12 -07:00
Ben Grant
35c7b5ad51 Restore default debugPort of 9229 2025-05-20 14:54:22 -07:00
12 changed files with 328 additions and 25 deletions

View File

@@ -2023,6 +2023,11 @@ export fn Bun__VirtualMachine__setOverrideModuleRunMainPromise(vm: *VirtualMachi
}
}
export fn Bun__VirtualMachine__getWorker(vm: *VirtualMachine) ?*anyopaque {
const worker = vm.worker orelse return null;
return worker.cpp_worker;
}
pub fn reloadEntryPointForTestRunner(this: *VirtualMachine, entry_path: []const u8) !*JSInternalPromise {
this.has_loaded = false;
this.main = entry_path;

View File

@@ -44,6 +44,7 @@
#include <JavaScriptCore/StructureCache.h>
#include <webcore/SerializedScriptValue.h>
#include <webcore/Worker.h>
#include "ProcessBindingTTYWrap.h"
#include "wtf/text/ASCIILiteral.h"
#include "wtf/text/StringToIntegerConversion.h"
@@ -664,7 +665,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, (JSC::JSGlobalObject * globalOb
return JSValue::encode(resultValue);
}
JSC_DEFINE_HOST_FUNCTION(Process_functionUmask, (JSGlobalObject * globalObject, CallFrame* callFrame))
JSC_DEFINE_HOST_FUNCTION(Process_functionUmask, (JSGlobalObject * jsGlobalObject, CallFrame* callFrame))
{
if (callFrame->argumentCount() == 0 || callFrame->argument(0).isUndefined()) {
mode_t currentMask = umask(0);
@@ -672,10 +673,15 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionUmask, (JSGlobalObject * globalObject,
return JSValue::encode(jsNumber(currentMask));
}
auto* globalObject = defaultGlobalObject(jsGlobalObject);
auto& vm = JSC::getVM(globalObject);
auto throwScope = DECLARE_THROW_SCOPE(vm);
auto value = callFrame->argument(0);
if (globalObject->worker()) {
return Bun::ERR::WORKER_UNSUPPORTED_OPERATION(throwScope, globalObject, "Setting process.umask()"_s);
}
auto value = callFrame->argument(0);
mode_t newUmask;
if (value.isString()) {
auto str = value.getString(globalObject);
@@ -3320,7 +3326,7 @@ static JSValue constructFeatures(VM& vm, JSObject* processObject)
return object;
}
static uint16_t debugPort;
static uint16_t debugPort = 9229;
JSC_DEFINE_CUSTOM_GETTER(processDebugPort, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName))
{
@@ -3576,8 +3582,6 @@ 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
_fatalException Process_stubEmptyFunction Function 1
_getActiveHandles Process_stubFunctionReturningArray Function 0
_getActiveRequests Process_stubFunctionReturningArray Function 0
@@ -3585,8 +3589,6 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu
_linkedBinding Process_stubEmptyFunction Function 0
_preload_modules Process_stubEmptyArray PropertyCallback
_rawDebug Process_stubEmptyFunction Function 0
_startProfilerIdleNotifier Process_stubEmptyFunction Function 0
_stopProfilerIdleNotifier Process_stubEmptyFunction Function 0
_tickCallback Process_stubEmptyFunction Function 0
abort Process_functionAbort Function 1
allowedNodeEnvironmentFlags Process_stubEmptySet PropertyCallback
@@ -3670,7 +3672,93 @@ const JSC::ClassInfo Process::s_info
= { "Process"_s, &Base::s_info, &processObjectTable, nullptr,
CREATE_METHOD_TABLE(Process) };
void Process::finishCreation(JSC::VM& vm)
#if OS(WINDOWS)
#define FOR_EACH_UNSUPPORTED_WORKER_POSIX_FUNCTION(V)
#else
#define FOR_EACH_UNSUPPORTED_WORKER_POSIX_FUNCTION(V) \
V(setuid) \
V(seteuid) \
V(setgid) \
V(setegid) \
V(setgroups) \
V(initgroups)
#endif
#define FOR_EACH_UNSUPPORTED_WORKER_FUNCTION(V) \
V(abort) \
V(chdir) \
V(send) \
V(disconnect) \
FOR_EACH_UNSUPPORTED_WORKER_POSIX_FUNCTION(V)
#define DEFINE_DISABLED_FUNCTION(ident) \
JSC_DEFINE_HOST_FUNCTION(processDisabledFunction_##ident, (JSC::JSGlobalObject * globalObject, JSC::CallFrame * callFrame)) \
{ \
static constexpr char msg[] = "process." #ident "()"; \
static constexpr ASCIILiteral msg_s { msg }; \
auto& vm = JSC::getVM(globalObject); \
auto scope = DECLARE_THROW_SCOPE(vm); \
return Bun::ERR::WORKER_UNSUPPORTED_OPERATION(scope, globalObject, msg_s); \
}
FOR_EACH_UNSUPPORTED_WORKER_FUNCTION(DEFINE_DISABLED_FUNCTION)
#undef DEFINE_DISABLED_FUNCTION
JSC_DEFINE_HOST_FUNCTION(processDisabledGetter_channel, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
return Bun::ERR::WORKER_UNSUPPORTED_OPERATION(scope, globalObject, "process.channel"_s);
}
JSC_DEFINE_HOST_FUNCTION(processDisabledGetter_connected, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
return Bun::ERR::WORKER_UNSUPPORTED_OPERATION(scope, globalObject, "process.connected"_s);
}
void Process::installDisabledFunctions(JSC::VM& vm, Zig::GlobalObject* globalObject)
{
#define ASSIGN_DISABLED_FUNCTION(ident) \
{ \
auto* fn = JSFunction::create(vm, globalObject, 0, "unavailableInWorker"_s, processDisabledFunction_##ident, ImplementationVisibility::Public); \
fn->putDirect(vm, Identifier::fromString(vm, "disabled"_s), jsBoolean(true)); \
putDirect(vm, Identifier::fromString(vm, #ident##_s), fn); \
}
// set up the functions that are supposed to throw and have `disabled` set to true
FOR_EACH_UNSUPPORTED_WORKER_FUNCTION(ASSIGN_DISABLED_FUNCTION)
#undef ASSIGN_DISABLED_FUNCTION
// if we have IPC, set up getters for these properties that throw
auto fdValue = globalObject->processEnvObject()->get(globalObject, Identifier::fromString(vm, "NODE_CHANNEL_FD"_s));
if (fdValue.toBoolean(globalObject)) {
putDirectAccessor(
globalObject,
Identifier::fromString(vm, "channel"_s),
GetterSetter::create(
vm,
globalObject,
JSFunction::create(vm, globalObject, 0, "unavailableInWorker"_s, processDisabledGetter_channel, ImplementationVisibility::Public),
jsUndefined()),
PropertyAttribute::ReadOnly | PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::Accessor);
putDirectAccessor(
globalObject,
Identifier::fromString(vm, "connected"_s),
GetterSetter::create(
vm,
globalObject,
JSFunction::create(vm, globalObject, 0, "unavailableInWorker"_s, processDisabledGetter_connected, ImplementationVisibility::Public),
jsUndefined()),
PropertyAttribute::ReadOnly | PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::Accessor);
}
}
void Process::finishCreation(JSC::VM& vm, Zig::GlobalObject& globalObject)
{
Base::finishCreation(vm);
@@ -3700,6 +3788,18 @@ void Process::finishCreation(JSC::VM& vm)
putDirect(vm, vm.propertyNames->toStringTagSymbol, jsString(vm, String("process"_s)), 0);
putDirect(vm, Identifier::fromString(vm, "_exiting"_s), jsBoolean(false), 0);
if (globalObject.worker()) {
installDisabledFunctions(vm, &globalObject);
} else {
// these properties need to not even exist in workers, since node tests that `prop in process` is false
auto* fn = JSFunction::create(vm, &globalObject, 0, "(anonymous)"_s, Process_stubEmptyFunction, ImplementationVisibility::Public);
putDirect(vm, Identifier::fromString(vm, "_startProfilerIdleNotifier"_s), fn);
putDirect(vm, Identifier::fromString(vm, "_stopProfilerIdleNotifier"_s), fn);
putDirect(vm, Identifier::fromString(vm, "_debugProcess"_s), fn);
putDirect(vm, Identifier::fromString(vm, "_debugPause"_s), fn);
putDirect(vm, Identifier::fromString(vm, "_debugEnd"_s), fn);
}
}
} // namespace Bun

View File

@@ -84,12 +84,12 @@ public:
JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
}
static Process* create(WebCore::JSDOMGlobalObject& globalObject, JSC::Structure* structure)
static Process* create(Zig::GlobalObject& globalObject, JSC::Structure* structure)
{
auto emitter = WebCore::EventEmitter::create(*globalObject.scriptExecutionContext());
Process* accessor = new (NotNull, JSC::allocateCell<Process>(globalObject.vm())) Process(structure, globalObject, WTFMove(emitter));
accessor->finishCreation(globalObject.vm());
return accessor;
Process* process = new (NotNull, JSC::allocateCell<Process>(globalObject.vm())) Process(structure, globalObject, WTFMove(emitter));
process->finishCreation(globalObject.vm(), globalObject);
return process;
}
DECLARE_VISIT_CHILDREN;
@@ -107,8 +107,6 @@ public:
[](auto& spaces, auto&& space) { spaces.m_subspaceForProcessObject = std::forward<decltype(space)>(space); });
}
void finishCreation(JSC::VM& vm);
inline void setUncaughtExceptionCaptureCallback(JSC::JSValue callback)
{
m_uncaughtExceptionCaptureCallback.set(vm(), this, callback);
@@ -124,6 +122,12 @@ public:
inline Structure* memoryUsageStructure() { return m_memoryUsageStructure.getInitializedOnMainThread(this); }
inline JSObject* bindingUV() { return m_bindingUV.getInitializedOnMainThread(this); }
inline JSObject* bindingNatives() { return m_bindingNatives.getInitializedOnMainThread(this); }
private:
void finishCreation(JSC::VM& vm, Zig::GlobalObject& globalObject);
// Replace functions that should be disabled in Workers with stubs that throw an error when
// called and have the `disabled` property set to true
void installDisabledFunctions(JSC::VM& vm, Zig::GlobalObject* globalObject);
};
bool isSignalName(WTF::String input);

View File

@@ -999,6 +999,13 @@ JSC::EncodedJSValue INVALID_FILE_URL_PATH(JSC::ThrowScope& throwScope, JSC::JSGl
return {};
}
JSC::EncodedJSValue WORKER_UNSUPPORTED_OPERATION(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const ASCIILiteral operation)
{
auto message = makeString(operation, " is not supported in workers"_s);
throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_WORKER_UNSUPPORTED_OPERATION, message));
return {};
}
JSC::EncodedJSValue UNKNOWN_ENCODING(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::StringView encoding)
{
auto message = makeString("Unknown encoding: "_s, encoding);

View File

@@ -145,6 +145,11 @@ JSC::EncodedJSValue INVALID_FILE_URL_HOST(JSC::ThrowScope& throwScope, JSC::JSGl
/// `File URL path {suffix}`
JSC::EncodedJSValue INVALID_FILE_URL_PATH(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const ASCIILiteral suffix);
// Worker
// `{operation} is not supported in workers`
JSC::EncodedJSValue WORKER_UNSUPPORTED_OPERATION(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const ASCIILiteral operation);
}
void throwBoringSSLError(JSC::VM& vm, JSC::ThrowScope& scope, JSGlobalObject* globalObject, int errorCode);

View File

@@ -252,6 +252,7 @@ const errors: ErrorCodeMapping = [
["ERR_VM_MODULE_LINK_FAILURE", Error],
["ERR_WASI_NOT_STARTED", Error],
["ERR_WORKER_INIT_FAILED", Error],
["ERR_WORKER_UNSUPPORTED_OPERATION", TypeError],
["ERR_ZLIB_INITIALIZATION_FAILED", Error],
["MODULE_NOT_FOUND", Error],
["ERR_INTERNAL_ASSERTION", Error],

View File

@@ -4376,6 +4376,14 @@ bool GlobalObject::hasNapiFinalizers() const
void GlobalObject::setNodeWorkerEnvironmentData(JSMap* data) { m_nodeWorkerEnvironmentData.set(vm(), this, data); }
extern "C" WebCore::Worker* Bun__VirtualMachine__getWorker(VirtualMachine* bunVM);
WebCore::Worker* GlobalObject::worker()
{
// TODO make bunVM typed instead of void* everywhere
return Bun__VirtualMachine__getWorker(reinterpret_cast<VirtualMachine*>(bunVM()));
}
extern "C" void Zig__GlobalObject__destructOnExit(Zig::GlobalObject* globalObject)
{
auto& vm = JSC::getVM(globalObject);

View File

@@ -25,6 +25,7 @@ class WorkerGlobalScope;
class SubtleCrypto;
class EventTarget;
class Performance;
class Worker;
} // namespace WebCore
namespace Bun {
@@ -388,6 +389,10 @@ public:
return func;
}
// Return the Worker object if this global object is running in a worker, or nullptr
// if it is not.
WebCore::Worker* worker();
bool asyncHooksNeedsCleanup = false;
double INSPECT_MAX_BYTES = 50;
bool isInsideErrorPrepareStackTraceCallback = false;

View File

@@ -514,8 +514,6 @@ extern "C" void WebWorker__dispatchError(Zig::GlobalObject* globalObject, Worker
}
}
extern "C" WebCore::Worker* WebWorker__getParentWorker(void* bunVM);
JSC_DEFINE_HOST_FUNCTION(jsReceiveMessageOnPort, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
@@ -551,7 +549,7 @@ JSValue createNodeWorkerThreadsBinding(Zig::GlobalObject* globalObject)
JSValue threadId = jsNumber(0);
JSMap* environmentData = nullptr;
if (auto* worker = WebWorker__getParentWorker(globalObject->bunVM())) {
if (auto* worker = globalObject->worker()) {
auto& options = worker->options();
auto ports = MessagePort::entanglePorts(*ScriptExecutionContext::getScriptExecutionContext(worker->clientIdentifier()), WTFMove(options.dataMessagePorts));
RefPtr<WebCore::SerializedScriptValue> serialized = WTFMove(options.workerDataAndEnvironmentData);
@@ -597,7 +595,7 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionPostMessage,
if (UNLIKELY(!globalObject))
return JSValue::encode(jsUndefined());
Worker* worker = WebWorker__getParentWorker(globalObject->bunVM());
Worker* worker = globalObject->worker();
if (worker == nullptr)
return JSValue::encode(jsUndefined());

View File

@@ -57,11 +57,6 @@ extern fn WebWorker__dispatchOnline(cpp_worker: *anyopaque, *jsc.JSGlobalObject)
extern fn WebWorker__fireEarlyMessages(cpp_worker: *anyopaque, *jsc.JSGlobalObject) void;
extern fn WebWorker__dispatchError(*jsc.JSGlobalObject, *anyopaque, bun.String, JSValue) void;
export fn WebWorker__getParentWorker(vm: *jsc.VirtualMachine) ?*anyopaque {
const worker = vm.worker orelse return null;
return worker.cpp_worker;
}
pub fn hasRequestedTerminate(this: *const WebWorker) bool {
return this.requested_terminate.load(.monotonic);
}

View File

@@ -0,0 +1,70 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const { Worker, parentPort } = require('worker_threads');
// Do not use isMainThread so that this test itself can be run inside a Worker.
if (!process.env.HAS_STARTED_WORKER) {
process.env.HAS_STARTED_WORKER = 1;
process.env.NODE_CHANNEL_FD = 'foo'; // Make worker think it has IPC.
const w = new Worker(__filename);
w.on('message', common.mustCall((message) => {
assert.strictEqual(message, true);
}));
} else {
{
const before = process.title;
const after = before + ' in worker';
process.title = after;
assert.strictEqual(process.title, after);
}
{
const before = process.debugPort;
const after = before + 1;
process.debugPort = after;
assert.strictEqual(process.debugPort, after);
}
{
const mask = 0o600;
assert.throws(() => { process.umask(mask); }, {
code: 'ERR_WORKER_UNSUPPORTED_OPERATION',
message: 'Setting process.umask() is not supported in workers'
});
}
const stubs = ['abort', 'chdir', 'send', 'disconnect'];
if (!common.isWindows) {
stubs.push('setuid', 'seteuid', 'setgid',
'setegid', 'setgroups', 'initgroups');
}
stubs.forEach((fn) => {
assert.strictEqual(process[fn].disabled, true);
assert.throws(() => {
process[fn]();
}, {
code: 'ERR_WORKER_UNSUPPORTED_OPERATION',
message: `process.${fn}() is not supported in workers`
});
});
['channel', 'connected'].forEach((fn) => {
assert.throws(() => {
process[fn]; // eslint-disable-line no-unused-expressions
}, {
code: 'ERR_WORKER_UNSUPPORTED_OPERATION',
message: `process.${fn} is not supported in workers`
});
});
assert.strictEqual('_startProfilerIdleNotifier' in process, false);
assert.strictEqual('_stopProfilerIdleNotifier' in process, false);
assert.strictEqual('_debugProcess' in process, false);
assert.strictEqual('_debugPause' in process, false);
assert.strictEqual('_debugEnd' in process, false);
parentPort.postMessage(true);
}

View File

@@ -1,4 +1,4 @@
import { bunEnv, bunExe } from "harness";
import { bunEnv, bunExe, isWindows } from "harness";
import { once } from "node:events";
import fs from "node:fs";
import { join, relative, resolve } from "node:path";
@@ -402,10 +402,12 @@ describe("environmentData", () => {
describe("error event", () => {
test("is fired with a copy of the error value", async () => {
const worker = new Worker("throw new TypeError('oh no')", { eval: true });
const worker = new Worker("const e = new TypeError('oh no'); e.code = 'ERR_OHNO'; throw e;", { eval: true });
const [err] = await once(worker, "error");
expect(err).toBeInstanceOf(TypeError);
expect(err.message).toBe("oh no");
// TODO(@190n) find out why extra properties on errors are not propagated
// expect(err.code).toBe("ERR_OHNO");
});
test("falls back to string when the error cannot be serialized", async () => {
@@ -421,3 +423,106 @@ describe("error event", () => {
expect(err.message).toMatch(/MessagePort \{.*\}/s);
});
});
describe("unsupported functions and properties", () => {
test("getting process.umask works", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
const worker = new Worker("process.exit(process.umask())", { eval: true });
worker.on("error", reject);
worker.on("exit", resolve);
expect(await promise).toBe(process.umask());
});
test("setting process.umask", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
const worker = new Worker(
/* js */ `
import assert from "node:assert";
assert.throws(() => process.umask(123), {
name: "TypeError",
code: "ERR_WORKER_UNSUPPORTED_OPERATION",
message: "Setting process.umask() is not supported in workers",
});
`,
{
eval: true,
},
);
worker.on("error", reject);
worker.on("exit", resolve);
expect(await promise).toBe(0);
});
test("functions that throw", async () => {
const stubs = ["abort", "chdir", "send", "disconnect"];
if (!isWindows) {
stubs.push("setuid", "seteuid", "setgid", "setegid", "setgroups", "initgroups");
}
let code = 'import assert from "node:assert"';
for (const fn of stubs) {
code += /* js */ `
assert.strictEqual(process.${fn}.disabled, true);
assert.strictEqual(process.${fn}.name, "unavailableInWorker");
assert.throws(process.${fn}, {
name: "TypeError",
code: "ERR_WORKER_UNSUPPORTED_OPERATION",
message: "process.${fn}() is not supported in workers",
});`;
}
const { promise, resolve, reject } = Promise.withResolvers();
const worker = new Worker(code, { eval: true });
worker.on("error", reject);
worker.on("exit", resolve);
expect(await promise).toBe(0);
});
test("getters that throw when IPC appears to be enabled", async () => {
const before = process.env.NODE_CHANNEL_FD;
try {
// make worker think we have IPC
process.env.NODE_CHANNEL_FD = "truthy";
const { promise, resolve, reject } = Promise.withResolvers();
const worker = new Worker(
/* js */ `
import assert from "node:assert";
assert.throws(() => process.channel, {
name: "TypeError",
code: "ERR_WORKER_UNSUPPORTED_OPERATION",
message: "process.channel is not supported in workers",
});
assert.throws(() => process.connected, {
name: "TypeError",
code: "ERR_WORKER_UNSUPPORTED_OPERATION",
message: "process.connected is not supported in workers",
});
`,
{ eval: true },
);
worker.on("error", reject);
worker.on("exit", resolve);
expect(await promise).toBe(0);
} finally {
process.env.NODE_CHANNEL_FD = before;
}
});
test("properties that are not defined", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
const worker = new Worker(
/* js */ `
import assert from "node:assert";
assert.strictEqual("_startProfilerIdleNotifier" in process, false);
assert.strictEqual("_stopProfilerIdleNotifier" in process, false);
assert.strictEqual("_debugProcess" in process, false);
assert.strictEqual("_debugPause" in process, false);
assert.strictEqual("_debugEnd" in process, false);
`,
{ eval: true },
);
worker.on("error", reject);
worker.on("exit", resolve);
expect(await promise).toBe(0);
});
});