← 输入框|github.com|刷新
Skip to content

AssemblyManager.FindAssemblyCandidates runs without GIL and crashes with "Attempted to read or write protected memory" unpredictably #2481

Open

Description

@acidbubbles

Environment

  • Pythonnet version: 3.0.4
  • Python version: 3.11.9
  • Operating System: Windows 11 Pro
  • .NET Runtime: .NET Core 8

Details

When assemblies are dynamically loaded in .NET, PythonNet assembly loading hook can crash with "Attempted to read or write protected memory." I'm still trying to make a repro, but maybe you'll immediately see what's wrong.

using var importWarning = Runtime.PyObject_GetAttrString(Exceptions.exceptions_module, "ImportWarning");

I think that the issue may be that during assembly loading, the AssemblyManager hook runs without a GIL, so it may crash with the error above. I've seen multiple stack traces happening at different times, often in code completely unrelated to PythonNet (there is multithreading, I use PythonEngine.BeginAllowThreads())

This did not happen in version 3.0.3.

  • Describe what you were trying to get done.

    Run an application normally that has more than one assembly being loaded (e.g. resources)

  • What commands did you run to trigger this issue? If you can provide a
    Minimal, Complete, and Verifiable example
    this will help us understand the issue.

I will try to provide one, although it's difficult to reproduce reliably

  • If there was a crash, please include the traceback here.
Fatal error. System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
   at Python.Runtime.Runtime.PyObject_GetAttrString(Python.Runtime.BorrowedReference, System.String)
   at Python.Runtime.AssemblyManager+<FindAssemblyCandidates>d__15.MoveNext()
   at System.Linq.Enumerable.TryGetFirst[[System.__Canon, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Collections.Generic.IEnumerable`1<System.__Canon>, Boolean ByRef)
   at System.Linq.Enumerable.FirstOrDefault[[System.__Canon, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Collections.Generic.IEnumerable`1<System.__Canon>)
   at Python.Runtime.AssemblyManager.FindAssembly(System.String)
   at Python.Runtime.AssemblyManager.LoadAssemblyPath(System.String)
   at Python.Runtime.AssemblyManager.ResolveHandler(System.Object, System.ResolveEventArgs)
   at System.Runtime.Loader.AssemblyLoadContext.InvokeResolveEvent(System.ResolveEventHandler, System.Reflection.RuntimeAssembly, System.String)
   at System.Reflection.RuntimeAssembly.InternalLoad(System.Reflection.AssemblyName, System.Threading.StackCrawlMark ByRef, System.Runtime.Loader.AssemblyLoadContext, System.Reflection.RuntimeAssembly, Boolean)
  at ...

Activity

  1. filmor commented on Oct 11, 2024

    @filmor
    Member

    You are absolutely right! To help you with your repro (in case you hadn't seen this, yet), this code was introduced in #2383 to warn about badly formatted strings in paths. You can set up your path as the test does. It should actually only happen in this case.

    If the call chain is indeed not GIL-protected, then it should be enough to add a line using var _ = Py.GIL() directly above the line you mentioned.

    @lostmsu

  2. acidbubbles commented on Oct 11, 2024

    @acidbubbles
    Author

    The issue is that I don't control assembly loading events, some of them happen without any user code. I did figure out the cause of the error (a \something instead of \\something in a string) so the change is a good thing, it's just a shame the message didn't make it through :)

    Here's a full example of a stack trace where it's impossible to wrap in a GIL:

    Fatal error. System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
       at Python.Runtime.Runtime.PyObject_GetAttrString(Python.Runtime.BorrowedReference, System.String)
       at Python.Runtime.AssemblyManager+<FindAssemblyCandidates>d__15.MoveNext()
       at System.Linq.Enumerable.TryGetFirst[[System.__Canon, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Collections.Generic.IEnumerable`1<System.__Canon>, Boolean ByRef)
       at System.Linq.Enumerable.FirstOrDefault[[System.__Canon, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Collections.Generic.IEnumerable`1<System.__Canon>)
       at Python.Runtime.AssemblyManager.FindAssembly(System.String)
       at Python.Runtime.AssemblyManager.LoadAssemblyPath(System.String)
       at Python.Runtime.AssemblyManager.ResolveHandler(System.Object, System.ResolveEventArgs)
       at System.Runtime.Loader.AssemblyLoadContext.InvokeResolveEvent(System.ResolveEventHandler, System.Reflection.RuntimeAssembly, System.String)
       at System.Reflection.RuntimeAssembly.InternalLoad(System.Reflection.AssemblyName, System.Threading.StackCrawlMark ByRef, System.Runtime.Loader.AssemblyLoadContext, System.Reflection.RuntimeAssembly, Boolean)
       at System.Reflection.RuntimeAssembly.InternalGetSatelliteAssembly(System.Globalization.CultureInfo, System.Version, Boolean)
       at System.Resources.ManifestBasedResourceGroveler.InternalGetSatelliteAssembly(System.Reflection.Assembly, System.Globalization.CultureInfo, System.Version)
       at System.Resources.ManifestBasedResourceGroveler.GetSatelliteAssembly(System.Globalization.CultureInfo)
       at System.Resources.ManifestBasedResourceGroveler.GrovelForResourceSet(System.Globalization.CultureInfo, System.Collections.Generic.Dictionary`2<System.String,System.Resources.ResourceSet>, Boolean, Boolean)
       at System.Resources.ResourceManager.InternalGetResourceSet(System.Globalization.CultureInfo, Boolean, Boolean)
       at System.Resources.ResourceManager.GetString(System.String, System.Globalization.CultureInfo)
       at System.SR.GetResourceString(System.String)
       at System.IO.FileSystemWatcher.NotifyInternalBufferOverflowEvent()
       at System.IO.FileSystemWatcher.ReadDirectoryChangesCallback(UInt32, UInt32, AsyncReadState)
       at System.IO.FileSystemWatcher+<>c.<StartRaisingEvents>b__85_0(UInt32, UInt32, System.Threading.NativeOverlapped*)
       at System.Threading.ThreadPoolTypedWorkItemQueue`2[[System.Threading.PortableThreadPool+IOCompletionPoller+Event, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Threading.PortableThreadPool+IOCompletionPoller+Callback, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].System.Threading.IThreadPoolWorkItem.Execute()
       at System.Threading.ThreadPoolWorkQueue.Dispatch()
       at System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
    

    I could probably try and make a repro, but it would probably be much easier to reproduce in a unit test with access to internal classes.

    So for now here's what I think is the recipe:

    • Add an invalid character in one of the PythonEngine.PythonPath paths, like \n for example
    • Start a thread, grab the GIL normally and do some Python stuff, Thread.Sleep would probably work.
    • On another thread, trigger an assembly load, for example in my case I reference Humanizer and do var _ DateTimeOffset.GetUtcNow().Humanize()

    If you feel like making a repro is worth it v.s. using the unit tests, let me know. I'd like to contribute but I'm short on time, so if you prefer leaving it to me I can work on it later (undetermined), but I don't know how I could fix this with my current knowledge. I guess we cannot grab the GIL here because it may have been grabbed, and checking whether it has been grabbed wouldn't be thread safe and it quickly becomes tricky :)

  3. filmor commented on Oct 11, 2024

    @filmor
    Member

    I'll check again, I think when looking through this code before, I found another place in the same call were we take the GIL implicitly, so it wouldn't make things worse.

  4. acidbubbles commented on Oct 13, 2024

    @acidbubbles
    Author

    My worry would be deadlocks, since C# assembly loading can happen both when calling CLR from Python (within a GIL) or outside (no GIL).

  5. filmor commented on Oct 13, 2024

    @filmor
    Member

    I know that, that would be my worry as well, hence my note about another potentially GIL-protected call. It looks like the function may also get called in some resolve handler.

    I suggest that we just gate the warnings with a PyGILState_Check and only emit warnings while the GIL is taken (e.g. during an explicit clr.AddReference).

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions