Skip to content

Conversation

@BrzVlad
Copy link
Member

@BrzVlad BrzVlad commented Oct 23, 2025

Every NSObject allocates native memory for NSObjectData via a NSObjectDataHandle critical handle. When the handle dies, the memory wasn't reclaimed because IsInvalid method was implemented incorrectly. Fixing this bug, leads to crashes in ReleaseManagedRef which tries to access the native data that was already freed. Normally, this shouldn't happen because NSObject is a normal finalizable object and NSObjectDataHandle is a critical finalizable object. This means that the finalizer for NSObject would always run first, considering that the 2 objects die at the same time. This means that the native NSObjectData would be cleared only after the finalizer for NSObject has run and ReleaseManagedRef finished executing. However, this is not the case because the "finalization" code of NSObject is not run from the finalizer thread but it is enqueued to NSObject_Disposer. This means that now the critical finalizer could have actually released the native memory before the finalization of the NSObject is done.

The simple fix for this is to create a GCHandle keeping the NSObjectDataHandle alive until all NSObject finalization is done, at the end of ReleaseManagedRef.

Every NSObject allocates native memory for `NSObjectData` via a `NSObjectDataHandle` critical handle. When the handle dies, the memory wasn't reclaimed because `IsInvalid` method was implemented incorrectly. Fixing this bug, leads to crashes in `ReleaseManagedRef` which tries to access the native data that was already freed. Normally, this shouldn't happen because `NSObject` is a normal finalizable object and `NSObjectDataHandle` is a critical finalizable object. This means that the finalizer for `NSObject` would always run first, considering that the 2 objects die at the same time. This means that the native `NSObjectData` would be cleared only after the finalizer for `NSObject` has run and `ReleaseManagedRef` finished executing. However, this is not the case because the "finalization" code of `NSObject` is not run from the finalizer thread but it is enqueued to `NSObject_Disposer`. This means that now the critical finalizer could have actually released the native memory before the finalization of the `NSObject` is done.

The simple fix for this is to create a GCHandle keeping the `NSObjectDataHandle` alive until all `NSObject` finalization is done, at the end of `ReleaseManagedRef`.
@BrzVlad BrzVlad requested a review from rolfbjarne as a code owner October 23, 2025 13:18
@BrzVlad
Copy link
Member Author

BrzVlad commented Oct 23, 2025

This seems to have started happening after #23300. I didn't look into that change and I don't have much understanding of this area. Fixes a leak I noticed in an app reported in dotnet/runtime#119491. The app was leaking about 50mb per 3hours, so it looks like this could be problematic in certain scenarios. Tested locally by rebuilding Microsoft.iOS.dll and patching the above app with it.


public override bool IsInvalid {
get => handle != IntPtr.Zero;
get => handle == IntPtr.Zero;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦‍♂️

@rolfbjarne rolfbjarne self-assigned this Oct 24, 2025
@rolfbjarne rolfbjarne marked this pull request as draft October 27, 2025 17:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants