Summary
The compiled/emitted structured-clone core ($Runtime.StructuredClone → cloneCore) 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:
- Cloneable-but-non-container types are aliased instead of deep-cloned —
Date, RegExp, TypedArrays, ArrayBuffer, Buffer, Error instances. structuredClone(x) returns the same reference, so mutating the source is visible through the "clone".
- 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.cs → EmitStructuredCloneHelper / 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).
Summary
The compiled/emitted structured-clone core (
$Runtime.StructuredClone→cloneCore) 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'sStructuredClone.Clonein two ways:Date,RegExp, TypedArrays,ArrayBuffer,Buffer,Errorinstances.structuredClone(x)returns the same reference, so mutating the source is visible through the "clone".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.cs→EmitStructuredCloneHelper/cloneCoredeep-handles only:null,SharedArrayBuffer,List<object>,Dictionary<string,object>,Dictionary<object,object>,HashSet<object>. Every other value hitsfallbackReturn, which doesldarg.0; ret(returns the input by reference). By contrast, the interpreter'sRuntime/Types/StructuredClone.csCloneInternaldeep-clones Date/RegExp/TypedArray/ArrayBuffer/Buffer/Error and throwsDataCloneErrorfor functions/symbols/class instances/promises/etc.Empirical confirmation (interpreter = correct, compiled = buggy)
Affected consumers
cloneCoreis shared by compiledstructuredClone,$MessagePort.postMessage, and$BroadcastChannel.postMessage.Suggested fix direction
Extend
cloneCoreto (a) deep-clone Date/RegExp/TypedArray/ArrayBuffer/Buffer/Error (the interpreter'sStructuredClone.Cloneis the reference implementation), and (b) detect genuinely-uncloneable values (recursing into object/array/map/set members) and surface a clone failure. Constraints:SharpTS.dlldependency (the whole point of the emitted$MessagePort/clone).structuredCloneshould throw synchronously, while$MessagePort/$BroadcastChanneluse the receiver-sidemessageerrormodel — so the failure signal should be raised at the public boundaries appropriately (e.g. a distinguishable sentinel/exception the callers translate).