LibraryImport generated stubs should be implicitly RequiresUnsafe#125802
LibraryImport generated stubs should be implicitly RequiresUnsafe#125802
Conversation
|
Tagging subscribers to this area: @dotnet/area-meta |
Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com>
…urce Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com>
|
What's the rationale behind every |
…for regular stubs Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com>
|
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. |
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 |
I actually don't think it fundamentally resolves any of the reasons that That is, they remain equivalent to a
I think this is just the expectation of all unsafe code and is why all 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. |
|
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 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. |
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 I think similar strategy can work for DllImport/LibraryImport too. |
The language has implemented it such that all That is, at the IL level functionally any method which is not abstract and has a null method body (i.e. is If we wanted a way to annotate that
The exact same logic applies to |
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.
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. |
@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 I do not want the runtime to be dealing with the problems created by safe wrappers over |
|
I'm not against being able to mark I think that escaping managed control (which from a user visible perspective is what
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 Additionally, due to the way |
|
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. |
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.
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. |
|
(The Comment button is way too close to the Close with comment button.) |
Since it'll already be everywhere in this system, I like Jan's suggestion of just using what we'll already have: (I still don't agree though that [LibraryImport]s should all be implicitly unsafe.) |
We have split the discharging of the obligations into two independent decisions:
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.
IMHO, the |
|
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 |
|
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. |
|
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). |
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. |
|
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). |
|
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 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 |
|
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. |
FWIW, the latest Rust https://doc.rust-lang.org/reference/items/external-blocks.html :
|
|
👍, I missed that they changed the definition of this, looks like in 2024 edition? |
Yes, we discussed this and the conclusion was that we are not going to be smart about |
[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'sRequiresUnsafeAnalyzer.Changes
RequiresUnsafeAttribute→publicsrc/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/RequiresUnsafeAttribute.cs:internal→publicso generated user code can reference itsrc/libraries/System.Runtime/ref/System.Runtime.cs: Added to reference assembly for API compatGenerator infrastructure
StubEnvironment: AddedRequiresUnsafeAttrTypelazy lookup propertyEnvironmentFlags: AddedRequiresUnsafeAvailable = 0x4flagTypeNames/NameSyntaxes: AddedSystem_Diagnostics_CodeAnalysis_RequiresUnsafeAttributeconstant and syntax helperLibraryImportGeneratorstub emissionCalculateStubInformation: SetsRequiresUnsafeAvailableflag 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]intoSignatureContext.AdditionalAttributesfor 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
RequiresUnsafeAddedandRequiresUnsafeAddedOnForwardingStubtoAdditionalAttributesOnStub.cs, verifying the attribute is emitted on both stub kinds with the expectedglobal::qualified syntax.Original prompt
📱 Kick off Copilot coding agent tasks wherever you are with GitHub Mobile, available on iOS and Android.