Skip to content

fs: reject empty mkdtemp prefix with EINVAL - #34908

Merged
Jarred-Sumner merged 4 commits into
mainfrom
claude/1ea1f65c/mkdtemp-empty-prefix
Aug 13, 2026
Merged

Jarred-Sumner merged 4 commits into
mainfrom
claude/1ea1f65c/mkdtemp-empty-prefix

Conversation

@robobun

@robobun robobun commented Jul 21, 2026 •

Copy link
Copy Markdown
Collaborator

What

fs.mkdtemp('') and fs.mkdtempSync('') (and the promises variant) now reject with EINVAL instead of creating a bare six-random-character directory in the process cwd.

Repro

import fs from "node:fs";
import fsp from "node:fs/promises";
try { fs.mkdtempSync(""); } catch (e) { console.log("sync:", e.code); }
try { await fsp.mkdtemp(""); } catch (e) { console.log("promises:", e.code); }

Before: creates gQOino-style directories in cwd with no error.
After (and Node v26.3.0): EINVAL for both.

Cause

Bun appends XXXXXX to the prefix and calls mkdtemp(3). With an empty prefix the template is exactly XXXXXX, which mkdtemp(3) accepts, creating a random-named directory in cwd. Node's C++ binding happens to produce a 5-X template for the empty-prefix case (an snprintf size quirk), which mkdtemp(3) rejects with EINVAL. The Node behavior is the useful one: a caller whose prefix resolves to '' (e.g. mkdtemp(cfg.tmpPrefix ?? '')) should get an error rather than litter cwd.

Fix

Return EINVAL from NodeFS::mkdtemp before touching the filesystem when the prefix slice is empty. Applies to sync, callback, and promise forms.

Verification

USE_SYSTEM_BUN=1 bun test test/js/node/fs/fs.test.ts -t "mkdtemp empty prefix"  # 3 fail
bun bd test test/js/node/fs/fs.test.ts -t "mkdtemp empty prefix"                 # 3 pass
bun bd test test/js/node/fs/fs.test.ts -t "mkdtemp"                              # 19 pass

Node parallel tests test-fs-mkdtemp.js, test-fs-mkdtemp-prefix-check.js, test-fs-mkdtempDisposableSync.js, and test-fs-promises-mkdtempDisposable.js all pass.


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

Node.js rejects fs.mkdtemp('') / fs.mkdtempSync('') with EINVAL. Bun was
building the template "XXXXXX" and letting mkdtemp(3) succeed, silently
creating a bare six-random-character directory in the process cwd.

Return EINVAL before touching the filesystem when the prefix is empty,
matching Node's observable behavior for sync, callback, and promise
variants.
@coderabbitai

coderabbitai Bot commented Jul 21, 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: 9 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: 0ec4c031-d641-4ac3-b9d9-ab5d6fcf4265

📥 Commits

Reviewing files that changed from the base of the PR and between 5b98630 and fd2dd9c.

📒 Files selected for processing (2)
  • src/runtime/node/node_fs.rs
  • test/js/node/fs/fs.test.ts

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

@robobun

robobun commented Jul 21, 2026 •

Copy link
Copy Markdown
Collaborator Author

Reproduced on 1.4.0-canary (system bun creates a random-named dir in cwd; Node v26.3.0 throws EINVAL). Fix returns EINVAL before the syscall when the prefix is empty; err.path matches Node's "XXXXX".

PR: #34908

CI (build 76783): fs.test.ts and the node-parallel mkdtemp tests pass on every lane. Remaining failures are all tagged flaky by the helper and unrelated to this diff (compile-windows-metadata, spawn.test.ts timeout on win-aarch64, require-cache leak on alpine, bun-upgrade ETXTBSY, fetch-gzip/http2 on darwin, etc.). Ready for review.

@robobun

robobun commented Jul 21, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:31 AM PT - Jul 21st, 2026

✅ @robobun, your commit fd2dd9c0693f05badb206b3028c1bc74be02a869 passed in Build #76783! 🎉


🧪   To try this PR locally:

