WebSocket: hand text frames to C++ as an owned BunString, delete EncodedSlice::utf16_global
Fixes #31968
Problem
EncodedSlice::utf16_global(&[u16])(src/bun_core/string/mod.rs:1262on main) was a safefnthat set the global ptr-tag on a borrowed slice. The tag tells C++ (Zig::toString) that it owns the bytes and must free them through the default allocator. Safe code could pass aVec's storage and get an invalid free, then a double free when theVecdropped.- Its only caller, the WebSocket client text-receive path (
src/http_jsc/websocket_client.rs), held the contract withManuallyDropand a comment.
Fix
- The text-receive path hands C++ an owned
BunStringby value. A transcodedVec<u16>moves into an external WTF string throughString::create_external_globally_allocated_utf16. ASCII text is copied withString::clone_latin1.WebSocket__didReceiveTextconsumes it withtransferToWTFString, asWebSocket__didClosealready does. utf16_globalis deleted. It was the only setter of bit 62, sois_globally_allocated, the twosubstring_with_lenpropagation branches, and the vacuousdebug_assertinEncodedSliceJsc::to_jsgo with it. Rust can no longer create a global-taggedEncodedSliceat all.- Correct because ownership now moves in the type:
BunStringowns one WTF ref and C++ adopts it.transferToWTFStringreturns an empty string for theEmpty,Dead, andOutOfMemorytags, the same as the oldtoString/toStringCopyfallbacks. - Verified:
test/js/web/websocket/websocket.test.js(new test drives both branches with ASCII, short and long non-ASCII, and a 70 KB frame, then releases the strings and forces a GC). Also the rest ofwebsocket.test.js,websocket-client.test.ts,websocket-utf16-headers.test.ts,parse-args.test.tsandxml.test.tsfor the substring change, and clippy on the three crates.
Background
EncodedSlice<'a>is a{ptr, len}view with encoding bits in the pointer's high byte. Bit 62 meant "the consumer owns this allocation": an ownership claim that a borrowed&'a [u16]cannot back.BunStringis the FFI string union shared with C++. Passed by value acrossextern "C"it transfers its ref.transferToWTFString()is the C++ consumer that takes that ref.- The C++ readers of bit 62 (
isTaggedExternalPtrinhelpers.h,BunString.cpp:743) now always see it clear. They sit in the adopt path that #40404 rewrites, so they are left for that PR.free_global_stringstays live throughEncodedSlice__toExternalValue. - The new test cannot fail on the unfixed build. The fix deletes an API, and the compiler is the remaining guard. The test is coverage for the rewritten receive path.
Notes
The original PR targeted ZigString::from16, from16_slice, deinit_global, and From<&[u16]> for String. Those were removed on main in #35002 and #40374 (ZigString became EncodedSlice<'a>). This is the rework for the survivor of that family, in the shape the stale-review comment above asked for. A source-text lint test was dropped after the same shape was rejected on #31981.
Local baselines on a stock build of this main: websocket.test.js has 3 failures that need ws.postman-echo.com, and xml.test.ts "deep values are a catchable error" times out at 5 s under ASAN. Both fail identically with and without this diff.
CI on earlier heads: test-crypto-dh-leak.js fails the same ~23 MB RSS threshold on x64-asan across unrelated branches (builds 112252 through 112369) and is reported for main-break triage.