Skip to content

Commit d06b7c3

Browse files
committed
Reject unsafe Simulator ownership claims
1 parent a34a778 commit d06b7c3

13 files changed

Lines changed: 153 additions & 48 deletions

‎Packages/macOS/CmuxSimulator/Sources/CmuxSimulator/Control/SimulatorControlService+Location.swift‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ extension SimulatorControlService {
1515
coordinate: SimulatorLocationCoordinate
1616
) async throws {
1717
let previousRoute = activeLocationRoutes[deviceID]
18-
let token = await beginLocationOperation(deviceID: deviceID)
18+
let token = try await beginLocationOperation(deviceID: deviceID)
1919
do {
2020
_ = try await output(arguments: [
2121
"simctl", "location", deviceID, "set", coordinateArgument(coordinate),
@@ -40,7 +40,7 @@ extension SimulatorControlService {
4040

4141
private func clearLocationExclusively(deviceID: String) async throws {
4242
let previousRoute = activeLocationRoutes[deviceID]
43-
let token = await beginLocationOperation(deviceID: deviceID)
43+
let token = try await beginLocationOperation(deviceID: deviceID)
4444
do {
4545
_ = try await output(arguments: ["simctl", "location", deviceID, "clear"])
4646
try await requireCurrentLocationOperation(deviceID: deviceID, token: token)
@@ -73,7 +73,7 @@ extension SimulatorControlService {
7373
initialCoordinate: SimulatorLocationCoordinate
7474
) async throws {
7575
let previousRoute = activeLocationRoutes[deviceID]
76-
let token = await beginLocationOperation(deviceID: deviceID)
76+
let token = try await beginLocationOperation(deviceID: deviceID)
7777
do {
7878
try await runLocationRouteCommand(deviceID: deviceID, route: route)
7979
} catch {
@@ -528,9 +528,9 @@ extension SimulatorControlService {
528528
}
529529
}
530530

531-
private func beginLocationOperation(deviceID: String) async -> UUID {
531+
private func beginLocationOperation(deviceID: String) async throws -> UUID {
532+
let token = try await locationOwnershipRegistry.claim(deviceIdentifier: deviceID)
532533
cancelLocationLifecycle(deviceID: deviceID)
533-
let token = await locationOwnershipRegistry.claim(deviceIdentifier: deviceID)
534534
locationRouteTokens[deviceID] = token
535535
return token
536536
}

‎Packages/macOS/CmuxSimulator/Sources/CmuxSimulator/Control/SimulatorLocationOwnershipScope.swift‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ actor SimulatorLocationOwnershipRegistry {
3131
self.store = store
3232
}
3333

34-
func claim(deviceIdentifier: String) -> UUID {
35-
let publishedToken = store.claim(namespace: "location", components: [deviceIdentifier])
34+
func claim(deviceIdentifier: String) throws -> UUID {
35+
let publishedToken = try store.claim(
36+
namespace: "location",
37+
components: [deviceIdentifier]
38+
)
3639
tokenByDeviceIdentifier[deviceIdentifier] = publishedToken
3740
return publishedToken
3841
}

‎Packages/macOS/CmuxSimulator/Sources/CmuxSimulator/Mutations/SimulatorCrossProcessOwnershipStore.swift‎

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,17 @@ struct SimulatorCrossProcessOwnershipStore: Sendable {
1111
.appendingPathComponent("com.cmux.simulator-ownership", isDirectory: true)
1212
}
1313

14-
func claim(namespace: String, components: [String]) -> UUID {
14+
func claim(namespace: String, components: [String]) throws -> UUID {
1515
let token = UUID()
16-
do {
17-
try FileManager().createDirectory(
18-
at: directory,
19-
withIntermediateDirectories: true,
20-
attributes: [.posixPermissions: 0o700]
21-
)
22-
try Data(token.uuidString.utf8).write(
23-
to: fileURL(namespace: namespace, components: components),
24-
options: .atomic
25-
)
26-
} catch {
27-
// A claim that cannot be published must never validate as current.
28-
}
16+
try FileManager().createDirectory(
17+
at: directory,
18+
withIntermediateDirectories: true,
19+
attributes: [.posixPermissions: 0o700]
20+
)
21+
try Data(token.uuidString.utf8).write(
22+
to: fileURL(namespace: namespace, components: components),
23+
options: .atomic
24+
)
2925
return token
3026
}
3127

‎Packages/macOS/CmuxSimulator/Sources/CmuxSimulator/WorkerClient/SimulatorCameraCleanupCoordinator.swift‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,22 @@ actor SimulatorCameraCleanupCoordinator {
1212
private var tail: Task<Void, Never>?
1313
private var revision: UInt64 = 0
1414
private var ownerByTarget: [Target: UUID] = [:]
15-
private let ownershipStore = SimulatorCrossProcessOwnershipStore()
15+
private let ownershipStore: SimulatorCrossProcessOwnershipStore
1616

17-
func claim(deviceIdentifier: String, bundleIdentifier: String) async -> UUID {
17+
init(
18+
ownershipStore: SimulatorCrossProcessOwnershipStore =
19+
SimulatorCrossProcessOwnershipStore()
20+
) {
21+
self.ownershipStore = ownershipStore
22+
}
23+
24+
func claim(deviceIdentifier: String, bundleIdentifier: String) async throws -> UUID {
1825
while let pendingCleanup = tail {
1926
let observedRevision = revision
2027
await pendingCleanup.value
2128
if revision == observedRevision { break }
2229
}
23-
let owner = ownershipStore.claim(
30+
let owner = try ownershipStore.claim(
2431
namespace: "camera",
2532
components: [deviceIdentifier, bundleIdentifier]
2633
)

‎Packages/macOS/CmuxSimulator/Sources/CmuxSimulator/WorkerClient/SimulatorWorkerClient+Actions.swift‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,6 @@ extension SimulatorWorkerClient {
241241
)
242242
)
243243
}
244-
for bundleIdentifier in cameraReplayConfigurations.compactMap(\.targetBundleIdentifier) {
245-
await claimCameraCleanupOwnership(bundleIdentifier: bundleIdentifier)
246-
}
247244
let requestID = UUID()
248245
let succeeded: Bool = try await requestWorkerValue(
249246
sending: .switchCameraSource(
@@ -281,9 +278,6 @@ extension SimulatorWorkerClient {
281278
)
282279
)
283280
}
284-
if let bundleIdentifier = configuration.targetBundleIdentifier {
285-
await claimCameraCleanupOwnership(bundleIdentifier: bundleIdentifier)
286-
}
287281
let requestID = UUID()
288282
let confirmation: SimulatorCameraConfigurationConfirmation = try await requestWorkerValue(
289283
sending: .configureCamera(requestID: requestID, configuration: configuration),

‎Packages/macOS/CmuxSimulator/Sources/CmuxSimulator/WorkerClient/SimulatorWorkerClient+CameraCleanup.swift‎

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,43 @@ extension SimulatorWorkerClient {
2020
)
2121
}
2222

23-
func claimCameraCleanupOwnership(bundleIdentifier: String) async {
23+
func claimCameraCleanupOwnership(bundleIdentifier: String) async throws {
2424
guard !bundleIdentifier.isEmpty,
2525
let deviceIdentifier = simulatorAttachedDeviceIdentifier(from: lastAttachment)
2626
else { return }
27-
cameraCleanupBundleIdentifiers.insert(bundleIdentifier)
28-
cameraCleanupOwners[bundleIdentifier] = await cameraCleanupCoordinator.claim(
27+
let owner = try await cameraCleanupCoordinator.claim(
2928
deviceIdentifier: deviceIdentifier,
3029
bundleIdentifier: bundleIdentifier
3130
)
31+
cameraCleanupBundleIdentifiers.insert(bundleIdentifier)
32+
cameraCleanupOwners[bundleIdentifier] = owner
33+
}
34+
35+
func prepareCameraCleanupOwnership(
36+
for message: SimulatorWorkerInbound
37+
) async throws {
38+
switch message {
39+
case let .configureCamera(_, configuration):
40+
if let bundleIdentifier = configuration.targetBundleIdentifier {
41+
try await claimCameraCleanupOwnership(bundleIdentifier: bundleIdentifier)
42+
}
43+
case .switchCameraSource:
44+
for bundleIdentifier in cameraReplayConfigurations.compactMap(
45+
\.targetBundleIdentifier
46+
) {
47+
try await claimCameraCleanupOwnership(bundleIdentifier: bundleIdentifier)
48+
}
49+
default:
50+
break
51+
}
52+
}
53+
54+
func cameraOwnershipFailure(_ error: Error) -> SimulatorFailure {
55+
SimulatorFailure(
56+
code: "simulator_camera_ownership_unavailable",
57+
message: error.localizedDescription,
58+
isRecoverable: true
59+
)
3260
}
3361

3462
func sendClosingMessages(shutdown: Bool) {

‎Packages/macOS/CmuxSimulator/Sources/CmuxSimulator/WorkerClient/SimulatorWorkerClient+InputState.swift‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@ extension SimulatorWorkerClient {
2929
pendingTextInputUsages[requestID] = Set(sequence.events.map(\.usage))
3030
case let .configureCamera(requestID, configuration):
3131
cameraRequestConfigurations[requestID] = configuration
32-
if let bundleIdentifier = configuration.targetBundleIdentifier {
33-
await claimCameraCleanupOwnership(bundleIdentifier: bundleIdentifier)
34-
}
3532
case let .switchCameraSource(requestID, configuration):
3633
cameraSourceSwitchRequests[requestID] = configuration
3734
case let .setCameraMirror(requestID, mode):

‎Packages/macOS/CmuxSimulator/Sources/CmuxSimulator/WorkerClient/SimulatorWorkerClient+Lifecycle.swift‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,20 @@ extension SimulatorWorkerClient {
173173
wasReplay: wasReplay
174174
)
175175
if wasReplay { await driveReplay() }
176-
case let .cameraTargetResolved(_, bundleIdentifier):
177-
if !bundleIdentifier.isEmpty {
178-
await claimCameraCleanupOwnership(bundleIdentifier: bundleIdentifier)
176+
case let .cameraTargetResolved(requestID, bundleIdentifier):
177+
do {
178+
if !bundleIdentifier.isEmpty {
179+
try await claimCameraCleanupOwnership(bundleIdentifier: bundleIdentifier)
180+
}
181+
} catch {
182+
let failure = cameraOwnershipFailure(error)
183+
await broadcast(.message(.requestFailure(
184+
requestID: requestID,
185+
failure
186+
)))
187+
discardWorker(intentional: true, clearReplayState: false)
188+
await broadcast(.workerStopped)
189+
return
179190
}
180191
acknowledgeCameraTargetResolution(for: message)
181192
case let .cameraMirror(requestID, succeeded):
@@ -457,6 +468,15 @@ extension SimulatorWorkerClient {
457468
!shouldDeferMessage(message),
458469
let child else { return }
459470
deferredMessages.removeFirst()
471+
do {
472+
try await prepareCameraCleanupOwnership(for: message)
473+
} catch {
474+
let failure = cameraOwnershipFailure(error)
475+
failDeferredDelivery(for: message, with: failure)
476+
await broadcast(.message(.failure(failure)))
477+
await flushDeferredMessageIfReady()
478+
return
479+
}
460480
do {
461481
try child.send(JSONEncoder().encode(message))
462482
await remember(message)

‎Packages/macOS/CmuxSimulator/Sources/CmuxSimulator/WorkerClient/SimulatorWorkerClient+Replay.swift‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ extension SimulatorWorkerClient {
5858
return
5959
}
6060
let message = replayMessages.removeFirst()
61+
do {
62+
try await prepareCameraCleanupOwnership(for: message)
63+
} catch {
64+
let failure = cameraOwnershipFailure(error)
65+
await broadcast(.message(.failure(failure)))
66+
discardWorker(intentional: true, clearReplayState: false)
67+
await broadcast(.workerStopped)
68+
return
69+
}
6170
do {
6271
await remember(message)
6372
if let requestIdentifier = message.requestIdentifier {

‎Packages/macOS/CmuxSimulator/Sources/CmuxSimulator/WorkerClient/SimulatorWorkerClient.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@ public actor SimulatorWorkerClient: SimulatorPaneClient {
443443
try await deferMessageUntilDelivered(message)
444444
return
445445
}
446+
try await prepareCameraCleanupOwnership(for: message)
446447
do {
447448
try connection.send(data)
448449
} catch {

0 commit comments

Comments
 (0)