Skip to content

Clamp blob range in RCTBlobManager instead of raising NSRangeException - #58566

Open
daledah wants to merge 1 commit into
react:mainfrom
daledah:clamp-blob-range-instead-of-raising
Open

daledah wants to merge 1 commit into
react:mainfrom
daledah:clamp-blob-range-instead-of-raising

Conversation

@daledah

@daledah daledah commented Sep 17, 2026

Copy link
Copy Markdown

Summary:

RCTBlobManager's resolve:offset:size: slices the stored NSData with the offset and size that come from the JS-side blob descriptor, without ever range-checking them against the data it actually holds:

if (offset != 0 || (size != -1 && size != data.length)) {
  data = [data subdataWithRange:NSMakeRange(offset, size)];
}

When the descriptor and the stored bytes disagree, subdataWithRange: raises NSRangeException. That is fatal rather than recoverable, because createFromParts:withId: runs on the TurboModule method queue (com.meta.react.turbomodulemanager.queue) with no JS frame on the stack to catch it, so the app is terminated.

This has been reported repeatedly since 2018 and is still unfixed on main:

What makes it worth guarding at the slice itself is that the reports come from at least three unrelated producers:

  1. Blob data deallocated early while a slice still referenced it - fixed separately in Prevent native blob resource from being de-allocated prematurely #31392.
  2. Strings truncated at an embedded NUL as they crossed into native, so the JS-recorded size exceeded the stored bytes - fixed separately in Fix Text truncation at NULL character on iOS and Android (#24129) #57906.
  3. A race between writing a blob and sending it, discussed in RCTBlobManager handleNetworkingRequest React native IOS #35096 - still open.

Each of those was (or will be) fixed one at a time, and each time the symptom was a hard crash in the same three lines. A descriptor that disagrees with the stored bytes is a data bug; it should not be a fatal one.

What this changes

  • resolve:offset:size: clamps the range to the bytes that are actually stored. An out-of-bounds offset returns nil; a size that overruns the available bytes is truncated to what is there. Both cases log via RCTLogWarn with the blob ID and the two lengths, so the underlying desync is visible instead of silent.
  • size < 0 still means "the rest of the blob", preserving the existing -1 contract.
  • createFromParts:withId: gains nil guards with the same logging. A part that fails to resolve, or a string part that fails to encode, previously contributed zero bytes with no signal at all - which is itself one of the ways a blob ends up shorter than its descriptor.

This also fixes a latent crash on the existing code path: with offset > 0 and size == -1, the old condition entered the branch and called NSMakeRange(offset, (NSUInteger)-1), which is an enormous length. resolveURL: defaults size to -1 when the query parameter is absent, so a blob: URL built by hand rather than by URL.createObjectURL would hit it.

Truncate rather than zero-pad

Android is already forgiving here - BlobModule.kt uses Arrays.copyOfRange, which zero-pads when the requested end is past the array, which is why this crash is iOS-only. This change deliberately truncates instead of matching that zero-padding: padding a short read with NUL bytes silently corrupts the payload with plausible-looking data, whereas a short read is at least a length mismatch that a server or a checksum can detect. Happy to switch to zero-padding for cross-platform symmetry if maintainers prefer it.

Changelog:

[IOS] [FIXED] - Clamp blob ranges in RCTBlobManager instead of raising NSRangeException

Test Plan:

The two producers above that have already been fixed cannot be used to reproduce this on main any more, so the repro below creates the descriptor/data disagreement directly. It is deterministic and does not depend on which producer caused it.

In RNTester, or any app:

// Native stores 318 bytes for this blob.
const real = new Blob(['A'.repeat(318)]);

// Hand it a descriptor that claims 321.
const lying = new Blob([]);
lying.data = {...real.data, size: 321};

// Consuming it goes createFromParts -> resolve: -> resolve:offset:size:
new Blob([lying]);

Before: the app terminates.

*** Terminating app due to uncaught exception 'NSRangeException', reason:
'*** -[NSConcreteMutableData subdataWithRange:]: range {0, 321} exceeds data length 318'

-[RCTBlobManager resolve:offset:size:]
-[RCTBlobManager resolve:]
-[RCTBlobManager createFromParts:withId:]
facebook::react::ObjCTurboModule::performVoidMethodInvocation(...)
_dispatch_workloop_worker_thread

After: the app stays up, the blob resolves to the 318 bytes that are actually stored, and the console shows:

[BlobManager] blob <uuid>: 321 bytes requested at offset 0, but only 318 are stored; truncating

Formatting:

node ./scripts/clang-format.js --check

@meta-cla

meta-cla Bot commented Sep 17, 2026

Copy link
Copy Markdown

Hi @daledah!

Thank you for your pull request and welcome to our community.

Action Required

In 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.

Process

In 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 CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

MelvinBot added a commit to Expensify/App that referenced this pull request Sep 17, 2026
react/react-native#58566 is now open, so replace the 'not filed' placeholder
with the link and keep the note that the change is still absent from every
released RN version.

Co-authored-by: daledah <daledah@users.noreply.github.com>
@meta-cla

meta-cla Bot commented Sep 17, 2026

Copy link
Copy Markdown

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

@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 Sep 17, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Sep 17, 2026
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.

1 participant