bunx bun-pr 34908

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

bun-34908 --bun

Comment thread src/runtime/node/node_fs.rs 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 — the earlier 5-X err.path note is addressed, and the remaining inline comment is a cosmetic dead-guard cleanup that doesn't affect behavior.

Extended reasoning...

Overview

Adds a 10-line early return in NodeFS::mkdtemp (src/runtime/node/node_fs.rs:6094) that rejects an empty prefix with EINVAL before any syscall, matching Node's observed behavior (including err.path === "XXXXX"). Three tests in test/js/node/fs/fs.test.ts cover the sync, callback, and promises entry points.

Security risks

None. The change short-circuits before touching the filesystem; no new allocation, no user-controlled data reaches a syscall on the new path. The sys::Error construction mirrors the existing error path a few lines below.

Level of scrutiny

Low. This is a narrow Node-compat edge-case fix at the shared NodeFS::mkdtemp layer (all three JS entry points route through it — confirmed there is only one fn mkdtemp in this file). [b'X'; 5].into() correctly targets sys::Error.path: Box<[u8]>. The tests use strong assertions (toMatchObject on code + syscall), the callback test resolves via Promise.withResolvers rather than throwing in the callback, and the .rejects case is awaited.

Other factors

My previous review flagged the 6-X vs 5-X err.path mismatch; the author fixed it in 80f640a and updated the code comment to note Node's snprintf quirk — that thread is resolved. The one new inline nit (the now-unreachable if len > 0 guard) is a REVIEW.md-style cleanup with zero behavioral impact and can be folded in or left as-is.

Comment thread src/runtime/node/node_fs.rs 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 — the two earlier nits (5-X err.path and the now-dead len > 0 guard) are both addressed.

What was reviewed:

  • Confirmed sync/callback/promises all route through the single NodeFS::mkdtemp, so one guard covers every entry point.
  • Checked sys::Error.path is Box<[u8]>; [b'X'; 5].into() type-checks and matches Node's observed err.path.
  • Tests await properly (.rejects is awaited, callback resolved via withResolvers) and assert specific code/syscall, not bare toThrow().
Extended reasoning...

Overview

Adds an is_empty() early return in NodeFS::mkdtemp (src/runtime/node/node_fs.rs:6094) that surfaces EINVAL with syscall: mkdtemp and path: "XXXXX" when the prefix is empty, matching Node's observed behavior. Drops the now-redundant if len > 0 guard around the prefix copy. Adds three tests in test/js/node/fs/fs.test.ts covering sync, callback, and promises variants.

Security risks

None. This tightens behavior — an input that previously created a directory in cwd now errors before any syscall. No new user-controlled data reaches a syscall path.

Level of scrutiny

Low-to-medium. This is a small, additive Node-compat guard in a well-understood function; the non-empty path is byte-identical to before (modulo the removed dead guard). All three JS entry points funnel through this one Rust function via the binding table (node_fs_binding.rs:378, node_fs.rs:9441), so there are no sibling sites to miss.

Other factors

Two prior review rounds raised (1) 6-X vs 5-X err.path and (2) the newly-dead len > 0 guard; both were fixed in follow-up commits and the threads are resolved. The PR description shows the tests fail under USE_SYSTEM_BUN=1 and pass under the debug build, and the relevant Node parallel tests still pass. The tests follow harness conventions (awaited .rejects, withResolvers for the callback form, specific error-code assertions).

@Jarred-Sumner
Jarred-Sumner merged commit b7a0431 into main Aug 13, 2026
79 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the claude/1ea1f65c/mkdtemp-empty-prefix branch August 13, 2026 06:00
alii pushed a commit that referenced this pull request Aug 17, 2026
#39445)

Stacked on #36463 (the base branch is that PR's branch, so the diff here
is only the additions). Merging this into #36463 adds the behavior
changes listed below; #36463 itself now covers the #38333 install batch,
the optional-peer correction, and the TOML / `bun init` fixes, so this
PR no longer touches those.

