Skip to content

[runtime] Fix a race condition when switching between weak and strong GCHandles. Fixes #24702.#24841

Draft
rolfbjarne wants to merge 4 commits intomainfrom
dev/rolf/gchandle-switching-race
Draft

[runtime] Fix a race condition when switching between weak and strong GCHandles. Fixes #24702.#24841
rolfbjarne wants to merge 4 commits intomainfrom
dev/rolf/gchandle-switching-race

Conversation

@rolfbjarne
Copy link
Member

@rolfbjarne rolfbjarne commented Mar 5, 2026

TODO:

  • Write better description.
  • Thorough review.
  • Validate fix.

Fixes #24702.

rolfbjarne and others added 4 commits March 5, 2026 09:43
Add a test that reproduces the race condition described in
#24702: one thread switches
an object's gchandle between weak and strong (via retain/release),
while another thread fetches the gchandle (by calling an exported
ObjC method). This can cause a crash when the fetching thread reads
a gchandle that has been freed by the switching thread.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of replacing a single gchandle between weak and strong (which
races with concurrent readers), use a dual-gchandle approach:

- A permanent weak gchandle is used for all lookups. This handle is
  created once and never modified, eliminating the race with readers.
- A separate strong gchandle is created/freed as needed to keep the
  managed object alive. This handle is stored in a global hash table
  and is never accessed by the reader path.

The one-time conversion from the initial strong handle to the weak
lookup handle happens during object initialization (from
RegisterToggleReferenceCoreCLR), before concurrent access is possible.

Fixes #24702

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace get/set_strong_gchandle with:

- clear_strong_gchandle: atomically looks up, removes, and frees the
  strong gchandle within a single lock acquisition.
- set_strong_gchandle: creates the gchandle before the lock, then
  atomically checks if one already exists. If so, frees the newly
  created one; otherwise stores it.

This eliminates TOCTOU races where two threads could both see no
strong gchandle and both try to set one, or where a gchandle could
be freed between lookup and removal.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The initial GCHandle stored on native objects is always a weak handle
(GCHandleType.WeakTrackResurrection), created in CreateManagedRef in
NSObject2.cs. Since it's always weak, the one-time strong-to-weak
conversion branch in xamarin_switch_gchandle is dead code.

This simplifies xamarin_switch_gchandle to just manage the strong
gchandle in the hash table without any flag checking, and removes the
XamarinGCHandleFlags_WeakGCHandle flag from both the native enum and
the managed enum since it is no longer used.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR changes the runtime’s GCHandle switching mechanism to avoid a race when transitioning between weak/strong handles for user types, and adds a regression test to exercise the concurrency scenario reported in #24702.

Changes:

  • Reworks xamarin_switch_gchandle to keep the per-object gc_handle stable (weak) and manage strong handles separately in a global map.
  • Removes the WeakGCHandle bit from the managed/native XamarinGCHandleFlags enums.
  • Adds GCHandleSwitchRaceTest to reproduce the retain/release vs exported-method lookup race.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
tests/monotouch-test/ObjCRuntime/GCHandleSwitchRaceTest.cs New test that races retain/release against an exported ObjC method call to catch the GCHandle switching race.
src/Foundation/NSObject2.cs Removes WeakGCHandle flag usage when creating managed refs for user types.
runtime/xamarin/trampolines.h Keeps native XamarinGCHandleFlags enum in sync with managed changes (removes WeakGCHandle).
runtime/runtime.m Implements the dual-GCHandle scheme using a global strong-handle dictionary and updates xamarin_switch_gchandle/cleanup logic.
Comments suppressed due to low confidence (3)

tests/monotouch-test/ObjCRuntime/GCHandleSwitchRaceTest.cs:26

  • The PR description still contains TODO items (“Write better description/Thorough review/Validate fix”). Given this is a runtime lifetime/GCHandle change, please replace the TODOs with a concrete description of the root cause, why the dual-GCHandle scheme is safe, and what test configurations (platform/registrar/linker) were used to validate the fix.
			// This test verifies a race condition in xamarin_switch_gchandle:
			// Thread A switches between weak/strong gchandles (via retain/release),
			// while Thread B fetches the gchandle (by calling an exported ObjC method).
			// The race is that Thread B can read the old gchandle after Thread A has
			// freed it but before Thread A has set the new one.
			// Ref: https://github.com/dotnet/macios/issues/24702
			var done = new ManualResetEvent (false);

runtime/runtime.m:1603

  • set_strong_gchandle always allocates a new strong GCHandle before checking if one already exists. Since xamarin_switch_gchandle is called on every retain, this can turn into an attach/lookup + allocate/free on every retain even when the object is already in strong mode. Consider checking for an existing strong handle first (and early-return in xamarin_switch_gchandle when no transition is needed), only creating a new GCHandle when the dictionary doesn't already contain one.
	GCHandle new_gchandle = xamarin_gchandle_new (managed_object, FALSE);

	GCHandle existing = INVALID_GCHANDLE;
	pthread_mutex_lock (&strong_gchandle_hash_lock);
	if (strong_gchandle_hash == NULL)
		strong_gchandle_hash = CFDictionaryCreateMutable (kCFAllocatorDefault, 0, NULL, NULL);
	const void *value;
	if (CFDictionaryGetValueIfPresent (strong_gchandle_hash, self, &value))
		existing = (GCHandle) value;
	if (existing == INVALID_GCHANDLE)
		CFDictionarySetValue (strong_gchandle_hash, self, (const void *) new_gchandle);
	pthread_mutex_unlock (&strong_gchandle_hash_lock);

runtime/runtime.m:1602

  • GCHandle is being stored in the CFDictionary by casting it directly to/from const void* ((const void *) new_gchandle / (GCHandle) value). If GCHandle is an integer type, this is implementation-defined and may also trigger compiler warnings depending on settings. Consider casting via uintptr_t (both when storing and retrieving) or store a small heap-allocated struct with proper CFDictionary value callbacks (similar to gchandle_hash in trampolines.m).
	const void *value;
	if (CFDictionaryGetValueIfPresent (strong_gchandle_hash, self, &value))
		existing = (GCHandle) value;
	if (existing == INVALID_GCHANDLE)
		CFDictionarySetValue (strong_gchandle_hash, self, (const void *) new_gchandle);

You can also share your feedback on Copilot code review. Take the survey.

@vs-mobiletools-engineering-service2
Copy link
Collaborator

