dotnet / dotnet/macios

Disposing immutable objects can lead to unpredictable behavior when Objective-C runtime does object pooling

Open
#21,425 4 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
C#
Stars
2.9k
Forks
576
Avg merge
2d 12h
Merged PRs (30d)
123

Description

Consider the following code:

```csharp
string notificationData =
"""
{
action = "show_pushed_mail";
aps =
{
alert =
{
body = "Compared Exchange Rates are out of tolerance in number of 115.";
subtitle = "navara@emclient.com";
title = "eM Client Licensing";
};
category = "PUSHED_MAIL";
"content-available" = 1;
"mutable-content" = 1;
sound = default;
"thread-id" = "mail_cf73f3bd-1cf9-437c-bc70-4b6bd2b5f7e4";
};
"em-account" = "navara@emclient.com";
"em-account-id" = "cf73f3bd-1cf9-437c-bc70-4b6bd2b5f7e4";
"em-body" = "";
"em-date" = "2024-10-09T01:30:01.0000000Z";
"em-from" = "eM Client Licensing";
"em-from-address" = "licensing@emclient.com";
"em-message-path" = "{\n \"Mailbox\": \"INBOX\",\n \"UIDVALIDITY\": \"1117940911\",\n \"UID\": \"317970\",\n \"Message-ID\": \"6bb2b08b259b4ed1b558c737a49f5c39@emclient.com\"\n}";
"em-notification" = Mixed;
"em-notification-id" = 5801afc0ceb977f379adc152333e0d25554a98d4;
"em-subject" = "Compared Exchange Rates are out of tolerance in number of 115.";
"gcm.message_id" = 1728437407395630;
"google.c.a.e" = 1;
"google.c.fid" = eEhP3hWX1U8DuSB9hb3siN;
"google.c.sender.id" = 417058856903;
}
""";

var d = NSData.FromString(notificationData);
NSPropertyListFormat fmt = NSPropertyListFormat.OpenStep;
var userInfo = (NSMutableDictionary)NSPropertyListSerialization.PropertyListWithData(d, ref fmt, out var error);

var apsKey = new NSString("aps");
var data = userInfo
.Where(kv =>
{
var r = !kv.Key.ToString().Equals("aps", StringComparison.OrdinalIgnoreCase);
apsKey.Dispose();
return r;
})
.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value.ToString() ?? string.Empty);
```

It will reliably crash with `ObjectDisposedException` which may be quite unexpected. In fact, this is a very reduced example of a problem that was happening in a multi-threaded application where it was even less obvious.

Why does it crash?
- We create the `NSString` object representing the string `aps`.
- When Objective-C enumerates the dictionary keys it reuses the same object, ie. same handle, when enumerating the `aps` key, and that in turns maps to the same managed `NSString` object.
- Calling `Dispose` on the `NSString` causes the managed representation to be invalid and next enumeration of the same key will crash with `ObjectDisposedException`.

What can we do about it?

Changing the behavior to remove disposed objects from the handle->managed object mapping seems dangerous. (would not help anyway in the original multi-threaded scenario)

Make the `Dispose` on immutable poolable classes like `NSString` a no-op? Make an analyzer that warns when someone tries to dispose a `NSString` instance (would flag the obvious error but not if someone disposes it as `NSObject` variable)?

Thoughts?

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with the reduced C# reproduction in the issue and investigate how NSString disposal interacts with Objective-C dictionary enumeration and object pooling. Done requires an agreed behavior or API change that prevents the ObjectDisposedException, along with regression coverage for the reproduced case.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, objective-c
Domain
mobile-dev
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.