fix(js): structuredClone cycles through Error.cause and clones own __proto__ (#419, #420) (#421)

* fix(js): structuredClone cycles through Error.cause and clones own __proto__ (#419, #420)

The structuredClone polyfill had two correctness/parity gaps in _structuredClone:

- The Error branch recursed into `cause` before recording the clone in `seen`,
  so a cycle through the error (e.cause === e) recursed until the stack
  overflowed. Chrome clones such graphs and preserves identity. Fix: seen.set
  the clone before recursing into cause. (#419)

- The generic-object branch built the clone on the source's prototype and
  copied properties by assignment, so an own enumerable `__proto__` data
  property (exactly what JSON.parse('{"__proto__":…}') produces) went through
  the inherited __proto__ setter and reparented the clone instead of copying
  the property. Fix: clone plain objects onto Object.prototype (like Chrome)
  and define each property instead of assigning it. (#420)

Adds two parity tests to structured_clone_crypto_parity.rs; full obscura-cdp
suite (73) green.

* perf(js): keep structuredClone fast path for all keys but __proto__

The own-__proto__ fix copied every own property with Object.defineProperty
to avoid triggering the inherited prototype setter. That is ~3x slower
than plain assignment per key, so cloning a large object regressed
(~75ms -> ~228ms for a 1000-key object x200).

Only the __proto__ key needs defineProperty; every other key is safe to
assign. Special-case it so the fast path applies to all other keys.
Measured back to parity with main (85ms vs 85ms); the own-__proto__
correctness test still passes.

---------

Co-authored-by: SGavrl <[email protected]>
This commit is contained in:
Andrey MnatsakanovandSGavrl authored and GitHub committed 2026-07-16 12:56:10 +02:00
1 parent f5dc97b131
commit b2e4bb49a7
2 files changed
+89 -2

No files matched your search

+23 -2
View File
@@ -5296,6 +5296,9 @@ function _structuredClone(value, seen) {
if (value instanceof Error) {
const Ctor = value.constructor || Error;
const e = new Ctor(value.message);
// Record the clone before recursing into `cause`, otherwise a cycle
// through the error (e.cause === e) recurses until the stack overflows.
seen.set(value, e);
if (value.name) e.name = value.name;
if (value.stack) e.stack = value.stack;
if (value.cause !== undefined) e.cause = _structuredClone(value.cause, seen);
@@ -5308,11 +5311,29 @@ function _structuredClone(value, seen) {
const hook = globalThis.__obscura_clone_hooks[value[Symbol.toStringTag]];
if (typeof hook === "function") return hook(value, seen);
}
const out = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
// Plain objects clone onto Object.prototype (like Chrome), not the source's
// prototype. Define each property instead of assigning it: a source with an
// own enumerable `__proto__` data prop (what JSON.parse('{"__proto__":…}')
// yields) would otherwise hit the inherited __proto__ setter and reparent
// the clone instead of copying the property.
const out = Array.isArray(value) ? [] : {};
seen.set(value, out);
for (const k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
out[k] = _structuredClone(value[k], seen);
const cloned = _structuredClone(value[k], seen);
// Only `__proto__` needs defineProperty: plain assignment would hit the
// inherited prototype setter and reparent the clone instead of adding an
// own data property. Every other key takes the fast assignment path.
if (k === "__proto__") {
Object.defineProperty(out, k, {
value: cloned,
writable: true,
enumerable: true,
configurable: true,
});
} else {
out[k] = cloned;
}
}
}
// Symbols are not enumerable via for-in; copy own symbol-keyed properties.