Compare commits

...

6 Commits

Author SHA1 Message Date
autofix-ci[bot]
9ff046212f [autofix.ci] apply automated fixes 2026-02-21 04:26:35 +00:00
Claude
032b5e6428 fix: prevent assertion crash when SharedArrayBuffer-backed views reach unsharedBuffer() 2026-02-21 04:24:35 +00:00
robobun
89d2b1cd0b fix(websocket): add missing incPendingActivityCount() in blob binaryType case (#26670)
## Summary

- Fix crash ("Pure virtual function called!") when WebSocket client
receives binary data with `binaryType = "blob"` and no event listener
attached
- Add missing `incPendingActivityCount()` call before `postTask` in the
Blob case of `didReceiveBinaryData`
- Add regression test for issue #26669

## Root Cause

The Blob case in `didReceiveBinaryData` (WebSocket.cpp:1324-1331) was
calling `decPendingActivityCount()` inside the `postTask` callback
without a matching `incPendingActivityCount()` beforehand. This bug was
introduced in #21471 when Blob support was added.

The ArrayBuffer and NodeBuffer cases correctly call
`incPendingActivityCount()` before `postTask`, but the Blob case was
missing this call.

## Test plan

- [x] New regression test verifies WebSocket with `binaryType = "blob"`
doesn't crash on ping frames
- [x] `bun bd test test/regression/issue/26669.test.ts` passes

Fixes #26669

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: Ciro Spaciari MacBook <ciro@anthropic.com>
2026-02-05 20:39:19 -08:00
Jarred Sumner
2019a1b11d Bump WebKit 2026-02-05 20:09:39 -08:00
SUZUKI Sosuke
6c70ce2485 Update WebKit to 7bc2f97e28353062bb54776ce01e4c2ff24c35cc (#26769)
### What does this PR do?

### How did you verify your code works?
2026-02-05 17:58:30 -08:00
SUZUKI Sosuke
0e386c4168 fix(stringWidth): correct width for Thai/Lao spacing vowels (#26728)
## Summary

`Bun.stringWidth` was incorrectly treating Thai SARA AA (U+0E32), SARA
AM (U+0E33), and their Lao equivalents (U+0EB2, U+0EB3) as zero-width
characters.

## Root Cause

In `src/string/immutable/visible.zig`, the range check for Thai/Lao
combining marks was too broad:
- Thai: `0xe31 <= cp <= 0xe3a` included U+0E32 and U+0E33
- Lao: `0xeb1 <= cp <= 0xebc` included U+0EB2 and U+0EB3

According to Unicode (UCD Grapheme_Break property), these are **spacing
vowels** (Grapheme_Base), not combining marks.

## Changes

- **`src/string/immutable/visible.zig`**: Exclude U+0E32, U+0E33,
U+0EB2, U+0EB3 from zero-width ranges
- **`test/js/bun/util/stringWidth.test.ts`**: Add tests for Thai and Lao
spacing vowels

## Before/After

| Character | Before | After |
|-----------|--------|-------|
| `\u0E32` (SARA AA) | 0 | 1 |
| `\u0E33` (SARA AM) | 0 | 1 |
| `คำ` (common Thai word) | 1 | 2 |
| `\u0EB2` (Lao AA) | 0 | 1 |
| `\u0EB3` (Lao AM) | 0 | 1 |

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-05 17:31:15 -08:00
8 changed files with 127 additions and 3 deletions

View File

@@ -6,7 +6,7 @@ option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of down
option(WEBKIT_BUILD_TYPE "The build type for local WebKit (defaults to CMAKE_BUILD_TYPE)")
if(NOT WEBKIT_VERSION)
set(WEBKIT_VERSION 7bc2f97e28353062bb54776ce01e4c2ff24c35cc)
set(WEBKIT_VERSION 8af7958ff0e2a4787569edf64641a1ae7cfe074a)
endif()
# Use preview build URL for Windows ARM64 until the fix is merged to main

View File

@@ -189,9 +189,13 @@ template<> struct WebCore::Converter<Bun::IDLArrayBufferRef>
return jsBuffer;
}
if (auto* jsView = JSC::jsDynamicCast<JSC::JSArrayBufferView*>(value)) {
if (jsView->isShared())
return std::nullopt;
return jsView->unsharedBuffer();
}
if (auto* jsDataView = JSC::jsDynamicCast<JSC::JSDataView*>(value)) {
if (jsDataView->isShared())
return std::nullopt;
return jsDataView->unsharedBuffer();
}
return std::nullopt;

View File

@@ -90,6 +90,11 @@ JSC_DEFINE_HOST_FUNCTION(structuredCloneForStream, (JSGlobalObject * globalObjec
auto* bufferView = jsCast<JSArrayBufferView*>(value);
ASSERT(bufferView);
if (bufferView->isShared()) {
throwDataCloneError(*globalObject, scope);
return {};
}
auto* buffer = bufferView->unsharedBuffer();
if (!buffer) {
throwDataCloneError(*globalObject, scope);

View File

@@ -1323,6 +1323,7 @@ void WebSocket::didReceiveBinaryData(const AtomString& eventName, const std::spa
if (auto* context = scriptExecutionContext()) {
RefPtr<Blob> blob = Blob::create(binaryData, context->jsGlobalObject());
this->incPendingActivityCount();
context->postTask([this, name = eventName, blob = blob.releaseNonNull(), protectedThis = Ref { *this }](ScriptExecutionContext& context) {
ASSERT(scriptExecutionContext());
protectedThis->dispatchEvent(MessageEvent::create(name, blob, protectedThis->m_url.string()));

View File

@@ -70,11 +70,13 @@ pub fn isZeroWidthCodepointType(comptime T: type, cp: T) bool {
}
// Thai combining marks
if ((cp >= 0xe31 and cp <= 0xe3a) or (cp >= 0xe47 and cp <= 0xe4e))
// Note: U+0E32 (SARA AA) and U+0E33 (SARA AM) are Grapheme_Base (spacing vowels), not combining
if (cp == 0xe31 or (cp >= 0xe34 and cp <= 0xe3a) or (cp >= 0xe47 and cp <= 0xe4e))
return true;
// Lao combining marks
if ((cp >= 0xeb1 and cp <= 0xebc) or (cp >= 0xec8 and cp <= 0xecd))
// Note: U+0EB2 and U+0EB3 are spacing vowels like Thai, not combining
if (cp == 0xeb1 or (cp >= 0xeb4 and cp <= 0xebc) or (cp >= 0xec8 and cp <= 0xecd))
return true;
// Combining Diacritical Marks Extended

View File

@@ -485,6 +485,28 @@ describe("stringWidth extended", () => {
expect(Bun.stringWidth("ก็")).toBe(1); // With maitaikhu
expect(Bun.stringWidth("ปฏัก")).toBe(3); // ป + ฏ + ั (combining) + ก = 3 visible
});
test("Thai spacing vowels (SARA AA and SARA AM)", () => {
// U+0E32 (SARA AA) and U+0E33 (SARA AM) are spacing vowels, not combining marks
expect(Bun.stringWidth("\u0E32")).toBe(1); // SARA AA alone
expect(Bun.stringWidth("\u0E33")).toBe(1); // SARA AM alone
expect(Bun.stringWidth("ก\u0E32")).toBe(2); // ก + SARA AA
expect(Bun.stringWidth("ก\u0E33")).toBe(2); // กำ (KO KAI + SARA AM)
expect(Bun.stringWidth("คำ")).toBe(2); // Common Thai word
expect(Bun.stringWidth("ทำ")).toBe(2); // Common Thai word
// True combining marks should still be zero-width
expect(Bun.stringWidth("\u0E31")).toBe(0); // MAI HAN-AKAT (combining)
expect(Bun.stringWidth("ก\u0E31")).toBe(1); // กั
});
test("Lao spacing vowels", () => {
// U+0EB2 and U+0EB3 are spacing vowels in Lao, similar to Thai
expect(Bun.stringWidth("\u0EB2")).toBe(1); // LAO VOWEL SIGN AA
expect(Bun.stringWidth("\u0EB3")).toBe(1); // LAO VOWEL SIGN AM
expect(Bun.stringWidth("ກ\u0EB2")).toBe(2); // KO + AA
// True combining marks should still be zero-width
expect(Bun.stringWidth("\u0EB1")).toBe(0); // MAI KAN (combining)
});
});
describe("non-ASCII in escape sequences and Indic script handling", () => {

View File

@@ -0,0 +1,21 @@
import { expect, test } from "bun:test";
test("Response.clone() does not crash when body stream contains SharedArrayBuffer-backed typed array", async () => {
const sab = new SharedArrayBuffer(8);
const view = new Uint8Array(sab);
const stream = new ReadableStream({
start(controller) {
controller.enqueue(view);
controller.close();
},
});
const resp = new Response(stream);
const clone = resp.clone();
// Reading the cloned body triggers structuredCloneForStream on the chunk.
// Before the fix, this would crash with:
// ASSERTION FAILED: !result || !result->isShared()
// Now it should throw a DataCloneError instead of crashing.
expect(async () => await clone.arrayBuffer()).toThrow("cloned");
});

View File

@@ -0,0 +1,69 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// https://github.com/oven-sh/bun/issues/26669
// WebSocket client crashes ("Pure virtual function called!") when binaryType = "blob"
// and no event listener is attached. The missing incPendingActivityCount() allows the
// WebSocket to be GC'd before the postTask callback runs.
test("WebSocket with binaryType blob should not crash when GC'd before postTask", async () => {
await using server = Bun.serve({
port: 0,
fetch(req, server) {
if (server.upgrade(req)) return undefined;
return new Response("Not a websocket");
},
websocket: {
open(ws) {
// Send binary data immediately - this triggers didReceiveBinaryData
// with the Blob path when client has binaryType = "blob"
ws.sendBinary(new Uint8Array(64));
ws.sendBinary(new Uint8Array(64));
ws.sendBinary(new Uint8Array(64));
},
message() {},
},
});
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const url = process.argv[1];
// Create many short-lived WebSocket objects with blob binaryType and no listeners.
// Without the fix, the missing incPendingActivityCount() lets the WebSocket get GC'd
// before the postTask callback fires, causing "Pure virtual function called!".
async function run() {
for (let i = 0; i < 100; i++) {
const ws = new WebSocket(url);
ws.binaryType = "blob";
// Intentionally: NO event listeners attached.
// This forces the postTask path in didReceiveBinaryData's Blob case.
}
// Force GC to collect the unreferenced WebSocket objects while postTask
// callbacks are still pending.
Bun.gc(true);
await Bun.sleep(50);
Bun.gc(true);
await Bun.sleep(50);
Bun.gc(true);
await Bun.sleep(100);
}
await run();
Bun.gc(true);
await Bun.sleep(200);
console.log("OK");
process.exit(0);
`,
`ws://localhost:${server.port}`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("OK");
expect(exitCode).toBe(0);
});