Skip to content

fix(runtime): make Object.assign(process.env, …) actually set environment variables#6404

Merged
proggeramlug merged 3 commits into
mainfrom
fix/process-env-object-assign
Jul 14, 2026
Merged

fix(runtime): make Object.assign(process.env, …) actually set environment variables#6404
proggeramlug merged 3 commits into
mainfrom
fix/process-env-object-assign

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

The bug

process.env is not an ordinary object — reads resolve through the runtime's env lookup — but Object.assign treated it as one. The merged keys landed in a plain object that nothing ever consulted, so every subsequent read came back undefined:

Object.assign(process.env, { DATABASE_URL: "mysql://…" });
process.env.DATABASE_URL;   // node: "mysql://…"   perry: undefined

Why it matters

This is exactly how @next/env installs a parsed .env file (Object.assign(process.env, parsed)), and how dotenv and most config loaders do it. Under Perry a Next.js app therefore ran with no environment at all — no DATABASE_URL, no API keys — and the failure was silent: every variable simply read as undefined, far from the assignment that was supposed to set it.

The fix

js_object_assign_one now recognizes a process.env target and routes each source key through js_setenv, so the values land where reads look for them. A plain-object target is untouched. js_setenv additionally writes the key into the cached env object, so a read that hits the cache sees it too.

Test

test_gap_object_assign_process_env — direct / bracket / dynamic-key reads, the in operator, overwriting an existing key, multi-source assign, plain process.env.X = … assignment, and an ordinary-object target (which must be unaffected). Byte-identical to node.

Found while compiling a real Next.js + MySQL app.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed Object.assign(process.env, values) so new keys are written correctly and become immediately readable.
    • Improved consistency across member access, bracket access, dynamic lookups, and the in operator for assigned .env-style keys.
    • Kept standard Object.assign behavior unchanged for non-process.env targets (including returning the original target).
    • Ensured newly set environment keys appear during enumeration/iteration over process.env.
    • Added robustness for nullish/primitive sources and “odd” environment key names.

…ment variables

`process.env` is not an ordinary object — reads resolve through the runtime's env
lookup — but `Object.assign` treated it as one. The merged keys landed in a plain
object that nothing ever consulted, so every subsequent `process.env.X` read came
back `undefined`:

    Object.assign(process.env, { DATABASE_URL: "mysql://…" });
    process.env.DATABASE_URL;   // node: "mysql://…"   perry: undefined

`js_object_assign_one` now recognizes a `process.env` target and routes each
source key through `js_setenv`, so the values land where reads look for them. A
plain-object target is untouched. `js_setenv` additionally writes the key into
the cached env object, so a read that hits the cache sees it too.

This is how `@next/env` installs a parsed `.env` file (`Object.assign(process.env,
parsed)`), and how dotenv and most config loaders do it — so under Perry a Next.js
app silently ran with no environment at all: no `DATABASE_URL`, no API keys.

Found while compiling a real Next.js + MySQL app. Covered by
`test_gap_object_assign_process_env` — direct/bracket/dynamic-key reads, `in`,
overwrite, multi-source assign, plain assignment, and an ordinary-object target.
Byte-identical to node.
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: efc51c2c-d3d6-4dcf-8012-ba9ddd4cde08

📥 Commits

Reviewing files that changed from the base of the PR and between 265278a and 264d9cd.

📒 Files selected for processing (4)
  • crates/perry-runtime/src/object/alloc.rs
  • crates/perry-runtime/src/process.rs
  • crates/perry-runtime/src/process/env_misc.rs
  • test-files/test_gap_object_assign_process_env.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/perry-runtime/src/process.rs
  • crates/perry-runtime/src/process/env_misc.rs

📝 Walkthrough

Walkthrough

Object.assign(process.env, source) now routes each string-key write through js_setenv, which updates both the OS environment and cached process.env object. Runtime identity helpers are exported, and tests cover assignment and edge-case behavior.

Changes

process.env assignment

Layer / File(s) Summary
Environment cache synchronization
crates/perry-runtime/src/process/env_misc.rs, crates/perry-runtime/src/process.rs
js_setenv validates keys, updates the OS environment and cached process.env, and exports identity helpers through the process compatibility surface.
Object.assign process.env path
crates/perry-runtime/src/object/alloc.rs
The string-key assignment funnel detects process.env targets and forwards each key/value to js_setenv.
Assignment behavior coverage
test-files/test_gap_object_assign_process_env.ts
Tests lookup, overwrite, multi-source, plain-object, nullish-source, primitive-source, and malformed-key behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: andrewtdiz

Sequence Diagram(s)

sequenceDiagram
  participant Source
  participant ObjectAssign
  participant js_setenv
  participant CachedProcessEnv
  Source->>ObjectAssign: Provide enumerable keys and values
  ObjectAssign->>js_setenv: Set each process.env key/value
  js_setenv->>CachedProcessEnv: Update cached property
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main runtime fix to Object.assign(process.env).
Description check ✅ Passed It covers the bug, fix, and test coverage, though it doesn't follow the template headings exactly.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/process-env-object-assign

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 `@crates/perry-runtime/src/object/alloc.rs`:
- Around line 1115-1142: Remove the early process.env fast-path block from
Object.assign and integrate the target check into object_assign_set_string_key.
When the target is the process.env object, route each property write through
crate::process::js_setenv; otherwise retain the existing property-assignment
behavior. Leave the existing source-decoding flow responsible for primitives,
arrays, proxies, and nullish sources.

In `@crates/perry-runtime/src/process/env_misc.rs`:
- Around line 886-898: Validate the environment variable name in the js_setenv
path before calling std::env::set_var, rejecting empty names and names
containing '=' or '\0'. For invalid keys, either silently ignore the assignment
or raise a JavaScript exception, but never invoke std::env::set_var; preserve
the existing cached process.env update only for valid names.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9f08a426-a172-4764-9d29-4bf6ec433c19

📥 Commits

Reviewing files that changed from the base of the PR and between baefb2a and 265278a.

📒 Files selected for processing (4)
  • crates/perry-runtime/src/object/alloc.rs
  • crates/perry-runtime/src/process.rs
  • crates/perry-runtime/src/process/env_misc.rs
  • test-files/test_gap_object_assign_process_env.ts

Comment thread crates/perry-runtime/src/object/alloc.rs Outdated
Comment thread crates/perry-runtime/src/process/env_misc.rs
Ralph Küpper and others added 2 commits July 14, 2026 19:29
…he entry

The env fast path sat at the top of js_object_assign_one and re-implemented
source decoding. That got three things wrong, all reachable from the very call
this PR exists to support -- Object.assign(process.env, parsed):

  * It cast ANY source pointer to `*const ObjectHeader`. A string or array
    source (`Object.assign(process.env, "abc")`) was then read through the
    wrong layout -- type confusion.
  * It enumerated the source with `js_object_keys_value`, which THROWS
    ToObject's TypeError on null/undefined. The spec says a nullish source is
    silently skipped, and node does; perry threw.
  * It fed arbitrary object keys to `js_setenv` -> `std::env::set_var`, which
    PANICS on a name that is empty or contains '=' or NUL. From an extern "C"
    frame that panic cannot unwind, so it ABORTED the process (SIGABRT). One
    malformed line in a .env file would have taken a Next.js server down --
    exactly the workload this PR targets.

Move the check to `object_assign_set_string_key`, the single write funnel all
four call sites already go through. Every source shape now flows through the
existing decoding (primitives, arrays, proxies, nullish), and only the write is
redirected. Also validate the name in `js_setenv` and skip an unsettable one
rather than abort -- node accepts such an assignment silently.

Verified against node: the .env round-trip this PR fixes still works, and
nullish / string / empty-key / '='-key sources are now byte-identical instead of
throwing or aborting. Extended the gap test to cover all four.
@proggeramlug

Copy link
Copy Markdown
Contributor Author

CodeRabbit was right on both counts, and the impact is worse than reported. Fixed in 264d9cd3d.

I built the branch and ran the edge cases against node:

case node this PR (before fix)
Object.assign(process.env, {"": "x"}) ok SIGABRT (134)
Object.assign(process.env, {"A=B": "x"}) ok SIGABRT (134)
Object.assign(process.env, null) ok (skipped) TypeError thrown
Object.assign(process.env, "abc") ok ok, but type-confused

The bad-key case is not a catchable error — std::env::set_var panics on a name that is empty or contains =/NUL, and from an extern "C" frame that panic cannot unwind, so it aborts the process:

panicked at library/std/src/env.rs:361:
failed to set environment variable "" to "x": Invalid argument (os error 22)
thread caused non-unwinding panic. aborting.

That is reachable from the exact workload this PR exists to support: @next/env doing Object.assign(process.env, parsed). One malformed line in a .env file would have taken the server down.

The fix takes CodeRabbit's suggested architecture. The root problem was placing the env check as an early exit in js_object_assign_one, which forced it to re-implement source decoding — and that re-implementation cast any source pointer to *const ObjectHeader and enumerated via js_object_keys_value, which throws on nullish.

Moved the check to object_assign_set_string_key, the single write funnel all four call sites already go through. Every source shape now flows through the existing decoding (primitives, arrays, proxies, nullish) and only the write is redirected. Also validated the name in js_setenv and skip an unsettable one rather than abort, matching node.

Verified: the .env round-trip this PR fixes still works (DATABASE_URL reads back), and all four cases above are now byte-identical to node. Extended test_gap_object_assign_process_env.ts to cover them, and confirmed those assertions genuinely fail on the pre-fix build.

Skipping the third nitpick (thread-local FD_REGISTRY consuming 3 fds/thread) — that's an operational characteristic, and the if duped >= 0 path already degrades gracefully rather than panicking.

@proggeramlug proggeramlug merged commit fef1041 into main Jul 14, 2026
26 checks passed
@proggeramlug proggeramlug deleted the fix/process-env-object-assign branch July 14, 2026 18:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant