Repository navigation
fix(ext/node): support abstract Unix sockets in node:net pipe bind - #33872
Conversation
Fixes #33759. `CString::new()` in `uv_pipe_bind` rejected paths containing NUL bytes, but abstract Unix sockets (Linux) require a `\0` prefix. This broke Nuxt 4/Nitro which uses `\0socketname` on Linux with Node >= 20. Extract a `make_sockaddr_un()` helper that handles both abstract sockets (raw bytes, no NUL terminator) and filesystem sockets (NUL-terminated, interior NULs rejected). Apply the same fix to `uv_pipe_connect`. Also: skip `remove_file` on close for abstract sockets (no filesystem entry), no-op `fchmod` for abstract sockets, and add the `readableAll`/`writableAll` validation from Node.js for abstract socket paths. Enables `test-pipe-abstract-socket.js` and `test-pipe-abstract-socket-http.js` node compat tests.
fibibot
left a comment
There was a problem hiding this comment.
LGTM. Real-world Nuxt 4 / Nitro fix β CString::new() was rejecting the leading-NUL paths that Linux uses for abstract Unix sockets. The walkthrough in the PR body (bind β CString::new fails β UV_EINVAL β listen finds internal_fd=None β "listen EINVAL") lines up exactly with what's in the diff.
What I verified
make_sockaddr_un helper (libs/core/uv_compat/pipe.rs:355-407): clean shape that distinguishes the two cases by leading-byte check:
- Abstract: copies bytes directly into
sun_path, no NUL terminator.addr_len = sizeof(sa_family_t) + path.len(). β matches Linux's expected layout. - Filesystem: rejects interior NULs (correct β they'd truncate), NUL-terminates,
addr_len = sizeof(sa_family_t) + path.len() + 1. β
Both branches check path.len() > sun_path.len() (108 on Linux) for overflow. Empty-path check up front returns UV_EINVAL. Edge cases (single \0, max-length abstract, max-length filesystem) all map correctly.
Applied consistently across the lifecycle:
uv_pipe_bind(line 431): uses the helper. βuv_pipe_connect(line 560): uses the helper. βclose_pipe(line 928):is_none_or(|&b| b != 0)correctly skipsremove_filefor abstract paths (no filesystem entry to clean up). βpipe_wrap.rs:fchmod(line 449-453): early-returns0for abstract paths (chmod not applicable). βnet.tsreadableAll/writableAllvalidation: throwsERR_INVALID_ARG_VALUEwhen used with abstract paths β matches Node's behavior. β
Test coverage: Two Node compat tests enabled (test-pipe-abstract-socket.js, test-pipe-abstract-socket-http.js) β both Linux-only, self-skip on macOS/Windows. The test-pipe-abstract-socket-http.js is the load-bearing one for Nuxt-style use cases (HTTP server over abstract socket).
Non-blocking nit
Minor typo in net.ts error message: "can not set readableAll or writableAllt to true..." β extra t after writableAll. Cosmetic, can land in a follow-up if you want to keep this PR's diff minimal.
CI
135 success, 1 skipping, 0 fail across all platforms Γ debug + release. The Linux-specific tests pass on Linux shards and skip cleanly on macOS/Windows.
β¦enoland#33872) Fixes denoland#33759. `CString::new()` in `uv_pipe_bind` rejected paths containing NUL bytes, but abstract Unix sockets (Linux) require a `\0` prefix in `sockaddr_un.sun_path`. This broke Nuxt 4 / Nitro, which uses `\0socketname` on Linux with Node >= 20. ### Root cause Nitro's `getSocketAddress()` on Linux: ```js if (process.platform === "linux") { const nodeMajor = Number.parseInt(process.versions.node.split(".")[0], 10); if (nodeMajor >= 20) { return `\0${socketName}`; // Abstract Unix socket } } ``` In `uv_pipe_bind`, `CString::new(path)` returns `Err(NulError)` for this path β mapped to `UV_EINVAL` β bind silently fails β `uv_pipe_listen` finds `internal_fd = None` β returns `UV_EINVAL` β surfaces as `"listen EINVAL: invalid argument"`. ### Changes - **`libs/core/uv_compat/pipe.rs`**: Extract `make_sockaddr_un()` helper that builds `sockaddr_un` correctly for both abstract sockets (raw bytes, no NUL terminator) and filesystem sockets (NUL-terminated, interior NULs rejected). Applied to both `uv_pipe_bind` and `uv_pipe_connect`. - **`libs/core/uv_compat/pipe.rs`** (`close_pipe`): Skip `remove_file` for abstract sockets (no filesystem entry). - **`ext/node/ops/pipe_wrap.rs`** (`fchmod`): No-op for abstract sockets (chmod doesn't apply). - **`ext/node/polyfills/net.ts`**: Add `readableAll`/`writableAll` validation for abstract socket paths (matching Node.js behavior). - **`tests/node_compat/config.jsonc`**: Enable `test-pipe-abstract-socket.js` and `test-pipe-abstract-socket-http.js`.
Summary
Fixes #33759.
CString::new()inuv_pipe_bindrejected paths containing NUL bytes, but abstract Unix sockets (Linux) require a\0prefix insockaddr_un.sun_path. This broke Nuxt 4 / Nitro, which uses\0socketnameon Linux with Node >= 20.Root cause
Nitro's
getSocketAddress()on Linux:In
uv_pipe_bind,CString::new(path)returnsErr(NulError)for this path β mapped toUV_EINVALβ bind silently fails βuv_pipe_listenfindsinternal_fd = Noneβ returnsUV_EINVALβ surfaces as"listen EINVAL: invalid argument".Changes
libs/core/uv_compat/pipe.rs: Extractmake_sockaddr_un()helper that buildssockaddr_uncorrectly for both abstract sockets (raw bytes, no NUL terminator) and filesystem sockets (NUL-terminated, interior NULs rejected). Applied to bothuv_pipe_bindanduv_pipe_connect.libs/core/uv_compat/pipe.rs(close_pipe): Skipremove_filefor abstract sockets (no filesystem entry).ext/node/ops/pipe_wrap.rs(fchmod): No-op for abstract sockets (chmod doesn't apply).ext/node/polyfills/net.ts: AddreadableAll/writableAllvalidation for abstract socket paths (matching Node.js behavior).tests/node_compat/config.jsonc: Enabletest-pipe-abstract-socket.jsandtest-pipe-abstract-socket-http.js.Test plan
readableAll/writableAllthrows on abstract socket pathstest-pipe-abstract-socket.js,test-pipe-abstract-socket-http.js) β both are Linux-only (self-skip on other platforms)