Repository navigation
Description
Summary
Under ReadyToRun on WebAssembly (Release / shipping config), System.Buffers.Text.Base64Url.DecodeFromUtf8 throws a spurious NullReferenceException for inputs large enough to take the Vector128 (SIMD) decode path. The same call is correct under the interpreter (DOTNET_ReadyToRun=0). Reproduces on both browser and wasi with Release crossgen2 + Release corerun, so it is an OS-neutral R2R codegen bug.
This is distinct from #131298 (that one is Debug/checked-only). This one reproduces in the Release/shipping configuration.
Minimal repro
using System;
using System.Buffers.Text;
class Program {
static int Main() {
string alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
Span<byte> source = new byte[64];
for (int i = 0; i < 64; i++) source[i] = (byte)alpha[i % alpha.Length];
Span<byte> dst = new byte[32];
var st = Base64Url.DecodeFromUtf8(source, dst, out int c, out int w, isFinalBlock: true);
Console.WriteLine($"status={st} c={c} w={w}");
return 42;
}
}- R2R off (interp):
status=DestinationTooSmall c=... w=...(correct) - R2R on:
Unhandled exception. System.NullReferenceException at System.Buffers.Text.Base64Helper.DecodeFrom[TBase64Decoder,T](TBase64Decoder decoder, ReadOnlySpan`1 source, Span`1 bytes, Int32& bytesConsumed, Int32& bytesWritten, Boolean isFinalBlock, Boolean ignoreWhiteSpace) at System.Buffers.Text.Base64Url.DecodeFromUtf8(...)
Trigger threshold
| source length | path | R2R result |
|---|---|---|
| ≤ 32 bytes | scalar | correct (DestinationTooSmall) |
| ≥ 64 bytes | Vector128Decode |
NullReferenceException |
Independent of isFinalBlock (both true/false fail at ≥64). Reproduces with both --codegenopt:JitWasmSimdNyiToR2RUnsupported=0 and =1, so it is not simply "SIMD allowed in R2R".
Localization
- Faulting method:
Base64Helper.DecodeFrom<TBase64Decoder,T>→ the wasm vectorized branchVector128Decode<TBase64Decoder,T>(guarded byPackedSimd.IsSupported,src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Helper/Base64DecoderHelper.cs). - The scalar path uses
decoder.DecodingMap(aReadOnlySpan<sbyte>→ RVA data blob) and works. The vectorized path additionally loadsdecoder.Vector128LutHigh/Low/Shift(ReadOnlySpan<int>/<uint>LUTs) and calls generic struct-interface methodsdecoder.TryLoadVector128/decoder.TryDecode128Core. - Isolated micro-repros that do not reproduce (so the bug is more specific than any of these alone): a plain
ReadOnlySpan<int>collection-expression +Vector128.Create; and a generic struct span-property dispatch +Vector128.Create. The NRE only appears in the fullVector128Decodecontext, pointing at the R2R codegen of that specialized generic method (Vector128Decode<TBase64Decoder,T>) rather than any single lowered construct.
Impact
- Affects shipping (Release) R2R-on-wasm: any Base64Url (and likely Base64) decode ≥ ~64 bytes throws. Surfaced by
Microsoft.Bcl.Memory.Tests(Base64UrlDecoderUnitTests.*) — 12 tests fail with this NRE under R2R, all pass under interp.
Environment
- CoreCLR R2R-on-wasm, Release crossgen2 + Release corerun,
browserandwasi, wasm32.
Note
This issue was authored with the assistance of GitHub Copilot.
Activity
dotnet-policy-service commentedon Jul 24, 2026
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.
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
Debugger root-cause (nesm WASI): R2R↔interpreter boundary for a many-Vector128 signature
Reproduced under the nesm WASI interpreter (full R2R managed-name symbolication) on both
browserandwasiRelease. The NRE originates in the R2R generic methodBase64Helper.DecodeFrom<Base64UrlDecoderByte,byte>at the call that dispatches the vectorized inner helper, which has wasm signatureiTVVVVVVVVip— i.e. a generic-context/this(T) + 8×Vector128(V) +i32+PCODE.Key facts from the debugger:
WasmDelayLoadHelper→WasmR2RToInterpreterThunk(iTVVVVVVVVip)→ interp, independent ofJitWasmSimdNyiToR2RUnsupported(0 or 1). The NRE is thrown inside the interpreted execution of that large-signature helper.Base64UrlDecoderByte.TryLoadVector128is not separately reached, so the fault is at the 8×Vector128helper dispatch, not the LUT loads.Vector128iteration) lines up exactly with taking this vectorized path.This points at the argument marshalling for a signature carrying many
Vector128args plus a generic context across the R2R↔interp boundary — the same'V'(v128) thunk-signature family recently introduced (#131246, #131239, #131227). The sig-key classification (helpers.cppToV128→'V') and the crossgen type-lowering (WasmLowering.IsWasmV128Type/LowerToAbiType) both look correct; the remaining gap is in arg layout/size on the boundary (candidates:ArgIterator.SizeOfFrameArgumentArray/per-arg offsets for v128 on wasm vs the interpreter's arg-local layout; the generic-context slot offset when preceded/followed by 8× v128). A latent related gap:src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cshas no'V'case, soTokenToSlotCount('V')returns 1 slot (8B) instead of 2 (16B) — not on this exact crossgen path, but worth closing.Minimal repro (unchanged) —
Base64Url.DecodeFromUtf8with ≥64 valid base64url bytes into a smaller destination: correct under interp, NRE under R2R.Note
This analysis was produced with the assistance of GitHub Copilot.
JulieLeeMSFT commentedon Jul 24, 2026
@adamperlin, PTAL.
cc @tannergooding
[wasm][R2R] Give Vector<T> 16-byte field alignment on wasm
[wasm][R2R] Fix Vector128 argument alignment in crossgen ArgIterator (#…
[wasm][R2R] Fix Vector128 argument alignment in crossgen ArgIterator (d…