Skip to content

LibraryImport generated stubs should be implicitly RequiresUnsafe#125802

Draft
Copilot wants to merge 4 commits intomainfrom
copilot/add-requires-unsafe-attribute
Draft

LibraryImport generated stubs should be implicitly RequiresUnsafe#125802
Copilot wants to merge 4 commits intomainfrom
copilot/add-requires-unsafe-attribute

Conversation

Copy link
Contributor

Copilot AI commented Mar 19, 2026

[LibraryImport] stubs call native code whose signature the compiler cannot validate, making them inherently unsafe. Generated stubs were not annotated with [RequiresUnsafe], causing inconsistency with forwarder stubs (raw [DllImport] extern methods) and making the unsafe nature invisible to tools like ILLink's RequiresUnsafeAnalyzer.

Changes

RequiresUnsafeAttributepublic

  • src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/RequiresUnsafeAttribute.cs: internalpublic so generated user code can reference it
  • src/libraries/System.Runtime/ref/System.Runtime.cs: Added to reference assembly for API compat

Generator infrastructure

  • StubEnvironment: Added RequiresUnsafeAttrType lazy lookup property
  • EnvironmentFlags: Added RequiresUnsafeAvailable = 0x4 flag
  • TypeNames / NameSyntaxes: Added System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute constant and syntax helper

LibraryImportGenerator stub emission

  • CalculateStubInformation: Sets RequiresUnsafeAvailable flag when attribute type is available in the compilation. Skips if the user already applied [RequiresUnsafe] on their declaration (prevents CS0579 duplicate attribute error).
  • GenerateSource: Injects [RequiresUnsafe] into SignatureContext.AdditionalAttributes for regular (marshalling) stubs.
  • PrintForwarderStub: Explicitly adds [RequiresUnsafe] to forwarder (pure DllImport) stubs.

The flag is the single source of truth for both stub paths. Availability is checked at compile time via Compilation.GetTypeByMetadataName, so older TFMs without the attribute are handled gracefully.

Tests

Added RequiresUnsafeAdded and RequiresUnsafeAddedOnForwardingStub to AdditionalAttributesOnStub.cs, verifying the attribute is emitted on both stub kinds with the expected global:: qualified syntax.

Original prompt

This section details on the original issue you should resolve

<issue_title>LibraryImport methods should be implicitly RequiresUnsafe</issue_title>
<issue_description>Methods attributed with LibraryImportAttribute are implicitly unsafe. Although the marshalling logic could be analyzed to be considered memory-safe, the compiler cannot validate that the underlying extern DllImport is targeting a method with a valid signature.

Today, the stubs generated by LibraryImportGenerator will not generate the RequiresUnsafeAttribute on the produced stubs. This causes inconsistency with stubs that have no marshalling (they are just a raw DllImport, which is extern).

The LibraryImportGenerator should add the RequiresUnsafeAttribute to all generated stubs to provide a consistent experience.</issue_description>

Comments on the Issue (you are @copilot in this section)


📱 Kick off Copilot coding agent tasks wherever you are with GitHub Mobile, available on iOS and Android.

@dotnet-policy-service
Copy link
Contributor

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

Copilot AI and others added 2 commits March 19, 2026 22:06
Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com>
…urce

Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com>
@stephentoub
Copy link
Member

What's the rationale behind every [LibraryImport] requiring unsafe?

…for regular stubs

Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com>
Copilot AI changed the title [WIP] Add RequiresUnsafeAttribute to generated LibraryImport methods LibraryImport generated stubs should be implicitly RequiresUnsafe Mar 19, 2026
Copilot AI requested a review from jkoritzinsky March 19, 2026 22:10
@jkoritzinsky
Copy link
Member

LibraryImport methods don't resolve all of the reasons that DllImports are unsafe. Primarily, LibraryImport (like DllImport) cannot guarantee that the provided signature is an accurate match to the target native method.

We could introduce a mechanism to request that a LibraryImport is safe (basically having the user promise that they got it right) but until then LibraryImport doesn't solve all of the reasons that DllImport requires unsafe.

@stephentoub
Copy link
Member

stephentoub commented Mar 20, 2026

LibraryImport methods don't resolve all of the reasons that DllImports are unsafe.

Of course. But this is swinging the pendulum to the other extreme and saying that because we don't know what's on the other end of the FFI, every single consumer must assume every single LibraryImport is unsafe.

Is this what was agreed on with @agocke et al?

My expectation is this is going to lead to developers wrapping [LibraryImport]s with yet another stub that serves purely to contain the virality of the [RequiresUnsafe], and such wrapping stubs are what the LibraryImport generator exists to simplify. If we're going to say that by default every [LibraryImport] requires unsafe, then we should add a mechanism that's part of [LibraryImport] to override that default, and we should do that as part of this.

