Skip to content

Commit df0c682

Browse files
committed
[Foundation] Fix NSObjectData leak.
1 parent c063629 commit df0c682

File tree

1 file changed

+86
-6
lines changed

1 file changed

+86
-6
lines changed

src/Foundation/NSObject2.cs

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ struct NSObjectData {
7676
}
7777

7878
class NSObjectDataHandle : CriticalHandle {
79+
bool invalidated;
7980
public NSObjectDataHandle ()
8081
: base (IntPtr.Zero)
8182
{
@@ -84,18 +85,34 @@ public NSObjectDataHandle ()
8485
}
8586
}
8687

88+
public NSObjectDataHandle (IntPtr handle)
89+
: base (handle)
90+
{
91+
}
92+
93+
public void Invalidate ()
94+
{
95+
invalidated = true;
96+
}
97+
8798
public unsafe NSObjectData* Data {
8899
get => (NSObjectData*) handle;
89100
}
90101

91102
public override bool IsInvalid {
92-
get => handle != IntPtr.Zero;
103+
get => handle == IntPtr.Zero;
93104
}
94105

95106
protected override bool ReleaseHandle ()
96107
{
97-
unsafe {
98-
NativeMemory.Free ((void*) handle);
108+
if (handle != IntPtr.Zero) {
109+
if (invalidated) {
110+
// nothing to do here.
111+
} else {
112+
unsafe {
113+
NativeMemory.Free ((void*) handle);
114+
}
115+
}
99116
}
100117
handle = IntPtr.Zero;
101118
return true;
@@ -149,13 +166,22 @@ unsafe NativeHandle handle {
149166

150167
internal unsafe NSObjectData* GetData ()
151168
{
152-
return AllocateData ().Data;
169+
var rv = AllocateData ().Data;
170+
171+
if (rv is null) {
172+
// Throwing an exception here is better than returning a null pointer, because that will crash the process when the pointer is dereferenced
173+
// (and none of the callers can do anything useful with a null pointer anyway).
174+
throw new ObjectDisposedException ($"This object (of type {GetType ().Name}) does not have a data pointer anymore, possibly because of a race condition. Please file a bug at https://github.com/dotnet/macios/issues.");
175+
}
176+
177+
return rv;
153178
}
154179

155180
unsafe NSObjectDataHandle AllocateData ()
156181
{
157-
if (data_handle is not null)
158-
return data_handle;
182+
var dh = data_handle;
183+
if (dh is not null)
184+
return dh;
159185

160186
var data = new NSObjectDataHandle ();
161187
var previousValue = Interlocked.CompareExchange (ref data_handle, data, null);
@@ -998,10 +1024,64 @@ protected virtual void Dispose (bool disposing)
9981024
ReleaseManagedRef ();
9991025
} else {
10001026
NSObject_Disposer.Add (this);
1027+
RecreateDataHandle ();
10011028
}
10021029
}
10031030
}
10041031

1032+
#nullable enable
1033+
void RecreateDataHandle ()
1034+
{
1035+
// OK, this code is _weird_.
1036+
// We need to delay the deletion of the native memory pointed to by data_handle until
1037+
// after this instance has been collected. A CriticalHandle seems to fit this purpose like glove, until
1038+
// you realize that a CriticalHandle is only kept alive until the parent object _becomes finalizable_,
1039+
// not _is collected_, which is very different - in other words, resurrected objects don't keep CriticalHandles
1040+
// they contain alive. This is a problem because every single managed NSObject instance is resurrected, and we
1041+
// need the native memory to stay alive after resurrection.
1042+
//
1043+
// So this solution depends on a few bits:
1044+
// * At this point, this instance may have become finalizable, but the native memory shouldn't have been freed yet.
1045+
// * The original NSObjectDataHandle (aka CriticalHandle) will be collected in this/upcoming GC cycle, and can't
1046+
// be trusted to keep the native memory alive anymore.
1047+
// * So we just create a new one, pointing to the same native memory, and replace the original NSObjectDataHandle (aka
1048+
// CriticalHandle) with it
1049+
// * This works, because since this instance has become / will become resurrected, it's not finalizable anymore,
1050+
// and it will keep the new NSObjectDataHandle instance (and the native memory it points to) alive.
1051+
// * Now if this instance is deemed finalizable, and then resurrected *again*, bad things will likely happen. This
1052+
// is a bit more unlikely though, because we don't re-register the finalizer for execution, so unless somebody
1053+
// else does that, it's quite unlikely this instance will become resurrected a second time.
1054+
var previous_data = data_handle;
1055+
if (previous_data is null) {
1056+
var msg = $"This object (of type {GetType ().Name}) does not have an existing data pointer, possibly because of a race condition. Please file a bug at https://github.com/dotnet/macios/issues.");
1057+
#if CONSISTENCY_CHECKS
1058+
throw new InvalidOperationException (msg);
1059+
#else
1060+
Runtime.NSLog (msg);
1061+
return;
1062+
#endif
1063+
}
1064+
1065+
unsafe {
1066+
data_handle = new NSObjectDataHandle ((IntPtr) previous_data.Data);
1067+
}
1068+
1069+
if (previous_data.IsInvalid) {
1070+
var msg = $"This object (of type {GetType ().Name}) does not have valid data pointer, possibly because of a race condition. Please file a bug at https://github.com/dotnet/macios/issues.");
1071+
#if CONSISTENCY_CHECKS
1072+
throw new InvalidOperationException (msg);
1073+
#else
1074+
Runtime.NSLog (msg);
1075+
return;
1076+
#endif
1077+
}
1078+
1079+
previous_data.Invalidate ();
1080+
// Don't dispose previous_data, because another thread might be referencing it, and trying to access its pointer - which is still valid.
1081+
// The GC will dispose of previous_data when its not accessible anymore.
1082+
}
1083+
#nullable disable
1084+
10051085
[Register ("__NSObject_Disposer")]
10061086
[Preserve (AllMembers = true)]
10071087
internal class NSObject_Disposer : NSObject {

0 commit comments

Comments
 (0)