Skip to content

[wasm] R2R: 32-byte struct passed by value is corrupted (fields shifted by one word) #131639

Description

@pavelsavara

Description

On browser-wasm (CoreCLR) with PublishReadyToRun, a 32-byte struct (four 8-byte fields) passed by value to a method is corrupted: the callee reads it shifted up by one 8-byte word, so each field takes the value of the next and the following argument bleeds in. 16- and 24-byte structs pass correctly.

Repro

using System;

struct S32 { public long A, B, C, D; }

class Program
{
    static string E32(int x, S32 s, int y) => $"{x}|{s.A},{s.B},{s.C},{s.D}|{y}";

    static void Main()
    {
        Console.WriteLine(E32(1, new S32 { A = 31, B = 32, C = 33, D = 34 }, 2));
    }
}

E32 must be ReadyToRun-compiled. Interpreted execution is correct.

Expected

1|31,32,33,34|2

Actual

1|32,33,34,2|0

s.A reads the input B, s.B reads C, s.C reads D, s.D reads the trailing int y (2), and y reads garbage (0) — i.e. the struct argument is read one 8-byte word too high.

Notes

  • 16-byte ({ long, long }) and 24-byte ({ long, long, long }) structs pass correctly; only the 32-byte (four-word) struct is affected.
  • Reproduces with a direct call; reflection-invoking the same method corrupts it further.

Configuration

  • -os browser -a wasm, CoreCLR, PublishReadyToRun (crossgen2, --obj-format:wasm)
  • Reproduces in Node and in the browser.

Note

This issue was written with the assistance of GitHub Copilot.