✅ [CI Build #6fd97a9] Build passed (Build packages) ✅

Pipeline on Agent
Hash: 6fd97a9c0e7191a68d6eed1e1616ecbeafbadb3f [PR build]

@vs-mobiletools-engineering-service2
Copy link
Collaborator

✅ [PR Build #6fd97a9] Build passed (Detect API changes) ✅

Pipeline on Agent
Hash: 6fd97a9c0e7191a68d6eed1e1616ecbeafbadb3f [PR build]

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2
Copy link
Collaborator

✅ API diff for current PR / commit

NET (empty diffs)

✅ API diff vs stable

NET (empty diffs)

ℹ️ Generator diff

Generator Diff: vsdrops (html) vsdrops (raw diff) gist (raw diff) - Please review changes)

Pipeline on Agent
Hash: 6fd97a9c0e7191a68d6eed1e1616ecbeafbadb3f [PR build]

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2
Copy link
Collaborator

🔥 [CI Build #6fd97a9] Build failed (Build macOS tests) 🔥

Build failed for the job 'Build macOS tests' (with job status 'Failed')

Pipeline on Agent
Hash: 6fd97a9c0e7191a68d6eed1e1616ecbeafbadb3f [PR build]

@vs-mobiletools-engineering-service2
Copy link
Collaborator

🔥 [CI Build #6fd97a9] Test results 🔥

Test results

❌ Tests failed on VSTS: test results

5 tests crashed, 31 tests failed, 100 tests passed.

Failures

❌ dotnettests tests (iOS) [attempt 2]

1 tests failed, 0 tests passed.

Failed tests

  • DotNet tests: Failed (Execution failed with exit code 1)
    • Xamarin.Tests.AppSizeTest.MonoVM_Interpreter(iOS,"ios-arm64"): No removed APIs (set the environment variable WRITE_KNOWN_FAILURES=1 and run the test again to update the expected set of APIs...
    • Xamarin.Tests.AppSizeTest.MonoVM(iOS,"ios-arm64"): No removed APIs (set the environment variable WRITE_KNOWN_FAILURES=1 and run the test again to update the expected set of APIs...

Html Report (VSDrops) Download

❌ dotnettests tests (MacCatalyst) [attempt 2]

1 tests failed, 0 tests passed.

Failed tests

  • DotNet tests: Failed (Execution failed with exit code 1)
    • Xamarin.Tests.DotNetProjectTest.BuildFatMonoTouchTest(MacCatalys...: 'dotnet build' failed with exit code 1
      Full command: /Users/builder/azdo/_work/3/s/macios/builds/downloads/dotnet-sdk-10.0.300-p...

Html Report (VSDrops) Download

❌ dotnettests tests (macOS) [attempt 2]

1 tests failed, 0 tests passed.

Failed tests

  • DotNet tests: Failed (Execution failed with exit code 1)
    • Xamarin.Tests.DotNetProjectTest.BuildFatMonoTouchTest(MacOSX,"os...: 'dotnet build' failed with exit code 1
      Full command: /Users/builder/azdo/_work/3/s/macios/builds/downloads/dotnet-sdk-10.0.300-p...

Html Report (VSDrops) Download

❌ monotouch tests (iOS) [attempt 2]

1 tests failed, 10 tests passed.

Failed tests

  • monotouch-test/iOS - simulator/Debug (interpreter): Crashed

Html Report (VSDrops) Download

❌ monotouch tests (MacCatalyst) [attempt 2]

15 tests failed, 0 tests passed.

Failed tests

  • monotouch-test/Mac Catalyst/Debug: BuildFailure
  • monotouch-test/Mac Catalyst/Debug (ARM64): BuildFailure ( (failed to parse the logs: The Writer is closed or in error state.))
  • monotouch-test/Mac Catalyst/Debug (managed static registrar): BuildFailure
  • monotouch-test/Mac Catalyst/Debug (static registrar): BuildFailure ( (failed to parse the logs: The Writer is closed or in error state.))
  • monotouch-test/Mac Catalyst/Debug (static registrar, ARM64): BuildFailure
  • monotouch-test/Mac Catalyst/Release (managed static registrar): BuildFailure
  • monotouch-test/Mac Catalyst/Release (managed static registrar, all optimizations): BuildFailure
  • monotouch-test/Mac Catalyst/Release (NativeAOT): BuildFailure ( (failed to parse the logs: The Writer is closed or in error state.))
  • monotouch-test/Mac Catalyst/Release (NativeAOT, ARM64): BuildFailure
  • monotouch-test/Mac Catalyst/Release (NativeAOT, x64): BuildFailure
  • monotouch-test/Mac Catalyst/Release (static registrar): BuildFailure
  • monotouch-test/Mac Catalyst/Release (static registrar, all optimizations): BuildFailure ( (failed to parse the logs: The Writer is closed or in error state.))
  • monotouch-test/Mac Catalyst/Release (ARM64, LLVM): BuildFailure
  • monotouch-test/Mac Catalyst/Debug (interpreter): BuildFailure
  • monotouch-test/Mac Catalyst/Release (interpreter): BuildFailure

Html Report (VSDrops) Download

❌ monotouch tests (macOS) [attempt 2]

12 tests failed, 0 tests passed.

Failed tests

  • monotouch-test/macOS/Debug: BuildFailure
  • monotouch-test/macOS/Debug (ARM64): BuildFailure ( (failed to parse the logs: The Writer is closed or in error state.))
  • monotouch-test/macOS/Debug (managed static registrar): BuildFailure
  • monotouch-test/macOS/Debug (static registrar): BuildFailure ( (failed to parse the logs: The Writer is closed or in error state.))
  • monotouch-test/macOS/Debug (static registrar, ARM64): BuildFailure
  • monotouch-test/macOS/Release (managed static registrar): BuildFailure
  • monotouch-test/macOS/Release (managed static registrar, all optimizations): BuildFailure
  • monotouch-test/macOS/Release (NativeAOT): BuildFailure
  • monotouch-test/macOS/Release (NativeAOT, ARM64): BuildFailure
  • monotouch-test/macOS/Release (NativeAOT, x64): BuildFailure
  • monotouch-test/macOS/Release (static registrar): BuildFailure
  • monotouch-test/macOS/Release (static registrar, all optimizations): BuildFailure

Html Report (VSDrops) Download

❌ Tests on macOS Monterey (12) tests

🔥 Failed catastrophically on VSTS: test results - mac_monterey (no summary found).

Html Report (VSDrops) Download

❌ Tests on macOS Ventura (13) tests

🔥 Failed catastrophically on VSTS: test results - mac_ventura (no summary found).

Html Report (VSDrops) Download

❌ Tests on macOS Sonoma (14) tests

🔥 Failed catastrophically on VSTS: test results - mac_sonoma (no summary found).

Html Report (VSDrops) Download

❌ Tests on macOS Sequoia (15) tests

🔥 Failed catastrophically on VSTS: test results - mac_sequoia (no summary found).

Html Report (VSDrops) Download

❌ Tests on macOS Tahoe (26) tests

🔥 Failed catastrophically on VSTS: test results - mac_tahoe (no summary found).

Html Report (VSDrops) Download

Successes

✅ cecil: All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests (Multiple platforms): All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests (tvOS): All 1 tests passed. Html Report (VSDrops) Download
✅ framework: All 2 tests passed. Html Report (VSDrops) Download
✅ fsharp: All 4 tests passed. Html Report (VSDrops) Download
✅ generator: All 5 tests passed. Html Report (VSDrops) Download
✅ interdependent-binding-projects: All 4 tests passed. Html Report (VSDrops) Download
✅ introspection: All 6 tests passed. Html Report (VSDrops) Download
✅ linker: All 44 tests passed. Html Report (VSDrops) Download
✅ monotouch (tvOS): All 11 tests passed. Html Report (VSDrops) Download
✅ msbuild: All 2 tests passed. Html Report (VSDrops) Download
✅ sharpie: All 1 tests passed. Html Report (VSDrops) Download
✅ windows: All 3 tests passed. Html Report (VSDrops) Download
✅ xcframework: All 4 tests passed. Html Report (VSDrops) Download
✅ xtro: All 1 tests passed. Html Report (VSDrops) Download

macOS tests

Pipeline on Agent
Hash: 6fd97a9c0e7191a68d6eed1e1616ecbeafbadb3f [PR build]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Test/crash in MonoTouchFixtures.UIKit.DocumentTest

3 participants