Skip to content

Audit: Race conditions and concurrency bugs #643

@Spongeacer

Description

@Spongeacer

Summary

Automated audit found 19 race condition and concurrency bugs across packages/app/src/ and packages/opencode/src/. These span optimistic updates without rollback, concurrent write races, missing cancellation, stale data dependencies, timer cleanup issues, async generator error boundaries, and WebSocket/SSE event ordering.

High Severity

File Lines Issue
packages/app/src/context/sync.tsx 254–261 Module-level optimistic Map has no concurrency control — rapid sequential submits or multi-tab usage can cause optimistic messages to disappear
packages/opencode/src/session/turn-change.ts 726–818 Undo/redo mutate() performs read-check-modify without transaction — double-click or multi-client usage can apply changes twice
packages/app/src/context/global-sdk.tsx 123–156 replayCursor.update(event.id) happens in raw SSE stream loop before events are flushed to UI store — connection drop between receive and flush causes event loss on reconnect

Medium Severity

Category File Lines Issue
Missing rollback packages/app/src/components/prompt-input/submit.ts 141–186, 573–681 Command failures don't rollback server state; pending map uses stale sessionID for cleanup
Missing cancellation packages/app/src/pages/session/message-timeline.tsx 427–585 turnChangeFetch continues after unmount — may show wrong dialog/toast for new session
Missing cancellation packages/app/src/context/global-sync/bootstrap.ts 280–467 Bootstrap requests not cancellable on dispose — callbacks may write to disposed store
Event ordering packages/app/src/context/global-sync/event-reducer.ts 208–300 part.delta silently dropped if part.updated not yet received (network reordering)
Timer cleanup packages/app/src/components/terminal.tsx 203–298, 504 sizeTimer may fire after unmount; reconn dedup may mask real failures
Effect errors packages/opencode/src/session/llm.ts 432–530 Inner Effect.runPromise timeout may cause unhandled defect if blockers service fails
Effect errors packages/opencode/src/control-plane/workspace.ts 312–358 parseSSE callback errors may kill sync loop (defensive wrapping needed)

Low Severity

File Lines Issue
packages/app/src/context/sync.tsx 342–417 getOptimistic may read mutated state during load
packages/app/src/pages/session/use-session-timeline-data.ts 197–212 lastGoodMessages mutable variable capture race
packages/app/src/context/file/tree-store.ts 42–127 TOCTOU in options.scope() check
packages/app/src/pages/layout.tsx 265–315 Interval recreation race on rapid toggle
packages/app/src/pages/session/session-running-state.ts 42–65 Extra setTick from stale timer
packages/app/src/context/global-sync.tsx 89–98, 403–418 Nested rAF + setTimeout may leak inner timer
packages/opencode/src/session/processor.ts 591–680 fnUntraced limits debuggability on event processing errors
packages/app/src/components/terminal.tsx 527–608 Async output.push() may cause out-of-order display
packages/opencode/src/control-plane/sse.ts 18–65 onEvent called synchronously in forEach — async handlers may process out of order

Recommendations

  1. Add atomic operations for the optimistic Map in sync.tsx (use a queue or mutex)
  2. Move cursor update in global-sdk.tsx to happen inside flush() alongside event emission
  3. Add sequence numbers to message.part.delta events or buffer deltas until part exists
  4. Wrap parseSSE call in workspace.ts with try/catch so malformed events don't terminate sync
  5. Use AbortSignal consistently in bootstrapDirectory to cancel in-flight requests on dispose
  6. Guard async callbacks in message-timeline.tsx with a disposed flag before calling dialog.show() or showToast()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions