Skip to content

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

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

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) and disabled by default.
  • Source-Generators shoud not break compilation for unsafe-v1

Legend: 🔍 analyzer, 🔧 code fixer, ⚙️ source generator.

1. Declaring contracts on members

2. Unsafe contexts at call sites

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

  • 🔧 IntroduceUnsafeContextCodeFixProvider: fixes CS9360, CS9361, CS9362, CS9363 and CS9376. Prefers unsafe { } around the statement, splits a local declaration into T x; unsafe { x = init; } so the local stays visible, and falls back to unsafe(...) 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 IntroduceUnsafeContextCodeFixProvider for unsafe-v2 call sites #131581)

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.
  • 🔧 [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)

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.

  • 🔍 UnsafeBlockMissingSafetyCommentAnalyzer (IL5009): an unsafe { } block or unsafe(...) expression with no // SAFETY: comment. Add safety documentation analyzers #131484
  • 🔧 AddSafetyCommentCodeFixProvider: fixes IL5009 by inserting a // SAFETY: TODO stub, matching the /* SAFETY: Audit */ convention already used by the other fixers. Add safety documentation analyzers #131484
  • 🔍 SafeModifierMissingJustificationAnalyzer (IL5010): an explicit safe modifier with no <safety> documentation. This is the symmetric hole to IL5005, since safe is a hand-written assertion the compiler cannot verify. It applies wherever safe can appear: extern members and [LibraryImport] methods, fields and field-backed members in [StructLayout(LayoutKind.Explicit)] and [ExtendedLayout] types (where safe asserts the overlap cannot be used to type-pun, for example aliasing a reference with an integer), and any other declaration once dotnet/roslyn#84602 allows safe as a no-op. No fixer, a human must write the justification. Add safety documentation analyzers #131484

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.

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.
  • 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
CS9364-CS9366 Roslyn An unsafe member cannot override or implement a safe member
CS9377, CS0106 Roslyn unsafe is meaningless or invalid here
CS9388, CS9389, CS9392 Roslyn Explicit safe/unsafe required, or safe used in an invalid position
CS0764, CS9390 Roslyn Partial declarations disagree on unsafe or safe
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 unsafe block has no // SAFETY: comment
IL5010 proposed 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" were added with GitHub Copilot.

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