Skip to content

[Mono] Interpreter: Missing write barrier in Delegate field assignment (+=) causes GC consistency error and crash in Delegate.Remove #124941

@Liangjia0411

Description

@Liangjia0411

Description

We are encountering random crashes in Delegate.Remove (specifically around InternalEqualTypes / MINT_INTRINS_GET_TYPE) when running on Mono Interpreter (iOS, Android, and Windows).

All signs point to a GC liveness/rooting issue where the Delegate object is prematurely collected by a Minor GC, resulting in a wild pointer access during the subsequent Delegate.Remove call.

After enabling the GC debug option check-remset-consistency, we caught a definitive log indicating a missing write barrier.

Configuration

  • Version: .NET 9.0.11 (Mono Runtime)
  • OS: iOS, Android, Windows
  • Execution Mode: Interpreter enabled
  • GC: SGen

Reproduction Steps

The issue occurs with C# event operations involving a long-lived object (Old Gen) referencing a short-lived Delegate (New Gen).

Pseudo-code context:

// Scenario: Use a custom EventProxy to manage events on a long-lived EventDispatcher.
partial class EventProxy
{
    // EventDispatcher is a long-lived object (likely in Old Gen)
    readonly EventDispatcher mDispatcher; 
    
    // ... verification logic ...

    public event Action ActivityNewyear2026RefreshPreviewSellPrice
    {
        add
        {
            // The 'value' is a new Delegate (likely in New Gen/Nursery).
            // We verify logic and then modify the field on mDispatcher.
            
            // PROBLEM TRIGGER: This operation (+=) seems to miss a Write Barrier in Interpreter mode.
            mDispatcher.EventActivityNewyear2026RefreshPreviewSellPrice += value;
        }
        remove
        {
             // CRASH HAPPENS HERE:
             // When attempting to remove, 'value' or the field in 'mDispatcher' 
             // points to garbage memory because it was collected in a prior Minor GC.
             mDispatcher.EventActivityNewyear2026RefreshPreviewSellPrice -= value;
        }
    }
}

partial class EventDispatcher
{
    // This object is promoted to Old Space.
    // Offset 8888 corresponds to this field.
    public event Action? EventActivityNewyear2026RefreshPreviewSellPrice = null; 
}

Analysis & Logs

We enabled the GC consistency check via environment variable/argument:
MONO_GC_DEBUG=check-remset-consistency

After executing the += operation (add), the following error is logged immediately before the crash occurs in a subsequent operation:

59:46 Begin heap consistency check...
2026-02-27 10:59:46 Oldspace->newspace reference 000002770A8153D0 at offset 8888 in object 0000027709A63798 (Tenth.EventDispatcher) not found in remsets.
2026-02-27 10:59:46 Heap consistency check done.

Interpretation:

  1. Tenth.EventDispatcher (0000027709A63798) is in Old Gen (Oldspace).
  2. The field at offset 8888 (EventActivityNewyear2026RefreshPreviewSellPrice) references 000002770A8153D0.
  3. The referenced object 000002770A8153D0 (The Delegate) is in New Gen (Newspace).
  4. Error: "not found in remsets".

This confirms that the Write Barrier was missing when the Interpreter executed the field assignment (likely inside the Combine implementation or the storing of the combined delegate back to the field).

Because the remset does not record this relationship, the subsequent Minor GC assumes the Delegate is unreachable (if no other roots exist) and reclaims it. When Delegate.Remove is called later, it tries to access this reclaimed object, causing a crash or ArgumentException inside InternalEqualTypes.

Related Issues

We are aware of issue #85318 which discusses missing write barriers, but that issue focuses on native code modifying managed fields. In our case, all operations (add/remove) are performed in pure C#, suggesting the issue lies within the Mono Interpreter's handling of delegate field updates (possibly stfld or specific delegate intrinsics).

Impact

This causes random stability issues on all platforms using the Mono Interpreter (including iOS/Android release builds) that are extremely hard to debug without consistency checks.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions