Skip to content

Bug: Bun C++ Exception Crash with PartyWebSocket (partysocket library) #304

@platon-ivanov

Description

@platon-ivanov

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 immediately

After (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:

  1. Remove partysocket import:

    // REMOVED: import { WebSocket as PartyWebSocket } from 'partysocket'
  2. Use native WebSocket:

    const websocket = new WebSocket(wsUrl)  // Instead of new PartyWebSocket(wsUrl)
  3. 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

Additional Context

The crash is not related to:

  • Native WebSocket usage (works fine)
  • Async iterators with native WebSocket
  • EventPublisher or mergeIterators
  • AbortSignal usage

The crash is specifically related to:

  • PartyWebSocket from partysocket library
  • Objects created by partysocket that 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

  1. Create a WebSocket connection using PartyWebSocket from partysocket
  2. Keep the connection alive for 3-4 minutes
  3. Wait for JavaScriptCore's parallel GC to run
  4. 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 WebSocket instead of PartyWebSocket

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions