Fix locking mechanism to avoid iOS 26 crash with progressiveLoad concurrency - #3876
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughTightened locking in SDImageIOAnimatedCoder: incremental image state updates, Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorLock the state transition, not just
CGImageSourceUpdateData.
_imageDataand_finishedare still updated before this new critical section. That leaves a window where another thread can observefinished == YESwhile_imageSourcestill 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
📒 Files selected for processing (1)
SDWebImage/Core/SDImageIOAnimatedCoder.m
There was a problem hiding this comment.
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 | 🟠 MajorLock
didReceiveMemoryWarning:around_imageSourcemutations.The progressive update path in
-updateIncrementalData:finished:is now properly serialized under lock, butdidReceiveMemoryWarning:still mutates the same shared_imageSourcewithout_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
📒 Files selected for processing (1)
SDWebImage/Core/SDImageIOAnimatedCoder.m
There was a problem hiding this comment.
🧹 Nitpick comments (1)
SDWebImage/Core/SDImageIOAnimatedCoder.m (1)
838-847: Consider moving the dimension gate inside the same lock.Line 838 reads
_width/_heightoutside 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
📒 Files selected for processing (1)
SDWebImage/Core/SDImageIOAnimatedCoder.m
This will break all the assumptions as well, not only the code here, like |
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? |
|
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 |
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