From 2d676d6460675d2552d7af5e1ea4eda45f0ae5a1 Mon Sep 17 00:00:00 2001 From: Rocuts Date: Mon, 2 Mar 2026 12:31:19 -0500 Subject: [PATCH] iOS: fix data races with OSAllocatedUnfairLock in TLS probe and camera delegates (cherry picked from commit 96a1edcdc2f7aa30e1ee69ef96b5a30f600a73c4) --- .../ios/Sources/Camera/CameraController.swift | 29 ++++++++++----- .../Gateway/GatewayConnectionController.swift | 35 +++++++++++++------ 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/apps/ios/Sources/Camera/CameraController.swift b/apps/ios/Sources/Camera/CameraController.swift index 115f36346dc..6b7a0db892c 100644 --- a/apps/ios/Sources/Camera/CameraController.swift +++ b/apps/ios/Sources/Camera/CameraController.swift @@ -1,6 +1,7 @@ import AVFoundation import OpenClawKit import Foundation +import os actor CameraController { struct CameraDeviceInfo: Codable, Sendable { @@ -260,7 +261,7 @@ actor CameraController { private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate { private let continuation: CheckedContinuation - private var didResume = false + private let resumed = OSAllocatedUnfairLock(initialState: false) init(_ continuation: CheckedContinuation) { self.continuation = continuation @@ -271,8 +272,12 @@ private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegat didFinishProcessingPhoto photo: AVCapturePhoto, error: Error? ) { - guard !self.didResume else { return } - self.didResume = true + let alreadyResumed = self.resumed.withLock { old in + let was = old + old = true + return was + } + guard !alreadyResumed else { return } if let error { self.continuation.resume(throwing: error) @@ -301,15 +306,19 @@ private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegat error: Error? ) { guard let error else { return } - guard !self.didResume else { return } - self.didResume = true + let alreadyResumed = self.resumed.withLock { old in + let was = old + old = true + return was + } + guard !alreadyResumed else { return } self.continuation.resume(throwing: error) } } private final class MovieFileDelegate: NSObject, AVCaptureFileOutputRecordingDelegate { private let continuation: CheckedContinuation - private var didResume = false + private let resumed = OSAllocatedUnfairLock(initialState: false) init(_ continuation: CheckedContinuation) { self.continuation = continuation @@ -321,8 +330,12 @@ private final class MovieFileDelegate: NSObject, AVCaptureFileOutputRecordingDel from connections: [AVCaptureConnection], error: Error?) { - guard !self.didResume else { return } - self.didResume = true + let alreadyResumed = self.resumed.withLock { old in + let was = old + old = true + return was + } + guard !alreadyResumed else { return } if let error { let ns = error as NSError diff --git a/apps/ios/Sources/Gateway/GatewayConnectionController.swift b/apps/ios/Sources/Gateway/GatewayConnectionController.swift index 53e32684988..259768a4df1 100644 --- a/apps/ios/Sources/Gateway/GatewayConnectionController.swift +++ b/apps/ios/Sources/Gateway/GatewayConnectionController.swift @@ -9,6 +9,7 @@ import Darwin import OpenClawKit import Network import Observation +import os import Photos import ReplayKit import Security @@ -990,12 +991,16 @@ extension GatewayConnectionController { #endif private final class GatewayTLSFingerprintProbe: NSObject, URLSessionDelegate, @unchecked Sendable { + private struct ProbeState { + var didFinish = false + var session: URLSession? + var task: URLSessionWebSocketTask? + } + private let url: URL private let timeoutSeconds: Double private let onComplete: (String?) -> Void - private var didFinish = false - private var session: URLSession? - private var task: URLSessionWebSocketTask? + private let state = OSAllocatedUnfairLock(initialState: ProbeState()) init(url: URL, timeoutSeconds: Double, onComplete: @escaping (String?) -> Void) { self.url = url @@ -1008,9 +1013,11 @@ private final class GatewayTLSFingerprintProbe: NSObject, URLSessionDelegate, @u config.timeoutIntervalForRequest = self.timeoutSeconds config.timeoutIntervalForResource = self.timeoutSeconds let session = URLSession(configuration: config, delegate: self, delegateQueue: nil) - self.session = session let task = session.webSocketTask(with: self.url) - self.task = task + self.state.withLock { s in + s.session = session + s.task = task + } task.resume() DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + self.timeoutSeconds) { [weak self] in @@ -1036,12 +1043,18 @@ private final class GatewayTLSFingerprintProbe: NSObject, URLSessionDelegate, @u } private func finish(_ fingerprint: String?) { - objc_sync_enter(self) - defer { objc_sync_exit(self) } - guard !self.didFinish else { return } - self.didFinish = true - self.task?.cancel(with: .goingAway, reason: nil) - self.session?.invalidateAndCancel() + let (shouldComplete, taskToCancel, sessionToInvalidate) = self.state.withLock { s -> (Bool, URLSessionWebSocketTask?, URLSession?) in + guard !s.didFinish else { return (false, nil, nil) } + s.didFinish = true + let task = s.task + let session = s.session + s.task = nil + s.session = nil + return (true, task, session) + } + guard shouldComplete else { return } + taskToCancel?.cancel(with: .goingAway, reason: nil) + sessionToInvalidate?.invalidateAndCancel() self.onComplete(fingerprint) }