Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
f8734c7
process: reject partial property descriptors on process.env
cirospaciari Jul 18, 2026
5c8028f
test: quarantine worker-terminate ASAN crashes, matching main
cirospaciari Jul 20, 2026
7ef097b
worker: hold an exception scope while populating process.env
cirospaciari Jul 20, 2026
19227b9
worker: validate execArgv against a real flag policy table (+2 tests)
cirospaciari Jul 22, 2026
608e06c
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 22, 2026
6c0412a
worker: allow Bun run-surface flags in execArgv and NODE_OPTIONS
cirospaciari Jul 22, 2026
4d05af1
ci: keep the binary size allowance on the stack tip [allow size]
cirospaciari Jul 22, 2026
a2c7d8d
Merge ciro/worker-threads-node-tests into claude/process-env-descript…
cirospaciari Jul 22, 2026
6a6c7d2
ci: keep the binary size allowance on the stack tip [allow size]
cirospaciari Jul 22, 2026
2fdb75a
worker: take the JSLock before installing globalThis.gc
cirospaciari Jul 22, 2026
54baff8
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 22, 2026
52f6889
worker: cover the full process.execArgv surface in the execArgv policy
cirospaciari Jul 22, 2026
7f26d4d
ci: rebuild [allow size]
cirospaciari Jul 23, 2026
0bd590d
worker: split glued short-flag values in execArgv like the CLI parser
cirospaciari Jul 23, 2026
b07212e
worker/env: chain short execArgv flags like the CLI parser; no phanto…
cirospaciari Jul 23, 2026
7ce8a68
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 23, 2026
5c7c5b3
worker: chain short flags in NODE_OPTIONS validation too
cirospaciari Jul 23, 2026
4698960
process.env: reject valueless data descriptors and symbol keys like n…
cirospaciari Jul 23, 2026
5798e87
process: normalize glued short flags when building process.execArgv
cirospaciari Jul 23, 2026
11bb858
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 23, 2026
e2ba2d4
process: scope execArgv short-flag normalization to the bun/node entr…
cirospaciari Jul 23, 2026
12b617a
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 23, 2026
31b3953
Delete flaky GC-observation node tests (#35182)
Jarred-Sumner Jul 22, 2026
c545cec
process: close the -br/run gaps in execArgv short-flag normalization
robobun Jul 24, 2026
f359442
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 24, 2026
5e87cc7
clippy: use ? now that push_normalized_short_token returns Option
robobun Jul 24, 2026
d1b410e
worker: share the process.execArgv token builder with the inherit-pat…
robobun Jul 24, 2026
ebcb030
worker: honor prev_takes_value before short-chain normalization; acce…
robobun Jul 24, 2026
6068f57
test: pin the -pe execArgv round-trip
cirospaciari Jul 24, 2026
b1010fb
process.env: stringify defineProperty values like node; isolate SHARE…
robobun Jul 24, 2026
490b699
process.env: coerce defined values to strings; fail preventExtensions…
cirospaciari Jul 24, 2026
f9ac6f4
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 24, 2026
7a8bf0c
worker: only One/Many flags consume the next argv token
cirospaciari Jul 24, 2026
12d92a7
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 24, 2026
4f1552f
windowsEnv: fix defineProperty bookkeeping for always-present accesso…
cirospaciari Jul 24, 2026
1fce00b
test: pin windowsEnv defineProperty bookkeeping for always-present ac…
robobun Jul 24, 2026
01d9f03
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 24, 2026
e8a82b8
test: drop stale bookkeeping for the GC-observation tests deleted by …
robobun Jul 24, 2026
cec7675
test: run the windowsEnv special-accessor defineProperty check in a p…
robobun Jul 24, 2026
f38b64b
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 24, 2026
409d44d
test: drop the orphaned comment block for the deleted tls memleak entry
robobun Jul 24, 2026
42233c1
Merge remote-tracking branch 'origin/ciro/worker-threads-node-tests' …
robobun Aug 3, 2026
d7b446a
worker: use AUTO_PARAMS directly in table_map after #36184 made the s…
robobun Aug 3, 2026
bdfa422
Worker.cpp: guard WebWorker__dispatchError/dispatchErrorWithValue aga…
robobun Aug 3, 2026
cb25e5d
web_worker: clear the pending TerminationException (not just the requ…
robobun Aug 3, 2026
9f100bf
trim comments to <=3 lines, cite spec/node source
robobun Aug 3, 2026
4da9c57
Revert the shutdown clear_termination_exception swap
robobun Aug 3, 2026
9c8c3a4
Merge remote-tracking branch 'origin/ciro/worker-threads-node-tests' …
robobun Aug 3, 2026
462ca19
Merge remote-tracking branch 'origin/ciro/worker-threads-node-tests' …
robobun Aug 3, 2026
e623e6c
Merge remote-tracking branch 'origin/ciro/worker-threads-node-tests' …
robobun Aug 4, 2026
af68afa
clippy: SAFETY comment for us_loop_idle_ns raw call from 388af0e9
robobun Aug 4, 2026
1ade7f3
Merge remote-tracking branch 'origin/ciro/worker-threads-node-tests' …
robobun Aug 4, 2026
21c5d62
Merge branch 'main' into claude/process-env-descriptor-validation
cirospaciari Aug 5, 2026
bd9ab18
Merge branch 'ciro/worker-threads-node-tests' into claude/process-env…
cirospaciari Aug 5, 2026
3bb4d69
Merge branch 'main' into claude/process-env-descriptor-validation
robobun Aug 7, 2026
e75f5ab
Merge branch 'ciro/worker-threads-node-tests' into claude/process-env…
cirospaciari Aug 7, 2026
451b70a
worker_exec_argv: route the = split through strings::index_of_char_usize
cirospaciari Aug 7, 2026
846e516
windows: wrap a worker's env snapshot in the windowsEnv proxy
robobun Aug 7, 2026
8ce6a4c
process.env: refuse preventExtensions on the main-thread map and the …
robobun Aug 7, 2026
dae59d4
retitle the Windows accessor test for the post-merge reject semantics…
robobun Aug 7, 2026
80485f4
Merge branch 'ciro/worker-threads-node-tests' into claude/process-env…
cirospaciari Aug 7, 2026
80f9d48
Merge branch 'ciro/worker-threads-node-tests' into claude/process-env…
cirospaciari Aug 7, 2026
945f6c3
env maps: align symbol-key handling across put/defineProperty and pla…
robobun Aug 7, 2026
c198cfe
Trim comments to node-source/spec references
robobun Aug 7, 2026
18dfe4a
Merge branch 'ciro/worker-threads-node-tests' into claude/process-env…
robobun Aug 7, 2026
bb2f2c4
NODE_OPTIONS: consume a required flag's next token unconditionally
robobun Aug 8, 2026
c52ec62
env: match node's Symbol conversion message on the regular map's put …
robobun Aug 8, 2026
bcf6f06
Merge remote-tracking branch 'origin/ciro/worker-threads-node-tests' …
robobun Aug 16, 2026
9797b85
worker: acknowledge the manufactured TerminationException; stop befor…
robobun Aug 16, 2026
29bfe3b
windows worker env: check and propagate like the POSIX arm instead of…
robobun Aug 16, 2026
c5e31ec
worker: tokenize compile-exec-argv with the same helper process.execA…
robobun Aug 16, 2026
9423635
Merge remote-tracking branch 'origin/ciro/worker-threads-node-tests' …
robobun Aug 16, 2026
df50bae
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 16, 2026
2fe062b
ci: retrigger
robobun Aug 16, 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
22 changes: 11 additions & 11 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1658,12 +1658,15 @@ pub type RuntimeState = *mut c_void;

/// The subset of a Worker's `execArgv` that bun acts on. Flags whose value must
/// outlive the parse (--cpu-prof-dir/-name) are absent; nothing needs them yet.
#[derive(Default, Clone, Copy)]
#[derive(Default, Clone)]
pub struct WorkerExecArgv {
pub allow_addons: Option<bool>,
pub use_system_ca: Option<bool>,
pub cpu_prof: bool,
pub cpu_prof_interval: Option<u32>,
pub expose_gc: bool,
/// `--require`/`-r`/`--preload`/`--import` specifiers, in order.
pub preloads: Vec<Box<[u8]>>,
}

pub struct RuntimeHooks {
Expand Down Expand Up @@ -1791,16 +1794,13 @@ pub struct RuntimeHooks {
transpiler: *mut Transpiler<'static>,
graph: &'static dyn bun_resolver::StandaloneModuleGraph,
),
/// Parse `execArgv` against the `RunCommand`
/// param table and return the resulting `allow_addons` value
/// (`!args.flag("--no-addons")`), or `None` if parsing failed.
/// The param table lives in
/// `bun_runtime::cli` (forward-dep). Only `--no-addons` is honoured;
/// the caller writes the returned `allow_addons` back into
/// `transform_options.allow_addons` so the override semantics
/// ("override the existing even if it was set") match, and applies
/// `cpu_prof` to the worker VM.
pub parse_worker_exec_argv: unsafe fn(exec_argv: &[bun_core::WTFStringImpl]) -> WorkerExecArgv,
/// Parse a worker's own `execArgv` against the worker flag policy table
/// (`bun_runtime::cli::worker_exec_argv`, forward-dep) and return the
/// honoured per-worker subset. `None` derives the honoured defaults for an
/// inheriting worker from the process argv (preloads and the CPU profiler
/// are excluded there — the parent VM already carries both).
pub parse_worker_exec_argv:
unsafe fn(exec_argv: Option<&[bun_core::WTFStringImpl]>) -> WorkerExecArgv,
/// `CronJob.clearAllForVM(vm, .teardown)`. `CronJob` lives in
/// `bun_runtime::api::cron`.
pub cron_clear_all_teardown: fn(vm: &mut VirtualMachine),
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/ErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ const errors: ErrorCodeMapping = [
["ERR_TRACE_EVENTS_UNAVAILABLE", Error],
["ERR_SQLITE_ERROR", Error],
["ERR_CRYPTO_ARGON2_NOT_SUPPORTED", Error],
["ERR_WORKER_INVALID_EXEC_ARGV", Error],
// llhttp reports a missing CRLF after a chunk's data as HPE_STRICT,
// distinct from a malformed chunk-size line (HPE_INVALID_CHUNK_SIZE).
["HPE_STRICT", Error],
Expand Down
104 changes: 98 additions & 6 deletions src/jsc/bindings/JSEnvironmentVariableMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "BunProcess.h"
#include "ScriptExecutionContext.h"
#include "SharedEnvStore.h"
#include "ErrorCode.h"
#include "wtf/NeverDestroyed.h"
#include "WebCoreJSBuiltins.h"

Expand Down Expand Up @@ -392,6 +393,31 @@ static SharedEnvStore* sharedEnvStoreFor(JSC::JSObject* object)
return globalObject ? sharedEnvStoreFor(globalObject) : nullptr;
}

// node rejects anything but a full, fully-permissive data descriptor on
// process.env (src/node_env_var.cc, EnvDefiner). Bun deliberately still accepts
// accessors — see the "does not let the store shadow an accessor defined on
// process.env" test — so only the data-descriptor half of node's rule is
// enforced here: a descriptor carrying a value must spell out writable,
// enumerable and configurable, all true. Accessor and empty descriptors keep
// their existing behaviour. Returns false with an exception pending on reject.
static bool validateEnvPropertyDescriptor(JSC::JSGlobalObject* globalObject, const JSC::PropertyDescriptor& descriptor, JSC::ThrowScope& scope)
{
static constexpr auto dataDescriptorMessage = "'process.env' only accepts a configurable, writable, and enumerable data descriptor"_s;

if (!descriptor.value())
return true;

// A partial data descriptor is rejected even when what it does specify is
// permissive: node requires all three attributes to be present and true.
if (!descriptor.writablePresent() || !descriptor.enumerablePresent() || !descriptor.configurablePresent()
|| !descriptor.writable() || !descriptor.enumerable() || !descriptor.configurable()) {
scope.throwException(globalObject, createError(globalObject, Bun::ErrorCode::ERR_INVALID_OBJECT_DEFINE_PROPERTY, dataDescriptorMessage));
return false;
}

return true;
}

// process.env variant whose reads/writes/deletes/enumeration go through the
// tree's SharedEnvStore; no instance state, so no custom subspace.
class JSSharedEnvMap final : public JSC::JSNonFinalObject {
Expand Down Expand Up @@ -602,6 +628,9 @@ bool JSSharedEnvMap::defineOwnProperty(JSObject* object, JSGlobalObject* globalO
VM& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);

if (!validateEnvPropertyDescriptor(globalObject, descriptor, scope))
return false;

auto* uid = propertyName.uid();
if (propertyName.isSymbol() || !uid || !descriptor.isDataDescriptor() || !descriptor.value()) {
// The descriptor lands on the Base object, but getOwnPropertySlot reads the
Expand Down Expand Up @@ -742,19 +771,82 @@ RefPtr<SharedEnvStore> ensureSharedEnvStoreForWorker(Zig::GlobalObject* globalOb
return store;
}

// The ordinary (non-SHARE_ENV) process.env. A plain object apart from
// defineOwnProperty, which node intercepts to reject descriptors that are not
// fully-permissive data descriptors; without a method-table hook the validation
// has nowhere to live, so process.env needs its own class rather than a
// constructEmptyObject().
class JSProcessEnvMap final : public JSC::JSNonFinalObject {
public:
using Base = JSC::JSNonFinalObject;

static constexpr unsigned StructureFlags = Base::StructureFlags;

template<typename CellType, JSC::SubspaceAccess>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
{
STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSProcessEnvMap, Base);
return &vm.plainObjectSpace();
}

DECLARE_INFO;

static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
}

static JSProcessEnvMap* create(JSC::VM& vm, JSC::Structure* structure)
{
JSProcessEnvMap* ptr = new (NotNull, JSC::allocateCell<JSProcessEnvMap>(vm)) JSProcessEnvMap(vm, structure);
ptr->finishCreation(vm);
return ptr;
}

static bool defineOwnProperty(JSObject* object, JSGlobalObject* globalObject, JSC::PropertyName propertyName, const JSC::PropertyDescriptor& descriptor, bool shouldThrow)
{
VM& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);

if (!validateEnvPropertyDescriptor(globalObject, descriptor, scope))
return false;

RELEASE_AND_RETURN(scope, Base::defineOwnProperty(object, globalObject, propertyName, descriptor, shouldThrow));
}

private:
JSProcessEnvMap(JSC::VM& vm, JSC::Structure* structure)
: Base(vm, structure)
{
}

void finishCreation(JSC::VM& vm)
{
Base::finishCreation(vm);
}
};

const JSC::ClassInfo JSProcessEnvMap::s_info = { "ProcessEnv"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSProcessEnvMap) };

JSObject* createEmptyProcessEnvMap(Zig::GlobalObject* globalObject)
{
VM& vm = globalObject->vm();
return JSProcessEnvMap::create(vm, JSProcessEnvMap::createStructure(vm, globalObject, globalObject->objectPrototype()));
}

JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject)
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

void* list;
size_t count = Bun__getEnvCount(globalObject, &list);
JSC::JSObject* object = nullptr;
if (count < 63) {
object = constructEmptyObject(globalObject, globalObject->objectPrototype(), count);
} else {
object = constructEmptyObject(globalObject, globalObject->objectPrototype());
}
// Unlike the constructEmptyObject() this replaces, the storage is not
// pre-sized to the env count: JSNonFinalObject asserts it has no inline
// storage, so the vars below always land in the butterfly. Only JSFinalObject
Comment thread
cirospaciari marked this conversation as resolved.
Outdated
// gets inline slots, and it is `final` — a defineOwnProperty hook and inline
// storage are mutually exclusive here.
JSC::JSObject* object = JSProcessEnvMap::create(vm, JSProcessEnvMap::createStructure(vm, globalObject, globalObject->objectPrototype()));

#if OS(WINDOWS)
JSArray* keyArray = constructEmptyArray(globalObject, nullptr, count);
Expand Down
5 changes: 5 additions & 0 deletions src/jsc/bindings/JSEnvironmentVariableMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ JSC::JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject);
// through the SharedEnvStore of the tree its global belongs to.
JSC::JSValue createSharedEnvironmentVariablesMap(Zig::GlobalObject* globalObject);

// Empty process.env for a worker that was handed a snapshot of the spawning
// thread's env: same class as the ordinary map so defineProperty validation
// applies on worker threads too. Caller populates it.
JSC::JSObject* createEmptyProcessEnvMap(Zig::GlobalObject* globalObject);

// Resolve the SHARE_ENV store for a worker spawned from `globalObject`: the
// spawning thread's existing store if it has one, otherwise a fresh store seeded
// from its `process.env` (which is then swapped to a write-through view).
Expand Down
13 changes: 12 additions & 1 deletion src/jsc/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,10 @@ extern "C" JSC::JSGlobalObject* Zig__GlobalObject__create(void* console_client,
if (executionContextId > -1) {
const auto initializeWorker = [&](WebCore::Worker& worker) -> void {
auto& options = worker.options();
// Outermost exception scope: this runs from Rust with no scope on the
// stack, and numeric env keys reach JSProcessEnvMap::defineOwnProperty
// (a throwing path) via putDirectMayBeIndex.
auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm);

if (options.env.has_value()) {
HashMap<String, String> map = *std::exchange(options.env, std::nullopt);
Expand All @@ -562,12 +566,16 @@ extern "C" JSC::JSGlobalObject* Zig__GlobalObject__create(void* console_client,
strings.append(jsString(vm, value));
}

auto env = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), size >= JSFinalObject::maxInlineCapacity ? JSFinalObject::maxInlineCapacity : size);
auto env = Bun::createEmptyProcessEnvMap(globalObject);
size_t i = 0;
for (auto k : map) {
// They can have environment variables with numbers as keys.
// So we must use putDirectMayBeIndex to handle that.
env->putDirectMayBeIndex(globalObject, JSC::Identifier::fromString(vm, WTF::move(k.key)), strings.at(i++));
// Numeric keys route through JSProcessEnvMap::defineOwnProperty,
// a throwing path; check between calls to satisfy scope discipline.
if (catchScope.exception()) [[unlikely]]
break;
}
globalObject->m_processEnvObject.set(vm, globalObject, env);
} else if (options.sharedEnvStore) {
Expand All @@ -578,6 +586,9 @@ extern "C" JSC::JSGlobalObject* Zig__GlobalObject__create(void* console_client,
globalObject->scriptExecutionContext()->setSharedEnvStore(*store);
globalObject->m_processEnvObject.set(vm, globalObject, Bun::createSharedEnvironmentVariablesMap(globalObject).getObject());
}
// Only fully-permissive data descriptors are defined above, so the
// env hook cannot reject them; nothing here may leave an exception.
catchScope.assertNoException();

// Ensure that the TerminationException singleton is constructed. Workers need this so
// that we can request their termination from another thread. For the main thread, we
Expand Down
31 changes: 31 additions & 0 deletions src/jsc/bindings/webcore/JSWorker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@
#include "JSEnvironmentVariableMap.h"
#include <JavaScriptCore/JSMap.h>

// Worker execArgv / NODE_OPTIONS policy (src/runtime/cli/worker_exec_argv.rs).
// Both return true when valid; otherwise write the ERR_WORKER_INVALID_EXEC_ARGV
// message tail into outMessage.
extern "C" bool Bun__Worker__validateExecArgv(WTF::StringImpl* const* argv, size_t len, BunString* outMessage);
extern "C" bool Bun__Worker__validateWorkerNodeOptions(WTF::StringImpl* nodeOptions, BunString* outMessage);

namespace WebCore {
using namespace JSC;

Expand Down Expand Up @@ -295,6 +301,21 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor::
env.add(key.impl()->isolatedCopy(), str);
}

// node_worker.cc: only an explicitly provided env object has its
// NODE_OPTIONS validated (the Rust side skips when it is
// byte-identical to the parent's, i.e. process.env passed through).
if (envValue && envValue.isCell()) {
auto nodeOptions = env.find("NODE_OPTIONS"_s);
if (nodeOptions != env.end()) {
BunString invalidNodeOptions = BunStringEmpty;
if (!Bun__Worker__validateWorkerNodeOptions(nodeOptions->value.impl(), &invalidNodeOptions)) {
auto message = makeString("Initiated Worker with invalid NODE_OPTIONS env variable: "_s, invalidNodeOptions.transferToWTFString());
throwScope.throwException(lexicalGlobalObject, Bun::createError(globalObject, Bun::ErrorCode::ERR_WORKER_INVALID_EXEC_ARGV, message));
return {};
}
}
}

options.env.emplace(WTF::move(env));
}
}
Expand Down Expand Up @@ -334,6 +355,16 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor::
execArgv.append(str);
});
RETURN_IF_EXCEPTION(throwScope, {});
// node_worker.cc: an explicit execArgv is validated synchronously
// against the worker flag policy table (unknown flags, flags Bun or
// node cannot honour in a worker, and missing required values).
BunString invalidExecArgv = BunStringEmpty;
static_assert(sizeof(WTF::String) == sizeof(WTF::StringImpl*));
if (!Bun__Worker__validateExecArgv(reinterpret_cast<WTF::StringImpl* const*>(execArgv.begin()), execArgv.size(), &invalidExecArgv)) {
auto message = makeString("Initiated Worker with invalid execArgv flags: "_s, invalidExecArgv.transferToWTFString());
throwScope.throwException(lexicalGlobalObject, Bun::createError(globalObject, Bun::ErrorCode::ERR_WORKER_INVALID_EXEC_ARGV, message));
return {};
}
options.execArgv.emplace(WTF::move(execArgv));
}
}
Expand Down
57 changes: 43 additions & 14 deletions src/jsc/web_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ pub struct WebWorker {
/// Heap-owned by this struct; freed in `destroy()`.
unresolved_specifier: Box<[u8]>,
preloads: Vec<Box<[u8]>>,
/// `--expose-gc` for this worker: own execArgv wins; an inheriting
/// worker takes the immediate parent's value (nested workers chain).
expose_gc: bool,
/// Honored options parsed once from an explicit execArgv in `create()`
/// (defaults for an inheriting worker); read again in `start_vm`.
own_exec_argv_options: virtual_machine::WorkerExecArgv,
/// Owned NUL-terminated bytes.
name: bun_core::ZBox,

Expand Down Expand Up @@ -591,6 +597,32 @@ impl WebWorker {
}
}

