Skip to content

Honor live $HOME mutations in os.homedir() - #29248

Open
robobun wants to merge 7 commits into
mainfrom
farm/eb38f683/fix-os-homedir-stale-env
Open

Honor live $HOME mutations in os.homedir()#29248
robobun wants to merge 7 commits into
mainfrom
farm/eb38f683/fix-os-homedir-stale-env

Conversation

@robobun

@robobun robobun commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator

Fixes #29244

Repro

const os = require('node:os');
console.log('Before:', os.homedir());
process.env.HOME = '/tmp/test-home';
console.log('After:', os.homedir());

Node:

Before: /Users/racerx
After:  /tmp/test-home

Bun (before this change):

Before: /Users/racerx
After:  /Users/racerx

A stronger form — mutate HOME before require('node:os') — also returned the stale value.

Cause

src/bun.js/node/node_os.zig's homedir() read HOME via bun.env_var.HOME.get(), which is the type-safe cached env-var accessor from src/env_var.zig. Once HOME is read, its value is atomically cached and subsequent .get() calls return the cached value — they never re-query the process environment. So runtime mutations of process.env.HOME were invisible to os.homedir().

os.tmpdir() doesn't have this bug because it reads Bun.env["TMPDIR"] live in src/js/node/os.ts on every call.

Fix

Mirror the tmpdir pattern for homedir. The HOME env check is now done in src/js/node/os.ts via Bun.env["HOME"], which reads process.env live on every call. The Zig binding is now the passwd-fallback path only (what libuv's uv_os_homedir does when HOME is unset/empty).

os.userInfo().homedir is unchanged: it still calls the Zig binding directly, which skips the env check and reads the passwd entry — matching Node's behavior of ignoring $HOME there.

On Windows, libuv's uv_os_homedir already reads USERPROFILE live on every call via syscall (not through Bun's cache), so the binding is called directly on Windows.

Verification

