-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
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
unsafekeyword 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
System.Runtime.InteropServices.MemoryMarshal
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_castoperator. 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_castoperator. 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
sizeofCIL instruction is always safe. See ECMA-335, Sec. III.4.25. -
(9) The
unboxCIL instruction is intended to return a controlled-mutability managed pointer. However,Unsafe.Unboxreturns a fully mutable gcref. This could allow mutation of a boxed readonly struct, which is illegal. See theUnsafe.Unboxdocs 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
NullReferenceExceptionas 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 =
intwith TTo =uintis 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 callingMemory<T>.Pinon 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, orMemoryManager<T>. However, sinceMemory<T>is an abstraction, new backing mechanisms may be introduced in the future. Callers must account for the runtime allowing all ofTryGet{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
NullReferenceExceptionbeing thrown. Note also that unlike pinningstringinstances, the buffer resulting from pinning aReadOnlySpan<char>orSpan<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 objectmay actually point to a field typed asstring. Removing the readonly constraint and treating it as a mutableref objectmay allow assignment of a non-stringto the backingstringfield.