-
Notifications
You must be signed in to change notification settings - Fork 56
Description
Summary
Bun v1.3.5 crashes with a "pure virtual method called" C++ exception after running for 3-4 minutes when using PartyWebSocket from the partysocket library. The crash occurs during JavaScriptCore's parallel garbage collection when marking WebCore/DOM objects.
Environment
- Bun version: v1.3.5 (1e86cebd)
- OS: Linux x86_64 (baseline)
- Kernel: v6.18.0 | glibc v2.42
- Library:
partysocket@1.1.10(via catalog) - Crash time: Consistently after 3-4 minutes of runtime
Stack Trace
Bun v1.3.5 (1e86cebd) on linux x86_64_baseline [AutoCommand]
**panic**: A C++ exception occurred
- JSGlobalObject.zig:854: `Zig__GlobalObject__onCrash`
- compiler_builtins.6c1f39da0e88e811-cgu.247:0: `__cxxabiv1::__terminate`
- ClassInfo.h:124: `WTF::Ref<WTF::SharedTask<void (...)>, WTF::RawPtrTraits<WTF::SharedTask<void (...)> >, WTF::DefaultRefDerefTraits<WTF::SharedTask<void (...)> > > JSC::Subspace::forEachMarkedCellInParallel<JSC::SlotVisitor, void WebCore::DOMGCOutputConstraint::executeImplImpl<JSC::SlotVisitor>(...)::'lambda'(...)::operator(...) const::'lambda'(...)>(...)::'lambda'(...)::operator(...) const::'lambda'(...) const&)::Task::run`
- MarkingConstraint.cpp:96: `JSC::MarkingConstraintSolver::runExecutionThread`
- SlotVisitor.cpp:686: `JSC::SlotVisitor::drainFromShared`
- Heap.cpp:1502: `WTF::SharedTaskFunctor<void (...)::$_1>::run`
- ParallelHelperPool.cpp:110: `WTF::ParallelHelperPool::Thread::work`
- AutomaticThread.cpp:225: `WTF::Detail::CallableWrapper<WTF::AutomaticThread::start(...)::$_0, void>::call`
- Function.h:82: `WTF::wtfThreadEntryPoint`
Root Cause
The crash occurs during JavaScriptCore's parallel garbage collection when marking WebCore::DOMGCOutputConstraint objects. The PartyWebSocket class from the partysocket library creates WebCore/DOM objects that are destroyed while parallel GC threads are trying to mark them, causing the "pure virtual method called" error.
Key observation: The stack trace shows WebCore::DOMGCOutputConstraint, indicating this is related to DOM/WebCore objects, not just WebSocket connections. The partysocket library likely wraps or extends native WebSocket functionality in a way that creates additional WebCore objects that become invalid during GC.
Problematic Code
Before (Causing Crash)
// backend/apps/support/src/lib/chat-api-client.ts
import { WebSocket as PartyWebSocket } from 'partysocket'
// Create WebSocket connection using partysocket (with auto-reconnect)
let sharedWebSocket: PartyWebSocket | null = null
let sharedWsClient: ContractRouterClient<typeof chatContract> | null = null
function getSharedWebSocketClient(): ContractRouterClient<typeof chatContract> {
// Return existing client if WebSocket is open
if (
sharedWsClient &&
sharedWebSocket &&
sharedWebSocket.readyState === PartyWebSocket.OPEN
) {
return sharedWsClient
}
// Create new WebSocket connection with partysocket
// partysocket provides automatic reconnection and message buffering
sharedWebSocket = new PartyWebSocket(getWebSocketUrl()) // ❌ CRASHES HERE
// Create oRPC client from WebSocket
const link = new WSLink({
websocket: sharedWebSocket as any,
})
sharedWsClient = createORPCClient(link) as ContractRouterClient<
typeof chatContract
>
sharedWebSocket.addEventListener('close', () => {
sharedWsClient = null
})
return sharedWsClient
}
// Created at module load time - WebSocket created immediately
export const chatApiClient: ContractRouterClient<typeof chatContract> =
getSharedWebSocketClient() // ❌ Creates PartyWebSocket immediatelyAfter (Fixed)
// backend/apps/support/src/lib/chat-api-client.ts
// REMOVED: import { WebSocket as PartyWebSocket } from 'partysocket'
// Using native WebSocket instead
// Create WebSocket connection using native WebSocket
let sharedWebSocket: WebSocket | null = null
let sharedWsClient: ContractRouterClient<typeof chatContract> | null = null
function getSharedWebSocketClient(): ContractRouterClient<typeof chatContract> {
// Return existing client if WebSocket is open
if (
sharedWsClient &&
sharedWebSocket &&
sharedWebSocket.readyState === WebSocket.OPEN // ✅ Native WebSocket
) {
return sharedWsClient
}
// Create new WebSocket connection with native WebSocket
// Using native WebSocket instead of PartyWebSocket to avoid GC crashes
sharedWebSocket = new WebSocket(getWebSocketUrl()) // ✅ Native WebSocket
// Create oRPC client from WebSocket
const link = new WSLink({
websocket: sharedWebSocket as any,
})
sharedWsClient = createORPCClient(link) as ContractRouterClient<
typeof chatContract
>
sharedWebSocket.addEventListener('close', () => {
sharedWsClient = null
})
return sharedWsClient
}
// Lazy-load chatApiClient to avoid creating WebSocket at module load time
let _chatApiClient: ContractRouterClient<typeof chatContract> | null = null
export const chatApiClient: ContractRouterClient<typeof chatContract> =
new Proxy({} as ContractRouterClient<typeof chatContract>, {
get(_target, prop) {
if (!_chatApiClient) {
_chatApiClient = getSharedWebSocketClient() // ✅ Lazy-loaded
}
return (_chatApiClient as any)[prop]
},
})Workaround
Replace PartyWebSocket from partysocket with native WebSocket:
-
Remove partysocket import:
// REMOVED: import { WebSocket as PartyWebSocket } from 'partysocket' -
Use native WebSocket:
const websocket = new WebSocket(wsUrl) // Instead of new PartyWebSocket(wsUrl)
-
Lazy-load WebSocket connections to avoid creating them at module load time:
// Use Proxy to lazy-load instead of creating at module load export const chatApiClient = new Proxy({}, { get(_target, prop) { if (!_chatApiClient) { _chatApiClient = getSharedWebSocketClient() } return (_chatApiClient as any)[prop] }, })
Related Issues
- Similar crash reported with worker threads: Crash in BunJS when using worker threads: "pure virtual method called" and C++ exception oven-sh/bun#14559
- Both involve "pure virtual method called" during parallel GC marking
- Both occur after several minutes of runtime
Additional Context
The crash is not related to:
- Native
WebSocketusage (works fine) - Async iterators with native WebSocket
- EventPublisher or mergeIterators
- AbortSignal usage
The crash is specifically related to:
PartyWebSocketfrompartysocketlibrary- Objects created by
partysocketthat reference WebCore/DOM - These objects being destroyed during parallel GC marking
Expected Behavior
The application should run indefinitely without crashing. Native WebSocket works correctly, but PartyWebSocket causes crashes after 3-4 minutes.
Actual Behavior
Application crashes with C++ exception after 3-4 minutes when using PartyWebSocket. Replacing with native WebSocket resolves the issue.
Reproduction
- Create a WebSocket connection using
PartyWebSocketfrompartysocket - Keep the connection alive for 3-4 minutes
- Wait for JavaScriptCore's parallel GC to run
- Crash occurs when GC tries to mark WebCore objects created by
partysocket
Impact
- Severity: High - causes application crashes
- Frequency: 100% reproducible after 3-4 minutes
- Workaround: Use native
WebSocketinstead ofPartyWebSocket