Skip to content
Open
Show file tree
Hide file tree
Changes from 54 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: 18 additions & 4 deletions src/js/builtins/ProcessObjectInternals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,13 +528,27 @@ export function windowsEnv(
return typeof p !== "symbol" ? delete internalEnv[k] : false;
},
defineProperty(_, p, attributes) {
if (typeof p === "symbol") {
// JSProcessEnvMap rejects symbol keys (after descriptor validation),
// matching node; no bookkeeping applies.
return $Object.$defineProperty(internalEnv, p, attributes);
}
const k = String(p).toUpperCase();
$assert(typeof p === "string"); // proxy is only string and symbol. the symbol would have thrown by now
if (!(k in internalEnv) && !envMapList.includes(p)) {
// Same predicate as the set trap: `k in internalEnv` would be always-true
// for the DontEnum TZ/TLS/proxy CustomAccessors and drop them from ownKeys.
const isNewKey = !envMapList.includes(p) && !envMapList.some(x => x.toUpperCase() === k);
// Define before bookkeeping: JSProcessEnvMap may throw on a partial data
// descriptor, and a rejected define must not leave a phantom envMapList key.
const r = $Object.$defineProperty(internalEnv, k, attributes);
if (isNewKey) {
envMapList.push(p);
}
editWindowsEnvVar(k, internalEnv[k]);
return $Object.$defineProperty(internalEnv, k, attributes);
// String-coerce: the data path is coerced natively, but an accessor
// descriptor (deliberate divergence) installs a getter whose result
// reaches this read raw, and editWindowsEnvVar requires string|null.
const v = internalEnv[k];
editWindowsEnvVar(k, v == null ? null : String(v));
return r;
},
getOwnPropertyDescriptor(target, p) {
if (typeof p === "string") {
Expand Down
13 changes: 9 additions & 4 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1664,12 +1664,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 @@ -1793,9 +1796,11 @@ pub struct RuntimeHooks {
transpiler: *mut Transpiler<'static>,
graph: &'static dyn bun_resolver::StandaloneModuleGraph,
),
/// Parse `execArgv` against the `RunCommand` param table (lives in `bun_runtime::cli`, forward-dep).
/// Caller writes `allow_addons` 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,
/// Parse a worker's `execArgv` (`bun_runtime::cli::worker_exec_argv`,
/// forward-dep); `None` derives an inheriting worker's defaults from the
/// process argv (preloads/cpu-prof excluded — the parent VM 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 @@ -367,6 +367,7 @@ const errors: ErrorCodeMapping = [
// 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],
["ERR_WORKER_INVALID_EXEC_ARGV", Error],
["ERR_CANNOT_WATCH_SIGINT", Error],
["ERR_INSPECTOR_NOT_AVAILABLE", Error],
["ERR_INVALID_REPL_EVAL_CONFIG", TypeError],
Expand Down
140 changes: 128 additions & 12 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 @@ -371,6 +372,27 @@ static SharedEnvStore* sharedEnvStoreFor(JSC::JSObject* object)
return globalObject ? sharedEnvStoreFor(globalObject) : nullptr;
}

// Node rejects all but a full writable+enumerable+configurable data descriptor
// (https://github.com/nodejs/node/blob/main/src/node_env_var.cc EnvDefiner).
// Bun diverges: accessors are still accepted. Returns false with a pending exception 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;

// Accessors pass (Bun divergence); everything else — including attribute-only
// and empty descriptors — must be a full permissive data descriptor per node.
if (descriptor.isAccessorDescriptor())
return true;
if (!descriptor.value()
|| !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 @@ -416,6 +438,11 @@ class JSSharedEnvMap final : public JSC::JSNonFinalObject {
static bool deletePropertyByIndex(JSCell*, JSGlobalObject*, unsigned);
static void getOwnPropertyNames(JSObject*, JSGlobalObject*, JSC::PropertyNameArrayBuilder&, JSC::DontEnumPropertiesMode);
static bool defineOwnProperty(JSObject*, JSGlobalObject*, JSC::PropertyName, const JSC::PropertyDescriptor&, bool shouldThrow);
// See JSProcessEnvMap::preventExtensions — node parity for freeze/seal.
static bool preventExtensions(JSC::JSObject*, JSC::JSGlobalObject*)
{
return false;
}

private:
JSSharedEnvMap(JSC::VM& vm, JSC::Structure* structure)
Expand Down Expand Up @@ -583,13 +610,21 @@ 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;

// node coerces the key to a string after validating the descriptor, so a
// symbol key throws the plain conversion TypeError (no code).
// Symbol-keyed accessors flow through with the accessor divergence.
if (propertyName.isSymbol() && !descriptor.isAccessorDescriptor()) {
JSC::throwTypeError(globalObject, scope, "Cannot convert a Symbol value to a string"_s);
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
// store first, so a store entry would shadow it. Move the entry onto Base as
// an enumerable data property first: a partial descriptor then keeps that
// enumerability, exactly as it does on the regular process.env. (Node rejects
// accessors on process.env outright — on both maps — so match bun's own map.)
if (propertyName.isSymbol() || !uid || descriptor.isAccessorDescriptor()) {
// getOwnPropertySlot reads the store first, so a store entry would shadow the
// Base-landed accessor; hoist it to Base as an enumerable data property first.
if (!propertyName.isSymbol() && uid) {
if (auto* store = sharedEnvStoreFor(object)) {
String existing = store->get(String(uid));
Expand Down Expand Up @@ -723,19 +758,100 @@ RefPtr<SharedEnvStore> ensureSharedEnvStoreForWorker(Zig::GlobalObject* globalOb
return store;
}

// Ordinary (non-SHARE_ENV) process.env: a plain object plus a defineOwnProperty
// hook for node's descriptor validation (https://github.com/nodejs/node/blob/main/src/node_env_var.cc).
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;

if (descriptor.isAccessorDescriptor())
RELEASE_AND_RETURN(scope, Base::defineOwnProperty(object, globalObject, propertyName, descriptor, shouldThrow));

// node coerces the key to a string after validating the descriptor,
// so a symbol key throws the plain conversion TypeError (no code).
// Symbol-keyed accessors flow through with the accessor divergence.
if (propertyName.isSymbol()) {
JSC::throwTypeError(globalObject, scope, "Cannot convert a Symbol value to a string"_s);
return false;
}

// node's EnvDefiner stringifies the value, matching the assignment
// trap; storing the raw value would break the string-only contract
// the Windows env sync (editWindowsEnvVar) relies on.
String stringValue = descriptor.value().toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, false);
JSC::PropertyDescriptor coerced(jsString(vm, stringValue), 0);
RELEASE_AND_RETURN(scope, Base::defineOwnProperty(object, globalObject, propertyName, coerced, shouldThrow));
}

// Node's process.env fails [[PreventExtensions]] so freeze/seal throw and the
// map stays extensible (https://github.com/nodejs/node/blob/main/src/node_env_var.cc).
static bool preventExtensions(JSC::JSObject*, JSC::JSGlobalObject*)
{
return false;
}

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());
}
// Not pre-sized: JSNonFinalObject has no inline storage (only JSFinalObject
// does, and it is `final`), so a defineOwnProperty hook precludes inline slots.
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
17 changes: 16 additions & 1 deletion src/jsc/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,9 @@ 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 scope (called from Rust with none on the stack). putDirect*
// bypasses the defineOwnProperty hook; only allocation can throw here.
auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm);

if (options.env.has_value()) {
HashMap<String, String> map = *std::exchange(options.env, std::nullopt);
Expand All @@ -590,12 +593,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++));
// Index-storage allocation can throw (OOM); stop populating
// rather than keep calling into JSC with a pending exception.
if (catchScope.exception()) [[unlikely]]
break;
}
globalObject->m_processEnvObject.set(vm, globalObject, env);
} else if (options.sharedEnvStore) {
Expand All @@ -606,6 +613,10 @@ extern "C" JSC::JSGlobalObject* Zig__GlobalObject__create(void* console_client,
globalObject->scriptExecutionContext()->setSharedEnvStore(*store);
globalObject->m_processEnvObject.set(vm, globalObject, Bun::createSharedEnvironmentVariablesMap(globalObject).getObject());
}
// The only possible exception above is allocation failure while
// populating env, and there is no JS frame to deliver it to during
// global creation — drop it at this top scope (termination stays).
catchScope.clearExceptionExceptTermination();

// 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 Expand Up @@ -3239,6 +3250,10 @@ JSC_DEFINE_HOST_FUNCTION(functionJsGc,
extern "C" [[ZIG_EXPORT(nothrow)]] void JSC__JSGlobalObject__addGc(JSC::JSGlobalObject* globalObject)
{
auto& vm = JSC::getVM(globalObject);
// Also reached from web_worker.rs start_vm before the worker thread takes
// the API lock; putDirectNativeFunction allocates and asserts the lock.
// JSLock is recursive, so this is a no-op on the main path.
JSC::JSLockHolder locker(vm);
globalObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "gc"_s), 0, functionJsGc, ImplementationVisibility::Public, JSC::NoIntrinsic, PropertyAttribute::DontEnum | 0);
}

Expand Down
30 changes: 30 additions & 0 deletions src/jsc/bindings/webcore/JSWorker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,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 @@ -294,6 +300,20 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor::
env.add(key.impl()->isolatedCopy(), str);
}

// Only an explicit env's NODE_OPTIONS is validated (Rust skips when
// byte-identical to the parent's): https://github.com/nodejs/node/blob/main/src/node_worker.cc
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 @@ -333,6 +353,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
Loading