Skip to content

usockets: follow the whole adopted-socket chain when re-deriving the live socket - #37661

Merged
Jarred-Sumner merged 1 commit into
mainfrom
farm/c878be7c/adopted-socket-chain
Aug 12, 2026
Merged

usockets: follow the whole adopted-socket chain when re-deriving the live socket#37661
Jarred-Sumner merged 1 commit into
mainfrom
farm/c878be7c/adopted-socket-chain

Conversation

@robobun

@robobun robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

When us_socket_adopt has to grow a socket's ext it goes through us_poll_resize, which allocates a new us_socket_t, copies the old one over, and retires the old block: is_closed = 1, flags.adopted = 1, prev = <replacement>, pushed onto loop->data.closed_head (context.c, us_socket_adopt). The dispatcher in loop.c is usually holding the old pointer while this happens (the adoption runs inside on_open, on_writable or on_data), so after each of those callbacks it re-derives the live socket from that bookkeeping. Until now every one of those sites followed exactly one link:

if (s && s->flags.adopted && s->prev) {
    s = s->prev;
}

That is only right if the socket was relocated once per callback. Every relocation retires its source block the same way, so a callback that adopts twice before returning (old -> mid -> new, with mid also flagged and pointing at new) leaves one hop parked on mid, which is itself retired. The is_closed checks that follow then give up on the rest of the event for a socket that is still live: the readable half of a combined readable+writable event, the rest of the recv drain loop and the eof/error handling behind it (loop.c), the deferred-accept readable dispatch after on_open, and on Windows the paused-socket probe and fin_deferred bookkeeping in poll_cb (libuv.c). The kernel re-reports these level-triggered conditions on the next tick, so the result is a dropped event tail rather than a crash, but the bookkeeping is only correct because there happens to be a single link.

The fix replaces the five copies of the one-hop if with one helper in internal/internal.h, us_internal_socket_follow_adopted, which walks to the end of the chain. loop.c uses it at the four dispatch sites, and the libuv.c us_internal_poll_cb_adopted_socket helper that poll_cb already funnels through now calls it too. Nothing else reads flags.adopted, so these are all the readers.

Why walking the chain is sound:

  • Every block on the chain is still allocated while a dispatch holds it. Retired blocks sit on closed_head and are only freed by us_internal_free_closed_sockets, which runs in the outermost tick's us_internal_loop_post, never from inside a dispatch (see the tick_depth check in loop.c). For the same reason a retired block's memory cannot be handed out as a new socket within the tick.
  • The walk terminates on the live block. us_poll_resize copies the source before us_socket_adopt flags it, so the copy, and therefore the live tail, always has adopted clear. adopted is set in exactly one place and always together with prev, so every link the walk follows is a forwarding link.

When adopted is clear, which is every socket today, the while evaluates its condition once, which is the same work the if did. The NULL tolerance of the old checks is kept because on_writable and on_data may return NULL.

Reachability and testing

