usockets: treat ENOBUFS/ENOMEM from send() as would-block, not fatal
us_socket_write_check_error classified every non-EWOULDBLOCK send() errno as a fatal write error. For transient kernel resource exhaustion (ENOBUFS/ENOMEM on POSIX, WSAENOBUFS on Windows) the connection is still healthy and a later retry can succeed, so the node:net drain path must keep the buffered bytes and re-arm the writable poll instead of dropping them.
Repro
Deterministic via the in-tree usockets fault injection (debug/ASan builds):
// net.Socket client: a short write leaves bytes in buffered_data_for_node_net,
// the drain-path flush (internal_flush) then hits ENOBUFS.
fault.set({ syscall: "send", action: "short", bytes: 1, repeat: 1, fd: clientFd });
client.write(head); // send #0: 1 byte accepted, 199 buffered
fault.set({ syscall: "send", action: "errno", errno: "ENOBUFS", repeat: 1, fd: clientFd });
// on_writable -> internal_flush -> send() -> ENOBUFS
Without the fix the peer receives head[0] followed by whatever is written next; the 199 buffered bytes are gone, no 'error' event, no failed write callback, no close. The short send that leaves data buffered is ordinary kernel backpressure on any real-network upload; a one-off ENOBUFS/ENOMEM from send(2) under memory pressure is ordinary kernel behaviour.
Cause
packages/bun-usockets/src/socket.c us_socket_write_check_error():
if (bsd_would_block()) { ...retry... } /* errno == EWOULDBLOCK only */
if (fatal_write_error) *fatal_write_error = 1; /* ENOBUFS/ENOMEM land here */
src/runtime/socket/socket_body.rs internal_flush() then clears buffered_data_for_node_net on fatal, and on_writable dispatches 'drain' regardless, so JS keeps writing on a connection that has silently lost a span of its byte stream.
Fix
Add bsd_send_is_transient_error() (ENOBUFS/ENOMEM on POSIX, WSAENOBUFS on Windows) and treat it the same as would-block in us_socket_write_check_error: set last_write_failed, re-arm the writable poll, return 0, do not set fatal_write_error. This matches libuv's uv__write, which retries on EAGAIN || EWOULDBLOCK || ENOBUFS.
EPIPE/ECONNRESET still reach the fatal branch; those are connection-fatal and surfaced on the read side. Making the write side itself fail those pending writes is the complementary fix in #33506.
Verification
New test.each(["ENOBUFS", "ENOMEM"]) case in test/js/node/net/net-syscall-fault.test.ts.
Without the fix (git stash -- packages/ && bun bd test ...):
{ intact: false, length: 4097 } // 1 head byte + 4096 body; 199 bytes dropped
With the fix:
{ intact: true, length: 4296 } // 200 head + 4096 body, byte-exact
The rest of net-syscall-fault.test.ts, tls-syscall-fault.test.ts, and socket-syscall-fault.test.ts pass unchanged.
no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/net/net-syscall-fault.test.ts