### Problem
- These 1.3 to 1.4 behavior changes are not in the guide at `701b3e2a0`:
- MySQL: the first `caching_sha2_password` connection over plain TCP is
refused unless `allowPublicKeyRetrieval: true` (#31129; 1.3.14 requested
the key automatically, `MySQLConnection.zig` in the 1.3.14 tag). SQL
`tls` / `ssl` options now require TLS instead of falling back to
plaintext, and `?ssl=` / `?ssl-mode=` are read (`shared.ts` 1.3.14 only
read `?sslmode=`; #37669).
- Install: `~/.npmrc` fallback when `XDG_CONFIG_HOME` is set (#36289),
credentials in `--registry` / env / bunfig object URLs are sent and
outrank same-host `.npmrc` tokens (#38796, #38824), `bun outdated` exits
1 on fetch failures (#38809), new `dedupe` / `up` commands shadow
scripts of those names and `bun feedback` is removed (#38333, #38444),
`workspace:` ranges inside registry packages (#37669), isolated store
entry names (#39014).
- Runtime: `module.enableCompileCache()` / `NODE_COMPILE_CACHE`
implemented (#34660), `require()` / `import` not-found messages
(#34660), `AbortError` message without the period (#39277; 1.3.14's
`BunCommonStrings.h` has the period), GCM IV length (#34092),
`mkdtemp("")` (#34908), vm options (#38381), `server.reload` (#38697),
ICU 75/73 to 78 (#38013), Compression stream chunking (#38695),
`Bun.SQL` sqlite bindings (#35950).
- Bundler: `splitting` with `cjs` / `iife` is an error (#32685),
block-scoped `enum` lowers to `let` (#34249), exports emitted ascending
instead of descending (#35957; `doStep5.zig` in 1.3.14 used `sortDesc`),
minified `$` (#35668).
- The TOML integer bullet did not say what the limit or the fix is.

### Fix
- Adds a MySQL public key section (plus a summary table row), a TLS note
under the `PGSSLMODE` section, an `.npmrc` / credentials addendum to the
`bunfig.toml` section, a `module.enableCompileCache()` section, and the
rest as bullets in the existing lists.
- `docs/pm/overrides.mdx`: one-line change adding a pointer to this
guide in the existing `lockfileVersion` 3 limitation. (The base branch
briefly had a duplicate "Nested overrides" section; it removed that
itself in `8257d01acb`, and this PR was rebased over it.)
- Verification: each runtime claim was run against
`1.4.0-canary.1+8326d1bd3` (22 commits behind main; contains every
change referenced), and each install or bundler claim was checked
against the source on main, with the 1.3 side taken from the
`bun-v1.3.14` tag where the PR body did not state it. The
`/runtime/sql#mysql` and `/upgrade-to-1.4` links resolve. `prettier
--check` passes.

### Not included on purpose
- Lifecycle scripts no longer receiving `npm_package_name` /
`npm_package_version` / `npm_package_json` / `npm_config_local_prefix`
during `bun install`, and transitive `"*"` ranges no longer
deduplicating onto the root's version: regressions with open fixes
(#36690, #38110, #38770). They need either the fixes or a guide line
before release.
- Postgres `sslmode=prefer` / `allow` (including `PGSSLMODE=prefer`,
which 1.4 newly reads) hangs until the connection timeout against a
server without SSL because nothing sends the startup message after the
`N` reply. Same code in 1.3.14; filed as a bug instead of documented.

<details>
<summary>Commands used to verify the runtime claims</summary>

```
timers/promises setTimeout with an aborted signal             # "The operation was aborted"
bun req.cjs                                                   # Cannot find module ... Require stack:
bun b.mjs (import() of a missing package / relative file)      # Cannot find package 'x' imported from /path, ERR_MODULE_NOT_FOUND
bun a_static.mjs (unhandled static import)                    # printed line still: Cannot find package 'x' from '/path'
process.versions.icu                                          # 78.3
createCipheriv("aes-128-gcm", key, Buffer.alloc(129))         # ERR_CRYPTO_INVALID_IV
DecompressionStream of a 1 MiB gzip member                    # 16 chunks of 65536 bytes
new SQL("sqlite://:memory:") with ${[1,2]} / ${new Date()}    # Binding expected ...
fs.mkdtempSync("")                                            # EINVAL
vm.runInThisContext("1", [])                                  # ERR_INVALID_ARG_TYPE
NODE_COMPILE_CACHE=/tmp/cc bun cc.cjs                         # creates /tmp/cc/v1.4.0-x86_64-<sha>-<uid>
NODE_DISABLE_COMPILE_CACHE=1 + enableCompileCache()           # status 3 (DISABLED)
bun dedupe / bun up with package.json scripts of those names  # built-in command runs
bun feedback                                                  # Script not found "feedback"
Bun.build({ splitting: true, format: "cjs" })                 # Code splitting is currently only supported ...
bun build of a function-scoped enum and import * as ns         # let Color; exports a, m, z
new SQL({ url: "postgres://...", tls: true }) on a non-TLS server  # ERR_POSTGRES_TLS_NOT_AVAILABLE
Bun.TOML.parse("a = 9007199254740993")                        # Integer cannot be losslessly represented ...
```

</details>

<details>
<summary>Previous revision</summary>

The first revision of this PR (`3c5611454a`) also rewrote the package
manager section for #38333 / #38853 (nested overrides and
`lockfileVersion: 3`, the optional-peer correction, `bun update`,
`bunfig.toml` over `.npmrc`, `--filter`) and fixed the TOML date and
`bun init` lines. #36463 picked those up in its own commits the same
day, so this PR was rebased onto its new head and reduced to the items
above.

</details>

<!-- robobun:evidence:begin -->

---

**no test proof** · iteration 0 · docs-only change; test-proof not
applicable

<!-- robobun:evidence:end -->
robobun added a commit that referenced this pull request Aug 21, 2026
#39445)

Stacked on #36463 (the base branch is that PR's branch, so the diff here
is only the additions). Merging this into #36463 adds the behavior
changes listed below; #36463 itself now covers the #38333 install batch,
the optional-peer correction, and the TOML / `bun init` fixes, so this
PR no longer touches those.

### Problem
- These 1.3 to 1.4 behavior changes are not in the guide at `701b3e2a0`:
- MySQL: the first `caching_sha2_password` connection over plain TCP is
refused unless `allowPublicKeyRetrieval: true` (#31129; 1.3.14 requested
the key automatically, `MySQLConnection.zig` in the 1.3.14 tag). SQL
`tls` / `ssl` options now require TLS instead of falling back to
plaintext, and `?ssl=` / `?ssl-mode=` are read (`shared.ts` 1.3.14 only
read `?sslmode=`; #37669).
- Install: `~/.npmrc` fallback when `XDG_CONFIG_HOME` is set (#36289),
credentials in `--registry` / env / bunfig object URLs are sent and
outrank same-host `.npmrc` tokens (#38796, #38824), `bun outdated` exits
1 on fetch failures (#38809), new `dedupe` / `up` commands shadow
scripts of those names and `bun feedback` is removed (#38333, #38444),
`workspace:` ranges inside registry packages (#37669), isolated store
entry names (#39014).
- Runtime: `module.enableCompileCache()` / `NODE_COMPILE_CACHE`
implemented (#34660), `require()` / `import` not-found messages
(#34660), `AbortError` message without the period (#39277; 1.3.14's
`BunCommonStrings.h` has the period), GCM IV length (#34092),
`mkdtemp("")` (#34908), vm options (#38381), `server.reload` (#38697),
ICU 75/73 to 78 (#38013), Compression stream chunking (#38695),
`Bun.SQL` sqlite bindings (#35950).
- Bundler: `splitting` with `cjs` / `iife` is an error (#32685),
block-scoped `enum` lowers to `let` (#34249), exports emitted ascending
instead of descending (#35957; `doStep5.zig` in 1.3.14 used `sortDesc`),
minified `$` (#35668).
- The TOML integer bullet did not say what the limit or the fix is.

### Fix
- Adds a MySQL public key section (plus a summary table row), a TLS note
under the `PGSSLMODE` section, an `.npmrc` / credentials addendum to the
`bunfig.toml` section, a `module.enableCompileCache()` section, and the
rest as bullets in the existing lists.
- `docs/pm/overrides.mdx`: one-line change adding a pointer to this
guide in the existing `lockfileVersion` 3 limitation. (The base branch
briefly had a duplicate "Nested overrides" section; it removed that
itself in `8257d01acb`, and this PR was rebased over it.)
- Verification: each runtime claim was run against
`1.4.0-canary.1+8326d1bd3` (22 commits behind main; contains every
change referenced), and each install or bundler claim was checked
against the source on main, with the 1.3 side taken from the
`bun-v1.3.14` tag where the PR body did not state it. The
`/runtime/sql#mysql` and `/upgrade-to-1.4` links resolve. `prettier
--check` passes.

### Not included on purpose
- Lifecycle scripts no longer receiving `npm_package_name` /
`npm_package_version` / `npm_package_json` / `npm_config_local_prefix`
during `bun install`, and transitive `"*"` ranges no longer
deduplicating onto the root's version: regressions with open fixes
(#36690, #38110, #38770). They need either the fixes or a guide line
before release.
- Postgres `sslmode=prefer` / `allow` (including `PGSSLMODE=prefer`,
which 1.4 newly reads) hangs until the connection timeout against a
server without SSL because nothing sends the startup message after the
`N` reply. Same code in 1.3.14; filed as a bug instead of documented.

<details>
<summary>Commands used to verify the runtime claims</summary>

```
timers/promises setTimeout with an aborted signal             # "The operation was aborted"
bun req.cjs                                                   # Cannot find module ... Require stack:
bun b.mjs (import() of a missing package / relative file)      # Cannot find package 'x' imported from /path, ERR_MODULE_NOT_FOUND
bun a_static.mjs (unhandled static import)                    # printed line still: Cannot find package 'x' from '/path'
process.versions.icu                                          # 78.3
createCipheriv("aes-128-gcm", key, Buffer.alloc(129))         # ERR_CRYPTO_INVALID_IV
DecompressionStream of a 1 MiB gzip member                    # 16 chunks of 65536 bytes
new SQL("sqlite://:memory:") with ${[1,2]} / ${new Date()}    # Binding expected ...
fs.mkdtempSync("")                                            # EINVAL
vm.runInThisContext("1", [])                                  # ERR_INVALID_ARG_TYPE
NODE_COMPILE_CACHE=/tmp/cc bun cc.cjs                         # creates /tmp/cc/v1.4.0-x86_64-<sha>-<uid>
NODE_DISABLE_COMPILE_CACHE=1 + enableCompileCache()           # status 3 (DISABLED)
bun dedupe / bun up with package.json scripts of those names  # built-in command runs
bun feedback                                                  # Script not found "feedback"
Bun.build({ splitting: true, format: "cjs" })                 # Code splitting is currently only supported ...
bun build of a function-scoped enum and import * as ns         # let Color; exports a, m, z
new SQL({ url: "postgres://...", tls: true }) on a non-TLS server  # ERR_POSTGRES_TLS_NOT_AVAILABLE
Bun.TOML.parse("a = 9007199254740993")                        # Integer cannot be losslessly represented ...
```

</details>

<details>
<summary>Previous revision</summary>

The first revision of this PR (`3c5611454a`) also rewrote the package
manager section for #38333 / #38853 (nested overrides and
`lockfileVersion: 3`, the optional-peer correction, `bun update`,
`bunfig.toml` over `.npmrc`, `--filter`) and fixed the TOML date and
`bun init` lines. #36463 picked those up in its own commits the same
day, so this PR was rebased onto its new head and reduced to the items
above.

</details>

<!-- robobun:evidence:begin -->

---

**no test proof** · iteration 0 · docs-only change; test-proof not
applicable

<!-- robobun:evidence:end -->
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.

2 participants