Activity

  1. added
    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
    on Jul 31, 2026
  2. dotnet-policy-service commented on Jul 31, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
    See info in area-owners.md if you want to be subscribed.

  3. pavelsavara commented on Jul 31, 2026

    @pavelsavara
    MemberAuthor
  4. added this to the 11.0.0 milestone on Jul 31, 2026
  5. AndyAyersMS commented on Jul 31, 2026

    @AndyAyersMS
    Member

    AI's take on this:

    Wasm thunks are keyed globally by signature string, and structs are encoded as S<N>  — size only, no alignment.  WasmLowering.RaiseSignature  recovers the parameter type via  GetCachedStructOfSize(N) , which returns whatever struct of that size the compilation happened to see first.

    But the wasm ArgIterator aligns a struct's transition-block slot to  clamp(structAlignment, 8, 16) . So a 32-byte struct of  long s (align 8) and one of  Int128 s (align 16) have different argument layouts yet share one thunk.

    CoreLib's cached size-32 stand-in is  ValueTuple<Int128,Int128>  → align 16. Its  viS32ip  thunk therefore places the struct at TB+24 /  y  at TB+56, while the real (align-8) layout is TB+16 / TB+48. Every arg after the first shifts by 8 — exactly the reported corruption.

    Explains everything: only size 32; only when a preceding arg leaves the offset at 8 (not 16-aligned); only the first call (later calls go direct-R2R, bypassing the interp→R2R thunk); and why  (long,S32,int) / (int,int,S32,int)  pass (no CoreLib thunk for those strings, so the app's own correct thunk is used).

  6. AndyAyersMS commented on Jul 31, 2026

    @AndyAyersMS
    Member

    I can put up a plausible fix, but wonder if this intersects with #131492. Let me dig in a bit more.

  7. modified the milestones: 11.0.0, 12.0.0 on Aug 3, 2026
  8. AndyAyersMS commented on Aug 11, 2026

    @AndyAyersMS
    Member

    yeah this should build on top of #131492

  9. pavelsavara commented on Aug 18, 2026

    @pavelsavara
    MemberAuthor

    I can't reproduce this anymore

  10. pavelsavara commented on Aug 28, 2026

    @pavelsavara
    MemberAuthor

    I have repro in blazor onClick event

    ThrowRtlRestoreContextTag() (helpers.cpp:562)
    ::RtlRestoreContext(PCONTEXT, PEXCEPTION_RECORD) (helpers.cpp:591)
    ClrRestoreNonvolatileContext(_CONTEXT*, unsigned long) (threads.cpp:6571)
    EECodeManager::ResumeAfterCatch(_CONTEXT*, unsigned long, bool) (eetwain.cpp:1826)
    InterlockedDecrement (exceptionhandling.cpp:3383)
    Thread::DecPreventAbort() (threads.h:2230)
    CallCatchFunclet(unsigned char*, REGDISPLAY*, ExInfo*) (exceptionhandling.cpp:3270)
    CrawlFrame::GetRegisterSet() (exceptionhandling.cpp:4560)
    DispatchExSecondPass(ExInfo*) (exceptionhandling.cpp:4520)
    DispatchManagedException(Object*, _CONTEXT*, _EXCEPTION_RECORD*, ExKind) (exceptionhandling.cpp:1816)
    IL_Throw_Impl(Object*, TransitionBlock*) (jithelpers.cpp:815)
    IL_Throw_IMPL(unsigned long, Object*, unsigned long) (jithelpers.cpp:902)
    ::IL_Throw() (jithelpers.cpp:894)
    $Microsoft_JSInterop_Microsoft_JSInterop_JSRuntime__GetObjectReference (Microsoft.JSInterop.yn49ktdxft.wasm:0x8e09)
    $Microsoft_JSInterop_Microsoft_JSInterop_Infrastructure_DotNetDispatcher__BeginInvokeDotNet (Microsoft.JSInterop.yn49ktdxft.wasm:0xa6ad)
    $Microsoft_AspNetCore_Components_WebAssembly_Microsoft_AspNetCore_Components_WebAssembly_Services_DefaultWebAssemblyJSRuntime___c___BeginInvokeDotNet_b__12_0 (Microsoft.AspNetCore.Components.WebAssembly.f751340po3.wasm:0x2ab66)
    $WasmInterpreterToR2RThunk_vTS32p_ (System.Private.CoreLib.5kzzcpu1rm.wasm:0x743285)
    InvokeCalliStub(unsigned long, void (*)(unsigned long, signed char*, signed char*), signed char*, signed char*, Object**) (helpers.cpp:1870)
    InterpByteCodeStart::GetByteCodes() const (interpexec.cpp:3557)
    InterpExecMethod(InterpreterFrame*, InterpMethodContextFrame*, InterpThreadContext*, ExceptionClauseArgs*) (interpexec.cpp:1433)
    ::ExecuteInterpretedMethod(TransitionBlock *, TADDR, void *) (prestub.cpp:2100)
    ExecuteInterpretedMethodWithArgs_PortableEntryPoint(unsigned long, TransitionBlock*, unsigned long, signed char*) (prestub.cpp:2288)
    $WasmR2RToInterpreterThunk_viS32ip_ (Microsoft.AspNetCore.Components.WebAssembly.f751340po3.wasm:0x5cdc2)
    $Microsoft_AspNetCore_Components_WebAssembly_Microsoft_AspNetCore_Components_WebAssembly_Services_DefaultWebAssemblyJSRuntime__BeginInvokeDotNet (Microsoft.AspNetCore.Components.WebAssembly.f751340po3.wasm:0x291e0)
    $Microsoft_AspNetCore_Components_WebAssembly_Microsoft_AspNetCore_Components_WebAssembly_Services_DefaultWebAssemblyJSRuntime_____Wrapper_BeginInvokeDotNet_1855805417_g____Stub_23_0 (Microsoft.AspNetCore.Components.WebAssembly.f751340po3.wasm:0x2a6bb)
    $Microsoft_AspNetCore_Components_WebAssembly_Microsoft_AspNetCore_Components_WebAssembly_Services_DefaultWebAssemblyJSRuntime____Wrapper_BeginInvokeDotNet_1855805417 (Microsoft.AspNetCore.Components.WebAssembly.f751340po3.wasm:0x2a083)
    (anonymous namespace)::CallFunc_I32_RetVoid_PE(unsigned long, signed char*, signed char*) (callhelpers-interp-to-managed.cpp:505)
    InvokeCalliStub(unsigned long, void (*)(unsigned long, signed char*, signed char*), signed char*, signed char*, Object**) (helpers.cpp:1870)
    InterpByteCodeStart::GetByteCodes() const (interpexec.cpp:3557)
    InterpExecMethod(InterpreterFrame*, InterpMethodContextFrame*, InterpThreadContext*, ExceptionClauseArgs*) (interpexec.cpp:1433)
    ::ExecuteInterpretedMethod(TransitionBlock *, TADDR, void *) (prestub.cpp:2100)
    ExecuteInterpretedMethodWithArgs_PortableEntryPoint(unsigned long, TransitionBlock*, unsigned long, signed char*) (prestub.cpp:2288)
    $WasmR2RToInterpreterThunk_iTiS8p_ (System.Private.CoreLib.5kzzcpu1rm.wasm:0x731cc8)
    $S_P_CoreLib_System_Reflection_MethodBaseInvoker__InvokeWithOneArg (System.Private.CoreLib.5kzzcpu1rm.wasm:0x385d73)
    $S_P_CoreLib_System_Reflection_RuntimeMethodInfo__Invoke (System.Private.CoreLib.5kzzcpu1rm.wasm:0x3a6ccc)
    $S_P_CoreLib_System_Reflection_MethodBase__Invoke (System.Private.CoreLib.5kzzcpu1rm.wasm:0x38428c)
    $System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JSHostImplementation___c__DisplayClass17_0___BindManagedFunction_b__0 (System.Runtime.InteropServices.JavaScript.vmpbo5azdi.wasm:0xa20f)
    $WasmInterpreterToR2RThunk_vTip_ (System.Private.CoreLib.5kzzcpu1rm.wasm:0x7443eb)
    InvokeCalliStub(unsigned long, void (*)(unsigned long, signed char*, signed char*), signed char*, signed char*, Object**) (helpers.cpp:1870)
    InterpByteCodeStart::GetByteCodes() const (interpexec.cpp:3557)
    InterpExecMethod(InterpreterFrame*, InterpMethodContextFrame*, InterpThreadContext*, ExceptionClauseArgs*) (interpexec.cpp:1433)
    ::ExecuteInterpretedMethod(TransitionBlock *, TADDR, void *) (prestub.cpp:2100)
    ExecuteInterpretedMethodWithArgs(unsigned long, signed char*, unsigned long, void*, unsigned long) (prestub.cpp:2164)
    ::ExecuteInterpretedMethodFromUnmanaged(MethodDesc *, int8_t *, size_t, int8_t *, PCODE) (prestub.cpp:2313)
    Call_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_CallJSExport_I32_I32_RetVoid(int, void*) (callhelpers-reverse.cpp:118)
    ::SystemInteropJS_CallJSExport(int32_t, void *) (callhelpers-reverse.cpp:123)
    SystemInteropJS_CallJSExport (dotnet.native.a4ygtxctyj.js:3340)
    ie (dotnet.runtime.6dqfcdbqmc.js:4)
    (anonymous) (dotnet.runtime.6dqfcdbqmc.js:4)
    beginInvokeDotNetFromJS (blazor.webassembly.js:1)
    invokeDotNetMethodAsync (blazor.webassembly.js:1)
    invokeMethodAsync (blazor.webassembly.js:1)
    (anonymous) (blazor.webassembly.js:1)
    T (blazor.webassembly.js:1)
    (anonymous) (blazor.webassembly.js:1)
    invokeWhenHeapUnlocked (blazor.webassembly.js:1)
    (anonymous) (blazor.webassembly.js:1)
    T (blazor.webassembly.js:1)
    R (blazor.webassembly.js:1)
    dispatchGlobalEventToAllElements (blazor.webassembly.js:1)
    onGlobalEvent (blazor.webassembly.js:1)
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

arch-wasmWebAssembly architecturearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions