Files
react-native/scripts
Christoph Purrer d84c13d511 Enforce the ArrayBuffer borrow contract for Java TurboModules (#57982)
Summary:
Pull Request resolved: https://github.com/react/react-native/pull/57982

Changelog: [ANDROID][FIXED]  Enforce the ArrayBuffer borrow contract for Java TurboModules

Follow-up hardening for the Android `ArrayBuffer` TurboModule type. Three problems:

**1. Borrowed JS-heap bytes outlived the call that lent them.** For a synchronous
method, `convertJSIArgsToJNIArgs` hands the module a `ByteBuffer` aliasing the JS
`ArrayBuffer`'s bytes without copying. Nothing stopped a module from stashing that
`ArrayBuffer` in a field and reading it later, after the JS heap may have moved,
freed, or reused the memory — a use-after-free that reads as intermittent data
corruption rather than a crash.

The borrow is now explicitly scoped to the call frame. `JNIArgs` records every
borrowed `ArrayBuffer` and revokes it in its destructor — including when the call
throws — via the new `JArrayBuffer::invalidate`, which drops the C++ side's
reference to the bytes. `ArrayBuffer.bytes` and `ArrayBuffer.size` then throw,
with a message pointing at `ArrayBuffer.arrayBufferWithCopiedBytes`, and
`JArrayBuffer::toJSBuffer` throws rather than aliasing revoked memory. Modules
that need the bytes past the call copy them; modules that don't keep the
zero-copy fast path.

Revocation lives entirely on the C++ side: the peer is the single source of
truth, and Kotlin asks it through `isBytesValid`. The destructor runs while the
stack unwinds, possibly with a Java exception pending, so it resolves each peer
pointer at borrow time — the `global_ref` alongside it keeps the Java object, and
therefore the peer, alive — and calls only the `noexcept`
`JArrayBuffer::invalidate`. No JNI calls are made from the destructor, which is
what lets it stay `noexcept` honestly.

**2. Argument conversion aborted under runtimes that refuse `tryGetMutableBuffer`.**
`jsi::Runtime::tryGetMutableBuffer` and `detached` are not universally
implemented: tracing and replay runtimes throw from `tryGetMutableBuffer`, and
`detached` throws a `JSINativeException` if the JS-side property isn't a bool.
`ArrayBuffer` argument conversion is not wrapped in a try/catch, so either throw
propagated out of a JNI frame. Both calls now go through exception-tolerant
helpers in `react/bridging/ArrayBuffer.h`; a runtime that refuses to answer is
treated as "no native buffer available", which selects the copy path. Routing
`AsyncArrayBuffer::acquire` and `::borrow` through the same helper fixes the
identical latent bug on the shared C++/ObjC path.

**3. A wrong return type from a module crashed instead of raising a JS error.**
The `ArrayBufferKind` return path cast the returned `jobject` to `JArrayBuffer`
unconditionally. A module returning any other object type produced undefined
behavior. The cast is now guarded by an `isInstanceOf` check that throws a
`jsi::JSError` naming the offending module and method.

Also in this change:
- `JByteBufferMutableBuffer::data()` reports null for a zero-capacity direct
  buffer instead of calling `getDirectBytes()`, which throws for one. That made
  `createArrayBuffer` throw for an empty `ArrayBuffer`.
- Dropped two dead zero-size branches in `JArrayBuffer`: `JByteBuffer::wrapBytes`
  already routes `size == 0` to an empty buffer.
- `JArrayBuffer.cpp` reuses the shared `detail::OwnedBytesBuffer` from
  `react/bridging/ArrayBuffer.h` instead of a second local copy.
- `ArrayBuffer.kt` KDoc corrected: the returned JS `ArrayBuffer` is a new object
  over the same bytes rather than the identical one, `size` is the capacity and
  not a view's remaining bytes, and `arrayBufferWithOwnedBytes` documents the
  caller's lifetime obligation.
- `ArrayBuffer.kt` moves from the `bridge` target to `native-types`, alongside the
  other JNI-backed bridge types.

Changelog:
[Android][Breaking] - TurboModule methods taking or returning an `ArrayBuffer`
now use `com.facebook.react.bridge.ArrayBuffer` instead of
`java.nio.ByteBuffer`, and an `ArrayBuffer` argument must not be retained past
the method that receives it unless its bytes are copied with
`ArrayBuffer.arrayBufferWithCopiedBytes()`.

Reviewed By: javache

Differential Revision: D115794808

fbshipit-source-id: 26f5d863469cc14a3f1bffc2cbc3302f3e983ecb
2026-08-17 17:57:28 -07:00
..