diff --git a/packages/react-native/src/private/webapis/structuredClone/__tests__/structuredClone-test.js b/packages/react-native/src/private/webapis/structuredClone/__tests__/structuredClone-test.js new file mode 100644 index 000000000000..32f2c32443a2 --- /dev/null +++ b/packages/react-native/src/private/webapis/structuredClone/__tests__/structuredClone-test.js @@ -0,0 +1,57 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import DOMException from '../../errors/DOMException'; + +// Snapshot the built-in prototypes before the first evaluation of the +// module under test. `structuredClone` is required (not imported) below so +// its top-level code runs after this snapshot, which is what lets the +// no-mutation regression test detect writes to the built-in prototypes. +const PROTOTYPE_SYMBOLS = { + WeakMap: Object.getOwnPropertySymbols(WeakMap.prototype), + WeakSet: Object.getOwnPropertySymbols(WeakSet.prototype), + Promise: Object.getOwnPropertySymbols(Promise.prototype), +}; + +function expectDataCloneError(fn: () => unknown) { + try { + fn(); + } catch (error) { + expect(error).toBeInstanceOf(DOMException); + expect(error.name).toBe('DataCloneError'); + return; + } + + throw new Error('Expected function to throw DataCloneError, but it did not'); +} + +describe('structuredClone', () => { + it('does not add properties to built-in prototypes', () => { + require('../structuredClone'); + + expect(Object.getOwnPropertySymbols(WeakMap.prototype)).toEqual( + PROTOTYPE_SYMBOLS.WeakMap, + ); + expect(Object.getOwnPropertySymbols(WeakSet.prototype)).toEqual( + PROTOTYPE_SYMBOLS.WeakSet, + ); + expect(Object.getOwnPropertySymbols(Promise.prototype)).toEqual( + PROTOTYPE_SYMBOLS.Promise, + ); + }); + + it('throws DataCloneError for non-serializable built-ins', () => { + const {default: structuredClone} = require('../structuredClone'); + + expectDataCloneError(() => structuredClone(new WeakMap())); + expectDataCloneError(() => structuredClone(new WeakSet())); + expectDataCloneError(() => structuredClone(new Promise(() => {}))); + }); +}); diff --git a/packages/react-native/src/private/webapis/structuredClone/structuredClone.js b/packages/react-native/src/private/webapis/structuredClone/structuredClone.js index 820534ef2a57..ef64df93f736 100644 --- a/packages/react-native/src/private/webapis/structuredClone/structuredClone.js +++ b/packages/react-native/src/private/webapis/structuredClone/structuredClone.js @@ -295,8 +295,6 @@ export default function structuredClone(value: T): T { } } -const NON_SERIALIZABLE_OBJECT_KEY = Symbol('nonSerializableObject'); - function cloneArrayBuffer(value: ArrayBuffer): ArrayBuffer { const existingClone = memory.get(value); if (existingClone instanceof ArrayBuffer) { @@ -392,16 +390,11 @@ function createErrorClone(value: Error): Error { } function isNonSerializableObject(obj: T): boolean { - // $FlowExpectedError[invalid-in-lhs] - return NON_SERIALIZABLE_OBJECT_KEY in obj; -} - -function markClassAsNonSerializable(cls: Class): void { - // $FlowExpectedError[incompatible-use] - cls.prototype[NON_SERIALIZABLE_OBJECT_KEY] = true; + // Built-in prototypes must not be written to because Hardened JS + // environments (SES) freeze them, so the non-serializable + // built-ins are detected with `instanceof` instead of marking the + // prototypes with e.g. Symbol('nonSerializableObject'). + return ( + obj instanceof WeakMap || obj instanceof WeakSet || obj instanceof Promise + ); } - -// Non-serializable built-ins. -markClassAsNonSerializable(WeakMap); -markClassAsNonSerializable(WeakSet); -markClassAsNonSerializable(Promise);