Fix premature blob deallocation during FileReader reads - #57796
Fix premature blob deallocation during FileReader reads#57796heecheolman wants to merge 1 commit into
Conversation
FileReader dispatched the native read with only the plain blob.data descriptor and kept no reference to the Blob instance. If the caller also drops its reference (which is exactly what whatwg-fetch's readBlobAsText does, i.e. every fetch().json() in React Native), the Blob and its attached BlobCollector become unreachable while the native read is still queued. When GC runs in that window, the collector's finalizer removes the bytes from BlobModule's store and the read rejects with "The specified blob is invalid". Retain the Blob on the FileReader instance until the read settles, so the reference chain pending native promise -> callbacks -> reader -> _blob -> Blob -> collector keeps the native buffer alive for the duration of the read. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Hi @heecheolman! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
|
@fabriziocucci has imported this pull request. If you are a Meta employee, you can view this in D114576384. |
Summary:
FileReader.readAsText/readAsDataURL/readAsArrayBufferpass only the plainblob.datadescriptor to the native module and retain no reference to theBlobinstance itself. If the caller also drops its reference, the Blob — and theBlobCollectorattached toblob.data.__collector— becomes unreachable while the native read is still in flight. When GC runs in that window, the collector's finalizer unconditionally removes the bytes from the native blob store (BlobCollector.cppcallsBlobModule.remove()on Android;RCTBlobCollector.mmcalls[RCTBlobManager remove:]on iOS), and the pending read rejects with "The specified blob is invalid" (Android) / "Unable to resolve data for blob" (iOS).This is not an exotic case: React Native's fetch polyfill (whatwg-fetch) reads blob bodies exactly this way —
readBlobAsTextcreates aFileReader, callsreader.readAsText(blob), and keeps a reference only to the reader. So a plainfetch(url).then(r => r.json()), where theResponseis not otherwise retained, is subject to this race. This matches the symptom profile of #56884: intermittent failures under many concurrent fetches (GC pressure plus native-module thread-hop latency), affecting both platforms, and disappearing when the same flow is rewritten withasync/await— the suspended frame keeps theResponse(and therefore the Blob and its collector) reachable, which is exactly the reference this fix restores.The fix retains the Blob on the FileReader instance until the native read settles, completing the reference chain: pending native promise → callbacks → reader →
_blob→ Blob → collector. The reference is cleared when the current read settles (after the existing read-id staleness check, so a read abandoned byabort()cannot drop a newer read's reference) and inabort()itself, before the abort event is dispatched, so a read started from an abort handler is retained correctly. Memory impact is negligible: the native bytes must live until the read completes anyway — this change only guarantees they do.The root cause is in the shared JS layer, so both Android and iOS are fixed.
Fixes #56884
Related prior art: #31392 fixed a different premature-deallocation path in the same subsystem (
blob.slice()creating a second collector for the same blobId).Changelog:
[GENERAL] [FIXED] - Retain Blob reference in FileReader during pending native reads to prevent premature deallocation by BlobCollector
Test Plan:
yarn jest packages/react-native/Libraries/Blob/__tests__/FileReader-test.js— 19 passed, including 4 new tests: the blob is retained while a read is pending, released on resolve / reject /abort(), and a stale read settling after abort does not drop a newer read's blob.yarn flow check— no errors.eslinton both changed files — clean.response.text()fails with "Unable to resolve data for blob" under concurrent fetch (Hermes, iOS device) #56884.