WebSocket client: throw TypeError for detached buffers in send/ping/pong

Closed
RO
1mo ago
+130-19

Problem

  • ws.send(view), ws.ping(view) and ws.pong(view) abort the debug build when the buffer behind view is detached: ASSERTION FAILED: m_ptr in RefPtr<JSC::ArrayBuffer>::operator*(), SIGABRT. The release build sends a frame with an empty payload. Node throws TypeError.
  • The cause is arrayBufferView.unsharedBuffer() in WebSocket::send/ping/pong(ArrayBufferView&) (src/jsc/bindings/webcore/WebSocket.cpp). It returns a null RefPtr for a detached view, and its RELEASE_ASSERT(!result->isShared()) dereferences that pointer.

Fix

  • The six binary overloads (send, ping, pong, each for ArrayBuffer& and ArrayBufferView&) check isDetached() and return TypeError. No frame goes on the wire and the socket stays open.
  • The view overloads read the bytes with arrayBufferView.span(). span() includes the byte offset and does not touch the buffer pointer. The 125-byte control frame check from #35030 stays.
  • The check runs after the CONNECTING check and before the CLOSING/CLOSED branch. On a closing or closed socket Bun now throws, and Node v26.3.0 returns without an error. In the other states Bun matches Node.
  • Verified: test/js/web/websocket/websocket-client.test.ts. Without the fix the debug build aborts with the assertion, and the release build fails 6 of 7 new tests. With the fix all 41 tests in the file pass.

Background

  • A transfer, for example postMessage(buf, [buf.buffer]), detaches an ArrayBuffer. The buffer then has no memory, and a view made before the transfer reports length 0.
  • ArrayBufferView::possiblySharedBuffer() returns nullptr for a detached view. unsharedBuffer() calls it and asserts on the result with no null check.
  • The argument converter already rejects a view over a SharedArrayBuffer, so unsharedBuffer() added no protection here.
Notes

Repro:

const ab = new ArrayBuffer(8);
const view = new Uint8Array(ab).fill(0x41);
structuredClone(ab, { transfer: [ab] }); // detach
ws.send(view); // debug: SIGABRT. release: sends op2 len0.

Behaviour for a detached view or a detached ArrayBuffer, per readyState:

readyStateBun beforeBun with this PRNode v26.3.0
CONNECTINGInvalidStateErrorInvalidStateErrorInvalidStateError
OPENview: abort (debug) or empty frame (release). ArrayBuffer: empty frameTypeErrorTypeError
CLOSING, CLOSEDno errorTypeErrorno error

The last row is the only behaviour change outside the crash. To match Node there too, move the isDetached() check below the CLOSING/CLOSED branch in the six overloads.

Test output:

bun bd test, src/ from main (without the fix):
ASSERTION FAILED: m_ptr
wtf/RefPtr.h(126) : T &WTF::RefPtr<JSC::ArrayBuffer>::operator*() const
SIGABRT (exit 134)

release build without the fix:
6 fail (Received function did not throw), 1 pass

bun bd test with the fix:
41 pass, 0 fail (full file)

History of this PR:

  • The first version read the pointer with baseAddress(). Review asked for span().
  • #35030 landed first and added the 125-byte check to the same ping() and pong() overloads. The rebase keeps that check and applies it to bytes.size().

Other suites: websocket-blob.test.ts passes. In websocket.test.js, three tests need the external host wss://ws.postman-echo.com. They fail the same way without this change.


[review] gate passed · iteration 1 · 2 files touched

fails on main (without fix)
ASAN without fix: BUILD FAILED (no junit output)
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/websocket/websocket-client.test.ts
bun test v1.4.0 (9dc3aef7b)

test/js/web/websocket/websocket-client.test.ts:
Listening: ws://127.0.0.1:36285/
(pass) WebSocket > url [8.10ms]
Received client error: Error: read ECONNRESET
    at TCP.onStreamRead (node:internal/stream_base_commons:216:20) {
  errno: -104,
  code: 'ECONNRESET',
  syscall: 'read'
}
(pass) WebSocket > binaryType > (default) [2.46ms]
Received client error: Error: read ECONNRESET
    at TCP.onStreamRead (node:internal/stream_base_commons:216:20) {
  errno: -104,
  code: 'ECONNRESET',
  syscall: 'read'
}
(pass) WebSocket > binaryType > (invalid) [3.64ms]
Received client error: Error: read ECONNRESET
    at TCP.onStreamRead (node:internal/stream_base_commons:216:20) {
  errno: -104,
  code: 'ECONNRESET',
  syscall: 'read'
}
Received upgrade: {
  host: '127.0.0.1:36285',
  connection: 'Upgrade',
  upgrade: 'websocket',
  'sec-websocket-version': '13',
  'sec-websocket-extensions': 'permessage-deflate; client_max_window_bits',
  'sec-websocket-key': 'AbW+l1VvQlSv69g
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (82f26dace)

test/js/web/websocket/websocket-client.test.ts:
Listening: ws://127.0.0.1:41729/
Received client error: Error: read ECONNRESET
    at TCP.onStreamRead (node:internal/stream_base_commons:216:20) {
  errno: -104,
  code: 'ECONNRESET',
  syscall: 'read'
}
(pass) WebSocket > url [8.42ms]
(pass) WebSocket > binaryType > (default) [0.13ms]
Received client error: Error: read ECONNRESET
    at TCP.onStreamRead (node:internal/stream_base_commons:216:20) {
  errno: -104,
  code: 'ECONNRESET',
  syscall: 'read'
}
(pass) WebSocket > binaryType > (invalid) [0.17ms]
Received client error: Error: read ECONNRESET
    at TCP.onStreamRead (node:internal/stream_base_commons:216:20) {
  errno: -104,
  code: 'ECONNRESET',
  syscall: 'read'
}
Received upgrade: {
  host: '127.0.0.1:41729',
  connection: 'Upgrade',
  upgrade: 'websocket',
  'sec-websocket-version': '13',
  'sec-websocket-extensions': 'permessage-deflate; client_max_window_bits',
  'sec-websocket-key': 'a0VHLIE+TkWRIDmGR7IYlA=='
}
Received connection: 127.0.0.1
Received upgrade: {
  host: '127.0.0.1:41729',
  connection: 'Upgrade',
  upgrade: 'websocket',
  'sec-websocket-version': '13'
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/websocket/websocket-client.test.ts
bun test v1.4.0 (9dc3aef7b)

test/js/web/websocket/websocket-client.test.ts:
Listening: ws://127.0.0.1:35583/
(pass) WebSocket > url [7.26ms]
Received client error: Error: read ECONNRESET
    at TCP.onStreamRead (node:internal/stream_base_commons:216:20) {
  errno: -104,
  code: 'ECONNRESET',
  syscall: 'read'
}
Received client error: Error: read ECONNRESET
    at TCP.onStreamRead (node:internal/stream_base_commons:216:20) {
  errno: -104,
  code: 'ECONNRESET',
  syscall: 'read'
}
(pass) WebSocket > binaryType > (default) [2.11ms]
Received client error: Error: read ECONNRESET
    at TCP.onStreamRead (node:internal/stream_base_commons:216:20) {
  errno: -104,
  code: 'ECONNRESET',
  syscall: 'read'
}
(pass) WebSocket > binaryType > (invalid) [3.56ms]
Received upgrade: {
  host: '127.0.0.1:35583',
  connection: 'Upgrade',
  upgrade: 'websocket',
  'sec-websocket-version': '13',
  'sec-websocket-extensions': 'permessage-deflate; client_max_window_bits',
  'sec-websocket-key': 'aSXJ1GssSC+4gkf
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 745ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/8] gen cpp.rs (cppbind)
[2/8] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 237 extern-C blocks audited
[2/8] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

^[[1m^[[92m   Compiling^[[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
^[[1m^[[92m   Compiling^[[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
^[[1m^[[92m   Compiling^[[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
^[[1m^[[92m   Compiling^[[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
^[[1m^[[92m   Compiling^[[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
^[[1m^[[92m   Compiling^[[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
^[[1m^[[92m   Compiling^[[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
^[[1m^[[92m   Compiling^[[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
^[[1m^[[92m   Compiling^[[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
^[[1m^[[92m  
... (truncated)
diff hotspot
src/jsc/bindings/webcore/WebSocket.cpp         |  33 ++++----
 test/js/web/websocket/websocket-client.test.ts | 108 +++++++++++++++++++++++++
 2 files changed, 126 insertions(+), 15 deletions(-)

gate history · 1 passed · 0 rejected · iteration 1

evidence per changed file
file                                            reads  edits  tests
src/jsc/bindings/webcore/WebSocket.cpp              2      8      0
test/js/web/websocket/websocket-client.test.ts      3      5      0