You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On the WebAssembly CoreCLR + ReadyToRun (crossgen2-wasm) configuration, all 64-bit JS interop values are corrupted when crossing the managed↔JS boundary. long/BigInt64, double, DateTime/DateTimeOffset and Int52 arguments and return values lose their high 32 bits in a very specific way:
corrupted = (original & 0xFFFFFFFF) << 32
i.e. the low 32 bits are moved into the high word of the 8-byte slot and the low word becomes zero. 32-bit types (int, float, char, bool, handles, nint) are not affected.
The same tests pass on main (CoreCLR + browser + interpreter, no R2R) — verified on CI: the full System.Runtime.InteropServices.JavaScript.Tests suite runs with 0 failures on the Chrome lane. So this is an R2R-only wasm codegen problem, not a pre-existing interpreter bug.
Motivation / expectations
.NET long is marshaled to/from JavaScript BigInt (via [JSMarshalAs<JSType.BigInt>]), and the managed side stores the value into a JSMarshalerArgument slot whose Int64Value field is documented as:
[FieldOffset(0)]internallongInt64Value;// must be aligned to 8 because of HEAPI64 alignment
The JS side reads/writes that slot through the 8-byte heap views:
The contract is that a long/BigInt round-trips losslessly: EchoBigInt64(x) == x for every x in [long.MinValue, long.MaxValue]. Under R2R this contract is violated for every value whose high 32 bits are non-zero.
Observed behavior (real repro)
JSImport echo of a BigInt64 through an identity JS function:
Instrumenting the JS callback shows the value arrives already corrupted, before the echo:
echoBigInt64 received: -4294967296 (0xFFFFFFFF00000000) for input -1
echoBigInt64 received: 4294967296 (0x0000000100000000) for input 1
echoBigInt64 received: -4294967296 for input long.MaxValue
echoBigInt64 received: 0 for input long.MinValue
So the corruption is on the managed→JS argument path (and symmetrically on the JS→managed return path).
Root cause
The corruption pattern low32 << 32 is exactly what happens when an 8-byte value is read through HEAP64[byteAddr >>> 3] while byteAddr is 4 mod 8 (4-byte aligned but not 8-byte aligned): >>> 3 rounds the address down by 4 bytes, so JS reads [0,0,0,0, low32(value)].
The JSMarshalerArgument slot is Size = 32 (a multiple of 8), so each slot is 8-aligned relative to the buffer base. Therefore the buffer base itself is landing at a 4-mod-8 address under R2R. (In a trivial sample assembly, R2R aligns stackalloc and collection-expression spans to 8 correctly; the misalignment shows up for the real marshaler buffer in the generated JSImport stub frame / pinned local.)
Minimal, deterministic, JS-free reproduction of the mechanism
This forces a 4-mod-8 slot address and reads it both ways — the managed exact-address read is correct, the HEAP64-style aligned-down read reproduces the corruption byte-for-byte. It also shows that a 4-byte read at the same address is fine:
usingSystem;usingSystem.Runtime.InteropServices;publicclassTest{[StructLayout(LayoutKind.Explicit,Size=32)]privatestructSlot{[FieldOffset(0)]publiclongInt64Value;}publicstaticintMain(){intf=0;foreach(longvinnew[]{-1L,1L,long.MaxValue,long.MinValue,9007199254740991L})f+=Check(v);returnf;}privatestaticunsafeintCheck(longvalue){Slots=default;s.Int64Value=value;byte*raw=stackallocbyte[3*32+16];nintaligned8=((nint)raw+7)&~7;byte*misaligned=(byte*)(aligned8+4);// force 4 mod 8varbuffer=newSpan<Slot>(misaligned,3);buffer.Clear();buffer[2]=s;nintaddr=(nint)(misaligned+2*32);// slot[2].Int64Value, FieldOffset 0longjsI64=*(long*)(addr&~7);// HEAP64[addr>>>3] (needs 8-align)intjsI32=*(int*)(addr&~3);// HEAP32[addr>>>2] (needs 4-align)Console.WriteLine($"value=0x{value:X16} i64(JS)=0x{jsI64:X16} i32(JS)=0x{jsI32:X8} "+$"i64:{(jsI64==value?"PASS":"FAIL")} i32:{(jsI32==(int)value?"PASS":"FAIL")}");returnjsI64==value?0:1;}}
i32: PASS for all — a 4-mod-8 address is still 0-mod-4, so HEAP32/HEAPU32 reads are unaffected. Only the 8-byte heap views break.
Affected types
8-byte JS-interop marshaling, which routes through HEAP64/HEAPF64:
long / BigInt64 (incl. nullable, arrays, Span<long>)
double (incl. arrays, Span<double>)
DateTime / DateTimeOffset (marshaled as double)
Int52
32-bit types are unaffected.
Expected fix
Ensure the JSMarshalerArgument marshaler buffer (the pinned span / stack local built by the generated JSImport/JSExport stub) is 8-byte aligned under crossgen2-wasm R2R, so that HEAP64[addr >>> 3] addresses the intended slot. Equivalently, R2R wasm codegen should honor 8-byte alignment for stack/pinned locals that contain 8-byte-aligned fields.
Summary
On the WebAssembly CoreCLR + ReadyToRun (crossgen2-wasm) configuration, all 64-bit JS interop values are corrupted when crossing the managed↔JS boundary.
long/BigInt64,double,DateTime/DateTimeOffsetandInt52arguments and return values lose their high 32 bits in a very specific way:i.e. the low 32 bits are moved into the high word of the 8-byte slot and the low word becomes zero. 32-bit types (
int,float,char,bool, handles,nint) are not affected.The same tests pass on
main(CoreCLR + browser + interpreter, no R2R) — verified on CI: the fullSystem.Runtime.InteropServices.JavaScript.Testssuite runs with 0 failures on the Chrome lane. So this is an R2R-only wasm codegen problem, not a pre-existing interpreter bug.Motivation / expectations
.NET
longis marshaled to/from JavaScriptBigInt(via[JSMarshalAs<JSType.BigInt>]), and the managed side stores the value into aJSMarshalerArgumentslot whoseInt64Valuefield is documented as:The JS side reads/writes that slot through the 8-byte heap views:
The contract is that a
long/BigIntround-trips losslessly:EchoBigInt64(x) == xfor everyxin[long.MinValue, long.MaxValue]. Under R2R this contract is violated for every value whose high 32 bits are non-zero.Observed behavior (real repro)
JSImportecho of aBigInt64through an identity JS function:Result under R2R:
-1-14294967295111(fits in 32 bits)long.MaxValue92233720368547758074294967295long.MinValue-92233720368547758080900719925474099190071992547409914294967295Instrumenting the JS callback shows the value arrives already corrupted, before the echo:
So the corruption is on the managed→JS argument path (and symmetrically on the JS→managed return path).
Root cause
The corruption pattern
low32 << 32is exactly what happens when an 8-byte value is read throughHEAP64[byteAddr >>> 3]whilebyteAddris4 mod 8(4-byte aligned but not 8-byte aligned):>>> 3rounds the address down by 4 bytes, so JS reads[0,0,0,0, low32(value)].The
JSMarshalerArgumentslot isSize = 32(a multiple of 8), so each slot is 8-aligned relative to the buffer base. Therefore the buffer base itself is landing at a4-mod-8address under R2R. (In a trivial sample assembly, R2R alignsstackallocand collection-expression spans to 8 correctly; the misalignment shows up for the real marshaler buffer in the generatedJSImportstub frame / pinned local.)Minimal, deterministic, JS-free reproduction of the mechanism
This forces a
4-mod-8slot address and reads it both ways — the managed exact-address read is correct, theHEAP64-style aligned-down read reproduces the corruption byte-for-byte. It also shows that a 4-byte read at the same address is fine:Output (slot at
4-mod-8):i32: PASSfor all — a4-mod-8address is still0-mod-4, soHEAP32/HEAPU32reads are unaffected. Only the 8-byte heap views break.Affected types
8-byte JS-interop marshaling, which routes through
HEAP64/HEAPF64:long/BigInt64(incl. nullable, arrays,Span<long>)double(incl. arrays,Span<double>)DateTime/DateTimeOffset(marshaled asdouble)Int5232-bit types are unaffected.
Expected fix
Ensure the
JSMarshalerArgumentmarshaler buffer (the pinned span / stack local built by the generatedJSImport/JSExportstub) is 8-byte aligned under crossgen2-wasm R2R, so thatHEAP64[addr >>> 3]addresses the intended slot. Equivalently, R2R wasm codegen should honor 8-byte alignment for stack/pinned locals that contain 8-byte-aligned fields.Environment
dotnet/runtimeWebAssembly CoreCLR + R2R (crossgen2-wasm) configuration.main(interpreter) CI:System.Runtime.InteropServices.JavaScript.TestsChrome lane = 0 failures.category
Note
This issue text was generated with the assistance of GitHub Copilot and reviewed before posting.