From da9673ffd84c36dc33cda77bf7c931feed5358ef Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 14 Aug 2026 14:40:04 +0200 Subject: [PATCH 1/2] test --- .../System.Runtime.Tests/System/GCTests.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/GCTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/GCTests.cs index 20b4fa37aa71f7..5aba4e2630a591 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/GCTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/GCTests.cs @@ -1200,5 +1200,70 @@ private unsafe static void AllocateArrayCheckPinning() } } } + + private sealed class GcRootBox + { + public int Value; + public GcRootBox Self; + public byte[] Payload; + + public GcRootBox(int v) + { + Value = v; + Self = this; + Payload = new byte[16]; + Payload[0] = (byte)v; + } + + public bool IsIntact(int expected) => + Value == expected && ReferenceEquals(Self, this) && Payload is not null && Payload[0] == (byte)expected; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static GcRootBox MakeGcRootBox(int v) => new GcRootBox(v); + + // Taking the address forces the caller's local to be an address-taken variable. + [MethodImpl(MethodImplOptions.NoInlining)] + private static void TouchGcRootBox(ref GcRootBox b) + { + if (b is null) + { + throw new InvalidOperationException(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void CollectAndScribble() + { + GC.Collect(); + byte[] scribble = new byte[64]; + scribble[0] = 1; + GC.KeepAlive(scribble); + } + + // Regression test for https://github.com/dotnet/runtime/issues/130592. On the Mono + // wasm LLVM-AOT backend a ref OP_MOVE alias of an address-taken local could be left + // rooted nowhere once the local was reassigned, letting the aliased object be collected + // while a live local still referenced it. The invariant -- an object reachable through a + // live local survives a collection -- holds on every runtime, so this only has teeth on + // wasm AOT (it must be in the browser Mono smoke set to run there). + [Fact] + public static void MovedAliasOfAddressTakenLocalIsRootedAcrossGC() + { + GcRootBox cur = MakeGcRootBox(0); + TouchGcRootBox(ref cur); + + for (int i = 1; i <= 128; i++) + { + GcRootBox alias = cur; // ref MOVE; sole remaining reference to box (i - 1) + cur = MakeGcRootBox(i); // overwrites cur's stack slot + TouchGcRootBox(ref cur); + CollectAndScribble(); + + Assert.True(alias.IsIntact(i - 1), + $"object referenced by a live local was lost across GC at iteration {i} (read Value={alias.Value})"); + GC.KeepAlive(alias); + } + } } } From b5010370215164cfcd706041633dffb5f6d8a232 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 14 Aug 2026 19:42:38 +0200 Subject: [PATCH 2/2] test --- .../tests/System.Runtime.Tests/System/GCTests.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/GCTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/GCTests.cs index 5aba4e2630a591..f84afe95ca7af7 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/GCTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/GCTests.cs @@ -1236,9 +1236,14 @@ private static void TouchGcRootBox(ref GcRootBox b) private static void CollectAndScribble() { GC.Collect(); - byte[] scribble = new byte[64]; - scribble[0] = 1; - GC.KeepAlive(scribble); + // Reclaim and overwrite any just-freed slot with same-sized objects so that an + // object lost across the collection is observable (it reads a filler's Value = -1). + GcRootBox[] filler = new GcRootBox[64]; + for (int j = 0; j < filler.Length; j++) + { + filler[j] = new GcRootBox(-1); + } + GC.KeepAlive(filler); } // Regression test for https://github.com/dotnet/runtime/issues/130592. On the Mono