From ad8d70ebe0e2a3b471cf85358dffc9f23087780c Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 29 Jun 2026 14:24:00 +0300 Subject: [PATCH] Fix MediaLoader EXC_BAD_ACCESS `ongoingRequests` crashes --- .../Services/Media/Provider/MediaLoader.swift | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/ElementX/Sources/Services/Media/Provider/MediaLoader.swift b/ElementX/Sources/Services/Media/Provider/MediaLoader.swift index aa815b5f2..8f447c153 100644 --- a/ElementX/Sources/Services/Media/Provider/MediaLoader.swift +++ b/ElementX/Sources/Services/Media/Provider/MediaLoader.swift @@ -12,10 +12,6 @@ import UIKit @preconcurrency import MatrixRustSDK -private final nonisolated class MediaRequest { - var continuations: [CheckedContinuation] = [] -} - actor MediaLoader: MediaLoaderProtocol { // We noticed that the keyboard appears to hold onto a reference to the `Context` of the last // screen that had text input focus, resulting in its MediaProvider staying alive which in @@ -28,7 +24,7 @@ actor MediaLoader: MediaLoaderProtocol { // bunch of workarounds in our preview tests to keep the mock provider alive as some ViewModels // don't have an accompanying ClientMock to own it. private weak var client: ClientProtocol? - private var ongoingRequests = [MediaSourceProxy: MediaRequest]() + private var ongoingRequests = [MediaSourceProxy: Task]() init(client: ClientProtocol) { self.client = client @@ -61,30 +57,22 @@ actor MediaLoader: MediaLoaderProtocol { // MARK: - Private - private func enqueueLoadMediaRequest(forSource source: MediaSourceProxy, operation: @escaping () async throws -> Data) async throws -> Data { + private func enqueueLoadMediaRequest(forSource source: MediaSourceProxy, operation: @escaping @Sendable () async throws -> Data) async throws -> Data { if let ongoingRequest = ongoingRequests[source] { - return try await withCheckedThrowingContinuation { continuation in - ongoingRequest.continuations.append(continuation) - } + return try await ongoingRequest.value } - let ongoingRequest = MediaRequest() + // Wrap the SDK call in a Task so `ongoingRequests` is only mutated when resuming from + // `task.value` (a runtime-managed await that hops back onto the actor) rather than when + // resuming directly from the SDK's continuation, which can fire on a Rust thread and + // mutate the dictionary off-actor, corrupting its storage. + let ongoingRequest = Task { try await operation() } ongoingRequests[source] = ongoingRequest defer { ongoingRequests[source] = nil } - do { - let result = try await operation() - - ongoingRequest.continuations.forEach { $0.resume(returning: result) } - - return result - - } catch { - ongoingRequest.continuations.forEach { $0.resume(throwing: error) } - throw error - } + return try await ongoingRequest.value } }