{"slug": "improving-c-memory-safety", "title": "Improving C# Memory Safety", "summary": "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.", "body_md": "We’re in the process of significantly improving memory safety in C#. The unsafe\nkeyword 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\nkeyword is used to encapsulate unsafe operations. The result is that safety contracts and assumptions become visible and reviewable instead of implied by convention.\nWe 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 and is taking shape.\nC# 1.0 introduced the unsafe\nkeyword 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\ncan 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\nand System.Runtime.InteropServices.Marshal\nthat required careful usage as a convention.\nThe unsafe\nkeyword 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\nuniformly (including on Unsafe\nand Marshal\nmembers) in the .NET runtime libraries, and most closely resembles the Rust implementation. The result: unsafe\nstops 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.\nC# 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 for several years, and AI-assisted code generation adds a new dimension as software production scales faster than human review.\nSafety\nAn earlier post discusses the structural safety mechanisms in .NET:\nsafety 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\nIndexOutOfRangeException\n.\nSource: What is .NET, and why should you choose it?\nC# 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.\nProgramming 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.\nProgramming 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. Accessing arbitrary memory isn’t possible in safe code, but is an ever-present possibility in unsafe code.\nThe model in a nutshell\n.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.\nUnsafe code can read or write arbitrary memory accessed via interop, by NativeMemory\n, 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.\nThe 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:\n- Inner\nunsafe { }\nblock: every unsafe operation (calling anunsafe\nmember, dereferencing a pointer, and otherunsafe\nactions) must appear inside an innerunsafe { }\nblock. This is the base mechanic. Unsafe operations are syntactically marked, scoped, and reviewable. - Propagation: adding\nunsafe\nto 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\nmethods, and the boundary methods between them. Developers can chain propagation through any number of intermediates before someone decides to stop. - Safety documentation: every\nunsafe\nmember should carry a/// <safety>\nblock: the formal contract between callee and caller. Authoring it is a strongly encouraged best practice, and analyzers can flag its absence. - Suppression at the boundary: a method that contains an inner\nunsafe\nblock but does not mark its own signatureunsafe\nis the boundary between unsafe and safe code. It discharges the callee’s documented obligations, through runtime guards on inputs, static reasoning, or documented invariants from upstream APIs (e.g.,malloc\nguaranteeing the returned pointer is valid for at leastsize\nbytes). Correct discharge is what makes safe callers actually safe.\nYou have to step through each layer to get the value. Do half the work and you get much less than half the value. Step through each layer correctly and you have a connected line of reasoning through a call graph that others can review and potentially improve.\nWriting unsafe code is a special skill that requires a strong understanding of this invariant and of many pitfalls. The new model makes unsafe code easier to reason about and review, not easier to write — it forces a formal, visible structure. The keywords and compiler enforcement aren’t the safety; they’re the scaffolding that gets developers to articulate and honor it.\nC# 1.0 grouped a category of “pointer features” under unsafe\n: declaring and dereferencing pointer types, taking the address of variables, stackalloc\nto a pointer, sizeof\non arbitrary types, and other capabilities added over the years, including the suppression of certain compiler errors. The new model is more selective.\nChanges relative to C# 1.0 rules include:\n- The\nunsafe\ntype modifier produces an error. Unsafe scope moves down to individual methods, properties, and fields, where its contract is in view and more minimally specified. Delegates also cannot be unsafe because they are type-shaped. unsafe\nis not allowed on static constructors or finalizers. Their invocations don’t have a call site pattern that can be wrapped in anunsafe { }\nblock, so the signature marker has nothing to propagate.- The\nnew()\ngeneric constraint matches only a safe parameterless constructor; a type whose parameterless constructor isunsafe\ncan’t satisfynew()\n. - A new\nsafe\nkeyword lets a developer attest that a declaration is sound where the compiler requires the choice to be explicit. Today the only such place isextern\ndeclarations, which must be markedsafe\norunsafe\n, includingLibraryImport\npartial method declarations. unsafe\non a member no longer establishes an unsafe context. Interiorunsafe\nblocks are now required at unsafe call sites.- Pointer types in signatures no longer propagate unsafety. Only pointer dereferences are unsafe, so a\nbyte*\nparameter doesn’t propagate unsafety to its callers on its own. For new code, avoidIntPtr\nfor pointers; prefer typed pointers likebyte*\n, orvoid*\nfor truly opaque pointers. For existingIntPtr\n-based APIs, consider adding pointer-typed overloads and hiding or soft-obsoleting theIntPtr\nversions. For opaque handles, preferSafeHandle\n.nint\nandIntPtr\nare indistinguishable in metadata, so when a parameter is genuinely a native-sized integer, document that explicitly.\nAdoption is via a new opt-in project-level property. See § Project-level opt-in for the details.\nThe model in practice\nUnsafe code significantly raises the stakes and is always unbounded in some dimension. The best unsafe APIs are designed to make the unboundedness as narrow as possible: pushing what they can into the signature, discharging what they can in the body, and leaving the caller with a small, well-defined residue to handle themselves.\nEncoding.GetString(byte*, int)\nis a good example.\npublic unsafe string GetString(byte* bytes, int byteCount)\n{\nArgumentNullException.ThrowIfNull(bytes);\nArgumentOutOfRangeException.ThrowIfNegative(byteCount);\nreturn string.CreateStringFromEncoding(bytes, byteCount, this);\n}\nThe method clearly communicates what the API expects: the byte*\nparameter advertises a raw, unmanaged buffer, and the paired byteCount\nsays exactly how many bytes the API will read. The body discharges what it can: a null pointer or negative length is rejected with an exception. The guards remove a subset of cases where string.CreateStringFromEncoding\nwill silently read arbitrary memory. GetString\nreturns a new string\n, removing any aliasing or lifetime concerns with the buffer.\nThe caller holds a single, narrow obligation: byteCount\nbytes starting at bytes\nmust be readable memory. Passing a length larger than the buffer is undefined behavior: the decoder may run into unreadable memory and crash, or it may read whatever happens to live past the end and return a string built from arbitrary foreign bytes. In the existing model, the byte*\nin the signature is what prevents this API from being called from safe code. Under the new model, a pointer in a signature no longer implies unsafety on its own; GetString\nwill be explicitly annotated unsafe\nso it stays uncallable from safe code.\n“Better unsafe” isn’t defined by more or less dangerous, but by more or less descriptive of unsafety; sharp knives make the finest cuts, and dull ones tear.\nMarshal.ReadByte\nis a more cautionary case.\npublic static unsafe byte ReadByte(IntPtr ptr, int ofs)\n{\ntry\n{\nbyte* addr = (byte*)ptr + ofs;\nreturn *addr;\n}\ncatch (NullReferenceException)\n{\nthrow new AccessViolationException();\n}\n}\nCallers of Marshal.ReadByte\npass an IntPtr\nand offset that together address a byte the program is allowed to read. The cautionary difference from GetString\nis that ReadByte\ndoesn’t perform any input validation and is callable from safe code today. The try\n/catch\nclause doesn’t offer any safety, but is used to change the exception type, for only one scenario of misbehavior. The reason this is considered OK is that Marshal\nand Unsafe\nare conventionally understood to be unsafe to call.\nWe can dissect the method a bit further. To", "url": "https://wpnews.pro/news/improving-c-memory-safety", "canonical_source": "https://devblogs.microsoft.com/dotnet/improving-csharp-memory-safety/", "published_at": "2026-05-21 16:54:48+00:00", "updated_at": "2026-05-23 12:04:53.281605+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["C#", ".NET", "System.Runtime.CompilerServices.Unsafe", "System.Runtime.InteropServices.Marshal"], "alternates": {"html": "https://wpnews.pro/news/improving-c-memory-safety", "markdown": "https://wpnews.pro/news/improving-c-memory-safety.md", "text": "https://wpnews.pro/news/improving-c-memory-safety.txt", "jsonld": "https://wpnews.pro/news/improving-c-memory-safety.jsonld"}}