Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,5 @@
- Support asynchronous full garbage collection through `node:inspector` HeapProfiler sessions on the main thread and workers, with Node-compatible connection errors and pending callback cleanup.

- Publish Node HTTP server request-start and response-created diagnostics before request dispatch, preserving response constructor timing and covering injected HTTP/1 connections. Adapts [oven-sh/bun#29588](https://github.com/oven-sh/bun/pull/29588) and [oven-sh/bun#32628](https://github.com/oven-sh/bun/pull/32628). Thanks @robobun and @cirospaciari!

- Apply `NODE_OPTIONS` and `BUN_OPTIONS` preloads in Node workers with explicit environments and `execArgv`, preserve selected CLI arguments through nested workers, and apply supported worker restrictions before preloads. Builds on [oven-sh/bun#42620](https://github.com/oven-sh/bun/pull/42620).
2 changes: 2 additions & 0 deletions docs/runtime/nodejs-compat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ The same replay took Node 24 **39.7 ms at 2,500 modules and 63.4 ms at 4,000**.

### [`node:worker_threads`](https://nodejs.org/api/worker_threads.html)

File workers select `--require` and `--import` preloads from their environment independently of `execArgv`. Explicit `env` or `execArgv` options re-read the worker's `NODE_OPTIONS` and `BUN_OPTIONS` before applying the selected CLI arguments. `execArgv: []` removes CLI preloads while environment preloads still apply. Require preloads run before import preloads.

`MessagePort.emit()` and `parentPort.emit()` preserve the emitted value for Node listeners. Web listeners receive `MessageEvent` objects for `message` and `messageerror`, and `CustomEvent` objects for other events.

MessagePort message and close events run in the port creation AsyncLocalStorage context. Transferred endpoints capture their receiving context.
Expand Down
84 changes: 84 additions & 0 deletions src/bun_core/node_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,90 @@ fn is_allowed(flag: &[u8]) -> bool {
ALLOWED.binary_search(&flag).is_ok()
}

pub fn is_allowed_worker_argument(argument: &[u8]) -> bool {
let (name, _) = split_name_value(argument);
let normalized = normalize(name);
// Node rejects this in env even though allowedNodeEnvironmentFlags lists its alias.
normalized.as_ref() != b"--prof-process"
&& (is_allowed(&normalized) || is_bun_flag(&normalized))
}

// Node v24.21 value options must be consumed even when Bun does not apply them.
static WORKER_VALUE_FLAGS: &[&[u8]] = &[
b"--allow-fs-read",
b"--allow-fs-write",
b"--cpu-prof-dir",
b"--cpu-prof-interval",
b"--cpu-prof-name",
b"--debug-port",
b"--diagnostic-dir",
b"--disable-proto",
b"--experimental-loader",
b"--experimental-test-isolation",
b"--heap-prof-dir",
b"--heap-prof-interval",
b"--heap-prof-name",
b"--heapsnapshot-near-heap-limit",
b"--heapsnapshot-signal",
b"--icu-data-dir",
b"--input-type",
b"--inspect-port",
b"--inspect-publish-uid",
b"--loader",
b"--localstorage-file",
b"--max-old-space-size-percentage",
b"--network-family-autoselection-attempt-timeout",
b"--openssl-config",
b"--report-dir",
b"--report-directory",
b"--report-filename",
b"--report-signal",
b"--secure-heap",
b"--secure-heap-min",
b"--snapshot-blob",
b"--stack-trace-limit",
b"--test-coverage-branches",
b"--test-coverage-exclude",
b"--test-coverage-functions",
b"--test-coverage-include",
b"--test-coverage-lines",
b"--test-global-setup",
b"--test-isolation",
b"--test-name-pattern",
b"--test-random-seed",
b"--test-reporter",
b"--test-reporter-destination",
b"--test-rerun-failures",
b"--test-shard",
b"--test-skip-pattern",
b"--tls-cipher-list",
b"--tls-keylog",
b"--trace-event-categories",
b"--trace-event-file-pattern",
b"--trace-require-module",
b"--use-largepages",
b"--v8-pool-size",
b"--watch-kill-signal",
b"--watch-path",
];

pub fn worker_required_value_flag(argument: &[u8]) -> Option<&'static [u8]> {
let (name, _) = split_name_value(argument);
let normalized = normalize(name);
match supported(&normalized) {
Some((canonical, Supported::Value)) => Some(canonical),
_ => WORKER_VALUE_FLAGS
.iter()
.copied()
.find(|flag| *flag == normalized.as_ref()),
}
}

pub fn worker_canonical_flag(argument: &[u8]) -> Option<&'static [u8]> {
let (name, _) = split_name_value(argument);
supported(&normalize(name)).map(|(canonical, _)| canonical)
}

