Skip to content

Enumerating type safety guarantees in MemoryMarshal and friends #41418

@GrabYourPitchforks

Description

@GrabYourPitchforks

I realize that with Unsafe, MemoryMarshal, and friends entering wide use, we never formally stated what type safety guarantees (if any) the APIs offer. That is, we never provided a list of what APIs are "safe" and which are "unsafe equivalents" (related: #31354).

This issue is an attempt to enumerate the APIs on Unsafe, MemoryMarshal, and related types from the perspective of type safety / memory safety. Ultimately I think this needs to be tracked somewhere, but whether that's a .md file in this repo or an official doc page I don't really know.

I'm also soliciting feedback on this list. Please let me know if I got something wrong.

I'm not listing APIs which expose raw pointers through their public API surface. APIs which take or return pointers are always assumed unsafe.

Reminder: "Unsafe equivalent" does not mean "must not be used." Rather, it means that the API can be used to violate a type's contract or will bypass the runtime's normal type safety checks. Think of these APIs as being equivalent to using the unsafe keyword within your code. If you are acting as a code reviewer, give extra scrutiny to calls to these APIs, as you would give extra scrutiny to any code involving pointers or other unsafe constructs.

System.Runtime.CompilerServices.Unsafe

API safe or unsafe See notes
Add<T>(ref T, int) unsafe equivalent (1)
Add<T>(ref T, IntPtr) unsafe equivalent (1)
AddByteOffset<T>(ref T, IntPtr) unsafe equivalent (1)
AreSame<T>(ref T, ref T) safe (2)
AsRef<T>(in T) unsafe equivalent (3), (22)
As<T>(object) unsafe equivalent (4)
As<TFrom, TTo>(ref TFrom) unsafe equivalent (4)
ByteOffset<T>(ref T, ref T) unsafe equivalent (5)
CopyBlock(ref byte, ref byte, uint) unsafe equivalent (6)
CopyBlockUnaligned(ref byte, ref byte, uint) unsafe equivalent (6)
InitBlock(ref byte, ref byte, uint) unsafe equivalent (6)
InitBlockUnaligned(ref byte, ref byte, uint) unsafe equivalent (6)
IsAddressGreaterThan<T>(ref T, ref T) safe (2)
IsAddressLessThan<T>(ref T, ref T) safe (2)
IsNullRef<T>(ref T) safe (2), (10)
NullRef<T>() safe (2), (10)
ReadUnaligned<T>(ref byte) unsafe equivalent (4)
SkipInit<T>(out T) unsafe equivalent (7)
SizeOf<T>() safe (8)
Subtract<T>(ref T, int) unsafe equivalent (1)
Subtract<T>(ref T, IntPtr) unsafe equivalent (1)
SubtractByteOffset<T>(ref T, IntPtr) unsafe equivalent (1)
Unbox<T>(object) unsafe equivalent (9)
WriteUnaligned<T>(ref byte) unsafe equivalent (4)

System.Runtime.InteropServices.MemoryMarshal

API safe or unsafe See notes
AsBytes<T>(ReadOnlySpan<T>) unsafe equivalent (11)
AsBytes<T>(Span<T>) unsafe equivalent (11)
AsMemory<T>(ReadOnlyMemory<T>) unsafe equivalent (3)
AsRef<T>(ReadOnlySpan<byte>) unsafe equivalent (11), (12)
AsRef<T>(Span<byte>) unsafe equivalent (11), (12)
Cast<TFrom, TTo>(ReadOnlySpan<T>) unsafe equivalent (11), (12), (14)
Cast<TFrom, TTo>(Span<T>) unsafe equivalent (11), (12), (14)
CreateFromPinnedArray<T>(T[], int, int) unsafe equivalent (15)
CreateReadOnlySpan<T>(ref T, int) unsafe equivalent (6)
CreateSpan<T>(ref T, int) unsafe equivalent (6)
GetArrayDataReference<T>(T[]) unsafe equivalent (16)
GetReference<T>(ReadOnlySpan<T>) unsafe equivalent (3), (16), (22)
GetReference<T>(Span<T>) unsafe equivalent (16)
Read<T>(ReadOnlySpan<byte>) unsafe equivalent (11), (13)
ToEnumerable<T>(ReadOnlyMemory<T>) safe
TryGetArray<T>(ReadOnlyMemory<T>, ...) unsafe equivalent (3), (17), (18)
TryGetMemoryManager<T>(ReadOnlyMemory<T>, ...) unsafe equivalent (3), (17), (18)
TryGetString<T>(ReadOnlyMemory<char>, ...) safe (17), (18)
TryRead<T>(ReadOnlySpan<byte>, out T) unsafe equivalent (11), (13)
TryWrite<T>(Span<byte>, ref T) unsafe equivalent (11), (13)
Write<T>(Span<byte>, ref T) unsafe equivalent (11), (13)

System.Runtime.InteropServices.SequenceMarshal

API safe or unsafe See notes
TryGetArray<T>(...) unsafe equivalent (3), (17), (18)
TryGetReadOnlyMemory<T>(...) safe
TryGetReadOnlySequenceSegment<T>(...) safe
TryRead<T>(...) unsafe equivalent (11), (13)

System.Runtime.InteropServices.CollectionsMarshal

API safe or unsafe See notes
AsSpan<T>(List<T>) safe (21)

System.GC

API safe or unsafe See notes
AllocateArray<T>(int, bool) safe
AllocateUninitializedArray<T>(int, bool) unsafe equivalent (7)

GetPinnableReference pattern

Though GetPinnableReference methods are intended for compiler use within fixed blocks, they're designed to be type-safe when called by hand.

API safe or unsafe See notes
string.GetPinnableReference() safe (19)
ReadOnlySpan<T>.GetPinnableReference() safe (20)
Span<T>.GetPinnableReference() safe (20)

Miscellaneous

API safe or unsafe See notes
ArrayPool<T>.Shared.Rent(int) unsafe equivalent (7)
MemoryPool<T>.Shared.Rent(int) unsafe equivalent (7)

Notes

In the below notes, I'm using the terms gcref and managed pointer interchangeably.

  • (1) Arithmetic operations on gcrefs (such as via Unsafe.Add) are not checked for correctness by the runtime. The resulting gcref may point to invalid memory or to a different object. See ECMA-335, Sec. III.1.5.

  • (2) It is legal and type-safe to perform comparisons against gcrefs. See ECMA-225, Sec. III.1.5 and Table III.4.

  • (3) Stripping the "readonly"-ness of a gcref is analogous to using C++'s const_cast operator. It could allow mutation of a value that the caller did not intend to make mutable.

  • (4) The runtime will not validate that casts performed by these APIs are correct. This is equivalent to C++'s reinterpret_cast operator. Improper casts could result in buffer overruns when accessing the backing value or in incorrect entry points being invoked when calling instance methods.

  • (5) While it is legal to calculate the absolute offset between two gcrefs, it is unverifiable to do so. See ECMA-335, Sec. III.1.5 and Table III.2.

  • (6) The runtime does not validate the buffer lengths provided to these APIs. Improper usage could result in buffer overruns.

  • (7) Use of this API could expose uninitialized memory to the caller. See ECMA-335, Sec. II.15.4.1.3 and Sec. III.1.8.1.1. If the uninitialized memory is projected as a non-primitive struct, the instance's backing fields could contain data which violates invariants that would normally be guaranteed by the instance's ctor.

  • (8) The sizeof CIL instruction is always safe. See ECMA-335, Sec. III.4.25.

  • (9) The unbox CIL instruction is intended to return a controlled-mutability managed pointer. However, Unsafe.Unbox returns a fully mutable gcref. This could allow mutation of a boxed readonly struct, which is illegal. See the Unsafe.Unbox docs for more information.

  • (10) Per ECMA-335, Sec. II.14.4.2, it is not strictly legal for a gcref to point to null. However, all .NET runtimes allow this and treat it in a type-safe fashion, including guarding accesses to null gcrefs by throwing NullReferenceException as appropriate.

  • (11) This method performs the equivalent of a C++-style reinterpret_cast. This bypasses normal constructor validation, potentially returning values with inconsistent internal state. Projecting unmanaged types as byte buffers may also expose or allow modification of private fields that the type author did not intend, an unsafe reflection equivalent.

  • (12) The runtime does not perform alignment checks. The caller is responsible for ensuring that any returned refs or spans are properly aligned. Most APIs that accept refs or spans as parameters assume that the references are properly aligned, and they may exhibit undefined behavior if this assumption is violated.

  • (13) This method handles unaligned data accesses correctly.

  • (14) This method is safe if TFrom and TTo are integral primitives of the same width. For example, TFrom = int with TTo = uint is safe. Integral primitives are: byte, sbyte, short, ushort, int, uint, long, ulong, nint, nuint, and enums backed by any of these. The caller is responsible for providing a correct TFrom and TTo; the runtime will not validate these type parameters.

  • (15) The runtime will not validate that the array is pre-pinned. Additionally, since Memory<T> instances are subject to struct tearing, any instances backed by pre-pinned arrays must be used with caution in multithreading scenarios, as calling Memory<T>.Pin on a torn instance backed by a pre-pinned array may result in an access violation.

  • (16) If called against a zero-length array or buffer, returns a gcref to where the value at index 0 would have been. It is legal to use such a gcref for comparison purposes (see, e.g., Unsafe.IsAddressLessThan), and the gcref will be properly GC-tracked. However, it is illegal to dereference such a gcref. See ECMA-335, Sec. III.1.1.5.2.

  • (17) Memory<T>'s implementation is currently backed by one of: T[], string, or MemoryManager<T>. However, since Memory<T> is an abstraction, new backing mechanisms may be introduced in the future. Callers must account for the runtime allowing all of TryGet{Array|MemoryManager|String} to return false; and callers must have a fallback code path in order to remain future-proof.

  • (18) This API may expose the larger buffer beyond the slice bounded by the Memory<T> instance. Callers should take care not to reference data beyond the slice provided to them.

  • (19) This API will never return a null reference. If called on an empty string, it will return a reference to the null terminator. The return value can always be safely dereferenced.

  • (20) This API will return a null reference if the underlying span contains no elements. Attempting to dereference it will result in a normal NullReferenceException being thrown. Note also that unlike pinning string instances, the buffer resulting from pinning a ReadOnlySpan<char> or Span<char> reference is not guaranteed to be null-terminated. Consumers must not attempt to read off the end of such buffers.

  • (21) Improper use of this API could corrupt the state of the associated object. However, it would not be considered a type safety or memory safety violation.

  • (22) The runtime will not validate that writes to the ref will satisfy covariant type safety constraints. For example, a local of type ref readonly object may actually point to a field typed as string. Removing the readonly constraint and treating it as a mutable ref object may allow assignment of a non-string to the backing string field.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Securityarea-MetadocumentationDocumentation bug or enhancement, does not impact product or test code

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions