Skip to content

Fix locking mechanism to avoid iOS 26 crash with progressiveLoad concurrency - #3876

Merged
dreampiggy merged 4 commits into
SDWebImage:masterfrom
chrisngabp:master
Apr 15, 2026
Merged

dreampiggy merged 4 commits into
SDWebImage:masterfrom
chrisngabp:master

Conversation

@chrisngabp

@chrisngabp chrisngabp commented Apr 13, 2026 •

Copy link
Copy Markdown
Contributor

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:

#3839

Pull Request Description

Lock before updating the image source to prevent concurrent access from the frame fetch queue.
CGImageSource is not thread-safe for simultaneous read+write.

Summary by CodeRabbit

  • Bug Fixes
    • Improved thread-safety during incremental animated image decoding to prevent concurrent access to internal image data.
    • Prevented race conditions when updating incremental data and cache, reducing crashes and visual artifacts.
    • Stabilized first-frame creation and frame validation under concurrent decoding workloads.

@coderabbitai

coderabbitai Bot commented Apr 13, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 26e5dba6-f8f1-48cd-9ffb-12334496a65b

📥 Commits

Reviewing files that changed from the base of the PR and between 8fc1e78 and bb4ba99.

📒 Files selected for processing (1)
  • SDWebImage/Core/SDImageIOAnimatedCoder.m
🚧 Files skipped from review as they are similar to previous changes (1)
  • SDWebImage/Core/SDImageIOAnimatedCoder.m

📝 Walkthrough

Walkthrough

Tightened locking in SDImageIOAnimatedCoder: incremental image state updates, CGImageSourceUpdateData, cache removal, and first-frame creation are now performed inside the coder lock to prevent concurrent access to _imageSource and related state. (47 words)

Changes

Cohort / File(s) Summary
Animated Image Decoder Threading
SDWebImage/Core/SDImageIOAnimatedCoder.m
Expanded SD_LOCK(_lock) critical sections: (1) wrap cache removal in didReceiveMemoryWarning:, (2) move _imageData/_finished assignments and CGImageSourceUpdateData(...), scanAndCheckFramesValidWithImageSource:_imageSource, and size/thumbnail updates into the lock in updateIncrementalData:finished:, (3) protect first-frame creation in incrementalDecodedImageWithOptions:; moved image.sd_imageFormat assignment outside the lock.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

thread safe

Poem

🐇
I nibble bytes in moonlit queues,
I nest each frame where locks diffuse;
No race, no tumble, calm and neat,
Pixels align in cozy seats,
I thump a paw — the build's complete.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing a locking mechanism to prevent concurrent access crashes with progressive loading on iOS 26.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
SDWebImage/Core/SDImageIOAnimatedCoder.m (1)

781-795: ⚠️ Potential issue | 🟠 Major

Lock the state transition, not just CGImageSourceUpdateData.

_imageData and _finished are still updated before this new critical section. That leaves a window where another thread can observe finished == YES while _imageSource still contains the previous bytes.

Suggested fix
-    if (_finished) {
-        return;
-    }
-    _imageData = data;
-    _finished = finished;
-
-    // Lock before updating the image source to prevent concurrent access from the frame fetch queue.
-    // CGImageSource is not thread-safe for simultaneous read+write.
-    SD_LOCK(_lock);
+    // Lock before mutating any incremental state so readers observe
+    // `_imageData`, `_finished`, and `_imageSource` atomically.
+    SD_LOCK(_lock);
+    if (_finished) {
+        SD_UNLOCK(_lock);
+        return;
+    }
+    _imageData = data;
+    _finished = finished;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SDWebImage/Core/SDImageIOAnimatedCoder.m` around lines 781 - 795, Move the
state mutation into the locked critical section: acquire SD_LOCK(_lock) before
updating _imageData and _finished, then call
CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished) while
still holding the lock, and finally release the lock; this ensures readers
cannot observe _finished == YES while _imageSource still has old data—use the
existing SD_LOCK/_lock and CGImageSourceUpdateData identifiers to locate and
reorder the statements accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@SDWebImage/Core/SDImageIOAnimatedCoder.m`:
- Around line 781-795: Move the state mutation into the locked critical section:
acquire SD_LOCK(_lock) before updating _imageData and _finished, then call
CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished) while
still holding the lock, and finally release the lock; this ensures readers
cannot observe _finished == YES while _imageSource still has old data—use the
existing SD_LOCK/_lock and CGImageSourceUpdateData identifiers to locate and
reorder the statements accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 989ec2c7-ea62-4237-ab26-b23275836d69

📥 Commits

Reviewing files that changed from the base of the PR and between 4bb1649 and 7936ed7.

📒 Files selected for processing (1)
  • SDWebImage/Core/SDImageIOAnimatedCoder.m

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
SDWebImage/Core/SDImageIOAnimatedCoder.m (1)

787-822: ⚠️ Potential issue | 🟠 Major

Lock didReceiveMemoryWarning: around _imageSource mutations.

The progressive update path in -updateIncrementalData:finished: is now properly serialized under lock, but didReceiveMemoryWarning: still mutates the same shared _imageSource without _lock. A memory warning can interleave with the incremental update or frame fetch operations, creating a data race.

