taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

commit 666387dd465ebfc651137433d54ec0a837b2f566
parent d35cc0b39011f4efa1ae2668ef944ca3c6d0b494
Author: Marc Stibane <marc@taler.net>
Date:   Thu, 20 Aug 2026 19:34:28 +0200

AI: cancellation can double-resume

Diffstat:
MTalerCommon/Helper/AsyncSemaphore.swift | 34++++++++++++++++++++++------------
1 file changed, 22 insertions(+), 12 deletions(-)

diff --git a/TalerCommon/Helper/AsyncSemaphore.swift b/TalerCommon/Helper/AsyncSemaphore.swift @@ -234,21 +234,31 @@ public final class AsyncSemaphore: @unchecked Sendable { @discardableResult public func signal() -> Bool { lock() - + value += 1 - - switch suspensions.popLast()?.state { // FIFO - case let .suspendedUnlessCancelled(continuation): - unlock() - continuation.resume() - return true - case let .suspended(continuation): - unlock() - continuation.resume() - return true - default: + + guard let suspension = suspensions.popLast() else { // FIFO unlock() return false } + switch suspension.state { + case let .suspendedUnlessCancelled(continuation): + // Mark this suspension as spent *before* unlocking. Otherwise, if + // the waiting task is cancelled concurrently, its `onCancel` + // handler (TalerCommon/Helper/AsyncSemaphore.swift) would still + // see `.suspendedUnlessCancelled` on `suspension.state` (we never + // mutated it) and resume `continuation` a second time. + suspension.state = .cancelled + unlock() + continuation.resume() + return true + case let .suspended(continuation): + unlock() + continuation.resume() + return true + default: + unlock() + return false + } } }