Fix handling of collectible thread statics and GC (#40671)
authorDavid Wrighton <davidwr@microsoft.com>
Tue, 11 Aug 2020 21:54:08 +0000 (14:54 -0700)
committerGitHub <noreply@github.com>
Tue, 11 Aug 2020 21:54:08 +0000 (14:54 -0700)
- NonGc Thread statics helper returns a ByRef, not a pointer for collectible statics
- Add test coverage for collectible statics
- Remove test coverage for converting thread static variable into byref and using that to keep alive the static data as it wasn't fully safe itself

src/coreclr/src/jit/flowgraph.cpp
src/coreclr/tests/issues.targets
src/tests/Loader/CollectibleAssemblies/ByRefLocals/ByRefLocals.cs
src/tests/Loader/CollectibleAssemblies/ByRefLocals/Unloaded.cs
src/tests/Loader/CollectibleAssemblies/Statics/Accessor.cs [new file with mode: 0644]
src/tests/Loader/CollectibleAssemblies/Statics/Accessor.csproj [new file with mode: 0644]
src/tests/Loader/CollectibleAssemblies/Statics/CollectibleStatics.cs [new file with mode: 0644]
src/tests/Loader/CollectibleAssemblies/Statics/CollectibleStatics.csproj [new file with mode: 0644]
src/tests/Loader/CollectibleAssemblies/Statics/Unloaded.cs [new file with mode: 0644]
src/tests/Loader/CollectibleAssemblies/Statics/Unloaded.csproj [new file with mode: 0644]

index 242a4c5..48f3273 100644 (file)
@@ -7401,6 +7401,7 @@ GenTreeCall* Compiler::fgGetStaticsCCtorHelper(CORINFO_CLASS_HANDLE cls, CorInfo
         case CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE_DYNAMICCLASS:
         case CORINFO_HELP_GETSHARED_GCTHREADSTATIC_BASE:
         case CORINFO_HELP_GETSHARED_GCTHREADSTATIC_BASE_DYNAMICCLASS:
+        case CORINFO_HELP_GETSHARED_NONGCTHREADSTATIC_BASE_DYNAMICCLASS:
             // type = TYP_BYREF;
             break;
 
@@ -7414,7 +7415,6 @@ GenTreeCall* Compiler::fgGetStaticsCCtorHelper(CORINFO_CLASS_HANDLE cls, CorInfo
 
         case CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE:
         case CORINFO_HELP_GETSHARED_NONGCTHREADSTATIC_BASE:
-        case CORINFO_HELP_GETSHARED_NONGCTHREADSTATIC_BASE_DYNAMICCLASS:
         case CORINFO_HELP_CLASSINIT_SHARED_DYNAMICCLASS:
             type = TYP_I_IMPL;
             break;
index 246f85c..c3001db 100644 (file)
         <ExcludeList Include="$(XunitTestBinBase)/Loader/CollectibleAssemblies/ByRefLocals/**">
             <Issue>https://github.com/dotnet/runtime/issues/40394</Issue>
         </ExcludeList>
+        <ExcludeList Include="$(XunitTestBinBase)/Loader/CollectibleAssemblies/Statics/**">
+            <Issue>https://github.com/dotnet/runtime/issues/40394</Issue>
+        </ExcludeList>
         <ExcludeList Include="$(XunitTestBinBase)/Interop/PInvoke/Miscellaneous/CopyCtor/**">
             <Issue>Handling for Copy constructors isn't present in mono interop</Issue>
         </ExcludeList>
index 834bc47..fb2bd10 100644 (file)
@@ -40,15 +40,6 @@ class Program
     [MethodImpl(MethodImplOptions.NoInlining)]
     static int HoldAssembliesAliveThroughByRefFields(out GCHandle gch1, out GCHandle gch2)
     {
-        // ThreadStatic lifetime check. Here we don't require the actual assembly to remain loaded, but we do require the field to remain accessible
-        var spanThreadStatic = LoadAssemblyThreadStatic(out GCHandle gchThreadStatic);
-        GC.Collect(2);
-        GC.WaitForPendingFinalizers();
-        GC.Collect(2);
-        GC.WaitForPendingFinalizers();
-        GC.Collect(2);
-        GC.WaitForPendingFinalizers();
-
         var span1 = LoadAssembly(out gch1);
         GC.Collect(2);
         GC.WaitForPendingFinalizers();
@@ -61,7 +52,6 @@ class Program
         {
             Console.WriteLine(span1[0]);
             Console.WriteLine(span2[0]);
-            Console.WriteLine(spanThreadStatic[0]);
             GC.Collect(2);
             GC.WaitForPendingFinalizers();
             if (gch1.Target == null)
@@ -72,11 +62,6 @@ class Program
             {
                 return 2;
             }
-            if (spanThreadStatic[0] != 7)
-            {
-                Console.WriteLine($"spanThreadStatic[0] = {spanThreadStatic[0]}");
-                return 5;
-            }
         }
 
         return 100;
@@ -97,20 +82,6 @@ class Program
     }
 
     [MethodImpl(MethodImplOptions.NoInlining)]
-    private static ReadOnlySpan<byte> LoadAssemblyThreadStatic(out GCHandle gchToAssembly)
-    {
-        var alc = new AssemblyLoadContext("test", isCollectible: true);
-        var a = alc.LoadFromAssemblyPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Unloaded.dll"));
-        gchToAssembly = GCHandle.Alloc(a, GCHandleType.WeakTrackResurrection);
-
-        var spanAccessor = (IReturnSpan)Activator.CreateInstance(a.GetType("ThreadStaticSpanAccessor"));
-
-        alc.Unload();
-
-        return spanAccessor.GetSpan();
-    }
-
-    [MethodImpl(MethodImplOptions.NoInlining)]
     private static ReadOnlySpan<byte> CreateAssemblyDynamically(out GCHandle gchToAssembly)
     {
         AssemblyBuilder ab =
index 3f2615e..0a326df 100644 (file)
@@ -13,14 +13,3 @@ public class SpanAccessor : IReturnSpan
         return RawData;
     }
 }
-
-public class ThreadStaticSpanAccessor : IReturnSpan
-{
-    [ThreadStatic]
-    public static byte ThreadStaticByte = 7;
-
-    public unsafe ReadOnlySpan<byte> GetSpan()
-    {
-        return new ReadOnlySpan<byte>(Unsafe.AsPointer(ref ThreadStaticByte), 1);
-    }
-}
\ No newline at end of file
diff --git a/src/tests/Loader/CollectibleAssemblies/Statics/Accessor.cs b/src/tests/Loader/CollectibleAssemblies/Statics/Accessor.cs
new file mode 100644 (file)
index 0000000..69343ad
--- /dev/null
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+public interface IStaticTest
+{
+    void SetStatic(int val, int val2, int val3, int val4, int val5);
+    void GetStatic(out int val1, out int val2, out int val3, out int val4, out int val5);
+    void SetStaticObject(object val, object val2, object val3, object val4, object val5);
+    void GetStaticObject(out object val1, out object val2, out object val3, out object val4, out object val5);
+}
\ No newline at end of file
diff --git a/src/tests/Loader/CollectibleAssemblies/Statics/Accessor.csproj b/src/tests/Loader/CollectibleAssemblies/Statics/Accessor.csproj
new file mode 100644 (file)
index 0000000..528466d
--- /dev/null
@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <OutputType>Library</OutputType>
+    <CLRTestKind>BuildOnly</CLRTestKind>
+    <CLRTestPriority>0</CLRTestPriority>
+  </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="Accessor.cs" />
+  </ItemGroup>
+</Project>
\ No newline at end of file
diff --git a/src/tests/Loader/CollectibleAssemblies/Statics/CollectibleStatics.cs b/src/tests/Loader/CollectibleAssemblies/Statics/CollectibleStatics.cs
new file mode 100644 (file)
index 0000000..16cbb15
--- /dev/null
@@ -0,0 +1,54 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.IO;
+using System.Reflection;
+using System.Reflection.Emit;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Loader;
+
+class Program
+{
+    static int Main(string[] args)
+    {
+        var alc = new AssemblyLoadContext("test", isCollectible: true);
+        var a = alc.LoadFromAssemblyPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Unloaded.dll"));
+
+        var accessor = (IStaticTest)Activator.CreateInstance(a.GetType("StaticTest"));
+        accessor.SetStatic(12759, 548739, 5468, 8518, 9995);
+        accessor.GetStatic(out int val1, out int val2, out int val3, out int val4, out int val5);
+        if (val1 != 12759)
+            return 1;
+        if (val2 != 548739)
+            return 2;
+        if (val3 != 5468)
+            return 3;
+        if (val4 != 8518)
+            return 4;
+        if (val5 != 9995)
+            return 5;
+
+        object obj1 = new object();
+        object obj2 = new object();
+        object obj3 = new object();
+        object obj4 = new object();
+        object obj5 = new object();
+        accessor.SetStaticObject(obj1, obj2, obj3, obj4, obj5);
+        accessor.GetStaticObject(out object val1Obj, out object val2Obj, out object val3Obj, out object val4Obj, out object val5Obj);
+        if (val1Obj != obj1)
+            return 11;
+        if (val2Obj != obj2)
+            return 12;
+        if (val3Obj != obj3)
+            return 13;
+        if (val4Obj != obj4)
+            return 14;
+        if (val5Obj != obj5)
+            return 15;
+
+        GC.KeepAlive(accessor);
+        return 100;
+    }
+}
\ No newline at end of file
diff --git a/src/tests/Loader/CollectibleAssemblies/Statics/CollectibleStatics.csproj b/src/tests/Loader/CollectibleAssemblies/Statics/CollectibleStatics.csproj
new file mode 100644 (file)
index 0000000..27b8b2a
--- /dev/null
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <OutputType>Exe</OutputType>
+    <CLRTestKind>BuildAndRun</CLRTestKind>
+    <CLRTestPriority>0</CLRTestPriority>
+  </PropertyGroup>
+  <ItemGroup>
+    <ProjectReference Include="Unloaded.csproj" />
+    <ProjectReference Include="Accessor.csproj" />
+    <Compile Include="CollectibleStatics.cs" />
+  </ItemGroup>
+</Project>
\ No newline at end of file
diff --git a/src/tests/Loader/CollectibleAssemblies/Statics/Unloaded.cs b/src/tests/Loader/CollectibleAssemblies/Statics/Unloaded.cs
new file mode 100644 (file)
index 0000000..447b8d9
--- /dev/null
@@ -0,0 +1,113 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Runtime.CompilerServices;
+
+// Use multiple classes to trigger multiple statics allocation events
+public class StaticTest2
+{
+    [ThreadStatic]
+    public static int ThreadStaticValue;
+    public static int StaticValue;
+}
+
+public class StaticTest3
+{
+    public static int StaticValue;
+}
+
+public class StaticTest4
+{
+    [ThreadStatic]
+    public static object ThreadStaticValue;
+    public static object StaticValue;
+}
+public class StaticTest5
+{
+    [ThreadStatic]
+    public static object ThreadStaticValue;
+    public static object StaticValue;
+}
+public class StaticTest6
+{
+    public static object StaticValue;
+}
+
+
+public class StaticTest : IStaticTest
+{
+    [ThreadStatic]
+    public static int ThreadStaticValue;
+    public static int StaticValue;
+
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    public static void SetValues(out int valTargetA, int valA
+                                , out int valTargetB, int valB
+                                , out int valTargetC, int valC
+                                , out int valTargetD, int valD
+                                , out int valTargetE, int valE
+                                )
+    {
+        valTargetA = valA;
+        valTargetB = valB;
+        valTargetC = valC;
+        valTargetD = valD;
+        valTargetE = valE;
+    }
+
+    public void SetStatic(int val, int val2, int val3, int val4, int val5)
+    {
+        // Use this odd pathway to increase the chance that in the presence of GCStress issues will be found
+        SetValues(out ThreadStaticValue, val
+                , out StaticValue, val2
+                , out StaticTest2.StaticValue, val3
+                , out StaticTest2.ThreadStaticValue, val4
+                , out StaticTest3.StaticValue, val5
+                );
+    }
+
+    public void GetStatic(out int val1, out int val2, out int val3, out int val4, out int val5)
+    {
+        val1 = ThreadStaticValue;
+        val2 = StaticValue;
+        val3 = StaticTest2.StaticValue;
+        val4 = StaticTest2.ThreadStaticValue;
+        val5 = StaticTest3.StaticValue;
+    }
+
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    public static void SetValuesObject(out object valTargetA, object valA
+                                , out object valTargetB, object valB
+                                , out object valTargetC, object valC
+                                , out object valTargetD, object valD
+                                , out object valTargetE, object valE
+                                )
+    {
+        valTargetA = valA;
+        valTargetB = valB;
+        valTargetC = valC;
+        valTargetD = valD;
+        valTargetE = valE;
+    }
+
+    public void SetStaticObject(object val, object val2, object val3, object val4, object val5)
+    {
+        // Use this odd pathway to increase the chance that in the presence of GCStress issues will be found
+        SetValuesObject(out StaticTest4.ThreadStaticValue, val
+                , out StaticTest4.StaticValue, val2
+                , out StaticTest5.StaticValue, val3
+                , out StaticTest5.ThreadStaticValue, val4
+                , out StaticTest6.StaticValue, val5
+                );
+    }
+
+    public void GetStaticObject(out object val1, out object val2, out object val3, out object val4, out object val5)
+    {
+        val1 = StaticTest4.ThreadStaticValue;
+        val2 = StaticTest4.StaticValue;
+        val3 = StaticTest5.StaticValue;
+        val4 = StaticTest5.ThreadStaticValue;
+        val5 = StaticTest6.StaticValue;
+    }
+}
diff --git a/src/tests/Loader/CollectibleAssemblies/Statics/Unloaded.csproj b/src/tests/Loader/CollectibleAssemblies/Statics/Unloaded.csproj
new file mode 100644 (file)
index 0000000..4d5f1d8
--- /dev/null
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <OutputType>Library</OutputType>
+    <CLRTestKind>BuildOnly</CLRTestKind>
+    <CLRTestPriority>0</CLRTestPriority>
+  </PropertyGroup>
+  <ItemGroup>
+    <ProjectReference Include="Accessor.csproj" />
+    <Compile Include="Unloaded.cs" />
+  </ItemGroup>
+</Project>
\ No newline at end of file