ITADN

Emit a null check in the compiler-synthesized inline-array helpers (`InlineArrayElementRef` / `InlineArrayAsSpan`)

#84344OpenEgorBo 创建于 2026-06-30
Area-CompilersFeature - Inline ArraysFeature - Unsafe Evolution
E
EgorBocommented
### Background When an inline-array value is indexed or converted to `Span<T>`, the compiler emits calls to synthesized helpers in `<PrivateImplementationDetails>` that form a byref/span from `buffer` with **no null check**. Element access (`buffer[index]`) emits `InlineArrayElementRef`: ```il .method assembly hidebysig static !!TElement& InlineArrayElementRef<TBuffer, TElement> ( !!TBuffer& buffer, int32 index) cil managed { IL_0000: ldarg.0 IL_0001: call !!1& Unsafe::As<!!TBuffer, !!TElement>(!!0&) IL_0006: ldarg.1 IL_0007: call !!0& Unsafe::Add<!!TElement>(!!0&, int32) IL_000c: ret } ``` The whole-array `Span<T>` conversion emits `InlineArrayAsSpan`: ```il .method assembly hidebysig static valuetype System.Span`1<!!TElement> InlineArrayAsSpan<TBuffer, TElement> ( !!TBuffer& buffer, int32 length) cil managed { IL_0000: ldarg.0 IL_0001: call !!1& Unsafe::As<!!TBuffer, !!TElement>(!!0&) IL_0006: ldarg.1 IL_0007: call valuetype System.Span`1<!!0> MemoryMarshal::CreateSpan<!!TElement>(!!0&, int32) IL_000c: ret } ``` Neither helper null-checks `buffer`. If `buffer` is a null byref, the helper hands out a byref/span at a caller-controlled offset (`index`/`length`) over a null/garbage base, and subsequent access yields arbitrary reads/writes that do **not** reliably fault. This is reachable **without `unsafe`** (`<AllowUnsafeBlocks>false</AllowUnsafeBlocks>`), e.g. from `Unsafe.NullRef<T>()`, or a `default`-initialized `ref struct` field that (recursively) contains a byref, etc. ### Repro (safe code with both unsafe-v1 and unsafe-v2) ```csharp using System; using System.Runtime.CompilerServices; ref S s = ref Unsafe.NullRef<S>(); ref byte e = ref s[0xA00000]; // InlineArrayElementRef -> Unsafe.Add(ref <null base>, index) e = 1; // may or may not fail with AccessViolationException. Expected: deterministic NRE. Span<byte> span = s; // InlineArrayAsSpan -> MemoryMarshal.CreateSpan(ref <null base>, length) span[0xA00000] = 1; // may or may not fail with AccessViolationException. Expected: deterministic NRE. [InlineArray(134217720)] // ~128 MB - 8 B public struct S { public byte F; } ``` ### Request Emit a null check on `buffer` at the top of these synthesized helpers, so that forming an element ref / span from a null inline-array ref reliably throws `NullReferenceException` instead of producing a byref/span over arbitrary memory. In the common case where the base is provably non-null, the JIT should be able to elide the redundant check. This affects the element-ref and as-span helper families: - `InlineArrayElementRef` / `InlineArrayElementRefReadOnly` - `InlineArrayAsSpan` / `InlineArrayAsReadOnlySpan` (`InlineArrayFirstElementRef` / `InlineArrayFirstElementRefReadOnly` form a ref at offset 0, so a null base faults on first dereference — they could be hardened for consistency but aren't the same silent-corruption hazard.) ### Originating discussion (dotnet/runtime) - https://github.com/dotnet/runtime/issues/126956#issuecomment-4844451848 - https://github.com/dotnet/runtime/issues/126956#issuecomment-4845009823
12 条评论