/// Bun-specific flags that commonly reach NODE_OPTIONS via tooling that
/// forwards `process.execArgv` to worker processes (Next.js, jest-worker).
/// Accepted silently so that `bun --bun next build` keeps working.
Expand Down
6 changes: 5 additions & 1 deletion src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2937,7 +2937,11 @@ pub struct RuntimeHooks {
/// Parse `execArgv` against the `RunCommand` param table (lives in `bun_runtime::cli`, forward-dep).
/// Caller writes `allow_addons` / `allow_ffi_cc` back into `transform_options` and applies
/// `cpu_prof` to the worker VM.
pub parse_worker_exec_argv: unsafe fn(exec_argv: &[bun_core::WTFStringImpl]) -> WorkerExecArgv,
pub parse_worker_exec_argv: unsafe fn(
exec_argv: &[bun_core::WTFStringImpl],
environment_argc: usize,
inherited_exec_argv: bool,
) -> WorkerExecArgv,
/// `CronJob.clearAllForVM(vm, .teardown)`. `CronJob` lives in
/// `bun_runtime::api::cron`.
pub stop_cron_for_vm_teardown: fn(vm: &mut VirtualMachine),
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/headers.h

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

98 changes: 90 additions & 8 deletions src/jsc/bindings/webcore/JSWorker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ static bool isNodeWorkerValueExecArgv(const String& flag)

static bool isNodeWorkerDisallowedExecArgv(const String& flag)
{
return flag == "--perf-basic-prof"_s
return flag == "--disallow-code-generation-from-strings"_s
|| flag == "--perf-basic-prof"_s
|| flag == "--perf-basic-prof-only-functions"_s
|| flag == "--perf-prof"_s
|| flag == "--perf-prof-unwinding-info"_s
Expand All @@ -136,13 +137,14 @@ static bool isNodeWorkerDisallowedExecArgv(const String& flag)
|| flag == "--zero-fill-buffers"_s;
}

static std::optional<String> parseNodeWorkerExecArgv(const Vector<String>& execArgv, Vector<String>& outputPreloads, size_t& evalPreloadCount, size_t& bunPreloadCount, size_t& requirePreloadCount, WorkerEvalMode& evalMode)
static std::optional<String> parseNodeWorkerExecArgv(const Vector<String>& execArgv, Vector<String>& outputPreloads, size_t& evalPreloadCount, size_t& bunPreloadCount, size_t& requirePreloadCount, WorkerEvalMode& evalMode, size_t nodeOptionsCount = 0, size_t bunOptionsCount = 0, bool inheritedNodeOptions = false, bool inheritedBunOptions = false, bool inheritedExecArgv = false)
{
Vector<String> bunPreloads;
Vector<String> requirePreloads;
Vector<String> importPreloads;

for (size_t i = 0; i < execArgv.size(); i++) {
const size_t optionIndex = i;
const String& argument = execArgv[i];
size_t equals = argument.find('=');
bool hasInlineValue = equals != notFound;
Expand Down Expand Up @@ -206,13 +208,20 @@ static std::optional<String> parseNodeWorkerExecArgv(const Vector<String>& execA
return makeString("Initiated Worker with invalid execArgv flags: "_s, flag, " requires an argument"_s);
} else if (flag == "--inspect"_s || flag == "--inspect-brk"_s || flag == "--inspect-port"_s) {
continue;
} else if (!isNodeWorkerDisallowedExecArgv(flag)) {
} else if ((optionIndex < nodeOptionsCount && inheritedNodeOptions)
|| (optionIndex >= nodeOptionsCount && optionIndex < nodeOptionsCount + bunOptionsCount && inheritedBunOptions)
|| (optionIndex >= nodeOptionsCount + bunOptionsCount && inheritedExecArgv)
|| !isNodeWorkerDisallowedExecArgv(flag)) {
// The complete option parser lives above this binding. Preserve
// previously accepted flags instead of rejecting valid Node
// options that this local list does not need to interpret.
continue;
}

if (optionIndex < nodeOptionsCount)
return makeString("Initiated Worker with invalid NODE_OPTIONS env variable: "_s, flag, " is not allowed in NODE_OPTIONS"_s);
if (optionIndex < nodeOptionsCount + bunOptionsCount)
return makeString("Initiated Worker with invalid BUN_OPTIONS env variable: "_s, flag, " is not allowed in BUN_OPTIONS"_s);
return makeString("Initiated Worker with invalid execArgv flags: "_s, argument);
}

Expand All @@ -225,6 +234,37 @@ static std::optional<String> parseNodeWorkerExecArgv(const Vector<String>& execA
return std::nullopt;
}

static void appendWorkerOptionStrings(Zig::GlobalObject* globalObject, JSValue value, Vector<String>& out)
{
auto& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
Strong<JSArray> array(vm, uncheckedDowncast<JSArray>(value.asCell()));
for (unsigned i = 0; i < array->length(); ++i) {
JSValue item = array->getIndex(globalObject, i);
RETURN_IF_EXCEPTION(scope, );
String string = item.toWTFString(globalObject).isolatedCopy();
RETURN_IF_EXCEPTION(scope, );
out.append(WTF::move(string));
}
}

static void appendWorkerEnvironmentOptions(Zig::GlobalObject* globalObject, const String& value, bool isBun, bool strict, Vector<String>& out)
{
if (value.isEmpty()) return;
auto& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
BunString source = Bun::toString(value);
JSValue parsed = JSValue::decode(Bun__Process__tokenizeWorkerOptions(globalObject, &source, isBun, strict));
RETURN_IF_EXCEPTION(scope, );
if (parsed.isString()) {
String message = parsed.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, );
scope.throwException(globalObject, Bun::createError(globalObject, Bun::ErrorCode::ERR_WORKER_INVALID_EXEC_ARGV, makeString("Initiated Worker with invalid "_s, isBun ? "BUN_OPTIONS"_s : "NODE_OPTIONS"_s, " env variable: "_s, message)));
return;
}
RELEASE_AND_RETURN(scope, appendWorkerOptionStrings(globalObject, parsed, out));
}

// Functions

static JSC_DECLARE_HOST_FUNCTION(jsWorkerPrototypeFunction_terminate);
Expand Down Expand Up @@ -311,6 +351,7 @@ template<> __attribute__((minsize)) JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES
EnsureStillAliveScope argument1 = callFrame->argument(1);

WorkerOptions options {};
bool explicitEnvironment = false;
// Founding an env tree swaps the parent's process.env, so it is deferred until
// every option has validated (below).
bool shareEnv = false;
Expand Down Expand Up @@ -413,6 +454,7 @@ template<> __attribute__((minsize)) JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES

auto envValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "env"_s));
RETURN_IF_EXCEPTION(throwScope, {});
explicitEnvironment = envValue && envValue.isObject();
// Recognize the SHARE_ENV registry symbol directly so `new globalThis.Worker(url, { env: SHARE_ENV })`
// (which bypasses the node:worker_threads wrapper) shares env instead of throwing
// ERR_INVALID_ARG_TYPE on its own sentinel.
Expand Down Expand Up @@ -495,13 +537,53 @@ template<> __attribute__((minsize)) JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES
});
RETURN_IF_EXCEPTION(throwScope, {});
options.execArgv.emplace(WTF::move(execArgv));
if (options.kind == WorkerOptions::Kind::Node) {
if (auto error = parseNodeWorkerExecArgv(*options.execArgv, options.execArgvPreloadModules, options.execArgvEvalPreloadCount, options.execArgvBunPreloadCount, options.execArgvRequirePreloadCount, options.execArgvEvalMode)) {
throwScope.throwException(lexicalGlobalObject, Bun::createError(globalObject, Bun::ErrorCode::ERR_WORKER_INVALID_EXEC_ARGV, *error));
return encodedJSValue();
}
options.inheritExecArgv = false;
}
}

if (options.kind == WorkerOptions::Kind::Node) {
if (!options.execArgv) {
JSValue inherited = JSValue::decode(Bun__Process__createExecArgv(globalObject));
RETURN_IF_EXCEPTION(throwScope, {});
options.execArgv.emplace();
appendWorkerOptionStrings(globalObject, inherited, *options.execArgv);
RETURN_IF_EXCEPTION(throwScope, {});
}
if (explicitEnvironment || !options.inheritExecArgv) {
options.inheritPreloads = false;
auto* parentEnv = globalObject->processEnvObject();
RETURN_IF_EXCEPTION(throwScope, {});
Vector<String> effectiveArgv;
auto parentOption = [&](ASCIILiteral key) -> String {
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue value = parentEnv->get(lexicalGlobalObject, Identifier::fromString(vm, key));
RETURN_IF_EXCEPTION(scope, {});
if (value.isUndefined()) return {};
RELEASE_AND_RETURN(scope, value.toWTFString(lexicalGlobalObject));
};
String parentNodeOptions = parentOption("NODE_OPTIONS"_s);
RETURN_IF_EXCEPTION(throwScope, {});
String parentBunOptions = parentOption("BUN_OPTIONS"_s);
RETURN_IF_EXCEPTION(throwScope, {});
String nodeOptions = options.env ? options.env->get("NODE_OPTIONS"_s) : parentNodeOptions;
String bunOptions = options.env ? options.env->get("BUN_OPTIONS"_s) : parentBunOptions;
appendWorkerEnvironmentOptions(globalObject, nodeOptions, false, explicitEnvironment, effectiveArgv);
RETURN_IF_EXCEPTION(throwScope, {});
size_t nodeOptionsCount = effectiveArgv.size();
appendWorkerEnvironmentOptions(globalObject, bunOptions, true, explicitEnvironment, effectiveArgv);
RETURN_IF_EXCEPTION(throwScope, {});
size_t bunOptionsCount = effectiveArgv.size() - nodeOptionsCount;
options.environmentArgc = effectiveArgv.size();
effectiveArgv.appendVector(*options.execArgv);
if (auto error = parseNodeWorkerExecArgv(effectiveArgv, options.execArgvPreloadModules, options.execArgvEvalPreloadCount, options.execArgvBunPreloadCount, options.execArgvRequirePreloadCount, options.execArgvEvalMode, nodeOptionsCount, bunOptionsCount, !explicitEnvironment || nodeOptions == parentNodeOptions, !explicitEnvironment || bunOptions == parentBunOptions, options.inheritExecArgv)) {
throwScope.throwException(lexicalGlobalObject, Bun::createError(globalObject, Bun::ErrorCode::ERR_WORKER_INVALID_EXEC_ARGV, *error));
return encodedJSValue();
}
options.effectiveExecArgv = WTF::move(effectiveArgv);
}
} else {
options.inheritPreloads = options.inheritExecArgv;
if (options.execArgv) options.effectiveExecArgv = *options.execArgv;
}

// Resolve the spawning thread's env tree (founding one if needed) so disjoint
Expand Down
10 changes: 9 additions & 1 deletion src/jsc/bindings/webcore/WorkerMessagingProxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,12 @@ void* WebWorker__create(
StringImpl** argvPtr,
size_t argvLen,
bool defaultExecArgv,
bool inheritPreloads,
StringImpl** execArgvPtr,
size_t execArgvLen,
StringImpl** effectiveExecArgvPtr,
size_t effectiveExecArgvLen,
size_t environmentArgc,
// NODE_USE_SYSTEM_CA as seen by the worker's own `env` option: 1 / 0, or -1 when it inherits the env.
int8_t envUseSystemCa,
BunString* preloadModulesPtr,
Expand Down Expand Up @@ -190,9 +194,13 @@ ExceptionOr<void> WorkerMessagingProxy::startWorkerGlobalScope(const String& scr
m_options.kind == WorkerOptions::Kind::Node,
reinterpret_cast<WTF::StringImpl**>(m_options.argv.begin()),
m_options.argv.size(),
!m_options.execArgv.has_value(),
m_options.inheritExecArgv,
m_options.inheritPreloads,
execArgv.data(),
execArgv.size(),
reinterpret_cast<WTF::StringImpl**>(m_options.effectiveExecArgv.begin()),
m_options.effectiveExecArgv.size(),
m_options.environmentArgc,
envUseSystemCa,
preloadModules.begin(),
preloadModules.size(),
Expand Down
8 changes: 6 additions & 2 deletions src/jsc/bindings/webcore/WorkerOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,13 @@ struct WorkerOptions {
Vector<String> preloadModules;
std::optional<HashMap<String, String>> env;
Vector<String> argv;
// If nullopt, inherit execArgv from the parent thread
// Node workers snapshot their selected CLI arguments, including inherited arguments.
std::optional<Vector<String>> execArgv;
// --require/--import modules parsed from an explicit node Worker execArgv.
bool inheritExecArgv { true };
bool inheritPreloads { true };
Vector<String> effectiveExecArgv;
size_t environmentArgc { 0 };
// --require/--import modules parsed from the selected environment and execArgv.
// Kept raw so resolution and evaluation happen in the worker VM.
Vector<String> execArgvPreloadModules;
size_t execArgvEvalPreloadCount { 0 };
Expand Down
Loading
Loading