Skip to content
Open
5 changes: 4 additions & 1 deletion src/jsc/bindings/BunObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ static JSValue constructWebViewObject(VM& vm, JSObject* bunObject);

static JSValue constructEnvObject(VM& vm, JSObject* object)
{
return uncheckedDowncast<Zig::GlobalObject>(object->globalObject())->processEnvObject();
auto scope = DECLARE_THROW_SCOPE(vm);
JSObject* env = uncheckedDowncast<Zig::GlobalObject>(object->globalObject())->processEnvObject();
RETURN_IF_EXCEPTION(scope, {});
return env;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

JSC::EncodedJSValue flattenArrayOfBuffersIntoArrayBufferOrUint8Array(JSGlobalObject* lexicalGlobalObject, JSValue arrayValue, size_t maxLength, bool asUint8Array)
Expand Down
17 changes: 8 additions & 9 deletions src/jsc/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2636,7 +2636,9 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb
};

auto constructEnvironmentVariables = [&]() -> JSC::JSValue {
return globalObject->processEnvObject();
JSC::JSObject* env = globalObject->processEnvObject();
RETURN_IF_EXCEPTION(scope, {});
return env;
};

auto constructCpus = [&]() -> JSC::JSValue {
Expand Down Expand Up @@ -3217,15 +3219,12 @@ static JSValue constructRevision(VM& vm, JSObject* processObject)
static JSValue constructEnv(VM& vm, JSObject* processObject)
{
auto* globalObject = uncheckedDowncast<Zig::GlobalObject>(processObject->globalObject());
// Lazy property builder: exceptions must not propagate into
// reifyStaticProperty, which performs no exception check.
// Not a ThrowScope: reifyAllStaticProperties runs the next builder without an exception check, which validateExceptionChecks flags as an unchecked simulated throw.
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
JSValue env = globalObject->processEnvObject();
if (auto* exception = scope.exception()) [[unlikely]] {
(void)scope.tryClearException();
Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception);
return JSC::jsUndefined();
}
JSObject* env = globalObject->processEnvObject();
// Left pending on purpose: the read throws and `env` stays unreified, so the next read builds it (like Bun.$).
if (scope.exception()) [[unlikely]]
return {};
return env;
}

Expand Down
3 changes: 2 additions & 1 deletion src/jsc/bindings/BunProcessReportObjectWindows.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,9 @@ JSValue constructReportObjectWindows(VM& vm, Zig::GlobalObject* globalObject, Pr
RETURN_IF_EXCEPTION(scope, {});

// Environment variables
report->putDirect(vm, Identifier::fromString(vm, "environmentVariables"_s), globalObject->processEnvObject(), 0);
JSObject* env = globalObject->processEnvObject();
RETURN_IF_EXCEPTION(scope, {});
report->putDirect(vm, Identifier::fromString(vm, "environmentVariables"_s), env, 0);

return report;
}
Expand Down
5 changes: 4 additions & 1 deletion src/jsc/bindings/ImportMetaObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,10 @@ JSC_DEFINE_CUSTOM_SETTER(jsImportMetaObjectSetter_require, (JSGlobalObject * jsG
JSC_DEFINE_CUSTOM_GETTER(jsImportMetaObjectGetter_env, (JSGlobalObject * jsGlobalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName))
{
auto* globalObject = uncheckedDowncast<Zig::GlobalObject>(jsGlobalObject);
return JSValue::encode(globalObject->m_processEnvObject.getInitializedOnMainThread(globalObject));
auto scope = DECLARE_THROW_SCOPE(globalObject->vm());
JSObject* env = globalObject->processEnvObject();
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(env);
}

static const HashTableValue ImportMetaObjectPrototypeValues[] = {
Expand Down
26 changes: 10 additions & 16 deletions src/jsc/bindings/JSEnvironmentVariableMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,7 @@ RefPtr<SharedEnvStore> ensureSharedEnvStoreForWorker(Zig::GlobalObject* globalOb
// Founding a new tree. processEnvObject() forces the lazy init so the OS
// environment is captured before the swap below.
JSObject* envObject = globalObject->processEnvObject();
RETURN_IF_EXCEPTION(scope, nullptr);
if (!envObject->staticPropertiesReified()) {
envObject->reifyAllStaticProperties(globalObject);
RETURN_IF_EXCEPTION(scope, nullptr);
Expand Down Expand Up @@ -979,7 +980,7 @@ RefPtr<SharedEnvStore> ensureSharedEnvStoreForWorker(Zig::GlobalObject* globalOb
return store;
}

JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject)
JSObject* createEnvironmentVariablesMap(Zig::GlobalObject* globalObject)
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
Expand All @@ -998,7 +999,7 @@ JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject)
}

JSArray* keyArray = constructEmptyArray(globalObject, nullptr, count);
RETURN_IF_EXCEPTION(scope, {});
RETURN_IF_EXCEPTION(scope, nullptr);
#else
auto* structure = JSEnvironmentVariableMap::createStructure(vm, globalObject, globalObject->objectPrototype());
JSC::JSObject* object = JSEnvironmentVariableMap::create(vm, structure);
Expand Down Expand Up @@ -1078,9 +1079,9 @@ JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject)
ZigString nameStr = toZigString(name);
if (Bun__getEnvValue(globalObject, &nameStr, &valueString)) {
JSValue value = jsString(vm, Zig::toStringCopy(valueString));
RETURN_IF_EXCEPTION(scope, {});
RETURN_IF_EXCEPTION(scope, nullptr);
object->putDirectIndex(globalObject, *index, value, 0, PutDirectIndexLikePutDirect);
RETURN_IF_EXCEPTION(scope, {});
RETURN_IF_EXCEPTION(scope, nullptr);
}
continue;
}
Expand Down Expand Up @@ -1135,25 +1136,18 @@ JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject)
auto editWindowsEnvVar = JSC::JSFunction::create(vm, globalObject, 0, String("editWindowsEnvVar"_s), jsEditWindowsEnvVar, ImplementationVisibility::Public);

JSC::JSFunction* getSourceEvent = JSC::JSFunction::create(vm, globalObject, processObjectInternalsWindowsEnvCodeGenerator(vm), globalObject);
RETURN_IF_EXCEPTION(scope, {});
RETURN_IF_EXCEPTION(scope, nullptr);
JSC::MarkedArgumentBuffer args;
args.append(object);
args.append(keyArray);
args.append(editWindowsEnvVar);
args.append(JSC::JSFunction::create(vm, globalObject, 2, "coerceForWrite"_s, jsProcessEnvCoerceForWrite, ImplementationVisibility::Private));
args.append(JSC::JSFunction::create(vm, globalObject, 1, "resetForDelete"_s, jsProcessEnvResetForDelete, ImplementationVisibility::Private));
auto clientData = WebCore::clientData(vm);
JSC::CallData callData = JSC::getCallData(getSourceEvent);
NakedPtr<JSC::Exception> returnedException = nullptr;
auto result = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, getSourceEvent, callData, globalObject->globalThis(), args, returnedException);
RETURN_IF_EXCEPTION(scope, {});

if (returnedException) {
throwException(globalObject, scope, returnedException.get());
return jsUndefined();
}

RELEASE_AND_RETURN(scope, result);
JSValue result = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, getSourceEvent, callData, globalObject->globalThis(), args);
RETURN_IF_EXCEPTION(scope, nullptr);
// windowsEnv returns `new Proxy(...)`, so a normal return is always an object.
return asObject(result);
#else
return object;
#endif
Expand Down
3 changes: 2 additions & 1 deletion src/jsc/bindings/JSEnvironmentVariableMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ class JSEnvironmentVariableMap final : public JSC::JSNonFinalObject {
}
};

JSC::JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject);
// Null with an exception pending if it threw (Windows finishes the map in JS).
JSC::JSObject* createEnvironmentVariablesMap(Zig::GlobalObject* globalObject);

