From 650d4af504f0a6dd24b135b0a71986f5e18027c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Mon, 12 Jan 2026 10:58:45 -0800 Subject: [PATCH] Fix crash on iOS 17 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes an arithmetic overflow crash on iOS 17 caused by the fallback lock. In iOS 17, Swift Mutexes are not available, so we have a fallback class for NdbUseLock that does not make use of them. This allows some thread safety for iOS 17 users, but unfortunately not as much as iOS 18+ users. This attempts to fix those remaining race conditions and subsequent crashes by using `NSLock` in the fallback class, which is available on iOS 17. Closes: https://github.com/damus-io/damus/issues/3512 Changelog-Fixed: Fixed a crash on iOS 17 that would happen on startup Signed-off-by: Daniel D’Aquino --- nostrdb/NdbUseLock.swift | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/nostrdb/NdbUseLock.swift b/nostrdb/NdbUseLock.swift index 38c433e3..87a7e409 100644 --- a/nostrdb/NdbUseLock.swift +++ b/nostrdb/NdbUseLock.swift @@ -7,6 +7,7 @@ import Dispatch import Synchronization +import Foundation extension Ndb { /// Creates a `sync` mechanism for coordinating usages of ndb (read or write) with the app's ability to close ndb. @@ -94,6 +95,11 @@ extension Ndb { class FallbackUseLock: UseLockProtocol { /// Number of functions using the `ndb` object (for reading or writing data) private var ndbUserCount: UInt = 0 + /// Lock for protecting access to `ndbUserCount` + private let ndbUserCountLock = NSLock() + /// Lock for protecting access to `ndbIsOpen` + private let ndbIsOpenLock = NSLock() + private var ndbIsOpen: Bool = false /// Semaphore for general access to `ndb`. A closing task requires exclusive access. Users of `ndb` (read/write tasks) share the access private let ndbAccessSemaphore: DispatchSemaphore = DispatchSemaphore(value: 0) /// How long a thread can block before throwing an error @@ -120,18 +126,29 @@ extension Ndb { /// Implementation note: NEVER change this to `async`! This is a blocking operation, so we want to minimize the time of the operation func waitUntilNdbCanClose(thenClose operation: () -> Bool, maxTimeout: DispatchTimeInterval = DEFAULT_TIMEOUT) throws { try ndbAccessSemaphore.waitOrThrow(timeout: .now() + maxTimeout) - let ndbIsOpen = operation() + ndbIsOpenLock.lock() + ndbIsOpen = operation() if ndbIsOpen { ndbAccessSemaphore.signal() } + ndbIsOpenLock.unlock() } /// Marks `ndb` as open to allow other users to use it. Do not call this more than once func markNdbOpen() { - ndbAccessSemaphore.signal() + ndbIsOpenLock.lock() + if !ndbIsOpen { + ndbIsOpen = true + ndbAccessSemaphore.signal() + } + ndbIsOpenLock.unlock() } private func incrementUserCount(maxTimeout: DispatchTimeInterval = .seconds(2)) throws { + ndbUserCountLock.lock() + defer { ndbUserCountLock.unlock() } + + // Signal that ndb cannot close while we have at least one user using ndb if ndbUserCount == 0 { try ndbAccessSemaphore.waitOrThrow(timeout: .now() + maxTimeout) } @@ -139,6 +156,9 @@ extension Ndb { } private func decrementUserCount() { + ndbUserCountLock.lock() + defer { ndbUserCountLock.unlock() } + ndbUserCount -= 1 // Signal that ndb can close if we have zero users using ndb if ndbUserCount == 0 {