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

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.

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 by wrapping the statement in unsafe { /* SAFETY: Audit */ }, or the expression in unsafe(/* SAFETY: Audit */ ...) where a block is not valid syntax or would change meaning.

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.
  • 🔧 AddSafetyCommentCodeFixProvider: fixes IL5009 by inserting a // SAFETY: TODO stub, matching the /* SAFETY: Audit */ convention already used by the other fixers.
  • 🔍 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.

6. Source generator output

The common root cause for the unfixed generators below is ContainingSyntaxContext.WrapMembersInContainingSyntaxWithUnsafeModifier and WriteToWithUnsafeModifier, which stamp unsafe onto generated type declarations. Under unsafe-v2 that is an error, and it no longer establishes an unsafe context for the members inside, so generated bodies that dereference pointers need explicit unsafe blocks instead.

  • ⚙️ LibraryImportGenerator: requires an explicit contract on the user-facing method (SYSLIB1064), keeps the hidden __PInvoke caller-unsafe, and wraps generated bodies in explicit unsafe blocks instead of relying on a type-level modifier. (Support unsafe evolution in LibraryImportGenerator #131245)
  • ⚙️ ComInterfaceGenerator and ComClassGenerator: both use the type-level helper above. [GeneratedComInterface] and [GeneratedComClass] are public API shipping since .NET 8, so this is real user impact. Needs the same treatment as [LibraryImport]. Note that the generated members mostly live in file-scoped types the user cannot name, so this is largely a codegen fix rather than an analyzer one; the open design question is whether [GeneratedComInterface] methods should also require an explicit safe/unsafe contract the way [LibraryImport] methods now do.
  • ⚙️ JSImportGenerator and JSExportGenerator: both use the type-level helper, and JSExportGenerator additionally stamps unsafe directly onto a generated ClassDeclaration. Neither ever emits an unsafe block, so every generated body relies on the type or method modifier for its unsafe context while taking JSMarshalerArgument* parameters. [JSImport] and [JSExport] are public API shipping since .NET 7.
  • ⚙️ VtableIndexStubGenerator: shares the same helper. [VirtualMethodIndex] is a test asset rather than public API, so there is no external impact, but fixing the helper covers it anyway.

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.

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

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