fix(image): fix data race in imageURLLoaderForURL: and imageDataDecoderForData:#57297
fix(image): fix data race in imageURLLoaderForURL: and imageDataDecoderForData:#57297mfazekas wants to merge 2 commits into
Conversation
…rForData imageURLLoaderForURL: had a broken double-checked lock on _loaders: the outer if (!_loaders) guard and the for-loop iteration both ran without _loadersMutex, while the assignment ran inside it. imageDataDecoderForData: had no lock at all on _decoders. Both methods write the ivar in two steps (raw list then sorted), so a concurrent reader could observe the intermediate value and iterate a concurrently-released array, crashing with EXC_BAD_ACCESS in objc_release. Fix mirrors the setUp() pattern from react#46153: add std::atomic<BOOL> flags (_loadersReady / _decodersReady) as acquire/release guards, protect the slow path with _loadersMutex, and iterate a local strong ref captured under the lock so the shared ivar is never read outside it. Verified with a reproducer (crashes 5/5 without the fix, 0/10 with it): https://github.com/mfazekas/rn-rctimageloader-dcl-repro Fixes: react#57296 See also: react#46115, react#46153
|
Warning Missing Test Plan Please add a "## Test Plan" section to your PR description. A Test Plan lets us know how these changes were tested. |
|
Nice fix on these two methods. I think one path is still open: Since |
…dleRequest: race Extract the DCL + init logic for _loaders and _decoders into private -loaders and -decoders accessors. All three readers (-imageURLLoaderForURL:, -imageDataDecoderForData:, -canHandleRequest:) now go through the accessor, closing the race in -canHandleRequest: identified in review and eliminating the duplicated DCL blocks.
|
@fabriziocucci has imported this pull request. If you are a Meta employee, you can view this in D109404243. |
Summary
imageURLLoaderForURL:had a broken double-checked lock on_loaders: the outerif (!_loaders)guard and theforloop iteration both ran without_loadersMutex, while the assignment ran inside it.imageDataDecoderForData:had no lock at all on_decoders. Both methods write the ivar in two steps (raw list then sorted), so a concurrent reader could observe the intermediate value and iterate a concurrently-released array, crashing withEXC_BAD_ACCESSinobjc_releaseon a GCD worker thread.Verified with a reproducer — crashes 5/5 without the fix, 0/10 with it: https://github.com/mfazekas/rn-rctimageloader-dcl-repro
Fix
The fix mirrors the
setUp()pattern from #46153: addstd::atomic<BOOL>flags (_loadersReady/_decodersReady) as acquire/release DCL guards, protect the slow path with_loadersMutex, and iterate a local strong ref captured under the lock so the shared ivar is never read outside it.Two simpler alternatives were considered and rejected:
_loadersMutexon every call, even after init): correct but ~40× slower on the hot path —imageURLLoaderForURL:is called for every image load.dispatch_once: correct and lock-free after init, but involves a function call into libdispatch on every call.The atomic DCL approach matches what
setUp()already does and compiles to a singleldar(load-acquire) instruction on ARM64 on the hot path — effectively free after first use.Related
Fixes #57296
See also #46115, #46153