Skip to content

Memory safety C# 15.0correctnessinterop

Pointer relaxations, requires-unsafe members and unsafe expressions tie unsafe to the operations that access memory.

Historically the unsafe keyword was needed for anything that touched pointer types, even harmless operations such as declaring a pointer or taking an address. That made unsafe blocks large and hid the operations that actually read and write unmanaged memory, which is where memory safety vulnerabilities come from. Marking a method unsafe also told its callers nothing.

C# 15 ties unsafe to the operations that access memory rather than to the existence of pointer types. With LangVersion set to preview the following no longer require an unsafe context:

  • Declaring a pointer type and taking the address of a variable with &
  • The fixed statement
  • Converting a stackalloc expression to a pointer
  • The sizeof operator on any unmanaged type

Dereferencing (*p), member access (p->member), element access (p[i]), element access on fixed-size buffers and invoking function pointers still require unsafe, so reviewers can find them easily.

Turning on the updated memory safety rules also changes what unsafe on a member means: the member becomes requires-unsafe and every caller must call it from an unsafe context. C# 15 adds an unsafe(expression) expression that establishes an unsafe context for just one expression, useful where an unsafe block can't be used such as field initializers, constructor initializers and catch filters.

Code ​

csharp
// Needs LangVersion preview, AllowUnsafeBlocks and the updated-memory-safety-rules feature
int number = 42;
int* pointer = &number;        // No unsafe context needed

int[] numbers = [10, 20, 30];
fixed (int* first = numbers)   // No unsafe context needed
{
    unsafe
    {
        Console.WriteLine(*first); // Dereferencing still needs an unsafe context
    }
}

class Header
{
    // ReadSignature is requires-unsafe so calling it needs an unsafe context,
    // and a field initializer can hold an unsafe expression but not an unsafe block
    static readonly int Signature = unsafe(ReadSignature());

    // unsafe on the signature passes the audit obligation to callers
    static unsafe int ReadSignature()
    {
        int rawValue = 0x1234;
        int* pointer = &rawValue;
        unsafe
        {
            // The modifier no longer makes the body unsafe, so the dereference needs its own block
            return *pointer;
        }
    }
}
csharp
unsafe
{
    int number = 42;
    int* pointer = &number;

    int[] numbers = [10, 20, 30];
    fixed (int* first = numbers)
    {
        Console.WriteLine(*first);
    }
}

class Header
{
    // Nothing tells this caller that ReadSignature dereferences a pointer
    static readonly int Signature = ReadSignature();

    // unsafe here only allowed pointers in the body, callers were unaffected
    static unsafe int ReadSignature()
    {
        int rawValue = 0x1234;
        int* pointer = &rawValue;
        return *pointer;
    }
}

Notes ​

  • Set LangVersion to preview to enable the new syntax and pointer relaxations
  • Enable the updated-memory-safety-rules compiler feature to opt in to the updated rules, including requires-unsafe members
  • unsafe expressions and the safe keyword need the AllowUnsafeBlocks compiler option
  • Under the updated rules unsafe on a member no longer makes its body an unsafe context, so dereferences inside it need their own unsafe block
  • Under the updated rules unsafe on a type declaration is an error
  • Under the updated rules every extern member, and every field of a type with explicit or extended layout, must be marked with the new safe contextual keyword or with unsafe
  • The compiler records that an assembly uses the updated rules with System.Runtime.CompilerServices.MemorySafetyRulesAttribute

More information ​

Powered by Intracia