Skip to content

Compiled structured clone (cloneCore) is shallow: aliases Date/RegExp/TypedArray/etc. and never throws DataCloneError on uncloneable values (dual-mode divergence) #1255

Description

@nickna

Summary

The compiled/emitted structured-clone core ($Runtime.StructuredClonecloneCore) is shallow: it only deep-handles a handful of container types and returns everything else by reference. This diverges from both Node and the interpreter's StructuredClone.Clone in two ways:

  1. Cloneable-but-non-container types are aliased instead of deep-clonedDate, RegExp, TypedArrays, ArrayBuffer, Buffer, Error instances. structuredClone(x) returns the same reference, so mutating the source is visible through the "clone".
  2. Uncloneable values are silently passed through instead of throwing DataCloneError — functions/closures, symbols, class instances, Promise, etc., both at the top level and nested inside objects/arrays.

Surfaced while implementing #1077 (PR #1253).

Root cause

Compilation/RuntimeEmitter.Worker.csEmitStructuredCloneHelper / cloneCore deep-handles only: null, SharedArrayBuffer, List<object>, Dictionary<string,object>, Dictionary<object,object>, HashSet<object>. Every other value hits fallbackReturn, which does ldarg.0; ret (returns the input by reference). By contrast, the interpreter's Runtime/Types/StructuredClone.cs CloneInternal deep-clones Date/RegExp/TypedArray/ArrayBuffer/Buffer/Error and throws DataCloneError for functions/symbols/class instances/promises/etc.

Empirical confirmation (interpreter = correct, compiled = buggy)

// (1) Date — should be independent
const d = new Date(1000); const c: any = structuredClone(d); d.setTime(2000);
console.log(c.getTime() === 1000);            // interp: true    compiled: false (aliased)

// (2) TypedArray — should be independent
const a = new Int32Array([5]); const b: any = structuredClone(a); a[0] = 99;
console.log(b[0] === 5);                        // interp: true    compiled: false (aliased)

// (3) top-level function — should throw DataCloneError
try { structuredClone(() => {}); console.log(false); } catch { console.log(true); }
                                                // interp: true    compiled: false (returned by ref)

// (4) nested function property — should throw DataCloneError
try { structuredClone({ fn: () => {} }); console.log(false); } catch { console.log(true); }
                                                // interp: true    compiled: false

// (5) symbol — should throw DataCloneError
try { structuredClone(Symbol("x")); console.log(false); } catch { console.log(true); }
                                                // interp: true    compiled: false

Affected consumers

cloneCore is shared by compiled structuredClone, $MessagePort.postMessage, and $BroadcastChannel.postMessage.

Note: PR #1253 (#1077) added a targeted top-level typeof "function"/"symbol" check in $MessagePort.postMessage so it fires messageerror for the common uncloneable case, but the underlying cloneCore gaps — nested uncloneables, and the aliasing of Date/RegExp/TypedArray/etc. — remain. $BroadcastChannel.postMessage and structuredClone itself still don't detect any uncloneable.

Suggested fix direction

Extend cloneCore to (a) deep-clone Date/RegExp/TypedArray/ArrayBuffer/Buffer/Error (the interpreter's StructuredClone.Clone is the reference implementation), and (b) detect genuinely-uncloneable values (recursing into object/array/map/set members) and surface a clone failure. Constraints:

  • Must stay standalone — pure emitted IL, no SharpTS.dll dependency (the whole point of the emitted $MessagePort/clone).
  • The three consumers want different failure surfacing: structuredClone should throw synchronously, while $MessagePort/$BroadcastChannel use the receiver-side messageerror model — so the failure signal should be raised at the public boundaries appropriately (e.g. a distinguishable sentinel/exception the callers translate).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions