mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
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>
This commit is contained in:
@@ -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()));
|
||||
|
||||
69
test/regression/issue/26669.test.ts
Normal file
69
test/regression/issue/26669.test.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user