[PR #7270] [CLOSED] fix(javascript): read spawn env as Node reads it #7802

Closed
opened 2026-11-05 01:47:46 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/7270
Author: @XiaoBinGan
Created: 10/26/2026
Status: ❌ Closed

Base: main ← Head: fix/spawn-env-nullish


📝 Commits (1)

  • 78bbd9f fix(javascript): read spawn env as Node reads it

📊 Changes

2 files changed (+44 additions, -8 deletions)

View changed files

📝 src/domain/javascript/childProcessOptionsRegression.test.ts (+28 -0)
📝 src/domain/javascript/javascriptSemanticChildProcesses.ts (+16 -8)

📄 Description

Summary

environment_supplied is true whenever the options object has an env key, but Node replaces a falsy options.env with a copy of the parent environment. So spawn("tool", [], { env: null }) is reported as supplying an environment and gets a resolved supplies-env relation, when the child actually inherits everything. The same presence test also misses the opposite direction: { ...{ env: { A: "1" } } } reports false although Node uses that env.

Problem and expected behavior

Node's normalizeSpawnArguments begins:

const env = options.env || { ...process.env };

Any falsy env is therefore replaced by a copy of the parent environment. Smallest trigger:

spawn("tool", [], { env: null });

The child inherits the full parent environment — the opposite of the security-relevant claim "env supplied".

Measured on Node v24.22.1 with a child that echoes a parent marker variable (REA_MARK=inherited in the parent):

options child sees
no env {"m":"inherited"}
env: null {"m":"inherited"}
env: undefined {"m":"inherited"}
env: void 0 {"m":"inherited"}
env: 0 {"m":"inherited"}
env: "" {"m":"inherited"}
env: false {"m":"inherited"}
env: {} {} (empty env is honoured)
env: { A: "1" } {"A":"1"}

Before this change all nine with an env key reported environment_supplied: true, including the five falsy ones that behave exactly like no env at all. Separately, spawn("tool", [], { ...{ env: { A: "1" } } }) reported false, although that env is used.

Expected: environment_supplied reflects whether Node uses a caller-supplied environment. Actual: it reflected key presence only, in both directions.

Change and scope

environmentSupplied now reads the declared value the way stdioMode already reads stdio in the same file:

  • resolve env through staticObjectProperty, so an inline object spread carrying env counts;
  • return false when the resolved value is statically nullish or statically falsy;
  • otherwise keep the existing behaviour, so an unresolved value stays true (it may supply an environment) and a non-object options expression stays false.

The replaced helper (objectHasProperty) had no other callers and is removed; staticObjectProperty was already imported here.

Intentionally not included: modelling which variables the child removes or adds relative to a merged parent environment, and any change to argvCount, stdioMode or resolution.

Contract and boundary impact

  • Semantic owner and earliest changed stage: src/domain/javascript/javascriptSemanticChildProcesses.ts produces environmentSupplied; the graph child-process projection turns it into a supplies-env relation. Both directions of that relation now follow the value Node uses.
  • Contribution scope fit and linked maintainer decision: not applicable. Ordinary fix within existing boundaries; the same class as #7083 (fork's silent stdio), which corrected an option REA read without applying Node's rule.
  • CLI and MCP/tool-catalog contract: no schema or tool change. child-process nodes can now report environment_supplied: false where they previously said true, and true for an inline-spread env.
  • Provider, bridge, target-format, or platform compatibility: none.
  • Evidence, artifact, provenance, or reconstruction contract: no observed fact is removed; a resolved supplies-env relation is no longer asserted for an env Node discards.
  • Target selection, authority, upstream behavior, and cleanup details (if applicable): none. The fixtures are parsed, never executed.
  • Generated metadata (docs/public/product-catalog.json), package, or installation impact: none.

Expected effects (check known effects; use documentation-only on its own).
Unchecked items do not establish absence; leave uncertainties visible.

  • Read existing files or captured data.
  • Launch or attach to a process.
  • Inject instrumentation or run scripts/code.
  • Change process memory, registers, files, or configuration.
  • Contact network services.
  • Recover protected code or work around protections.
  • Documentation or usability only; no target interaction.
  • Not sure yet; investigation or maintainer guidance needed.

(The analyzed fixtures are parsed, never executed. The separate Node v24.22.1 probe that established the expected behavior spawned children inside the probe process only.)

Evidence and regression coverage

  • Tests added or updated: reads env as Node does, where a falsy value inherits the parent environment in src/domain/javascript/childProcessOptionsRegression.test.ts covers twelve cases — absent, null, undefined, void, 0, "", false, {}, {A:"1"}, inline spread, unresolved value, unresolved options.
  • Base reproduction or other evidence: with only the source change reverted and the new test applied, it fails at the second case — AssertionError: null: expected true to be false. With the change, all twelve match the Node table above.
  • User-visible CLI/MCP output (if applicable): analyzing a file with env: null, env: 0, env: {} and env: { A: "1" } reports environment_supplied true×4, false×0 before and true×2, false×2 after — inherits-null => false, inherits-zero => false, supplies-empty => true, supplies-key => true.
  • Remaining proof gaps: Node versions other than v24.22.1 were not probed; options.env || { ...process.env } is long-standing behavior, and the env option's documented contract ("Environment key-value pairs") does not describe the falsy fallback.

For boundary fixes: not applicable. No filesystem, process, or provider boundary is involved; this is a pure IR projection change covered by the domain tests above.

For evidence-bearing changes:

  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, source provenance, and failed attempts remain preserved.
  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

  • Node v24.22.1 probe spawning a child that echoes a parent marker variable — absent, null, undefined, void 0, 0, "" and false inherit; {} yields an empty env; {A:"1"} yields exactly that
  • node node_modules/vitest/vitest.mjs run src/domain/javascript/childProcessOptionsRegression.test.ts — 5 passed
  • node node_modules/vitest/vitest.mjs run src/domain/javascript/childProcessOptionsRegression.test.ts src/domain/javascript/javascriptSemanticAnalysis.native_bindings.test.ts src/domain/javascript/javascriptSemanticAnalysis.runtime_boundaries.test.ts tests/acceptance/applications/javascriptChildProcessStdioCli.test.ts — 35 passed, 0 failed
  • node node_modules/typescript/bin/tsc --noEmit -p . — exit 0
  • npx oxfmt --check src/domain/javascript/javascriptSemanticChildProcesses.ts src/domain/javascript/childProcessOptionsRegression.test.ts — clean
  • node scripts/rea.mjs analyze-javascript-application <fixture> --format json — environment_supplied true×2/false×2, previously true×4/false×0

Compatibility, safety, and release

  • Breaking changes or migration steps: none. No public schema, tool contract, or CLI flag changes. A child-process node's environment_supplied can change value, which is the correction.
  • Real Hopper/Ghidra, browser, or OS coverage: not applicable.
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review: none. Static analysis only; no target interaction. The change removes a false "env supplied" claim, which is the safer direction.

Author readiness

Human owner: @XiaoBinGan

  • I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work.
  • I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps.
  • I declare this PR ready for initial maintainer review.

Before merge

  • This PR addresses a concrete problem or an agreed enhancement.
  • Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/7270 **Author:** [@XiaoBinGan](https://github.com/XiaoBinGan) **Created:** 10/26/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `fix/spawn-env-nullish` --- ### 📝 Commits (1) - [`78bbd9f`](https://github.com/morluto/rea/commit/78bbd9f42205a68f09631ff9bd0361d2b17d781a) fix(javascript): read spawn env as Node reads it ### 📊 Changes **2 files changed** (+44 additions, -8 deletions) <details> <summary>View changed files</summary> 📝 `src/domain/javascript/childProcessOptionsRegression.test.ts` (+28 -0) 📝 `src/domain/javascript/javascriptSemanticChildProcesses.ts` (+16 -8) </details> ### 📄 Description ## Summary `environment_supplied` is true whenever the options object has an `env` key, but Node replaces a falsy `options.env` with a copy of the parent environment. So `spawn("tool", [], { env: null })` is reported as supplying an environment and gets a resolved `supplies-env` relation, when the child actually inherits everything. The same presence test also misses the opposite direction: `{ ...{ env: { A: "1" } } }` reports false although Node uses that env. ## Problem and expected behavior Node's `normalizeSpawnArguments` begins: ```js const env = options.env || { ...process.env }; ``` Any falsy `env` is therefore replaced by a copy of the parent environment. Smallest trigger: ```js spawn("tool", [], { env: null }); ``` The child inherits the full parent environment — the opposite of the security-relevant claim "env supplied". Measured on Node v24.22.1 with a child that echoes a parent marker variable (`REA_MARK=inherited` in the parent): | options | child sees | | --- | --- | | no `env` | `{"m":"inherited"}` | | `env: null` | `{"m":"inherited"}` | | `env: undefined` | `{"m":"inherited"}` | | `env: void 0` | `{"m":"inherited"}` | | `env: 0` | `{"m":"inherited"}` | | `env: ""` | `{"m":"inherited"}` | | `env: false` | `{"m":"inherited"}` | | `env: {}` | `{}` (empty env is honoured) | | `env: { A: "1" }` | `{"A":"1"}` | Before this change all nine with an `env` key reported `environment_supplied: true`, including the five falsy ones that behave exactly like no `env` at all. Separately, `spawn("tool", [], { ...{ env: { A: "1" } } })` reported `false`, although that env is used. Expected: `environment_supplied` reflects whether Node uses a caller-supplied environment. Actual: it reflected key presence only, in both directions. ## Change and scope `environmentSupplied` now reads the declared value the way `stdioMode` already reads `stdio` in the same file: - resolve `env` through `staticObjectProperty`, so an inline object spread carrying `env` counts; - return false when the resolved value is statically nullish or statically falsy; - otherwise keep the existing behaviour, so an unresolved value stays `true` (it may supply an environment) and a non-object options expression stays `false`. The replaced helper (`objectHasProperty`) had no other callers and is removed; `staticObjectProperty` was already imported here. Intentionally not included: modelling which variables the child removes or adds relative to a merged parent environment, and any change to `argvCount`, `stdioMode` or `resolution`. ## Contract and boundary impact - Semantic owner and earliest changed stage: `src/domain/javascript/javascriptSemanticChildProcesses.ts` produces `environmentSupplied`; the graph child-process projection turns it into a `supplies-env` relation. Both directions of that relation now follow the value Node uses. - Contribution scope fit and linked maintainer decision: not applicable. Ordinary fix within existing boundaries; the same class as #7083 (fork's `silent` stdio), which corrected an option REA read without applying Node's rule. - CLI and MCP/tool-catalog contract: no schema or tool change. `child-process` nodes can now report `environment_supplied: false` where they previously said `true`, and `true` for an inline-spread env. - Provider, bridge, target-format, or platform compatibility: none. - Evidence, artifact, provenance, or reconstruction contract: no observed fact is removed; a resolved `supplies-env` relation is no longer asserted for an env Node discards. - Target selection, authority, upstream behavior, and cleanup details (if applicable): none. The fixtures are parsed, never executed. - Generated metadata (`docs/public/product-catalog.json`), package, or installation impact: none. Expected effects (check known effects; use documentation-only on its own). Unchecked items do not establish absence; leave uncertainties visible. - [x] Read existing files or captured data. - [ ] Launch or attach to a process. - [ ] Inject instrumentation or run scripts/code. - [ ] Change process memory, registers, files, or configuration. - [ ] Contact network services. - [ ] Recover protected code or work around protections. - [ ] Documentation or usability only; no target interaction. - [ ] Not sure yet; investigation or maintainer guidance needed. (The analyzed fixtures are parsed, never executed. The separate Node v24.22.1 probe that established the expected behavior spawned children inside the probe process only.) ## Evidence and regression coverage - Tests added or updated: `reads env as Node does, where a falsy value inherits the parent environment` in `src/domain/javascript/childProcessOptionsRegression.test.ts` covers twelve cases — absent, null, undefined, void, 0, "", false, `{}`, `{A:"1"}`, inline spread, unresolved value, unresolved options. - Base reproduction or other evidence: with only the source change reverted and the new test applied, it fails at the second case — `AssertionError: null: expected true to be false`. With the change, all twelve match the Node table above. - User-visible CLI/MCP output (if applicable): analyzing a file with `env: null`, `env: 0`, `env: {}` and `env: { A: "1" }` reports `environment_supplied` **true×4, false×0** before and **true×2, false×2** after — `inherits-null => false`, `inherits-zero => false`, `supplies-empty => true`, `supplies-key => true`. - Remaining proof gaps: Node versions other than v24.22.1 were not probed; `options.env || { ...process.env }` is long-standing behavior, and the `env` option's documented contract ("Environment key-value pairs") does not describe the falsy fallback. For boundary fixes: not applicable. No filesystem, process, or provider boundary is involved; this is a pure IR projection change covered by the domain tests above. For evidence-bearing changes: - [x] Observed, derived, and inferred claims remain distinguishable. - [x] Artifact identity, source provenance, and failed attempts remain preserved. - [x] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible. ## Validation performed - Node v24.22.1 probe spawning a child that echoes a parent marker variable — absent, null, undefined, void 0, 0, "" and false inherit; `{}` yields an empty env; `{A:"1"}` yields exactly that - `node node_modules/vitest/vitest.mjs run src/domain/javascript/childProcessOptionsRegression.test.ts` — 5 passed - `node node_modules/vitest/vitest.mjs run src/domain/javascript/childProcessOptionsRegression.test.ts src/domain/javascript/javascriptSemanticAnalysis.native_bindings.test.ts src/domain/javascript/javascriptSemanticAnalysis.runtime_boundaries.test.ts tests/acceptance/applications/javascriptChildProcessStdioCli.test.ts` — 35 passed, 0 failed - `node node_modules/typescript/bin/tsc --noEmit -p .` — exit 0 - `npx oxfmt --check src/domain/javascript/javascriptSemanticChildProcesses.ts src/domain/javascript/childProcessOptionsRegression.test.ts` — clean - `node scripts/rea.mjs analyze-javascript-application <fixture> --format json` — `environment_supplied` true×2/false×2, previously true×4/false×0 ## Compatibility, safety, and release - Breaking changes or migration steps: none. No public schema, tool contract, or CLI flag changes. A child-process node's `environment_supplied` can change value, which is the correction. - Real Hopper/Ghidra, browser, or OS coverage: not applicable. - Package or release metadata impact: none. - Security, privacy, process, or containment review: none. Static analysis only; no target interaction. The change removes a false "env supplied" claim, which is the safer direction. ## Author readiness Human owner: @XiaoBinGan - [x] I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work. - [x] I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps. - [x] I declare this PR ready for initial maintainer review. ## Before merge - [x] This PR addresses a concrete problem or an agreed enhancement. - [x] Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff. - [x] The PR has one focused outcome and the title follows `type(scope): outcome`. - [x] Related issue is linked, or the reason for not linking one is stated above. - [x] Tests cover changed observable behavior and meaningful failure paths. - [x] Owning docs, contracts, and generated metadata are updated where needed. - [x] User-visible CLI/MCP changes include representative output. - [x] I checked the final diff for secrets, unrelated cleanup, and unsupported claims. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-11-05 01:47:46 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#7802
No description provided.