Emit a null check in the compiler-synthesized inline-array helpers (`InlineArrayElementRef` / `InlineArrayAsSpan`)
Area-CompilersFeature - Inline ArraysFeature - Unsafe Evolution
### 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 条评论