Skip to content

node:fs: name the operation's syscall in the ENAMETOOLONG for an over-long path - #40603

Open
robobun wants to merge 1 commit into
mainfrom
farm/67d844a9/fs-enametoolong-syscall
Open

robobun wants to merge 1 commit into
mainfrom
farm/67d844a9/fs-enametoolong-syscall

Conversation

@robobun

@robobun robobun commented Aug 27, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Every node:fs operation reports an over-long path (MAX_PATH_BYTES or more) as ENAMETOOLONG with err.syscall === undefined, and a message that says open for every operation: fs.stat(p, cb) calls back with ENAMETOOLONG: name too long, open '/aaa...'. Node names the syscall the operation issues (stat, scandir, lstat, ...), in err.syscall and in the message, like any other syscall error from that operation.
  • The cause is Valid::path_too_long (src/runtime/node/types.rs:1228). The argument parser raises this error before any syscall runs, so it did not know the operation. It built the error with Tag::open and blanked syscall. node:fs: reject the promise instead of throwing when a path is too long for any syscall #38383 moved the delivery to the callback and left this as a known gap.

Fix

  • ArgumentsSlice (src/jsc/CallFrame.rs) gains syscall: bun_sys::Tag, default open. The parser builds the ENAMETOOLONG from it, for the sync throw and for the deferred async rejection alike.
  • NodeFSFunctionEnum::syscall() (src/runtime/node/node_fs.rs) maps each operation to the tag its own errors carry: the same one its ENOENT names today (readdir is scandir, rm and the emulated realpath are lstat, utimes is utime). run_sync, run_async and parse_async_args set it before they parse. cp uses lstat, fs.watch uses watch, watchFile uses stat.
  • Correct because the error now reads exactly like the error the operation's first syscall would have returned for that path. All three forms (sync, callback, promise) agree. Seven Tag constants in bun_sys become pub so the runtime crate can name them.
  • Verified: test/js/node/fs/fs-path-length.test.ts (31 callback operations now check err.syscall and the message prefix, plus sync, promise and fs.watch cases; 33 fail on 1.4.1, all pass with the fix). Also fs.test.ts, cp.test.ts, promises.test.js, dir.test.ts, fs-mkdir.test.ts, fs.watch.test.ts, 34 ported test-fs-* files, clippy, and cargo check for the Windows and macOS targets.

Background

  • ArgumentsSlice is the cursor a native binding walks over a call's arguments. node:fs bindings set will_be_async on it so path strings are copied thread-safe, and node:fs: reject the promise instead of throwing when a path is too long for any syscall #38383 added deferred_error so a path errno met while parsing for an async call becomes a rejected promise instead of a throw.
  • bun_sys::Tag is the syscall name on a bun_sys::Error. to_system_error turns it into err.syscall and into the , <syscall> '<path>' part of the node-style message.
  • Every fs operation copies its path into a fixed PathBuffer right before the syscall, so the parser rejects a path of MAX_PATH_BYTES or more up front. The kernel's limit is the same (PATH_MAX), so the early ENAMETOOLONG is the error the syscall would have produced. Only its name was missing.
Notes

Node 26.3.0 and this branch, err.syscall for a 5001-byte path on Linux, callback form (identical for sync and promise forms):

access access | appendFile open | chmod chmod | chown chown | copyFile copyfile
cp lstat | lstat lstat | mkdir mkdir | mkdtemp mkdtemp | open open | opendir opendir
readdir scandir | readFile open | readlink readlink | realpath lstat
realpath.native realpath | rename rename | rm lstat | rmdir rmdir | stat stat
statfs statfs | symlink symlink | link link | unlink unlink | utimes utime
lutimes lutime | lchown lchown | writeFile open

Differences that remain, all pre-existing and out of scope here:

Other callers of the path parser (Bun.file, Bun.write, S3 keys, static routes) keep the default open: their message already said open, and err.syscall is now "open" instead of undefined.

The two S3 tests in test/js/bun/s3/s3.test.ts that fail in this container fail because its egress proxy denies the S3 host. They do not touch path parsing.


no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/fs/fs-path-length.test.ts

…-long path

The argument parser rejects a path of MAX_PATH_BYTES or more before any
syscall runs. It built that error with `Tag::open` and then blanked the
`syscall` field, so every operation reported `err.syscall === undefined`
and a message that said `open`. Node names the syscall the operation
issues: `stat`, `scandir`, `lstat` for rm and realpath, and so on.

`ArgumentsSlice` gains a `syscall` tag. Each fs binding sets it to the
syscall its operation issues (`NodeFSFunctionEnum::syscall`) before it
parses, and the parser builds the error from it. The sync, callback and
promise forms now all carry the same `syscall` and message as the
operation's other errors. `fs.watch` names `watch`, as in node.
@robobun

robobun commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced with fs.stat("/" + "a".repeat(5000), cb) on bun 1.4.1: the callback gets ENAMETOOLONG with err.syscall === undefined and a message that says open. Node names stat. The fix is in this PR. The test is test/js/node/fs/fs-path-length.test.ts (33 of its cases fail on 1.4.1 and pass with the fix).

@coderabbitai

coderabbitai Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 93ea88f0-b511-4e06-b1e1-9781daffc3a2

📥 Commits

Reviewing files that changed from the base of the PR and between 0e395c2 and 9c702a9.

📒 Files selected for processing (6)
  • src/jsc/CallFrame.rs
  • src/runtime/node/node_fs.rs
  • src/runtime/node/node_fs_binding.rs
  • src/runtime/node/types.rs
  • src/sys/lib.rs
  • test/js/node/fs/fs-path-length.test.ts

Included review availability: Your plan provides up to 5 included reviews per hour; 0 remain after this review.


Walkthrough

Changes

The filesystem bindings now attach operation-specific syscall tags to path-length errors. ENAMETOOLONG errors preserve the originating syscall across callback, synchronous, promise, and watch APIs. Tests cover syscall names, paths, codes, and messages.

Filesystem path error reporting

Layer / File(s) Summary
Syscall tag contract
src/jsc/CallFrame.rs, src/runtime/node/node_fs.rs, src/sys/lib.rs
ArgumentsSlice stores a syscall tag. NodeFSFunctionEnum::syscall() maps operations to tags. Several syscall constants are now public.
Syscall tag propagation
src/runtime/node/node_fs_binding.rs, src/runtime/node/types.rs
Filesystem bindings store operation tags during argument parsing. Path-length errors use the stored tag instead of always using open.
Path error validation
test/js/node/fs/fs-path-length.test.ts
Tests verify syscall metadata and error details for callback, synchronous, promise, and watch APIs.

Suggested reviewers: jarred-sumner, dylan-conway, cirospaciari

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: operation-specific syscall names for ENAMETOOLONG errors from over-long paths.
Description check ✅ Passed The description explains the problem, implementation, behavior, scope, known differences, and verification. It provides the information required by the template, although it uses Problem and Fix headi…
Full details: Description check

Explanation

The description explains the problem, implementation, behavior, scope, known differences, and verification. It provides the information required by the template, although it uses Problem and Fix headings instead of the template headings.


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 — focused Node compat fix threading the operation's syscall tag into the pre-syscall ENAMETOOLONG.

What was reviewed:

  • NodeFSFunctionEnum::syscall() match covers all 40 variants; mappings line up with the Node-verified table in the description (readdir→scandir, rm/realpath-emulated→lstat, utimes→utime, etc.).
  • ArgumentsSlice.syscall defaults to open, so non-fs callers of the path parser (from_bun_string at types.rs:1174, Bun.file/S3) keep their prior message and gain a defined err.syscall — no regression.
  • All entry points that parse fs paths set the tag before parsing: macro-generated sync/async, parse_async_args, and the hand-written cp/cpSync/readdirRecursive/watch/watchFile bindings.
  • Test matrix asserts err.syscall and message prefix across 31 callback ops plus sync/promises/watch, so it fails on the old blank-syscall behavior.
Extended reasoning...

Overview

This PR fixes a Node.js compatibility gap: when Bun's node:fs argument parser rejects an over-long path before any syscall runs, the resulting ENAMETOOLONG error previously had err.syscall === undefined and a message that always said open. The fix threads a bun_sys::Tag through ArgumentsSlice so the parser can name the syscall the operation would have issued. Six files change: ArgumentsSlice gains a syscall field defaulting to open; NodeFSFunctionEnum gains a const fn syscall() mapping each op to its representative tag; the sync/async binding entry points (macro-generated and hand-written) set the tag before parsing; Valid::path_too_long takes the tag instead of hardcoding open and blanking the field; seven bun_sys::Tag constants are widened to pub; and the test file is expanded from bare code/path checks to per-operation syscall and message-prefix assertions across callback, sync, promise, and watch forms.

Security risks

None. This change only affects which string appears in err.syscall and the error message for a path-length validation error that already existed. No new input is parsed, no new syscalls are issued, no allocation or lifetime changes, no user-controlled data flows anywhere new. Widening pub(crate) to pub on inert integer-newtype constants has no security implication.

Level of scrutiny

Low-to-moderate. This is a mechanical Node-compat improvement: threading a Copy enum value from the binding entry point to the error constructor. The mapping table is the only place a mistake could hide, and the PR description documents it was derived by running Node 26.3.0 against a 5001-byte path and matching each op's reported syscall — the same tag each op already uses for its ENOENT. The exhaustive match means a new NodeFSFunctionEnum variant will fail to compile until mapped. The ArgumentsSlice default of open preserves prior behavior for every caller that doesn't set it (Bun-native Bun.file, S3, static routes), which the PR description calls out explicitly.

Other factors

Test coverage is strong: the existing it.each over 31 callback operations now asserts err.syscall and the ENAMETOOLONG: name too long, <syscall> ' message prefix for each, and new cases cover statSync/readdirSync/realpathSync/realpathSync.native/cpSync/renameSync, fs.promises.readFile/readdir, and fs.watch. These assertions fail on the pre-fix behavior (undefined syscall), satisfying the fails-for-the-right-reason requirement. The known remaining divergences (truncate, fs.promises.realpath, opendir message shaping, watchFile throwing) are pre-existing, documented in the PR notes, and out of scope. No CODEOWNERS cover the changed paths, no prior review comments exist, and the bug hunt exited on dry_streak with no findings.

@robobun

robobun commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 10:12 PM PT - Aug 26th, 2026

✅ @robobun, your commit 9c702a9b80fe38db09b0e85ae2cb5b0261d3a0a1 passed in Build #106594! 🎉


🧪   To try this PR locally:

bunx bun-pr 40603

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

bun-40603 --bun

This branch has not been deployed

No deployments
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