WebSocket client: throw TypeError for detached buffers in send/ping/pong
Closed
RO
1mo ago+130+130+012345678901234567890123456789-19-19-01234567890123456789
Problem
ws.send(view),ws.ping(view)andws.pong(view)abort the debug build when the buffer behindviewis detached:ASSERTION FAILED: m_ptrinRefPtr<JSC::ArrayBuffer>::operator*(), SIGABRT. The release build sends a frame with an empty payload. Node throwsTypeError.- The cause is
arrayBufferView.unsharedBuffer()inWebSocket::send/ping/pong(ArrayBufferView&)(src/jsc/bindings/webcore/WebSocket.cpp). It returns a nullRefPtrfor a detached view, and itsRELEASE_ASSERT(!result->isShared())dereferences that pointer.
Fix
- The six binary overloads (
send,ping,pong, each forArrayBuffer&andArrayBufferView&) checkisDetached()and returnTypeError. 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
CONNECTINGcheck and before theCLOSING/CLOSEDbranch. 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 anArrayBuffer. The buffer then has no memory, and a view made before the transfer reports length 0. ArrayBufferView::possiblySharedBuffer()returnsnullptrfor 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, sounsharedBuffer()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:
| readyState | Bun before | Bun with this PR | Node v26.3.0 |
|---|---|---|---|
| CONNECTING | InvalidStateError | InvalidStateError | InvalidStateError |
| OPEN | view: abort (debug) or empty frame (release). ArrayBuffer: empty frame | TypeError | TypeError |
| CLOSING, CLOSED | no error | TypeError | no 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 forspan(). - #35030 landed first and added the 125-byte check to the same
ping()andpong()overloads. The rebase keeps that check and applies it tobytes.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