From ba00242f7f465f1f08fb85bcb1e59c694805f8f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:11:41 +0000 Subject: [PATCH 1/2] Initial plan From 48345778f6685c49978be336dc77c74cde1ecc19 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:18:30 +0000 Subject: [PATCH 2/2] Fix GCHandle leak in NetworkReachability.SetNotification and add tests Co-authored-by: rolfbjarne <249268+rolfbjarne@users.noreply.github.com> --- .../NetworkReachability.cs | 16 ++++ .../NetworkReachabilityTest.cs | 93 +++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/src/SystemConfiguration/NetworkReachability.cs b/src/SystemConfiguration/NetworkReachability.cs index 3fd3a4ad48f5..26831252dd2b 100644 --- a/src/SystemConfiguration/NetworkReachability.cs +++ b/src/SystemConfiguration/NetworkReachability.cs @@ -416,6 +416,15 @@ public NetworkReachability (IPAddress localAddress, IPAddress remoteAddress) { } + protected override void Dispose (bool disposing) + { + if (gchAllocated) { + gch.Free (); + gchAllocated = false; + } + base.Dispose (disposing); + } + [SupportedOSPlatform ("ios")] [SupportedOSPlatform ("tvos")] [SupportedOSPlatform ("maccatalyst")] @@ -487,6 +496,7 @@ public StatusCode GetFlags (out NetworkReachabilityFlags flags) Notification? notification; GCHandle gch; + bool gchAllocated; [UnmanagedCallersOnly] static void Callback (IntPtr handle, NetworkReachabilityFlags flags, IntPtr info) @@ -518,6 +528,7 @@ public StatusCode SetNotification (Notification? callback) return StatusCode.OK; gch = GCHandle.Alloc (this); + gchAllocated = true; var ctx = new SCNetworkReachabilityContext (GCHandle.ToIntPtr (gch)); unsafe { @@ -532,6 +543,11 @@ public StatusCode SetNotification (Notification? callback) if (!rv) return StatusCodeError.SCError (); + if (gchAllocated) { + gch.Free (); + gchAllocated = false; + } + return StatusCode.OK; } } diff --git a/tests/monotouch-test/SystemConfiguration/NetworkReachabilityTest.cs b/tests/monotouch-test/SystemConfiguration/NetworkReachabilityTest.cs index 368f42839548..04f176ef89a3 100644 --- a/tests/monotouch-test/SystemConfiguration/NetworkReachabilityTest.cs +++ b/tests/monotouch-test/SystemConfiguration/NetworkReachabilityTest.cs @@ -142,5 +142,98 @@ public void Schedule () Assert.IsTrue (defaultRouteReachability.Schedule (CFRunLoop.Main, CFRunLoop.ModeDefault), "Schedule"); Assert.IsTrue (defaultRouteReachability.Unschedule (CFRunLoop.Main, CFRunLoop.ModeDefault), "Unschedule"); } + + [Test] + public void SetNotification_SetAndRemove () + { + using var nr = new NetworkReachability (IPAddress.Loopback); + bool callbackInvoked = false; + NetworkReachability.Notification callback = (flags) => { + callbackInvoked = true; + }; + + // Set notification + var result = nr.SetNotification (callback); + Assert.AreEqual (StatusCode.OK, result, "SetNotification should succeed"); + + // Remove notification + result = nr.SetNotification (null); + Assert.AreEqual (StatusCode.OK, result, "Removing notification should succeed"); + + // Setting null again should be OK + result = nr.SetNotification (null); + Assert.AreEqual (StatusCode.OK, result, "Setting null when no notification is set should succeed"); + } + + [Test] + public void SetNotification_DisposeAfterSet () + { + // This test ensures that disposing after setting a notification doesn't leak the GCHandle + var nr = new NetworkReachability (IPAddress.Loopback); + bool callbackInvoked = false; + NetworkReachability.Notification callback = (flags) => { + callbackInvoked = true; + }; + + var result = nr.SetNotification (callback); + Assert.AreEqual (StatusCode.OK, result, "SetNotification should succeed"); + + // Dispose without removing notification - should not leak + nr.Dispose (); + } + + [Test] + public void SetNotification_MultipleSetRemoveCycles () + { + using var nr = new NetworkReachability (IPAddress.Loopback); + bool callbackInvoked = false; + NetworkReachability.Notification callback = (flags) => { + callbackInvoked = true; + }; + + // Set notification + var result = nr.SetNotification (callback); + Assert.AreEqual (StatusCode.OK, result, "First SetNotification should succeed"); + + // Remove notification + result = nr.SetNotification (null); + Assert.AreEqual (StatusCode.OK, result, "First remove should succeed"); + + // Set notification again + result = nr.SetNotification (callback); + Assert.AreEqual (StatusCode.OK, result, "Second SetNotification should succeed"); + + // Remove notification again + result = nr.SetNotification (null); + Assert.AreEqual (StatusCode.OK, result, "Second remove should succeed"); + } + + [Test] + public void SetNotification_UpdateCallback () + { + using var nr = new NetworkReachability (IPAddress.Loopback); + bool firstCallbackInvoked = false; + bool secondCallbackInvoked = false; + + NetworkReachability.Notification firstCallback = (flags) => { + firstCallbackInvoked = true; + }; + + NetworkReachability.Notification secondCallback = (flags) => { + secondCallbackInvoked = true; + }; + + // Set first notification + var result = nr.SetNotification (firstCallback); + Assert.AreEqual (StatusCode.OK, result, "First SetNotification should succeed"); + + // Update with second notification + result = nr.SetNotification (secondCallback); + Assert.AreEqual (StatusCode.OK, result, "Updating notification should succeed"); + + // Remove notification + result = nr.SetNotification (null); + Assert.AreEqual (StatusCode.OK, result, "Removing notification should succeed"); + } } }