websocket client: remove the remaining unsafe from the module

Merged
JA
3mo ago
+156-252

What

Follow-up to #40002. Takes the whole websocket client module — websocket_client.rs, WebSocketUpgradeClient.rs, WebSocketProxyTunnel.rs, WebSocketProxy.rs, CppWebSocket.rs — from 55 / 71 / 44 / 1 / 9 unsafe sites to zero (the one remaining unsafe keyword is the unsafe extern "C" { block declaring the C++ imports, all of which are now safe fn). Rather than annotate call sites, this fixes the layers underneath:

  • Ownership. The refs held on behalf of the socket's userdata pointer and of C++'s m_upgradeClient/m_connectedWebSocket are RefPtr<Self> fields, released with RefPtr::deref; no more hand-paired Self::deref(ptr). Custom deinit destructors become Drop.
  • Back-references. Tunnel ↔ client links are BackRef<_, Mut> built from the dispatch-time ThisPtr and cleared by the pointee before it can be freed; the tunnel dispatches into either transport via UpgradeClientRef. SslWrapper handlers are typed ThisPtr<WebSocketProxyTunnel> (the wrapper was already generic over its ctx).
  • C++ boundary. Every Bun__WebSocket* entry point is a plain HOST_EXPORT fn; generate-host-exports.ts now scans src/http_jsc, and learns &[T] (C passes ptr, len) and ThisPtr<T> params. C++ passes the header arrays as (ptr, len) pairs, the handshake-overflow bytes as one opaque InitialData box, the custom SSL_CTX as Option<OwnedSslCtx>, and setConnectedWebSocket passes nullptr once the open handler has closed the client. The WebSocket__* imports are safe fns over a new by-value bun_core::ffi::FfiSlice<'a, T> ({ptr,len} with the borrow's lifetime).
  • Initial-data microtask. JSGlobalObject::queue_microtask_boxed<T: MicrotaskCallback> owns the trampoline; the task and the client hold detachable back-references to each other, and the C++ pending-activity ref is still released at VM shutdown if the queue never drains.
  • Small safe accessors replacing per-site raw handling: Socket::ssl_mut() (and ssl() no longer returns the connecting-socket sentinel), Socket::take_ext_owner, SSL::servername(), VirtualMachine::{default_client_ssl_ctx, ssl_ctx_cache_get_or_create} (the RuntimeHooks entries are now safe fns returning OwnedSslCtx), RefPtr::from_this, BackRef<_, Mut>::this_ptr.

Also from the earlier commit on this branch: WebSocketProxyTunnel::write/shutdown hold a ref across the SslWrapper call (its on_close can release the caller's ref mid-call).

Testing

Debug+ASAN: all 22 test/js/web/websocket/*.test.ts files (178 tests incl. proxy, proxy re-entrancy/leak, deflate, autobahn) pass; hand-driven ws/wss echo, wss verify-rejection, close-during-connect/handshake, non-101; BUN_DEBUG_alloc shows 50/50 new/destroy for both HTTPClient and WebSocket over 50 connections.