Skip to content

fix(ext/node): support abstract Unix sockets in node:net pipe bind - #33872

Merged
bartlomieju merged 1 commit into
mainfrom
fix/abstract-unix-socket
May 6, 2026
Merged

bartlomieju merged 1 commit into
mainfrom
fix/abstract-unix-socket

Conversation

@bartlomieju

Copy link
Copy Markdown
Member

Summary

Fixes #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:

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.

Test plan

  • Verified EINVAL no longer occurs with abstract socket paths (previously reproducible on all platforms)
  • Verified filesystem socket paths still work correctly
  • Verified readableAll/writableAll throws on abstract socket paths
  • Verified Nuxt 4 dev server starts and serves HTTP 200 with the fix
  • Enabled two Node.js compat tests (test-pipe-abstract-socket.js, test-pipe-abstract-socket-http.js) β€” both are Linux-only (self-skip on other platforms)

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 fibibot 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. 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 skips remove_file for abstract paths (no filesystem entry to clean up). βœ“
  • pipe_wrap.rs:fchmod (line 449-453): early-returns 0 for abstract paths (chmod not applicable). βœ“
  • net.ts readableAll/writableAll validation: throws ERR_INVALID_ARG_VALUE when 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.

@bartlomieju
bartlomieju merged commit df92ffe into main May 6, 2026
136 checks passed
@bartlomieju
bartlomieju deleted the fix/abstract-unix-socket branch May 6, 2026 18:52
littledivy pushed a commit to crowlKats/deno that referenced this pull request Jun 10, 2026
…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`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deno 2.7.13 introduces EINVAL: invalid argument nitro-worker-XXX.sock with Nuxt 4

2 participants