// Setting TZ must make *existing* Date instances recompute local time. JSC's DateCache
// reset only clears shared slots; live DateInstances keep a Ref to DateInstanceData
Expand Down
32 changes: 15 additions & 17 deletions src/jsc/bindings/JSPropertyIterator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,22 @@ extern "C" JSPropertyIterator* Bun__JSPropertyIterator__create(JSC::JSGlobalObje
if (object->type() == JSC::ProxyObjectType) [[unlikely]] {
// Check if we're actually iterating through the JSEnvironmentVariableMap's proxy.
auto* zigGlobal = defaultGlobalObject(globalObject);
if (zigGlobal->m_processEnvObject.isInitialized()) {
if (object == zigGlobal->m_processEnvObject.get(zigGlobal)) {
object->methodTable()->getOwnPropertyNames(
object,
globalObject,
array,
DontEnumPropertiesMode::Exclude);
RETURN_IF_EXCEPTION(scope, nullptr);

*count = array.size();
if (array.size() == 0) {
return nullptr;
}

auto* iter = JSPropertyIterator::create(vm, array.releaseData());
iter->isSpecialProxy = true;
return iter;
if (object == zigGlobal->m_processEnvObject.get()) {
object->methodTable()->getOwnPropertyNames(
object,
globalObject,
array,
DontEnumPropertiesMode::Exclude);
RETURN_IF_EXCEPTION(scope, nullptr);

*count = array.size();
if (array.size() == 0) {
return nullptr;
}

auto* iter = JSPropertyIterator::create(vm, array.releaseData());
iter->isSpecialProxy = true;
return iter;
}
}
#endif
Expand Down
21 changes: 16 additions & 5 deletions src/jsc/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2541,11 +2541,6 @@ void GlobalObject::finishCreation(VM& vm)
init.set(toJS(init.owner, globalObject, globalObject->performance().get()).getObject());
});

m_processEnvObject.initLater(
[](const JSC::LazyProperty<JSC::JSGlobalObject, JSC::JSObject>::Initializer& init) {
init.set(Bun::createEnvironmentVariablesMap(static_cast<Zig::GlobalObject*>(init.owner)).getObject());
});

m_processObject.initLater(
[](const JSC::LazyProperty<JSC::JSGlobalObject, Bun::Process>::Initializer& init) {
auto* globalObject = defaultGlobalObject(init.owner);
Expand Down Expand Up @@ -3086,6 +3081,22 @@ JSC_DEFINE_CUSTOM_GETTER(functionLazyNavigatorGetter,
return JSC::JSValue::encode(static_cast<Zig::GlobalObject*>(globalObject)->navigatorObject());
}

JSC::JSObject* GlobalObject::processEnvObject()
{
if (JSObject* env = m_processEnvObject.get())
return env;

auto& vm = this->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
JSObject* env = Bun::createEnvironmentVariablesMap(this);
RETURN_IF_EXCEPTION(scope, nullptr);
// Built re-entrantly if user code run by the Windows builtin read process.env; everyone must get that one.
if (JSObject* existing = m_processEnvObject.get())
return existing;
m_processEnvObject.set(vm, this, env);
return env;
}

JSC::GCClient::IsoSubspace* GlobalObject::subspaceForImpl(JSC::VM& vm)
{
return WebCore::subspaceForImpl<GlobalObject, WebCore::UseCustomHeapCellType::Yes>(
Expand Down
5 changes: 3 additions & 2 deletions src/jsc/bindings/ZigGlobalObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,8 @@ class GlobalObject : public Bun::GlobalScope {
void forbidExecution();

Bun::Process* processObject() const { return m_processObject.getInitializedOnMainThread(this); }
JSC::JSObject* processEnvObject() const { return m_processEnvObject.getInitializedOnMainThread(this); }
// Null with the exception pending if building it threw; nothing is cached then.
JSC::JSObject* processEnvObject();
JSC::JSObject* bunObject() const { return m_bunObject.getInitializedOnMainThread(this); }

uint8_t drainMicrotasks();
Expand Down Expand Up @@ -563,7 +564,7 @@ class GlobalObject : public Bun::GlobalScope {
\
V(public, Bun::JSMockModule, mockModule) \
\
V(public, LazyPropertyOfGlobalObject<JSObject>, m_processEnvObject) \
V(public, WriteBarrier<JSObject>, m_processEnvObject) \
\
V(public, LazyPropertyOfGlobalObject<Structure>, m_JSS3FileStructure) \
V(public, LazyPropertyOfGlobalObject<Structure>, m_S3ErrorStructure) \
Expand Down
5 changes: 3 additions & 2 deletions src/jsc/bindings/webcore/JSWorker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,9 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor::

if (envValue && envValue.isCell()) {
envObject = dynamicDowncast<JSC::JSObject>(envValue);
} else if (globalObject->m_processEnvObject.isInitialized()) {
envObject = globalObject->processEnvObject();
} else {
// Null (nothing to copy) unless process.env was ever built.
envObject = globalObject->m_processEnvObject.get();
}

if (envObject) {
Expand Down
127 changes: 126 additions & 1 deletion test/js/node/process/process.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { spawnSync, which } from "bun";
import { describe, expect, it } from "bun:test";
import { familySync } from "detect-libc";
import { bunEnv, bunExe, isMacOS, isWindows, tempDir, tmpdirSync } from "harness";
import { bunEnv, bunExe, isDebug, isMacOS, isWindows, tempDir, tmpdirSync } from "harness";
import { basename, join, resolve } from "path";

const process_sleep = resolve(import.meta.dir, "process-sleep.js");
Expand Down Expand Up @@ -1438,6 +1438,131 @@ describe.concurrent(() => {
delete process.env.BUN_TEST_ENV_PROXY;
}
});

// Windows finishes building process.env by calling the windowsEnv JS
// builtin (POSIX builds the map without entering JS), so a first read with
// almost no stack left throws a RangeError out of the builder. That used to
// hit the RELEASE_ASSERT in the LazyProperty initializer holding the env,
// which on Windows exits with STATUS_STACK_BUFFER_OVERRUN (0xC0000409) and
// prints nothing; Bun.$ reads process.env and bun:sql reads Bun.env while
// being created, so first-touching them died the same way. The read has to
// throw and the next one has to build the real env, so the child retries the
// entry point at every depth while unwinding and then checks the env.
async function firstReadNearStackLimitBuildsRealEnv(entryPoint) {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`const entryPoint = process.argv[1];
let reached = false;
const readers = {
"process.env": () => { reached = true; return process.env; },
"Bun.env": () => { reached = true; return Bun.env; },
"import.meta.env": () => { reached = true; return import.meta.env; },
"Bun.$": () => { reached = true; return Bun.$; },
"Bun.sql": () => { reached = true; return Bun.sql; },
};
const read = readers[entryPoint];
let value;
let readThrew = 0;
function recurse() {
try {
recurse();
} catch {}
if (value !== undefined) return;
reached = false;
try {
value = read();
} catch {
// Only count throws from the read itself, not from failing to
// enter the reader at the very bottom of the stack.
if (reached) readThrew++;
}
}
recurse();
const result = {
retried: readThrew > 0,
type: typeof value,
envIntact: process.env.BUN_TEST_LAZY_ENV === "lazy-env-value",
sameObject: Bun.env === process.env && import.meta.env === process.env,
};
if (entryPoint.endsWith("env")) result.isEnv = value === process.env;
if (entryPoint === "Bun.$") result.echo = (await value({ raw: ["echo $BUN_TEST_LAZY_ENV"] }).text()).trim();
console.log(JSON.stringify(result));`,
entryPoint,
],
env: { ...bunEnv, BUN_TEST_LAZY_ENV: "lazy-env-value" },
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

const expected = { retried: true, envIntact: true, sameObject: true };
if (entryPoint.endsWith("env")) Object.assign(expected, { type: "object", isEnv: true });
else if (entryPoint === "Bun.$") Object.assign(expected, { type: "function", echo: "lazy-env-value" });
else Object.assign(expected, { type: "function" });
expect({ stderr, exitCode, result: stdout && JSON.parse(stdout) }).toEqual({
stderr: "",
exitCode: 0,
result: expected,
});
}

it.each(["process.env", "import.meta.env"])(
"%s first read near the stack limit throws and the next read builds the real env",
firstReadNearStackLimitBuildsRealEnv,
);

// These three are looked up on the Bun object, and the env builder reifies
// Bun.inspect (transitioning Bun) before the point where it can throw, so
// assertion builds trip the stale-structure ASSERT in Structure::storedPrototype
// that the WebKit change in #37001 removes. Release builds (what CI runs on
// Windows) are unaffected.
it.skipIf(isDebug).each(["Bun.env", "Bun.$", "Bun.sql"])(
"%s first read near the stack limit throws and the next read builds the real env",
firstReadNearStackLimitBuildsRealEnv,
);

// The windowsEnv builtin assigns toJSON onto an ordinary object, so a setter
// on Object.prototype runs user code in the middle of the env build and can
// read process.env, re-entering it. The inner read used to come back empty
// without an exception, which is another RELEASE_ASSERT ("did not produce a
// property") and the same 0xC0000409 exit. Now the inner read builds the env
// and the outer one adopts it instead of installing a second object.
it("env build re-entered from user code yields one env object", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`let inner;
Object.defineProperty(Object.prototype, "toJSON", {
configurable: true,
set(fn) {
// Remove the trap first: the inner build assigns toJSON too.
delete Object.prototype.toJSON;
inner = process.env;
Object.defineProperty(this, "toJSON", { value: fn, writable: true, configurable: true, enumerable: true });
},
});
const outer = Bun.env;
console.log(JSON.stringify({
reentered: inner !== undefined,
oneObject: outer === inner && process.env === inner && import.meta.env === inner,
envIntact: inner.BUN_TEST_LAZY_ENV,
json: JSON.parse(JSON.stringify(process.env)).BUN_TEST_LAZY_ENV,
}));`,
],
env: { ...bunEnv, BUN_TEST_LAZY_ENV: "lazy-env-value" },
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stderr, exitCode, result: stdout && JSON.parse(stdout) }).toEqual({
stderr: "",
exitCode: 0,
result: { reentered: true, oneObject: true, envIntact: "lazy-env-value", json: "lazy-env-value" },
});
});
}

it("catches exceptions with process.setUncaughtExceptionCaptureCallback", async () => {
Expand Down
Loading