@tannergooding
Copy link
Member

LibraryImport methods don't resolve all of the reasons that DllImports are unsafe

I actually don't think it fundamentally resolves any of the reasons that DllImports are unsafe. At best the generator is introducing some marshalling support (replacing the older built-in marshalling support on DllImport) and so it is not actually introducing any kinds of bounds checking, lifetime management, or other guarantees that would be needed to make them "safe".

That is, they remain equivalent to a DllImport/extern call in essentially every consideration. The one notable exception is that if you create a very custom marshaller, you might be able to do API specific extended validation, but that's a lot of additional work for something where the wrapper makes it much more clear.

My expectation is this is going to lead to developers wrapping [LibraryImport]s with yet another stub that serves purely to contain the virality of the [RequiresUnsafe], and such wrapping stubs are what the LibraryImport generator exists to simplify. If we're going to say that by default every [LibraryImport] requires unsafe, then we should add a mechanism that's part of [LibraryImport] to override that default, and we should do that as part of this.

I think this is just the expectation of all unsafe code and is why all extern methods, including those that simply call into the runtime like Math.Sin, are unsafe (and so we must introduce wrappers around these APIs -or- change them to recursive calls as well).

We expect the wrapper to exist to make the assertion that there is no memory unsafe operations occurring. That all the potential unsafety of crossing the managed -> unmanaged boundary is handled. Since a LibraryImport can make no more guarantees of this than any other extern method, we should likely treat them the same as well.

@stephentoub
Copy link
Member

stephentoub commented Mar 20, 2026

If I were writing the wrapper that LibraryImport is writing, then I could choose to do so in a way that stops the virality, by using unsafe { ... } inside my method.

But I'm not writing the wrapper, the library import generator is. And there's nothing being exposed that let's me do the equivalent of moving the unsafety into the body. Which means I'm the stuck having to write yet another wrapper around its wrapper.

I'm not sold on the notion that because we don't know what the thing being called is doing it's inherently unsafe. But assuming I buy that premise, we should at least make it trivial to annotate the [LibraryImport] in such a way that the virality is contained so I don't have to write yet another wrapper.

@jkotas
Copy link
Member

jkotas commented Mar 20, 2026

all extern methods, including those that simply call into the runtime like Math.Sin, are unsafe

I do not think that's the plan. FCalls and other similar methods in CoreLib implemented using runtime magic are not going to be implicitly unsafe. We can have analyzers that tell you to mark it as unsafe that you can suppress as needed (or have RequiresUnsafe(false) to explicitly mark the methods as safe).

I think similar strategy can work for DllImport/LibraryImport too.

@tannergooding
Copy link
Member

I do not think that's the plan. FCalls and other similar methods in CoreLib implemented using runtime magic are not going to be implicitly unsafe

The language has implemented it such that all extern methods are unsafe. You can see some of their tests validating this here: https://github.com/dotnet/roslyn/blob/main/src/Compilers/CSharp/Test/CSharp15/UnsafeEvolutionTests.cs#L8004-L8098

That is, at the IL level functionally any method which is not abstract and has a null method body (i.e. is extern). So FCalls and similar will be unsafe and in many cases fundamentally are.

If we wanted a way to annotate that LibraryImport was safe, you would likely have the same expectation for extern calls and for the same reasons.

If I were writing the wrapper that LibraryImport is writing, then I could choose to do so in a way that stops the virality, by using unsafe { ... } inside my method.

The exact same logic applies to DllImport using built-in marshalling, there is no real difference between the two. One is just taking the modern approach of a source generator so we can more easily version it and fix bugs over time.

@stephentoub
Copy link
Member

The exact same logic applies to DllImport using built-in marshalling, there is no real difference between the two.

Then we can have the same switch for DllImport as well. I don't know why the same logic applying to DllImport means we can't have nice things.

One is just taking the modern approach

Which is why I'm highlighting it. It's the thing we tell developers to use now, so it's the thing we should focus on ensuring has a good experience.

@jkotas
Copy link
Member

jkotas commented Mar 20, 2026

The language has implemented it such that all extern methods are unsafe. You can see some of their tests validating this here: https://github.com/dotnet/roslyn/blob/main/src/Compilers/CSharp/Test/CSharp15/UnsafeEvolutionTests.cs#L8004-L8098

@jjonescz I thought that we agreed on that the compiler is just going to look at RequiresUnsafeAttribute only, and it won't try to be smart about extern. Was there a misunderstanding about the plan?

I do not want the runtime to be dealing with the problems created by safe wrappers over extern low-level extern methods that are required just to make the C# compiler happy.

@tannergooding
Copy link
Member

I'm not against being able to mark extern or LibraryImport methods as being "safe" in some way, I just don't think it actually really buys much of anything and I have a learning towards it ultimately being worse.

I think that escaping managed control (which from a user visible perspective is what LibraryImport does) is fundamentally unsafe and that a wrapper (even if it is a wrapper over a wrapper) at least helps give a place for users to document why it is safe without explicit extra validation.

so it's the thing we should focus on ensuring has a good experience.

I'm not convinced there is a "good experience", or at least not an experience that helps lead users to a pit of success here.

In my experience people do not write correct interop bindings regardless of what approach they use. Even tools like cswin32 have regularly gotten things wrong and have to have the various edges called out and fixed. This also applies to the runtime (one of the reasons we eventually created the LibraryImport generator), and other well known tools that are believed to be "robust".

Rather, I find that devs are likely to assume that using string or Span<T> or ref T rather than T* makes their code "safe", when all the wrapper is doing is fixed (T* ptr = &data) { PInvoke(ptr); } or doing other pinning and fixups for cases like struct S { T[] field; }. So I think that allowing them to annotate it as safe is actually a bit of a potential pit and more of one than the annoyance of needing to create a wrapper. I think that annoyance at least encourages minimal thought on why that might be required and reading of docs if they're annoyed enough to log an issue.

Additionally, due to the way LibraryImport generator is setup today, there is no actual guarantee a wrapper is created. For example, if the signature was already "blittable" (say [LibraryImport] static partial void Sleep(uint milliseconds)) then it is directly generated as [DllImport] static partial extern void Sleep(uint milliseconds), which as is currently spec'd/designed is implicitly unsafe. So we'd definitely have to have it working for both cases

@agocke
Copy link
Member

agocke commented Mar 20, 2026

Tanner's right on the current status of the language feature: extern methods are effectively [RequiresUnsafe].

I also agree that it seems like LibraryImport doesn't meet the currently described rules for when it's OK to suppress the unsafety -- it hasn't discharged the validation obligation. That's still on the user.

However, I also agree w/ Stephen that it seems like it would be nice to have a simple gesture to say "this is fine". My suggestion in my team meeting today was, "LibraryImport can have a Safe = true property that people can set which will automatically suppress the warnings". This wasn't a very popular position 😆 (too ugly). But it seems like a cheap way to mark things safe would be nice. Open to suggestions.

@stephentoub
Copy link
Member

I think that annoyance at least encourages minimal thought on why that might be required and reading of docs if they're annoyed enough to log an issue.

Developers (or AI agents) will look for the path-of-least-resistance to suppressing the failures. I expect all we'll be doing by forcing them to write such a wrapper is increasing their annoyance and the amount of code they need to maintain.

Additionally, due to the way LibraryImport generator is setup today, there is no actual guarantee a wrapper is created. [...] So we'd definitely have to have it working for both cases

Not necessarily. LibraryImport could start emitting wrappers in such cases; it doesn't mean DllImport would need the same mechanism. I don't really care whether DllImport has the same mechanism or not. What I care about is use of LibraryImport being made even harder than it already is, and forcing developers to write wrapper methods is making it harder than it already is.

@stephentoub stephentoub reopened this Mar 20, 2026
@stephentoub
Copy link
Member

(The Comment button is way too close to the Close with comment button.)

@stephentoub
Copy link
Member

stephentoub commented Mar 20, 2026

Open to suggestions.

Since it'll already be everywhere in this system, I like Jan's suggestion of just using what we'll already have: [RequiresUnsafe(false)]. That would not be specific to LibraryImport, but LibraryImport would be a primary beneficiary. Presumably this would require language/compiler updates (in addition to the attribute changing).

(I still don't agree though that [LibraryImport]s should all be implicitly unsafe.)

@jkotas
Copy link
Member

jkotas commented Mar 20, 2026

it hasn't discharged the validation obligation

We have split the discharging of the obligations into two independent decisions:

  • (1) Whether the method it unsafe for the caller
  • (2) Whether the method implementation in C# calls unsafe code

There is nothing in the C# language that enforces that these two decisions are coherent. It is going to be on the human review aided by analyzers or AI to make sure that it is right.

extern methods do not have C# implementation so (2) does not apply to them. Given that (1) and (2) are indepedent, (1) should not depend on how the method is implemented and thus it should not be different between methods implemented in C# vs. method implemented via runtime magic.

ugly

IMHO, the [RequiresUnsafe] attribute is the main ugly part - it requires separate line, it is a lot of letters. Implicit [RequiresUnsafe] just complicates the rules, it is not fixing the ugliness.

@jkoritzinsky
Copy link
Member

Instead of having the generator add the RequiresUnsafe attribute, I considered having an analyzer require it at the declaration site (build error otherwise) unless explicitly suppressed. Then the pragma suppression would be the user action to say "I know what I'm doing and this is safe". We've used such a pattern in the past when we need a user acknowledgment of "I know what I'm doing" in the interop generators and analyzers.

Then the generator would generate a stub even for non-marshalling LibraryImports to suppress the implicit RequiresUnsafe that comes with DllImport.

I personally like this UX as it follows our existing pattern of "pragma to acknowledge", but if people prefer the Safe = true model that @agocke proposed we can do that as well.

@stephentoub
Copy link
Member

Is every exported method from every library on nuget today implicitly considered unsafe? I don't see what the difference is between such methods and LibraryImports.

@jkoritzinsky
Copy link
Member

The main problem with LibraryImport (and DllImport) is that, while the managed code of the stub can be guaranteed to be memory safe, there's no guarantee that the signature is correct and that you won't corrupt memory due to ABI mismatches, even if the marshalling logic is all memory-safe.

This concern doesn't exist for calling a managed method in an external library unless that method also uses unsafe code (which in a perfect world is annotated with RequiresUnsafe when needed).

@stephentoub
Copy link
Member

stephentoub commented Mar 20, 2026

unless that method also uses unsafe code (which in a perfect world is annotated with RequiresUnsafe when needed).

Which it absolutely can use and there's zero guarantee it's annotated... in fact every single such method in every library on nuget today is not annotated.

@jkoritzinsky
Copy link
Member

If we have LibraryImport methods suppress the internal DllImport's RequiresUnsafe requirement, then LibraryImport is effectively making the promise that it has validated the API shape matches native code.

I'm fine adding some sort of suppression/acknowledgement system that isn't wrapper functions because libraries like CsWin32 can promise that they got the signatures right (since the onus is on them to do it correctly), but I believe that the acknowledgement should require some user action (and not be the default).

@tannergooding
Copy link
Member

I view the difference as one being an explicit transition out of managed control and into something which is therefore explicitly (with almost guaranteed certainty) doing operations which would require unsafe { } if it were directly translated to C#.

It is for all considerations of the term, unsafe, and one of the bigger sources of potential crashes, bugs, stack corruption, and other memory safety issues. This is even if you get the signatures right, which many people trivially get wrong and have for the entire history of .NET (which leads to its own set of issues).

There are very few APIs, especially off of Windows, that do or even can do proper argument validation for their native calls. In most cases it is expected you consume it correctly or undefined behavior will occur. This is in part a fundamental of the C ABI and its simplicity, where things like null-terminated strings and unpaired pointers + lengths are passed in and presumed to be correct. -- It's such a known and big issue that it and the problems it presents are one of the driving factors for improving memory safety in C#/.NET

We do have to make a compromise somewhere (due to back compat), and in the current design we had specified this was that assemblies which hadn't opted into the new memory safety rules are presumed unsafe as they have historically been (which is if they had pointers in their signature). While things that opt into the new rules can be explicit and consider things escaping managed control as fundamentally unsafe as part of that opt-in.

@tannergooding
Copy link
Member

Noting this transition outside of the "managed control" is also in part how Rust and other modern languages choose to define 'unsafe', as the expectations, risks, and other considerations in such a transition fundamentally change.

@jkotas
Copy link
Member

jkotas commented Mar 20, 2026

Rust and other modern languages choose to define 'unsafe'

FWIW, the latest Rust https://doc.rust-lang.org/reference/items/external-blocks.html :

  • Requires you to write unsafe explicitly on extern blocks, extern is not implicitly unsafe.
  • Has safe qualifier to make the extern safe without wrappers

@tannergooding
Copy link
Member

👍, I missed that they changed the definition of this, looks like in 2024 edition?

@jjonescz
Copy link
Member

jjonescz commented Mar 20, 2026

I thought that we agreed on that the compiler is just going to look at RequiresUnsafeAttribute only, and it won't try to be smart about extern. Was there a misunderstanding about the plan?

Yes, we discussed this and the conclusion was that we are not going to be smart about extern coming from metadata (because it's impossible to distinguish ref assemblies from extern methods, both of which can have null method bodies). When you enable updated memory safety rules in your compilation, the compiler will still implicitly synthesize RequiresUnsafe attribute on all your extern methods though. (And then we only look at RequiresUnsafe attribtutes when checking members from metadata.) See dotnet/csharplang#9883 (comment).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

LibraryImport methods should be implicitly RequiresUnsafe

7 participants