-
Notifications
You must be signed in to change notification settings - Fork 19
Description
Summary
VoltraLiveActivityService is a plain class with unprotected mutable state (monitoredActivityIds: Set<String> and monitoringTasks: [Task<Void, Never>]). These properties are mutated concurrently from multiple Swift cooperative threads, causing a EXC_BAD_ACCESS (SIGSEGV) crash.
Environment
- voltra version: 1.2.1
- iOS version: 18.x (simulator, macOS 26.2)
- Expo SDK: 54
- React Native: 0.81.4
Crash Details
The crash occurs at swift_isUniquelyReferenced_nonNull_native during Set._Variant.insert(_:) inside monitorActivity(_:enablePush:). This is a classic Swift copy-on-write data race — the Set's backing buffer is referenced from multiple threads simultaneously.
Crash thread (Thread 8):
0 libswiftCore.dylib swift_isUniquelyReferenced_nonNull_native + 0
1 libswiftCore.dylib Set._Variant.insert(_:) + 660
2 PUMPDStaging.debug.dylib VoltraLiveActivityService.monitorActivity(_:enablePush:) + 468
3 PUMPDStaging.debug.dylib closure #1 in VoltraLiveActivityService.startActivityUpdatesObservation(enablePush:) + 96
At the time of the crash, three separate cooperative threads (Threads 8, 10, and 15) were all inside VoltraLiveActivityService.monitorActivity(_:enablePush:) concurrently.
Root Cause
In ios/app/VoltraLiveActivityService.swift:
Line 80 — VoltraLiveActivityService is a plain class, not an actor:
public class VoltraLiveActivityService {Lines 261-262 — unprotected mutable state:
private var monitoredActivityIds: Set<String> = []
private var monitoringTasks: [Task<Void, Never>] = []Lines 317-322 — concurrent mutation without synchronization:
private func monitorActivity(_ activity: Activity<VoltraAttributes>, enablePush: Bool) {
let activityId = activity.id
guard !monitoredActivityIds.contains(activityId) else { return } // READ
monitoredActivityIds.insert(activityId) // WRITE — crash
...
}This method is called from:
- A synchronous loop over existing activities (line 303)
- A
TaskiteratingActivity<VoltraAttributes>.activityUpdates(line 308) — fires on a cooperative thread when a new activity is created - The
activityUpdatesstream can deliver multiple updates concurrently on different cooperative threads
Additionally, monitoringTasks.append() at lines 298, 313, 335, and 350 has the same unprotected concurrent mutation problem on the Array.
Reproduction Path
- App launches →
OnStartObservingcallsstartMonitoring()→ spawns aTasklistening toactivityUpdates - User triggers a live activity → JS calls
startLiveActivity→ nativecreateActivity()callsActivity.request()on a cooperative thread Activity.request()triggers theactivityUpdatesasync sequence on another cooperative thread- Multiple cooperative threads enter
monitorActivitysimultaneously → crash onSet.insert
Suggested Fix
Make VoltraLiveActivityService an actor instead of a class:
public actor VoltraLiveActivityService {
private var monitoredActivityIds: Set<String> = []
private var monitoringTasks: [Task<Void, Never>] = []
// ...
}This would provide automatic isolation for all mutable state. Alternatively, protect the mutable properties with a lock or serial DispatchQueue if actor conversion is not feasible.