🐛 Fix "Cannot assign to read only property toString" error in plugins runtime

The error "Cannot assign to read only property 'toString' of function"
occurs during React's commit phase after a plugin is loaded. The root
cause is an initialization ordering issue in the SES (Secure EcmaScript)
lockdown sequence.

When loadPlugin() is called, ses.harden(context) runs first, which
transitively freezes everything reachable from the context object —
including Function.prototype and Object.prototype — via prototype chain
traversal of getter functions. Later, createSandbox() calls
ses.hardenIntrinsics(), which attempts to run enablePropertyOverrides()
to convert frozen data properties (like Function.prototype.toString)
into accessor pairs that work around JavaScript's "override mistake".
However, enablePropertyOverrides checks "if (configurable)" before
converting, and since Function.prototype is already frozen (all
properties have configurable: false), the override taming is silently
skipped. This leaves Function.prototype.toString as a frozen
non-writable data property, causing any subsequent code that assigns
.toString to a function instance in strict mode to throw a TypeError.

The fix calls ses.hardenIntrinsics() before ses.harden(context) in
loadPlugin(), ensuring override taming installs the accessor pairs on
prototype properties before they get frozen. The existing
hardenIntrinsics() call in createSandbox() becomes a harmless no-op
thanks to the idempotency guard.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
Andrey Antukh
2026-03-13 20:22:15 +00:00
committed by Alonso Torres
parent 27a934dcfd
commit f796f7ccb9
2 changed files with 11 additions and 0 deletions

View File

@@ -22,6 +22,7 @@ vi.mock('./create-plugin', () => ({
vi.mock('./ses.js', () => ({
ses: {
harden: vi.fn().mockImplementation((obj) => obj),
hardenIntrinsics: vi.fn(),
},
}));

View File

@@ -52,6 +52,16 @@ export const loadPlugin = async function (
closeAllPlugins();
// hardenIntrinsics must be called BEFORE harden() to ensure that
// override taming (enablePropertyOverrides) converts prototype
// properties like Function.prototype.toString into accessor pairs.
// Without this, harden() would freeze Function.prototype with plain
// data properties, making them non-configurable, which causes
// enablePropertyOverrides to silently skip them when hardenIntrinsics
// runs later — resulting in "Cannot assign to read only property
// 'toString'" errors.
ses.hardenIntrinsics();
const plugin = await createPlugin(
ses.harden(context) as Context,
manifest,