// execArgv honouring: an explicit list contributes its preload flags
// (raw specifiers — `load_preloads` resolves worker-side, so a bad path
// fails at runtime like node) and `--expose-gc`; inherit chains the parent.
let hooks = runtime_hooks().expect("RuntimeHooks not installed");
let mut own_exec_argv_options = virtual_machine::WorkerExecArgv::default();
let expose_gc = if inherit_exec_argv {
match parent_ref.worker_ref() {
Some(parent_worker) => parent_worker.expose_gc,
// SAFETY: `None` reads only process-constant state.
None => unsafe { (hooks.parse_worker_exec_argv)(None) }.expose_gc,
}
} else {
// SAFETY: `exec_argv_ptr`/`exec_argv_len` describe the C++
// `WorkerOptions` array, alive for this call; the hook only reads it.
let mut parsed = unsafe {
(hooks.parse_worker_exec_argv)(Some(bun_core::ffi::slice(
exec_argv_ptr,
exec_argv_len,
)))
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
};
preloads.extend(core::mem::take(&mut parsed.preloads));
let expose_gc = parsed.expose_gc;
own_exec_argv_options = parsed;
expose_gc
};

let store_fd = parent_ref.transpiler.resolver.store_fd;

let worker = bun_core::heap::into_raw(Box::new(WebWorker {
Expand All @@ -608,6 +640,8 @@ impl WebWorker {
inherit_exec_argv,
unresolved_specifier: spec_slice.slice().to_vec().into_boxed_slice(),
preloads,
expose_gc,
own_exec_argv_options,
name: if name_str.is_empty() {
bun_core::ZBox::default()
} else {
Expand Down Expand Up @@ -911,23 +945,11 @@ impl WebWorker {
// and passes the owned struct as `args` to the new VM.
let mut transform_options = (*parent.transpiler.options.transform_options).clone();

// Honours `--no-addons` and `--cpu-prof`; the hook owns the temporary
// UTF-8 allocs. The param table lives in `bun_runtime::cli` (forward-dep),
// so dispatch through `RuntimeHooks::parse_worker_exec_argv`.
//
// SAFETY: `exec_argv` borrows C++ `WorkerOptions` kept alive by the
// owning `WebCore::Worker` for `self`'s lifetime; the hook only reads
// the slice and owns its own temporary allocations.
// `None` means "inherit from the parent" (WorkerOptions.h); an explicit
// Honored execArgv options were parsed once in `create()`; an explicit
// list — even an empty one — replaces the parent's, as node resets to
// fresh defaults whenever execArgv is given (node_worker.cc).
let own_exec_argv = self.exec_argv();
let exec_argv = match own_exec_argv {
// SAFETY: `a` is this worker's execArgv, owned by the WebWorker and
// alive for the call; the hook only reads it.
Some(a) => unsafe { (hooks.parse_worker_exec_argv)(a) },
None => Default::default(),
};
let exec_argv = &self.own_exec_argv_options;
if let Some(allow_addons) = exec_argv.allow_addons {
// override the existing even if it was set
transform_options.allow_addons = Some(allow_addons);
Expand Down Expand Up @@ -1055,6 +1077,13 @@ impl WebWorker {
// SAFETY: `jsc_vm` is set by `init_worker` above.
crate::bun_cpu_profiler::start_cpu_profiler(unsafe { &mut *vm_ref.jsc_vm });
}

// `--expose-gc` is per-global in JSC, so a worker can honour its
// own execArgv (or the inherited setting) independently of the
// main thread — same helper `add_conditional_globals` uses.
if self.expose_gc {
crate::cpp::JSC__JSGlobalObject__addGc(vm_ref.global());
}
}

// Publish `vm` now (rather than at the end of startVM) so that:
Expand Down
1 change: 1 addition & 0 deletions src/runtime/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ pub mod test {
#[path = "Arguments.rs"]
pub mod arguments;
pub use arguments as Arguments;
pub mod worker_exec_argv;
// bunfig.toml without a tier-6 dependency. Re-export under the original path so
// existing `crate::cli::bunfig` / `crate::cli::Bunfig` callers are unaffected.
pub use bun_bunfig::Bunfig;
Expand Down
Loading
Loading