Repository navigation
Description
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
corerunto exercise the path end-to-end. - The cDAC ARGITER stress harness cannot catch it.
src/native/managed/cdac/tests/StressTestsruns a debuggee undercorerun(x64/x86/ARM) and compares cDACArgIterator<CdacTypeHandle>-derived GCRefMap blobs against the runtime'sComputeCallRefMap. The Wasm32 alignment code is a separateswitchbranch entered only for a Wasm32TransitionBlock; an x64/ARMcorerunrun 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.ReadyToRunhas noInternalsVisibleTo, andILCompiler.ReadyToRun.Testsreferences crossgen2 withReferenceOutputAssembly=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
- A test project (or an addition to an existing one) that compile-references
ILCompiler.ReadyToRunso it can seeArgIterator<TypeHandle>,ArgIteratorData<TypeHandle>,TypeHandle, andTransitionBlock. InternalsVisibleTofromILCompiler.ReadyToRunto that test project (these types are internal).- A crossgen
TypeSystemContextwithTarget.Architecture == Wasm32whoseSystemModuleresolvesSystem.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)
WasmR2RToInterpreterThunkdisassembly 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
dotnet-policy-service commentedon Jul 24, 2026
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.
lewing commentedon Jul 24, 2026
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 consecutiveVector128arguments — from a browser composite R2R image built with the fix (func[49556]):The two v128 argument slots land at frame offsets 16 and 32 — both 16-byte aligned, with a 16-byte stride, and the
v128.storealignment hint is4(2^4 = 16 bytes). This matches the interpreter/VMArgIterator, which 16-aligns v128 arguments viagetClassAlignmentRequirement.Without the fix,
GetFieldAlignment()returns 8 forVector128<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 atoffsets[1] + 16).Note
This comment was generated with GitHub Copilot.
[wasm][R2R] Fix Vector128 argument alignment in crossgen ArgIterator (#…
[wasm][R2R] Fix Vector128 argument alignment in crossgen ArgIterator (d…
JulieLeeMSFT commentedon Jul 27, 2026
@tannergooding, PTAL. cc @adamperlin @AndyAyersMS .
dotnet-policy-service commentedon Jul 27, 2026
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.
Make wasm 'V' signature raising deterministic and add a regression te…