Improving C# Memory Safety The C# team is redesigning the `unsafe` keyword to enforce stricter memory safety contracts, expanding it from marking pointer usage to any code the compiler cannot validate as safe. This change, planned as a C# 16 feature preview in .NET 11, makes safety obligations visible and reviewable by requiring the keyword to encapsulate all unsafe operations. The new model establishes compiler-enforced guard rails for unsafe code, similar to Rust's approach, while maintaining C#'s default blocking of unsafe code for most developers. We’re in the process of significantly improving memory safety in C https://github.com/dotnet/runtime/issues/125800 . The unsafe keyword is being redesigned to inform callers that they have obligations that must be discharged to maintain safety, documented via a new safety comment style. The keyword will expand from marking pointers to any code that interacts with memory in ways the compiler cannot validate as safe. The compiler will enforce that the unsafe keyword is used to encapsulate unsafe operations. The result is that safety contracts and assumptions become visible and reviewable instead of implied by convention. We plan to release the new model and syntax nominally a C 16 feature as a preview in .NET 11 and as a production release in .NET 12. It will initially be opt-in and may become the default in a later release. We will update templates to enable the new model just like we have done with nullable reference types. The early compiler implementation has landed in main https://github.com/dotnet/roslyn/pull/82547 and is taking shape. C 1.0 introduced the unsafe keyword as the way to establish an unsafe context on types, methods, and interior method blocks, letting developers choose the most convenient scope. An unsafe context grants access to pointer features. A method marked unsafe can use those features in its signature and implementation while unmarked methods cannot. We also exposed a set of unsafe types like System.Runtime.CompilerServices.Unsafe https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.unsafe and that required careful usage as a convention. https://learn.microsoft.com/dotnet/api/system.runtime.interopservices.marshal System.Runtime.InteropServices.Marshal The unsafe keyword has since been reused and remixed in Rust and Swift, where those language teams gave it stricter, propagation-oriented semantics. C 16 follows the same path, applies unsafe uniformly including on Unsafe and Marshal members in the .NET runtime libraries, and most closely resembles the Rust implementation. The result: unsafe stops marking a kind of syntax and starts marking a kind of contract; one the compiler can’t verify, that a skilled developer has to read and uphold. C already blocks unsafe code by default. Most developers won’t notice any change when they enable the new model because they don’t enable or use unsafe APIs. The default block will cover a much larger surface area when the C 16 safety model is enabled. The new model establishes strong guard rails that are visible, reviewable, and enforced by the compiler. It is also an important tool to enforce engineering and supply chain standards. Memory safety has been a rising priority across industry and government https://www.cisa.gov/resources-tools/resources/memory-safe-languages-reducing-vulnerabilities-modern-software-development for several years, and AI-assisted code generation adds a new dimension as software production scales faster than human review. Safety An earlier post discusses the structural safety mechanisms in .NET: safety is enforced by a combination of the language and the runtime … Variables either reference live objects, are null, or are out of scope. Memory is auto-initialized by default such that new objects do not use uninitialized memory. Bounds checking ensures that accessing an element with an invalid index will not allow reading undefined memory — often caused by off-by-one errors — but instead will result in a IndexOutOfRangeException . Source: What is .NET, and why should you choose it? https://devblogs.microsoft.com/dotnet/why-dotnet/ safety C comes with strong safety enforcement for regular safe code. The new model enables developers and agents to accurately mark safety boundaries in unsafe code. There are two reasons to write unsafe code: interoperating with native code, and in some cases for performance. Go, Rust, and Swift also include an unsafe dialect for these cases. The language typically cannot help you write unsafe code; its role is to make clear where unsafe code is used and how it transitions back to safe code. Programming safety may be easier to understand if we consider another domain. Road designers improve safety by painting solid yellow or white lines that prohibit crossing into oncoming traffic. Drivers understand and abide by this convention. High-speed highways use barriers to provide safety via structural separation that continues to function in the absence of sober compliance. The highway example shows us that higher speeds come with higher stakes. Programming has its own kind of accidents, with memory. Every application has potential access to gigabytes of virtual memory. Writing to or reading from arbitrary memory results in arbitrary behavior Undefined Behavior , or UB , is the industry term and is the cause of most security bugs https://security.googleblog.com/2024/10/safer-with-google-advancing-memory.html . Accessing arbitrary memory isn’t possible in safe code, but is an ever-present possibility in unsafe code. The model in a nutshell .NET programs are expected to uphold one core invariant: every memory access targets live memory : memory that is allocated, initialized, and available at the time of access. Safe code upholds this by construction: compiler rules and runtime checks combine to make a stray access impossible. Unsafe code is any operation that can violate the invariant, typically by reading or writing memory that isn’t live, or by leaving memory in a state where a later access will fail. Unsafe code can read or write arbitrary memory accessed via interop, by NativeMemory , or hand-managed by the developer. The invariant must hold all the same. The compiler can’t detect UB there, so the burden of validation shifts to the developer. The solution to this risk is a layered set of mechanics that intentionally and transparently push unsafety through the call graph, each layer enabling the next: Inner : every unsafe operation calling an unsafe { } block unsafe member, dereferencing a pointer, and other unsafe actions must appear inside an inner unsafe { } block. This is the base mechanic. Unsafe operations are syntactically marked, scoped, and reviewable. Propagation : adding unsafe to the enclosing method’s signature republishes the inner block’s obligations to its own callers, unless discharged. This carves the call graph into safe methods, unsafe methods, and the boundary methods between them. Developers can chain propagation through any number of intermediates before someone decides to stop. Safety documentation : every unsafe member should carry a ///