Skip to content

Prevent native blob resource from being de-allocated prematurely - #31392

Closed
awinograd wants to merge 2 commits into
react:mainfrom
awinograd:patch-1
Closed

Prevent native blob resource from being de-allocated prematurely#31392
awinograd wants to merge 2 commits into
react:mainfrom
awinograd:patch-1

Conversation

@awinograd

Copy link
Copy Markdown
Contributor

Summary

This PR prevents blob data from being prematurely de-allocated in native code when using slice to create views into an existing blob. Currently, whenever a new blob is created via createFromOptions, BlobManager.js creates a new blobCollector object since options.__collector is never provided.

https://github.com/facebook/react-native/blob/dc80b2dcb52fadec6a573a9dd1824393f8c29fdc/Libraries/Blob/BlobManager.js#L115-L123

When the reference to a blobCollector is garbage collected, the corresponding native memory for the blob data is de-allocated.

https://github.com/facebook/react-native/blob/27651720b40cab564a0cbd41be56a02584e0c73a/Libraries/Blob/RCTBlobCollector.mm#L19-L25

Since, blob.slice() is supposed to create a new view onto the same binary data as the original blob, we need to re-use the same collector object when slicing so that it is not GC'd until the last reference to the binary data is no longer reachable. Currently, since each blob slice gets a new blobCollector object, the memory is de-allocated when the first blob is GC'd.

Fixes#29970
Fixes#27857

Changelog

[iOS] [Fixed] - Blob data is no longer prematurely deallocated when using blob.slice

Test Plan

I could use help coming up with a test plan here. I could add a referential equality check for the blob.data.__collector in Blob-test but it doesn't seem quite right to be testing the implementation detail there.

@facebook-github-botfacebook-github-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 Apr 20, 2021

@analysis-botanalysis-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.

Code analysis results:

  • eslint found some issues. Run yarn lint --fix to automatically fix problems.

Comment threadLibraries/Blob/Blob.js Outdated
Comment threadLibraries/Blob/Blob.js Outdated
@analysis-bot

analysis-bot commented Apr 20, 2021

Copy link
Copy Markdown
PlatformEngineArchSize (bytes)Diff
androidhermesarm64-v8a7,104,346+41
androidhermesarmeabi-v7a6,473,107+40
androidhermesx867,522,621+40
androidhermesx86_647,381,045+37
androidjscarm64-v8a8,971,785+7
androidjscarmeabi-v7a7,703,213+5
androidjscx869,034,393+6
androidjscx86_649,511,776-3

Base commit: e509007
Branch: main

@analysis-bot

analysis-bot commented Apr 20, 2021

Copy link
Copy Markdown
PlatformEngineArchSize (bytes)Diff
ios-universaln/a--

Base commit: 322a796
Branch: main

@facebook-github-botfacebook-github-bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Mar 28, 2022
@lachenmayer

Copy link
Copy Markdown

Hi there, we have been facing this issue for quite a while. We are using a resumable/chunked upload client called tus which uses Blob#slice() under the hood (see tus/tus-js-client#326 for a detailed bug report).

We have been applying this patch (using patch-package) for several months now, and we can confirm that it works. Also a bit unclear how you would test something like this other than saying it has massively reduced our crash rate :)

Without the patch we consistently see crashes with a *** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[0] exception.

One of my team members implemented a repro here if anyone is interested: samal-rasmussen/tus-js-client-slice-bug@112843b

@awinograd

Copy link
Copy Markdown
ContributorAuthor

@kelset sorry to bother you with a ping, but would be great to get some 👁️ on this PR. Is there anything I can do to entice a maintainer to take a look?

Since `blob.slice()` creates a new view onto the same binary
data as the original blob, we should re-use the same collector
object so that the underlying resource gets deallocated when
the last view into the data is released, not the first.
@facebook-github-bot

Copy link
Copy Markdown
Contributor

@cortinico has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

@pull-bot

Copy link
Copy Markdown

PR build artifact for d064dfe is ready.
To use, download tarball from "Artifacts" tab in this CircleCI job then run yarn add <path to tarball> in your React Native project.

@pull-bot

Copy link
Copy Markdown

PR build artifact for d064dfe is ready.
To use, download tarball from "Artifacts" tab in this CircleCI job then run yarn add <path to tarball> in your React Native project.

@github-actions

Copy link
Copy Markdown

This pull request was successfully merged by @awinograd in 36cc71a.

When will my fix make it into a release? | Upcoming Releases

@github-actionsgithub-actionsBot added the Merged This PR has been merged. label Dec 5, 2022
@awinograd

Copy link
Copy Markdown
ContributorAuthor

Thank you @kelset@cortinico for pushing this PR forward and getting it merged. Much appreciated!

