Skip to content

[WebAPIs] structuredClone: WeakMap/WeakSet/Promise prototype mutations break initial render under Hardened JS (SES) #60002

Open

Description

@leotm

Description

const NON_SERIALIZABLE_OBJECT_KEY = Symbol('nonSerializableObject');

function isNonSerializableObject<T extends interface {}>(obj: T): boolean {
// $FlowExpectedError[invalid-in-lhs]
return NON_SERIALIZABLE_OBJECT_KEY in obj;
}
function markClassAsNonSerializable<T>(cls: Class<T>): void {
// $FlowExpectedError[incompatible-use]
cls.prototype[NON_SERIALIZABLE_OBJECT_KEY] = true;
}
// Non-serializable built-ins.
markClassAsNonSerializable(WeakMap);
markClassAsNonSerializable(WeakSet);
markClassAsNonSerializable(Promise);

crashes apps whose built-in prototypes are frozen (Hardened JS / SES)

i.e. via LavaMoat React Native lockdown

e.g. react-native-template-new-architecture

unless patched with the/a fix when upgrading to v0.89

when apps like metamask/metamask-mobile eventually adopt v0.89 stable
they will crash on first render as soon as structuredClone loads

TypeError: cannot add a new property
    at markClassAsNonSerializable (structuredClone.js:401:45)
    at anonymous (structuredClone.js:405:27)
    ...
    at metroRequire (require.js:98:24)
    at metroImportDefault (require.js:123:31)
    at measure (Performance.js:343:30)
    at renderRootSync (ReactFabric-dev.js:15001:26)
    at performWorkOnRootViaSchedulerTask (ReactFabric.js:3928:24)

then remaining calls fail since structuredClone is undefined

TypeError: undefined is not a function
    at measure (Performance.js:343:30)
    ...
Error: Should not already be working.
    at performWorkOnRoot (ReactFabric-dev.js:14078:20)

... the measure frame: performance.measure() called with a detail
343: (structuredClone(detail)) is behind the detail !== undefined guard
so the crashing call must have passed a detail
performance.mark() with a detail fails similarly

structuredClone.js currently marks built-in classes non-serializable by adding the symbol prop to the built-in prototypes at the module top level

since the built-in prototypes are frozen (non-extensible) and RN source is strict-mode
the first cls.prototype[SYMBOL] = true throws during eval on WeakMap

structuredClone installs lazily
setUpDOM.js: polyfillGlobal('structuredClone', () => require('../webapis/structuredClone/structuredClone').default)
the first use runs that require and throws, the factory's require(...).default never resolves, so the structuredClone global is left undefined
the first throw is contained rather than fatal (the app still reaches later structuredClone uses), so every following use fails with undefined is not a function
so the first performance.measure(name, { detail }) during the first render (Fabric dev instrumentation) lazily loads structuredClone.js then throws at its top level

its not a new mutation, 0.88.1 includes the module (same code), but 0.89 pulled the module into the startup path
245bbfaf0a exposes structuredClone/DOMException as globals in packages/react-native/src/private/setup/setUpDOM.js (not in 0.88.1)
14de25bb2d extends structuredClone with ArrayBuffer/DataView/typed array/BigInt/Error support, and Performance.js (imported by startup timing) calls structuredClone in mark()/measure() whenever detail is defined

as an aside spotted couple spec-compliant gaps RE WeakRef and FinalizationRegistry which i'll follow up seperately

Steps to reproduce

1a. clone react-native-template-new-architecture
1b. checkout to branch react-native-0.88.1-to-0.89.0-rc.0-repro
2. run yarn android or yarn ios

React Native Version

0.89.0-rc.0

Affected Platforms

Runtime - iOS, Runtime - Android

Output of npx @react-native-community/cli info

npx @react-native-community/cli info
info Fetching system and libraries information...
System:
  OS: macOS 27.3
  CPU: (12) arm64 Apple M2 Pro
  Memory: 612.80 MB / 16.00 GB
  Shell:
    version: 5.9.2
    path: /opt/homebrew/bin/zsh
Binaries:
  Node:
    version: 22.23.1
    path: /Users/leo/.local/state/fnm_multishells/29305_1794588089192/bin/node
  Yarn:
    version: 4.18.1
    path: /Users/leo/.local/state/fnm_multishells/29305_1794588089192/bin/yarn
  npm:
    version: 10.9.8
    path: /Users/leo/.local/state/fnm_multishells/29305_1794588089192/bin/npm
  Watchman: Not Found
Managers:
  CocoaPods:
    version: 1.17.0
    path: /var/folders/w0/pbc5gqt14yjg4b1y7s6x6wpw0000gn/T/frum_29306_1794588089207/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 27.3
      - iOS 27.3
      - macOS 27.3
      - tvOS 27.3
      - visionOS 27.3
      - watchOS 27.3
  Android SDK: Not Found
IDEs:
  Android Studio: Not Found
  Xcode:
    version: 27.3/27C5020n
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.21.0.3
    path: /Users/leo/.jenv/shims/javac
  Ruby:
    version: 3.4.10
    path: /var/folders/w0/pbc5gqt14yjg4b1y7s6x6wpw0000gn/T/frum_29306_1794588089207/bin/ruby
npmPackages:
  "@react-native-community/cli":
    installed: 21.0.0-alpha.1
    wanted: 21.0.0-alpha.1
  react:
    installed: 19.3.0
    wanted: 19.3.0
  react-native:
    installed: 0.89.0-rc.0
    wanted: 0.89.0-rc.0
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: true
iOS:
  hermesEnabled: true
  newArchEnabled: true

Stacktrace or Logs

Running "MyApp" with {"rootTag":11,"initialProps":{},"fabric":true}
console.js:667 TypeError: cannot add a new property
    at markClassAsNonSerializable (structuredClone.js:401:45)
    at anonymous (structuredClone.js:405:27)
    at loadModuleImplementation (require.js:285:12)
    at guardedLoadModule (require.js:183:45)
    at metroRequire (require.js:98:24)
    at metroImportDefault (require.js:123:31)
    at measure (Performance.js:343:30)
    at run (native)
    at prepareFreshStack (ReactFabric-dev.js:14720:28)
    at renderRootSync (ReactFabric-dev.js:15001:26)
    at performWorkOnRoot (ReactFabric-dev.js:14158:25)
    at performWorkOnRootViaSchedulerTask (ReactFabric-dev.js:3928:24)
anonymous @ console.js:667
overrideMethod @ backend.js:17416
reactConsoleErrorHandler @ ExceptionsManager.js:184
anonymous @ setUpDeveloperTools.js:42
reportException @ ExceptionsManager.js:108
handleException @ ExceptionsManager.js:173
handleError @ setUpErrorHandling.js:26
reportFatalError @ error-guard.js:53
guardedLoadModule @ require.js:185
metroRequire @ require.js:98
metroImportDefault @ require.js:123
measure @ Performance.js:343
prepareFreshStack @ ReactFabric-dev.js:14720
renderRootSync @ ReactFabric-dev.js:15001
performWorkOnRoot @ ReactFabric-dev.js:14158
performWorkOnRootViaSchedulerTask @ ReactFabric-dev.js:3928
console.js:667 TypeError: undefined is not a function
    at measure (Performance.js:343:30)
    at run (native)
    at prepareFreshStack (ReactFabric-dev.js:14720:28)
    at renderRootSync (ReactFabric-dev.js:15001:26)
    at performWorkOnRoot (ReactFabric-dev.js:14158:25)
    at performWorkOnRootViaSchedulerTask (ReactFabric-dev.js:3928:24)
anonymous @ console.js:667
overrideMethod @ backend.js:17416
reactConsoleErrorHandler @ ExceptionsManager.js:184
anonymous @ setUpDeveloperTools.js:42
reportException @ ExceptionsManager.js:108
handleException @ ExceptionsManager.js:173
handleError @ setUpErrorHandling.js:26
reportFatalError @ error-guard.js:53
console.js:667 Error: Should not already be working.
    at performWorkOnRoot (ReactFabric-dev.js:14078:20)
    at performWorkOnRootViaSchedulerTask (ReactFabric-dev.js:3928:24)

MANDATORY Reproducer

https://github.com/leotm/react-native-template-new-architecture

Screenshots and Videos

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions