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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions