Support the updated memory safety rules (C# 15 / .NET 11) - #1819
Draft
Gérald Barré (meziantou) wants to merge 2 commits into
Draft
Support the updated memory safety rules (C# 15 / .NET 11)#1819Gérald Barré (meziantou) wants to merge 2 commits into
Gérald Barré (meziantou) wants to merge 2 commits into
Conversation
C# 15 and .NET 11 preview an updated memory safety model in which `unsafe` on a member is a contract that propagates an audit obligation to callers rather than a marker that pointer syntax appears nearby. A project opts in with the `updated-memory-safety-rules` compiler feature flag, and under those rules the code we generate today no longer compiles: `unsafe` on a type or delegate declaration is an error, every `extern` member and every instance field of an explicitly laid out type must declare its safety, the modifier on a signature no longer establishes an unsafe context for the body, and calling an `unsafe` member requires a context at the call site. Detect the feature flag in the parse options — the only signal available while the feature is in preview, since it has neither an MSBuild property nor a public Roslyn API — and adapt the emitted code accordingly. The adaptation is a post-processing pass over the final syntax trees, which keeps the rules in one place and covers the hand-written templates along with everything the generator composes. When the flag is off, output is unchanged. A member carries `unsafe` if and only if its signature mentions a pointer. That matches the obligation the compiler already infers for a pointer-bearing signature under LangVersion=preview without the flag, so a call site's obligations don't change with the opt-in, and it keeps the modifier off of overrides and interface implementations of safe members such as ToString and SafeHandle.ReleaseHandle, which the updated rules would otherwise reject. Constructors are excluded: nothing can discharge a caller-unsafe obligation at a `this(…)`/`base(…)` initializer, and the projection's pointer-taking constructors only store the pointer, which the updated model treats as safe. Bodies get an inner `unsafe` block, and the positions where a block can't appear — field and property initializers, constructor-initializer arguments — get an `unsafe(…)` expression. This is applied to every member rather than only the ones that were in an unsafe context before, because deciding which bodies truly need a context requires semantic information that isn't available while the code is being generated. A redundant block or expression produces no diagnostic, and .NET 11 marks runtime members the projection depends on (System.Runtime.CompilerServices.Unsafe, MemoryMarshal.CreateSpan) as caller-unsafe, so members that were entirely safe under the original model need a context now too. Plumb the MSBuild $(Features) property through the build task and the command line generator so the build-task path sees the same opt-in the source generator does. Verified by generating ~1.1M lines across roughly 30 namespaces and compiling them with the .NET 11 RC 1 compiler with the flag set: zero diagnostics, identical to the baseline without the flag. Also verified for unmarshalled COM, public and internal projections, pointer overloads, preserveSig, an extension receiver, and net472 references. One combination still fails, and not from what we emit: the SDK's own COM interop source generator emits `unsafe` on the interfaces it generates, which the updated rules reject, and the error surfaces on every partial declaration of those types including ours. This reproduces with a hand-written [GeneratedComInterface] and no CsWin32 involved, so it has to be fixed in the SDK. Documented, along with the workaround of turning off comInterop.useComSourceGenerators.
CommandLineBuilder decides for itself whether a switch value needs quoting — a semicolon-separated list gets quoted, a single feature name doesn't — so asserting that the value appears verbatim was wrong. Match either form instead: what the test is about is that the task forwards $(Features) to the generator, not MSBuild's quoting rules. The plumbing itself was correct. A quoted list arrives as a single argument, and the generator splits it on semicolons.
Gérald Barré (meziantou)
force-pushed
the
feature/dotnet-11-unsafe-model-bc4ba4
branch
from
September 10, 2026 05:21
60a9869 to
322df84
Compare
Member
|
Jeremy Koritzinsky (@jkoritzinsky) Aaron R Robinson (@AaronRobinsonMSFT) The call-out that COM source generators are broken with new safety rules, are you aware of this? Gérald Barré (@meziantou) can you file an issue on https://github.com/dotnet/runtime ? |
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds support for the updated memory safety model that is in preview in C# 15 / .NET 11, so that the generated projection compiles without new errors or warnings when a project opts in.
A project opts in with a compiler feature flag — there is no MSBuild property or public Roslyn API for it while the feature is in preview:
When the flag is off, generated output is unchanged.
Why
The updated model splits
unsafeinto two roles: a contract that propagates an audit obligation to callers, and a block that scopes the operations which actually access unmanaged memory. Under those rules, the code CsWin32 emits today does not compile. Verified against the .NET 11 RC 1 compiler:unsafeon a type or delegate declarationunsafe partial struct,unsafe interface,unsafe delegateextern/LibraryImportmember must beunsafeorsafeLocalExternFunctionlocal functionsunsafemember needs an unsafe contextSystem.Runtime.CompilerServices.UnsafeandMemoryMarshal.CreateSpancaller-unsafe, so members that were entirely safe before need a context nowunsafemember may not override or implement a safe oneToString,Equals,SafeHandle.ReleaseHandleHow
Generator.MemorySafetyRewriteris a post-processing pass over the final syntax trees, applied inGetCompilationUnitsahead of the whitespace rewriter. Keeping the rules in one place also means the hand-writtentemplates/are covered by the same logic as everything the generator composes.The rules:
unsafefrom type and delegate declarations, and from fields. Holding a pointer is a safe operation under the updated model, so a pointer-typed field needs no modifier — and leaving one off keeps the field readable and writable from safe code, exactly as it is today.safe.extern-like memberunsafewhen its signature mentions a pointer,safeotherwise.unsafeif and only if its signature mentions a pointer.unsafeblock for bodies (converting expression bodies to block bodies), and anunsafe(…)expression for the positions where a block cannot appear — field and property initializers, constructor-initializer arguments.$(Features)is plumbed throughCsWin32CodeGeneratorTaskand the command line generator so the build-task path sees the same opt-in the source generator does.Notes for reviewers
Three decisions are worth a look:
unsafe, making them caller-unsafe. This matches the obligation the compiler already infers for a pointer-bearing signature underLangVersion=previewwithout the flag (CS9363), so a call site's obligations do not change when the flag is flipped. It also keeps the modifier off of overrides and interface implementations of safe members, whose signatures never mention pointers. The alternative — marking every externsafe— would be a false attestation for a pointer-taking P/Invoke.this(…)/base(…)initializer: the initializer call sits outside any block, and anunsafe(…)expression can only cover the arguments. A caller-unsafe constructor would be unreachable from a chained one. The projection's pointer-taking constructors only store the pointer, which the updated model treats as safe anyway.unsafeblock or expression produces no diagnostic, and because .NET 11 marks runtime members the projection depends on as caller-unsafe, members that were entirely safe under the original model need a context too. The cost is some extra nesting in generated code.safeandunsafe(…)are emitted as an identifier token and an invocation respectively, because the Roslyn versions this generator builds against predate C# 15 and have no syntax nodes for them. Only the generated text matters — the C# 15 compiler that consumes it parses them correctly.Verification
Generated with the flag set and compiled with the .NET 11 RC 1 compiler:
preserveSig, an extension receiver, and net472 reference assemblies. Identical in every case.Microsoft.Windows.CsWin32.Testsand 245 inCsWin32Generator.Tests, plus theFullGeneration_Net9regression test.UpdatedMemorySafetyRulesTests. They assert on generated text rather than compiling it, because the Roslyn version the test project builds against predates C# 15 and can neither parsesafe/unsafe(…)nor enforce the rules.Known limitation: the SDK's COM interop generator
With CsWin32's COM source generators enabled, the build still fails — but not because of what CsWin32 emits. The SDK's own
Microsoft.Interop.ComInterfaceGeneratoremitsunsafeon the interfaces it generates, which CS9377 rejects, and the error surfaces on every partial declaration of those types, including the half CsWin32 emits. It also emitsunsafeimplementations of inherited members that resolve as safe (CS9366).This reproduces with a hand-written
[GeneratedComInterface]and no CsWin32 involved:so it has to be fixed in the SDK. Documented in
getting-started.mdalong with the workaround: setcomInterop.useComSourceGenerators(orallowMarshaling) tofalsewhen opting into the updated rules.Not covered here
CsWin32Generator.BuildTasks.Teststargets net472, so the two new command-line assertions there compile but were not run in this environment.