{"id":4127,"date":"2026-05-21T16:12:26","date_gmt":"2026-05-21T16:12:26","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/05\/21\/improving-c-memory-safety\/"},"modified":"2026-05-21T16:12:26","modified_gmt":"2026-05-21T16:12:26","slug":"improving-c-memory-safety","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/05\/21\/improving-c-memory-safety\/","title":{"rendered":"Improving C# Memory Safety"},"content":{"rendered":"<p>We\u2019re in the process of significantly <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/125800\">improving memory safety in C#<\/a>. The <code>unsafe<\/code> 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 <code>unsafe<\/code> keyword is used to encapsulate unsafe operations. The result is that safety contracts and assumptions become visible and reviewable instead of implied by convention.<\/p>\n<p>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 <a href=\"https:\/\/github.com\/dotnet\/roslyn\/pull\/82547\">landed in main<\/a> and is taking shape.<\/p>\n<p>C# 1.0 introduced the <code>unsafe<\/code> 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 <code>unsafe<\/code> can use those features in its signature and implementation while unmarked methods cannot. We also exposed a set of unsafe types like <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.runtime.compilerservices.unsafe\"><code>System.Runtime.CompilerServices.Unsafe<\/code><\/a> and <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.runtime.interopservices.marshal\"><code>System.Runtime.InteropServices.Marshal<\/code><\/a> that required careful usage as a convention.<\/p>\n<p>The <code>unsafe<\/code> 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 <code>unsafe<\/code> uniformly (including on <code>Unsafe<\/code> and <code>Marshal<\/code> members) in the .NET runtime libraries, and most closely resembles the Rust implementation. The result: <code>unsafe<\/code> stops marking a kind of syntax and starts marking a kind of contract; one the compiler can\u2019t verify, that a skilled developer has to read and uphold.<\/p>\n<p>C# already blocks unsafe code by default. Most developers won\u2019t notice any change when they enable the new model because they don\u2019t 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 <a href=\"https:\/\/www.cisa.gov\/resources-tools\/resources\/memory-safe-languages-reducing-vulnerabilities-modern-software-development\">industry and government<\/a> for several years, and AI-assisted code generation adds a new dimension as software production scales faster than human review.<\/p>\n<h2>Safety<\/h2>\n<p>An earlier post discusses the structural safety mechanisms in .NET:<\/p>\n<blockquote>\n<p>safety is enforced by a combination of the language and the runtime \u2026 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 \u2014 often caused by off-by-one errors \u2014 but instead will result in a <code>IndexOutOfRangeException<\/code>.<\/p>\n<\/blockquote>\n<p>Source: <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/why-dotnet\/#safety\">What is .NET, and why should you choose it?<\/a><\/p>\n<p>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.<\/p>\n<p>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.<\/p>\n<p>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 (<strong>Undefined Behavior<\/strong>, or <strong>UB<\/strong>, is the industry term) and is the <a href=\"https:\/\/security.googleblog.com\/2024\/10\/safer-with-google-advancing-memory.html\">cause of most security bugs<\/a>. Accessing arbitrary memory isn\u2019t possible in safe code, but is an ever-present possibility in unsafe code.<\/p>\n<h2>The model in a nutshell<\/h2>\n<p>.NET programs are expected to uphold one core invariant: every memory access targets <strong>live memory<\/strong>: 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. <strong>Unsafe code<\/strong> is any operation that can violate the invariant, typically by reading or writing memory that isn\u2019t live, or by leaving memory in a state where a later access will fail.<\/p>\n<p>Unsafe code can read or write arbitrary memory accessed via interop, by <code>NativeMemory<\/code>, or hand-managed by the developer. The invariant must hold all the same. The compiler can\u2019t detect UB there, so the burden of validation shifts to the developer.<\/p>\n<p>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:<\/p>\n<ol>\n<li><strong>Inner <code>unsafe { }<\/code> block<\/strong>: every unsafe operation (calling an <code>unsafe<\/code> member, dereferencing a pointer, and other <code>unsafe<\/code> actions) must appear inside an inner <code>unsafe { }<\/code> block. This is the base mechanic. Unsafe operations are syntactically marked, scoped, and reviewable.<\/li>\n<li><strong>Propagation<\/strong>: adding <code>unsafe<\/code> to the enclosing method\u2019s signature republishes the inner block\u2019s obligations to its own callers, unless discharged. This carves the call graph into safe methods, <code>unsafe<\/code> methods, and the boundary methods between them. Developers can chain propagation through any number of intermediates before someone decides to stop.<\/li>\n<li><strong>Safety documentation<\/strong>: every <code>unsafe<\/code> member should carry a <code>\/\/\/ &lt;safety&gt;<\/code> block: the formal contract between callee and caller. Authoring it is a strongly encouraged best practice, and analyzers can flag its absence.<\/li>\n<li><strong>Suppression at the boundary<\/strong>: a method that contains an inner <code>unsafe<\/code> block but does <em>not<\/em> mark its own signature <code>unsafe<\/code> is the boundary between unsafe and safe code. It discharges the callee\u2019s documented obligations, through runtime guards on inputs, static reasoning, or documented invariants from upstream APIs (e.g., <code>malloc<\/code> guaranteeing the returned pointer is valid for at least <code>size<\/code> bytes). Correct discharge is what makes safe callers actually safe.<\/li>\n<\/ol>\n<p>You 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.<\/p>\n<p>Writing 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 \u2014 it forces a formal, visible structure. The keywords and compiler enforcement aren\u2019t the safety; they\u2019re the scaffolding that gets developers to articulate and honor it.<\/p>\n<p>C# 1.0 grouped a category of \u201cpointer features\u201d under <code>unsafe<\/code>: declaring and dereferencing pointer types, taking the address of variables, <code>stackalloc<\/code> to a pointer, <code>sizeof<\/code> on arbitrary types, and other capabilities added over the years, including the suppression of certain compiler errors. The new model is more selective.<\/p>\n<p>Changes relative to C# 1.0 rules include:<\/p>\n<ul>\n<li>The <code>unsafe<\/code> type 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.<\/li>\n<li><code>unsafe<\/code> is not allowed on static constructors or finalizers. Their invocations don\u2019t have a call site pattern that can be wrapped in an <code>unsafe { }<\/code> block, so the signature marker has nothing to propagate.<\/li>\n<li>The <code>new()<\/code> generic constraint matches only a safe parameterless constructor; a type whose parameterless constructor is <code>unsafe<\/code> can\u2019t satisfy <code>new()<\/code>.<\/li>\n<li>A new <code>safe<\/code> keyword lets a developer attest that a declaration is sound where the compiler requires the choice to be explicit. Today the only such place is <code>extern<\/code> declarations, which must be marked <code>safe<\/code> or <code>unsafe<\/code>, including <code>LibraryImport<\/code> partial method declarations.<\/li>\n<li><code>unsafe<\/code> on a member no longer establishes an unsafe context. Interior <code>unsafe<\/code> blocks are now required at unsafe call sites.<\/li>\n<li>Pointer types in signatures no longer propagate unsafety. Only pointer <em>dereferences<\/em> are unsafe, so a <code>byte*<\/code> parameter doesn\u2019t propagate unsafety to its callers on its own. For new code, avoid <code>IntPtr<\/code> for pointers; prefer typed pointers like <code>byte*<\/code>, or <code>void*<\/code> for truly opaque pointers. For existing <code>IntPtr<\/code>-based APIs, consider adding pointer-typed overloads and hiding or soft-obsoleting the <code>IntPtr<\/code> versions. For opaque handles, prefer <code>SafeHandle<\/code>. <code>nint<\/code> and <code>IntPtr<\/code> are indistinguishable in metadata, so when a parameter is genuinely a native-sized integer, document that explicitly.<\/li>\n<\/ul>\n<p>Adoption is via a new opt-in project-level property. See <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/improving-csharp-memory-safety\/#project-level-opt-in\">\u00a7 Project-level opt-in<\/a> for the details.<\/p>\n<h2>The model in practice<\/h2>\n<p>Unsafe 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.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/blob\/e42458bf1f276f49d6e3364458021a5f2d749531\/src\/libraries\/System.Private.CoreLib\/src\/System\/Text\/Encoding.cs#L904\"><code>Encoding.GetString(byte*, int)<\/code><\/a> is a good example.<\/p>\n<pre><code class=\"language-csharp\">public unsafe string GetString(byte* bytes, int byteCount)\r\n{\r\n    ArgumentNullException.ThrowIfNull(bytes);\r\n\r\n    ArgumentOutOfRangeException.ThrowIfNegative(byteCount);\r\n\r\n    return string.CreateStringFromEncoding(bytes, byteCount, this);\r\n}<\/code><\/pre>\n<p>The method clearly communicates what the API expects: the <code>byte*<\/code> parameter advertises a raw, unmanaged buffer, and the paired <code>byteCount<\/code> says 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 <code>string.CreateStringFromEncoding<\/code> will silently read arbitrary memory. <code>GetString<\/code> returns a new <code>string<\/code>, removing any aliasing or lifetime concerns with the buffer.<\/p>\n<p>The caller holds a single, narrow obligation: <code>byteCount<\/code> bytes starting at <code>bytes<\/code> must 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 <code>byte*<\/code> in 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; <code>GetString<\/code> will be explicitly annotated <code>unsafe<\/code> so it stays uncallable from safe code.<\/p>\n<p>\u201cBetter unsafe\u201d isn\u2019t defined by more or less dangerous, but by more or less descriptive of unsafety; sharp knives make the finest cuts, and dull ones tear.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/blob\/e42458bf1f276f49d6e3364458021a5f2d749531\/src\/libraries\/System.Private.CoreLib\/src\/System\/Runtime\/InteropServices\/Marshal.cs#L287\"><code>Marshal.ReadByte<\/code><\/a> is a more cautionary case.<\/p>\n<pre><code class=\"language-csharp\">public static unsafe byte ReadByte(IntPtr ptr, int ofs)\r\n{\r\n    try\r\n    {\r\n        byte* addr = (byte*)ptr + ofs;\r\n        return *addr;\r\n    }\r\n    catch (NullReferenceException)\r\n    {\r\n        throw new AccessViolationException();\r\n    }\r\n}<\/code><\/pre>\n<p>Callers of <code>Marshal.ReadByte<\/code> pass an <code>IntPtr<\/code> and offset that together address a byte the program is allowed to read. The cautionary difference from <code>GetString<\/code> is that <code>ReadByte<\/code> doesn\u2019t perform any input validation and is callable from safe code today. The <code>try<\/code>\/<code>catch<\/code> clause doesn\u2019t 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 <code>Marshal<\/code> and <code>Unsafe<\/code> are conventionally understood to be unsafe to call.<\/p>\n<p>We can dissect the method a bit further. Today\u2019s <code>unsafe<\/code> signature on <code>ReadByte<\/code> establishes an unsafe context for the implementation but doesn\u2019t create a caller contract or document a caller warning. The existing model propagates unsafety through pointer types in signatures, but <code>IntPtr<\/code> dodges that rule; the API is effectively pointer smuggling.<\/p>\n<p>The new model closes this gap. It widens unsafety to cover any operation that can violate the live-memory invariant (not just operations involving pointer types), and makes the <code>unsafe<\/code> signature marker the member contract, with inner <code>unsafe<\/code> blocks encapsulating the unsafe operations. It also aligns the safety character of <code>IntPtr<\/code> and pointers like <code>byte*<\/code>: both can be held, assigned, and exposed in signatures outside an <code>unsafe<\/code> block; it is pointer dereference that is unsafe.<\/p>\n<p><code>ReadByte<\/code> changes with the new model, per the following mockup:<\/p>\n<pre><code class=\"language-csharp\">\/\/\/ &lt;summary&gt;Reads a single byte from unmanaged memory.&lt;\/summary&gt;\r\n\/\/\/ &lt;safety&gt;\r\n\/\/\/ The sum of &lt;paramref name=\"ptr\"\/&gt; and &lt;paramref name=\"ofs\"\/&gt; must address a byte\r\n\/\/\/ the caller is permitted to read.\r\n\/\/\/ &lt;\/safety&gt;\r\npublic static unsafe byte ReadByte(IntPtr ptr, int ofs)\r\n{\r\n    try\r\n    {\r\n        byte* addr = (byte*)ptr;\r\n        unsafe\r\n        {\r\n            \/\/ SAFETY: relies on caller obligation.\r\n            return addr[ofs];\r\n        }\r\n    }\r\n    catch (NullReferenceException)\r\n    {\r\n        throw new AccessViolationException();\r\n    }\r\n}<\/code><\/pre>\n<p>Let\u2019s dig into the implementation. The cast <code>(byte*)ptr<\/code> is pointer manipulation, not a dereference; <code>IntPtr<\/code> and <code>byte*<\/code> are the same shape, different representation; both are just a number. The unsafety is on a single line: <code>return addr[ofs]<\/code>. That is the point where the developer needs to attest that <code>addr + ofs<\/code> addresses readable memory, since the indexing dereferences that address. <code>byte*<\/code> \u2192 <code>byte<\/code> requires copying memory from the pointer address into a value. That\u2019s the dangerous operation.<\/p>\n<p>The new model works because the pointer dereference, <code>addr[ofs]<\/code>, gets wrapped in an <code>unsafe<\/code> block, shining light on the unsafety. The <code>unsafe<\/code> signature becomes a caller contract, forcing callers to wrap their calls in an <code>unsafe<\/code> block as well, and a reminder to look at the callee <code>safety<\/code> doc.<\/p>\n<p>A strict \u201csmallest unsafe block\u201d reading would put the <code>+ ofs<\/code> arithmetic outside the block, since arithmetic on its own isn\u2019t a dereference. We prefer to keep <code>addr[ofs]<\/code> together: indexing <em>is<\/em> the indirection (<code>addr[0]<\/code> is by spec the same as <code>*addr<\/code>), and grouping makes the exact address being read visible at the point of access. We expect these kinds of choices to be codified in unsafe coding guidelines over time.<\/p>\n<p>Violations are compile errors, not warnings. The model isn\u2019t an \u201chonor system\u201d. Take <code>Marshal.ReadByte<\/code> from above: it is marked <code>unsafe<\/code> because its implementation dereferences an opaque caller-supplied pointer. In the new model, it will continue to be marked <code>unsafe<\/code> because it passes a pointer validity obligation on to callers. The obligation was previously understood by convention. The compiler now requires <code>Marshal.ReadByte<\/code> to expose the obligation as a contract.<\/p>\n<h2>Propagation and suppression<\/h2>\n<p>The <a href=\"https:\/\/doc.rust-lang.org\/book\/ch20-01-unsafe-rust.html\">safety marking system established by Rust<\/a> is a good guide for propagation and suppression. C# 16 is adopting the same approach and syntax. The <code>unsafe<\/code> keyword is used in two ways. The first is an inner <code>unsafe<\/code> block that wraps an unsafe operation, typically due to calling another unsafe method and\/or dereferencing a pointer. The second is an outer <code>unsafe<\/code> signature marker that defines a caller contract.<\/p>\n<p>To propagate unsafety to the caller, the developer adds <code>unsafe<\/code> to the member signature; to suppress unsafety as an implementation detail, they leave <code>unsafe<\/code> absent. Presence or absence of <code>unsafe<\/code> on a member signature (for methods with inner <code>unsafe<\/code>) is the compiler signal for propagation or suppression. Propagation pushes unsafety one caller higher while suppression caps unsafety by offering a safe-caller-compatible surface area.<\/p>\n<h3>C# 1.0 model<\/h3>\n<p>C# 1.0 uses <code>unsafe<\/code> on a type or member to mean \u201cunsafe context from this point\u201d. It doesn\u2019t inform or change the caller contract. Pointers are the sole propagation mechanism in C# 1.0. Inner <code>unsafe<\/code> can be used to tighten the scope of unsafety.<\/p>\n<p>Let\u2019s start with code that is legal today, in the C# 1.0 model.<\/p>\n<pre><code class=\"language-csharp\">void Caller()\r\n{\r\n    M();\r\n}\r\n\r\nunsafe void M() { }<\/code><\/pre>\n<p><code>Caller<\/code> can call <code>unsafe M<\/code> without any ceremony.<\/p>\n<p>The reason is twofold:<\/p>\n<ul>\n<li><code>unsafe<\/code> is being used to create an inner <code>unsafe<\/code> block for the entire method, not to define a caller contract.<\/li>\n<li><code>M<\/code> doesn\u2019t expose pointers, so doesn\u2019t propagate unsafety.<\/li>\n<\/ul>\n<p>This example is analogous to <code>ReadByte<\/code>. <code>Caller<\/code> could call <code>ReadByte<\/code> just as freely as it is calling <code>M<\/code>. It could not call <code>Encoding.GetString<\/code> in the same way due to pointer usage.<\/p>\n<p>We need to critique the existing model to understand why we are moving away from it. The roles and responsibilities of <code>M<\/code> and <code>Caller<\/code> are specified only by convention. There is no standard for the safety concerns or obligations that <code>M<\/code> should communicate to <code>Caller<\/code> or how <code>Caller<\/code> meets the expectations of its safe callers. In short, there is no overarching system that pushes developers towards actual safety or that enables straightforward auditing. Safety is currently deployed by skilled engineers who understand how to define obligations and risks, without help from the compiler.<\/p>\n<h3>C# 16 model<\/h3>\n<p><a href=\"https:\/\/github.com\/dotnet\/designs\/blob\/main\/accepted\/2025\/memory-safety\/caller-unsafe.md\">The new model<\/a> adopts <code>unsafe<\/code> on a method signature as a caller-facing propagation mechanism. The absence of <code>unsafe<\/code> is used to communicate suppression.<\/p>\n<p><code>Caller<\/code> from the previous example would have to be adjusted to either <code>Caller1<\/code> or <code>Caller2<\/code> below.<\/p>\n<pre><code class=\"language-csharp\">\/\/\/ &lt;safety&gt;\r\n\/\/\/ Caller must satisfy obligation 1\r\n\/\/\/ &lt;\/safety&gt;\r\nunsafe void Caller1()\r\n{\r\n    unsafe\r\n    {\r\n        \/\/ SAFETY: Obligation is passed to caller.\r\n        M();\r\n    }\r\n}\r\n\r\nvoid Caller2()\r\n{\r\n    if (\/* obligation 1 not satisfied *\/) throw new Exception();\r\n\r\n    unsafe\r\n    {\r\n        \/\/ SAFETY: obligation 1 is discharged by the check above\r\n        M();\r\n    }\r\n}\r\n\r\n\/\/\/ &lt;safety&gt;\r\n\/\/\/ Caller must satisfy obligation 1\r\n\/\/\/ &lt;\/safety&gt;\r\nunsafe void M() { }<\/code><\/pre>\n<p>Both <code>M<\/code> and <code>Caller1<\/code> propagate unsafety to their callers. <code>Caller2<\/code> suppresses the unsafety of its callees and is an unsafe boundary method. Either form is a valid replacement for <code>Caller<\/code>. The developer decides which is appropriate based on whether it is possible or desirable to validate obligation 1. If caller obligations remain, then <code>Caller1<\/code> is the right choice. Choosing between propagation and suppression isn\u2019t compiler-enforced (or compiler-suggested), but requires careful judgment.<\/p>\n<p><code>Caller1<\/code> carries two <code>unsafe<\/code> markers by design: the outer one projects the caller contract, the inner one scopes the unsafe operations. Inside an <code>unsafe<\/code> member, omitting the inner <code>unsafe<\/code> block at an unsafe operation is a compile error; the signature marker no longer establishes an unsafe context on its own. This outer-propagates \/ inner-scopes shape matches Rust\u2019s <code>unsafe fn<\/code> \/ <code>unsafe { }<\/code> and Swift\u2019s <code>@unsafe<\/code> \/ <code>unsafe expr<\/code>.<\/p>\n<p><code>Caller2<\/code> is safe-callable, placing no obligation on its callers and requiring no <code>unsafe<\/code> blocks at their call sites.<\/p>\n<p>The model applies to any caller. The example above demonstrates callers on the same type. The model applies uniformly across types, projects, and packages. It also applies to source generators. There is no planned scoped opt-out mechanism.<\/p>\n<p>The enforcement is compile-time only. The model introduces no new runtime checks and has no performance impact; existing runtime checks that result in exceptions like <code>IndexOutOfRangeException<\/code> and <code>ArgumentNullException<\/code> are unchanged.<\/p>\n<p>The .NET runtime libraries will opt in. That\u2019s necessary as the basis of the model for callers. Consuming a library that has opted in does not require your project to opt in, and vice versa. Cross-assembly behavior depends on which side has opted in:<\/p>\n<ul>\n<li><strong>Opted-in caller, opted-in callee.<\/strong> The new model. The callee\u2019s <code>unsafe<\/code> markers travel via metadata, and the caller must wrap calls in an <code>unsafe { }<\/code> block; without one, the call is a compile error.<\/li>\n<li><strong>Opted-in caller, non-opted-in (legacy) callee.<\/strong> Compat mode. The compiler treats any callee member with a pointer type in its signature as <code>unsafe<\/code>, requiring an enclosing <code>unsafe { }<\/code> block at the call site. Non-pointer unsafe surface (<code>IntPtr<\/code>\/<code>nint<\/code> parameters, P\/Invoke signatures, and so on) isn\u2019t flagged, because the legacy assembly carries no metadata to distinguish it. Compat mode prevents a \u201csafety dip\u201d where a legacy package\u2019s unsafe APIs would silently lose their pointer-driven <code>unsafe<\/code> propagation when the new model is enabled.<\/li>\n<li><strong>Non-opted-in caller, opted-in callee.<\/strong> No enforcement of the new model\u2019s <code>unsafe<\/code> markers; the legacy caller can\u2019t interpret them. Legacy C# 1.0 pointer rules still apply: a callee that exposes a pointer type in its signature still requires the legacy caller to be in an <code>unsafe<\/code> context. The gap is new-model <code>unsafe<\/code> methods that have no pointer types in their signature (e.g., <code>unsafe byte ReadByte(IntPtr, int)<\/code>). Those become callable from legacy safe code.<\/li>\n<\/ul>\n<p>Migration of the runtime libraries is already underway: the <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues?q=label%3Areduce-unsafe\"><code>reduce-unsafe<\/code> label<\/a> tracks the running list of PRs removing unsafe code from the libraries, including swaps like <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127394\">#127394<\/a> (replacing <code>MemoryMarshal.Read<\/code>\/<code>Write<\/code> with <code>BitConverter<\/code> equivalents) and <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127485\">#127485<\/a> (removing unsafe code from <code>IBinaryInteger.TryReadBigEndian<\/code>). This migration is also a sign that industrial code can be moved to safe patterns. Your unsafe code probably can, too.<\/p>\n<p>To summarize the changes from C# 1.0:<\/p>\n<ul>\n<li><code>unsafe<\/code> on a member signature now defines a caller-facing contract that propagates unsafety up the call graph. C# 1.0 used it only to establish an unsafe context.<\/li>\n<li>An <code>unsafe<\/code> block is required at every call to an <code>unsafe<\/code> member.<\/li>\n<\/ul>\n<h3>Cross-language comparison: propagation<\/h3>\n<p>The differences between C#, Rust, and Swift are both subtle and instructive. C# 16 propagates unsafety <strong>only<\/strong> when the <code>unsafe<\/code> keyword appears on the member; pointer types and other unsafe-typed parameters do not propagate on their own. Rust behaves the same way: a <code>*const u8<\/code> parameter on a plain <code>fn<\/code> propagates nothing. Swift is the outlier: any <code>@unsafe<\/code> type appearing in a signature implicitly makes the declaration <code>@unsafe<\/code>, in addition to the explicit <code>@unsafe<\/code> attribute.<\/p>\n<p>The implicit Swift model leads to needing <code>@safe<\/code> as a broadly-applicable opt-out for APIs that encapsulate the unsafety (e.g., <code>Array.withUnsafeBufferPointer<\/code>). Both C# and Rust include a narrow positive <code>safe<\/code> form for interop (FFI), but for different reasons. Rust\u2019s <code>safe fn<\/code> inside an <code>unsafe extern<\/code> block is an override of the default. The block is unsafe by default and <code>safe<\/code> opts an individual declaration out, analogous in shape to Swift\u2019s <code>@safe<\/code>. C# 16\u2019s <code>safe extern<\/code> for <code>LibraryImport<\/code> declarations is not an override. It\u2019s a statement about the whole declaration and it\u2019s required because the language biases toward explicit markings and won\u2019t let a developer leave a foreign declaration\u2019s safety implicit.<\/p>\n<p>Every <code>LibraryImport<\/code> partial method must be marked <code>safe<\/code> or <code>unsafe<\/code>:<\/p>\n<pre><code class=\"language-csharp\">[LibraryImport(\"libc\")]\r\ninternal static safe partial int getpid();\r\n\r\n[LibraryImport(\"libc\", StringMarshalling = StringMarshalling.Utf8)]\r\ninternal static unsafe partial nint strlen(byte* str);<\/code><\/pre>\n<p><code>getpid<\/code> has no parameters and returns a primitive; the author attests that the call is sound and safe callers can use it without ceremony. <code>strlen<\/code> takes a raw pointer the native code will dereference; the author has no way to discharge that obligation at the boundary, so the declaration propagates <code>unsafe<\/code> and a <code>&lt;safety&gt;<\/code> block names the caller\u2019s obligation. Omitting both modifiers is a compile error \u2014 the developer has to make the choice.<\/p>\n<p>Let\u2019s look at a propagation example. A short Rust program (edition 2024) triggers both an <code>unsafe_op_in_unsafe_fn<\/code> warning (an unsafe op inside an <code>unsafe fn<\/code> body without an inner <code>unsafe<\/code> block) and a hard E0133 error (a call to an <code>unsafe fn<\/code> from a safe context without an <code>unsafe<\/code> block):<\/p>\n<pre><code class=\"language-bash\">$ cat main.rs\r\n\/\/\/ # Safety\r\n\/\/\/\r\n\/\/\/ `bytes` must be non-null and point to at least one readable byte.\r\npub unsafe fn first_byte(bytes: *const u8) -&gt; u8 {\r\n    \/\/ No inner `unsafe { }`: warns under `unsafe_op_in_unsafe_fn` (edition 2024).\r\n    *bytes\r\n}\r\n\r\nfn main() {\r\n    let data = [42u8];\r\n    \/\/ No `unsafe { }` around the call: hard error E0133.\r\n    let value = first_byte(data.as_ptr());\r\n    println!(\"{value}\");\r\n}\r\n\r\n$ cargo build\r\n   Compiling unsafe_demo v0.1.0 (\/private\/tmp\/unsafe-demo)\r\nwarning[E0133]: dereference of raw pointer is unsafe and requires unsafe block\r\n --&gt; src\/main.rs:6:5\r\n  |\r\n6 |     *bytes\r\n  |     ^^^^^^ dereference of raw pointer\r\n  |\r\n  = note: raw pointers may be null, dangling or unaligned; they can violate aliasing rules and cause data races: all of these are undefined behavior\r\nnote: an unsafe function restricts its caller, but its body is safe by default\r\n --&gt; src\/main.rs:4:1\r\n  |\r\n4 | pub unsafe fn first_byte(bytes: *const u8) -&gt; u8 {\r\n  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  = note: for more information, see &lt;https:\/\/doc.rust-lang.org\/edition-guide\/rust-2024\/unsafe-op-in-unsafe-fn.html&gt;\r\n  = note: `#[warn(unsafe_op_in_unsafe_fn)]` (part of `#[warn(rust_2024_compatibility)]`) on by default\r\n\r\nerror[E0133]: call to unsafe function `first_byte` is unsafe and requires unsafe block\r\n  --&gt; src\/main.rs:12:17\r\n   |\r\n12 |     let value = first_byte(data.as_ptr());\r\n   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^ call to unsafe function\r\n   |\r\n   = note: consult the function's documentation for information on how to avoid undefined behavior\r\n\r\nFor more information about this error, try `rustc --explain E0133`.\r\nwarning: `unsafe_demo` (bin \"unsafe_demo\") generated 1 warning\r\nerror: could not compile `unsafe_demo` (bin \"unsafe_demo\") due to 1 previous error; 1 warning emitted<\/code><\/pre>\n<p>This experience is very similar to what we have planned. The key difference is that both of these cases will be errors in C# 16.<\/p>\n<p>Boiling this all down, C# and Rust code bias toward simple explicit rules and arguably require less domain knowledge. A case-in-point is that it is reasonable to use <code>grep<\/code> as a safety audit tool with C# 16 and Rust since the explicit keywords act as a fixture that queries can easily grab onto.<\/p>\n<h2>Project-level opt-in<\/h2>\n<p>The C# 16 safety model has two project-level switches. They are independent and serve different purposes.<\/p>\n<p>The first switch is a new opt-in property (final name landing with the .NET 11 preview). With it off, the legacy C# 1.0 rules continue to govern; with it on, the new caller-unsafe rules apply. This switch decides <em>what counts as unsafe<\/em> and how it <em>propagates<\/em>.<\/p>\n<p>The second switch is the existing <code>&lt;AllowUnsafeBlocks&gt;<\/code> property. It defaults to <code>false<\/code> (under all versions of C#) and gates every appearance of the <code>unsafe<\/code> keyword in the project\u2019s source: member signatures, inner blocks, fields, and <code>safe extern<\/code> declarations under the new rules. Calling an unsafe API from another project counts, because the call site needs an inner <code>unsafe { }<\/code> block. So a project at the default cannot use any unsafe API.<\/p>\n<p>The two properties combine as follows:<\/p>\n<ul>\n<li><strong>New property on, <code>&lt;AllowUnsafeBlocks&gt;<\/code> off (default).<\/strong> The safest configuration. The project participates in the new model and allows no unsafe code. You know your code isn\u2019t calling <code>Marshal.ReadByte<\/code> or any other <code>unsafe<\/code> member.<\/li>\n<li><strong>New property on, <code>&lt;AllowUnsafeBlocks&gt;<\/code> on.<\/strong> The project participates in the new model and allows unsafe code.<\/li>\n<li><strong>New property off, <code>&lt;AllowUnsafeBlocks&gt;<\/code> off.<\/strong> The legacy model continues to apply. The project may not use pointer types.<\/li>\n<li><strong>New property off, <code>&lt;AllowUnsafeBlocks&gt;<\/code> on.<\/strong> The legacy model continues to apply. The project may use pointer types.<\/li>\n<\/ul>\n<p>We want everyone to move to the new model. We also expect fewer projects to enable <code>&lt;AllowUnsafeBlocks&gt;<\/code> over time. That\u2019s what we\u2019re doing with our own code.<\/p>\n<p>To help with the move, we plan to ship a <code>dotnet format<\/code> fixer that performs a best-effort migration on projects that haven\u2019t yet flipped the new property on: wrapping unsafe call sites in <code>unsafe { }<\/code> blocks, moving the <code>unsafe<\/code> modifier off types onto their members, and similar mechanical rewrites. The fixer can\u2019t infer safety obligations or write <code>&lt;safety&gt;<\/code> blocks; that work stays with the developer. It\u2019s a starting point that gets the code compiling under the new rules, not a finished migration.<\/p>\n<p>The core question with agents generating code is whose responsibility it is to determine whether unsafe code has been written. With the new model, that\u2019s the compiler\u2019s. Assuming you haven\u2019t set <code>AllowUnsafeBlocks=true<\/code>, the compiler will refuse to compile any unsafe code at all. No code review can match the efficiency of a compile error. Memory-safety auditing collapses from inspecting every diff to checking one project property.<\/p>\n<h3>Cross-language comparison: defaults<\/h3>\n<p>The differences are subtle and important here as well. We can frame the three languages along two safety axes: <em>strict propagation<\/em> (how aggressively unsafety propagates and what counts as unsafe) and <em>disallowing unsafe code<\/em> outright. For each axis, the safer posture is either the default or available as an opt-in.<\/p>\n<table>\n<thead>\n<tr>\n<th>Language<\/th>\n<th>Strict propagation<\/th>\n<th>Safe code only<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>C#<\/td>\n<td>Opt-in (C# 16 model)<\/td>\n<td>Default (<code>AllowUnsafeBlocks=false<\/code>)<\/td>\n<\/tr>\n<tr>\n<td>Rust<\/td>\n<td>Default (the only model)<\/td>\n<td>Opt-in (<code>#![forbid(unsafe_code)]<\/code>)<\/td>\n<\/tr>\n<tr>\n<td>Swift<\/td>\n<td>Opt-in (<code>-strict-memory-safety<\/code>)<\/td>\n<td>Opt-in (no standard switch)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>C# 16 will enable the strict model with the new safety keyword. <code>AllowUnsafeBlocks=false<\/code> remains the default. Under the new model it performs even heavier lifting, because the set of <code>unsafe<\/code> actions it gates is much larger.<\/p>\n<p>Rust has only one safety model, a strict one. The compiler allows <code>unsafe<\/code> in any crate by default and requires the <code>#![forbid(unsafe_code)]<\/code> lint to disable it.<\/p>\n<p>Swift also offers a strict opt-in mode (<code>-strict-memory-safety<\/code>, <a href=\"https:\/\/github.com\/swiftlang\/swift-evolution\/blob\/main\/proposals\/0458-strict-memory-safety.md\">SE-0458<\/a>), which can be set per file or per module to turn implicit unsafety into diagnostics.<\/p>\n<p>These comparisons are not really apples to apples since they are multi-dimensional. Rust has the strongest default position. Our viewpoint aligns with the <a href=\"https:\/\/memorysafety.openssf.org\/memory-safety-continuum\/\">Memory Safety Continuum<\/a>: stricter defaults are better. Our intention is to make the new C# safety model the new normal. We\u2019ll start by enabling it with templates. It is simpler for us to introduce a stricter safety model given that unsafe code is already prohibited by default, and we expect good adoption because of that.<\/p>\n<h2>Safety documentation<\/h2>\n<p>It\u2019s easy to interpret the term \u201cunsafe\u201d literally, but it is misleading. It means \u201cdisable the safeties\u201d. Safe code is known by the compiler to comply with a defined safety model, while unsafe code is not. With unsafe code, the burden of <em>knowing<\/em> falls to the developer. <em>Knowing<\/em> starts with reading dedicated safety documentation. Properly written unsafe code documents the caller\u2019s obligations: the conditions the caller must satisfy for the code to behave correctly.<\/p>\n<p>Unsafe code with missing or poorly written documentation isn\u2019t safe to call since the caller is left guessing. Code auditors pay close attention to that. That\u2019s already the case in the Rust community: <a href=\"https:\/\/github.com\/google\/rust-crate-audits\/blob\/main\/auditing_standards.md#soundness\">Google<\/a> and <a href=\"https:\/\/mozilla.github.io\/cargo-vet\/index.html\">Mozilla<\/a>.<\/p>\n<p>An analyzer will flag missing <code>\/\/\/ &lt;safety&gt;<\/code> blocks.<\/p>\n<h3>Rust safety comments<\/h3>\n<p>We\u2019ll rely on Rust for canonical examples since it is well-established. Rust uses <a href=\"https:\/\/std-dev-guide.rust-lang.org\/policy\/safety-comments.html\">Safety Comments<\/a> to demonstrate that unsafe code is sound.<\/p>\n<p>An unsafe Rust function, <a href=\"https:\/\/github.com\/rust-lang\/rust\/blob\/a08f25a7ef2800af5525762e981c24d96c14febe\/library\/core\/src\/str\/mod.rs#L278\"><code>as_bytes_mut<\/code><\/a>:<\/p>\n<pre><code class=\"language-plaintext\">\/\/\/ Converts a mutable string slice to a mutable byte slice.\r\n\/\/\/\r\n\/\/\/ # Safety\r\n\/\/\/\r\n\/\/\/ The caller must ensure that the content of the slice is valid UTF-8\r\n\/\/\/ before the borrow ends and the underlying `str` is used.\r\n\/\/\/\r\n\/\/\/ Use of a `str` whose contents are not valid UTF-8 is undefined behavior.\r\n\/\/\/\r\n\/\/\/ ...\r\npub unsafe fn as_bytes_mut(&amp;mut self) -&gt; &amp;mut [u8] {\r\n    \/\/ SAFETY: the cast from `&amp;str` to `&amp;[u8]` is safe since `str`\r\n    \/\/ has the same layout as `&amp;[u8]` (only libstd can make this guarantee).\r\n    \/\/ The pointer dereference is safe since it comes from a mutable reference which\r\n    \/\/ is guaranteed to be valid for writes.\r\n    unsafe { &amp;mut *(self as *mut str as *mut [u8]) }\r\n}<\/code><\/pre>\n<p>Clippy enforces this convention. An unsafe function without a <code># Safety<\/code> section trips the <a href=\"https:\/\/rust-lang.github.io\/rust-clippy\/master\/index.html#missing_safety_doc\"><code>missing_safety_doc<\/code><\/a> lint:<\/p>\n<pre><code class=\"language-bash\">$ cat main.rs\r\n#![deny(clippy::missing_safety_doc)]\r\n\r\npub unsafe fn first_byte(bytes: *const u8) -&gt; u8 {\r\n    unsafe { *bytes }\r\n}\r\n\r\nfn main() {\r\n    let data = [42u8];\r\n    let value = unsafe { first_byte(data.as_ptr()) };\r\n    println!(\"{value}\");\r\n}\r\n\r\n$ cargo clippy\r\n    Checking unsafe_demo v0.1.0 (\/private\/tmp\/unsafe-demo)\r\nerror: unsafe function's docs are missing a `# Safety` section\r\n --&gt; src\/main.rs:3:1\r\n  |\r\n3 | pub unsafe fn first_byte(bytes: *const u8) -&gt; u8 {\r\n  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  |\r\n  = help: for further information visit https:\/\/rust-lang.github.io\/rust-clippy\/rust-1.95.0\/index.html#missing_safety_doc\r\nnote: the lint level is defined here\r\n --&gt; src\/main.rs:1:9\r\n  |\r\n1 | #![deny(clippy::missing_safety_doc)]\r\n  |         ^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\nerror: could not compile `unsafe_demo` (bin \"unsafe_demo\") due to 1 previous error<\/code><\/pre>\n<p>If you are new to Rust, yes, it has <a href=\"https:\/\/doc.rust-lang.org\/reference\/comments.html#doc-comments\"><code>\/\/\/<\/code> doc comments<\/a>. It also has <a href=\"https:\/\/doc.rust-lang.org\/reference\/attributes.html\">attributes<\/a>, which are used for proposed <a href=\"https:\/\/github.com\/safer-rust\/safety-tags\/blob\/main\/demo\/foo\/src\/main.rs#L10\">safety tags<\/a>.<\/p>\n<p>The <code>\/\/\/ # Safety<\/code> block above the function documents formal and contractual caller obligations. It is the <em>caller\u2019s<\/em> responsibility to read safety comments. Neglecting to do that can result in writing incorrect unsafe code with undefined consequences. If bad things happen, blame falls to the caller. That\u2019s why we refer to this feature as \u201c<em>caller<\/em> unsafe\u201d.<\/p>\n<p>The <code>\/\/\/<\/code> comments get copied directly into the <a href=\"https:\/\/doc.rust-lang.org\/std\/primitive.str.html#method.as_bytes_mut\">public Rust docs for <code>as_bytes_mut<\/code><\/a>. The safety comments are lifted out of the code into a public portal where callers see them. That\u2019s a strong indication of their importance and why they need to be distinct from regular comments.<\/p>\n<p>The example also includes a second, more internal, kind of safety comment. The <code>\/\/ SAFETY:<\/code> notes inside the function body are for developers or auditors of the codebase; they outline safety assumptions, not caller obligations. The compiler doesn\u2019t read, require, or honor these comments. They are a convention.<\/p>\n<p>Both comment styles are important. Together they tell a two-sided story about safety, anchored to the call graph.<\/p>\n<blockquote>\n<p>With the unsafe block, we\u2019re asserting to Rust that we\u2019ve read the function\u2019s documentation, we understand how to use it properly, and we\u2019ve verified that we\u2019re fulfilling the contract of the function.<\/p>\n<\/blockquote>\n<p>Source: <a href=\"https:\/\/doc.rust-lang.org\/book\/ch20-01-unsafe-rust.html#calling-an-unsafe-function-or-method\">Calling an Unsafe Function or Method<\/a><\/p>\n<p>This excerpt from the Rust Book makes clear that safety depends on a process that starts with compiler diagnostics but doesn\u2019t end there. The corresponding Rust lint (<a href=\"https:\/\/rust-lang.github.io\/rfcs\/2585-unsafe-block-in-unsafe-fn.html\"><code>unsafe_op_in_unsafe_fn<\/code><\/a>) was <code>allow<\/code> by default in earlier editions, so missing inner <code>unsafe<\/code> blocks were silently accepted. The <a href=\"https:\/\/doc.rust-lang.org\/edition-guide\/rust-2024\/unsafe-op-in-unsafe-fn.html\">2024 edition<\/a> promoted it to warn-by-default, a compatibility compromise that keeps existing crates building across the edition boundary. C# 16 doesn\u2019t carry the same legacy and makes it a compile error.<\/p>\n<h3>C# safety comments<\/h3>\n<p>C# uses two safety comment styles, shown here in the <code>ReadByte<\/code> mockup:<\/p>\n<pre><code class=\"language-csharp\">\/\/\/ &lt;summary&gt;Reads a single byte from unmanaged memory.&lt;\/summary&gt;\r\n\/\/\/ &lt;safety&gt;\r\n\/\/\/ The sum of &lt;paramref name=\"ptr\"\/&gt; and &lt;paramref name=\"ofs\"\/&gt; must address a byte\r\n\/\/\/ the caller is permitted to read.\r\n\/\/\/ &lt;\/safety&gt;\r\npublic static unsafe byte ReadByte(IntPtr ptr, int ofs)\r\n{\r\n    try\r\n    {\r\n        byte* addr = (byte*)ptr;\r\n        unsafe\r\n        {\r\n            \/\/ SAFETY: relies on caller obligation.\r\n            return addr[ofs];\r\n        }\r\n    }\r\n    catch (NullReferenceException)\r\n    {\r\n        throw new AccessViolationException();\r\n    }\r\n}<\/code><\/pre>\n<p>The <code>\/\/\/ &lt;safety&gt;<\/code> block above the signature is the formal caller contract. The <code>\/\/ SAFETY:<\/code> comment inside the body is an internal note naming what the unsafe operation relies on.<\/p>\n<p>The signature alone, <code>unsafe byte ReadByte(IntPtr, int)<\/code>, tells you the shape, not the safety contract. The <code>\/\/\/ &lt;safety&gt;<\/code> block is the contract, which is why an analyzer will flag its absence. The lesson is that knowing the shape of an unsafe API is necessary but not sufficient to write correct code. Writing unsafe code calls for safety glasses.<\/p>\n<p>A single residual obligation is named: <code>ptr + ofs<\/code> must address a readable byte. The caller must discharge it. The <code>unsafe<\/code> keyword on the signature is what surfaces that obligation to callers. The <code>\/\/ SAFETY:<\/code> comment names what the dereference is relying on: that the caller has safety guards for the obligation.<\/p>\n<p>Consider the states an <code>IntPtr<\/code> parameter can be in when a caller passes it:<\/p>\n<ul>\n<li><strong><code>IntPtr.Zero<\/code> (null):<\/strong> The dereference traps on the runtime\u2019s null-check guard pages and surfaces as a <code>NullReferenceException<\/code>, which the catch translates to <code>AccessViolationException<\/code>. Removing the <code>catch<\/code> wouldn\u2019t change safety, only the type of exception.<\/li>\n<li><strong>A pointer to unmapped memory<\/strong> (uninitialized, freed, or a garbage value): The dereference takes a hardware access violation. On most platforms this terminates the process; the catch may not even run.<\/li>\n<li><strong>A pointer to mapped memory the caller doesn\u2019t own<\/strong> (someone else\u2019s buffer, the GC heap, a code segment): The dereference <em>may succeed<\/em>. Mapped pages can still be unreadable (guard pages, for example), in which case behavior matches the previous bullet. When it does succeed, <code>ReadByte<\/code> returns an arbitrary byte from memory with an arbitrary value. No exception, no warning. This is the textbook UB outcome; the program continues with corrupted assumptions. Worst case is that it reads memory that is interpreted as a valid value for the program.<\/li>\n<li><strong>A pointer the caller correctly knows points to a readable byte:<\/strong> Works as intended.<\/li>\n<\/ul>\n<p>The try\/catch handles the first state, fails ungracefully on the second, and is invisible to the third. None of that is validation. The contract travels up to the caller, where information about the buffer\u2019s origin, length, and lifetime can be used to rule out the dangerous states. The <code>\/\/\/ &lt;safety&gt;<\/code> block is what makes that contract visible. The caller needs to understand and protect against these cases.<\/p>\n<h2>Safety guards<\/h2>\n<p>Documentation names the obligations. Guards discharge them. This pattern matters most at the unsafe boundary, where a developer attests that unsafe code has been brought into alignment with compiler-provided safety. The boundary is also where a review should start. With good documentation as a guide, the reviewer can tell whether the code is compliant.<\/p>\n<p>One might wonder why unsafe methods don\u2019t include enough <code>if<\/code> checks to remove the need for caller obligations. For <code>ReadByte<\/code>, no <code>if<\/code> check inside the method can validate that a caller-supplied <code>IntPtr<\/code> points to readable memory: the runtime simply doesn\u2019t know what the caller has allocated, where, or for how long. Callers are uniquely able to determine the minimum set of checks that maintain safety while maximizing performance.<\/p>\n<p>Note: there isn\u2019t a standard name for these boundary methods\/functions. Rust docs call them \u201csafe elements\u201d. This post calls them \u201cunsafe boundary methods\u201d: methods that sit at the boundary of safe and unsafe code, where unsafety is suppressed. The label <em>unsafe<\/em> is deliberate: these methods retain every dangerous capability of <code>unsafe<\/code>-decorated methods; they just don\u2019t propagate that to their callers.<\/p>\n<h3>Rust safety guards<\/h3>\n<p>Another Rust example, <a href=\"https:\/\/github.com\/rust-lang\/rust\/blob\/a08f25a7ef2800af5525762e981c24d96c14febe\/library\/core\/src\/str\/mod.rs#L570\"><code>str.split_at<\/code><\/a>:<\/p>\n<pre><code class=\"language-plaintext\">pub fn split_at(&amp;self, mid: usize) -&gt; (&amp;str, &amp;str) {\r\n    \/\/ is_char_boundary checks that the index is in [0, .len()]\r\n    if self.is_char_boundary(mid) {\r\n        \/\/ SAFETY: just checked that `mid` is on a char boundary.\r\n        unsafe { (self.get_unchecked(0..mid), self.get_unchecked(mid..self.len())) }\r\n    } else {\r\n        slice_error_fail(self, 0, mid)\r\n    }\r\n}<\/code><\/pre>\n<p>Unsafe boundary functions typically have only <code>\/\/ SAFETY:<\/code> comments; they don\u2019t impose obligations of their own. The formal <code>\/\/\/<\/code> style is reserved for <code>unsafe<\/code> methods, whose obligations the boundary then discharges. Functions that propagate must be marked <code>unsafe<\/code>.<\/p>\n<p>The <code>if self.is_char_boundary(mid)<\/code> check in <code>split_at<\/code> is a guard that maintains safety for the unsafe code it calls. It ensures that the split is on a character boundary, since Unicode characters can be multi-byte. If that test fails, then the program panics via <code>slice_error_fail<\/code>. A <a href=\"https:\/\/doc.rust-lang.org\/reference\/panic.html\">panic<\/a> will crash the program to prevent undefined behavior.<\/p>\n<p>A program that panics to avoid undefined behavior is far more reliable than one that lets it happen.<\/p>\n<h3>C# safety guards<\/h3>\n<p>The same boundary pattern from Rust applies in C#: same <code>\/\/ SAFETY:<\/code> convention, same absence of an <code>unsafe<\/code> marker on the signature.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/blob\/ddba7854ea97d1899650af2779a98fcfdc3fc83c\/src\/libraries\/System.Private.CoreLib\/src\/System\/String.cs\"><code>String.CopyTo<\/code><\/a>:<\/p>\n<pre><code class=\"language-csharp\">\/\/ Converts a substring of this string to an array of characters. Copies the\r\n\/\/ characters of this string beginning at position sourceIndex and ending at\r\n\/\/ sourceIndex + count - 1 to the character array buffer, beginning\r\n\/\/ at destinationIndex.\r\n\/\/\r\npublic void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count)\r\n{\r\n    ArgumentNullException.ThrowIfNull(destination);\r\n\r\n    ArgumentOutOfRangeException.ThrowIfNegative(count);\r\n    ArgumentOutOfRangeException.ThrowIfNegative(sourceIndex);\r\n    ArgumentOutOfRangeException.ThrowIfGreaterThan(count, Length - sourceIndex, nameof(sourceIndex));\r\n    ArgumentOutOfRangeException.ThrowIfGreaterThan(destinationIndex, destination.Length - count);\r\n    ArgumentOutOfRangeException.ThrowIfNegative(destinationIndex);\r\n\r\n    unsafe\r\n    {\r\n        \/\/ SAFETY: the bounds checks above ensure that `count` characters\r\n        \/\/ starting at `sourceIndex` are in range of this string, and that\r\n        \/\/ `count` characters starting at `destinationIndex` fit in `destination`.\r\n        Buffer.Memmove(\r\n            destination: ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(destination), destinationIndex),\r\n            source: ref Unsafe.Add(ref _firstChar, sourceIndex),\r\n            elementCount: (uint)count);\r\n    }\r\n}<\/code><\/pre>\n<p>Every <code>ThrowIf*<\/code> call here is a memory-safety guard. Each one props up an invariant that the raw <code>Buffer.Memmove<\/code> call assumes:<\/p>\n<ul>\n<li><code>ThrowIfNull(destination)<\/code>: without it, <code>MemoryMarshal.GetArrayDataReference(null)<\/code> is UB.<\/li>\n<li><code>ThrowIfNegative(count)<\/code>: without it, <code>(uint)count<\/code> silently wraps a negative value into a huge <code>elementCount<\/code>, and the resulting out-of-range copy is UB.<\/li>\n<li><code>ThrowIfNegative(sourceIndex)<\/code> and <code>ThrowIfNegative(destinationIndex)<\/code>: without them, <code>Unsafe.Add(ref \u2026, negativeIndex)<\/code> walks the ref off the front of the storage, and the resulting read or write is UB.<\/li>\n<li>The two <code>ThrowIfGreaterThan<\/code> checks layer on top of the negative checks above (and rely on the runtime invariant that <code>Length<\/code> is in <code>[0, int.MaxValue]<\/code>, so that <code>Length - sourceIndex<\/code> doesn\u2019t overflow) to bound <code>count<\/code> against the remaining capacity of source and destination. Without them, the copy can run past the end of either buffer, and the resulting read or write is UB.<\/li>\n<\/ul>\n<p>The checks compose. Each one is only sufficient because the preceding ones have already ruled out classes of inputs. Change any link in that chain (switch to an unsigned index type, or change what the runtime guarantees about <code>Length<\/code>), and the safety reasoning has to be re-derived.<\/p>\n<p>The <code>ThrowIf*<\/code> methods are the C# analog of Rust panic helpers like <code>slice_error_fail<\/code>; both crash the program at the boundary rather than let UB happen, and both are factored into separate functions to keep cold paths out of hot code.<\/p>\n<h2>Unsafe fields<\/h2>\n<p>Fields deserve a discussion. A field needs to be <code>unsafe<\/code> when its declared type doesn\u2019t express an invariant the enclosing type maintains and downstream code depends on. The unsafety lives in the gap between what the type system sees and what the enclosing type promises.<\/p>\n<p>The simplest case is a field holding a native pointer. The example below is a mockup; it isn\u2019t sourced from dotnet\/runtime like the other examples.<\/p>\n<pre><code class=\"language-csharp\">public class NativeBuffer : IDisposable\r\n{\r\n    \/\/\/ &lt;safety&gt;\r\n    \/\/\/ Must be null or point to a buffer of Length bytes.\r\n    \/\/\/ &lt;\/safety&gt;\r\n    private unsafe byte* _ptr;\r\n    public int Length { get; }\r\n\r\n    public NativeBuffer(int length)\r\n    {\r\n        ArgumentOutOfRangeException.ThrowIfNegative(length);\r\n        unsafe\r\n        {\r\n            \/\/ SAFETY: NativeMemory.Alloc throws OutOfMemoryException on failure rather than\r\n            \/\/ returning null (unlike the malloc it wraps), so on return _ptr points to `length` bytes.\r\n            _ptr = (byte*)NativeMemory.Alloc((nuint)length);\r\n        }\r\n        Length = length;\r\n    }\r\n\r\n    public byte ReadAt(int index)\r\n    {\r\n        ArgumentOutOfRangeException.ThrowIfNegative(index);\r\n        ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Length);\r\n        unsafe\r\n        {\r\n            ObjectDisposedException.ThrowIf(_ptr is null, this);\r\n            \/\/ SAFETY: bounds checked above; null check just above; _ptr therefore points to Length bytes\r\n            return _ptr[index];\r\n        }\r\n    }\r\n\r\n    public void Dispose()\r\n    {\r\n        unsafe\r\n        {\r\n            \/\/ SAFETY: _ptr is null or was returned by NativeMemory.Alloc; Free accepts both\r\n            NativeMemory.Free(_ptr);\r\n            _ptr = null;\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<p>The class is safe-callable, with the <code>unsafe<\/code> field carrying the validity invariant on behalf of the public surface. <code>Length<\/code> is a get-only auto-property fixed at construction; its immutability is the other half of the invariant, since <code>_ptr<\/code>\u2018s size obligation is stated in terms of <code>Length<\/code>. If <code>Length<\/code> could change after construction, it would need its own <code>unsafe<\/code> marker and <code>&lt;safety&gt;<\/code> block to keep the pair coherent. <code>Dispose<\/code> deliberately weakens that invariant from \u201cvalid\u201d to \u201cnull or valid\u201d by writing <code>null<\/code>, which is why <code>_ptr<\/code> can\u2019t be <code>readonly<\/code> and why <code>ReadAt<\/code> checks for null before dereferencing. The <code>unsafe<\/code> marker on the field keeps both writes (the allocation in the constructor and the invalidation in <code>Dispose<\/code>) reviewable in one place.<\/p>\n<p>A more idiomatic case in the runtime libraries is a field whose declared type is sound but less specific than what the class actually maintains. The <a href=\"https:\/\/github.com\/dotnet\/designs\/blob\/main\/accepted\/2025\/memory-safety\/caller-unsafe.md\">design doc<\/a> gives a simplified version of this pattern: a generic class holds an <code>Array<\/code> field that must always contain a <code>T[]<\/code>. <code>Array<\/code> is the <code>object<\/code> of array types; every <code>T[]<\/code> is an <code>Array<\/code>, so declaring the field as <code>Array<\/code> is type-correct, and doing so avoids generic specialization costs. The C# type system permits any array to be assigned to that field, while the class promises always exactly <code>T[]<\/code>. The unsafety lives in that gap: the type system can\u2019t see the tighter invariant, and the class is responsible for upholding it.<\/p>\n<pre><code class=\"language-csharp\">public class ArrayWrapper&lt;T&gt;\r\n{\r\n    \/\/\/ &lt;safety&gt;\r\n    \/\/\/ Must always hold a value whose runtime type is T[].\r\n    \/\/\/ &lt;\/safety&gt;\r\n    private readonly unsafe Array _array;\r\n\r\n    public ArrayWrapper(T[] items)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(items);\r\n        unsafe\r\n        {\r\n            \/\/ SAFETY: items is statically T[], so the field invariant holds.\r\n            _array = items;\r\n        }\r\n    }\r\n\r\n    public T GetItem(int index)\r\n    {\r\n        unsafe\r\n        {\r\n            \/\/ SAFETY: _array is always a T[] per the field's &lt;safety&gt; block\r\n            var typedArray = Unsafe.As&lt;T[]&gt;(_array);\r\n            return typedArray[index];\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<p>The pattern is the same as <code>NativeBuffer<\/code>: an <code>unsafe<\/code> field with a documented invariant, unsafe blocks at the boundary discharging it, and a safe-callable public surface.<\/p>\n<p>Rust is working through the same problem, and the <a href=\"https:\/\/rust-lang.github.io\/rust-project-goals\/2025h2\/unsafe-fields.html\">unsafe-fields proposal<\/a> uses <code>Vec&lt;T&gt;<\/code> as its motivating case. <code>Vec&lt;T&gt;<\/code> carries an invariant that the elements at <code>data[i]<\/code> for <code>i &lt; len<\/code> are initialized. Today, that invariant lives only in comments and prose. There is nothing stopping a method (even a private one) from desynchronizing <code>len<\/code> and <code>data<\/code> in entirely safe code:<\/p>\n<pre><code class=\"language-rust\">pub struct Vec&lt;T&gt; {\r\n    data: Box&lt;[MaybeUninit&lt;T&gt;]&gt;,\r\n    len: usize,\r\n}\r\n\r\nimpl&lt;T&gt; Vec&lt;T&gt; {\r\n    \/\/ Safe code, but the next read is undefined behavior.\r\n    pub fn evil(&amp;mut self) {\r\n        self.len += 2;\r\n    }\r\n}<\/code><\/pre>\n<p>The proposed future shape moves the invariant into the type system by marking both fields <code>unsafe<\/code>:<\/p>\n<pre><code class=\"language-rust\">struct Vec&lt;T&gt; {\r\n    \/\/ SAFETY: The elements `data[i]` for\r\n    \/\/ `i &lt; len` are in a valid state.\r\n    unsafe data: Box&lt;[MaybeUninit&lt;T&gt;]&gt;,\r\n    unsafe len: usize,\r\n}<\/code><\/pre>\n<p>With that change, any write to <code>len<\/code> or <code>data<\/code> has to happen inside an <code>unsafe<\/code> block; <code>evil<\/code> no longer compiles as written. The two fields are reviewed together, in the same place, against the same contract. That\u2019s the same benefit <code>NativeBuffer<\/code> gets from pairing <code>unsafe byte* _ptr<\/code> with a fixed <code>Length<\/code>, and that <code>ArrayWrapper&lt;T&gt;<\/code> gets from pairing <code>readonly unsafe Array _array<\/code> with the always-<code>T[]<\/code> promise.<\/p>\n<p>You might say that \u201cyou can still write <code>evil<\/code> with <code>unsafe<\/code> and it still results in UB\u201d. Yes. The entire proposition is that unsafe code is marked and easy to audit. That\u2019s the basis of safety in all of these languages.<\/p>\n<p>A few rules of thumb for <code>unsafe<\/code> on fields:<\/p>\n<ul>\n<li><strong>Writes are the primary motivation.<\/strong> <code>unsafe<\/code> on the field forces every write into a reviewable context where the contract is in view, establishing (at least) the member-to-member discipline that keeps the invariant intact. For example, a write to <code>_ptr<\/code> in the <code>NativeBuffer<\/code> example would violate <code>Length<\/code>.<\/li>\n<li><strong>Readonly fields satisfy much of the same need.<\/strong> It helps to think of <code>unsafe readonly<\/code> as the contract plus a built-in guard: <code>unsafe<\/code> names the invariant, and <code>readonly<\/code> is the safety guard that prevents post-construction writes from violating it. Drop the <code>readonly<\/code> and the contract remains; it just has to be discharged the harder way, by reviewing every write site. The <code>ArrayWrapper&lt;T&gt;<\/code> example above is <code>readonly unsafe<\/code> for exactly this reason. Rust is converging on the same shape via the <a href=\"https:\/\/rust-lang.github.io\/rust-project-goals\/2025h2\/unsafe-fields.html#design-axioms\">unsafe-fields design axioms<\/a>: the marker stays, but the operations it gates (writes, reinitialization) are exactly the ones immutability already prevents.<\/li>\n<li><strong>Private isn\u2019t a free pass.<\/strong> It\u2019s tempting to assume that because a field is private, the type\u2019s own methods can be trusted to maintain the invariant. That was the old <code>unsafe<\/code> type model. In the new model, member-to-member interaction is itself a contract surface; one method\u2019s correct write can be undone by another method\u2019s uncoordinated write. Unsafety is about protecting the contract from any code that might violate it, including code within the type itself.<\/li>\n<\/ul>\n<h2>A migration walkthrough<\/h2>\n<p>The best way to understand the model is to migrate some existing code to it. This is what the .NET team is doing across the runtime libraries. Pick an <code>unsafe<\/code> API, follow it to a caller, and decide whether the migration can discharge the callee\u2019s obligations inline or has to propagate them upward. Each caller is a candidate place for the boundary; the migration answers whether that\u2019s where the boundary belongs.<\/p>\n<p>This section is speculative. The model isn\u2019t finalized and the runtime libraries haven\u2019t been migrated yet. The examples are informed guesses, intended to convey where we\u2019re headed and what the new model implies for existing code.<\/p>\n<p>We\u2019re going to migrate some methods in this section that bottom out in <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.runtime.interopservices.nativememory.alloc\"><code>NativeMemory.Alloc<\/code><\/a> and <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.runtime.interopservices.nativememory.free\"><code>NativeMemory.Free<\/code><\/a>. Here\u2019s how the two <code>NativeMemory<\/code> methods look under the new model:<\/p>\n<pre><code class=\"language-csharp\">public static void* Alloc(nuint byteCount);\r\n\r\n\/\/\/ &lt;safety&gt;\r\n\/\/\/ The caller must ensure:\r\n\/\/\/\r\n\/\/\/ - &lt;paramref name=\"ptr\"\/&gt; was returned by &lt;see cref=\"Alloc(nuint)\"\/&gt; (or a\r\n\/\/\/   compatible allocator) and has not already been freed.\r\n\/\/\/ - No live pointer or span aliases the storage at the time of this call.\r\n\/\/\/ &lt;\/safety&gt;\r\npublic static unsafe void Free(void* ptr);<\/code><\/pre>\n<p>The asymmetry is intentional. <code>Alloc<\/code> becomes safe. It returns a <code>void*<\/code>, but holding a pointer isn\u2019t unsafe on its own; the unsafety is in the eventual dereference, which the caller wraps. Failing to free is a leak, not a safety issue. (<code>Alloc<\/code> also differs from <code>malloc<\/code> in that it throws <code>OutOfMemoryException<\/code> on failure rather than returning null, so callers don\u2019t have to guard the return.) <code>Free<\/code> remains <code>unsafe<\/code> because it carries real preconditions: the pointer must be one a compatible allocator returned and not already freed, and nothing else can alias the storage. The <code>&lt;safety&gt;<\/code> block makes those obligations visible where every caller and reviewer can see them.<\/p>\n<p>We\u2019ll now jump to a caller. Here\u2019s a mockup of <a href=\"https:\/\/github.com\/dotnet\/runtime\/blob\/bb1148d8e8af1df20e483cf19346907c98d408d5\/src\/libraries\/System.Diagnostics.FileVersionInfo\/src\/System\/Diagnostics\/FileVersionInfo.Windows.cs#L11-L42\"><code>FileVersionInfo<\/code>\u2018s constructor<\/a> under the new model. The constructor parses a native version-info blob into this object\u2019s string and integer fields (<code>_companyName<\/code>, <code>_fileVersion<\/code>, <code>_fileMajor<\/code>, and so on); the allocation is just the scratch buffer that holds the blob while <code>GetVersionInfoForCodePage<\/code> reads from it.<\/p>\n<p>Current signature: <code>private unsafe FileVersionInfo(string fileName)<\/code>. It is <code>unsafe<\/code> solely to establish an unsafe context.<\/p>\n<p>Here\u2019s the updated signature and implementation, with internal safety comments.<\/p>\n<pre><code class=\"language-csharp\">private FileVersionInfo(string fileName)\r\n{\r\n    _fileName = fileName;\r\n\r\n    uint infoSize = Interop.Version.GetFileVersionInfoSizeEx(\r\n        Interop.Version.FileVersionInfoType.FILE_VER_GET_LOCALISED, _fileName, out _);\r\n    if (infoSize != 0)\r\n    {\r\n        unsafe\r\n        {\r\n            \/\/ SAFETY:\r\n            \/\/ - bounds: `infoSize` is the size returned by GetFileVersionInfoSizeEx\r\n            \/\/   and is the same value passed to both Alloc and GetFileVersionInfoEx,\r\n            \/\/   so all reads through `memPtr` stay within the allocated range.\r\n            \/\/ - lifetime: `memPtr` is freed in the finally before this constructor\r\n            \/\/   returns, and never escapes; every consumer (GetLanguageAndCodePage,\r\n            \/\/   GetVersionInfoForCodePage) is called from within this method and\r\n            \/\/   writes its results into this object's fields.\r\n            void* memPtr = NativeMemory.Alloc(infoSize);\r\n            try\r\n            {\r\n                if (Interop.Version.GetFileVersionInfoEx(\r\n                    \/* flags *\/ default, _fileName, 0U, infoSize, memPtr))\r\n                {\r\n                    uint lcp = GetLanguageAndCodePage(memPtr);\r\n                    _ = GetVersionInfoForCodePage(memPtr, lcp.ToString(\"X8\")) ||\r\n                        (lcp != 0x040904B0 &amp;&amp; GetVersionInfoForCodePage(memPtr, \"040904B0\")) ||\r\n                        (lcp != 0x040904E4 &amp;&amp; GetVersionInfoForCodePage(memPtr, \"040904E4\")) ||\r\n                        (lcp != 0x04090000 &amp;&amp; GetVersionInfoForCodePage(memPtr, \"04090000\"));\r\n                }\r\n            }\r\n            finally\r\n            {\r\n                NativeMemory.Free(memPtr);\r\n            }\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<p>The constructor is a sound unsafe boundary. The remaining unsafety (the interop calls that read through <code>memPtr<\/code>, and the <code>Free<\/code> at the end) is discharged inline:<\/p>\n<ul>\n<li><strong>Bounds:<\/strong> a single <code>infoSize<\/code> value flows from the size-query call into <code>Alloc<\/code> and into every interop call that reads through <code>memPtr<\/code>; the three uses are tied together by name, so reads stay within the allocated range.<\/li>\n<li><strong>Lifetime:<\/strong> the <code>try\/finally<\/code> guarantees <code>Free<\/code> runs before the constructor returns, even on an exception from the interop calls. The pointer never escapes; every helper that consumes it is called inside this method, so no alias survives past <code>Free<\/code>.<\/li>\n<\/ul>\n<p>No <code>unsafe<\/code> marker on the constructor, no <code>&lt;safety&gt;<\/code> block; the unsafety is fully sealed inside the body. Unsafe constructors are possible in the new model (they propagate the obligation to whatever code instantiates the type), but this one doesn\u2019t need to be unsafe.<\/p>\n<p>Now a mockup of <a href=\"https:\/\/github.com\/dotnet\/runtime\/blob\/bb1148d8e8af1df20e483cf19346907c98d408d5\/src\/libraries\/System.Security.Cryptography\/src\/System\/Security\/Cryptography\/FixedMemoryKeyBox.cs\"><code>FixedMemoryKeyBox<\/code><\/a>:<\/p>\n<pre><code class=\"language-csharp\">internal sealed class FixedMemoryKeyBox : SafeHandle\r\n{\r\n    \/\/\/ &lt;safety&gt;\r\n    \/\/\/ Must equal the byte size of the allocation pointed to by &lt;c&gt;handle&lt;\/c&gt;.\r\n    \/\/\/ &lt;\/safety&gt;\r\n    private readonly unsafe int _length;\r\n\r\n    internal FixedMemoryKeyBox(ReadOnlySpan&lt;byte&gt; key) : base(IntPtr.Zero, ownsHandle: true)\r\n    {\r\n        void* memory;\r\n        unsafe\r\n        {\r\n            \/\/ SAFETY:\r\n            \/\/ - alloc:   NativeMemory.Alloc returns a pointer to key.Length writable bytes.\r\n            \/\/ - span:    new Span&lt;byte&gt;(memory, key.Length) addresses exactly those bytes.\r\n            \/\/ - lifetime: ownership of the pointer transfers to this SafeHandle\r\n            \/\/             via SetHandle below; ReleaseHandle frees the allocation when\r\n            \/\/             the ref-count reaches zero.\r\n            \/\/ - _length: paired with the allocation made on this line.\r\n            memory = NativeMemory.Alloc((nuint)key.Length);\r\n            key.CopyTo(new Span&lt;byte&gt;(memory, key.Length));\r\n            _length = key.Length;\r\n        }\r\n        SetHandle((IntPtr)memory);\r\n    }\r\n\r\n    \/\/\/ &lt;safety&gt;\r\n    \/\/\/ The returned span aliases storage owned by this SafeHandle.\r\n    \/\/\/ The caller must ensure:\r\n    \/\/\/\r\n    \/\/\/ - the span is not used after this SafeHandle is disposed;\r\n    \/\/\/ - access is bracketed by &lt;see cref=\"SafeHandle.DangerousAddRef\"\/&gt; and\r\n    \/\/\/   &lt;see cref=\"SafeHandle.DangerousRelease\"\/&gt; (or equivalent), so\r\n    \/\/\/   disposal on another thread can't free the buffer mid-use.\r\n    \/\/\/ &lt;\/safety&gt;\r\n    internal unsafe ReadOnlySpan&lt;byte&gt; DangerousKeySpan\r\n    {\r\n        get\r\n        {\r\n            unsafe\r\n            {\r\n                \/\/ SAFETY:\r\n                \/\/ - bounds: `_length` matches the allocation made in the ctor.\r\n                \/\/ - lifetime: NOT discharged here; propagated to the caller\r\n                \/\/   via the &lt;safety&gt; block above. The `Dangerous` prefix\r\n                \/\/   echoes that contract in the API name.\r\n                return new ReadOnlySpan&lt;byte&gt;((void*)handle, _length);\r\n            }\r\n        }\r\n    }\r\n\r\n    internal TRet UseKey&lt;TState, TRet&gt;(TState state, Func&lt;TState, ReadOnlySpan&lt;byte&gt;, TRet&gt; func)\r\n    {\r\n        bool addedRef = false;\r\n        unsafe\r\n        {\r\n            \/\/ SAFETY: AddRef holds the SafeHandle alive for the duration of\r\n            \/\/ the callback, so `DangerousKeySpan` aliases live storage. The\r\n            \/\/ span is not retained beyond `func`'s return.\r\n            try\r\n            {\r\n                DangerousAddRef(ref addedRef);\r\n                return func(state, DangerousKeySpan);\r\n            }\r\n            finally\r\n            {\r\n                if (addedRef)\r\n                {\r\n                    DangerousRelease();\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    protected override bool ReleaseHandle()\r\n    {\r\n        unsafe\r\n        {\r\n            \/\/ SAFETY: SafeHandle's ref-counting guarantees no live span\r\n            \/\/ aliases `handle` at this point; the new Span&lt;byte&gt;(handle, _length)\r\n            \/\/ addresses the allocation made in the ctor.\r\n            CryptographicOperations.ZeroMemory(new Span&lt;byte&gt;((void*)handle, _length));\r\n            NativeMemory.Free((void*)handle);\r\n        }\r\n        return true;\r\n    }\r\n\r\n    public override bool IsInvalid =&gt; handle == IntPtr.Zero;\r\n}<\/code><\/pre>\n<p><code>FixedMemoryKeyBox<\/code> is two boundaries in one type, illustrating both directions:<\/p>\n<ul>\n<li><code>DangerousKeySpan<\/code> is <strong>caller-unsafe<\/strong>. Bounds is discharged inline by the <code>_length<\/code> field invariant. The <code>int<\/code> is safe on its own; the safety issue is its coupling to <code>handle<\/code>: they are a pair that have to match. Lifetime is <em>not<\/em> discharged. The span aliases storage owned by the SafeHandle, and the storage outlives the property call by design. The <code>&lt;safety&gt;<\/code> block names two residual obligations: don\u2019t outlive the SafeHandle, and bracket access with <code>DangerousAddRef<\/code>\/<code>Release<\/code>. The compiler can\u2019t enforce either. The marker tells callers they have work to do.<\/li>\n<li><code>UseKey<\/code> is the <strong>sound boundary<\/strong> built on top. It discharges the lifetime obligation by bracketing the callback with <code>DangerousAddRef<\/code>\/<code>Release<\/code> in a <code>try\/finally<\/code>. The <code>ReadOnlySpan&lt;byte&gt;<\/code> passed to <code>func<\/code> is safe by virtue of <code>ref struct<\/code> lifetime rules. From the outside, <code>UseKey<\/code> is safe-callable; the unsafety is sealed inside the bracket.<\/li>\n<\/ul>\n<h2>Binary distribution<\/h2>\n<p>.NET libraries are often distributed as binaries. A popular library published to nuget.org might have zero or a thousand warnings, but you know it has zero errors. Errors are one of the few aspects of compilation that are reliably communicated between producer and consumer.<\/p>\n<p>C# 16 <code>unsafe<\/code> relies heavily on new compiler errors. Opting in to the new model means that the annotation work has been done. It will be straightforward to inspect a project and see whether it uses <code>unsafe<\/code> code at all.<\/p>\n<p>Swift, for example, relies more heavily on warnings for memory safety adoption. The burden for Swift is much lower since dependencies are distributed as source. You can see errors and warnings of dependencies with equal fidelity when you build. Rust also has source-distributed dependencies, but relies heavily on errors.<\/p>\n<p>We are considering adding badges on nuget.org to encourage adoption of the new memory-safety enforcement and to make it easier to find libraries that have done so. Libraries and packages that have adopted the model will be stamped accordingly, making it easy to inspect and understand the safety status of your supply chain (as it relates to what the compiler sees).<\/p>\n<p>It will be common for projects compiled with the old model to consume packages built with the new one, and vice versa. As described in the nutshell section, the two directions are asymmetric. An opted-in project enforces compat-mode rules against legacy packages: any pointer type in a callee signature requires an enclosing <code>unsafe { }<\/code> block at the call site. A legacy project, by contrast, sees opted-in packages as ordinary assemblies and is not subject to any new diagnostics. The asymmetry is deliberate. The opted-in side carries the safety guarantee, and compat mode keeps that guarantee from quietly degrading when consuming legacy code.<\/p>\n<h2>Remaining design space<\/h2>\n<p>The intent of our project is to deploy all aspects of the new model at once, both because it is only coherent as a whole, and to avoid developers needing to adopt a progressive set of breaking changes. However, there are a couple of design aspects that we could not address in C# 16.<\/p>\n<p>The first is reflection, which is a carve-out in the model. Code can call <code>unsafe<\/code> APIs through <code>MethodInfo.Invoke<\/code> without an enclosing <code>unsafe<\/code> block, and reflection writes can violate the invariants documented on <code>unsafe<\/code> fields. Reflection-heavy code should be reviewed for unsafe API calls and for writes that bypass the contracts the new model expresses. We may address reflection usage in a later version.<\/p>\n<p>The second is lifetimes. Rust addresses lifetime through its borrow checker; we are not planning a pervasive system like borrowing. C# relies on a GC and ref-based ownership to cover some of the same territory. We are considering a targeted ownership model, as mentioned in <a href=\"https:\/\/github.com\/dotnet\/designs\/blob\/main\/accepted\/2025\/memory-safety\/memory-safety.md\">Memory Safety in .NET<\/a>. We\u2019ll post design plans around that later.<\/p>\n<p>The primary use case for stronger lifetime enforcement is <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.buffers.arraypool-1\"><code>ArrayPool<\/code><\/a>, particularly for the <code>Rent<\/code> and <code>Return<\/code> methods. The key scenario is returning an array and continuing to use it. That\u2019s a \u201cuse after free\u201d violation. It is easy to get the convention wrong, and we\u2019ve made that mistake in our own code. In contrast, <code>Rent<\/code> with no <code>Return<\/code> is a leak and not a memory safety violation.<\/p>\n<h2>Analogies<\/h2>\n<p>The Caller-unsafe feature invites analogies. Most fall down on close inspection.<\/p>\n<p><strong>Claim \u2014 Nullable is similar to caller-unsafe.<\/strong> Nullable reference types require method inspection and potential signature updates to correctly participate in the model. They also push nullability from callee to caller and include a suppression mechanism.<\/p>\n<p><strong>Reality.<\/strong> Nullable reference types are a use-site concern that affects the type of an expression. They don\u2019t affect the nature of the caller at all. Nullable suppression (<code>!<\/code>) operates on a single expression; it tells the compiler the value is non-null at that point. With unsafe, there\u2019s no expression-level shortcut; every call to an <code>unsafe<\/code> member requires an enclosing <code>unsafe { }<\/code> block. Suppression in the unsafe model is a scoped, reviewable region, not a per-value annotation.<\/p>\n<p><strong>Claim \u2014 Async is similar to caller-unsafe.<\/strong> Async propagates from method to method. The <code>async<\/code> keyword forces the propagation, just like <code>unsafe<\/code>. <code>Task.Wait()<\/code> is the suppression mechanism.<\/p>\n<p><strong>Reality.<\/strong> The <code>async<\/code> keyword is more similar to C# 1.0 <code>unsafe<\/code> in that it establishes an async context for the method in which <code>await<\/code> can be used; the method\u2019s return type (<code>Task<\/code>\/<code>ValueTask<\/code>) is what callers see. The propagation mechanism is an awaitable return type: the type system itself. <code>Task.Wait()<\/code> forces a transition from async to sync, which comes with significant trade-offs. It\u2019s not a formal suppression mechanism.<\/p>\n<p><strong>Claim \u2014 Swift\u2019s type-level <code>@unsafe<\/code> is just C# 1.0\u2019s type-level <code>unsafe<\/code>.<\/strong> Both languages use the keyword on a type, so the C# 16 removal of type-level <code>unsafe<\/code> looks like it\u2019s giving up something Swift kept.<\/p>\n<p><strong>Reality.<\/strong> The two markers share a keyword and a target but are nearly orthogonal in semantics. Swift\u2019s <code>@unsafe<\/code> on a type is a <em>caller-facing contract<\/em>: any declaration that uses the type becomes implicitly <code>@unsafe<\/code>, and callers have to wrap accesses in <code>unsafe<\/code> expressions. C# 1.0\u2019s <code>unsafe<\/code> on a type was an <em>implementation scope<\/em>: it let the type\u2019s member bodies use pointers but didn\u2019t propagate anything to callers. C# 16 removes the C# 1.0 form because it carried no caller information. The two also differ in disposition: Swift\u2019s marker is non-permissive, adding an obligation on callers, while C# 1.0\u2019s marker was permissive, unlocking capabilities inside the type\u2019s members. The Swift form is the more safety-first of the two. Reading Swift\u2019s type-level marker through a C# 1.0 lens is the wrong lens.<\/p>\n<h2>AI enablement<\/h2>\n<p>The model adds two things an agent can\u2019t ignore: a call graph partitioned into safe, <code>unsafe<\/code>, and boundary methods; and a compiler that rejects <code>unsafe<\/code> calls without an enclosing <code>unsafe<\/code> block. An analyzer will also contribute warnings for missing <code>&lt;safety&gt;<\/code> docs. Each of these narrows the code an agent can generate while keeping the build happy, particularly if <code>TreatWarningsAsErrors<\/code> is set. An agent generating code against <code>MemoryMarshal.ReadByte<\/code> has to either propagate <code>unsafe<\/code> upward to its caller or suppress it with guards at the boundary.<\/p>\n<p><code>&lt;safety&gt;<\/code> docs act as per-API instructions. Even with that, a code-generating agent can and sometimes will miss a guard, and the compiler won\u2019t notice. The informative boundary still helps: it tells a human or code-review agent exactly where the guards must live and what they should protect. The same dynamic applies to nullable reference types and AOT analyzers: tighter grammars narrow the search space, and model output tracks accordingly.<\/p>\n<p>There are two key ways that agents can subvert the model:<\/p>\n<ul>\n<li>Generate code that doesn\u2019t compile.<\/li>\n<li>Switch the project back to the old model and\/or enable <code>AllowUnsafeBlocks<\/code>. This is similar to when agents sometimes want to disable <code>TreatWarningsAsErrors<\/code> or <code>IsAotCompatible<\/code>.<\/li>\n<\/ul>\n<p>Both categories are easy to detect in code review or identify in git history. \u201cEasier to detect in code review\u201d is the tagline for the entire initiative.<\/p>\n<p>The migration to the new model is also a good fit for agents. Migrating existing code to the model and writing new code within the model isn\u2019t really a different activity. Once the first set of .NET runtime library APIs are migrated, conformance becomes a uniform task for old and new code.<\/p>\n<p>Patterns well-established in Rust (<code>unsafe fn<\/code>, <code>unsafe {}<\/code>) map cleanly onto C#-shaped code. Agents can pattern-match on the existing corpora (Rust\u2019s std, Swift\u2019s standard library) and on the .NET runtime libraries as they migrate. Arguably, the highest-value pattern match is on the structure and idiom of safety documentation. That aspect of the migration would be the most difficult to skill-ify. As noted earlier, safety documentation is the most critical aspect of the new model.<\/p>\n<p>Adjacent research comes to the same conclusions:<\/p>\n<ul>\n<li><a href=\"https:\/\/arxiv.org\/abs\/2504.15254\">CRUST-Bench: A Comprehensive Benchmark for C-to-safe-Rust Transpilation<\/a> (Khatry et al., COLM \u201925) found that agents with compiler feedback roughly double the success rate of single-shot generation on C-to-safe-Rust repository translation.<\/li>\n<li><a href=\"https:\/\/arxiv.org\/abs\/2305.15334\">Gorilla: Large Language Model Connected with Massive APIs<\/a> (Patil et al., NeurIPS \u201924) showed that LLMs given retrievable API documentation call APIs more reliably and hallucinate less than unaided baselines.<\/li>\n<li><a href=\"https:\/\/arxiv.org\/abs\/2211.03622\">Do Users Write More Insecure Code with AI Assistants?<\/a> (Perry et al., CCS \u201923) found that developers using AI coding assistants produced significantly less secure code than an unassisted control group, while rating their own output as <em>more<\/em> secure. That\u2019s the gap language-level safety enforcement is meant to close.<\/li>\n<li><a href=\"https:\/\/arxiv.org\/abs\/2504.09246\">Type-Constrained Code Generation with Language Models<\/a> (M\u00fcndler et al., PLDI \u201925) showed that pushing type-system constraints into LLM decoding (rather than relying on post-hoc compiler feedback) cuts compilation errors by more than half and improves functional correctness across synthesis, translation, and repair. Richer language rules shape generation, not just validate it.<\/li>\n<li><a href=\"https:\/\/arxiv.org\/abs\/2208.08227\">MultiPL-E: A Scalable and Extensible Approach to Benchmarking Neural Code Generation<\/a> (Cassano et al., TSE \u201923) showed that LLM code-generation performance tracks syntactic proximity to high-resource languages, not just training volume in the target language. That\u2019s the lever that lets Rust\u2019s <code>unsafe fn<\/code> \/ <code>unsafe {}<\/code> corpus carry over to C#-shaped code.<\/li>\n<li><a href=\"https:\/\/www.microsoft.com\/research\/publication\/llm-assistance-for-memory-safety\/\">LLM Assistance for Memory Safety<\/a> (Rastogi et al., ICSE \u201925) tackled the migration version of this problem: inferring the source-level annotations needed to retrofit legacy C onto the Checked C safe dialect. Their tool inferred 86% of the annotations symbolic tools couldn\u2019t, on real codebases up to 20K LOC. The same shape of work (naming the obligations existing code already implicitly relies on) is what migrating to the new C# model will require.<\/li>\n<\/ul>\n<h2>Closing<\/h2>\n<p>The new model layers a set of (opt-in) breaking changes onto code that uses <code>unsafe<\/code> today: <code>unsafe<\/code> on a member signature defines a caller-facing contract, an <code>unsafe<\/code> block is required at every call to an <code>unsafe<\/code> member, and every <code>unsafe<\/code> member should carry a <code>\/\/\/ &lt;safety&gt;<\/code> block. Three smaller deltas round out the model: the <code>unsafe<\/code> type modifier becomes an error, the new <code>safe<\/code> keyword marks <code>extern<\/code> declarations whose safety the compiler can\u2019t classify on its own, and pointer types in signatures no longer propagate unsafety on their own.<\/p>\n<p>We envision a future where C# is among a set of languages chosen and noted for their type- and memory-safety enforcement. With this model change, C#, Rust, and Swift have a more common safety vocabulary and workflow. We imagine teams adopting a complete supply-chain view of their dependencies, whether C# all the way down or C# at the app layer over Rust at the system layer. Our own team has moved large blocks of C++ to C# over the years for exactly this reason: safe C# doesn\u2019t carry a memory-safety review burden.<\/p>\n<p>Once a team moves a subset of a codebase to the new safety model, there will likely be increased motivation to move all of it and its dependencies. This may be easier than it seems for many dev shops. The new model maintains C# largely as-is and tweaks the <code>unsafe<\/code> patterns that most developers do not touch, while significantly improving the overall safety capability and posture of the language. We believe that this feature is among the highest-leverage changes that we can make to improve developer confidence in this new era of coding.<\/p>\n<p>This project benefits from contributions from: <a href=\"https:\/\/github.com\/agocke\">Andy Gocke<\/a>, <a href=\"https:\/\/github.com\/EgorBo\">Egor Bogatov<\/a>, <a href=\"https:\/\/github.com\/333fred\">Fred Silberberg<\/a>, <a href=\"https:\/\/github.com\/jjonescz\">Jan Jones<\/a>, <a href=\"https:\/\/github.com\/jkotas\">Jan Kotas<\/a>, <a href=\"https:\/\/github.com\/jcouv\">Julien Couvreur<\/a>, <a href=\"https:\/\/github.com\/MadsTorgersen\">Mads Torgersen<\/a>, <a href=\"https:\/\/github.com\/richlander\">Rich Lander<\/a>, <a href=\"https:\/\/github.com\/tannergooding\">Tanner Gooding<\/a>, and others.<\/p>\n<p>The post <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/improving-csharp-memory-safety\/\">Improving C# Memory Safety<\/a> appeared first on <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\">.NET Blog<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>We\u2019re in the process of significantly improving memory safety in C#. The unsafe keyword is being redesigned to inform callers [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":94,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[7],"tags":[],"class_list":["post-4127","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/4127","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=4127"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/4127\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media\/94"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=4127"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=4127"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=4127"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}