From f796f7ccb9dcf9e4f927450550920ad63f1de08d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 13 Mar 2026 20:22:15 +0000 Subject: [PATCH] :bug: Fix "Cannot assign to read only property toString" error in plugins runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../libs/plugins-runtime/src/lib/load-plugin.spec.ts | 1 + plugins/libs/plugins-runtime/src/lib/load-plugin.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts b/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts index a46bbc8573..c8d31131a0 100644 --- a/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts @@ -22,6 +22,7 @@ vi.mock('./create-plugin', () => ({ vi.mock('./ses.js', () => ({ ses: { harden: vi.fn().mockImplementation((obj) => obj), + hardenIntrinsics: vi.fn(), }, })); diff --git a/plugins/libs/plugins-runtime/src/lib/load-plugin.ts b/plugins/libs/plugins-runtime/src/lib/load-plugin.ts index 990a9148a1..df62cd4fca 100644 --- a/plugins/libs/plugins-runtime/src/lib/load-plugin.ts +++ b/plugins/libs/plugins-runtime/src/lib/load-plugin.ts @@ -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,