Suggested fix
 - (void)didReceiveMemoryWarning:(NSNotification *)notification
 {
+    if (_incremental) {
+        SD_LOCK(_lock);
+    }
     if (_imageSource) {
         for (size_t i = 0; i < _frameCount; i++) {
             CGImageSourceRemoveCacheAtIndex(_imageSource, i);
         }
     }
+    if (_incremental) {
+        SD_UNLOCK(_lock);
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SDWebImage/Core/SDImageIOAnimatedCoder.m` around lines 787 - 822,
didReceiveMemoryWarning: currently mutates the shared _imageSource without
synchronization, causing races with updateIncrementalData:finished: and frame
fetches; wrap the entire body of -didReceiveMemoryWarning: (the code paths that
call CFRelease/CFBridgingRelease/CGImageSourceRelease or otherwise
modify/replace _imageSource and related state like
_imageData/_finished/_frameCount) with SD_LOCK(_lock) / SD_UNLOCK(_lock) so
mutations use the same lock used in updateIncrementalData:finished:, ensuring
accesses to _imageSource, _imageData, _finished and any calls that replace or
reset the image source are performed while holding _lock.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@SDWebImage/Core/SDImageIOAnimatedCoder.m`:
- Around line 787-822: didReceiveMemoryWarning: currently mutates the shared
_imageSource without synchronization, causing races with
updateIncrementalData:finished: and frame fetches; wrap the entire body of
-didReceiveMemoryWarning: (the code paths that call
CFRelease/CFBridgingRelease/CGImageSourceRelease or otherwise modify/replace
_imageSource and related state like _imageData/_finished/_frameCount) with
SD_LOCK(_lock) / SD_UNLOCK(_lock) so mutations use the same lock used in
updateIncrementalData:finished:, ensuring accesses to _imageSource, _imageData,
_finished and any calls that replace or reset the image source are performed
while holding _lock.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 68073365-3a05-49e1-ba85-82c64fdc6443

📥 Commits

Reviewing files that changed from the base of the PR and between 7936ed7 and 41fa472.

📒 Files selected for processing (1)
  • SDWebImage/Core/SDImageIOAnimatedCoder.m

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

838-847: Consider moving the dimension gate inside the same lock.

Line 838 reads _width/_height outside synchronization. It’s low risk, but keeping the readiness check under the same lock would make the concurrency model fully consistent.

Suggested refactor
-    if (_width + _height > 0) {
+    SD_LOCK(_lock);
+    if (_width + _height > 0) {
         // Create the image
         CGFloat scale = _scale;
         NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
         if (scaleFactor != nil) {
             scale = MAX([scaleFactor doubleValue], 1);
         }
-        SD_LOCK(_lock);
         image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:NO decodeToHDR:_finished ? _decodeToHDR : NO];
-        SD_UNLOCK(_lock);
         if (image) {
             image.sd_imageFormat = self.class.imageFormat;
         }
     }
+    SD_UNLOCK(_lock);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SDWebImage/Core/SDImageIOAnimatedCoder.m` around lines 838 - 847, The
width/height readiness check is performed outside the mutex, which can race with
other state changes; move the `_width/_height > 0` gate inside the same lock
guarded by `_lock` and then call `+[self.class
createFrameAtIndex:source:scale:preserveAspectRatio:thumbnailSize:lazyDecode:animatedImage:decodeToHDR:]`
while holding that lock; ensure you `SD_LOCK(_lock)` before checking
`_width`/`_height` and only `SD_UNLOCK(_lock)` after the image is created (or
use early unlocks consistently) so the entire readiness check and frame creation
run under the same synchronization.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@SDWebImage/Core/SDImageIOAnimatedCoder.m`:
- Around line 838-847: The width/height readiness check is performed outside the
mutex, which can race with other state changes; move the `_width/_height > 0`
gate inside the same lock guarded by `_lock` and then call `+[self.class
createFrameAtIndex:source:scale:preserveAspectRatio:thumbnailSize:lazyDecode:animatedImage:decodeToHDR:]`
while holding that lock; ensure you `SD_LOCK(_lock)` before checking
`_width`/`_height` and only `SD_UNLOCK(_lock)` after the image is created (or
use early unlocks consistently) so the entire readiness check and frame creation
run under the same synchronization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aae88bce-03e5-456f-9296-d4c4849b5642

📥 Commits

Reviewing files that changed from the base of the PR and between 41fa472 and 8fc1e78.

📒 Files selected for processing (1)
  • SDWebImage/Core/SDImageIOAnimatedCoder.m

@dreampiggy

dreampiggy commented Apr 14, 2026 •

Copy link
Copy Markdown
Contributor

CGImageSource is not thread-safe for simultaneous read+write.

This will break all the assumptions as well, not only the code here, like SDAnimatedImagePlayer and SDImageFramePool

@chrisngabp

Copy link
Copy Markdown
Contributor Author
- CGImageSourceCreateWithURL / CGImageSourceCreateWithData — these are effectively immutable. The backing data is fixed at creation time and doesn't change.

- CGImageSourceCreateIncremental — this is explicitly mutable. You feed it data progressively via CGImageSourceUpdateData, and its internal state changes with each update.

I think that in this case, since the issue was related to progressiveLoad the lib is using the incremental I assume which is mutable. I can be more specific with the comment if it makes sense. Or were you thinking of something else?

@dreampiggy

dreampiggy commented Apr 15, 2026 •

Copy link
Copy Markdown
Contributor

CGImageSourceUpdateData seems only related to incremental loading, if this causes issues during multi-thread rendering. I think the correct way is to update the implementation in SDAnimatedImageIOCoder, which should lock the internal status during incremental loading.

Also, after fixing, can you do a test by yourself to ensure it's fixed? By simulating the incremental loading for large images and using ImageIO

@dreampiggy dreampiggy added ImageIO Anything related to Apple ImageIO codec apple bug apple's bug cause our framework author's pain labels Apr 15, 2026
@dreampiggy
dreampiggy merged commit c3ad5e1 into SDWebImage:master Apr 15, 2026
3 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

apple bug apple's bug cause our framework author's pain ImageIO Anything related to Apple ImageIO codec

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants