Skip to content

Conversation

@tarikfp
Copy link
Contributor

@tarikfp tarikfp commented Jan 26, 2026

fix: prevent crash when monitoring Live Activities

Summary

Fixes two crashes in monitorActivity caused by thread-unsafe Set access from multiple Swift Concurrency Tasks.

Problem

We're seeing intermittent crashes when Live Activities are started/stopped quickly. Found two distinct crash types:

Crash 1: Set.contains() blows up

EXC_BAD_ACCESS (SIGSEGV)
KERN_INVALID_ADDRESS at 0x0000000000000010

0   libobjc.A.dylib       objc_msgSend + 20
1   libswiftCore.dylib    Set._Variant.contains(_:) + 128
2   VitalaDev.debug.dylib VoltraModule.monitorActivity(_:) + 228

Crash 2: Set.insert() blows up with PAC failure

EXC_BAD_ACCESS (SIGSEGV)
KERN_INVALID_ADDRESS at 0x8000000000000010 -> 0x0000000000000010
(possible pointer authentication failure)

0   libswiftCore.dylib    _NativeSet.insertNew(_:at:isUnique:) + 80
1   libswiftCore.dylib    Set._Variant.insert(_:) + 744
2   VitalaDev.debug.dylib VoltraModule.monitorActivity(_:) + 576

What's happening

The for await loops in observeLiveActivityUpdates() spawn Tasks that call monitorActivity() concurrently. Multiple Tasks hit the monitoredActivityIds Set at the same time → race condition → crash.

Why not DispatchQueue.sync?

Mixing GCD with Swift Concurrency doesn't work. GCD's memory barriers don't apply to Swift's cooperative thread pool. The Tasks can still see stale data.

Fix: OSAllocatedUnfairLock

Using OSAllocatedUnfairLock (iOS 16+) for the Set access. It's synchronous, lower overhead than actors, and should work correctly with Swift Concurrency Tasks.

Changes

+ import os

- private var monitoredActivityIds: Set<String> = []
+ private let monitoredActivityIds = OSAllocatedUnfairLock<Set<String>>(initialState: [])

  OnStopObserving {
    VoltraEventBus.shared.unsubscribe()
-   monitoredActivityIds.removeAll()
+   monitoredActivityIds.withLock { $0.removeAll() }
  }

  private func monitorActivity(_ activity: Activity<VoltraAttributes>) {
+   guard activity.activityState != .ended else { return }
+
    let activityId = activity.id
+   let activityName = activity.attributes.name
+   let pushEnabled = pushNotificationsEnabled

-   guard !monitoredActivityIds.contains(activityId) else { return }
-   monitoredActivityIds.insert(activityId)
+   // Thread-safe check-and-insert
+   let shouldMonitor = monitoredActivityIds.withLock { ids -> Bool in
+     guard !ids.contains(activityId) else { return false }
+     ids.insert(activityId)
+     return true
+   }
+   guard shouldMonitor else { return }

    Task {
      for await state in activity.activityStateUpdates {
        VoltraEventBus.shared.send(
-         .stateChange(activityName: activity.attributes.name, ...)
+         .stateChange(activityName: activityName, ...)
        )
      }
    }

-   if pushNotificationsEnabled {
+   if pushEnabled {
      // ... same pattern
    }
  }

Also captures activityName and pushEnabled before the async boundary to avoid accessing potentially stale activity references.

Test Plan

  • Rapid start/stop cycles don't crash
  • Normal Live Activity lifecycle works
  • No race conditions under load

@vercel
Copy link

vercel bot commented Jan 26, 2026

@tarikfp is attempting to deploy a commit to the Callstack Team on Vercel.

A member of the Team first needs to authorize it.

@tarikfp tarikfp force-pushed the fix/monitor-activity-crash branch from 6621483 to 8407adb Compare January 26, 2026 13:58
@tarikfp tarikfp marked this pull request as draft January 26, 2026 14:00
Add safety checks to prevent crashes in monitorActivity:

1. Skip activities that are already in .ended state before accessing
   their properties (prevents accessing deallocated objects)

2. Use serial dispatch queue for thread-safe access to monitoredActivityIds
   Set (prevents concurrent modification crashes when multiple async tasks
   call monitorActivity simultaneously)

The crashes occurred when:
- Accessing activity.id on activities that had already ended
- Multiple async tasks from activityUpdates stream tried to modify
  the monitoredActivityIds Set concurrently
@tarikfp tarikfp force-pushed the fix/monitor-activity-crash branch from 8407adb to 1aef2a6 Compare January 26, 2026 14:17
DispatchQueue.sync doesn't work correctly with Swift Concurrency.
OSAllocatedUnfairLock provides proper synchronous locking that
works with the cooperative thread pool.
@tarikfp tarikfp changed the title fix: prevent crash when monitoring ended activities fix: prevent crash when monitoring Live Activities Jan 26, 2026
@V3RON
Copy link
Contributor

V3RON commented Jan 27, 2026

Could you write a harness test that causes this to happen during a test run and then fix it with your changes? This will confirm your assumptions in a case where starting and stopping happen one millisecond after the other.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants