From dd308f604575fb06c9004344b273157a11790eef Mon Sep 17 00:00:00 2001 From: Qiu Date: Mon, 9 Nov 2026 13:54:27 +0100 Subject: [PATCH 1/3] Reject cyclic prototypes on scriptlet proxies Akamai's prototype probe can create a cycle on the proxied Function.prototype.toString, causing fn.toString() calls across the page to overflow the stack. run() now wraps Proxy with a setPrototypeOf trap that checks the chain before changing it. The check rejects chains reaching the proxy or its target and stops at other proxies from the same wrapper. Returning false makes Reflect.setPrototypeOf() return false and Object.setPrototypeOf() throw a TypeError. --- build.ts | 4 ++++ proxy.js | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ test.js | 38 ++++++++++++++++++++++++++++++++++++++ ubo.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 135 insertions(+) create mode 100644 proxy.js diff --git a/build.ts b/build.ts index fbb87b7..faad594 100644 --- a/build.ts +++ b/build.ts @@ -1,3 +1,5 @@ +import { createProxy } from './proxy.js'; + const tagName = Deno.args.includes('--tagName') ? Deno.args[Deno.args.findIndex((o) => o === '--tagName') + 1] : false; @@ -52,7 +54,9 @@ fn: '${scriptlet.fn.name}', .join('\n')} // The calls come last, so the class dependencies, which are not hoisted, are declared by then. +// Proxy shadows the global for every scriptlet in run(), see proxy.js. export function run(scriptletGlobals, calls) { +const Proxy = (${createProxy})(globalThis.Proxy); ${[...functions.values()].join('\n')} const table = { ${entries.map((scriptlet) => scriptlet.fn.name).join(', ')} }; for (const [name, ...args] of calls) { diff --git a/proxy.js b/proxy.js new file mode 100644 index 0000000..c0cdf68 --- /dev/null +++ b/proxy.js @@ -0,0 +1,48 @@ +/** + * Returns a Proxy constructor wrapper that rejects prototype chains reaching + * the proxy or its target before changing the target's prototype. + */ +export function createProxy(NativeProxy) { + const reflectApply = Reflect.apply; + const reflectGetPrototypeOf = Reflect.getPrototypeOf; + const reflectSetPrototypeOf = Reflect.setPrototypeOf; + const objectCreate = Object.create; + const TypeErrorCtor = TypeError; + const proxies = new WeakSet(); + const { add, has } = WeakSet.prototype; + + // Stop at other proxies created by this wrapper. Unrecognized proxies are + // traversed, which can invoke page-owned getPrototypeOf traps unlike native checks. + const isCyclic = (proxy, target, proto) => { + const stops = (p) => p === null || p === proxy || p === target || reflectApply(has, proxies, [p]); + let slow = proto; + let fast = proto; + // Classic "tortoise and hare" cycle check + while (!stops(fast)) { + fast = reflectGetPrototypeOf(fast); + if (stops(fast)) break; + fast = reflectGetPrototypeOf(fast); + slow = reflectGetPrototypeOf(slow); + if (slow === fast) return false; + } + return fast === proxy || fast === target; + }; + + return function Proxy(target, handler) { + if (new.target === undefined) { + throw new TypeErrorCtor("Constructor Proxy requires 'new'"); + } + let proxy; + // Inherit from the handler rather than copy it, so traps defined on a prototype still apply + if (handler instanceof Object && handler.setPrototypeOf === undefined) { + // Returning false makes Object.setPrototypeOf throw while Reflect.setPrototypeOf + // returns false. The error message differs from native cycle rejection. + const setPrototypeOf = (target, proto) => + !(proto !== null && isCyclic(proxy, target, proto)) && reflectSetPrototypeOf(target, proto); + handler = objectCreate(handler, { setPrototypeOf: { value: setPrototypeOf } }); + } + proxy = new NativeProxy(target, handler); + reflectApply(add, proxies, [proxy]); + return proxy; + }; +} diff --git a/test.js b/test.js index a0be436..726ff1f 100644 --- a/test.js +++ b/test.js @@ -84,3 +84,41 @@ suite("run()", () => { assert.strictEqual(keys.length, 0); }); }); + +suite("proxied functions", () => { + // A fresh realm is the page; returns it with the scriptlet applied + function createPage(name, ...args) { + const page = vm.createContext({ EventTarget: class {}, Request: class {} }); + vm.runInContext("self = globalThis; nativeSetTimeout = setTimeout = function setTimeout() {};", page); + vm.runInContext(`(${runSource})(...${JSON.stringify([{}, [[scriptlets[name].fn, ...args]]])});`, page); + return page; + } + + // Akamai Bot Manager's proxy probe + const probe = (fn) => ` + (() => { + const fn = ${fn}; + let error; + try { Object.setPrototypeOf(fn, Object.create(fn)); } catch (e) { error = e; } + return { error: error?.name, reflect: Reflect.setPrototypeOf(fn, Object.create(fn)), call: typeof fn.call }; + })() + `; + const native = vm.runInContext(probe("function native() {}"), vm.createContext({})); + + for (const [name, fn] of [ + ["prevent-setTimeout.js", "Function.prototype.toString"], // proxied by proxyApplyFn() + ["adjust-setTimeout.js", "setTimeout"], // proxied by the scriptlet itself + ]) { + test(`${name}: rejects a cyclic prototype on ${fn} like a native function`, () => { + const page = createPage(name, "x"); + assert(vm.runInContext("setTimeout !== nativeSetTimeout", page), `${name} did not run`); + assert.deepStrictEqual({ ...vm.runInContext(probe(fn), page) }, { ...native }); + }); + } + + test("still sets a non-cyclic prototype", () => { + const page = createPage("prevent-setTimeout.js", "x"); + const set = "const proto = Object.create(Function.prototype); Object.setPrototypeOf(setTimeout, proto) === setTimeout && Object.getPrototypeOf(setTimeout) === proto"; + assert(vm.runInContext(set, page)); + }); +}); diff --git a/ubo.js b/ubo.js index fd9ad99..916e4e7 100644 --- a/ubo.js +++ b/ubo.js @@ -807,7 +807,52 @@ fn: 'trustedOverrideElementMethod', // The calls come last, so the class dependencies, which are not hoisted, are declared by then. +// Proxy shadows the global for every scriptlet in run(), see proxy.js. export function run(scriptletGlobals, calls) { +const Proxy = (function createProxy(NativeProxy) { + const reflectApply = Reflect.apply; + const reflectGetPrototypeOf = Reflect.getPrototypeOf; + const reflectSetPrototypeOf = Reflect.setPrototypeOf; + const objectCreate = Object.create; + const TypeErrorCtor = TypeError; + const proxies = new WeakSet(); + const { add, has } = WeakSet.prototype; + + // Stop at other proxies created by this wrapper. Unrecognized proxies are + // traversed, which can invoke page-owned getPrototypeOf traps unlike native checks. + const isCyclic = (proxy, target, proto) => { + const stops = (p) => p === null || p === proxy || p === target || reflectApply(has, proxies, [p]); + let slow = proto; + let fast = proto; + // Classic "tortoise and hare" cycle check + while (!stops(fast)) { + fast = reflectGetPrototypeOf(fast); + if (stops(fast)) break; + fast = reflectGetPrototypeOf(fast); + slow = reflectGetPrototypeOf(slow); + if (slow === fast) return false; + } + return fast === proxy || fast === target; + }; + + return function Proxy(target, handler) { + if (new.target === undefined) { + throw new TypeErrorCtor("Constructor Proxy requires 'new'"); + } + let proxy; + // Inherit from the handler rather than copy it, so traps defined on a prototype still apply + if (handler instanceof Object && handler.setPrototypeOf === undefined) { + // Returning false makes Object.setPrototypeOf throw while Reflect.setPrototypeOf + // returns false. The error message differs from native cycle rejection. + const setPrototypeOf = (target, proto) => + !(proto !== null && isCyclic(proxy, target, proto)) && reflectSetPrototypeOf(target, proto); + handler = objectCreate(handler, { setPrototypeOf: { value: setPrototypeOf } }); + } + proxy = new NativeProxy(target, handler); + reflectApply(add, proxies, [proxy]); + return proxy; + }; +})(globalThis.Proxy); function safeSelf() { if ( safeSelf.safe ) { return safeSelf.safe; From 026a6e83f0233ba96f3580f5619c95d6fc34688d Mon Sep 17 00:00:00 2001 From: Krzysztof Modras Date: Tue, 10 Nov 2026 11:24:01 +0100 Subject: [PATCH 2/3] Reject prototype cycles across scriptlet proxies --- proxy.js | 50 ++++++++++++++++++-------------------------------- test.js | 16 ++++++++++++++++ ubo.js | 54 ++++++++++++++++++++---------------------------------- 3 files changed, 54 insertions(+), 66 deletions(-) diff --git a/proxy.js b/proxy.js index c0cdf68..cd9b5ee 100644 --- a/proxy.js +++ b/proxy.js @@ -3,46 +3,32 @@ * the proxy or its target before changing the target's prototype. */ export function createProxy(NativeProxy) { - const reflectApply = Reflect.apply; - const reflectGetPrototypeOf = Reflect.getPrototypeOf; - const reflectSetPrototypeOf = Reflect.setPrototypeOf; + const { getPrototypeOf, setPrototypeOf } = Reflect; const objectCreate = Object.create; - const TypeErrorCtor = TypeError; - const proxies = new WeakSet(); - const { add, has } = WeakSet.prototype; - // Stop at other proxies created by this wrapper. Unrecognized proxies are - // traversed, which can invoke page-owned getPrototypeOf traps unlike native checks. - const isCyclic = (proxy, target, proto) => { - const stops = (p) => p === null || p === proxy || p === target || reflectApply(has, proxies, [p]); + // Proxies are walked like any other object, so a cycle spanning several scriptlet + // proxies is found. The tortoise ends walks over chains that are already cyclic. + const reaches = (proto, proxy, target) => { let slow = proto; let fast = proto; - // Classic "tortoise and hare" cycle check - while (!stops(fast)) { - fast = reflectGetPrototypeOf(fast); - if (stops(fast)) break; - fast = reflectGetPrototypeOf(fast); - slow = reflectGetPrototypeOf(slow); - if (slow === fast) return false; + for (let i = 1; fast !== null; i++) { + if (fast === proxy || fast === target) return true; + fast = getPrototypeOf(fast); + if (i % 2 === 0) { + slow = getPrototypeOf(slow); + if (slow === fast) return false; + } } - return fast === proxy || fast === target; + return false; }; return function Proxy(target, handler) { - if (new.target === undefined) { - throw new TypeErrorCtor("Constructor Proxy requires 'new'"); - } - let proxy; - // Inherit from the handler rather than copy it, so traps defined on a prototype still apply - if (handler instanceof Object && handler.setPrototypeOf === undefined) { - // Returning false makes Object.setPrototypeOf throw while Reflect.setPrototypeOf - // returns false. The error message differs from native cycle rejection. - const setPrototypeOf = (target, proto) => - !(proto !== null && isCyclic(proxy, target, proto)) && reflectSetPrototypeOf(target, proto); - handler = objectCreate(handler, { setPrototypeOf: { value: setPrototypeOf } }); - } - proxy = new NativeProxy(target, handler); - reflectApply(add, proxies, [proxy]); + // Inherit from the handler rather than copy it, so traps added to it later still apply. + const proxy = new NativeProxy(target, objectCreate(handler, { + // Returning false makes Object.setPrototypeOf throw and Reflect.setPrototypeOf + // return false, as both do for a native function. + setPrototypeOf: { value: (target, proto) => !reaches(proto, proxy, target) && setPrototypeOf(target, proto) }, + })); return proxy; }; } diff --git a/test.js b/test.js index 726ff1f..b91a519 100644 --- a/test.js +++ b/test.js @@ -116,6 +116,22 @@ suite("proxied functions", () => { }); } + test("rejects a cycle spanning two proxied functions like native functions", () => { + const probeBoth = (a, b) => ` + (() => { + const a = ${a}; + const b = ${b}; + Object.setPrototypeOf(a, Object.create(b)); + let error; + try { Object.setPrototypeOf(b, Object.create(a)); } catch (e) { error = e; } + return { error: error?.name, reflect: Reflect.setPrototypeOf(b, Object.create(a)), call: typeof b.call }; + })() + `; + const expected = vm.runInContext(probeBoth("function a() {}", "function b() {}"), vm.createContext({})); + const page = createPage("prevent-setTimeout.js", "x"); + assert.deepStrictEqual({ ...vm.runInContext(probeBoth("setTimeout", "Function.prototype.toString"), page) }, { ...expected }); + }); + test("still sets a non-cyclic prototype", () => { const page = createPage("prevent-setTimeout.js", "x"); const set = "const proto = Object.create(Function.prototype); Object.setPrototypeOf(setTimeout, proto) === setTimeout && Object.getPrototypeOf(setTimeout) === proto"; diff --git a/ubo.js b/ubo.js index 916e4e7..39a0653 100644 --- a/ubo.js +++ b/ubo.js @@ -810,46 +810,32 @@ fn: 'trustedOverrideElementMethod', // Proxy shadows the global for every scriptlet in run(), see proxy.js. export function run(scriptletGlobals, calls) { const Proxy = (function createProxy(NativeProxy) { - const reflectApply = Reflect.apply; - const reflectGetPrototypeOf = Reflect.getPrototypeOf; - const reflectSetPrototypeOf = Reflect.setPrototypeOf; + const { getPrototypeOf, setPrototypeOf } = Reflect; const objectCreate = Object.create; - const TypeErrorCtor = TypeError; - const proxies = new WeakSet(); - const { add, has } = WeakSet.prototype; - - // Stop at other proxies created by this wrapper. Unrecognized proxies are - // traversed, which can invoke page-owned getPrototypeOf traps unlike native checks. - const isCyclic = (proxy, target, proto) => { - const stops = (p) => p === null || p === proxy || p === target || reflectApply(has, proxies, [p]); + + // Proxies are walked like any other object, so a cycle spanning several scriptlet + // proxies is found. The tortoise ends walks over chains that are already cyclic. + const reaches = (proto, proxy, target) => { let slow = proto; let fast = proto; - // Classic "tortoise and hare" cycle check - while (!stops(fast)) { - fast = reflectGetPrototypeOf(fast); - if (stops(fast)) break; - fast = reflectGetPrototypeOf(fast); - slow = reflectGetPrototypeOf(slow); - if (slow === fast) return false; - } - return fast === proxy || fast === target; + for (let i = 1; fast !== null; i++) { + if (fast === proxy || fast === target) return true; + fast = getPrototypeOf(fast); + if (i % 2 === 0) { + slow = getPrototypeOf(slow); + if (slow === fast) return false; + } + } + return false; }; return function Proxy(target, handler) { - if (new.target === undefined) { - throw new TypeErrorCtor("Constructor Proxy requires 'new'"); - } - let proxy; - // Inherit from the handler rather than copy it, so traps defined on a prototype still apply - if (handler instanceof Object && handler.setPrototypeOf === undefined) { - // Returning false makes Object.setPrototypeOf throw while Reflect.setPrototypeOf - // returns false. The error message differs from native cycle rejection. - const setPrototypeOf = (target, proto) => - !(proto !== null && isCyclic(proxy, target, proto)) && reflectSetPrototypeOf(target, proto); - handler = objectCreate(handler, { setPrototypeOf: { value: setPrototypeOf } }); - } - proxy = new NativeProxy(target, handler); - reflectApply(add, proxies, [proxy]); + // Inherit from the handler rather than copy it, so traps added to it later still apply. + const proxy = new NativeProxy(target, objectCreate(handler, { + // Returning false makes Object.setPrototypeOf throw and Reflect.setPrototypeOf + // return false, as both do for a native function. + setPrototypeOf: { value: (target, proto) => !reaches(proto, proxy, target) && setPrototypeOf(target, proto) }, + })); return proxy; }; })(globalThis.Proxy); From a4ac5a1527c59af09a25d2a943c2ca091841237f Mon Sep 17 00:00:00 2001 From: Qiu Date: Thu, 12 Nov 2026 13:40:49 +0100 Subject: [PATCH 3/3] Isolate scriptlet proxy handlers from page prototype pollution Copy scriptlet-owned traps into a null-prototype handler so page changes to Object.prototype cannot inject traps or interfere with cycle checks. --- proxy.js | 51 +++++++++++++++++++++++++++++-------------------- test.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ubo.js | 49 ++++++++++++++++++++++++++++------------------- 3 files changed, 117 insertions(+), 41 deletions(-) diff --git a/proxy.js b/proxy.js index cd9b5ee..97627d5 100644 --- a/proxy.js +++ b/proxy.js @@ -1,34 +1,43 @@ /** - * Returns a Proxy constructor wrapper that rejects prototype chains reaching - * the proxy or its target before changing the target's prototype. + * Returns a Proxy constructor for scriptlets. Their handlers do not define + * setPrototypeOf; the page-provided prototype chains are untrusted. */ export function createProxy(NativeProxy) { - const { getPrototypeOf, setPrototypeOf } = Reflect; - const objectCreate = Object.create; + const reflectGetPrototypeOf = Reflect.getPrototypeOf; + const reflectSetPrototypeOf = Reflect.setPrototypeOf; - // Proxies are walked like any other object, so a cycle spanning several scriptlet - // proxies is found. The tortoise ends walks over chains that are already cyclic. - const reaches = (proto, proxy, target) => { + // Walk through other scriptlet proxies to catch cycles spanning several hooks. + // This can invoke page-owned getPrototypeOf traps, unlike native cycle checks. + const isCyclic = (proxy, target, proto) => { + const stops = (p) => p === null || p === proxy || p === target; let slow = proto; let fast = proto; - for (let i = 1; fast !== null; i++) { - if (fast === proxy || fast === target) return true; - fast = getPrototypeOf(fast); - if (i % 2 === 0) { - slow = getPrototypeOf(slow); - if (slow === fast) return false; - } + // Classic "tortoise and hare" cycle check + while (!stops(fast)) { + fast = reflectGetPrototypeOf(fast); + if (stops(fast)) break; + fast = reflectGetPrototypeOf(fast); + slow = reflectGetPrototypeOf(slow); + if (slow === fast) return false; } - return false; + return fast === proxy || fast === target; }; return function Proxy(target, handler) { - // Inherit from the handler rather than copy it, so traps added to it later still apply. - const proxy = new NativeProxy(target, objectCreate(handler, { - // Returning false makes Object.setPrototypeOf throw and Reflect.setPrototypeOf - // return false, as both do for a native function. - setPrototypeOf: { value: (target, proto) => !reaches(proto, proxy, target) && setPrototypeOf(target, proto) }, - })); + // Copy only the scriptlet's own traps; never inherit traps from the page. + const proxy = new NativeProxy(target, { + __proto__: null, + ...handler, + setPrototypeOf(target, proto) { + // Do not inspect the target: it may itself be a page-owned proxy. + if (isCyclic(proxy, target, proto)) { + // Object.setPrototypeOf throws; Reflect.setPrototypeOf returns false. + // The engine's error text still differs from native cycle rejection. + return false; + } + return reflectSetPrototypeOf(target, proto); + }, + }); return proxy; }; } diff --git a/test.js b/test.js index b91a519..b7c1d92 100644 --- a/test.js +++ b/test.js @@ -137,4 +137,62 @@ suite("proxied functions", () => { const set = "const proto = Object.create(Function.prototype); Object.setPrototypeOf(setTimeout, proto) === setTimeout && Object.getPrototypeOf(setTimeout) === proto"; assert(vm.runInContext(set, page)); }); + + test("can change prototypes when the page supplied a proxy target", () => { + const page = vm.createContext({ EventTarget: class {}, Request: class {} }); + vm.runInContext(` + self = globalThis; + nativeSetTimeout = function setTimeout() {}; + pageSetTimeout = setTimeout = new Proxy(nativeSetTimeout, { + getPrototypeOf() { throw new Error("page target trap ran"); }, + }); + `, page); + const calls = [[scriptlets["prevent-setTimeout.js"].fn, "x"]]; + vm.runInContext(`(${runSource})(...${JSON.stringify([{}, calls])});`, page); + assert(vm.runInContext("setTimeout !== pageSetTimeout", page)); + assert(vm.runInContext(` + const proto = Object.create(Function.prototype); + Object.setPrototypeOf(setTimeout, proto) === setTimeout && + Object.getPrototypeOf(nativeSetTimeout) === proto; + `, page)); + assert.deepStrictEqual({ ...vm.runInContext(probe("setTimeout"), page) }, { ...native }); + }); + + for (const timing of ["before", "after"]) { + test(`ignores page-defined traps added ${timing} scriptlet injection`, () => { + const page = vm.createContext({ EventTarget: class {}, Request: class {} }); + vm.runInContext(` + self = globalThis; + nativeSetTimeout = setTimeout = function setTimeout(callback) { callback(); return 42; }; + `, page); + const pollute = ` + for (const trap of ["get", "getPrototypeOf", "setPrototypeOf", "apply"]) { + Object.defineProperty(Object.prototype, trap, { + __proto__: null, + value() { throw new Error("page trap ran: " + trap); }, + }); + } + `; + if (timing === "before") vm.runInContext(pollute, page); + const calls = [[scriptlets["prevent-setTimeout.js"].fn, "blockedProbe"]]; + vm.runInContext(`(${runSource})(...${JSON.stringify([{}, calls])});`, page); + if (timing === "after") vm.runInContext(pollute, page); + assert(vm.runInContext("setTimeout !== nativeSetTimeout", page)); + assert.deepStrictEqual({ ...vm.runInContext(probe("setTimeout"), page) }, { ...native }); + const result = vm.runInContext(` + const original = Object.getPrototypeOf(setTimeout); + Object.setPrototypeOf(setTimeout, Object.create(Function.prototype.toString)); + const cycle = Reflect.setPrototypeOf(Function.prototype.toString, Object.create(setTimeout)); + Object.setPrototypeOf(setTimeout, original); + let blocked = false, allowed = false; + const blockedResult = setTimeout(function blockedProbe() { blocked = true; }); + const allowedResult = setTimeout(function allowedProbe() { allowed = true; }); + ({ cycle, name: setTimeout.name, blocked, allowed, blockedResult, allowedResult }); + `, page); + assert.deepStrictEqual({ ...result }, { + cycle: false, name: "setTimeout", blocked: false, allowed: true, + blockedResult: 42, allowedResult: 42, + }); + }); + } }); diff --git a/ubo.js b/ubo.js index 39a0653..8e65962 100644 --- a/ubo.js +++ b/ubo.js @@ -810,32 +810,41 @@ fn: 'trustedOverrideElementMethod', // Proxy shadows the global for every scriptlet in run(), see proxy.js. export function run(scriptletGlobals, calls) { const Proxy = (function createProxy(NativeProxy) { - const { getPrototypeOf, setPrototypeOf } = Reflect; - const objectCreate = Object.create; + const reflectGetPrototypeOf = Reflect.getPrototypeOf; + const reflectSetPrototypeOf = Reflect.setPrototypeOf; - // Proxies are walked like any other object, so a cycle spanning several scriptlet - // proxies is found. The tortoise ends walks over chains that are already cyclic. - const reaches = (proto, proxy, target) => { + // Walk through other scriptlet proxies to catch cycles spanning several hooks. + // This can invoke page-owned getPrototypeOf traps, unlike native cycle checks. + const isCyclic = (proxy, target, proto) => { + const stops = (p) => p === null || p === proxy || p === target; let slow = proto; let fast = proto; - for (let i = 1; fast !== null; i++) { - if (fast === proxy || fast === target) return true; - fast = getPrototypeOf(fast); - if (i % 2 === 0) { - slow = getPrototypeOf(slow); - if (slow === fast) return false; - } - } - return false; + // Classic "tortoise and hare" cycle check + while (!stops(fast)) { + fast = reflectGetPrototypeOf(fast); + if (stops(fast)) break; + fast = reflectGetPrototypeOf(fast); + slow = reflectGetPrototypeOf(slow); + if (slow === fast) return false; + } + return fast === proxy || fast === target; }; return function Proxy(target, handler) { - // Inherit from the handler rather than copy it, so traps added to it later still apply. - const proxy = new NativeProxy(target, objectCreate(handler, { - // Returning false makes Object.setPrototypeOf throw and Reflect.setPrototypeOf - // return false, as both do for a native function. - setPrototypeOf: { value: (target, proto) => !reaches(proto, proxy, target) && setPrototypeOf(target, proto) }, - })); + // Copy only the scriptlet's own traps; never inherit traps from the page. + const proxy = new NativeProxy(target, { + __proto__: null, + ...handler, + setPrototypeOf(target, proto) { + // Do not inspect the target: it may itself be a page-owned proxy. + if (isCyclic(proxy, target, proto)) { + // Object.setPrototypeOf throws; Reflect.setPrototypeOf returns false. + // The engine's error text still differs from native cycle rejection. + return false; + } + return reflectSetPrototypeOf(target, proto); + }, + }); return proxy; }; })(globalThis.Proxy);