This PR has no test because the code it changes cannot currently execute. No in-tree adopter grows its ext: upgradeTLS, the Postgres and MySQL TLS upgrades (adopt_tls) and the WebSocket client (adopt_group) pass equal pointer-sized old/new ext sizes, and the uWS server WebSocket upgrade shrinks (checked against this tree's headers: sizeof(WebSocketData) + sizeof(void *) is 168 vs sizeof(HttpResponseData<SSL>) at 232), so us_poll_resize always takes its early return and flags.adopted is never set. The redirection itself dates from #25361; #37098 (open, same grow path) reaches the same conclusion and adds a fault-injection hook so that a single relocation can be exercised. A two-link chain additionally needs two adoptions inside one callback, which nothing JS-reachable does even with that hook. There is therefore no input on which the old and new code behave differently, and any test added here would pass with or without the change, so none is included. The change adds no state and touches no data structures.

It also does not touch context.c or epoll_kqueue.c, so it is independent of #37098; if that lands first, its tests run the helper's one-link case for real.

How did you verify your code works?

  • The debug (ASAN) build compiles, including the C++ translation units that include internal.h (AsyncSocket.h, libuwsockets.cpp, bindings.cpp). libuv.c is Windows-only and its change is a one-line body replacement behind the same signature; the Windows CI lanes compile it.
  • bun bd test test/js/bun/net/socket.test.ts and test/js/bun/websocket/websocket-server.test.ts cover the in-tree adopters (upgradeTLS, server WebSocket upgrade). The only failures are the ones this sandbox also produces with the released binary: localhost resolving to ::1 makes a handful of Bun.connect tests fail with ECONNREFUSED, one test needs public DNS, and the 300k-message (benchmark) case exceeds its 30s budget under ASAN. Everything else passes.

…live socket

us_socket_adopt retires the old block of a relocated socket with adopted set
and prev pointing at its replacement. The dispatcher re-derived the live
socket after each callback by following exactly one such link, so a callback
that relocated the socket more than once left the dispatcher on a block that
was itself retired. Put the walk in one helper (internal.h) and use it at the
four loop.c sites and the libuv.c poll_cb helper.
@coderabbitai

coderabbitai Bot commented Aug 11, 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: 13 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: 30c0dd57-9bd9-445b-b4c5-18f51233d7c1

📥 Commits

Reviewing files that changed from the base of the PR and between 0e5d9df and 6a8e47d.

📒 Files selected for processing (3)
  • packages/bun-usockets/src/eventing/libuv.c
  • packages/bun-usockets/src/internal/internal.h
  • packages/bun-usockets/src/loop.c

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

@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 4:16 PM PT - Aug 11th, 2026

@robobun, your commit 6a8e47d has 1 failures in Build #92546 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 37661

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

bun-37661 --bun

@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Status: merged as aa9a59f. Bookkeeping-only change in a path no in-tree caller reaches today (see the reachability section in the description), so no failing-before test was possible; verified by building (debug/ASAN) and running the upgradeTLS and server WebSocket upgrade suites. The one red CI lane on build 92546 was test/bake/deinitialization.test.ts on Windows 2019 x64, the known intermittent deinit crash tracked in #34850, unrelated to this change.

@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 — mechanical dedup of five identical one-hop if checks into a shared static inline helper that walks the whole chain.

What was reviewed:

  • Confirmed all readers of flags.adopted are covered (grep shows only the new helper reads it; context.c/loop.c/socket.c sites are writers).
  • Verified us_socket_adopt sets adopted=1 together with prev=new_s and pushes to closed_head, and that us_internal_free_closed_sockets is gated on tick_depth <= 1, so the walk reads live memory mid-dispatch.
  • Helper is placed after the us_socket_t definition and is static inline, so it compiles in the C++ TUs that include internal.h; NULL tolerance matches the old checks.
Extended reasoning...

Overview

Replaces five copies of if (s && s->flags.adopted && s->prev) s = s->prev; (four in loop.c, one in libuv.c) with a single static inline helper in internal/internal.h that uses while instead of if, so a multi-hop adoption chain is followed to its live tail rather than stopping after one link.

Security risks

None. No user input reaches this code path differently than before; the helper reads two flag bits and a pointer on structures the dispatcher already holds. No new allocation, no new syscalls, no external data.

Level of scrutiny

This is core socket event-dispatch code, so I looked closely at the one semantic change (ifwhile). For every currently-reachable input the loop body executes at most once — the PR demonstrates that no in-tree adopter grows its ext, so us_poll_resize early-returns and flags.adopted is never set — making the change behaviorally identical to the old code today. The multi-hop case is a defensive correctness fix for a latent issue, and the termination/liveness argument (retired blocks freed only in the outermost loop_post; the live tail's adopted bit is clear because us_poll_resize copies before us_socket_adopt flags) checks out against context.c and loop.c.

Other factors

  • No CODEOWNERS entry covers packages/bun-usockets/.
  • The deduplication into a shared helper is exactly what the repo's review guidelines call for when the same block appears at multiple sites, and moving the guard into a shared helper is the preferred fix shape.
  • No test is included; the PR's justification (no input distinguishes old vs new behavior, so any test would pass with or without the change) is consistent with the repo rule against tests that cannot fail.
  • The libuv.c change is a one-line body swap behind an unchanged static signature, so Windows call sites are unaffected.

@Jarred-Sumner
Jarred-Sumner merged commit aa9a59f into main Aug 12, 2026
51 of 52 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/c878be7c/adopted-socket-chain branch August 12, 2026 00:27
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