Skip to content

fix(dgram): enable SO_REUSEPORT on macOS and improve error reporting#27772

Open
robobun wants to merge 4 commits intomainfrom
claude/fix-dgram-reuseport-macos-27771
Open

fix(dgram): enable SO_REUSEPORT on macOS and improve error reporting#27772
robobun wants to merge 4 commits intomainfrom
claude/fix-dgram-reuseport-macos-27771

Conversation

@robobun
Copy link
Collaborator

@robobun robobun commented Mar 3, 2026

Summary

Fixes #27771.

  • Enable SO_REUSEPORT on macOS (and other BSDs): bsd_set_reuseport() was restricted to #if defined(__linux__) only, causing reusePort: true in node:dgram to always fail on macOS with ENOTSUP. macOS has supported SO_REUSEPORT since at least 10.12 (Sierra), and the reporter confirmed it works with raw C sockets. Changed the guard to #if defined(SO_REUSEPORT) && !defined(_WIN32) to enable it on all platforms that define the constant.
  • Fix missing errno propagation: When bsd_set_reuse() failed in bsd_create_udp_socket(), errno was not stored into *err, causing the Zig layer to fall through to a generic "Failed to bind socket" error instead of a descriptive one (e.g., "bind ENOTSUP 0.0.0.0:56780"). Now properly propagates the errno.
  • Fix socket fd leak: The failed bsd_set_reuse() path in bsd_create_udp_socket() was not calling bsd_close_socket() before returning, leaking the file descriptor.

Test plan

  • Existing test-dgram-reuseport.js passes on Linux
  • Existing node-dgram.test.js has no new failures
  • New regression test test/regression/issue/27771.test.ts passes
  • Repro script from the issue works correctly with both reusePort: false and reusePort: true
  • CI validates macOS behavior

🤖 Generated with Claude Code

…27771)

`bsd_set_reuseport()` was restricted to Linux only, causing `reusePort: true`
in `node:dgram` to fail on macOS with a generic "Failed to bind socket" error,
even though macOS natively supports SO_REUSEPORT. Now enabled on all platforms
that define SO_REUSEPORT. Also fixed missing errno propagation when
`bsd_set_reuse()` fails in UDP socket creation, and a socket fd leak in that
error path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions bot added the claude label Mar 3, 2026
@robobun
Copy link
Collaborator Author

robobun commented Mar 3, 2026

Updated 2:04 PM PT - Mar 3rd, 2026

@Jarred-Sumner, your commit 7774147 has 2 failures in Build #38595 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 27772

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

bun-27772 --bun

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4aab06d and eda1daf.

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

Walkthrough

Enable SO_REUSEPORT where available (not Linux-only), record reuse-related socket errors into the provided err (WSAGetLastError() on Windows, errno elsewhere), update enum comment about platform behavior, and add regression tests for UDP reusePort binding.

Changes

Cohort / File(s) Summary
Socket implementation
packages/bun-usockets/src/bsd.c
Gate SO_REUSEPORT usage on the macro being defined (removes Linux-only guard). When bsd_set_reuse(...) fails during UDP socket creation, record the platform error into *err (WSAGetLastError() on Windows, errno elsewhere) before closing socket and returning error.
Documentation / enum comment
packages/bun-usockets/src/libusockets.h
Update LIBUS_LISTEN_REUSE_PORT enum comment to list platforms that define SO_REUSEPORT (e.g., Linux, macOS, BSDs) and clarify Linux kernel-level load balancing vs macOS/BSD port sharing semantics.
Tests
test/regression/issue/27771.test.ts
Add regression tests exercising UDP reusePort behavior: helper to bind UDP4 sockets with reuseAddr and optional reusePort, test single-socket bind and two-sockets-binding-to-same-port scenarios; includes proper error handling and socket close awaiting.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main changes: enabling SO_REUSEPORT on macOS and improving error reporting for UDP socket operations.
Description check ✅ Passed The description comprehensively covers what the PR does, how it was verified, addresses the linked issue, and includes test results confirming the fix.
Linked Issues check ✅ Passed The code changes fully address the objectives from issue #27771: enabling SO_REUSEPORT on macOS/BSDs [#27771], fixing errno propagation for descriptive error messages [#27771], and fixing socket fd leak [#27771].
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the linked issue: C code modifications for SO_REUSEPORT guard and error handling, header comment documentation, and regression test implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/bun-usockets/src/bsd.c (1)

1246-1253: 🧹 Nitpick | 🔵 Trivial

Pre-existing issue: Socket leak and missing error on IPV6_V6ONLY failure.

This error path returns LIBUS_SOCKET_ERROR without closing the socket or setting *err, which is inconsistent with the new error handling pattern added above. This would leak the file descriptor.

Consider applying consistent error handling:

♻️ Suggested fix for consistency
 `#ifdef` IPV6_V6ONLY
     if (listenAddr->ai_family == AF_INET6) {
         int enabled = (options & LIBUS_SOCKET_IPV6_ONLY) != 0;
         if (setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, &enabled, sizeof(enabled)) != 0) {
+            if (err != NULL) {
+#ifdef _WIN32
+                *err = WSAGetLastError();
+#else
+                *err = errno;
+#endif
+            }
+            bsd_close_socket(listenFd);
+            freeaddrinfo(result);
             return LIBUS_SOCKET_ERROR;
         }
     }
 `#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bun-usockets/src/bsd.c` around lines 1246 - 1253, The IPV6_V6ONLY
error path leaks listenFd and doesn't set *err; update the failure branch inside
the IPV6_V6ONLY block (the setsockopt call that currently returns
LIBUS_SOCKET_ERROR) to close(listenFd), set *err to the appropriate errno (or
existing error variable used by this file), and then return LIBUS_SOCKET_ERROR
so it matches the error-handling pattern used above; reference the symbols
listenFd, listenAddr, options, LIBUS_SOCKET_IPV6_ONLY, and the setsockopt call.
🤖 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/27771.test.ts`:
- Around line 33-43: The duplicated socket-bind Promise should be replaced by
extending the existing bindSocket helper to accept an optional port parameter;
modify bindSocket (the helper that currently takes a boolean reusePort) to add a
second optional parameter port?: number and use that port when calling
socket.bind, preserving the current default behavior when port is undefined,
then update the test to call bindSocket(true, port) for the second socket
instead of inlining the Promise so both bindings reuse the same helper (ensure
references to createSocket, reuseAddr/reusePort options and the returned { ok,
error, socket } shape remain unchanged).

---

Outside diff comments:
In `@packages/bun-usockets/src/bsd.c`:
- Around line 1246-1253: The IPV6_V6ONLY error path leaks listenFd and doesn't
set *err; update the failure branch inside the IPV6_V6ONLY block (the setsockopt
call that currently returns LIBUS_SOCKET_ERROR) to close(listenFd), set *err to
the appropriate errno (or existing error variable used by this file), and then
return LIBUS_SOCKET_ERROR so it matches the error-handling pattern used above;
reference the symbols listenFd, listenAddr, options, LIBUS_SOCKET_IPV6_ONLY, and
the setsockopt call.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6c7e972 and 4aab06d.

📒 Files selected for processing (3)
  • packages/bun-usockets/src/bsd.c
  • packages/bun-usockets/src/libusockets.h
  • test/regression/issue/27771.test.ts

Claude Bot and others added 2 commits March 3, 2026 19:14
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add optional port parameter to bindSocket() helper instead of inlining
a duplicate Promise in the two-socket test case.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

The production changes look correct and well-structured, but since this is AI-generated C code in a low-level networking library, it warrants a quick human look -- particularly the SO_REUSEPORT platform guard change and the new error/cleanup path in bsd_create_udp_socket().

Extended reasoning...

Overview

This PR makes three changes to packages/bun-usockets/src/bsd.c and updates a comment in libusockets.h:

  1. Enables SO_REUSEPORT on macOS/BSDs by changing the preprocessor guard from #if defined(linux) to #if defined(SO_REUSEPORT) && !defined(_WIN32)
  2. Fixes a socket fd leak in bsd_create_udp_socket() when bsd_set_reuse() fails
  3. Fixes missing errno propagation in that same failure path
  4. Adds a new regression test for reusePort: true in dgram sockets

Security risks

No direct security risks. The changes improve resource cleanup (fixing an fd leak) and error reporting. The SO_REUSEPORT enablement on macOS does not introduce new attack surface -- it enables an OS-level feature that was already available on Linux.

Level of scrutiny

This is production-critical C code in the low-level socket layer (bun-usockets). While the changes are small and follow existing patterns in the codebase (the new cleanup block mirrors the bind() failure path 40 lines below), the PR is AI-generated (claude label) and has received no human review yet. C networking code deserves human verification even for straightforward changes, given the potential for subtle resource leaks or platform-specific behavior differences.

Other factors

  • The new cleanup block (errno propagation + bsd_close_socket + freeaddrinfo) exactly matches the pattern at the bind() failure path (lines 1285-1296), confirming correctness.
  • The SO_REUSEPORT behavioral difference between Linux (kernel-level load balancing) and macOS (port sharing without load balancing) is correctly documented in the updated comment.
  • Bug 1 (test nit): The new test lacks a Windows platform guard -- reusePort: true will fail on Windows since SO_REUSEPORT is unsupported there and LIBUS_LISTEN_DISALLOW_REUSE_PORT_FAILURE prevents silent fallback.
  • Bug 2 (pre-existing): The adjacent IPV6_V6ONLY failure path at line 1250 has the same fd/addrinfo leak pattern this PR fixes, but was not addressed. This is pre-existing and not a blocker for this PR.

Comment on lines +18 to +23
test("dgram reusePort: single socket binds successfully", async () => {
const result = await bindSocket(true);
expect(result.ok).toBe(true);
expect(result.error).toBeNull();
await new Promise<void>(r => result.socket.close(() => r()));
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Test test/regression/issue/27771.test.ts unconditionally expects reusePort: true to succeed, but Windows does not support SO_REUSEPORT. Since dgram.ts always sets LIBUS_LISTEN_DISALLOW_REUSE_PORT_FAILURE, the error won't be silently swallowed on Windows, causing both test cases to fail. Add test.skipIf(process.platform === "win32") or a runtime capability check similar to checkSupportReusePort() used by the existing test-dgram-reuseport.js.

Extended reasoning...

What the bug is

The new regression test at test/regression/issue/27771.test.ts tests reusePort: true functionality by calling bindSocket(true) and asserting expect(result.ok).toBe(true). However, Windows does not define SO_REUSEPORT, so these assertions will always fail on Windows.

The specific code path

When reusePort: true is passed to createSocket() in node:dgram, the JS layer in dgram.ts (line 302) always sets LIBUS_LISTEN_DISALLOW_REUSE_PORT_FAILURE in the options flags. This flows down to bsd_set_reuse() in bsd.c, which calls bsd_set_reuseport(). On Windows, bsd_set_reuseport() sets errno = ENOTSUP and returns -1. Back in bsd_set_reuse() (line 939-940), when errno == ENOTSUP is true, it checks whether LIBUS_LISTEN_DISALLOW_REUSE_PORT_FAILURE == 0 — but since that flag IS set, the condition is false, so the error propagates rather than being silently swallowed. The socket creation fails, the error event fires, and expect(result.ok).toBe(true) fails.

Why existing code doesn't prevent it

The test has no platform guard whatsoever. It runs unconditionally on all platforms. The existing Node.js compat test test/js/node/test/parallel/test-dgram-reuseport.js correctly handles this by using checkSupportReusePort() from test/js/node/test/common/udp.js, which probes whether the platform actually supports reusePort and skips the test if it does not.

Step-by-step proof on Windows

  1. Test calls bindSocket(true) which creates a socket with reusePort: true
  2. dgram.ts sets LIBUS_LISTEN_DISALLOW_REUSE_PORT_FAILURE flag (always)
  3. bsd_create_udp_socket() calls bsd_set_reuse(listenFd, options)
  4. bsd_set_reuse() calls bsd_set_reuseport(listenFd) since LIBUS_LISTEN_REUSE_PORT is set
  5. bsd_set_reuseport() on Windows hits the #else branch: sets WSASetLastError(WSAEOPNOTSUPP), errno = ENOTSUP, returns -1
  6. Back in bsd_set_reuse(): errno == ENOTSUP is true, but (options & LIBUS_LISTEN_DISALLOW_REUSE_PORT_FAILURE) == 0 is false (flag is set), so it falls through and returns the error
  7. bsd_create_udp_socket() stores errno into *err, closes the socket, and returns LIBUS_SOCKET_ERROR
  8. The error event fires on the JS socket, bindSocket() resolves with { ok: false, ... }
  9. expect(result.ok).toBe(true) fails — both test cases fail on Windows CI

Impact

This is a test-only issue — no production code is affected. However, it will cause Windows CI failures for both test cases in this file. The fix is straightforward: add test.skipIf(process.platform === "win32") to both test cases, or add a runtime capability check like the existing reusePort tests do.

Comment on lines 1233 to 1244
if (bsd_set_reuse(listenFd, options) != 0) {
if (err != NULL) {
#ifdef _WIN32
*err = WSAGetLastError();
#else
*err = errno;
#endif
}
bsd_close_socket(listenFd);
freeaddrinfo(result);
return LIBUS_SOCKET_ERROR;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🟣 Pre-existing bug: The IPV6_V6ONLY setsockopt failure path at line 1249-1250 has the same triple-bug pattern (fd leak, addrinfo leak, missing errno propagation) that this PR fixes in the bsd_set_reuse() path just 6 lines above. The bare return LIBUS_SOCKET_ERROR should be replaced with the same cleanup pattern used at lines 1233-1244 and at the bind() failure path at lines 1285-1296.

Extended reasoning...

What the bug is

In bsd_create_udp_socket(), when the setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, ...) call fails at line 1249, the function returns LIBUS_SOCKET_ERROR immediately without performing any cleanup. This means: (1) listenFd is leaked because bsd_close_socket() is never called, (2) the result addrinfo linked list is leaked because freeaddrinfo() is never called, and (3) *err is not set with the errno, so the caller gets no useful error information.

The specific code path

This occurs in bsd_create_udp_socket() at lines 1246-1253. When getaddrinfo() resolves to an IPv6 address (AF_INET6), the function creates a socket, successfully calls bsd_set_reuse(), then attempts to set IPV6_V6ONLY. If the setsockopt call fails (returns non-zero), the code at line 1250 does a bare return LIBUS_SOCKET_ERROR with no cleanup.

Why existing code doesn't prevent it

The PR correctly fixed the identical pattern in the bsd_set_reuse() failure path at lines 1233-1244, adding bsd_close_socket(listenFd), freeaddrinfo(result), and *err = errno. The bind() failure path at lines 1285-1296 also correctly does all three cleanup steps. However, the IPV6_V6ONLY path between these two was missed.

Step-by-step proof

  1. Call bsd_create_udp_socket("::1", 5000, LIBUS_SOCKET_IPV6_ONLY, &err) on a system where IPV6_V6ONLY setsockopt could fail.
  2. getaddrinfo resolves to an AF_INET6 address, so listenAddr->ai_family == AF_INET6 is true.
  3. bsd_create_socket() succeeds, allocating a file descriptor (e.g., fd=5). result holds the addrinfo allocation.
  4. bsd_set_reuse() succeeds (returns 0), so we proceed past the now-fixed cleanup block.
  5. At line 1249, setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, ...) fails and returns non-zero.
  6. Line 1250 executes: return LIBUS_SOCKET_ERROR; — fd 5 is leaked, result is leaked, err still holds 0.
  7. The caller has no way to close fd 5 or free result. The error code in *err is 0, giving no diagnostic information.

Impact

While the IPV6_V6ONLY setsockopt failure is uncommon in practice, when it does occur it causes a file descriptor leak and a memory leak per call. On a long-running server process, repeated failures could exhaust file descriptors. The missing errno propagation also makes debugging harder since the Zig layer would fall through to a generic error message.

How to fix

Apply the same cleanup pattern used in the bsd_set_reuse() and bind() failure paths. Replace the bare return LIBUS_SOCKET_ERROR at line 1250 with:

if (err != NULL) {
#ifdef _WIN32
    *err = WSAGetLastError();
#else
    *err = errno;
#endif
}
bsd_close_socket(listenFd);
freeaddrinfo(result);
return LIBUS_SOCKET_ERROR;

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.

node:dgram on macOS with UDP reusePort: true does not work, but Mac natively supports SO_REUSEPORT

2 participants