process.env: coerce assigned values to strings across all construction paths - #34728
process.env: coerce assigned values to strings across all construction paths#34728robobun wants to merge 9 commits into
Conversation
|
Updated 5:52 AM PT - Jul 23rd, 2026
✅ @robobun, your commit 372ec7f35d88ea64659d867e9b55522137fd701b passed in 🧪 To try this PR locally: bunx bun-pr 34728That installs a local version of the PR into your bun-34728 --bun |
WalkthroughChanges
process.env string coercion
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Re the duplicate flag:
This PR is the focused 3-file version of just the coercion piece. Happy to close if #31831 is landing imminently; otherwise this can go in first and #31831 rebases over it (the |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/js/builtins/ProcessObjectInternals.ts (1)
533-541: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winSync the Windows environment after a successful definition.
Line 540 writes
internalEnv[k]before Line 541 defines the property, soObject.defineProperty(process.env, "X", { value: "new" })propagates the old value (or deletion) to the OS environment. Define first, then sync the coerced value only on success.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/js/builtins/ProcessObjectInternals.ts` around lines 533 - 541, Update the defineProperty handling around editWindowsEnvVar and $Object.$defineProperty so the property is defined first, then synchronize Windows using the coerced resulting value only after successful definition. Preserve the existing envMapList bookkeeping and ensure failed definitions do not update the OS environment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/js/builtins/ProcessObjectInternals.ts`:
- Around line 530-536: Update the defineProperty trap to delegate non-string
property keys directly to internalEnv before converting keys or coercing
descriptor values. Preserve the existing string-key behavior, including String
conversion of descriptor values, while ensuring symbol keys and their values
remain unchanged.
In `@src/jsc/bindings/JSEnvironmentVariableMap.cpp`:
- Around line 746-751: The comment above the default non-SHARE_ENV process.env
implementation exceeds the three-line limit. Condense it to three lines or fewer
while preserving the key behavior: assigned values are string-coerced, and
Windows relies on proxy traps with own functions stored directly.
In `@test/js/node/process/process.test.js`:
- Around line 235-288: Update the process.env test setup and its finally cleanup
to snapshot each targeted key’s original property descriptor before mutation,
then restore that descriptor afterward instead of unconditionally deleting the
keys. Apply this to the keys used by the COERCE_* assignments and "4242424242",
preserving absence for keys that were not originally present.
---
Outside diff comments:
In `@src/js/builtins/ProcessObjectInternals.ts`:
- Around line 533-541: Update the defineProperty handling around
editWindowsEnvVar and $Object.$defineProperty so the property is defined first,
then synchronize Windows using the coerced resulting value only after successful
definition. Preserve the existing envMapList bookkeeping and ensure failed
definitions do not update the OS environment.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 2e47aa95-1874-4b29-9464-6664362619d9
📒 Files selected for processing (4)
src/js/builtins/ProcessObjectInternals.tssrc/jsc/bindings/JSEnvironmentVariableMap.cpptest/cli/run/env.test.tstest/js/node/process/process.test.js
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🔴
src/jsc/bindings/JSEnvironmentVariableMap.cpp:844-845— On Windows,internalEnvis now aJSProcessEnvMap, so the direct(internalEnv as any).toJSON = () => {...}assignment insidewindowsEnv()(ProcessObjectInternals.ts:464) hitsJSProcessEnvMap::putand stores the arrow function's source text as a string instead of the function. This breaksJSON.stringify(process.env)'s original-case-key output and will fail the existing Windows test assertingtypeof process.env.toJSON === 'function'. Either keepconstructEmptyObjecton Windows (the Proxysettrap already coerces) or installtoJSONviaputDirect/$putByIdDirect.Extended reasoning...
What the bug is
createEnvironmentVariablesMapnow unconditionally builds the backing object as aJSProcessEnvMap— aJSNonFinalObjectsubclass withOverridesPutwhoseput()callsvalue.toString()on every non-symbol write. On Windows, that object is passed asinternalEnvinto thewindowsEnv()builtin (src/js/builtins/ProcessObjectInternals.ts), which — before creating the Proxy — does:(internalEnv as any).toJSON = () => { ... }; // line 464
This is a plain
=assignment (compiles toput_by_id) directly oninternalEnv, not through the Proxy. It therefore dispatches toJSProcessEnvMap::put, which sees a non-symbol key"toJSON", callsvalue.toString(globalObject)on the arrow function, and stores the function's source text as aJSString. TheBun.inspect.customassignment at line 456 survives because it uses a Symbol key, whichJSProcessEnvMap::putpasses through toBase::putunchanged.Why the existing code doesn't prevent it
The PR description says "On Windows the Proxy still wraps this object; its set trap already stringifies, so writes coerce once in JS and pass a string through." That reasoning is correct only for writes that go through the Proxy (
process.env.X = ...). TheinternalEnv.toJSON = fnassignment happens insidewindowsEnv()on the raw backing object, beforenew Proxy(internalEnv, {...})is even constructed, so it bypasses thesettrap entirely and hits the new C++puthook directly. Before this PRinternalEnvwas a plainconstructEmptyObject()JSObjectwith noputhook, so the function landed as-is.Step-by-step proof
createEnvironmentVariablesMap(JSEnvironmentVariableMap.cpp:844-845) createsobject = JSProcessEnvMap::create(...)— no#if !OS(WINDOWS)guard.- Under
#if OS(WINDOWS),objectis passed asargs[0]toprocessObjectInternalsWindowsEnvCodeGenerator, becoming theinternalEnvparameter ofwindowsEnv(). windowsEnv()executes(internalEnv as any).toJSON = () => {...}(line 464).propertyNameis"toJSON"(not a symbol), soJSProcessEnvMap::putrunsvalue.toString(globalObject)→ the arrow's source text — and stores that string.windowsEnv()returnsnew Proxy(internalEnv, {...})(line 475). Itsgettrap for"toJSON"uppercases to"TOJSON", which is not ininternalEnv, then falls through tointernalEnv["toJSON"](line 490) — returning the stringified function body.- Therefore
typeof process.env.toJSON === 'string'.
Impact
- The existing Windows-only test "windows process.env exposes prototype methods and own props alongside case-insensitive vars" in
test/js/node/process/process.test.jsassertsexpect(typeof process.env.toJSON).toBe("function")andexpect(json.Bun_Test_Env_Proxy_Mixed).toBe("mixed")(which relies on the customtoJSONemitting original-case keys). Both will fail on Windows CI. JSON.stringify(process.env)on Windows no longer routes through the customtoJSON, so its output loses the original-case key names and instead exposes the canonical UPPERCASE storage keys plus a spurious"toJSON"env-var-looking string property.
How to fix
The cleanest fix is to only construct
JSProcessEnvMapon POSIX and keepconstructEmptyObjecton Windows, since the Proxyset/definePropertytraps already coerce every user-visible write — the C++ hook adds nothing there. Alternatively, installtoJSONon the backing object viaputDirectfrom C++ (or$putByIdDirectin the builtin) so it bypasses the coercingput, or special-case"toJSON"inJSProcessEnvMap::put. The first option is smallest and matches the PR's stated intent (the fix targets POSIX; Windows already coerced correctly). -
🔴
src/jsc/bindings/JSEnvironmentVariableMap.cpp:844-845— There's a fourthprocess.envconstruction site this misses: a worker spawned with an explicit env dictionary —new Worker(url, { env: { KEY: 'v' } })— builds itsprocess.envviaJSC::constructEmptyObject(...)atZigGlobalObject.cpp:565and assigns it directly tom_processEnvObject, so inside that workerprocess.env.X = 42still stores the raw number. Per REVIEW.md ('Fix the whole class in the same PR — grep for every sibling site sharing the pattern'), that site should also become aJSProcessEnvMap— expose a small factory (e.g.createProcessEnvMapObject(globalObject)) from this file and call it there in place ofconstructEmptyObject.Extended reasoning...
What the bug is
The PR introduces
JSProcessEnvMapsoprocess.envcoerces assigned values to strings, and the description enumerates three construction paths as covered: the default POSIX object (nowJSProcessEnvMap), the Windows Proxy wrap (whosesettrap already stringifies), and theSHARE_ENVpath (JSSharedEnvMap::putalready callstoWTFString). But there is a fourth path that constructsprocess.env, and it is not touched.At
src/jsc/bindings/ZigGlobalObject.cpp:553-572, when a worker is created with an explicit env dictionary —new Worker(url, { env: { FOO: 'bar' } })— the worker's global builds itsprocess.envvia:auto env = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), ...); // ...populate with putDirectMayBeIndex... globalObject->m_processEnvObject.set(vm, globalObject, env);
This assigns a plain
JSFinalObjectdirectly tom_processEnvObject. It bypassescreateEnvironmentVariablesMapentirely (so it is not aJSProcessEnvMap, and on Windows it gets no Proxy wrap either), and it is not theSHARE_ENVbranch (that's theelse if (options.sharedEnvStore)at line 573).Why existing code doesn't prevent it
JSProcessEnvMapis only instantiated insidecreateEnvironmentVariablesMap, which is the lazy initializer form_processEnvObject(line 2490-2493). The explicit-env worker path pre-populatesm_processEnvObjectbefore the lazy initializer ever runs, socreateEnvironmentVariablesMapis never called for that global. The plain object it installs has noput/putByIndex/defineOwnPropertyoverrides, so writes go straight toJSObject::putand store whateverJSValuearrived — exactly the pre-PR POSIX behavior.Step-by-step proof
- Main thread runs
new Worker('./w.js', { env: { FOO: 'bar' } }). - Worker global initialization enters the
options.env.has_value()branch atZigGlobalObject.cpp:553. - Line 565:
env = JSC::constructEmptyObject(...)→ a plainJSFinalObject. - Line 572:
globalObject->m_processEnvObject.set(vm, globalObject, env)— the LazyProperty is now set, so the initializer at line 2492 (which would have created aJSProcessEnvMap) never runs. - Inside the worker, user code does
process.env.X = 42. envis aJSFinalObject; its method table'sputisJSObject::put, which storesjsNumber(42)verbatim.process.env.Xreads back as the number42, andprocess.env.Y = undefinedreads back asundefined(not'undefined') — the exact divergence this PR fixes on the main thread.
Repro:
const { Worker } = require('worker_threads'); new Worker( "process.env.X = 42; require('assert').strictEqual(process.env.X, '42');", { eval: true, env: { FOO: 'bar' } } );
Node passes; Bun (with this PR) throws
42 !== '42'inside the worker.Impact
Inside any worker spawned with an explicit
env: {...}option,process.envstill fails to coerce on assignment — diverging from Node.js and from Bun's own main-thread behavior after this PR lands. This is exactly the bug class the PR sets out to close, at a sibling construction site. REVIEW.md is explicit on this: "Fix the whole class in the same PR (same-class sites are ONE concern, not scope creep). Grep for every sibling site sharing the pattern… If a site is intentionally excluded, say so in the PR." The PR description does not note this exclusion.How to fix
JSProcessEnvMapis currently file-local toJSEnvironmentVariableMap.cpp. Expose a small factory alongsidecreateSharedEnvironmentVariablesMap:JSObject* createProcessEnvMapObject(Zig::GlobalObject* globalObject) { VM& vm = globalObject->vm(); auto* structure = JSProcessEnvMap::createStructure(vm, globalObject, globalObject->objectPrototype()); return JSProcessEnvMap::create(vm, structure); }
declare it in the header, and at
ZigGlobalObject.cpp:565replaceJSC::constructEmptyObject(...)withBun::createProcessEnvMapObject(globalObject). The subsequentputDirectMayBeIndexloop populating the initial keys is unaffected (it writes directly, bypassing the coercingputhook, and the values are alreadyjsStrings). Add a worker-with-explicit-env case to the new coercion test so this path is covered. - Main thread runs
|
Re the two claude[bot] findings:
Also picked up the related pre-existing Windows bug the new test surfaced: the |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/js/builtins/ProcessObjectInternals.ts`:
- Around line 539-546: Update the defineProperty path around envMapList and
$Object.$defineProperty to use the same case-insensitive key matching as set(),
including hidden variables such as HTTP_PROXY, HTTPS_PROXY, and NO_PROXY.
Perform $Object.$defineProperty first, then add the canonical key to envMapList
only after success, preserving ownKeys() and spread visibility without leaving
entries when definition fails.
In `@test/js/node/process/process.test.js`:
- Around line 292-319: Update the worker test around the message posted by the
Worker to include process.env.SEED, then extend the expected parsed output to
assert SEED equals "seed". Preserve the existing assertions for X, Y, Z, and
their types.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 9bdda5cb-15a3-46da-92de-c1641fe334e2
📒 Files selected for processing (6)
src/js/builtins/ProcessObjectInternals.tssrc/jsc/bindings/JSEnvironmentVariableMap.cppsrc/jsc/bindings/JSEnvironmentVariableMap.hsrc/jsc/bindings/ZigGlobalObject.cpptest/cli/run/env.test.tstest/js/node/process/process.test.js
|
CI at 372ec7f (build #78581, post-rebase): the diff's own tests (
Ready for review. |
Node.js documents that every process.env assignment coerces the value to a string. On POSIX, Bun's process.env was a plain JSObject with no put hook, so assigning a non-string value stored it verbatim: process.env.foo = undefined; // read back as undefined, not "undefined" process.env.bar = 42; // typeof process.env.bar === "number" This broke the common "=== 'undefined'" sentinel check and meant the parent's readback diverged from what a spawned child actually received. The Windows path (a Proxy with a set trap) and the SHARE_ENV path (JSSharedEnvMap::put) already coerced. This change gives the default POSIX object the same treatment by wrapping it in a JSProcessEnvMap that overrides put/putByIndex/defineOwnProperty to call toString() on the value before storing it. The CustomValue getters for lazy OS-env loading are unchanged; they sit on the base object and still work. Also un-todos the existing "setting process.env coerces" test in env.test.ts, which was todoOnPosix for exactly this reason.
The windowsEnv Proxy stores toJSON and Bun.inspect.custom as own properties on the target object before wrapping it. Routing those assignments through JSProcessEnvMap::put coerced the toJSON function to its string source, breaking typeof process.env.toJSON on Windows. Windows already coerces in the Proxy's set trap, so keep the target as a plain JSObject there and add the matching String(value) coercion to the Proxy's defineProperty trap instead. JSProcessEnvMap is now POSIX-only.
ZigGlobalObject's initializeWorker path builds process.env for
new Worker(url, { env: {...} }) as a plain constructEmptyObject and
assigns it to m_processEnvObject directly, so createEnvironmentVariablesMap
never runs and writes inside that worker stored raw values. Route that
construction through the same JSProcessEnvMap via a small factory.
JSProcessEnvMap is no longer #if-guarded: Windows still uses a plain
object inside createEnvironmentVariablesMap (the windowsEnv Proxy coerces
and stores toJSON on the target), but the worker-env path has no Proxy
on any platform and so uses JSProcessEnvMap everywhere.
Also condensed the class comment to three lines per review.
The Windows defineProperty trap called editWindowsEnvVar(k, internalEnv[k]) before defining the property, so a new key synced undefined (tripping the isNull()||isString() assert in debug builds) and an existing key synced its old value. Coerce the descriptor value, define, then sync the coerced string only when a value was supplied.
JSProcessEnvMap is a JSNonFinalObject, so canDoFastPutDirectIndex() is false and indexed putDirectMayBeIndex routes through methodTable()->defineOwnProperty. That path declares ThrowScopes, whose destructors simulate a throw to the caller; with no enclosing scope in initializeWorker the next DECLARE_*_SCOPE elsewhere trips verifyExceptionCheckNeedIsSatisfied. A TopExceptionScope (used the same way elsewhere in bun for top-of-stack loops) absorbs the simulated throw and asserts no real exception per iteration. Also: add finally-cleanup to the un-todo'd env.test.ts coercion test.
JSProcessEnvMap carries its own ClassInfo, so the ObjectStartState check
in CloneSerializer returned DataCloneError for structuredClone(process.env)
and postMessage({env: process.env}). Node structured-clones process.env
as a plain object of its string entries, and Bun did too when the object
was a JSFinalObject. Whitelist both env-map classes alongside
NapiPrototype via a small isEnvironmentVariablesMapObject() predicate.
Test: the coercion tests now assert structuredClone(process.env) succeeds
and round-trips string values; the worker test also verifies the seed env
var is visible and process.env clones inside the worker. Dropped the
stderr-exactly-empty assertion per repo review rules.
windowsEnv defineProperty trap: record the key in envMapList only after
a successful define, using the same case-insensitive check as set().
Windows wraps process.env in a Proxy that structured-clone rejects; that's pre-existing and unchanged by this PR. The worker test still asserts structuredClone(process.env) on every platform (the worker env is a JSProcessEnvMap on Windows too).
…SON.stringify Template-literal coercion in the windowsEnv Proxy set/defineProperty traps so Symbol values throw TypeError the same as POSIX and Node.js (String() special- cases Symbol and would have silently stored "Symbol(s)"). Extends the coercion test to cover BigInt, array, function, Object.assign, the Object.values all-string invariant, and JSON.stringify(process.env) surviving a BigInt assignment.
85f4bbb to
372ec7f
Compare
|
Rebased on main (149 commits behind) and pushed 372ec7f:
All touched test files pass locally on the debug build. |
There was a problem hiding this comment.
I didn't find new issues after the rebase, but this introduces a new JSC class (JSProcessEnvMap) with method-table put/putByIndex/defineOwnProperty overrides, whitelists it in the structured-clone ObjectStartState check, and diverges the POSIX vs Windows process.env backing object — worth a human look, especially given the acknowledged Windows defineProperty Proxy-invariant edge case being deferred to #34727.
Checked: exception-scope handling in the three new hooks matches JSSharedEnvMap (Symbol keys / non-data descriptors pass through, toString → RETURN_IF_EXCEPTION → RELEASE_AND_RETURN); the worker seed loop's DECLARE_TOP_EXCEPTION_SCOPE + assertNoException() is sound because the values are pre-built JSString*s from strings; isEnvironmentVariablesMapObject covers both env-map ClassInfos so structuredClone/postMessage(process.env) don't regress; the existing CustomValue/CustomAccessor getters (TZ, proxy vars) still work under Base::put — the value arrives already coerced to a JSString, so their own toString/toWTFString is idempotent; the Windows defineProperty trap's return true matches the old return $Object.$defineProperty(...) (which returned the target object, also truthy).
Extended reasoning...
Overview
This PR makes process.env coerce all assigned values to strings, matching Node.js. It introduces JSProcessEnvMap (a JSNonFinalObject subclass with overridden put/putByIndex/defineOwnProperty), routes createEnvironmentVariablesMap through it on POSIX, routes the worker explicit-env path in ZigGlobalObject.cpp through a new createProcessEnvMapObject() factory, whitelists both env-map ClassInfos in SerializedScriptValue.cpp's ObjectStartState check, and updates the Windows windowsEnv Proxy's set/defineProperty traps to use template-literal coercion (so Symbol values throw) and to sync the new value to the OS env after a successful define. Two test files gain coverage; one todoOnPosix is un-todo'd.
Security risks
None identified. The change tightens behavior (rejects Symbol values, coerces to strings) rather than loosening it. The structured-clone whitelist addition is narrowly scoped to two internal ClassInfos.
Level of scrutiny
High. This is JSC bindings work: a new class with a custom method table, subspace sharing with plainObjectSpace, exception-scope discipline across three overrides, a change to the structured-clone serializer's object-acceptance predicate, and a platform split where POSIX and Windows now use different backing types for process.env. Earlier review passes on this PR found real regressions (structuredClone DataCloneError, exception-scope validator failure on the ASAN lane, a Windows defineProperty ordering bug), all of which were fixed — but that iteration history itself argues for a human sign-off on the final shape.
Other factors
- The Windows
definePropertytrap has an acknowledged edge case (non-stringvalue+ nowritable/configurableon an all-uppercase new key trips the ES Proxy invariant with an engineTypeError) that the author deliberately deferred to #34727's descriptor validation. That's a reasonable call — Node rejects the same input — but the coordination with #34727 / #31831 is a merge-order decision a maintainer should own. - The
Object.values(process.env).every(v => typeof v === "string")assertion depends on the pre-existing special-var getters (TZ,NODE_TLS_REJECT_UNAUTHORIZED,BUN_CONFIG_VERBOSE_FETCH) never being enumerable-and-undefined underbunEnv; that holds today because they'reDontEnumwhen unset, but it's not something this PR changed. - The latest push (372ec7f) was a rebase over 149 commits plus a behavior change (template-literal coercion in the Windows traps); the timeline does not yet show a green CI on that commit, only "passes locally on the debug build".
Node.js documents that every
process.envassignment coerces the value to a string. Bun stored the value verbatim on two of the three construction paths, so assigning a non-string read back as the raw value:Two user-visible consequences beyond the raw readback: after a BigInt assignment,
JSON.stringify(process.env)throws for the rest of the process (breaking error reporters and config dumpers); andprocess.env.X = Symbol()is silently accepted instead of throwing like Node.Cause
process.envis built in three places, only one of which coerced on assignment:createEnvironmentVariablesMap(main thread, default): a plainJSObjectpopulated withCustomValuegetters for lazy OS-env reads. Those getters have no setter, so writes went straight toJSObject::putand stored whateverJSValuearrived. On Windows the same object is wrapped in thewindowsEnvProxy whosesettrap calledString(value), which coerces everything but special-cases Symbol (returns"Symbol(s)"instead of throwing).ZigGlobalObjectinitializeWorkerwith{ env: {...} }: a plainconstructEmptyObjectassigned directly tom_processEnvObject, bypassingcreateEnvironmentVariablesMapon every platform.JSSharedEnvMap(SHARE_ENV): already coerced viatoWTFStringin itsputhook.Fix
JSProcessEnvMap, aJSNonFinalObjectsubclass whoseput/putByIndex/defineOwnPropertycallvalue.toString()(specToString: throws on Symbol) before delegating toBase. Symbol keys fall through unchanged, matchingJSSharedEnvMap::put.createEnvironmentVariablesMapuses it on POSIX. Windows keeps a plain object as the Proxy target, becausewindowsEnv()storestoJSONas an own function on that object directly and the Proxy's traps already coerce.set/definePropertytraps now use template-literal coercion instead ofString()so Symbol values throwTypeErrorthe same as POSIX and Node. ThedefinePropertytrap now also syncs the new value to the OS env after a successful define (it previously synced the old value orundefined, which tripped a debugASSERT).envpath atZigGlobalObject.cppbuilds its object via a newcreateProcessEnvMapObject()factory, so workers on every platform coerce.SerializedScriptValuewhitelists the new class sostructuredClone(process.env)keeps working.Verification
test/js/node/process/process.test.js: new "process.env coerces assigned values to strings" case covering number, boolean,null,undefined, BigInt, array, function, object withtoString,Object.assign, indexed key,Object.defineProperty, a throwingtoString, Symbol-throws on all three paths, theObject.values(process.env)all-string invariant, andJSON.stringify(process.env)after a BigInt write; plus "... in a worker with an explicit env". Both fail on main, pass with the fix.test/cli/run/env.test.ts: un-todos the pre-existing "setting process.env coerces the value to a string" test, which wastodoOnPosixfor exactly this reason.test/js/node/worker_threads/worker_threads.test.ts: all SHARE_ENV tests still pass.Related: #31831 is a broader process-compat omnibus that includes this among many other fixes; #34727 adds
defineOwnPropertydescriptor validation to the same class. Whichever lands first, the others rebase over it.no test proof · iteration 4 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/run/env.test.ts test/js/node/process/process.test.js