Fix GC issue on CoreCLR where finalized objects are returned by Runtime.GetNSObject (#14070)

Fixes race condition where finalized objects are resurrected by the finalizer and for a brief moment `Runtime.TryGetNSObject`/`Runtime.GetNSObject` would return them even though a new instance is supposed to be used. In multi-threaded scenarios this would result in reused objects (same native handle) to suddenly become invalid when the UI thread processes the delayed finalization.

Fixes #13921
This commit is contained in:
Filip Navara 2022-02-08 11:08:44 +01:00 коммит произвёл GitHub
Родитель 5eda463eaf
Коммит 0622ae4af2
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
2 изменённых файлов: 78 добавлений и 0 удалений

Просмотреть файл

@ -932,6 +932,20 @@ namespace Foundation {
if (disposing) {
ReleaseManagedRef ();
} else {
#if NET
// By adding an external reference to the object from finalizer we will
// resurrect it. Since Runtime class tracks the NSObject instances with
// GCHandle(..., WeakTrackResurrection) we need to make sure it's aware
// that the object was finalized.
//
// On CoreCLR the non-tracked objects don't get a callback from the
// garbage collector when they enter the finalization queue but the
// information is necessary for Runtime.TryGetNSObject to work correctly.
// Since we are on the finalizer thread now we can just set the flag
// directly here.
actual_flags |= Flags.InFinalizerQueue;
#endif
NSObject_Disposer.Add (this);
}
} else {

Просмотреть файл

@ -0,0 +1,64 @@
#if __MACOS__
using System;
using System.Threading;
using NUnit.Framework;
using AppKit;
using ObjCRuntime;
using Foundation;
namespace Xamarin.Mac.Tests
{
[TestFixture]
[Preserve (AllMembers = true)]
public class NSObjectGCTest
{
[Test]
public void NSObject_GCResurrectTest ()
{
// Run GC and pump run loop to ensure that NSObject_Disposer has processed everything
GC.Collect ();
GC.WaitForPendingFinalizers ();
PumpLoop ();
// Create a single object and immediately lose a reference to it, save the handle
// to immitate getting the same handle from a native API at later point in time
IntPtr nativeHandle = CreateNativeObject ();
// Ensure that the GC is run and the one new object gets collected
GC.Collect ();
GC.WaitForPendingFinalizers ();
// The object should now be resurrected through `NSObject_Disposer.Add(this)`
// and so `GCHandle` will return its reference:
var o = ObjCRuntime.Runtime.GetNSObject<NSString> (nativeHandle);
Assert.AreNotEqual (IntPtr.Zero, (IntPtr) o.Handle);
// Pump the run loop and thus drain the NSObject_Disposer
PumpLoop ();
// The object is invalid now
Assert.AreNotEqual (IntPtr.Zero, (IntPtr) o.Handle);
}
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
private static IntPtr CreateNativeObject ()
{
var foo = new NSString ("foo");
foo.DangerousRetain();
return foo.Handle;
}
private static void PumpLoop ()
{
using (new NSAutoreleasePool ()) {
NSEvent? evnt;
while ((evnt = NSApplication.SharedApplication.NextEvent ((NSEventMask)NSEventMask.AnyEvent, NSDate.DistantPast, NSRunLoopMode.Default, true)) != null) {
NSApplication.SharedApplication.SendEvent (evnt);
}
}
}
}
}
#endif // __MACOS__