Skip to content

Add crossgen ArgIterator unit test for wasm Vector128 argument 16-byte alignment #131339

Closed

Description

@lewing

Summary

PR #131328 fixed a wasm ABI bug (#131299): the crossgen R2R ArgIterator under-aligned Vector128<T> arguments. The crossgen type system reports InstanceFieldAlignment == 8 for v128, while the runtime and interpreter 16-align v128 arguments via getClassAlignmentRequirement. The 8-byte desync accumulated per v128 argument, corrupting later arguments (e.g. the trailing generic-context byref) and producing a spurious NullReferenceException on the vectorized Base64Url.DecodeFromUtf8 path.

The fix landed without an automated regression test because there is no CI-runnable home for one today (see "Why deferred"). This issue tracks adding a deterministic crossgen ArgIterator unit test that locks in the invariant, so a future regression can't silently reintroduce the desync.

Why deferred (harness reality)

  • wasm R2R is not CI-runnable yet — there is no wasm corerun to exercise the path end-to-end.
  • The cDAC ARGITER stress harness cannot catch it. src/native/managed/cdac/tests/StressTests runs a debuggee under corerun (x64/x86/ARM) and compares cDAC ArgIterator<CdacTypeHandle>-derived GCRefMap blobs against the runtime's ComputeCallRefMap. The Wasm32 alignment code is a separate switch branch entered only for a Wasm32 TransitionBlock; an x64/ARM corerun run never enters it. So a stress run would go green with the wasm bug present — false confidence.
  • No existing xunit project reaches crossgen's ArgIterator<TypeHandle>. ILCompiler.ReadyToRun has no InternalsVisibleTo, and ILCompiler.ReadyToRun.Tests references crossgen2 with ReferenceOutputAssembly=false (build-only, no API access) and has no [Fact] files.

The fix is otherwise validated by equivalence (the pre-review fix and the reshaped fix produce byte-identical composites), a green browser run, and an artifact-level disassembly of WasmR2RToInterpreterThunk showing 16-aligned v128 slots for a v128-argument method.

Scaffolding required

  1. A test project (or an addition to an existing one) that compile-references ILCompiler.ReadyToRun so it can see ArgIterator<TypeHandle>, ArgIteratorData<TypeHandle>, TypeHandle, and TransitionBlock.
  2. InternalsVisibleTo from ILCompiler.ReadyToRun to that test project (these types are internal).
  3. A crossgen TypeSystemContext with Target.Architecture == Wasm32 whose SystemModule resolves System.Runtime.Intrinsics.Vector128\1`.

Because this is standalone crossgen/ILCompiler infrastructure, it is best done as part of broader crossgen ArgIterator unit-test coverage rather than bolted on for a single test.

Test to add

The v128 argument must be preceded by an i64 so the 8-vs-16 alignment is observable (otherwise both alignments land the v128 at the same offset). Signature: static void M(long, Vector128<int>, ref int).

Wasm32 ArgIterator offset math: _wasmOfsStack = ALIGN_UP(_wasmOfsStack, align); argOfs = OffsetOfArgs + _wasmOfsStack; _wasmOfsStack += ALIGN_UP(argSize, 8).

  • long: align 8 -> rel 0, advance to 8.
  • Vector128<int> (size 16): fixed align 16 -> rel 16, advance to 32. reverted align 8 -> rel 8, advance to 24.
  • ref int: align 8 -> fixed rel 32; reverted rel 24.

Expected: reverted crossgen -> offsets == [0, 8, 24] (asserts fail); fixed -> [0, 16, 32] (pass).

Variant A — via GCRefMapBuilder.BuildArgIterator

[Fact]
public void Wasm32_Vector128Argument_Is16ByteAligned()
{
    TypeSystemContext context = /* TODO(harness): Wasm32 context resolving Vector128`1 */;

    TypeDesc int32 = context.GetWellKnownType(WellKnownType.Int32);
    TypeDesc int64 = context.GetWellKnownType(WellKnownType.Int64);
    MetadataType vector128OfT = context.SystemModule.GetType("System.Runtime.Intrinsics", "Vector128`1");
    TypeDesc vector128OfInt = vector128OfT.MakeInstantiatedType(int32);
    TypeDesc refInt = int32.MakeByRefType();

    var signature = new MethodSignature(
        MethodSignatureFlags.Static, genericParameterCount: 0,
        returnType: context.GetWellKnownType(WellKnownType.Void),
        parameters: new[] { int64, vector128OfInt, refInt });

    (ArgIterator<TypeHandle> argit, TransitionBlock tb) =
        GCRefMapBuilder.BuildArgIterator(signature, context);

    var offsets = new List<int>();
    int argOffset;
    while ((argOffset = argit.GetNextOffset()) != TransitionBlock.InvalidOffset)
        offsets.Add(argOffset - tb.OffsetOfArgs);

    Assert.Equal(3, offsets.Count);
    Assert.Equal(0, offsets[0]);
    Assert.Equal(16, offsets[1]);
    Assert.Equal(0, offsets[1] % 16);
    Assert.Equal(offsets[1] + 16, offsets[2]);
}

