Repository navigation
Conversation
A Bun.udpSocket reply server (the shape the docs show, no error handler) or a node:dgram echo server with no 'error' listener dies with an uncaught ECONNREFUSED the first time a client sends a datagram and disappears before the reply arrives. The reply hits a closed port, the kernel answers with ICMP port unreachable, and because Bun enables IP_RECVERR on every UDP socket that error is queued and then routed to uncaught_exception. The same oversize send that already throws EMSGSIZE synchronously is also redelivered asynchronously: ip_local_error queues the errno with SO_EE_ORIGIN_LOCAL and the drain loop surfaces it a second time. Fix: - loop.c: factor the MSG_ERRQUEUE drain into a helper that also reads ee_origin, skips SO_EE_ORIGIN_LOCAL entries (already reported synchronously), and reads SO_ERROR afterwards to clear the mirrored sk_err so EPOLLERR stops level-triggering with an empty queue. - loop.c: when recvmmsg fails with a non-EAGAIN errno on Linux, drain the error queue first. If it has the entry, the recvmmsg errno was its sk_err mirror; surface the queue (is_errqueue=1) instead of a second un-flagged copy. This is the path a reply server hits when on_data sends to a port that closed between the datagram and the reply. - udp_socket.rs: drop is_errqueue=1 errors on unconnected sockets. Node never enables IP_RECVERR so Linux drops these in the kernel; doing it here means both Bun.udpSocket and node:dgram inherit Node's semantics. Connected sockets keep the error (Node surfaces that via recvmsg too). Tests: - reply server with no error handler survives a vanished client and answers the next one (Bun.udpSocket) - same for a node:dgram echo server with no 'error' listener - oversized send throws EMSGSIZE once and is not redelivered async - the connected-socket ECONNREFUSED test now connect()s (the previous unconnected variant no longer surfaces remote ICMP, by design)
|
Warning Review limit reached
Next review available in: 14 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (6)
Comment |
|
Updated 9:35 AM PT - Jul 26th, 2026
❌ @robobun, your commit 70b5e14 has 1 failures in
Add 🧪 To try this PR locally: bunx bun-pr 35923That installs a local version of the PR into your bun-35923 --bun |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Not a duplicate of #35922. Same bug, different mechanism, and this PR covers two cases #35922 does not:
#29135 already exists for the toggle approach; this PR implements the dispatch-layer fix (read |
|
All test lanes green. The only hard failure is Ready for review. |
…h a connected socket, match ASAN stderr convention - src/js/node/dgram.ts: remove the errqueue/unconnected filter now that on_recv_error drops those natively; the only reader is gone - src/runtime/socket/udp_socket.rs: stop attaching .errqueue to the JS error (its only reader was the dgram.ts filter above) - test/js/bun/udp/udp_socket.test.ts: the EMSGSIZE double-report test now uses a connected socket so the SO_EE_ORIGIN_LOCAL skip is the only clause that prevents the async redelivery; filter the ASAN banner from subprocess stderr to match the rest of the file - test/js/bun/udp/udp_socket_recv_flags.test.ts: retitle 'stays usable' to 'stays open' (Bun.udpSocket exposes no public reconnect to prove usability via a round-trip on the connected path; the reply-server test covers I/O-after-ICMP)
There was a problem hiding this comment.
All four nits from my earlier pass are addressed (ASAN stderr filter, dead dgram.ts guard removed, EMSGSIZE test now connected, recv_flags title matches its assertion). I didn't find anything further, but leaving this for a maintainer: it changes low-level loop.c UDP dispatch semantics and competes with #35922/#29135's toggle-IP_RECVERR approach — someone should pick one.
Checked: the factored us_internal_udp_drain_errqueue re-checks u->closed around on_recv_error (which enters JS) and clears SO_ERROR only when still open; the second call site from the recvmmsg-failure path correctly breaks to avoid the un-flagged double delivery; call_error_handler invokes the JS error callback with a single err arg, matching the new tests' handler shape.
Extended reasoning...
Overview
The PR fixes a Linux-specific crash where an unconnected Bun.udpSocket / node:dgram reply server dies with an uncaught ECONNREFUSED when a client vanishes before the reply arrives (ICMP port unreachable → IP_RECVERR errqueue → on_recv_error → uncaught_exception). It also fixes a double-report of EMSGSIZE (synchronous sendmsg failure + async SO_EE_ORIGIN_LOCAL errqueue redelivery). Files: packages/bun-usockets/src/loop.c (factor errqueue drain into a helper, read ee_origin, skip SO_EE_ORIGIN_LOCAL, clear mirrored sk_err via SO_ERROR, drain from the recvmmsg-failure path), src/runtime/socket/udp_socket.rs (drop errqueue errors on unconnected sockets, remove now-unread .errqueue property write), src/js/node/dgram.ts (delete the now-dead JS-side errqueue guard), plus three Linux-only tests.
Security risks
None identified. The change narrows what reaches user JS (drops remote-origin ICMP on unconnected sockets, skips local-origin entries) rather than widening it. Untrusted input here is kernel-generated cmsg data, walked via the standard CMSG_FIRSTHDR/CMSG_NXTHDR macros with a fixed 512-byte control buffer; sock_extended_err fields are read only after the cmsg_level/cmsg_type match.
Level of scrutiny
High. loop.c's us_internal_dispatch_ready_poll is the core UDP poll dispatch shared with QUIC and node:dgram; the change adds a second call into user JS (on_recv_error) from inside the recvmmsg loop and depends on Linux kernel semantics (sk_err mirroring, SO_EE_ORIGIN_LOCAL, level-triggered EPOLLERR) that are hard to verify by reading. The refactor is largely a hoist of the pre-existing drain loop, and the !u->closed guards around and after the JS re-entry look correct, but this is exactly the kind of platform-specific event-loop change a maintainer should sign off on.
Other factors
- Competing PRs. #35922 and #29135 solve the same headline bug by toggling
IP_RECVERRon connect/disconnect instead of filtering at dispatch. The author's comparison table argues this PR additionally fixes the connected-socketEMSGSIZEdouble-report and thesk_err-mirror double-delivery that the toggle approach leaves. A maintainer should choose between the approaches (or merge this and close the others). - Prior review addressed. My four earlier inline nits were all applied in e83009c / e429ecd / 70b5e14: ASAN stderr filter on both new subprocess tests, dead
dgram.tserrqueue guard and its.errqueuewriter removed, EMSGSIZE test switched to a connected socket soSO_EE_ORIGIN_LOCALis the load-bearing clause, recv_flags test retitled to match its weakened assertion (Bun.udpSockethas no publicdisconnect(), so the round-trip couldn't be restored there — reasonable). - Test coverage. Three new Linux-only subprocess tests each pin one clause of the fix; the author reports fail-before / pass-after on all three plus green on the full UDP, dgram-parallel, and HTTP/3 suites. CI is green except an unrelated binary-size baseline job.
- Comment-cop bot. Fired three times on the
on_recv_errordoc comment; the author folded the rationale into the pre-existing function comment at the same length asmain. That resolution looks fine — it's Node-compat semantics documentation, not a workaround justification.
|
Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-26, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main. |
Repro
A docs-shape
Bun.udpSocketreply server, no error handler:On
mainthe process dies withECONNREFUSED errno: -111as soon as the reply hits the closed port and the kernel answers with ICMP port unreachable. Node survives. The same happens to anode:dgramecho server with no'error'listener.Separately, a
send()that already throwsEMSGSIZEsynchronously is redelivered a second time via the error queue, so a caught oversize send still kills the process.Cause
Bun enables
IP_RECVERRon every UDP socket so Linux queues the ICMP instead of dropping it. Three delivery paths turn that into an uncaught:loop.cdrainsMSG_ERRQUEUEand callson_recv_error(.., is_errqueue=1);udp_socket.rsroutes that touncaught_exceptionwhen there is no error handler.sk_err, so the nextrecvmmsgin the same dispatch (right afteron_datasent the reply) fails with the same errno. That reacheson_recv_error(.., is_errqueue=0), whichnode:dgram's errqueue filter cannot drop, so'error'is emitted on a socket with no listener.ip_local_error()queues theEMSGSIZEfrom the failingsendmsgwithSO_EE_ORIGIN_LOCAL; the drain loop never readee_origin, so it redelivered an errno the syscall had already returned.Fix
packages/bun-usockets/src/loop.c: factor the errqueue drain into a helper that readsee_origin, skipsSO_EE_ORIGIN_LOCALentries, and readsSO_ERRORafterwards to clear the mirroredsk_err. Whenrecvmmsgfails with a non-EAGAINerrno on Linux, drain the error queue first; if it has the entry, surface that (flagged) instead of a second un-flagged copy.src/runtime/socket/udp_socket.rs: dropis_errqueue=1errors on unconnected sockets. Node never enablesIP_RECVERR, so the kernel drops these before they reachsk_err; dropping them here givesBun.udpSocketandnode:dgramthe same semantics. Connected sockets keep the error.Verification
New tests (all Linux-only,
IP_RECVERRis Linux-specific), each fail onmainand pass here, 5/5 runs:test/js/bun/udp/udp_socket.test.ts: reply server with no error handler survives a vanished client and answers the next one; oversizedsend()throwsEMSGSIZEonce and is not redeliveredtest/js/bun/udp/dgram.test.ts:node:dgramecho server with no'error'listener survives a vanished clienttest/js/bun/udp/udp_socket_recv_flags.test.tsnow uses a connected socket to exercise the ECONNREFUSED path (unconnected sockets intentionally no longer surface remote ICMP).262/262 in
test/js/bun/udp/, 75/75test/js/node/test/parallel/test-dgram-*.js, 52/52 infetch-http3-client.test.ts(shares the UDP poll path).Related: #29135 took the alternative of toggling
IP_RECVERRon connect/disconnect; this PR keepsIP_RECVERRon and filters at dispatch instead, so thesk_errmirror on a reply server's ownrecvmmsgloop is handled andSO_EE_ORIGIN_LOCALis distinguished.no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/udp/dgram.test.ts test/js/bun/udp/udp_socket.test.ts test/js/bun/udp/udp_socket_recv_flags.test.ts