Disposing immutable objects can lead to unpredictable behavior when Objective-C runtime does object pooling
- 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