Repository navigation
[API Proposal]: Mark Unsafe as caller-unsafe #126956
Copy link
Copy link
Open
Labels
api-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedarea-System.Runtime.CompilerServicesreduce-unsafe
Milestone
Description
Background and motivation
API Proposal
public static partial class Unsafe
{
+ /// <safety>Applies an unchecked byte offset to a managed reference; the result may point outside the source object and can crash the GC even if it is never dereferenced.</safety>
+ unsafe
public static ref T AddByteOffset<T>(ref T source, System.IntPtr byteOffset) where T : allows ref struct { throw null; }
+ /// <safety>Applies an unchecked byte offset to a managed reference; the result may point outside the source object and can crash the GC even if it is never dereferenced.</safety>
+ unsafe
public static ref T AddByteOffset<T>(ref T source, nuint byteOffset) where T : allows ref struct { throw null; }
+ /// <safety>Applies an unchecked element offset to a managed reference; the result may point outside the source object and can crash the GC even if it is never dereferenced.</safety>
+ unsafe
public static ref T Add<T>(ref T source, int elementOffset) where T : allows ref struct { throw null; }
+ /// <safety>Applies an unchecked element offset to a managed reference; the result may point outside the source object and can crash the GC even if it is never dereferenced.</safety>
+ unsafe
public static ref T Add<T>(ref T source, System.IntPtr elementOffset) where T : allows ref struct { throw null; }
+ /// <safety>Applies an unchecked element offset to a managed reference; the result may point outside the source object and can crash the GC even if it is never dereferenced.</safety>
+ unsafe
public static ref T Add<T>(ref T source, nuint elementOffset) where T : allows ref struct { throw null; }
public static bool AreSame<T>([System.Diagnostics.CodeAnalysis.AllowNull] ref readonly T left, [System.Diagnostics.CodeAnalysis.AllowNull] ref readonly T right) where T : allows ref struct { throw null; }
+ /// <safety>Strips read-only protection from a reference, yielding a writable reference; writing through it to genuinely read-only memory (e.g. an RVA static field or a string literal) can corrupt memory or fault.</safety>
+ unsafe
public static ref T AsRef<T>(scoped ref readonly T source) where T : allows ref struct { throw null; }
+ /// <safety>Reinterprets an object reference as an unrelated type with no type check; later field access can read past the object's bounds or treat arbitrary bytes as a reference.</safety>
+ unsafe
public static T? As<T>(object? o) where T : class? { throw null; }
+ /// <safety>
+ /// Reinterprets a reference to one type as a reference to another with no type or size check;
+ /// accesses may exceed the source's bounds or fabricate references from arbitrary bytes.
+ /// </safety>
+ unsafe
public static ref TTo As<TFrom, TTo>(ref TFrom source) where TFrom : allows ref struct where TTo : allows ref struct { throw null; }
+ /// <safety>
+ /// Reinterprets the bits of a value as another type of the same size; may expose the source's
+ /// padding bytes or fabricate a reference/pointer field in the result from arbitrary bits.
+ /// </safety>
+ unsafe
public static TTo BitCast<TFrom, TTo>(TFrom source) where TFrom : allows ref struct where TTo : allows ref struct { throw null; }
public static System.IntPtr ByteOffset<T>([System.Diagnostics.CodeAnalysis.AllowNull] ref readonly T origin, [System.Diagnostics.CodeAnalysis.AllowNull] ref readonly T target) where T : allows ref struct { throw null; }
+ /// <safety>Copies a caller-specified number of bytes between two references with no bounds check; may read or write past the end of either buffer.</safety>
+ unsafe
public static void CopyBlock(ref byte destination, ref readonly byte source, uint byteCount) { }
+ /// <safety>
+ /// Copies a caller-specified number of bytes between two references with no bounds check;
+ /// may read or write past the end of either buffer.
+ /// </safety>
+ unsafe
public static void CopyBlockUnaligned(ref byte destination, ref readonly byte source, uint byteCount) { }
+ /// <safety>Writes a caller-specified number of bytes through a reference with no bounds check; may write past the end of the buffer.</safety>
+ unsafe
public static void InitBlock(ref byte startAddress, byte value, uint byteCount) { }
+ /// <safety>Writes a caller-specified number of bytes through a reference with no bounds or alignment check; may write past the end of the buffer.</safety>
+ unsafe
public static void InitBlockUnaligned(ref byte startAddress, byte value, uint byteCount) { }
public static bool IsAddressGreaterThan<T>([System.Diagnostics.CodeAnalysis.AllowNull] ref readonly T left, [System.Diagnostics.CodeAnalysis.AllowNull] ref readonly T right) where T : allows ref struct { throw null; }
public static bool IsAddressGreaterThanOrEqualTo<T>([System.Diagnostics.CodeAnalysis.AllowNull] ref readonly T left, [System.Diagnostics.CodeAnalysis.AllowNull] ref readonly T right) where T : allows ref struct { throw null; }
public static bool IsAddressLessThan<T>([System.Diagnostics.CodeAnalysis.AllowNull] ref readonly T left, [System.Diagnostics.CodeAnalysis.AllowNull] ref readonly T right) where T : allows ref struct { throw null; }
public static bool IsAddressLessThanOrEqualTo<T>([System.Diagnostics.CodeAnalysis.AllowNull] ref readonly T left, [System.Diagnostics.CodeAnalysis.AllowNull] ref readonly T right) where T : allows ref struct { throw null; }
public static bool IsNullRef<T>(ref readonly T source) where T : allows ref struct { throw null; }
public static ref T NullRef<T>() where T : allows ref struct { throw null; }
+ /// <safety>Reads a value of type T from a byte reference with no bounds or alignment check; may read past the buffer and can populate a pointer/reference field of T from arbitrary bytes.</safety>
+ unsafe
public static T ReadUnaligned<T>(scoped ref readonly byte source) where T : allows ref struct { throw null; }
public static int SizeOf<T>() where T : allows ref struct { throw null; }
+ /// <safety>Leaves the variable uninitialized, so subsequent reads observe arbitrary, uninitialized memory.</safety>
+ unsafe
public static void SkipInit<T>(out T value) where T : allows ref struct { throw null; }
+ /// <safety>Applies an unchecked byte offset to a managed reference; the result may point outside the source object and can crash the GC even if it is never dereferenced.</safety>
+ unsafe
public static ref T SubtractByteOffset<T>(ref T source, System.IntPtr byteOffset) where T : allows ref struct { throw null; }
+ /// <safety>Applies an unchecked byte offset to a managed reference; the result may point outside the source object and can crash the GC even if it is never dereferenced.</safety>
+ unsafe
public static ref T SubtractByteOffset<T>(ref T source, nuint byteOffset) where T : allows ref struct { throw null; }
+ /// <safety>Applies an unchecked element offset to a managed reference; the result may point outside the source object and can crash the GC even if it is never dereferenced.</safety>
+ unsafe
public static ref T Subtract<T>(ref T source, int elementOffset) where T : allows ref struct { throw null; }
+ /// <safety>Applies an unchecked element offset to a managed reference; the result may point outside the source object and can crash the GC even if it is never dereferenced.</safety>
+ unsafe
public static ref T Subtract<T>(ref T source, System.IntPtr elementOffset) where T : allows ref struct { throw null; }
+ /// <safety>Applies an unchecked element offset to a managed reference; the result may point outside the source object and can crash the GC even if it is never dereferenced.</safety>
+ unsafe
public static ref T Subtract<T>(ref T source, nuint elementOffset) where T : allows ref struct { throw null; }
+ /// <safety>Returns a mutable interior reference into a boxed value with no type check; a wrong T reinterprets the box's contents.</safety>
+ unsafe
public static ref T Unbox<T>(object box) where T : struct { throw null; }
+ /// <safety>Writes a value of type T through a byte reference with no bounds or alignment check; may write past the end of the buffer.</safety>
+ unsafe
public static void WriteUnaligned<T>(ref byte destination, T value) where T : allows ref struct { }
}API Usage
The annotated APIs will require unsafe{} context at their call site.
Alternative Designs
No response
Risks
No response
Reacted by Tanner Gooding and Hamish Arblaster
Metadata
Metadata
Assignees
Labels
api-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedarea-System.Runtime.CompilerServicesreduce-unsafe
Activity
dotnet-policy-service commentedon Apr 15, 2026
Tagging subscribers to this area: @dotnet/area-system-runtime-compilerservices
See info in area-owners.md if you want to be subscribed.
hez2010 commentedon Apr 16, 2026
I think most operations like
Add/Subtractare completely safe as long as no one is dereferencing the ref. Maybe we need anunsafe refconcept in the language?hamarb123 commentedon Apr 16, 2026
I am pretty sure that is not true that it is only unsafe to dereference it in all cases, and that merely having it can be problematic.
tannergooding commentedon Apr 16, 2026
Right, we expect that byrefs pointing into the managed heap are valid even if not dereferenced and so
Unsafe.Add(ref T source, int elementOffset)puts an onus of responsibility on the caller to ensure invalid byrefs are not produced.Where-as
Unsafe.Add(void* source, int elementOffset)does a similar operation, there is no expectation that an unmanaged pointer is valid and so the caller has no responsibility. The consumer of the output of the call may have a responsibility to not dereference it, but that is done by requiring indirection of a pointer to be in anunsafe contexttannergooding commentedon Apr 16, 2026
@agocke, it looks like we don't have any explicit callout in the memory safety doc (https://github.com/dotnet/designs/blob/main/accepted/2025/memory-safety/caller-unsafe.md) about why such operations on
byrefsare unsafe (but I know we've discussed it plenty internally). Is this just an oversight?huoyaoyuan commentedon Apr 17, 2026
Should the address comparison methods be considered safe? They would be unreliable if referencing different objects.
EgorBo commentedon Apr 17, 2026
Sure, but It doesn't qualify as a memory safety issue according to our definitions: it doesn't let you touch a memory you may not own or it doesn't expose a non-initialized memory, we don't annotate APIs based on whether you can write a bad code or not with them.
Annotate System.Runtime ref APIs with caller-unsafe semantics
tannergooding commentedon Apr 20, 2026
This one seems like it should be unsafe. Users expect they can dereference a
ref T, but this one will cause aNullReferenceException. It's basically likeMemoryMarshal.GetReference()where it may be null-ref, may be valid and dereferenceable, or may be valid but not dereferenceable because it points to start+length.The other annotations and addition/removal of unsafe look correct.
jkotas commentedon Apr 20, 2026
You can trivially implement NullRef() in 100% safe C#. If NullRefRef is unsafe, then something in the following code should be unsafe too:
tannergooding commentedon Apr 20, 2026
Its still a memory safety violation and a form of AV. It just happens to be an AV that has a special exception type thrown due to its prominence.
jkotas commentedon Apr 20, 2026
NullReferenceException (caused by accessing null object reference or null byref) is not a memory safety violation. It is likely a bug in the program, but not a memory safety violation.
21 remaining items
jeffhandley commentedon Sep 9, 2026
@EgorBo I moved this to .NET 12. Please pull back in to .NET 11 if that's still the target.
jnm2 commentedon Oct 16, 2026
I believe this is factually incorrect. The wrong T does not reinterpret the box's contents*:
https://lab.razor.fyi/#45rLyMUVUJSfXpSYq5dcLNTHWFqcmZeuEFxZXJKaq-ecn5OTmlySmZ9XrOeZm1takpiUk2rNhaImqDSvJDM3Vc85P7cgMye1KDi1qCwzObXYmosrPykrNblEISm_IjVFwVZBA26EY1FRYqVNZl6JnWa0Yaw1V2hecWJaql5oXlJ-BVhYA6xH09qLuag0L4qT48SqFS9WZAiwJDBWMAIA
As documented for the
unboxIL instruction (which this method provides access to):*with one exception: type punning is allowed by the runtime between enums with the same underlying type and with the underlying type itself
EgorBo commentedon Oct 16, 2026
Yes and it was mentioned in the API review. The API is still unsafe, but the actual comment will be different.
GeeLaw commentedon Oct 17, 2026
@jnm2 's mentioning of
Unbox(andenumtype punning) prompted me to think adversarially and there seems to be a defect in ECMA-335 that invalidates immutability of boxed primitive types (claimed by the spec itself).unboxbetweenenums and their underlying types.enums emitted by C# usepublicinstance fields and that field is notinitonly, hence assignable given controlled-mutability&to thatenum.Therefore, a boxed primitive can be mutated, in a way valid to the word of ECMA-335, by unboxing to a typical
enumtype then assigning to its instance field. It seems the validity of such mutation is deeply rooted because ECMA-355 explicitly wants boxed primitive to be unboxable toenumunderlied by it. (The full analysis chain is Edition 6, p.431unbox<- p.312 verifier-assignable-to <- p.36 verification type <- p.36 reduced type <- p.35 underlying type.)Proof of concept using C# and IL in
<details>.Details
In addition, in PowerShell:
hamarb123 commentedon Oct 17, 2026
@GeeLaw interesting point, however unsafe v2 is pretty C#-centric and C# already does not allow accessing the
value__field - presumably it would make sense for other languages that want to support unsafe v2 to also implement this rule for safe code (and there's not really a good reason to allow direct access to this field anyway tbh) - it may make sense to add an ECMA-335 augment that specifies thatldfldafor the underlying field on anenumis controlled-mutability-ref when the original byref was a controlled-mutability-ref, my guess is that this was meant to be implied in ECMA-335 - note similarly thatldfldaonly specifies thatfieldcannot be init-only & that using it to compute the address of a static init-only field and then using that to modify the value is UB, it does not explicitly ban this either:but this is obviously illegal. Probably there's a lot of updates that ECMA-335 needs in these kinds of areas - it was written before we had a lot of new .NET Runtime APIs that are effectively acting as IL instructions, before
ref readonlyexisted in c#, before byref locals existed in c#, etc. - some of these changes / new features have made their way into the augments, but not all have necessarily been spec'd there fully.GeeLaw commentedon Oct 17, 2026
@hamarb123 The C# example isn't related. ROS indexer gives
ref readonly, whose C# semantics is more restrictive than IL controlled-mutability&. The 2nd line won't compile even if C# allowed access tovalue__, and should be changed toref readonly field = ref rvaSpan[0].value__;. After this change, the assignment on the 3rd line won't compile.Yes and it should also ban
stfldon controlled-mutability&toenum, so thatenumare alwaysreadonly structeven if the emitted IL code doesn't say that.hamarb123 commentedon Oct 17, 2026
Right, the C# semantics are more restrictive, but you will not find any mention of
ref readonlyreturns in ECMA-335, nor any rule forbiddingldfldaon the ref returned from it and then assigning into that (which is what I pointed out). New C# features expose things in IL that were previously not available to safe c# code, which then needs new rules.No, I was giving pseudo-C# to represent the relevant IL (there's no benefit to posting a massive chunk of IL here when it is obvious from the pseudo-C# what IL was meant).
ECMA-335 does actually spec that enums are
readonlystructs in effect (it specifies that all of its methods must be what we refer to asreadonly). The only theoretical loophole is using the field directly, but that is not allowed in C# already by spec (readonlytype is a C# feature also (not an ECMA-335 one), and thusenums are actually already effectively meeting all the requirements forreadonlyin C#). See ECMA-335 III.2.1:Plus
II.22.37for not being able to define custom members.agocke commentedon Oct 17, 2026
The biggest flaw is that IL has no lifetime analysis for managed references, so all managed references in IL need to be considered unsafe.
hamarb123 commentedon Oct 17, 2026
Right, there are very few uses of managed references in IL that ECMA-335 actually calls out as verifiable compared to what we can actually write in modern (even safe) C#.
GeeLaw commentedon Oct 17, 2026
OK gotcha. In that case, my mental model is that
ROSitself imposes additional constraints on what operations are valid, so being valid in IL (while suppressing cross-method analysis; see the next paragraph) doesn't imply well-definedness perROS. The point I raised is simply IL semantics and it could date back to the initial release of .NET Framework (but I don't have .NET fx 1.0 installed, sigh...), and it's about "verifiable => safe".Another issue with controlled-mutability
&is that restriction cannot be flowed across methods via metadata. Therefore, whether code violates controlled-mutability restriction must be done with all code inlined (barring P/Invoke). Also, therefultimately comes fromldsflda initonly, which is unverifiable, and safety must be reasoned by other logic (going back to "additional constraints ofROS").On focusing on C#, I do consider PowerShell a .NET language. OTOH I think maybe the issue with ctrl-mut
&toenumshould be filed separately, instead of being collocated to where the idea is inspired.hamarb123 commentedon Oct 17, 2026
What I am meaning here is: if powershell wants to have an unsafe v2 interop feature, it would not just be
value__that changes, it would be a brand new massive feature. Unsafe v2 is a C# feature, and other languages that want to benefit from it also probably need to change their access/memory models to match C#, otherwise something that is safe in C# might not be safe in another language or vice versa. For example, in VB.NET (which doesn't even support "unsafe v1" or pointers at all) you can define:and then call that from C# like so:
From a C# compiler rules pov, this is an unsafe API, but VB.NET has its own model in which this is fine (vb.net doesn't allow SkipLocalsInit for example and automatically initializes all locals, but more the point is that VB.NET unsafety model is basically pointers or ref structs (certain uses) or a few other minor things == unsafe & hard blocked, rest is okay) - in fact, if you have
[SkipLocalsInit]on your c# assembly, evenxwill not be initialized potentially (it literally is equivalent toUnsafe.SkipInitAPI, just minus theallows ref struct).My point here is simply that just because something is possible from non-C# without unsafe code, doesn't mean that it's actually safe under the C# unsafe v2 feature.
Different .NET languages model different safety kinds in different ways - C# unsafe v2 considerations can't consider what every theoretical and pre-existing .NET language could legally call on it without requiring their version of unsafe, as then pretty much all APIs would have to be unsafe - the ECMA-335 issue is an actual issue here, but it's not as though this one controlled-mutability-ref thing is the only spot it is out of date or missing some clarifications like this either.
jkotas commentedon Oct 17, 2026
The original ECMA-335 makes returning byref unverifiable (look for "Using a managed pointer type for the return type of methods is not verifiable."). It was adequate for most uses of managed references until the introduction of byref-like types. There was a draft of updated IL verification rules for byref-like types, but it was not never completed and included in ECMA augments.