test/regression/issue/29244.test.ts runs each case in a subprocess (so mutating process.env.HOME doesn't affect the test runner):

  • os.homedir() reflects HOME mutated after requirefail-before, pass-after
  • os.homedir() reflects HOME mutated before requirefail-before, pass-after
  • os.homedir() honors HOME inherited from parent env
  • os.homedir() falls back to passwd when HOME is empty
  • os.userInfo().homedir still ignores HOME mutations

Existing test/js/node/os/os.test.js homedir tests (including Symbol.toPrimitive / template-literal coercion) continue to pass.

Related: #29237 / #29239 — similar stale-env-snapshot bug in child_process sync variants.


no test proof · iteration 16 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/os/os-homedir-env.test.js

@robobun

robobun commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:05 PM PT - Aug 12th, 2026

@robobun, your commit 578bd78 has 2 failures in Build #93658 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 29248

That installs a local version of the PR into your bun-29248 executable, so you can run:

bun-29248 --bun

@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bcbce9bd-07d9-4872-a79d-4bfccfe937b5

📥 Commits

Reviewing files that changed from the base of the PR and between 44f6469 and 578bd78.

📒 Files selected for processing (3)
  • src/js/node/os.ts
  • src/runtime/node/node_os.rs
  • test/js/node/os/os-homedir-env.test.js

Walkthrough

POSIX os.homedir() now reflects live changes to process.env.HOME via a JS wrapper; the Zig binding no longer uses a Posix HOME fast-path and always falls back to passwd lookup. Added hardened memory handling in Zig and subprocess-based regression tests validating runtime, inherited, empty, and userInfo() behaviors.

Changes

Cohort / File(s) Summary
Zig implementation
src/bun.js/node/node_os.zig
Removed the POSIX HOME fast-path, added block comments clarifying JS vs Zig responsibilities, and hardened buffer/free logic in the passwd (getpwuid_r) fallback to prevent double-free/dangling-pointer on ERANGE/alloc failure.
JavaScript API wrapper
src/js/node/os.ts
Changed exported os.homedir from binding.homedir to homedirFactory(binding.homedir): on non-Windows it checks Bun.env["HOME"] on each call and returns it if non-empty, otherwise calls the binding; Windows continues to call the binding directly.
Regression tests
test/regression/issue/29244.test.ts
Added subprocess-based tests (skipped on Windows) covering: runtime process.env.HOME mutations after require, mutations before require, inherited HOME via child env, empty-string HOME fallback behavior, and that os.userInfo().homedir does not reflect HOME mutations.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the primary change: making os.homedir() honor live HOME mutations.
Description check ✅ Passed The description explains the cause, fix, behavior differences, and verification tests, although it does not use the template headings exactly.

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

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — clean fix that mirrors the existing pattern; the two flagged issues are minor test-style nits.

Extended reasoning...

Overview

This PR fixes a Node.js compatibility bug where returned a stale value after was mutated at runtime. Three files are touched: the Zig binding (), the JS wrapper (), and a new regression test file.

Security Risks

None. The change only affects how the HOME environment variable is read — moving from Bun's cached env-var accessor to a live lookup on every call, exactly matching what already does. There is no user-controlled input, no new attack surface, and no privilege boundary crossing.

Level of Scrutiny

Low. This is a small, targeted bug fix with a clear root cause (cached env-var snapshot vs. live read) and a well-understood fix pattern already used by . The code path is deliberately left unchanged, preserving Node's behavior of ignoring $HOME there. The regression tests cover the exact failure modes described in the issue.

Other Factors

The two bug reports are both nits about test conventions (missing and assertion ordering), not correctness issues. The implementation is correct and the tests do verify the fix. No CODEOWNERS appear to own these files based on my review.

Comment thread test/regression/issue/29244.test.ts Outdated
Comment thread test/regression/issue/29244.test.ts Outdated
Comment thread src/runtime/node/node_os.zig Outdated
Comment thread src/runtime/node/node_os.zig Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/29244.test.ts`:
- Around line 52-53: Remove brittle empty-stderr assertions and rely on the
subprocess exit code: delete the expect(stderr).toBe("") checks in this test
(and the other occurrences noted) and keep/assert only expect(exitCode).toBe(0);
if stderr content is the actual behavior under test then replace the
empty-string assertion with a targeted assertion for that expected output,
otherwise do not assert on the stderr variable. Use the stderr and exitCode
variable names from the test to locate and update the assertions.
🪄 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: b998db56-47a1-411d-8221-0fefaded7bad

📥 Commits

Reviewing files that changed from the base of the PR and between c539fa2 and d7564f8.

📒 Files selected for processing (1)
  • test/regression/issue/29244.test.ts

Comment thread test/regression/issue/29244.test.ts Outdated
@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

No merge conflicts detected when merging into main.

Your branch is good to go!

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — all substantive bugs addressed, the nit is cosmetically redundant but behaviorally harmless.

Extended reasoning...

Overview

The PR moves the POSIX $HOME env check from Bun's cached Zig accessor into the JS layer (src/js/node/os.ts), mirroring the existing tmpdir pattern. The Zig binding becomes the passwd-fallback path only, and userInfo().homedir continues calling the binding directly to match Node's behavior.

Changes reviewed

Three files changed: node_os.zig (remove cached HOME fast-path, fix ERANGE stack-free UB), os.ts (add homedirFactory), and a new regression test file.

Security risks

None. The change reads an environment variable that is entirely user-controlled; no privilege escalation, injection, or data exposure vectors exist.

Prior feedback resolution

All four prior issues I raised were resolved or declined with good justification:

  • test.concurrent added (d7564f8)
  • stdout-before-exitCode assertion order fixed (d7564f8)
  • ERANGE guard against freeing stack buffer added (7158e41)
  • pw.pw_dir null behavior: author verified libuv unconditionally strlens pw_dir with no null check, so Bun is actually more defensive — decline accepted.
  • Empty stderr assertions dropped (55c115a)

Remaining nit

The new finding (home.length > 0 dead code on line 44 of os.ts) is a pure cosmetic redundancy with zero behavioral impact; it does not warrant blocking the PR.

Comment thread src/js/node/os.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — all prior feedback addressed; the fix correctly mirrors the tmpdir pattern and the Zig ERANGE guard is properly hardened.

Extended reasoning...

Overview

This PR fixes a bug where returned a stale value after was mutated, by moving the HOME env check from Zig's cached env-var accessor to a live Bun.env["HOME"] read in the JS layer (). The Zig binding becomes the pure passwd-fallback path. Three files are touched: , , and a new regression test.

Security risks

None. There is no auth, crypto, or permissions code involved. The change reads an environment variable that the process already owns.

Level of scrutiny

Moderate. The fix is a well-scoped behavioral correctness fix matching Node's libuv behavior. The ERANGE double-free pre-existing bug was found and fixed in this PR (7158e41), the redundant length check was simplified (35e252a), test ordering and concurrency were corrected (d7564f8, 55c115a). The remaining pre-existing issue (geteuid vs getuid in userInfo for setuid processes) is out of scope for this PR and does not block it.

Other factors

All inline comments from prior review runs have been addressed by the author. The regression tests are well-isolated (subprocess-per-test, concurrent, no shared state). The Windows path correctly delegates to libuv which already reads USERPROFILE live.

Comment thread src/runtime/node/node_os.zig Outdated
@robobun

robobun commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator Author

Note on CI status: the failing lane is debian-13-x64-asan-test-bun (exit 2). Full log audit shows all 195 individual tests in the ASAN shard passed; the harness exits 2 after --- End with no test-level failure reported. Also happens on build #45447 (unrelated PR autofix commit), confirming a harness-level flake on this lane, not caused by this PR.

Comment thread src/js/node/os.ts Outdated
Comment thread src/js/node/os.ts Outdated
Comment thread test/regression/issue/29244.test.ts Outdated
Comment thread src/runtime/node/node_os.zig Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All prior feedback has been addressed and I didn't find anything new, but this changes runtime semantics for os.homedir() and os.userInfo().homedir (now always passwd-backed) plus the geteuid→getuid swap and ERANGE allocator path in Zig — worth a human sanity check before merge.

Extended reasoning...

Overview

This PR makes os.homedir() honor live mutations of process.env.HOME by moving the env check from the Zig binding (which used Bun's snapshot-on-first-read bun.env_var.HOME accessor) into a JS wrapper in src/js/node/os.ts that reads Bun.env["HOME"] on every call. Three files change: src/js/node/os.ts (new homedirFactory wrapper), src/runtime/node/node_os.zig (removed HOME fast-path, fixed an ERANGE free-of-stack-buffer bug, switched geteuid()getuid() for the passwd lookup), and a new 6-test regression file.

Security risks

None identified. Reading $HOME from the live process environment is exactly what Node/libuv does and introduces no new attack surface. The geteuid()getuid() change aligns with libuv and is, if anything, the safer choice in setuid contexts (homedir now matches the real user, consistent with the uid field userInfo() already reports).

Level of scrutiny

Moderate-to-high. While each individual change is small and well-reasoned, the combined effect touches a hot, widely-depended-on Node-compat API and bundles several distinct behavior changes into one PR:

  • os.homedir() now reflects runtime $HOME mutations (the headline fix).
  • os.homedir() with HOME="" now returns "" instead of the passwd entry (new Node-parity, but a change from prior Bun behavior).
  • os.userInfo().homedir now always returns the passwd entry — previously the Zig binding short-circuited on cached $HOME, so this is a real semantics shift even though it's the Node-correct one.
  • The getpwuid_r retry loop's allocator handling was rewritten (guarded free + reset-to-stack between free and alloc).
  • geteuid()getuid() in the passwd lookup.

Other factors

The PR has been through ~8 rounds of bot review; every raised point was either fixed (test concurrency, assertion ordering, stderr brittleness, redundant length check, ERANGE stack-free UB, geteuid/getuid mismatch, stale doc comments, non-load-bearing delete-HOME test, HOME="" libuv parity) or reasonably declined/deferred (the pre-existing userInfo() username/shell env-sourcing divergence, punted to a follow-up). Test coverage is solid and the Symbol.toPrimitive wiring is preserved via symbolToStringify(out, "homedir"). CI failures noted in the thread (musl build, Windows http timeout) appear unrelated/flaky per the author's log audit. I'm deferring rather than approving because the cumulative behavior surface — especially the userInfo().homedir change and the Zig allocator path — is the kind of thing a maintainer should glance at, not because anything looks wrong.

@robobun

robobun commented May 4, 2026

Copy link
Copy Markdown
Collaborator Author

CI build #51218 failures are pre-existing Windows flakes, not caused by this PR:

  • Primary: test-http-should-emit-close-when-connection-is-aborted.ts timed out 4× on all 3 Windows lanes. Same test is ERRORing on unrelated PR #51216 right now → confirmed pre-existing flake.
  • Widespread CI infra issues visible in neighboring builds (#51215, #51219, #51220 all have "expired agent" / "retrigger CI" messages).

My PR touches only src/js/node/os.ts (os.homedir JS wrapper), src/runtime/node/node_os.zig (os.homedir Zig binding), and a Windows-skipped regression test — none of which can affect HTTP close-on-abort behavior.

Comment thread test/regression/issue/29244.test.ts Outdated
Comment thread src/runtime/node/node_os.zig Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All prior feedback has been addressed and I didn't find any remaining issues, but this changes os.userInfo().homedir semantics (cached $HOME → always passwd), swaps geteuidgetuid in the passwd lookup, and reworks the Zig ERANGE allocator handling — worth a human sanity-check before merge.

Extended reasoning...

Overview

This PR fixes #29244 by making os.homedir() honor live process.env.HOME mutations, matching Node. It touches three files:

  • src/js/node/os.ts: new homedirFactory wrapper that reads Bun.env["HOME"] live on every POSIX call (!== undefined check, so HOME=""""), falling through to the Zig binding only when HOME is absent. Windows delegates straight to the binding.
  • src/runtime/node/node_os.zig: removes the cached bun.env_var.HOME.get() fast-path so the binding is now pure passwd-fallback on POSIX; changes geteuid()getuid() in getpwuid_r; hardens the ERANGE retry loop (guards the free against the stack buffer, resets to stack buffer between free and alloc so the defer can't double-free on alloc failure); adds extensive doc comments including a flagged Windows userInfo() follow-up.
  • test/js/node/os/os.test.js: 6 new concurrent subprocess tests covering mutation before/after require, inherited HOME, HOME="", deleted HOME → passwd, and userInfo().homedir ignoring HOME.

Security risks

None introduced. Reading $HOME from live process.env is standard Node behavior and the same pattern tmpdir() already uses. The geteuidgetuid change is arguably a hardening: in a setuid process, userInfo() now reports a self-consistent {uid, homedir} pair sourced from the real UID, matching libuv.

Level of scrutiny

Medium-high. While the JS wrapper is straightforward and mirrors the existing tmpdir pattern, the PR also makes three non-trivial runtime changes that go beyond the headline fix:

  1. os.userInfo().homedir behavior change — previously short-circuited via cached $HOME when set; now always reads the passwd entry. Correct (matches Node's uv_os_get_passwd), but user-observable.
  2. geteuidgetuid — correct per libuv, but changes which user's passwd entry is consulted in setuid contexts.
  3. Zig allocator rework in the ERANGE loop — fixes a real pre-existing free-of-stack-memory bug, but is memory-safety-critical code.

These are all improvements and look correct to me, but collectively they exceed the "simple/mechanical" bar for bot-only approval.

Other factors

This PR has been through ~8 rounds of bot review; every inline comment is resolved with a corresponding fixup commit, and the author's responses (including the declined libuv-ENOENT claim and the deferred userInfo username/shell follow-up) were well-reasoned. Test coverage is solid: 6 subprocess tests with fail-before/pass-after verification, load-bearing sentinel checks, and the existing Symbol.toPrimitive coverage for homedir still applies via symbolToStringify. The bug-hunting system found nothing in the current revision. CI failures noted in the thread were diagnosed as unrelated Windows/ASAN flakes.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found, but this touches native Zig allocator handling (the ERANGE retry-loop hardening) plus a few subtle behavioral shifts — userInfo().homedir now always hits getpwuid_r on POSIX, geteuidgetuid, and HOME="" semantics — so it's worth a human pass before merge.

Extended reasoning...

Overview

Three files: src/js/node/os.ts adds a homedirFactory wrapper that reads Bun.env["HOME"] live on every call (mirroring the existing tmpdir pattern) and falls back to the binding; src/runtime/node/node_os.zig removes the cached bun.env_var.HOME.get() fast-path, hardens the getpwuid_r ERANGE retry loop (guards the free against the on-stack buffer and resets to the stack buffer between free and alloc so a failed alloc can't leave a dangling pointer for the defer), and switches the passwd lookup from geteuid() to getuid(); test/js/node/os/os.test.js gains 6 subprocess-isolated concurrent tests covering mutate-after-require, mutate-before-require, inherited HOME, HOME="", deleted HOME → passwd, and userInfo().homedir ignoring HOME.

Security risks

None identified. No injection, auth, or crypto surfaces. The geteuidgetuid change is security-adjacent (setuid processes) but moves Bun toward libuv's behavior and toward internal consistency with the uid field userInfo() already reports, so it's a correctness fix rather than a new exposure.

Level of scrutiny

Medium-high. The JS wrapper is trivial and pattern-matched against tmpdir, but the Zig changes are not mechanical: (1) the ERANGE buffer-free hardening rewrites allocator handling in a retry loop — the logic looks correct and is well-commented, but it's exactly the kind of native memory-management change that benefits from a second pair of eyes; (2) removing the HOME fast-path means userInfo().homedir on POSIX now always issues a getpwuid_r syscall instead of returning the cached env value — more Node-correct, but a real behavioral change; (3) HOME="" now returns "" instead of falling through to passwd, flipping prior Bun behavior to match Node.

Other factors

The PR has been through roughly ten rounds of bot review and every thread is resolved; test coverage for the new behavior is thorough and subprocess-isolated; CI failures noted in the thread are documented Windows flakes unrelated to these files; no CODEOWNERS apply. The remaining userInfo() divergences (POSIX username/shell from env, Windows homedir via uv_os_homedir) are explicitly deferred to a follow-up and called out in the doc comments. Nothing blocks merge from my side — deferring purely because native allocator changes plus three behavioral shifts in one PR exceed the "simple/mechanical" bar for auto-approval.

@robobun

robobun commented May 4, 2026

Copy link
Copy Markdown
Collaborator Author

CI update: the Windows x64-baseline test-bun shard pg=7/8 has reliably timed out on test-http-should-emit-close-when-connection-is-aborted.ts for 4 consecutive builds (#51218, #51459, #51475 on this PR, plus currently failing on unrelated PRs #51473 and similar). Last commit here was comment-only (afff210) / empty (210f2f0), so nothing in this PR can be causing the timeout. The test appears broken repo-wide on that Windows lane; will need a maintainer to skip/fix it or force-merge past it.

@robobun
robobun force-pushed the farm/eb38f683/fix-os-homedir-stale-env branch from 210f2f0 to 00c44b9 Compare June 26, 2026 05:14
@robobun

robobun commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main to resolve the merge conflict. The conflict was non-trivial: main's Zig→Rust migration deleted src/bun.js/node/node_os.zig (later src/runtime/node/node_os.zig) and replaced it with src/runtime/node/node_os.rs. I re-applied the fix to the Rust port:

  • src/runtime/node/node_os.rs: removed the cached env_var::HOME.get() fast-path (the HOME check now lives in os.ts), and switched the passwd lookup from geteuid() to getuid() to match libuv and keep homedir/uid consistent under setuid.
  • The earlier Zig ERANGE allocator fix is not needed in Rust: the port already uses a Vec<u8> for the heap buffer (owned, auto-freed), so there's no free-of-stack-pointer hazard.
  • src/js/node/os.ts: unchanged logic (live Bun.env["HOME"] read), kept main's $rust("node_os.rs", ...) binding.
  • test/js/node/os/os.test.js: the 6 tests apply unchanged.

Gate re-verified on the rebased tree: 4/6 tests fail without the fix, all 6 pass with it. History collapsed to a single commit since the original Zig-targeting commits no longer apply to the ported file.

@robobun

robobun commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

Post-rebase CI (build #64856): the diff is green. 45 lanes passed; the only failure is darwin-26-aarch64-test-bun, which failed on a buildkite infra error, not a test:

Error: buildkite-agent artifact download timed out after 120s for step
'darwin-aarch64-build-bun'. Refusing to continue with a partial download.

That's the test-bun step unable to download the compiled binary artifact from the build-bun step (S3/agent hiccup), unrelated to this PR's 3-file change. The homedir tests themselves pass locally (gate: 4/6 fail without the fix, 6/6 pass with it).

A maintainer can retry just that one job rather than re-running all lanes. Not pushing an empty retrigger commit since it would reset the 45 lanes that already passed.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — the rebase onto the Rust port carries over the fix cleanly and all prior review feedback is addressed.

Extended reasoning...

Overview

This PR fixes #29244: os.homedir() was returning a stale value after process.env.HOME was mutated at runtime, because the native binding read HOME through Bun's snapshot-on-first-read env-var cache. The fix moves the live HOME check into src/js/node/os.ts (mirroring the existing tmpdir pattern) and reduces the native binding to the passwd-fallback path. Three files touched: src/js/node/os.ts (+35 lines, mostly comments, adding homedirFactory), src/runtime/node/node_os.rs (−8/+18: remove the cached env_var::HOME fast-path, switch geteuid()getuid() to match libuv, add explanatory comments), and test/js/node/os/os.test.js (+119: six subprocess-isolated tests under describe("homedir live \$HOME mutations")).

The PR has been through ~10 rounds of automated review since April. Every finding I raised — test concurrency, assertion ordering, ERANGE free-of-stack-pointer, HOME="" vs unset semantics, stale doc comments, sentinel-based passwd-fallback test, geteuid/getuid consistency, test file location, POSIX-only qualification of the userInfo() parity claim — was addressed and resolved. The two remaining items (POSIX userInfo() username/shell sourcing, Windows userInfo().homedir honoring USERPROFILE) are pre-existing divergences explicitly deferred to a follow-up and now documented inline. The most recent commit is a rebase onto main's Zig→Rust migration of node_os; the author re-applied the (now much smaller) native-side change to the Rust port and re-verified the gate (4/6 tests fail without the fix, 6/6 pass with it). The Rust port's Vec<u8> heap buffer makes the earlier Zig ERANGE allocator fix moot.

Security risks

None identified. The change reads an environment variable the process already controls and passes it back verbatim — no shell, no filesystem, no privilege boundary crossed. The geteuid()getuid() switch aligns with libuv and, if anything, makes setuid behavior more predictable (homedir and uid now describe the same user).

Level of scrutiny

Moderate. os.homedir() is widely used, but the JS-side change is a thin wrapper that strictly adds a live env read before the existing fallback — behavior only changes for callers who mutate HOME at runtime (the bug being fixed) or set HOME="" (now returns "" like Node instead of falling through to passwd). The native diff is tiny: an 8-line deletion, a one-identifier change, and comments. The new tests are subprocess-isolated, concurrent, Windows-skipped, and load-bearing for the regression class.

Other factors

No CODEOWNERS cover these paths. No outstanding unresolved review comments. The bug-hunting system found nothing on the post-rebase revision. Existing os.test.js coverage (including the Symbol.toPrimitive coercion suite that exercises homedir) continues to pass per the PR description. CI failures noted in the thread were unrelated Windows HTTP-test flakes confirmed on other PRs. Given the small, well-understood diff, exhaustive prior review, and verified test gate, this is safe to approve.

@robobun

robobun commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Confirming the current state for a maintainer: the fix is ported to the Rust node_os.rs (post Zig→Rust migration), all review threads are resolved, and the gate holds (4/6 tests fail without the fix, 6/6 pass with it). The only red lane on the latest build was darwin-26-aarch64-test-bun failing on a buildkite artifact-download timeout (infra, not this diff) — retrying that single job should green the build.

@robobun

robobun commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

CI update (build #64856): all 72 individual lanes now pass, including darwin-26-aarch64-test-bun (the earlier artifact-download timeout auto-retried and passed in 16m31s). The only remaining red is the aggregate buildkite/bun rollup status, which is stale — it recorded a build-level failure at the 1h23m mark (when the darwin job first hit the infra timeout) and didn't refresh after the job's retry went green.

So the diff is fully green on every real lane. The stale aggregate just needs a maintainer to re-run the build or merge past it; I'm not pushing an empty retrigger since that would reset all 72 passing lanes.

Comment thread src/js/node/os.ts Outdated
Comment thread src/js/node/os.ts Outdated
Comment thread src/js/node/os.ts Outdated
Comment thread src/runtime/node/node_os.rs Outdated
Comment thread src/runtime/node/node_os.rs Outdated
@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased on main (670d8ce) and trimmed comment verbosity; all homedir tests pass locally.

Note: #35270 (the full setenv/unsetenv sync for process.env) does not make this change redundant. os.homedir() previously read HOME via bun_core::env_var::HOME::get(), which caches the first getenv result for the process lifetime, so even with environ updated the cache stays stale. This PR moves the HOME check into src/js/node/os.ts (reading Bun.env["HOME"] live, same pattern as tmpdir) and leaves the native binding as the passwd-only fallback, which is also what userInfo().homedir needs.

On win32 the factory returned a function that only forwarded to the
binding; return the binding itself instead. Compress the remaining
comments to one sentence each.
Comment thread src/js/node/os.ts
Comment thread src/runtime/node/node_os.rs
@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the comment-cop findings in 5a280e5:

  • Fixed the code where the bot had a point: the Windows branch wrapped the binding in a pass-through function whose only purpose was to carry a comment. It now returns the binding directly, deleting both the wrapper and its comment.
  • Compressed the remaining comments to one sentence each. The two it re-flagged after that are staying: they document why the live HOME check is in JS (POSIX process.env writes never reach the C environ, so the native binding can't observe them) and the userInfo()/passwd contract. These aren't workaround justifications — they're the contracts that, when undocumented, led to the exact regressions caught earlier in this PR's review.

All tests pass, including the Symbol.toPrimitive coercion suite.

@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Note: the walkthrough above is out of date — it describes the pre-rebase diff (src/bun.js/node/node_os.zig, test/regression/issue/29244.test.ts). The current PR touches:

  • src/js/node/os.ts: homedirFactory reads Bun.env["HOME"] live on every POSIX call; Windows returns the binding directly.
  • src/runtime/node/node_os.rs (post Zig→Rust migration): removed the cached HOME fast-path so the binding is the pure passwd fallback; getuid() instead of geteuid() for the lookup.
  • test/js/node/os/os.test.js: six subprocess tests under the homedir live $HOME mutations (#29244) describe block.

Head is 5a280e5, all review threads resolved.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Additional findings (outside current diff — PR may have been updated during review):

  • 🔴 src/runtime/node/node_os.rs:751-756 — The new comment claims "libuv's uv__getpwuid_r uses the real uid" to justify switching geteuid()getuid(), but that's factually wrong — libuv's uv_os_get_passwd() (which backs both Node's os.homedir() passwd fallback and os.userInfo()) calls uv__getpwuid_r(pwd, geteuid()), and the libuv docs say "the current effective uid (not the real uid)". Before this PR the code used geteuid() and matched Node/libuv; this change (from earlier review comment #6, which was itself unverified) now diverges from Node in setuid processes while claiming to match it. Revert to libc::geteuid() and fix the comment; if consistency with userInfo().uid is wanted, that field should also move to geteuid() in the deferred follow-up.

    Extended reasoning...

    What the bug is

    Commit 627e2c3 (carried forward into the Rust port) changed the passwd lookup in homedir() from libc::geteuid() to libc::getuid(), and added a comment at src/runtime/node/node_os.rs:751-752:

    // libuv's uv__getpwuid_r uses the real uid; userInfo() below reports
    // uid = getuid(), so geteuid here would desync them under setuid.

    The first clause is factually false. libuv's uv_os_get_passwd() — which is what Node's os.userInfo() wraps, and what uv_os_homedir() falls back to when HOME is unset — is defined in libuv src/unix/core.c as:

    int uv_os_get_passwd(uv_passwd_t* pwd) {
      return uv__getpwuid_r(pwd, geteuid());
    }

    and the libuv API docs for uv_os_get_passwd state: "Gets a subset of the password file entry for the current effective uid (not the real uid)." And uv_os_homedir()'s fallback path calls uv_os_get_passwd(), so it too keys on geteuid().

    The specific code path

    1. Before this PR: node_os.rs called libc::getpwuid_r(libc::geteuid(), …) — matching libuv/Node exactly.
    2. Earlier review comment (previous_comments #6, resolved in 627e2c3) asserted "to match libuv" and requested geteuid()getuid(). That assertion was never verified against libuv source and is wrong.
    3. This PR applied the change and added a comment enshrining the incorrect claim.
    4. Now, in a setuid process where getuid() ≠ geteuid(), Bun's os.homedir() (with HOME unset) and os.userInfo().homedir return the passwd entry for the real user, whereas Node returns the passwd entry for the effective user.

    Why nothing catches it

    Under normal execution getuid() == geteuid(), so all tests pass identically. No test exercises a setuid binary. The comment's factual claim is not checked by anything. REVIEW.md is explicit on this class: "Verify semantics empirically, never from names or intuition… For ported code, the reference implementation (esbuild, Node) is the spec — diff control flow against it before 'fixing' apparent bugs." This is exactly a case where an unverified claim about the reference led to changing correct code to incorrect code.

    Step-by-step proof

    1. Setuid binary: real uid = 1000 (alice, pw_dir=/home/alice), effective uid = 0 (root, pw_dir=/root). HOME is unset.
    2. Node: os.homedir()uv_os_homedirgetenv("HOME") is NULL → uv_os_get_passwduv__getpwuid_r(pwd, geteuid()=0) → returns /root. os.userInfo().homedir → same path → /root, and os.userInfo().uidpwd.uid = 0.
    3. Bun before this PR: getpwuid_r(geteuid()=0, …)/root. ✅ Matches Node.
    4. Bun after this PR: getpwuid_r(getuid()=1000, …)/home/alice. ❌ Diverges from Node.
    5. The comment says this change was made "to match libuv" — it does the opposite.

    The "internal consistency" half of the rationale ("userInfo() below reports uid = getuid()") points at a pre-existing Bun divergence: Node's userInfo().uid is pwd.uid from the geteuid()-keyed lookup (i.e. the effective uid), not getuid(). So the change makes homedir consistent with Bun's wrong uid rather than fixing both toward Node. That pre-existing uid/username/shell divergence is already flagged for the deferred follow-up PR.

    Impact

    • Comment: enshrines misinformation about the reference implementation right next to the call it justifies; future maintainers will read "libuv uses the real uid" as authoritative.
    • Behavior: newly-introduced Node divergence in setuid processes for both os.homedir() (HOME unset) and os.userInfo().homedir. Narrow surface, but a regression in previously-correct code.

    How to fix

    Revert to libc::geteuid() and correct the comment (or delete it — the surrounding docblock already says this path "match[es] Node's uv_os_get_passwd", which is sufficient once the call actually does):

    // libuv's uv_os_get_passwd keys on the *effective* uid (geteuid()); Node's
    // os.userInfo() and os.homedir() passwd fallback both go through it.
    let ret = unsafe {
        libc::getpwuid_r(
            libc::geteuid(),

    If internal consistency with userInfo().uid is desired, that field should also move to the effective uid — but that belongs in the already-deferred username/shell/Windows-userInfo follow-up, not here.

Comment thread test/js/node/os/os.test.js Outdated
Verified against libuv v1.x src/unix/core.c: uv_os_get_passwd calls
uv__getpwuid_r(pwd, geteuid()), and uv_os_homedir's fallback goes
through it. The earlier switch to getuid() was based on an incorrect
claim about libuv and diverged from Node in setuid processes.

Also trim the test header to the issue URL plus the invariants and
drop the stale Zig reference.
@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Both findings addressed in 0cd7f84:

  • Reverted the passwd lookup to geteuid(). Verified against libuv v1.x src/unix/core.c directly: uv_os_get_passwd is return uv__getpwuid_r(pwd, geteuid()); (line 1509) and uv_os_homedir's fallback goes through it (line 1279). The earlier switch to getuid() was applied from an unverified review claim and diverged from Node in setuid processes — my mistake for not checking the reference then. The comment now states the effective-uid contract.
  • Trimmed the test describe-block header to the issue URL plus the invariants, removing the stale "Zig binding" reference and the bug-history prose.

All 9 homedir tests pass (including the Symbol.toPrimitive suite).

@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

CI note on build #82132: the binary-size failure (+512-576 KB on all 12 targets vs canary) is not from this diff — the canary baseline is main build #79916, far behind the main this branch is rebased on, and the identical failure appears on unrelated PRs (e.g. build #82125). Reported for main-side triage. This PR's diff is ~15 lines of bundled JS plus a net-negative Rust change. The only other failure is a tooling-tagged flaky EADDRINUSE in test/cli/inspect/inspect.test.ts, unrelated to os.homedir.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — the 0cd7f84 pass addressed the prior feedback (header trimmed, geteuid() restored per libuv). One leftover comment-only nit inline; non-blocking.

Extended reasoning...

Overview

Three-file Node-compat fix: src/js/node/os.ts gains homedirFactory reading Bun.env["HOME"] live on every POSIX call (mirroring the existing tmpdir pattern); src/runtime/node/node_os.rs drops the cached env_var::HOME.get() fast-path so the binding is the pure passwd fallback (which userInfo() already calls directly); six subprocess tests added to test/js/node/os/os.test.js.

Delta since last review

0cd7f84 applied both prior findings: the describe-block header was trimmed to issue URL + invariants (stale Zig reference gone), and the passwd lookup was reverted to geteuid() after verifying against libuv's uv_os_get_passwd source. The one remaining nit this run — a single "Previously Bun treated…" sentence in the empty-string test's per-test comment — is the same bug-history class that slipped past the header trim. Comment-only, non-blocking.

Security risks

None. Reads an env var and calls getpwuid_r; no new external input surface, no privilege boundaries crossed.

Level of scrutiny

Low-to-moderate. Small, focused behavior change following an established in-file pattern (tmpdir). Windows path is unchanged (uv_os_homedir already reads USERPROFILE live). Symbol.toPrimitive coercion still works — symbolToStringify(out, "homedir") attaches to the returned closure/binding, and the existing #10259 suite covers it. The env_var import in node_os.rs remains live via USER/SHELL in user_info().

Other factors

All prior review threads resolved; the deferred pre-existing divergences (POSIX username/shell from env, Windows userInfo().homedir via uv_os_homedir) were correctly scoped out to a follow-up. Tests are subprocess-isolated, sentinel-based, and gate-verified (4/6 fail without the fix).

Comment thread test/js/node/os/os.test.js Outdated
@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the last nit in c06bfae: dropped the bug-history sentence from the empty-HOME test comment, keeping the libuv contract lines. All homedir tests pass; every review thread is resolved.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — small, focused Node-compat fix that mirrors the existing tmpdir pattern.

What was reviewed:

  • homedirFactory in os.ts: Bun.env["HOME"] live read on POSIX, home !== undefined so HOME="""" (matches libuv); Windows returns the binding unchanged, so symbolToStringify behavior is identical to before.
  • node_os.rs: removed env fast-path leaves getpwuid_r(geteuid(), …) as the passwd fallback; env_var import still live via USER/SHELL in user_info(); userInfo().homedir correctly still skips $HOME.
  • Tests: 6 subprocess cases with it.concurrent, stdout drained before exited, stderr: "inherit" so no pipe-fill risk; all prior comment-cop / bug-history / file-placement nits addressed through c06bfae.
Extended reasoning...

Overview

Three-file change fixing #29244: os.homedir() returned a stale value after process.env.HOME was mutated because the native binding read HOME through bun_core::env_var::HOME.get(), a snapshot-on-first-read cache. The fix moves the live $HOME check into src/js/node/os.ts (reading Bun.env["HOME"] on every call, exactly like tmpdir does immediately above it) and reduces the native homedir() in src/runtime/node/node_os.rs to the pure passwd/getpwuid_r fallback. Six subprocess tests are added to test/js/node/os/os.test.js covering mutate-after-require, mutate-before-require, inherited HOME, HOME="", deleted HOME → passwd, and userInfo().homedir still ignoring $HOME.

Security risks

None. No user input reaches a syscall or path operation any differently than before; the only behavioral change is which store $HOME is read from (JS-side Bun.env vs a cached native accessor). The passwd lookup path (getpwuid_r) is unchanged apart from the removed env short-circuit. No auth, crypto, permissions, or parsing surface is touched.

Level of scrutiny

Low-to-moderate. This is a Node.js compatibility fix in a well-understood API (os.homedir()), the JS-side change is a direct copy of the tmpdir pattern in the same file, and the Rust change is a net deletion (removing a fast-path) plus a one-line comment. On Windows the exported homedir is byte-identical to before (homedirFactory returns binding.homedir directly, and symbolToStringify already handles native bindings for freemem/hostname/etc.). The Symbol.toPrimitive coercion suite in the same test file continues to cover the wrapped function. userInfo() still calls the native binding directly, which now correctly skips $HOME on POSIX — an improvement over the pre-PR behavior.

Other factors

This PR has been through extensive iteration: file placement (moved from test/regression/ into os.test.js), HOME="" semantics verified against Node 25, getuid vs geteuid verified against libuv source (reverted to geteuid to match uv_os_get_passwd), comment verbosity trimmed per comment-cop, and all bug-history prose removed. Every one of my prior inline nits and the comment-cop findings are resolved as of c06bfae. The env_var import remains live (used by user_info for USER/SHELL). Tests follow harness conventions: bunEnv spread, it.concurrent for independent subprocesses, await using for cleanup, stdout asserted before exit code, stderr: "inherit" avoids the pipe-drain requirement. The known Windows userInfo().homedir divergence (honors USERPROFILE via uv_os_homedir where Node uses uv_os_get_passwd) is pre-existing, documented in the review thread, and correctly deferred to a follow-up alongside the POSIX username/shell divergence — it is not introduced or worsened by this change.

@robobun

robobun commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

Two notes from re-checking this against main (9a543cc), where it still merges cleanly and the bug still reproduces:

  • Because the check reads Bun.env (which in a worker is the worker's own process.env object), this also fixes os.homedir() inside new Worker(file, { env: { ...process.env, HOME: "/x" } }), which on main returns the parent's home. That case is worth a test here; it is the one embedders running a tenant per worker hit. (Note that when writing that test, a literal process.env.HOME read inside the worker is currently inlined at transpile time with the parent's value, see Runtime transpiler cache inlines process.env dot-reads during Worker-thread imports — later processes execute the first process's env values #34210 / Stop inlining process.env dot-reads in Worker-thread transpiles #34211, so assert on os.homedir() or read the env via Bun.env.)
  • Windows: uv_os_homedir reads the process-wide USERPROFILE, which a Worker's env option never touches, so the worker case stays broken there. Doing the same JS-side check for USERPROFILE on win32 (libuv checks the variable first there too, then falls back to the profile directory) would make the two platforms behave the same; the binding would remain the fallback on both.

Related: #34629 changes the same homedir() function in node_os.rs (empty $HOME, userInfo().homedir); this PR's JS-side check covers both of those behaviors as well, so whichever lands second needs a small rebase.

@alii alii left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use node:test and make sure the test behaves the same in Node.js that way

Move the six homedir//root tests into os-homedir-env.test.mjs using
node:test + node:assert and process.execPath, so the identical file
runs under Node.js:

  node test/js/node/os/os-homedir-env.test.mjs   # 6 pass on v26.3.0

Verified fail-before/pass-after under bun bd (4/6 fail without the
fix).
@robobun

robobun commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

Done in 58e7023. The six homedir tests now live in test/js/node/os/os-homedir-env.test.mjs using node:test + node:assert, spawning process.execPath so the same file runs under either runtime.

Verified under Node v26.3.0 directly:

$ node test/js/node/os/os-homedir-env.test.mjs
✔ homedir() reflects HOME mutation after require
✔ homedir() reflects HOME mutation before require
✔ homedir() honors HOME from parent env
✔ homedir() returns '' when HOME is set to empty string
✔ homedir() falls back to passwd when HOME is deleted
✔ userInfo().homedir ignores HOME mutation
ℹ pass 6 / fail 0

Same 6 pass under bun bd test, and the gate still holds (4/6 fail against an unfixed build). The previous bun:test block in os.test.js is removed.

Plain .test.js runs under bun's test runner and directly under node
(test/ is type: commonjs). 6/6 pass on Node v26.3.0 and bun bd.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 test/js/node/os/os-homedir-env.test.mjs:4-9 — Commit 58e7023 moves the 6 homedir tests back out of os.test.js into a new os-homedir-env.test.mjs, reversing the c05b47a fix this PR already accepted for the earlier "tests belong in the module's existing file" review, and in doing so drops two other harness conventions that were also settled: {...process.env} instead of bunEnv (REVIEW.md "Spread bunEnv when modifying it") and 6 serial spawnSync calls (REVIEW.md "async spawns over spawnSync" / test/CLAUDE.md "Prefer concurrent tests"). The Node-parity rationale is reasonable and there is precedent for node:test under test/js/node/, but it doesn't require undoing three already-resolved review threads — nit only, no functional impact.

    Extended reasoning...

    What the issue is

    Commit 58e7023 ("port homedir env tests to node:test for direct Node parity") moved the 6 os.homedir() env tests out of test/js/node/os/os.test.js and into a new standalone test/js/node/os/os-homedir-env.test.mjs that uses node:test + spawnSync(process.execPath, …, { env: { ...process.env } }). This reverses c05b47a — the fix this same PR applied in response to the 2026-05-04 review comment ("these 6 tests belong in test/js/node/os/os.test.js … rather than a new file"), which was accepted and marked resolved with "Moved the 6 tests into test/js/node/os/os.test.js under a describe block".

    Reverting to a separate file also drops two other harness conventions that earlier review rounds on this PR had settled:

    • bunEnv: REVIEW.md "Copy harness conventions exactly" says "Spread bunEnv when modifying it". The new file spawns with { ...process.env }, so children don't get BUN_DEBUG_QUIET_LOGS=1, NO_COLOR=1, JSC_useJIT: undefined, etc. from harness.ts:64-85.
    • spawnSync / concurrency: REVIEW.md's same section says "async spawns over spawnSync" and "test.concurrent for independent subprocess suites". The 6 serial spawnSync calls can't be made concurrent, regressing the earlier test.concurrent review fix that was also accepted on this PR.

    Why the stated rationale doesn't require this shape

    The header comment says "Uses node:test so the same file runs under Node.js to verify parity". That's a legitimate goal, and there is precedent for node:test under test/js/node/ (e.g. node-tls-connect-hostname-verification.test.ts, node-http2-continuation.test.ts, child-process-rlimit-nofile.test.ts) — so this is not unprecedented. But the repo's dedicated location for run-under-Node parity is test/js/node/test/parallel/, and the other .test.mjs files that live alongside module tests (e.g. test/js/node/util/parse_args/*.test.mjs) still import from bun:test + harness. Wanting a Node-runnable copy doesn't require moving the tests out of os.test.js and dropping bunEnv/async-spawn — a small test/js/node/test/parallel/-style shim could carry the parity check while the primary tests stay where c05b47a put them.

    Practical impact

    Limited. Debug logs go to stderr and the file only JSON.parses stdout (stderr is only surfaced in the assertion message on non-zero exit), so leaked BUN_DEBUG_* output wouldn't actually break the assertions. The 6 serial spawns add a few hundred ms to the file vs. concurrent async spawns. This is file-organization / harness-convention only — no effect on what the tests verify.

    Step-by-step

    1. test/CLAUDE.md says: "Add tests to the existing test file for the code you're changing — do not create a new file." The existing file is test/js/node/os/os.test.js.
    2. Review on 2026-05-04 flagged the separate-file placement; c05b47a moved the 6 tests into os.test.js and the thread was resolved.
    3. 58e7023 creates os-homedir-env.test.mjs and removes the tests from os.test.js (verified: grep 29244 os.test.js → no matches).
    4. The new file uses spawnSync + { ...process.env } instead of async Bun.spawn + bunEnv, contrary to REVIEW.md's "Copy harness conventions exactly" bullets.
    5. ⇒ Three previously-accepted review fixes on this PR (file placement, bunEnv, concurrent async spawns) are dropped in one commit.

    How to fix

    Either:

    • Keep the tests in os.test.js (as they were after c05b47a) using Bun.spawn + bunEnv + test.concurrent, and if a Node-runnable parity check is wanted, add a small shim under test/js/node/test/parallel/; or
    • Keep the separate .test.mjs but note in the PR why re-opening the resolved file-placement thread is preferable, and at minimum spread the harness env ({ ...process.env, BUN_DEBUG_QUIET_LOGS: "1", NO_COLOR: "1" }) so children match what bunEnv would set — the same pattern child-process-rlimit-nofile.test.ts uses.

    Nit — non-blocking; conventions only.

No harness import is possible (the file runs under plain node), so set
BUN_DEBUG_QUIET_LOGS/NO_COLOR explicitly like bunEnv would.
@robobun

robobun commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

On the conventions note: the separate file, plain spawnSync, and absence of bunEnv/test.concurrent all follow from the maintainer request that the test run unmodified under Node.js — bun:test, harness imports, and Bun.spawn would each break node os-homedir-env.test.js, and the file can't live inside the bun:test-based os.test.js for the same reason. That supersedes the earlier placement thread, which predated the request.

The one compatible improvement is applied in 578bd78: the spawn env now sets BUN_DEBUG_QUIET_LOGS/NO_COLOR explicitly (what bunEnv would provide; node ignores them), matching the child-process-rlimit-nofile.test.ts pattern. 6/6 still pass under Node v26.3.0 and bun bd test.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — small, well-scoped Node-compat fix mirroring the existing tmpdir pattern.

What was reviewed:

  • homedirFactory in os.ts: live Bun.env["HOME"] read on POSIX, binding passthrough on Windows; Symbol.toPrimitive wiring unchanged (Windows still attaches to the native fn as before).
  • node_os.rs: removal of the cached env_var::HOME fast-path leaves the binding as pure passwd fallback; env_var import still live via user_info(); geteuid() matches libuv's uv_os_get_passwd.
  • Test file: 6 subprocess cases run under both Bun and Node via node:test + process.execPath; empty-string vs absent HOME distinction, delete-then-fallback, and userInfo().homedir non-honoring all covered.
Extended reasoning...

Overview

Three files: src/js/node/os.ts gains a ~15-line homedirFactory that reads Bun.env["HOME"] live on every POSIX os.homedir() call (mirroring the existing tmpdir implementation in the same file) and returns the native binding unchanged on Windows. src/runtime/node/node_os.rs drops the 6-line cached env_var::HOME.get() fast-path so the binding is now the pure getpwuid_r fallback, and adds a one-line comment pinning the geteuid() choice to libuv. test/js/node/os/os-homedir-env.test.js is a new 90-line node:test file with 6 subprocess-isolated cases, verified to pass under both bun bd test and node directly.

Security risks

None identified. The change reads an environment variable and returns it as a string; no path traversal, no filesystem access, no privilege operations added. The geteuid() vs getuid() question was already litigated in prior review rounds and settled by direct reference to libuv source (uv_os_get_passwd uses geteuid()).

Level of scrutiny

Low-to-medium. This is a Node-compat behavioral fix in a non-hot-path getter, following an established in-file pattern (tmpdir). The Rust change is purely subtractive on the POSIX branch. The PR has been through ~10 rounds of automated review over four months with every thread resolved; the remaining diff is minimal and each earlier finding (test placement, comment verbosity, geteuid/getuid, empty-vs-unset HOME semantics, Windows userInfo divergence scoping) has a documented resolution.

Other factors

All prior inline threads are resolved. The author's own 2026-08-12 note about a Worker-env test and a Windows JS-side USERPROFILE check are self-identified follow-ups (scope expansion), not unaddressed review feedback, and don't block this fix. The test file's use of spawnSync/no-harness is intentional so it runs unmodified under Node (per the author's explanation), and it sets BUN_DEBUG_QUIET_LOGS/NO_COLOR inline to compensate for the absent bunEnv. The gate (4/6 fail without the fix, 6/6 pass with it, 6/6 pass under Node) has been re-verified after each substantive change. The overlap with #34629 is a rebase-coordination note only.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — small, well-tested Node-compat fix that mirrors the existing tmpdir pattern.

What was reviewed:

  • homedirFactory in os.ts: Bun.env["HOME"] live read on POSIX with !== undefined gate (so HOME="" returns "", matching libuv); Windows returns the binding unchanged, so Symbol.toPrimitive wiring is identical to before.
  • node_os.rs: removing the cached env_var::HOME fast-path leaves userInfo().homedir correctly reading the passwd entry via geteuid() (matches libuv's uv_os_get_passwd); env_var import stays live via USER/SHELL.
  • The 6 node:test subprocess cases cover mutate-after/before-require, inherited env, empty vs. deleted HOME, and the userInfo() negative — verified runnable under Node per the parity constraint.
Extended reasoning...

Overview

Three files: src/js/node/os.ts adds a ~12-line homedirFactory that reads Bun.env["HOME"] live on POSIX and delegates to the native binding on Windows; src/runtime/node/node_os.rs removes the ~8-line cached env_var::HOME fast-path so the binding is a pure getpwuid_r(geteuid()) fallback (also used by userInfo()); and a new 90-line test/js/node/os/os-homedir-env.test.js with 6 subprocess tests written against node:test/node:assert so the same file runs under Node.js for parity verification.

Security risks

None. No auth, crypto, permissions, or untrusted-input parsing. Reading process.env.HOME live is exactly what Node/libuv do; the passwd fallback path is unchanged except for dropping the env short-circuit.

Level of scrutiny

Low-to-medium. This is a narrow Node-compat fix following an established in-file pattern (tmpdir already reads Bun.env live in the same way). The Rust change is a pure deletion plus a one-line comment. The PR has been through many review rounds — every prior finding (empty-string semantics, geteuid vs getuid, test placement, comment verbosity, node:test portability) is resolved and reflected in the current diff.

Other factors

  • All prior inline threads from claude, comment-cop, and coderabbit are marked resolved; no human reviewer has outstanding comments.
  • The bug-hunting system found nothing on this revision.
  • The Windows userInfo().homedir / USERPROFILE divergence and the Worker-env test suggestion were explicitly deferred to follow-up work earlier in the thread; they are pre-existing behavior this PR does not touch and do not block the #29244 fix.
  • Confirmed symbolToStringify(out, "homedir") still works: on Windows it decorates the same native function as before, and on POSIX it decorates the JS wrapper whose () call returns the string.
  • Overlap with #34629 is a merge-order concern only, not a correctness issue for this diff.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Build #93658: 188/196 lanes green. The only failures are darwin x64 build-cpp/build-bun — the agent could not download the prebuilt WebKit tarball ([WebKit] error: Failed to download after 5 attempts ... cause: fetch failed, 30s into the job, before compiling anything), and build-bun cascade-failed behind it. Infra, not this diff; a job retry on that lane should clear it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

os.homedir() uses process-start HOME snapshot instead of current process.env.HOME

2 participants