Skip to content

Commit 04a5836

Browse files
github-actions[bot]StephenMolloyjanvorli
authored
[release/6.0] 1388 xml serializer assembly load context awareness (#61266)
* Generate dynamic serialization assembly in the appropriate ALC, and don't keep any hard refs to types that could prevent unloading. * Add small test for ALC unloading. * PR feedback; Move into TempAssembly; Strong/Weak lookup tables; Test refinement * Refining TempAssembly cache; Reflection-based fix; Test corrections * Mono/wasm test fixup * Ensure that serializers from XmlMappings don't run afoul of collectible ALCs. * PR feedback * Unloadable contexts not yet supported in Mono. * Fix trimming of types for XmlSerializer tests that got moved out of main test assembly. * Encapsulating the duality of dealing with unloadable ALC instead of rewriting similar code everywhere. * Missed new file in last commit. * Compilation bug not seen locally. * Perf improvement. * Remove extraneous line. Test feedback. * Fix unloadability (#58948) There is a bug in AppDomain::FindAssembly code path that iterates over the domain assemblies. The iteration loop leaves the refcount of the LoaderAllocator related to the returned DomainAssembly bumped, but nothing ever decrements it. So when a code that needs to be unloaded ends up in that code path, all the managed things like managed LoaderAllocator, LoaderAllocatorScout are destroyed, but the unloading doesn't complete due to the refcount. We have never found it before as this code path is never executed in any of the coreclr tests even with unloadability testing option. (cherry picked from commit 8b38c19) Co-authored-by: Steve Molloy <[email protected]> Co-authored-by: Jan Vorlicek <[email protected]>
1 parent 3f6d8fa commit 04a5836

File tree

13 files changed

+418
-195
lines changed

13 files changed

+418
-195
lines changed

src/coreclr/vm/appdomain.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3533,8 +3533,8 @@ DomainAssembly * AppDomain::FindAssembly(PEAssembly * pFile, FindAssemblyOptions
35333533
!pManifestFile->IsResource() &&
35343534
pManifestFile->Equals(pFile))
35353535
{
3536-
// Caller already has PEAssembly, so we can give DomainAssembly away freely without AddRef
3537-
return pDomainAssembly.Extract();
3536+
// Caller already has PEAssembly, so we can give DomainAssembly away freely without added reference
3537+
return pDomainAssembly.GetValue();
35383538
}
35393539
}
35403540
return NULL;

src/libraries/Common/tests/System/Runtime/Serialization/Utils.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
using System.Threading.Tasks;
1010
using System.Xml.Linq;
1111
using System.Linq;
12+
using System.Reflection;
13+
using System.Runtime.Loader;
1214
using Xunit;
1315

1416
internal static class Utils
@@ -351,3 +353,30 @@ private static bool IsPrefixedAttributeValue(string atrValue, out string localPr
351353
return false;
352354
}
353355
}
356+
357+
internal class TestAssemblyLoadContext : AssemblyLoadContext
358+
{
359+
private AssemblyDependencyResolver _resolver;
360+
361+
public TestAssemblyLoadContext(string name, bool isCollectible, string mainAssemblyToLoadPath = null) : base(name, isCollectible)
362+
{
363+
if (!PlatformDetection.IsBrowser)
364+
_resolver = new AssemblyDependencyResolver(mainAssemblyToLoadPath ?? Assembly.GetExecutingAssembly().Location);
365+
}
366+
367+
protected override Assembly Load(AssemblyName name)
368+
{
369+
if (PlatformDetection.IsBrowser)
370+
{
371+
return base.Load(name);
372+
}
373+
374+
string assemblyPath = _resolver.ResolveAssemblyToPath(name);
375+
if (assemblyPath != null)
376+
{
377+
return LoadFromAssemblyPath(assemblyPath);
378+
}
379+
380+
return null;
381+
}
382+
}

src/libraries/System.Private.Xml/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2787,6 +2787,9 @@
27872787
<data name="XmlNotSerializable" xml:space="preserve">
27882788
<value>Type '{0}' is not serializable.</value>
27892789
</data>
2790+
<data name="XmlTypeInBadLoadContext" xml:space="preserve">
2791+
<value>Type '{0}' is from an AssemblyLoadContext which is incompatible with that which contains this XmlSerializer.</value>
2792+
</data>
27902793
<data name="XmlPregenInvalidXmlSerializerAssemblyAttribute" xml:space="preserve">
27912794
<value>Invalid XmlSerializerAssemblyAttribute usage. Please use {0} property or {1} property.</value>
27922795
</data>

src/libraries/System.Private.Xml/src/System.Private.Xml.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@
446446
<Compile Include="System\Xml\Serialization\CodeIdentifiers.cs" />
447447
<Compile Include="System\Xml\Serialization\Compilation.cs" />
448448
<Compile Include="System\Xml\Serialization\Compiler.cs" />
449+
<Compile Include="System\Xml\Serialization\ContextAwareTables.cs" />
449450
<Compile Include="System\Xml\Serialization\ImportContext.cs" />
450451
<Compile Include="System\Xml\Serialization\indentedWriter.cs" />
451452
<Compile Include="System\Xml\Serialization\IXmlSerializable.cs" />
@@ -565,6 +566,7 @@
565566
<Reference Include="System.Runtime.CompilerServices.Unsafe" />
566567
<Reference Include="System.Runtime.Extensions" />
567568
<Reference Include="System.Runtime.InteropServices" />
569+
<Reference Include="System.Runtime.Loader" />
568570
<Reference Include="System.Security.Cryptography.Algorithms" />
569571
<Reference Include="System.Security.Cryptography.Primitives" />
570572
<Reference Include="System.Text.Encoding.Extensions" />

src/libraries/System.Private.Xml/src/System/Xml/Serialization/Compilation.cs

Lines changed: 183 additions & 126 deletions
Large diffs are not rendered by default.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System.Xml.Serialization
5+
{
6+
using System;
7+
using System.Collections;
8+
using System.Diagnostics.CodeAnalysis;
9+
using System.Runtime.CompilerServices;
10+
using System.Runtime.Loader;
11+
12+
internal class ContextAwareTables<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]T> where T : class?
13+
{
14+
private Hashtable _defaultTable;
15+
private ConditionalWeakTable<Type, T> _collectibleTable;
16+
17+
public ContextAwareTables()
18+
{
19+
_defaultTable = new Hashtable();
20+
_collectibleTable = new ConditionalWeakTable<Type, T>();
21+
}
22+
23+
internal T GetOrCreateValue(Type t, Func<T> f)
24+
{
25+
// The fast and most common default case
26+
T? ret = (T?)_defaultTable[t];
27+
if (ret != null)
28+
return ret;
29+
30+
// Common case for collectible contexts
31+
if (_collectibleTable.TryGetValue(t, out ret))
32+
return ret;
33+
34+
// Not found. Do the slower work of creating the value in the correct collection.
35+
AssemblyLoadContext? alc = AssemblyLoadContext.GetLoadContext(t.Assembly);
36+
37+
// Null and non-collectible load contexts use the default table
38+
if (alc == null || !alc.IsCollectible)
39+
{
40+
lock (_defaultTable)
41+
{
42+
if ((ret = (T?)_defaultTable[t]) == null)
43+
{
44+
ret = f();
45+
_defaultTable[t] = ret;
46+
}
47+
}
48+
}
49+
50+
// Collectible load contexts should use the ConditionalWeakTable so they can be unloaded
51+
else
52+
{
53+
lock (_collectibleTable)
54+
{
55+
if (!_collectibleTable.TryGetValue(t, out ret))
56+
{
57+
ret = f();
58+
_collectibleTable.AddOrUpdate(t, ret);
59+
}
60+
}
61+
}
62+
63+
return ret;
64+
}
65+
}
66+
}

src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -627,40 +627,48 @@ private static void AddObjectsIntoTargetCollection(object targetCollection, List
627627
}
628628
}
629629

630-
private static readonly ConcurrentDictionary<(Type, string), ReflectionXmlSerializationReaderHelper.SetMemberValueDelegate> s_setMemberValueDelegateCache = new ConcurrentDictionary<(Type, string), ReflectionXmlSerializationReaderHelper.SetMemberValueDelegate>();
630+
private static readonly ContextAwareTables<Hashtable> s_setMemberValueDelegateCache = new ContextAwareTables<Hashtable>();
631631

632632
[RequiresUnreferencedCode(XmlSerializer.TrimSerializationWarning)]
633633
private static ReflectionXmlSerializationReaderHelper.SetMemberValueDelegate GetSetMemberValueDelegate(object o, string memberName)
634634
{
635635
Debug.Assert(o != null, "Object o should not be null");
636636
Debug.Assert(!string.IsNullOrEmpty(memberName), "memberName must have a value");
637-
(Type, string) typeMemberNameTuple = (o.GetType(), memberName);
638-
if (!s_setMemberValueDelegateCache.TryGetValue(typeMemberNameTuple, out ReflectionXmlSerializationReaderHelper.SetMemberValueDelegate? result))
637+
Type type = o.GetType();
638+
var delegateCacheForType = s_setMemberValueDelegateCache.GetOrCreateValue(type, () => new Hashtable());
639+
var result = delegateCacheForType[memberName];
640+
if (result == null)
639641
{
640-
MemberInfo memberInfo = ReflectionXmlSerializationHelper.GetEffectiveSetInfo(o.GetType(), memberName);
641-
Debug.Assert(memberInfo != null, "memberInfo could not be retrieved");
642-
Type memberType;
643-
if (memberInfo is PropertyInfo propInfo)
644-
{
645-
memberType = propInfo.PropertyType;
646-
}
647-
else if (memberInfo is FieldInfo fieldInfo)
648-
{
649-
memberType = fieldInfo.FieldType;
650-
}
651-
else
642+
lock (delegateCacheForType)
652643
{
653-
throw new InvalidOperationException(SR.XmlInternalError);
654-
}
644+
if ((result = delegateCacheForType[memberName]) == null)
645+
{
646+
MemberInfo memberInfo = ReflectionXmlSerializationHelper.GetEffectiveSetInfo(o.GetType(), memberName);
647+
Debug.Assert(memberInfo != null, "memberInfo could not be retrieved");
648+
Type memberType;
649+
if (memberInfo is PropertyInfo propInfo)
650+
{
651+
memberType = propInfo.PropertyType;
652+
}
653+
else if (memberInfo is FieldInfo fieldInfo)
654+
{
655+
memberType = fieldInfo.FieldType;
656+
}
657+
else
658+
{
659+
throw new InvalidOperationException(SR.XmlInternalError);
660+
}
655661

656-
MethodInfo getSetMemberValueDelegateWithTypeGenericMi = typeof(ReflectionXmlSerializationReaderHelper).GetMethod("GetSetMemberValueDelegateWithType", BindingFlags.Static | BindingFlags.Public)!;
657-
MethodInfo getSetMemberValueDelegateWithTypeMi = getSetMemberValueDelegateWithTypeGenericMi.MakeGenericMethod(o.GetType(), memberType);
658-
var getSetMemberValueDelegateWithType = (Func<MemberInfo, ReflectionXmlSerializationReaderHelper.SetMemberValueDelegate>)getSetMemberValueDelegateWithTypeMi.CreateDelegate(typeof(Func<MemberInfo, ReflectionXmlSerializationReaderHelper.SetMemberValueDelegate>));
659-
result = getSetMemberValueDelegateWithType(memberInfo);
660-
s_setMemberValueDelegateCache.TryAdd(typeMemberNameTuple, result);
662+
MethodInfo getSetMemberValueDelegateWithTypeGenericMi = typeof(ReflectionXmlSerializationReaderHelper).GetMethod("GetSetMemberValueDelegateWithType", BindingFlags.Static | BindingFlags.Public)!;
663+
MethodInfo getSetMemberValueDelegateWithTypeMi = getSetMemberValueDelegateWithTypeGenericMi.MakeGenericMethod(o.GetType(), memberType);
664+
var getSetMemberValueDelegateWithType = (Func<MemberInfo, ReflectionXmlSerializationReaderHelper.SetMemberValueDelegate>)getSetMemberValueDelegateWithTypeMi.CreateDelegate(typeof(Func<MemberInfo, ReflectionXmlSerializationReaderHelper.SetMemberValueDelegate>));
665+
result = getSetMemberValueDelegateWithType(memberInfo);
666+
delegateCacheForType[memberName] = result;
667+
}
668+
}
661669
}
662670

663-
return result;
671+
return (ReflectionXmlSerializationReaderHelper.SetMemberValueDelegate)result;
664672
}
665673

666674
private object? GetMemberValue(object o, MemberInfo memberInfo)

src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ namespace System.Xml.Serialization
1919
using System.Xml.Serialization;
2020
using System.Xml;
2121
using System.Diagnostics.CodeAnalysis;
22+
using System.Runtime.CompilerServices;
2223

2324
///<internalonly/>
2425
public abstract class XmlSerializationWriter : XmlSerializationGeneratedCode
@@ -1465,14 +1466,13 @@ internal static class DynamicAssemblies
14651466
{
14661467
private static readonly Hashtable s_nameToAssemblyMap = new Hashtable();
14671468
private static readonly Hashtable s_assemblyToNameMap = new Hashtable();
1468-
private static readonly Hashtable s_tableIsTypeDynamic = Hashtable.Synchronized(new Hashtable());
1469+
private static readonly ContextAwareTables<object> s_tableIsTypeDynamic = new ContextAwareTables<object>();
14691470

14701471
// SxS: This method does not take any resource name and does not expose any resources to the caller.
14711472
// It's OK to suppress the SxS warning.
14721473
internal static bool IsTypeDynamic(Type type)
14731474
{
1474-
object? oIsTypeDynamic = s_tableIsTypeDynamic[type];
1475-
if (oIsTypeDynamic == null)
1475+
object oIsTypeDynamic = s_tableIsTypeDynamic.GetOrCreateValue(type, () =>
14761476
{
14771477
Assembly assembly = type.Assembly;
14781478
bool isTypeDynamic = assembly.IsDynamic /*|| string.IsNullOrEmpty(assembly.Location)*/;
@@ -1500,8 +1500,8 @@ internal static bool IsTypeDynamic(Type type)
15001500
}
15011501
}
15021502
}
1503-
s_tableIsTypeDynamic[type] = oIsTypeDynamic = isTypeDynamic;
1504-
}
1503+
return isTypeDynamic;
1504+
});
15051505
return (bool)oIsTypeDynamic;
15061506
}
15071507

src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializer.cs

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.IO;
1111
using System.Reflection;
1212
using System.Runtime.CompilerServices;
13+
using System.Runtime.Loader;
1314
using System.Runtime.Versioning;
1415
using System.Security;
1516
using System.Text;
@@ -161,8 +162,7 @@ private static XmlSerializerNamespaces DefaultNamespaces
161162
internal const string TrimSerializationWarning = "Members from serialized types may be trimmed if not referenced directly";
162163
private const string TrimDeserializationWarning = "Members from deserialized types may be trimmed if not referenced directly";
163164

164-
private static readonly Dictionary<Type, Dictionary<XmlSerializerMappingKey, XmlSerializer>> s_xmlSerializerTable = new Dictionary<Type, Dictionary<XmlSerializerMappingKey, XmlSerializer>>();
165-
165+
private static readonly ContextAwareTables<Dictionary<XmlSerializerMappingKey, XmlSerializer>> s_xmlSerializerTable = new ContextAwareTables<Dictionary<XmlSerializerMappingKey, XmlSerializer>>();
166166
protected XmlSerializer()
167167
{
168168
}
@@ -235,30 +235,28 @@ public XmlSerializer(Type type, string? defaultNamespace)
235235
_tempAssembly = s_cache[defaultNamespace, type];
236236
if (_tempAssembly == null)
237237
{
238+
XmlSerializerImplementation? contract = null;
239+
Assembly? assembly = TempAssembly.LoadGeneratedAssembly(type, defaultNamespace, out contract);
240+
if (assembly == null)
238241
{
239-
XmlSerializerImplementation? contract = null;
240-
Assembly? assembly = TempAssembly.LoadGeneratedAssembly(type, defaultNamespace, out contract);
241-
if (assembly == null)
242-
{
243-
if (Mode == SerializationMode.PreGenOnly)
244-
{
245-
AssemblyName name = type.Assembly.GetName();
246-
var serializerName = Compiler.GetTempAssemblyName(name, defaultNamespace);
247-
throw new FileLoadException(SR.Format(SR.FailLoadAssemblyUnderPregenMode, serializerName));
248-
}
249-
250-
// need to reflect and generate new serialization assembly
251-
XmlReflectionImporter importer = new XmlReflectionImporter(defaultNamespace);
252-
_mapping = importer.ImportTypeMapping(type, null, defaultNamespace);
253-
_tempAssembly = GenerateTempAssembly(_mapping, type, defaultNamespace)!;
254-
}
255-
else
242+
if (Mode == SerializationMode.PreGenOnly)
256243
{
257-
// we found the pre-generated assembly, now make sure that the assembly has the right serializer
258-
// try to avoid the reflection step, need to get ElementName, namespace and the Key form the type
259-
_mapping = XmlReflectionImporter.GetTopLevelMapping(type, defaultNamespace);
260-
_tempAssembly = new TempAssembly(new XmlMapping[] { _mapping }, assembly, contract);
244+
AssemblyName name = type.Assembly.GetName();
245+
var serializerName = Compiler.GetTempAssemblyName(name, defaultNamespace);
246+
throw new FileLoadException(SR.Format(SR.FailLoadAssemblyUnderPregenMode, serializerName));
261247
}
248+
249+
// need to reflect and generate new serialization assembly
250+
XmlReflectionImporter importer = new XmlReflectionImporter(defaultNamespace);
251+
_mapping = importer.ImportTypeMapping(type, null, defaultNamespace);
252+
_tempAssembly = GenerateTempAssembly(_mapping, type, defaultNamespace)!;
253+
}
254+
else
255+
{
256+
// we found the pre-generated assembly, now make sure that the assembly has the right serializer
257+
// try to avoid the reflection step, need to get ElementName, namespace and the Key form the type
258+
_mapping = XmlReflectionImporter.GetTopLevelMapping(type, defaultNamespace);
259+
_tempAssembly = new TempAssembly(new XmlMapping[] { _mapping }, assembly, contract);
262260
}
263261
}
264262
s_cache.Add(defaultNamespace, type, _tempAssembly);
@@ -403,7 +401,9 @@ public void Serialize(XmlWriter xmlWriter, object? o, XmlSerializerNamespaces? n
403401
}
404402
}
405403
else
404+
{
406405
_tempAssembly.InvokeWriter(_mapping, xmlWriter, o, namespaces == null || namespaces.Count == 0 ? DefaultNamespaces : namespaces, encodingStyle, id);
406+
}
407407
}
408408
catch (Exception? e)
409409
{
@@ -629,7 +629,10 @@ public static XmlSerializer[] FromMappings(XmlMapping[]? mappings, Type? type)
629629
{
630630
XmlSerializer[] serializers = new XmlSerializer[mappings.Length];
631631
for (int i = 0; i < serializers.Length; i++)
632+
{
632633
serializers[i] = (XmlSerializer)contract!.TypedSerializers[mappings[i].Key!]!;
634+
TempAssembly.VerifyLoadContext(serializers[i]._rootType, type!.Assembly);
635+
}
633636
return serializers;
634637
}
635638
}
@@ -696,16 +699,9 @@ internal static bool GenerateSerializer(Type[]? types, XmlMapping[] mappings, St
696699
private static XmlSerializer[] GetSerializersFromCache(XmlMapping[] mappings, Type type)
697700
{
698701
XmlSerializer?[] serializers = new XmlSerializer?[mappings.Length];
699-
700702
Dictionary<XmlSerializerMappingKey, XmlSerializer>? typedMappingTable = null;
701-
lock (s_xmlSerializerTable)
702-
{
703-
if (!s_xmlSerializerTable.TryGetValue(type, out typedMappingTable))
704-
{
705-
typedMappingTable = new Dictionary<XmlSerializerMappingKey, XmlSerializer>();
706-
s_xmlSerializerTable[type] = typedMappingTable;
707-
}
708-
}
703+
704+
typedMappingTable = s_xmlSerializerTable.GetOrCreateValue(type, () => new Dictionary<XmlSerializerMappingKey, XmlSerializer>());
709705

710706
lock (typedMappingTable)
711707
{

src/libraries/System.Private.Xml/tests/XmlSerializer/ReflectionOnly/System.Xml.XmlSerializer.ReflectionOnly.Tests.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
<DefineConstants>$(DefineConstants);ReflectionOnly</DefineConstants>
44
<TargetFrameworks>$(NetCoreAppCurrent)</TargetFrameworks>
55
</PropertyGroup>
6+
<ItemGroup>
7+
<ProjectReference Include="..\..\..\..\Microsoft.XmlSerializer.Generator\tests\SerializableAssembly.csproj" />
8+
<TrimmerRootAssembly Include="SerializableAssembly" />
9+
</ItemGroup>
610
<ItemGroup>
711
<Compile Include="$(CommonTestPath)System\Runtime\Serialization\Utils.cs" />
8-
<Compile Include="$(TestSourceFolder)..\..\..\..\System.Runtime.Serialization.Xml\tests\SerializationTypes.cs" />
12+
<None Include="$(TestSourceFolder)..\..\..\..\System.Runtime.Serialization.Xml\tests\SerializationTypes.cs" />
913
<Compile Include="$(TestSourceFolder)..\..\..\..\System.Runtime.Serialization.Xml\tests\SerializationTypes.RuntimeOnly.cs" />
1014
<Compile Include="$(TestSourceFolder)..\XmlSerializerTests.cs" />
1115
<Compile Include="$(TestSourceFolder)..\XmlSerializerTests.RuntimeOnly.cs" />

0 commit comments

Comments
 (0)