[GH-ISSUE #1660] iOS app crashes when clearing notifications (mutation during enumeration) #1157

Open
opened 2026-05-07 00:30:41 +02:00 by BreizhHardware · 1 comment

Originally created by @jflammia on GitHub (Mar 17, 2026).
Original GitHub issue: https://github.com/binwiederhier/ntfy/issues/1660

Bug Description

The ntfy iOS app crashes with SIGABRT when attempting to clear/delete all notifications for a subscription. The app becomes unusable once enough notifications accumulate (~870 in my case) because the only recovery is to delete and reinstall the app.

Crash Analysis

Exception: EXC_CRASH / SIGABRT (Abort trap: 6)
Device: iPhone 15 Pro (iPhone16,1), iOS 26.3.1
App Version: 1.3 (build 4)

Root Cause

The crash occurs in Store.swift at delete(allNotificationsFor:) (line ~151 in ntfy-ios repo):

func delete(allNotificationsFor subscription: Subscription) {
    guard let notifications = subscription.notifications else { return }
    do {
        notifications.forEach { notification in
            context.delete(notification as! Notification)  // Mutates collection during enumeration
        }
        try context.save()
    } catch let error {
        Log.w(Store.tag, "Cannot delete notification(s)", error)
        rollbackAndRefresh()
    }
}

The forEach loop calls context.delete() on each notification while iterating over subscription.notifications (an NSSet). Core Data's _processRecentChanges: fires on the run loop observer during this iteration and tries to enumerate the same collection, causing a mutation-during-enumeration crash.

Stack Trace

Main thread (triggered):

__pthread_kill → abort() → __abort_message → demangling_terminate_handler()
→ _objc_terminate() → __cxa_rethrow → objc_exception_rethrow
→ -[NSManagedObjectContext _processRecentChanges:]
→ _performRunLoopAction
→ __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

Last exception backtrace:

__exceptionPreprocess → objc_exception_throw
→ +[__NSFastEnumerationEnumerator allocWithZone:]   ← collection mutated during enumeration
→ -[NSManagedObjectContext _processRecentChanges:]

Background SQLite thread was inside _prefetchObjectsForDeletePropagation at crash time, confirming Core Data was processing the deletes concurrently.

Suggested Fix

Option A — Copy before iterating (minimal fix):

func delete(allNotificationsFor subscription: Subscription) {
    guard let notifications = subscription.notifications else { return }
    let snapshot = Array(notifications)  // Copy to avoid mutation during enumeration
    do {
        snapshot.forEach { notification in
            context.delete(notification as! Notification)
        }
        try context.save()
    } catch let error {
        Log.w(Store.tag, "Cannot delete notification(s)", error)
        rollbackAndRefresh()
    }
}

Option B — NSBatchDeleteRequest (better for large datasets):

func delete(allNotificationsFor subscription: Subscription) {
    let fetchRequest: NSFetchRequest<NSFetchRequestResult> = Notification.fetchRequest()
    fetchRequest.predicate = NSPredicate(format: "subscription == %@", subscription)
    let batchDelete = NSBatchDeleteRequest(fetchRequest: fetchRequest)
    batchDelete.resultType = .resultTypeObjectIDs
    
    do {
        let result = try context.execute(batchDelete) as? NSBatchDeleteResult
        if let objectIDs = result?.result as? [NSManagedObjectID] {
            let changes = [NSDeletedObjectsKey: objectIDs]
            NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
        }
    } catch let error {
        Log.w(Store.tag, "Cannot batch delete notification(s)", error)
    }
}

Option B is preferred as it doesn't load objects into memory and handles any number of notifications efficiently.

The same pattern should also be applied to delete(notifications:) which has the same mutation-during-enumeration vulnerability.

Steps to Reproduce

  1. Subscribe to a topic on a self-hosted ntfy server
  2. Accumulate 500+ notifications over time
  3. Attempt to clear/delete all notifications in the iOS app
  4. App crashes immediately

Environment

  • Self-hosted ntfy server v2.17.0
  • ntfy iOS app v1.3 (build 4) from App Store
  • iPhone 15 Pro, iOS 26.3.1

Workaround

Delete and reinstall the ntfy iOS app to clear the local Core Data database.

Originally created by @jflammia on GitHub (Mar 17, 2026). Original GitHub issue: https://github.com/binwiederhier/ntfy/issues/1660 ## Bug Description The ntfy iOS app crashes with `SIGABRT` when attempting to clear/delete all notifications for a subscription. The app becomes unusable once enough notifications accumulate (~870 in my case) because the only recovery is to delete and reinstall the app. ## Crash Analysis **Exception:** `EXC_CRASH` / `SIGABRT` (Abort trap: 6) **Device:** iPhone 15 Pro (iPhone16,1), iOS 26.3.1 **App Version:** 1.3 (build 4) ### Root Cause The crash occurs in `Store.swift` at `delete(allNotificationsFor:)` (line ~151 in ntfy-ios repo): ```swift func delete(allNotificationsFor subscription: Subscription) { guard let notifications = subscription.notifications else { return } do { notifications.forEach { notification in context.delete(notification as! Notification) // Mutates collection during enumeration } try context.save() } catch let error { Log.w(Store.tag, "Cannot delete notification(s)", error) rollbackAndRefresh() } } ``` The `forEach` loop calls `context.delete()` on each notification while iterating over `subscription.notifications` (an `NSSet`). Core Data's `_processRecentChanges:` fires on the run loop observer during this iteration and tries to enumerate the same collection, causing a **mutation-during-enumeration crash**. ### Stack Trace **Main thread (triggered):** ``` __pthread_kill → abort() → __abort_message → demangling_terminate_handler() → _objc_terminate() → __cxa_rethrow → objc_exception_rethrow → -[NSManagedObjectContext _processRecentChanges:] → _performRunLoopAction → __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ ``` **Last exception backtrace:** ``` __exceptionPreprocess → objc_exception_throw → +[__NSFastEnumerationEnumerator allocWithZone:] ← collection mutated during enumeration → -[NSManagedObjectContext _processRecentChanges:] ``` **Background SQLite thread** was inside `_prefetchObjectsForDeletePropagation` at crash time, confirming Core Data was processing the deletes concurrently. ## Suggested Fix **Option A — Copy before iterating (minimal fix):** ```swift func delete(allNotificationsFor subscription: Subscription) { guard let notifications = subscription.notifications else { return } let snapshot = Array(notifications) // Copy to avoid mutation during enumeration do { snapshot.forEach { notification in context.delete(notification as! Notification) } try context.save() } catch let error { Log.w(Store.tag, "Cannot delete notification(s)", error) rollbackAndRefresh() } } ``` **Option B — NSBatchDeleteRequest (better for large datasets):** ```swift func delete(allNotificationsFor subscription: Subscription) { let fetchRequest: NSFetchRequest<NSFetchRequestResult> = Notification.fetchRequest() fetchRequest.predicate = NSPredicate(format: "subscription == %@", subscription) let batchDelete = NSBatchDeleteRequest(fetchRequest: fetchRequest) batchDelete.resultType = .resultTypeObjectIDs do { let result = try context.execute(batchDelete) as? NSBatchDeleteResult if let objectIDs = result?.result as? [NSManagedObjectID] { let changes = [NSDeletedObjectsKey: objectIDs] NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context]) } } catch let error { Log.w(Store.tag, "Cannot batch delete notification(s)", error) } } ``` Option B is preferred as it doesn't load objects into memory and handles any number of notifications efficiently. The same pattern should also be applied to `delete(notifications:)` which has the same mutation-during-enumeration vulnerability. ## Steps to Reproduce 1. Subscribe to a topic on a self-hosted ntfy server 2. Accumulate 500+ notifications over time 3. Attempt to clear/delete all notifications in the iOS app 4. App crashes immediately ## Environment - Self-hosted ntfy server v2.17.0 - ntfy iOS app v1.3 (build 4) from App Store - iPhone 15 Pro, iOS 26.3.1 ## Workaround Delete and reinstall the ntfy iOS app to clear the local Core Data database.
Author
Owner

@RezzZ commented on GitHub (Mar 20, 2026):

app crashes for me too, but messages do get deleted and a restart of the app works for me. so no workaround required here

<!-- gh-comment-id:4100847538 --> @RezzZ commented on GitHub (Mar 20, 2026): app crashes for me too, but messages do get deleted and a restart of the app works for me. so no workaround required here
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/ntfy#1157
No description provided.