Skip to content

[API Proposal]: Mark Unsafe as caller-unsafe #126956

Open

Description

@EgorBo

Background and motivation

#125145

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

Activity

  1. added
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    on Apr 15, 2026
  2. dotnet-policy-service commented on Apr 15, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @dotnet/area-system-runtime-compilerservices
    See info in area-owners.md if you want to be subscribed.

  3. hez2010 commented on Apr 16, 2026

    @hez2010
    Contributor

    I think most operations like Add/Subtract are completely safe as long as no one is dereferencing the ref. Maybe we need an unsafe ref concept in the language?

  4. hamarb123 commented on Apr 16, 2026

    @hamarb123
    Contributor

    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.

  5. tannergooding commented on Apr 16, 2026

    @tannergooding
    Member

    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 an unsafe context

  6. tannergooding commented on Apr 16, 2026

    @tannergooding
    Member

    @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 byrefs are unsafe (but I know we've discussed it plenty internally). Is this just an oversight?

  7. huoyaoyuan commented on Apr 17, 2026

    @huoyaoyuan
    Member

    Should the address comparison methods be considered safe? They would be unreliable if referencing different objects.

  8. EgorBo commented on Apr 17, 2026

    @EgorBo
    MemberAuthor

    Should the address comparison methods be considered safe? They would be unreliable if referencing different objects.

    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.

  9. tannergooding commented on Apr 20, 2026

    @tannergooding
    Member

    public static ref T NullRef<T>() where T : allows ref struct { throw null; }

    This one seems like it should be unsafe. Users expect they can dereference a ref T, but this one will cause a NullReferenceException. It's basically like MemoryMarshal.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.

  10. jkotas commented on Apr 20, 2026

    @jkotas
    Member

    public static ref T NullRef() where T : allows ref struct { throw null; }
    This one seems like it should be unsafe. Users expect they can dereference a ref T

    You can trivially implement NullRef() in 100% safe C#. If NullRefRef is unsafe, then something in the following code should be unsafe too:

    Console.WriteLine(MyNullRef<int>());
    
    static ref T MyNullRef<T>()
    {
        MyStruct<T> s = default;
        return ref s.x;
    }
    
    ref struct MyStruct<T>
    {
        public ref T x;
    }
    
  11. tannergooding commented on Apr 20, 2026

    @tannergooding
    Member

    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.

  12. jkotas commented on Apr 20, 2026

    @jkotas
    Member

    Its still a memory safety violation

    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.

  13. 21 remaining items

  14. modified the milestones: 11.0.0, 12.0.0 on Sep 9, 2026
  15. jeffhandley commented on Sep 9, 2026

    @jeffhandley
    Member

    @EgorBo I moved this to .NET 12. Please pull back in to .NET 11 if that's still the target.

  16. jnm2 commented on Oct 16, 2026

    @jnm2
    Contributor
    /// <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; }

    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 unbox IL instruction (which this method provides access to):

    InvalidCastException is thrown if the object is not boxed as valType.

    NullReferenceException is thrown if the object reference is a null reference.

    *with one exception: type punning is allowed by the runtime between enums with the same underlying type and with the underlying type itself

  17. EgorBo commented on Oct 16, 2026

    @EgorBo
    MemberAuthor

    I believe this is factually incorrect.

    Yes and it was mentioned in the API review. The API is still unsafe, but the actual comment will be different.

  18. GeeLaw commented on Oct 17, 2026

    @GeeLaw

    @jnm2 's mentioning of Unbox (and enum type 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).

    • Type punning allowed by unbox between enums and their underlying types.
    • Controlled mutability allows storing to public instance fields.
    • Typical enums emitted by C# use public instance fields and that field is not initonly, hence assignable given controlled-mutability & to that enum.

    Therefore, a boxed primitive can be mutated, in a way valid to the word of ECMA-335, by unboxing to a typical enum type 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 to enum underlied by it. (The full analysis chain is Edition 6, p.431 unbox <- 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
    public class Program
    {
      public enum Trickery { Zero, One, Two }
    
      public static void Foo(object o)
      {
        // ILDASM, then rewrite the body, then ILASM.
        // .maxstack 32
        // ldarg.0
        // unbox Program/Trickery
        // ldc.i4.1
        // stfld int32 Program/Trickery::value__
        // ret
        // Note: ILVerify 10.0.13 incorrectly rejects this method.
      }
    
      public static void Main()
      {
        object o = 0;
        Foo(o);
        Console.WriteLine(o);
      }
    }

    In addition, in PowerShell:

    $obj = [System.StringSplitOptions]::None;
    $obj.value__ = 1;
    $obj; // RemoveEmptyEntries
  19. hamarb123 commented on Oct 17, 2026

    @hamarb123
    Contributor

    @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 that ldflda for the underlying field on an enum is 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 that ldflda only specifies that field cannot 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:

    ReadOnlySpan<ConsoleColor> rvaSpan = [1, 2, 3]; // no ldflda, ldsflda, or other relevant ECMA-335 section on this - it uses a custom runtime API instead
    ref var field = ref rvaSpan[0].value__;
    field = 10; // ldflda + assigning is not invalid as per above if you read the ECMA-335 "literally"

    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 readonly existed 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.

  20. GeeLaw commented on Oct 17, 2026

    @GeeLaw

    @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 to value__, and should be changed to ref readonly field = ref rvaSpan[0].value__;. After this change, the assignment on the 3rd line won't compile.

    it may make sense to add an ECMA-335 augment that specifies that ldflda for the underlying field on an enum is controlled-mutability-ref when the original byref was a controlled-mutability-ref

    Yes and it should also ban stfld on controlled-mutability & to enum, so that enum are always readonly struct even if the emitted IL code doesn't say that.

  21. hamarb123 commented on Oct 17, 2026

    @hamarb123
    Contributor

    C# example isn't related. ROS indexer gives ref readonly, whose C# semantics is more restrictive than IL controlled-mutability &.

    Right, the C# semantics are more restrictive, but you will not find any mention of ref readonly returns in ECMA-335, nor any rule forbidding ldflda on 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.

    The 2nd line won't compile even if C# allowed access to value__, and should be changed to ref readonly field = ref rvaSpan[0].value__;. After this change, the assignment on the 3rd line won't compile.

    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).

    Yes and it should also ban stfld on controlled-mutability & to enum, so that enum are always readonly struct even if the emitted IL code doesn't say that.

    ECMA-335 does actually spec that enums are readonly structs in effect (it specifies that all of its methods must be what we refer to as readonly). The only theoretical loophole is using the field directly, but that is not allowed in C# already by spec (readonly type is a C# feature also (not an ECMA-335 one), and thus enums are actually already effectively meeting all the requirements for readonly in C#). See ECMA-335 III.2.1:

    This last case can only occur when method was defined on System.Object, System.ValueType, 
    or System.Enum and not overridden by thisType. In this last case, the boxing causes a copy of 
    the original object to be made, however since all methods on System.Object, 
    System.ValueType, and System.Enum do not modify the state of the object, this fact cannot be 
    detected.
    

    Plus II.22.37 for not being able to define custom members.

  22. agocke commented on Oct 17, 2026

    @agocke
    Member

    The biggest flaw is that IL has no lifetime analysis for managed references, so all managed references in IL need to be considered unsafe.

  23. hamarb123 commented on Oct 17, 2026

    @hamarb123
    Contributor

    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#.

  24. GeeLaw commented on Oct 17, 2026

    @GeeLaw

    you will not find any mention of ref readonly returns in ECMA-335, nor any rule forbidding ldflda on the ref returned from it and then assigning into that (which is what I pointed out).
    pseudo-C# to represent the relevant IL

    OK gotcha. In that case, my mental model is that ROS itself 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 per ROS. 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, the ref ultimately comes from ldsflda initonly, which is unverifiable, and safety must be reasoned by other logic (going back to "additional constraints of ROS").

    On focusing on C#, I do consider PowerShell a .NET language. OTOH I think maybe the issue with ctrl-mut & to enum should be filed separately, instead of being collocated to where the idea is inspired.

  25. hamarb123 commented on Oct 17, 2026

    @hamarb123
    Contributor

    On focusing on C#, I do consider PowerShell a .NET language. OTOH I think maybe the issue with ctrl-mut & to enum should be filed separately, instead of being collocated to where the idea is inspired.

    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:

    Public Shared Sub SkipInit(Of T)(<Out> ByRef x As T)
    End Sub

    and then call that from C# like so:

    void M1()
    {
        {
            SomeVBClass.SkipInit(out int x);
            x = 1;
        }
        {
            SomeVBClass.SkipInit(out int y);
            Console.WriteLine(y); // may be 0 or 1, depending on if roslyn re-uses the local (it probably re-uses in Release but not Debug)
        }
    }

    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, even x will not be initialized potentially (it literally is equivalent to Unsafe.SkipInit API, just minus the allows 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.

  26. jkotas commented on Oct 17, 2026

    @jkotas
    Member

    The biggest flaw is that IL has no lifetime analysis for managed references, so all managed references in IL need to be considered unsafe.

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions