Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Roslyn Code analyzers for interop #37039

Open
5 of 17 tasks
elinor-fung opened this issue May 27, 2020 · 13 comments
Open
5 of 17 tasks

Roslyn Code analyzers for interop #37039

elinor-fung opened this issue May 27, 2020 · 13 comments
Labels
area-Interop-coreclr Bottom Up Work Not part of a theme, epic, or user story User Story A single user-facing feature. Can be grouped under an epic.
Milestone

Comments

@elinor-fung
Copy link
Member

elinor-fung commented May 27, 2020

This tracking issue for a small backlog of rules around interop to add to the officially recommended set of analyzers. The rules are grouped around runtime functionality/features, but would represent separate analyzers in implementation. Since many issues concerning interop are rooted in a discrepancy between the user's intent and implementation, it is expected that only a small number of rules would be useful and applicable to all users. Official documentation will remain the main source of guidance for more nuanced behaviour that requires clear understanding of intent.

The upcoming plan to provide source generators for p/invokes should not directly affect the rules here, as that functionality would be using a different attribute for declaring p/invokes. However, there is the opportunity to provide an analyzer to help users migrate to the source generator approach. Once a source generator exists, the rules here could also be updated to consider p/invokes which use that generator or, depending on their severity, warnings within the generator itself.

The intent is that as the runtime interop team works through a backlog of analyzers around existing features and functionality, the concept of considering and adding analyzers for any new features will become natural and simply be part of that feature work. The P/Invokes and Marshalling rules are expected to be the starting points for working through this backlog.

P/Invokes

These rules inspect the actual declaration and invocation of p/invokes.

  • Do not use [Out] string for P/Invokes: Do not use [Out] string for P/Invokes #35692
  • Avoid StringBuilder parameters for P/Invokes: Avoid StringBuilder parameters for P/Invokes #35693
  • Prefer ExactSpelling=true in [DllImport] for known APIs: Prefer ExactSpelling=true on [DllImport] for known APIs #35695
  • Remove redundant configuration from [DllImport] declaration Remove redundant configuration from [DllImport] declaration #33808
  • CA1404 port: Port FxCop rule CA1404: CallGetLastErrorImmediatelyAfterPInvoke roslyn-analyzers#420
  • Prefer SafeHandle over IntPtr for known APIs: Analyzer suggestion: Prefer SafeHandle over IntPtr #42404
  • Get the last error after calls to P/Invokes with SetLastError=true
    • Category: Interoperability
    • Default: Enabled
    • After calling a p/invoke that sets the last error, Marshal.GetLastWin32Error should be called to retrieve the last error.
      [DllImport("MyLibrary", SetLastError = true)]
      private static extern void Foo();
      
      public static void Bar()
      {
          Foo(); // Flag last error not retrieved
      }
      
      public static void Baz()
      {
          Foo(); // OK
          if (Marshal.GetLastWin32Error() == 0) { ... }
      }
  • Specify SetLastError=true in [DllImport] for known APIs
    • Category: Interoperability
    • Default: Enabled
    • For APIs that are known to set last error, SetLastError=true should be specified. This would require having/building a database of APIs to compare against.
      // Flag SetLastError=false for known API
      [DllImport("kernel32")]
      public static extern bool CloseHandle(IntPtr hObject);
      
      // OK - known API does not set last error
      [DllImport("kernel32")]
      public static extern int GetCurrentProcessId();
    • Recommend:
      [DllImport("kernel32.dll", SetLastError = true)]
      public static extern bool CloseHandle(IntPtr hObject);
    • If the user does not intend to check the error, this can be skipped, but the general guidance is to both define the p/invoke to set last error (for APIs that do so) and check the error.

Marshalling

These rules inspect how data is marshalled. They would apply to the parameters on p/invokes and any types (e.g. marshalled structs, delegates) used by p/invokes.

COM

These rules inspect COM-related functionality. Many of the existing rules that have not yet been ported are around COM.

Low-level interop APIs

These rules are related to APIs that provide low-level interaction/integration with the runtime's interop system.

Cross-platform

There is an existing proposal for an analyzer that detects the use of platform-specific APIs where the API might not be available. Many interop-related APIs are platform-specific and would be given the appropriate attribute such that the proposed analyzer would flag their use.

The logic that will be used by the proposed analyzer for platform-specific APIs could be leveraged for platform-specifc interop behaviour that is not tied to a specifid API. There are some types or MarshalAs values for which marshalling is not supported on all platforms. In these cases, it is not a known API that is platform-specific, but a user-defined p/invoke that is platform-specific due to the way it is defined. An analyzer would detect those p/invokes and treat those as platform-specific calls, following the same logic as that for the platform-specific APIs for determining the platform context and checking for platform guards around the call site.

Rules:

  • Marshalling of <Type> requires <OS>
  • Marshalling as <UnmanagedType.*> requires <OS>

Existing rules

There are a number of rules around interop and marshalling that go through legacy (static) analysis. They have all previously been deprecated or slated to be ported as time allows. See FxCop rule port status for a list of all ported, tracked, and deprecated rules.

@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added the untriaged New issue has not been triaged by the area owner label May 27, 2020
@elinor-fung
Copy link
Member Author

@AaronRobinsonMSFT AaronRobinsonMSFT changed the title Code analyzers for interop Roslyn Code analyzers for interop May 27, 2020
@rseanhall
Copy link
Contributor

In most Win32 APIs, it is wrong to use Marshal.GetLastWin32Error to check if the API failed because most functions that set the thread's last-error code [only] set it when they fail.

@elinor-fung
Copy link
Member Author

That is definitely the case for the Win32 APIs themselves. However, when going through a p/invoke in .NET Core with SetLastError=true, the runtime will explicitly clear the last error before calling the target function: https://github.com/dotnet/runtime/blob/master/src/coreclr/src/vm/dllimport.cpp#L862-L868

@rseanhall
Copy link
Contributor

Interesting, I didn't know that. It doesn't really solve the problem though. CreateFile is a typical Win32 API. In its documentation, it states how to tell when there's a failure (the returned value is INVALID_HANDLE_VALUE). Then it says if it failed, call GetLastError for more information. Nowhere does it say that GetLastError will return 0 on success. So theoretically, CreateFile could internally call another API which sets the last error to non-zero, and still complete successfully without ever putting it back to zero while still adhering to the documented contract.

This code relies on undocumented behavior:

var hFile = CreateFile(...);
if (Marshal.GetLastWin32Error() == 0)
{
  //succeeded
}
else
{
  //failed
}

This code doesn't directly check the last error but is perfectly valid (Win32Exception calls Marshal.GetLastWin32Error internally):

var hFile = CreateFile(...);
if (hFile == INVALID_HANDLE_VALUE)
{
  throw new Win32Exception();
}

@elinor-fung
Copy link
Member Author

The spirit of the rule is that there is a path after the p/invoke where the last error is retrieved without another call in between. As your example points out, only expecting Marshal.GetLastWin32Error is not sufficient, but I believe that still fits within the intent of the rule (i.e. the rule should consider Win32Exception and not flag something like that).

@richlander richlander added the Epic Groups multiple user stories. Can be grouped under a theme. label Jun 1, 2020
@AaronRobinsonMSFT AaronRobinsonMSFT removed the untriaged New issue has not been triaged by the area owner label Jun 8, 2020
@AaronRobinsonMSFT AaronRobinsonMSFT added this to the 5.0 milestone Jun 8, 2020
@elinor-fung elinor-fung modified the milestones: 5.0.0, 6.0.0 Aug 8, 2020
@Mrnikbobjeff
Copy link

One issue which seems to have fallen through the tracks seems to be #33808 which is not referenced in the todo list if I read this correctly

@elinor-fung
Copy link
Member Author

Thanks @Mrnikbobjeff - updated the list.

@Mrnikbobjeff
Copy link

Are these rules already approved when they are in the list? Would a PR be accepted for them or do they individually require approval?

@AaronRobinsonMSFT
Copy link
Member

@Mrnikbobjeff Great question. This issue is for tracking interop analyzers as an initiative and doesn't represent specific approval for any of them. Each analyzer requires a separate API review via a new issue. Some however have had API reviews and been approved - see #35695 for "Prefer ExactSpelling=true in [DllImport] for known APIs". When the API review is completed and marked with the api-approved tag the analyzer can be implemented by anyone. See previously completed analyzer issues above and refer to the associated PR for the repo and process.

If so incline, anyone in the community can submit a PR for the existing approved but not completed analyzer above (#35695) and/or submit new issues marked with api-suggestion and begin the review process. If submitting a new PR, please let us know here and tag one of us (@elinor-fung, @jkoritzinsky, or myself) on the issue so we can update this issue and help the process along.

@Mrnikbobjeff
Copy link

For the GetLastError heuristic some things are not clear to me. How would one arrive at a list of methods calling setlasterror. I thought about how other languages might approach this, as I had already written a Java bytecode analyzer which finds method calls inside of a function. Build the list up transitively and one would know which methods call SetLastError. As this is not viable in ASM and SetLastError is documented on a per function basis I have no idea on creating the desired list. Are there any approaches already considered?

@AaronRobinsonMSFT
Copy link
Member

@Mrnikbobjeff Unfortunately there isn't. Any Win32 API could call another Win32 API that call SetLastError() - this means even if a function doesn't call it directly it can be called. One could argue that any Win32 API that returns a non-error code (i.e. BOOL or handle type) is implicitly going to call SetLastError() in order to provide more detailed information.

The TL;DR of the hand-wavy statements above is the only way to "know" is to follow the official documentation or guess based on the function signature.

@jeffschwMSFT jeffschwMSFT added Team Epic and removed Epic Groups multiple user stories. Can be grouped under a theme. labels Oct 21, 2020
@jeffschwMSFT jeffschwMSFT added Bottom Up Work Not part of a theme, epic, or user story and removed Team Epic labels Jan 20, 2021
@jeffschwMSFT jeffschwMSFT added the User Story A single user-facing feature. Can be grouped under an epic. label Jan 20, 2021
@elinor-fung elinor-fung modified the milestones: 6.0.0, 7.0.0 Jun 21, 2021
@carlossanlop
Copy link
Member

@elinor-fung Looks like this other approved proposal belongs to this list:
#51193

@elinor-fung
Copy link
Member Author

@carlossanlop This issue is listing out a backlog of interop-related analyzers. That one looks to be more general performance. I don't believe we do a tracking list for general runtime/performance analyzers.

@AaronRobinsonMSFT AaronRobinsonMSFT modified the milestones: 7.0.0, 8.0.0 Aug 5, 2022
@jkoritzinsky jkoritzinsky modified the milestones: 8.0.0, 9.0.0 Jul 19, 2023
@agocke agocke modified the milestones: 9.0.0, Future Feb 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-Interop-coreclr Bottom Up Work Not part of a theme, epic, or user story User Story A single user-facing feature. Can be grouped under an epic.
Projects
Status: No status
Development

No branches or pull requests

10 participants