Repository navigation
[Tracking] Unsafe-v2 migration tooling (analyzers and code fixers) #131451
Description
Tracking issue for the analyzers and code fixers that help migrate a code base to the updated memory safety rules (unsafe-v2).
Migration workflow: https://gist.github.com/EgorBo/1ed6752b3430a181240dcee7dace7070
Migration driver (runs the workflow through dotnet format): https://gist.github.com/EgorBo/456f9e7d3680a5f9c8e54a168a907187
Design goals carried over from #131002:
- Tooling must be idempotent, since migration happens incrementally.
- Code fixers should lean on existing Roslyn diagnostics wherever possible, and only add an
ILxxxxdiagnostic where the language deliberately does not require something. - Everything here is currently non-shipping (
#if DEBUG#if UNSAFE_MIGRATION, built in every configuration but Release, so Checked has it too, Unsafe-v2: fix migration tooling and add MemorySafetyRulesVersion enrollment #136621) and disabled by default. - Source-Generators shoud not break compilation for unsafe-v1
Legend: 🔍 analyzer, 🔧 code fixer, ⚙️ source generator, 🏗️ infrastructure.
0. Enrolling an assembly (#136621)
- 🏗️
<MemorySafetyRulesVersion>2</MemorySafetyRulesVersion>in the project file opts an assembly in. It is the compiler's own property (dotnet/roslyn#85980);eng/memorySafetyRules.targetsmaps it to theupdated-memory-safety-rulesfeature flag until that compiler flows in.AllowUnsafeBlocksstays the library's own decision: a library without unsafe operations needs neither it nor anysafe/unsafeannotations. The ref projects and System.Text.Json tests that used the feature flag directly now use the property. - 🏗️
EnableUnsafeMigrationAnalyzers=trueenablesIL5005-IL5010(eng/unsafeMigration.globalconfig); they are disabled by default anddotnet format --diagnosticsdoes not enable them. - 🏗️
UnsafeMigrationProject/UnsafeMigrationTargetFrameworknarrow the migrated project to one target framework.dotnet format -fcan't be used: it setsTargetFrameworkglobally, retargeting every referenced project too. - 🏗️
UnsafeMigrationFixAllProvider: every migration fixer applies each diagnostic's own fix. The batch fixer takes the equivalence key from the first registered action of one arbitrary diagnostic, so underdotnet formata single diagnostic the fixer deliberately leaves alone (e.g.IL5005on a pointer signature) turned the whole pass into a silent no-op, and results depended on diagnostic order. - 🏗️
MemorySafetyRulesPolyfills(Common/src/Roslyn): one extension property standing in forIModuleSymbol.MemorySafetyRulesVersion(reflection, with the feature flag as fallback) replaces three reflection-based copies in the ILLink analyzers, the interop generators and the System.Text.Json generator. Once our Roslyn reference has the API, call sites bind to it (RSEXPERIMENTAL006is suppressed) and the shim can be deleted.
1. Declaring contracts on members
- 🔧
AddUnsafeToExternCodeFixProvider: fixesCS9389by marking anexternmemberunsafe. (Add unsafe modifier migration code fixer #131002) - 🔧
AddUnsafeToFieldCodeFixProvider: fixesCS9392for fields, field-backed properties and field-like events in explicit/extended-layout types. (Add unsafe modifier migration code fixer #131002)CS9392now also covers[StructLayout]with a nonzeroPackorSize(dotnet/roslyn#85921); the fixer keys on the diagnostic so it follows automatically, andIL5005knows the new rule (Unsafe-v2: fix migration tooling and add MemorySafetyRulesVersion enrollment #136621). - 🔍
PointerSignatureRequiresUnsafeAnalyzer(IL5006) - 🔧
AddUnsafeToPointerSignatureCodeFixProvider: pointer and function-pointer signatures that lost the caller-unsafe contract they had under unsafe-v1. (Add unsafe modifier migration code fixer #131002) - 🔍
LibraryImportRequiresExplicitSafetyAnalyzer(IL5007) - 🔧
AddUnsafeToLibraryImportCodeFixProvider:[LibraryImport]methods with no explicit contract. (Support unsafe evolution in LibraryImportGenerator #131245)
2. Unsafe contexts at call sites
Highest volume by far, since every consumer of a newly-unsafe API breaks.
- 🔧
IntroduceUnsafeContextCodeFixProviderUnsafeContextCodeFixProvider: fixesCS9360,CS9361,CS9362,CS9363andCS9376. Prefersunsafe { }around the statement, splits a local declaration intoT x; unsafe { x = init; }so the local stays visible, and falls back tounsafe(...)where a block would be invalid syntax or would shorten a scope. A body that would need three or more separate regions is wrapped whole when every use site is fixed at once. (Add a code fixer that introduces inner unsafe contexts for unsafe-v2 diagnostics #135341)- Also fixes
CS9402, address of an unassigned variable outside an unsafe context (dotnet/roslyn#86017), and also runs under the legacy rules, which report the same diagnostics for pointer operations, so other consumers of a shared file can be repaired. (Unsafe-v2: fix migration tooling and add MemorySafetyRulesVersion enrollment #136621)
- Also fixes
3. Contract consistency
- 🔧
SynchronizeUnsafeContractCodeFixProvider: fixesCS9364,CS9365andCS9366(derived isunsafe, base is safe) with two actions: mark the base memberunsafewhen it is in source, or dropunsafefrom the derived member. Add unsafe contract consistency code fixes #131454dotnet formatapplies the first registered action, which narrows the derived member: it removesunsafe, or replaces it withsafeon anextern. Both now share one equivalence key, so one pass applies whichever each member needs. (Unsafe-v2: fix migration tooling and add MemorySafetyRulesVersion enrollment #136621)
- 🔧
MatchPartialSafetyModifierCodeFixProvider: fixesCS0764andCS9390by copying the safety modifier to the other part. Relevant because source generators author one half of those partials. Add unsafe contract consistency code fixes #131454 - 🔍 [TBD]
UnsafeContractNotPropagatedAnalyzer(IL5008): an override or interface implementation left unannotated while the member it overrides or implements isunsafe. Needs its own diagnostic because narrowing to safe is legal, so the compiler is silent and migration silently drops the obligation. (No hits in the System.Text.Json and System.IO.Compression trials.) - 🔧 [TBD]
PropagateUnsafeContractCodeFixProvider: fixesIL5008by addingunsafe.
4. Cleaning up legacy unsafe
- 🔧
RemoveInvalidUnsafeCodeFixProvider: fixesCS9377and unsafe-specificCS0106by removing anunsafemodifier that is now meaningless or invalid, for example on a type declaration. (Add unsafe modifier migration code fixer #131002) - 🔍
UnsafeMemberMissingSafetyDocumentationAnalyzer(IL5005) - 🔧
RemoveUndocumentedUnsafeCodeFixProvider: removes an undocumentedunsafemodifier when it was an unsafe-v1 lexical scope rather than an intentional caller-unsafe contract. (Add unsafe modifier migration code fixer #131002)- Keeps
unsafeon pointer signatures, onexternand[LibraryImport]methods (removing it oscillated with theCS9389/SYSLIB1064fixers), onCS9392fields, and on members whose inactive#ifbranches contain pointer operations: those may rely on the modifier's lexical context in another configuration or in another assembly compiling the same file (Marvin.csbroke CoreLib this way). (Unsafe-v2: fix migration tooling and add MemorySafetyRulesVersion enrollment #136621)
- Keeps
5. Safety documentation
IL5005 covers signatures, but nothing covers bodies and nothing covers safe. The speclet recommends Rust-style // SAFETY comments, and LDM 2026-05-27 left "compiler or analyzer?" open.
dotnet/roslyn#85842 does not cover this: it makes IDE0380 (unnecessary unsafe modifier) skip members documented with <safety>, and offers an "Add <safety> documentation" fix, but it never looks at unsafe blocks, unsafe(...) expressions or safe. It is not a replacement for IL5005 either: it skips declarations with errors (the migration runs on a red build), only checks the active compilation (inactive #if code), and under v2 calls pointer-signature and [LibraryImport] contracts "unnecessary". It ships in the CodeStyle package, which we'd have to bump to get it.
#131484 was closed in favor of the Roslyn analyzer; restored in #136621:
- 🔍
UnsafeBlockMissingSafetyCommentAnalyzer(IL5009): anunsafe { }block orunsafe(...)expression with no// SAFETY:comment.Add safety documentation analyzers #131484Unsafe-v2: fix migration tooling and add MemorySafetyRulesVersion enrollment #136621. An inline/* SAFETY: */comment right beforeunsafe(counts too. - 🔧
AddSafetyCommentCodeFixProvider: fixesIL5009by inserting a// SAFETY: TODOstub, matching the/* SAFETY: Audit */convention already used by the other fixers// SAFETY: To be auditedplaceholder, the same oneUnsafeContextCodeFixProviderwrites, so later passes treat the region as unaudited.Add safety documentation analyzers #131484Unsafe-v2: fix migration tooling and add MemorySafetyRulesVersion enrollment #136621 - 🔍
SafeModifierMissingJustificationAnalyzer(IL5010): an explicitsafemodifier with no<safety>documentation. This is the symmetric hole toIL5005, sincesafeis a hand-written assertion the compiler cannot verify. It applies whereversafecan appear:externmembers and[LibraryImport]methods, fields and field-backed members in[StructLayout(LayoutKind.Explicit)]and[ExtendedLayout]types (wheresafeasserts the overlap cannot be used to type-pun, for example aliasing a reference with an integer), and any other declaration once dotnet/roslyn#84602 allowssafeas a no-op. No fixer, a human must write the justification.Add safety documentation analyzers #131484Unsafe-v2: fix migration tooling and add MemorySafetyRulesVersion enrollment #136621
6. Source generator output
The common root cause for the unfixed generators below is that every generated body relied on an unsafe modifier stamped onto the generated type declaration. Under unsafe-v2 a type modifier establishes no unsafe context for the members inside it, so generated bodies that dereference pointers need explicit unsafe blocks instead. (The type modifier itself is only a suppressed warning, CS9377, and has to stay: under unsafe-v1 it is what makes a pointer legal to name in a stub whose modifiers are copied from a user declaration carrying unsafe on the type rather than on the member.)
- ⚙️
LibraryImportGenerator: requires an explicit contract on the user-facing method (SYSLIB1064), keeps the hidden__PInvokecaller-unsafe, and wraps generated bodies in explicitunsafeblocks instead of relying on a type-level modifier. (Support unsafe evolution in LibraryImportGenerator #131245) - ⚙️
ComInterfaceGeneratorandComClassGenerator: generated bodies now open their ownunsafeblocks, and a shadowing member forwarding to a caller-unsafe base member is emittedunsafewith a block body so it does not hitCS9366/CS9362.[GeneratedComInterface]and[GeneratedComClass]are public API shipping since .NET 8, so this was real user impact. The open design question is untouched: whether[GeneratedComInterface]methods should also require an explicitsafe/unsafecontract the way[LibraryImport]methods now do. (Support unsafe evolution in ComInterface, ComClass, JSImport, JSExport and VtableIndexStub generators #131701) - ⚙️
JSImportGeneratorandJSExportGenerator: generated bodies now open their ownunsafeblocks, and the generated__GeneratedInitializerclass drops itsunsafemodifier since none of its members names a pointer type. The[JSImport]bodies name no pointer type today but are wrapped anyway, because they callUnsafe.SkipInit, which [API Proposal]: Mark Unsafe as caller-unsafe #126956 makes caller-unsafe. (Support unsafe evolution in ComInterface, ComClass, JSImport, JSExport and VtableIndexStub generators #131701) - ⚙️
VtableIndexStubGenerator:PopulateUnmanagedVirtualMethodTablewraps its body.[VirtualMethodIndex]is a test asset rather than public API, so there is no external impact. (Support unsafe evolution in ComInterface, ComClass, JSImport, JSExport and VtableIndexStub generators #131701) - ⚙️ Opt-in detection: the interop and System.Text.Json generators
read theuseupdated-memory-safety-rulesfeature flagIModuleSymbol.MemorySafetyRulesVersionthroughMemorySafetyRulesPolyfills, so they keep working once projects opt in throughMemorySafetyRulesVersion. (Unsafe-v2: fix migration tooling and add MemorySafetyRulesVersion enrollment #136621)
Verified clean under updated-memory-safety-rules, no work needed: RegexGenerator (emits [SkipLocalsInit] but no stackalloc, so the tightened stackalloc rule does not apply), JsonSourceGenerator, and the Microsoft.Extensions.* and System.Private.CoreLib generators.
7. Trial migrations
Run with the driver, then reverted from #136621 to keep it infrastructure-only:
- System.Text.Json (all 5 target frameworks): fully automatic,
MemorySafetyRules(2), no requires-unsafe members. 135 of the 163 sites werestackallocunder the assembly-wide[SkipLocalsInit](CS9361). The only manual change wasAllowUnsafeBlocksinMicrosoft.Extensions.Configuration.EnvironmentVariables, which compiles the sharedValueStringBuilder. - System.IO.Compression (all 5 target frameworks): deterministic (identical output across runs), about 100 unsafe contexts, every
[LibraryImport]conservativelyunsafe, no requires-unsafe public surface. - Shared sources (
Common/, CoreLib) are edited from whichever assembly is migrated, so their other consumers (other#ifbranches, legacy rules, noAllowUnsafeBlocks) have to be built; the driver lists them.
Blocked / external
- dotnet/roslyn#84602: allow
safeon non-externmembers. Until it merges,safecannot be spelled on a[LibraryImport]whose generated implementation is a wrapper. - dotnet/roslyn#84616: allow
awaitin anunsafecontext. Gates how aggressively the call-site fixer can useunsafeblocks rather thanunsafe(...)expressions in async code. - dotnet/roslyn#82546: public API for the memory-safety-rules version.
Until then, tooling detects the opt-in through theAdded asupdated-memory-safety-rulesfeature flag.IModuleSymbol.MemorySafetyRulesVersion(dotnet/roslyn#84860, experimentalRSEXPERIMENTAL006); tooling reaches it throughMemorySafetyRulesPolyfillsuntil our Roslyn reference has it. (Unsafe-v2: fix migration tooling and add MemorySafetyRulesVersion enrollment #136621) - Consume the compiler with dotnet/roslyn#85980 (
MemorySafetyRulesVersionMSBuild property and-memorysafetyrulesversion), then delete the feature-flag mapping ineng/memorySafetyRules.targets. It has to happen together with dotnet/roslyn#86224 (open), which makes theupdated-memory-safety-rulesflag a warning, i.e. an error withTreatWarningsAsErrors. - New errors in the next compiler update (our toolset
5.12.0-1.26471.108and the SDK predate them):CS9402(dotnet/roslyn#86017): address of an unassigned variable outside an unsafe context. It is gated on the language version, not on the opt-in, so legacy-rules code breaks too;UnsafeContextCodeFixProviderhandles it.CS9401(dotnet/roslyn#85784):safe/unsafemodifiers requireAllowUnsafeBlocks(previously onlyunsafedid).CS9392extended toPack/Sizelayouts (dotnet/roslyn#85921).
-
dotnet formatquirks (dotnet/sdk); the driver works around them.- It only computes compiler (
CS) diagnostics for a project when some analyzer passes the--diagnosticsfilter, so a compiler-only pass silently does nothing. The driver addsIL5007, which never reports under the updated rules. -fsetsTargetFrameworkglobally, retargeting every referenced project (fails for platform-specific TFMs).- It exits successfully when the project fails to load ("Required references did not load").
- "Formatted N files" counts files it touched without changing.
- It only computes compiler (
- GenAPI does not round-trip the safety contract. It infers
unsafein reference source from pointer types in the signature rather than from the declaration, so a hand-removedunsafeor an addedsafeis lost on the next regeneration. Harmless before the opt-in, sinceunsafehas no metadata representation and nothing ApiCompat compares can disagree. Once an assembly opts in,unsafebecomesRequiresUnsafeAttributein metadata and the reference assembly has to agree with the implementation about which members are caller-unsafe, so GenAPI needs to emit the modifier from metadata and ApiCompat needs to compare it. GenAPI lives in dotnet/dotnet. - StyleCop SA1206 does not know the
safecontextual keyword and asks for it ahead of the accessibility modifier, which is not how it ordersunsafe. Disabled repo-wide ineng/CodeAnalysis.src.globalconfigas a stopgap (Audit CoreLib for members that stay safe under unsafe-v2 #131719); needs a fix in DotNetAnalyzers/StyleCopAnalyzers before the rule can be turned back on. - ApiCompat does not compare the safety contract. A reference assembly compiled with the updated rules carries
MemorySafetyRulesAttributeand emitsRequiresUnsafeAttributefor itsunsafemembers, while an implementation assembly still on the legacy rules emits neither, so the two disagree in metadata about which members are caller-unsafe. Whether ApiCompat currently objects to that divergence is unverified. It should compare the contract, so that a reference assembly cannot silently promise a contract the implementation does not keep, or drop one it does.
Diagnostic ID map
| ID | Owner | Meaning |
|---|---|---|
CS9360-CS9363, CS9376 |
Roslyn | An unsafe context is required at this use site |
CS9402 |
Roslyn | Address of an unassigned variable outside an unsafe context (dotnet/roslyn#86017, not yet in our compiler) |
CS9364-CS9366 |
Roslyn | An unsafe member cannot override or implement a safe member (renamed from "caller-unsafe" to "requires-unsafe", same IDs) |
CS9377, CS0106 |
Roslyn | unsafe is meaningless or invalid here |
CS9388, CS9389, CS9392 |
Roslyn | Explicit safe/unsafe required, or safe used in an invalid position; CS9392 now also for nonzero Pack/Size |
CS9401 |
Roslyn | safe/unsafe modifiers require AllowUnsafeBlocks (dotnet/roslyn#85784, not yet in our compiler) |
CS0764, CS9390 |
Roslyn | Partial declarations disagree on unsafe or safe |
IDE0380 |
Roslyn (CodeStyle) | Unnecessary unsafe modifier; under v2 skips <safety>-documented members (dotnet/roslyn#85842) |
SYSLIB1064 |
This effort | [LibraryImport] method must be marked safe or unsafe |
IL5005 |
This effort | unsafe member has no <safety> documentation |
IL5006 |
This effort | Pointer signature is missing unsafe |
IL5007 |
This effort | [LibraryImport] has no explicit contract |
IL5008 |
proposed | Unsafe contract not propagated to override or implementation |
IL5009 |
unsafe block has no // SAFETY: comment |
|
IL5010 |
Explicit safe modifier has no justification |
Note on testing: the interop generator test harnesses compile with LanguageVersion.Preview, which already relaxes the pointer-declaration rule without the feature flag. A generator change that breaks unsafe-v1 still passes the entire existing suite, so output has to be verified out-of-band with LangVersion=latest.
Note
The GenAPI, StyleCop, and ApiCompat items under "Blocked / external", and the updates referencing #136621, were added with GitHub Copilot.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
dotnet-policy-service commentedon Jul 28, 2026
Tagging subscribers to this area: @agocke, @dotnet/illink
See info in area-owners.md if you want to be subscribed.
Add unsafe contract consistency code fixes (#131454)
Support unsafe evolution in LibraryImportGenerator (#131245)
Support unsafe evolution in ComInterface, ComClass, JSImport, JSExpor…
Support unsafe evolution in ComInterface, ComClass, JSImport, JSExpor…
Add a code fixer that introduces inner unsafe contexts for unsafe-v2 …
Unsafe-v2: fix migration tooling and add MemorySafetyRulesVersion enr…