From 13614e7fa014d9cc8de051865b50497bcc912c5e Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Thu, 19 Feb 2026 17:25:14 +0100 Subject: [PATCH] Fix CFNetwork SIGABRT crash when creating download tasks on invalidated session The background URLSession could be in an invalid state when downloadTask(with:) is called, because invalidateAndCancel() is asynchronous internally. This adds an ObjC exception handler to catch NSExceptions from CFNetwork, nil guards on the session, and safer session lifecycle management (nil after invalidation, finishTasksAndInvalidate for cellular toggle). --- Yattee/Helpers/ObjCExceptionHandler.h | 17 ++++++ Yattee/Helpers/ObjCExceptionHandler.m | 20 +++++++ .../Downloads/DownloadManager+Execution.swift | 54 ++++++++++++++++++- .../Services/Downloads/DownloadManager.swift | 8 +-- Yattee/Yattee-Bridging-Header.h | 1 + 5 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 Yattee/Helpers/ObjCExceptionHandler.h create mode 100644 Yattee/Helpers/ObjCExceptionHandler.m diff --git a/Yattee/Helpers/ObjCExceptionHandler.h b/Yattee/Helpers/ObjCExceptionHandler.h new file mode 100644 index 00000000..a4d201fe --- /dev/null +++ b/Yattee/Helpers/ObjCExceptionHandler.h @@ -0,0 +1,17 @@ +// +// ObjCExceptionHandler.h +// Yattee +// +// Catches ObjC NSExceptions that Swift cannot handle natively. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Executes a block and catches any NSException thrown. +/// Returns YES if the block executed without throwing, NO if an exception was caught. +/// If an exception is caught, it is returned via the outException parameter. +BOOL tryCatchObjCException(void (NS_NOESCAPE ^block)(void), NSException *_Nullable *_Nullable outException); + +NS_ASSUME_NONNULL_END diff --git a/Yattee/Helpers/ObjCExceptionHandler.m b/Yattee/Helpers/ObjCExceptionHandler.m new file mode 100644 index 00000000..dac39604 --- /dev/null +++ b/Yattee/Helpers/ObjCExceptionHandler.m @@ -0,0 +1,20 @@ +// +// ObjCExceptionHandler.m +// Yattee +// +// Catches ObjC NSExceptions that Swift cannot handle natively. +// + +#import "ObjCExceptionHandler.h" + +BOOL tryCatchObjCException(void (NS_NOESCAPE ^block)(void), NSException *_Nullable *_Nullable outException) { + @try { + block(); + return YES; + } @catch (NSException *exception) { + if (outException) { + *outException = exception; + } + return NO; + } +} diff --git a/Yattee/Services/Downloads/DownloadManager+Execution.swift b/Yattee/Services/Downloads/DownloadManager+Execution.swift index 17644557..104e3498 100644 --- a/Yattee/Services/Downloads/DownloadManager+Execution.swift +++ b/Yattee/Services/Downloads/DownloadManager+Execution.swift @@ -99,10 +99,41 @@ extension DownloadManager { resumeData: Data?, httpHeaders: [String: String]? = nil ) { + guard urlSession != nil else { + LoggingService.shared.logDownloadError( + "[Downloads] URLSession is nil in startStreamDownload (\(phase))", + error: DownloadError.downloadFailed("URLSession not available - session may be invalidated") + ) + handleDownloadError( + downloadID: downloadID, + phase: phase, + error: DownloadError.downloadFailed("URLSession not available") + ) + return + } + let task: URLSessionDownloadTask if let resumeData { - task = urlSession.downloadTask(withResumeData: resumeData) + var caughtException: NSException? + var resumeTask: URLSessionDownloadTask? + let success = tryCatchObjCException({ + resumeTask = self.urlSession.downloadTask(withResumeData: resumeData) + }, &caughtException) + + guard success, let resumeTask else { + LoggingService.shared.logDownloadError( + "[Downloads] NSException creating resume task (\(phase))", + error: DownloadError.downloadFailed("CFNetwork exception: \(caughtException?.reason ?? "unknown")") + ) + handleDownloadError( + downloadID: downloadID, + phase: phase, + error: DownloadError.downloadFailed("Failed to create download task: \(caughtException?.reason ?? "unknown")") + ) + return + } + task = resumeTask } else { // Starting fresh without resumeData - reset progress for this phase // to avoid jumping when saved progress conflicts with new URLSession progress @@ -128,7 +159,26 @@ extension DownloadManager { request.setValue(value, forHTTPHeaderField: key) } } - task = urlSession.downloadTask(with: request) + + var caughtException: NSException? + var newTask: URLSessionDownloadTask? + let success = tryCatchObjCException({ + newTask = self.urlSession.downloadTask(with: request) + }, &caughtException) + + guard success, let newTask else { + LoggingService.shared.logDownloadError( + "[Downloads] NSException creating download task (\(phase))", + error: DownloadError.downloadFailed("CFNetwork exception: \(caughtException?.reason ?? "unknown")") + ) + handleDownloadError( + downloadID: downloadID, + phase: phase, + error: DownloadError.downloadFailed("Failed to create download task: \(caughtException?.reason ?? "unknown")") + ) + return + } + task = newTask } task.taskDescription = "\(downloadID.uuidString):\(phase.rawValue)" diff --git a/Yattee/Services/Downloads/DownloadManager.swift b/Yattee/Services/Downloads/DownloadManager.swift index 634fa34b..b49aed0d 100644 --- a/Yattee/Services/Downloads/DownloadManager.swift +++ b/Yattee/Services/Downloads/DownloadManager.swift @@ -148,8 +148,9 @@ final class DownloadManager: NSObject { self.downloadSettings = settings // Invalidate old session before creating new one with correct settings urlSession?.invalidateAndCancel() + urlSession = nil setupSession() - + // Resume interrupted downloads only on initial setup if isInitialSetup { resumeInterruptedDownloads() @@ -186,8 +187,9 @@ final class DownloadManager: NSObject { } } - // 3. Invalidate the old session - urlSession.invalidateAndCancel() + // 3. Invalidate the old session (use finishTasksAndInvalidate since downloads are already paused) + urlSession?.finishTasksAndInvalidate() + urlSession = nil // 4. Create new session with updated cellular config setupSession() diff --git a/Yattee/Yattee-Bridging-Header.h b/Yattee/Yattee-Bridging-Header.h index 5d592292..5e984a0c 100644 --- a/Yattee/Yattee-Bridging-Header.h +++ b/Yattee/Yattee-Bridging-Header.h @@ -8,4 +8,5 @@ #ifndef Yattee_Bridging_Header_h #define Yattee_Bridging_Header_h #import "SMBBridge.h" +#import "ObjCExceptionHandler.h" #endif /* Yattee_Bridging_Header_h */