Skip to content

Conversation

@dreampiggy
Copy link
Contributor

@dreampiggy dreampiggy commented Dec 2, 2025

New Pull Request Checklist

  • I have read and understood the CONTRIBUTING guide

  • I have read the Documentation

  • I have searched for a similar pull request in the project and found none

  • I have updated this branch with the latest master to avoid conflicts (via merge from master or rebase)

  • I have added the required tests to prove the fix/feature I am adding

  • I have updated the documentation (if necessary)

  • I have run the tests and they pass

  • I have run the lint and it passes (pod lib lint)

This merge request fixes / refers to the following issues: ...

Pull Request Description

This close #3842

Maybe there are better way to fix, but this is the simplest one in my mind

Summary by CodeRabbit

  • Bug Fixes
    • Improved stability and reliability of image download operations by preventing race conditions when starting and resuming downloads.
    • Ensured task priority settings and resume happen in a safe sequence to avoid unexpected behavior under concurrent download scenarios.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 2, 2025

Walkthrough

This change modifies SDWebImageDownloaderOperation's start to use a local dataTask variable, set self.dataTask and self.executing inside a synchronized block, and resume the URL session task only after those assignments to prevent a race when starting the task.

Changes

Cohort / File(s) Summary
Thread-safety fix in downloader operation
SDWebImage/Core/SDWebImageDownloaderOperation.m
Use a local dataTask in start; apply priority settings to the local task; assign self.dataTask = dataTask and self.executing = YES inside a synchronized block; resume the task after those assignments (removes immediate resume outside the sync block).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10–15 minutes

  • Review synchronized block covers all state mutations (self.dataTask, self.executing) and any notifications posted while synchronized.
  • Confirm priority-setting moved to the local task and that no code path still references self.dataTask before assignment.
  • Check for any potential retain-cycle or lifetime changes introduced by holding the local dataTask until resume.
  • Validate behavior on targeted runtimes (iOS 26) and older iOS versions if possible.

Poem

🐰 I found a race that made code frown,
I wrapped the start in a tidy gown.
Assign then resume, no frantic leap—
Now downloads wake from a peaceful sleep. 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements a partial fix (thread-safe dataTask initialization) but lacks comprehensive testing, validation on iOS 26, and documentation of the fix, which are key objectives from issue #3842. Add unit/integration and stress tests for multithreaded scenarios, validate the fix on iOS 26 simulator/devices, confirm SPM compatibility, and include release notes documenting the iOS 26-specific fix.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: moving dataTask property initialization into a synchronized lock to address thread-safety issues in SDWebImageDownloaderOperation.
Out of Scope Changes check ✅ Passed All changes are directly related to addressing thread-safety in SDWebImageDownloaderOperation dataTask initialization, which is within the scope of issue #3842.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 29a3921 and ca209c3.

