Skip to content

[Tracking] Unsafe-v2 migration tooling (analyzers and code fixers) #131451

Open

Description

@EgorBo

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 ILxxxx diagnostic 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.targets maps it to the updated-memory-safety-rules feature flag until that compiler flows in. AllowUnsafeBlocks stays the library's own decision: a library without unsafe operations needs neither it nor any safe/unsafe annotations. The ref projects and System.Text.Json tests that used the feature flag directly now use the property.
  • 🏗️ EnableUnsafeMigrationAnalyzers=true enables IL5005-IL5010 (eng/unsafeMigration.globalconfig); they are disabled by default and dotnet format --diagnostics does not enable them.
  • 🏗️ UnsafeMigrationProject/UnsafeMigrationTargetFramework narrow the migrated project to one target framework. dotnet format -f can't be used: it sets TargetFramework globally, 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 under dotnet format a single diagnostic the fixer deliberately leaves alone (e.g. IL5005 on 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 for IModuleSymbol.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 (RSEXPERIMENTAL006 is suppressed) and the shim can be deleted.

1. Declaring contracts on members

2. Unsafe contexts at call sites

Highest volume by far, since every consumer of a newly-unsafe API breaks.

3. Contract consistency

  • 🔧 SynchronizeUnsafeContractCodeFixProvider: fixes CS9364, CS9365 and CS9366 (derived is unsafe, base is safe) with two actions: mark the base member unsafe when it is in source, or drop unsafe from the derived member. Add unsafe contract consistency code fixes #131454
  • 🔧 MatchPartialSafetyModifierCodeFixProvider: fixes CS0764 and CS9390 by 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 is unsafe. 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: fixes IL5008 by adding unsafe.

4. Cleaning up legacy unsafe

  • 🔧 RemoveInvalidUnsafeCodeFixProvider: fixes CS9377 and unsafe-specific CS0106 by removing an unsafe modifier that is now meaningless or invalid, for example on a type declaration. (Add unsafe modifier migration code fixer #131002)
  • 🔍 UnsafeMemberMissingSafetyDocumentationAnalyzer (IL5005)
  • 🔧 RemoveUndocumentedUnsafeCodeFixProvider: removes an undocumented unsafe modifier when it was an unsafe-v1 lexical scope rather than an intentional caller-unsafe contract. (Add unsafe modifier migration code fixer #131002)
    • Keeps unsafe on pointer signatures, on extern and [LibraryImport] methods (removing it oscillated with the CS9389/SYSLIB1064 fixers), on CS9392 fields, and on members whose inactive #if branches contain pointer operations: those may rely on the modifier's lexical context in another configuration or in another assembly compiling the same file (Marvin.cs broke CoreLib this way). (Unsafe-v2: fix migration tooling and add MemorySafetyRulesVersion enrollment #136621)

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:

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.)

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 were stackalloc under the assembly-wide [SkipLocalsInit] (CS9361). The only manual change was AllowUnsafeBlocks in Microsoft.Extensions.Configuration.EnvironmentVariables, which compiles the shared ValueStringBuilder.
  • System.IO.Compression (all 5 target frameworks): deterministic (identical output across runs), about 100 unsafe contexts, every [LibraryImport] conservatively unsafe, no requires-unsafe public surface.
  • Shared sources (Common/, CoreLib) are edited from whichever assembly is migrated, so their other consumers (other #if branches, legacy rules, no AllowUnsafeBlocks) have to be built; the driver lists them.

Blocked / external

  • dotnet/roslyn#84602: allow safe on non-extern members. Until it merges, safe cannot be spelled on a [LibraryImport] whose generated implementation is a wrapper.
  • dotnet/roslyn#84616: allow await in an unsafe context. Gates how aggressively the call-site fixer can use unsafe blocks rather than unsafe(...) expressions in async code.
  • dotnet/roslyn#82546: public API for the memory-safety-rules version. Until then, tooling detects the opt-in through the updated-memory-safety-rules feature flag. Added as IModuleSymbol.MemorySafetyRulesVersion (dotnet/roslyn#84860, experimental RSEXPERIMENTAL006); tooling reaches it through MemorySafetyRulesPolyfills until our Roslyn reference has it. (Unsafe-v2: fix migration tooling and add MemorySafetyRulesVersion enrollment #136621)
  • Consume the compiler with dotnet/roslyn#85980 (MemorySafetyRulesVersion MSBuild property and -memorysafetyrulesversion), then delete the feature-flag mapping in eng/memorySafetyRules.targets. It has to happen together with dotnet/roslyn#86224 (open), which makes the updated-memory-safety-rules flag a warning, i.e. an error with TreatWarningsAsErrors.
  • New errors in the next compiler update (our toolset 5.12.0-1.26471.108 and 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; UnsafeContextCodeFixProvider handles it.
    • CS9401 (dotnet/roslyn#85784): safe/unsafe modifiers require AllowUnsafeBlocks (previously only unsafe did).
    • CS9392 extended to Pack/Size layouts (dotnet/roslyn#85921).
  • dotnet format quirks (dotnet/sdk); the driver works around them.
    • It only computes compiler (CS) diagnostics for a project when some analyzer passes the --diagnostics filter, so a compiler-only pass silently does nothing. The driver adds IL5007, which never reports under the updated rules.
    • -f sets TargetFramework globally, 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.
  • GenAPI does not round-trip the safety contract. It infers unsafe in reference source from pointer types in the signature rather than from the declaration, so a hand-removed unsafe or an added safe is lost on the next regeneration. Harmless before the opt-in, since unsafe has no metadata representation and nothing ApiCompat compares can disagree. Once an assembly opts in, unsafe becomes RequiresUnsafeAttribute in 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 safe contextual keyword and asks for it ahead of the accessibility modifier, which is not how it orders unsafe. Disabled repo-wide in eng/CodeAnalysis.src.globalconfig as 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 MemorySafetyRulesAttribute and emits RequiresUnsafeAttribute for its unsafe members, 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 proposed This effort unsafe block has no // SAFETY: comment
IL5010 proposed This effort 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.

Activity

  1. dotnet-policy-service commented on Jul 28, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @agocke, @dotnet/illink
    See info in area-owners.md if you want to be subscribed.

  2. added this to the 12.0.0 milestone on Jul 28, 2026
  3. removed
    untriagedNew issue has not been triaged by the area owner
    on Jul 28, 2026
  4. self-assigned this
    on Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area-Tools-ILLink.NET linker development as well as trimming analyzers

Type

No type

Projects

  • Status
    No status

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions