Skip to content

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

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.

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