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

Last PInvoke error in LibraryImport may not be preserved after runtime internal calls #79546

Open
k15tfu opened this issue Dec 12, 2022 · 7 comments

Comments

@k15tfu
Copy link
Contributor

k15tfu commented Dec 12, 2022

Hi!

It turned out that sometimes .NET 7 managed app may behave differently while under profiling. For example, as far as I can see File.Exists() uses GetFileAttributesExW() as follows:

internal static unsafe partial class Kernel32
{
    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.1805")]
    private static partial bool GetFileAttributesExPrivate(string name, global::Interop.Kernel32.GET_FILEEX_INFO_LEVELS fileInfoLevel, ref global::Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA lpFileInformation)
    {
        int __lastError;
        bool __retVal;
        int __retVal_native;
        // Pin - Pin data in preparation for calling the P/Invoke.
        fixed (void* __name_native = &global::System.Runtime.InteropServices.Marshalling.Utf16StringMarshaller.GetPinnableReference(name))
        fixed (global::Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA* __lpFileInformation_native = &lpFileInformation)
        {
            System.Runtime.InteropServices.Marshal.SetLastSystemError(0);
            __retVal_native = __PInvoke((ushort*)__name_native, fileInfoLevel, __lpFileInformation_native);
            __lastError = System.Runtime.InteropServices.Marshal.GetLastSystemError();  <-- here
        }

        // Unmarshal - Convert native data to managed data.
        __retVal = __retVal_native != 0;
        System.Runtime.InteropServices.Marshal.SetLastPInvokeError(__lastError);
        return __retVal;
        // Local P/Invoke
        [System.Runtime.InteropServices.DllImportAttribute("kernel32.dll", EntryPoint = "GetFileAttributesExW", ExactSpelling = true)]
        static extern unsafe int __PInvoke(ushort* name, global::Interop.Kernel32.GET_FILEEX_INFO_LEVELS fileInfoLevel, global::Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA* lpFileInformation);
    }
}

In this case File.Exists() returns true for files that do not exist when profiler's ELT hooks are used, here is demo app:

using System.Runtime.InteropServices;

namespace file_exists_csharp
{
    internal class Program
    {

        static void Main(string[] args)
        {
            if (File.Exists("file-that-should-not-exist.txt"))
                throw new Exception("File exists");
        }
    }
}

Profiler Enter callback resets the last error code when Marshal.GetLastSystemError() is being called:

public static int GetLastSystemError()
{
    return Interop.Kernel32.GetLastError();
}

These callbacks are set by SetJitHelperFunction() and there are other JIT helper functions (e.g. CORINFO_HELP_NEWARR_1_OBJ) that probably won't be called before Marshal.GetLastSystemError() in the generated code, and also there are BEGIN_PRESERVE_LAST_ERROR / END_PRESERVE_LAST_ERROR macros that are used in internal JIT helpers etc.

I have also looked into Thread::InjectActivation() which uses PAL_InjectActivation() and inject_activation_handler() on Linux/macOS, and it preserves errno before calling HandleSuspensionForInterruptedThread(), but it is not done for CheckActivationSafePoint() which uses GetThreadNULLOk() and e.g. on Linux ARM32 it internally calls __tls_get_addr(), do we need to wrap it as well?

Same also applies for profiler based IL instrumentation (code coverage).

Linked issues: #10727, #75828, #77364.

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Dec 12, 2022
@ghost
Copy link

ghost commented Dec 12, 2022

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

Issue Details

Hi!

It turned out that sometimes .NET 7 managed app may behave differently while under profiling. For example, as far as I can see File.Exists() uses GetFileAttributesExW() as follows:

internal static unsafe partial class Kernel32
{
    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.1805")]
    private static partial bool GetFileAttributesExPrivate(string name, global::Interop.Kernel32.GET_FILEEX_INFO_LEVELS fileInfoLevel, ref global::Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA lpFileInformation)
    {
        int __lastError;
        bool __retVal;
        int __retVal_native;
        // Pin - Pin data in preparation for calling the P/Invoke.
        fixed (void* __name_native = &global::System.Runtime.InteropServices.Marshalling.Utf16StringMarshaller.GetPinnableReference(name))
        fixed (global::Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA* __lpFileInformation_native = &lpFileInformation)
        {
            System.Runtime.InteropServices.Marshal.SetLastSystemError(0);
            __retVal_native = __PInvoke((ushort*)__name_native, fileInfoLevel, __lpFileInformation_native);
            __lastError = System.Runtime.InteropServices.Marshal.GetLastSystemError();  <-- here
        }

        // Unmarshal - Convert native data to managed data.
        __retVal = __retVal_native != 0;
        System.Runtime.InteropServices.Marshal.SetLastPInvokeError(__lastError);
        return __retVal;
        // Local P/Invoke
        [System.Runtime.InteropServices.DllImportAttribute("kernel32.dll", EntryPoint = "GetFileAttributesExW", ExactSpelling = true)]
        static extern unsafe int __PInvoke(ushort* name, global::Interop.Kernel32.GET_FILEEX_INFO_LEVELS fileInfoLevel, global::Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA* lpFileInformation);
    }
}

In this case File.Exists() returns true for files that do not exist when profiler's ELT hooks are used, here is demo app:

using System.Runtime.InteropServices;

namespace file_exists_csharp
{
    internal class Program
    {

        static void Main(string[] args)
        {
            if (File.Exists("file-that-should-not-exist.txt"))
                throw new Exception("File exists");
        }
    }
}

Profiler Enter callback resets the last error code when Marshal.GetLastSystemError() is being called:

public static int GetLastSystemError()
{
    return Interop.Kernel32.GetLastError();
}

These callbacks are set by SetJitHelperFunction() and there are other JIT helper functions (e.g. CORINFO_HELP_NEWARR_1_OBJ) that probably won't be called before Marshal.GetLastSystemError() in the generated code, and also there are BEGIN_PRESERVE_LAST_ERROR / END_PRESERVE_LAST_ERROR macros that are used in internal JIT helpers etc.

I have also looked into Thread::InjectActivation() which uses PAL_InjectActivation() and inject_activation_handler() on Linux/macOS, and it preserves errno before calling HandleSuspensionForInterruptedThread(), but it is not done for CheckActivationSafePoint() which uses GetThreadNULLOk() and e.g. on Linux ARM32 it internally calls __tls_get_addr(), do we need to wrap it as well?

Linked issues: #10727, #75828, #77364.

Author: k15tfu
Assignees: -
Labels:

area-System.Runtime.InteropServices, untriaged

Milestone: -

@AaronRobinsonMSFT AaronRobinsonMSFT removed the untriaged New issue has not been triaged by the area owner label Dec 12, 2022
@AaronRobinsonMSFT AaronRobinsonMSFT added this to the 8.0.0 milestone Dec 12, 2022
@ww898
Copy link
Contributor

ww898 commented Dec 13, 2022

I would be great if the future fix will be backported to 7.0.x. Because huge impact for all profilers.

@k15tfu
Copy link
Contributor Author

k15tfu commented Feb 13, 2023

Friendly ping.

1 similar comment
@k15tfu

This comment was marked as duplicate.

@jkoritzinsky
Copy link
Member

I believe it is up to profilers to ensure that they don't change any state that can be unintentionally observed by managed code.

I'll send this to the profiler team to see what they think.

@jkoritzinsky jkoritzinsky removed this from the 8.0.0 milestone Jul 27, 2023
@ghost ghost added the untriaged New issue has not been triaged by the area owner label Jul 27, 2023
@jkoritzinsky jkoritzinsky added area-Diagnostics-coreclr and removed untriaged New issue has not been triaged by the area owner area-System.Runtime.InteropServices labels Jul 27, 2023
@ghost
Copy link