@awinograd
awinograd deleted the patch-1 branch December 5, 2022 15:47
kelset pushed a commit that referenced this pull request Mar 28, 2023
)
Summary:
This PR prevents blob data from being prematurely de-allocated in native code when using slice to create views into an existing blob. Currently, whenever a new blob is created via createFromOptions, BlobManager.js creates a new blobCollector object since options.__collector is never provided.
https://github.com/facebook/react-native/blob/dc80b2dcb52fadec6a573a9dd1824393f8c29fdc/Libraries/Blob/BlobManager.js#L115-L123
When the reference to a blobCollector is garbage collected, the corresponding native memory for the blob data is de-allocated.
https://github.com/facebook/react-native/blob/27651720b40cab564a0cbd41be56a02584e0c73a/Libraries/Blob/RCTBlobCollector.mm#L19-L25
Since, `blob.slice()` is supposed to create a new view onto the same binary data as the original blob, we need to re-use the same collector object when slicing so that it is not GC'd until the last reference to the binary data is no longer reachable. Currently, since each blob slice gets a new blobCollector object, the memory is de-allocated when the first blob is GC'd.
Fixes#29970Fixes#27857
## Changelog
<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://github.com/facebook/react-native/wiki/Changelog
-->
[iOS] [Fixed] - Blob data is no longer prematurely deallocated when using blob.slice
Pull Request resolved: #31392
Test Plan: I could use help coming up with a test plan here. I could add a referential equality check for the blob.data.__collector in `Blob-test` but it doesn't seem quite right to be testing the implementation detail there.
Reviewed By: javache
Differential Revision: D41730782
Pulled By: cortinico
fbshipit-source-id: 5671ae2c69908f4c9acb5d203ba198b41b421294
kelset pushed a commit that referenced this pull request Apr 3, 2023
)
Summary:
This PR prevents blob data from being prematurely de-allocated in native code when using slice to create views into an existing blob. Currently, whenever a new blob is created via createFromOptions, BlobManager.js creates a new blobCollector object since options.__collector is never provided.
https://github.com/facebook/react-native/blob/dc80b2dcb52fadec6a573a9dd1824393f8c29fdc/Libraries/Blob/BlobManager.js#L115-L123
When the reference to a blobCollector is garbage collected, the corresponding native memory for the blob data is de-allocated.
https://github.com/facebook/react-native/blob/27651720b40cab564a0cbd41be56a02584e0c73a/Libraries/Blob/RCTBlobCollector.mm#L19-L25
Since, `blob.slice()` is supposed to create a new view onto the same binary data as the original blob, we need to re-use the same collector object when slicing so that it is not GC'd until the last reference to the binary data is no longer reachable. Currently, since each blob slice gets a new blobCollector object, the memory is de-allocated when the first blob is GC'd.
Fixes#29970Fixes#27857
## Changelog
<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://github.com/facebook/react-native/wiki/Changelog
-->
[iOS] [Fixed] - Blob data is no longer prematurely deallocated when using blob.slice
Pull Request resolved: #31392
Test Plan: I could use help coming up with a test plan here. I could add a referential equality check for the blob.data.__collector in `Blob-test` but it doesn't seem quite right to be testing the implementation detail there.
Reviewed By: javache
Differential Revision: D41730782
Pulled By: cortinico
fbshipit-source-id: 5671ae2c69908f4c9acb5d203ba198b41b421294
kelset pushed a commit that referenced this pull request Apr 3, 2023
)
Summary:
This PR prevents blob data from being prematurely de-allocated in native code when using slice to create views into an existing blob. Currently, whenever a new blob is created via createFromOptions, BlobManager.js creates a new blobCollector object since options.__collector is never provided.
https://github.com/facebook/react-native/blob/dc80b2dcb52fadec6a573a9dd1824393f8c29fdc/Libraries/Blob/BlobManager.js#L115-L123
When the reference to a blobCollector is garbage collected, the corresponding native memory for the blob data is de-allocated.
https://github.com/facebook/react-native/blob/27651720b40cab564a0cbd41be56a02584e0c73a/Libraries/Blob/RCTBlobCollector.mm#L19-L25
Since, `blob.slice()` is supposed to create a new view onto the same binary data as the original blob, we need to re-use the same collector object when slicing so that it is not GC'd until the last reference to the binary data is no longer reachable. Currently, since each blob slice gets a new blobCollector object, the memory is de-allocated when the first blob is GC'd.
Fixes#29970Fixes#27857
## Changelog
<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://github.com/facebook/react-native/wiki/Changelog
-->
[iOS] [Fixed] - Blob data is no longer prematurely deallocated when using blob.slice
Pull Request resolved: #31392
Test Plan: I could use help coming up with a test plan here. I could add a referential equality check for the blob.data.__collector in `Blob-test` but it doesn't seem quite right to be testing the implementation detail there.
Reviewed By: javache
Differential Revision: D41730782
Pulled By: cortinico
fbshipit-source-id: 5671ae2c69908f4c9acb5d203ba198b41b421294
@cipolleschicipolleschi mentioned this pull request Oct 11, 2023
meta-codesyncBot pushed a commit that referenced this pull request Aug 3, 2026
Summary:
`FileReader.readAsText` / `readAsDataURL` / `readAsArrayBuffer` pass only the plain `blob.data` descriptor to the native module and retain no reference to the `Blob` instance itself. If the caller also drops its reference, the Blob — and the `BlobCollector` attached to `blob.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.cpp` calls `BlobModule.remove()` on Android; `RCTBlobCollector.mm` calls `[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 — `readBlobAsText` creates a `FileReader`, calls `reader.readAsText(blob)`, and keeps a reference only to the reader. So a plain `fetch(url).then(r => r.json())`, where the `Response` is 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 with `async`/`await` — the suspended frame keeps the `Response` (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 by `abort()` cannot drop a newer read's reference) and in `abort()` 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
Pull Request resolved: #57796
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. `eslint` on both changed files — clean.
- The GC race itself cannot be reproduced deterministically under Jest (it requires a real engine GC collecting the Blob between dispatch and native execution), so the unit tests assert the reference-retention behavior instead. A deterministic on-device repro is in the issue comment below / #56884.
Reviewed By: javache
Differential Revision: D114576384
Pulled By: fabriziocucci
fbshipit-source-id: ed3f5b51f2d246e041c2b178e3eadd58aeb70038
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

BugCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.MergedThis PR has been merged.Needs: React Native Team AttentionPlatform: iOSiOS applications.Shared with MetaApplied 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 14 Only attempt to insert nil object from objects[0] iOS Native Crash when uploading blobs using aws-sdk

8 participants

@awinograd@analysis-bot@lachenmayer@facebook-github-bot@pull-bot@sota000@kelset@react-native-bot