Writing · 21.05.26 · 3 min

Improving C# memory safety (C# 16 / .NET 11)

Full editorial translation of Richard Lander's post — four-layer model, guards, fields, migration.

Improving C# memory safety (C# 16 / .NET 11)

Source: Improving C# Memory Safety — .NET Blog · Richard Lander · May 21, 2026

Microsoft is redesigning C# memory safety. The unsafe keyword no longer merely opens pointer syntax—it marks a caller-facing contract. The compiler makes every operation that can violate the live-memory invariant visible; as AI-generated code volume grows, that visibility becomes critical for review.

C# memory safety The new model (nominally C# 16) targets .NET 11 preview and .NET 12 production, initially opt-in with template migration like nullable reference types. Early compiler support is in main.

What .NET already guarantees in safe code

References point to live objects, null, or out-of-scope values. Zero-initialization and bounds checks turn invalid access into IndexOutOfRangeException instead of undefined reads. Unsafe code—for interop or performance—leaves the invariant to the developer. The language’s role is marking boundaries, not helping you write unsafe code.

The C# 16 model: four layers

  1. Inner unsafe { } blocks around every risky operation.
  2. Propagation via unsafe on member signatures—obligations flow to callers.
  3. /// <safety> documentation formalizing callee expectations; analyzers flag absence.
  4. Suppression at boundaries where the signature stays safe but the body discharges obligations via guards or invariants.

Half measures yield little value; all four layers together create an auditable safety chain.

Changes from C# 1.0

No unsafe on types; no unsafe on static constructors/finalizers; new() requires safe ctor; new safe keyword for extern/LibraryImport; signature unsafe no longer establishes context; pointers in signatures don’t propagate alone—prefer byte* over IntPtr, SafeHandle for opaque handles.

Examples: sharp vs dull knives

Encoding.GetString(byte*, int) is descriptive with guards and a new string return. Marshal.ReadByte(IntPtr, int) is cautionary—callable from safe code via pointer smuggling, no real validation. Under the new model: explicit unsafe, scoped dereference, /// <safety>, inner // SAFETY: notes. Violations are compile errors.

Propagation and suppression

Legacy unsafe void M() hid obligations from callers. C# 16 requires Caller1-style propagation or Caller2-style suppression with guards. Cross-assembly behavior depends on opt-in and compat mode for legacy callees. Runtime libraries are migrating under the reduce-unsafe label. Compile-time only—no runtime performance impact.

C# and Rust propagate only explicit unsafe signatures; Swift propagates @unsafe types implicitly. grep unsafe works as an audit tool in C# 16 and Rust.

Project-level opt-in

Two switches: the new safety model property and AllowUnsafeBlocks (default false). Safest combo: new model on, unsafe off—Marshal.ReadByte blocked at compile time. A dotnet format fixer will wrap call sites mechanically but cannot write <safety> blocks. With AllowUnsafeBlocks=false, the compiler refuses unsafe codegen—more efficient than diff review.

Safety documentation

/// <safety> is the caller contract; // SAFETY: is internal reasoning. An analyzer flags missing <safety> blocks. For ReadByte, only the caller knows buffer origin, length, and lifetime.

Safety guards

Documentation names obligations; guards discharge them. String.CopyTo shows composed ThrowIf* guards before Buffer.Memmove. Boundary methods suppress propagation while retaining dangerous capability—review starts here.

Unsafe fields

NativeBuffer pairs unsafe byte* _ptr with fixed Length. ArrayWrapper<T> holds readonly unsafe Array _array always containing T[]. Rust’s unsafe-fields proposal for Vec<T> follows the same invariant-in-the-gap pattern. Writes motivate unsafe on fields; readonly unsafe adds a built-in guard; private isn’t a free pass.

Migration walkthrough (summary)

NativeMemory.Alloc becomes safe; Free stays unsafe with documented preconditions. Migrating callers like FileVersionInfo decides inline discharge vs propagation at each boundary.

Why it matters with AI-assisted codegen

Memory safety is an industry priority; AI scales output faster than human review. unsafe becomes compiler-enforced, grep-auditable contract—not expert convention. PR checklist: <safety> docs, inner blocks, project properties, propagation vs suppression choices.

Full code samples, cross-language tables, and migration PR lists remain in the source post linked above.

Improving C# memory safety (C# 16 / .NET 11) — Aziz Osmanoğlu