ghost commented Jul 27, 2023

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

Issue Details

Hi!

It turned out that sometimes .NET 7 managed app may behave differently while under profiling. For example, as far as I can see File.Exists() uses GetFileAttributesExW() as follows:

internal static unsafe partial class Kernel32
{
    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.1805")]
    private static partial bool GetFileAttributesExPrivate(string name, global::Interop.Kernel32.GET_FILEEX_INFO_LEVELS fileInfoLevel, ref global::Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA lpFileInformation)
    {
        int __lastError;
        bool __retVal;
        int __retVal_native;
        // Pin - Pin data in preparation for calling the P/Invoke.
        fixed (void* __name_native = &global::System.Runtime.InteropServices.Marshalling.Utf16StringMarshaller.GetPinnableReference(name))
        fixed (global::Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA* __lpFileInformation_native = &lpFileInformation)
        {
            System.Runtime.InteropServices.Marshal.SetLastSystemError(0);
            __retVal_native = __PInvoke((ushort*)__name_native, fileInfoLevel, __lpFileInformation_native);
            __lastError = System.Runtime.InteropServices.Marshal.GetLastSystemError();  <-- here
        }

        // Unmarshal - Convert native data to managed data.
        __retVal = __retVal_native != 0;
        System.Runtime.InteropServices.Marshal.SetLastPInvokeError(__lastError);
        return __retVal;
        // Local P/Invoke
        [System.Runtime.InteropServices.DllImportAttribute("kernel32.dll", EntryPoint = "GetFileAttributesExW", ExactSpelling = true)]
        static extern unsafe int __PInvoke(ushort* name, global::Interop.Kernel32.GET_FILEEX_INFO_LEVELS fileInfoLevel, global::Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA* lpFileInformation);
    }
}

In this case File.Exists() returns true for files that do not exist when profiler's ELT hooks are used, here is demo app:

using System.Runtime.InteropServices;

namespace file_exists_csharp
{
    internal class Program
    {

        static void Main(string[] args)
        {
            if (File.Exists("file-that-should-not-exist.txt"))
                throw new Exception("File exists");
        }
    }
}

Profiler Enter callback resets the last error code when Marshal.GetLastSystemError() is being called:

public static int GetLastSystemError()
{
    return Interop.Kernel32.GetLastError();
}

These callbacks are set by SetJitHelperFunction() and there are other JIT helper functions (e.g. CORINFO_HELP_NEWARR_1_OBJ) that probably won't be called before Marshal.GetLastSystemError() in the generated code, and also there are BEGIN_PRESERVE_LAST_ERROR / END_PRESERVE_LAST_ERROR macros that are used in internal JIT helpers etc.

I have also looked into Thread::InjectActivation() which uses PAL_InjectActivation() and inject_activation_handler() on Linux/macOS, and it preserves errno before calling HandleSuspensionForInterruptedThread(), but it is not done for CheckActivationSafePoint() which uses GetThreadNULLOk() and e.g. on Linux ARM32 it internally calls __tls_get_addr(), do we need to wrap it as well?

Same also applies for profiler based IL instrumentation (code coverage).

Linked issues: #10727, #75828, #77364.

Author: k15tfu
Assignees: -
Labels:

area-System.Runtime.InteropServices, area-Diagnostics-coreclr

Milestone: -

@jkoritzinsky jkoritzinsky added the untriaged New issue has not been triaged by the area owner label Jul 27, 2023
@tommcdon
Copy link
Member

We are past platform shutdown for .NET 8 and are driving to zero bugs. Since this looks like a pre-existing issue and seems to be a non-trivial amount of work, moving to .NET 9

@tommcdon tommcdon added this to the 9.0.0 milestone Jul 31, 2023
@tommcdon tommcdon removed the untriaged New issue has not been triaged by the area owner label Jul 31, 2023
@tommcdon tommcdon modified the milestones: 9.0.0, Future Jan 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants