Skip to content

fix(image): fix data race in imageURLLoaderForURL: and imageDataDecoderForData:#57297

Open
mfazekas wants to merge 1 commit into
react:mainfrom
mfazekas:fix/rctimageloader-loaders-decoders-dcl
Open

fix(image): fix data race in imageURLLoaderForURL: and imageDataDecoderForData:#57297
mfazekas wants to merge 1 commit into
react:mainfrom
mfazekas:fix/rctimageloader-loaders-decoders-dcl

Conversation

@mfazekas

@mfazekas mfazekas commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Summary

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 on 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: add std::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:

  • Always lock (always acquire _loadersMutex on 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 single ldar (load-acquire) instruction on ARM64 on the hot path — effectively free after first use.

Related

Fixes #57296
See also #46115, #46153

…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
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jun 20, 2026
@github-actions

Copy link
Copy Markdown

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.

Caution

Missing Changelog

Please add a Changelog to your PR description. See Changelog format

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS: RCTImageLoader._loaders/_decoders lazy init has broken double-checked locking — ARC over-release crash (unfixed half of #46115)

1 participant