📒 Files selected for processing (1)
  • SDWebImage/Core/SDWebImageDownloaderOperation.m (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build Library
  • GitHub Check: Unit Test (macOS)
  • GitHub Check: Cocoapods Lint
  • GitHub Check: Cocoapods Demo
🔇 Additional comments (1)
SDWebImage/Core/SDWebImageDownloaderOperation.m (1)

171-249: Cancel/start race window allows unowned session task to resume after cancellation

There is a race condition window between lines 232 and 246: dataTask is created but self.dataTask remains nil. If -cancel is called during this window with an unownedSession:

  • cancelInternal checks if (self.dataTask) which is false (still nil), so the underlying task is not canceled via [self.dataTask cancel].
  • reset() does not invalidate an unowned session, so the task persists in the session.
  • When start re-enters and calls [self.dataTask resume], the task proceeds to execution without user callbacks (tokens were cleared during cancellation), causing unexpected network activity.

To restore cancel semantics, assign self.dataTask = dataTask in the first @synchronized(self) block immediately after task creation. Configure priority on the local dataTask reference, then in the second @synchronized(self) block, copy tokens and flip executing, but use the local dataTask for resume() rather than re-assigning self.dataTask. This ensures cancel() can always see and cancel the task, while priority and resume operations use the local strong reference and avoid re-assignment races.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…alization into synchronized lock to avoid thread-safe issue
@dreampiggy dreampiggy force-pushed the fix/try_dataTask_thread_safe branch from 29a3921 to ca209c3 Compare December 2, 2025 10:52
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
SDWebImage/Core/SDWebImageDownloaderOperation.m (1)

235-255: Consider rechecking isCancelled before assigning self.dataTask / executing and resuming

There is still a small window between the first and second @synchronized(self) blocks where cancel can run:

  • start has already created dataTask and left the first @synchronized(self).
  • Another thread calls cancel, which (via cancelInternal and reset) can:
    • Fire completion blocks and clear callbackTokens.
    • Invalidate the session.
    • Leave the operation in a cancelled state (isCancelled == YES) but with isExecuting == NO and isFinished == NO.

After that, the second @synchronized(self) in start will still execute, set self.dataTask = dataTask; self.executing = YES;, and [self.dataTask resume];, effectively “reviving” an operation that was just cancelled and had its callbacks fired.

This is a pre‑existing race, not introduced by this PR, but it becomes more visible now that dataTask handling is tightened. To align cancel semantics with expectations (cancel-before-or-during-start should prevent the request from ever starting), you could recheck cancellation inside the second synchronized block before assigning state:

-    if (dataTask) {
+    if (dataTask) {
         if (self.options & SDWebImageDownloaderHighPriority) {
             dataTask.priority = NSURLSessionTaskPriorityHigh;
         } else if (self.options & SDWebImageDownloaderLowPriority) {
             dataTask.priority = NSURLSessionTaskPriorityLow;
         } else {
             dataTask.priority = NSURLSessionTaskPriorityDefault;
         }
-        NSArray<SDWebImageDownloaderOperationToken *> *tokens;
-        @synchronized (self) {
-            tokens = [self.callbackTokens copy];
-            self.dataTask = dataTask;
-            self.executing = YES;
-        }
+        BOOL shouldStart = NO;
+        NSArray<SDWebImageDownloaderOperationToken *> *tokens = nil;
+        @synchronized (self) {
+            if (!self.isCancelled && !self.isFinished) {
+                tokens = [self.callbackTokens copy];
+                self.dataTask = dataTask;
+                self.executing = YES;
+                shouldStart = YES;
+            }
+        }
+        if (!shouldStart) {
+            // Operation was cancelled/finished after task creation; do not resume.
+            return;
+        }
         for (SDWebImageDownloaderOperationToken *token in tokens) {
             ...
         }
         // Put the URLSession task resume at the end of start method to avoid race condition
         [self.dataTask resume];
         ...

This keeps the “resume at end” behavior, but guarantees you never mark the operation executing or resume a task after a concurrent cancel/reset has already run.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e53f622 and 29a3921.

📒 Files selected for processing (1)
  • SDWebImage/Core/SDWebImageDownloaderOperation.m (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Unit Test (visionOS)
  • GitHub Check: Unit Test (macOS)
  • GitHub Check: Unit Test (iOS)
  • GitHub Check: Cocoapods Demo
  • GitHub Check: Cocoapods Lint
🔇 Additional comments (1)
SDWebImage/Core/SDWebImageDownloaderOperation.m (1)

170-233: dataTask creation inside @synchronized(self) is a solid thread-safety improvement

Creating the NSURLSessionTask inside the synchronized block and using a local dataTask variable avoids unsynchronized writes to self.dataTask versus cancel/reset, and keeps the lock held only while touching shared state. This change looks correct and clearly improves the data‑task lifetime handling around start.

@dreampiggy dreampiggy merged commit e890a1d into SDWebImage:master Dec 3, 2025
6 of 8 checks passed
@dreampiggy dreampiggy deleted the fix/try_dataTask_thread_safe branch December 3, 2025 03:31
@dreampiggy dreampiggy added this to the 5.21.5 milestone Dec 3, 2025
@dreampiggy dreampiggy added the thread safe Thread Safe related topic label Dec 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

thread safe Thread Safe related topic

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crash in SDWebImageDownloaderOperation start (objc_retain) on iOS 26 only

1 participant