Variant B — inline construction (no BuildArgIterator dependency)

TransitionBlock transitionBlock = TransitionBlock.FromTarget(
    context.Target.Architecture,
    context.Target.OperatingSystem == TargetOS.Windows,
    context.Target.IsApplePlatform,
    context.Target.Abi == TargetAbi.NativeAotArmel);

var parameterTypes = new TypeHandle[signature.Length];
for (int i = 0; i < parameterTypes.Length; i++)
    parameterTypes[i] = new TypeHandle(signature[i]);

var argIteratorData = new ArgIteratorData<TypeHandle>(
    hasThis: false, isVarArg: false, parameterTypes, new TypeHandle(signature.ReturnType));

var argit = new ArgIterator<TypeHandle>(
    transitionBlock, argIteratorData, CallingConventions.ManagedStatic,
    hasParamType: false, hasAsyncContinuation: false, extraFunctionPointerArg: false,
    forcedByRefParams: new bool[parameterTypes.Length], skipFirstArg: false, extraObjectFirstArg: false,
    isWindows: context.Target.IsWindows,
    objectTypeHandle: new TypeHandle(context.GetWellKnownType(WellKnownType.Object)),
    intPtrTypeHandle: new TypeHandle(context.GetWellKnownType(WellKnownType.IntPtr)));
// ... same GetNextOffset loop + assertions as Variant A.

Both variants are fix-shape-agnostic (they drive GetNextOffset() and check offsets; they never reference the removed RequiresAlign16OnWasm predicate), so they validate the invariant regardless of whether the fix is the predicate form or the GetFieldAlignment fold.

Interim evidence (already available)

  • WasmR2RToInterpreterThunk disassembly for a v128-argument method showing 16-aligned v128 slots (to be attached).
  • Equivalence: pre-review fix composites are byte-identical to the reshaped fix composites.
  • Green browser run of the relevant SIMD suite.

Note

This issue was authored 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 24, 2026
  2. dotnet-policy-service commented on Jul 24, 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. lewing commented on Jul 24, 2026

    @lewing
    MemberAuthor

    Interim evidence (artifact-level proof from the fixed crossgen output, as referenced in the "Interim evidence" section):

    Disassembly of WasmR2RToInterpreterThunk(VVVp) — a thunk taking three consecutive Vector128 arguments — from a browser composite R2R image built with the fix (func[49556]):

    16b3582 func[49556] <WasmR2RToInterpreterThunk(VVVp)>:
      local.get 0
      i32.const 64
      i32.sub
      local.set 0
      ...
      local.get 0
      local.get 1
      v128.store 4 16      ;; v128 arg 0 -> frame+16
      local.get 0
      local.get 2
      v128.store 4 32      ;; v128 arg 1 -> frame+32

    The two v128 argument slots land at frame offsets 16 and 32 — both 16-byte aligned, with a 16-byte stride, and the v128.store alignment hint is 4 (2^4 = 16 bytes). This matches the interpreter/VM ArgIterator, which 16-aligns v128 arguments via getClassAlignmentRequirement.

    Without the fix, GetFieldAlignment() returns 8 for Vector128<T> in the crossgen type system, so these slots would be 8-aligned instead (each v128 landing 8 bytes earlier than the interpreter expects, with the desync accumulating per v128 argument and corrupting the trailing arguments).

    This is the observable, artifact-level form of the same invariant the deferred unit test asserts at the offset-computation level (offsets[1] % 16 == 0, trailing arg at offsets[1] + 16).

    Note

    This comment was generated with GitHub Copilot.

  4. JulieLeeMSFT commented on Jul 27, 2026

    @JulieLeeMSFT
    Member
  5. added this to the 11.0.0 milestone on Jul 27, 2026
  6. dotnet-policy-service commented on Jul 27, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
    See info in area-owners.md if you want to be subscribed.

  7. added a commit that references this issue on Jul 27, 2026
    79a5a09
  8. locked and limited conversation to collaborators on Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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