Skip to content

Commit ff00ac7

Browse files
authored
Rework unloadability fix (#68550)
* Rework the unloadability fix The recent fix had a problem when GC collected the managed Assembly loaded via the Load override or the Resolving event before we added the reference between the related native LoaderAllocators. The managed LoaderAllocator of the ALC we've loaded the Assembly into was collected and we couldn't create the managed Type objects for the interfaces from that assembly in the GetInterfaces call on a type that implemented those. This change fixes it by moving the creation of reference between the native LoaderAllocators to the RuntimeInvokeHostAssemblyResolver where we still have a live managed reference to the loaded Assembly. * Move clearing of the m_pAssemblyLoaderAllocator to a later stage * Throw exception on collectible assembly in non-collectible ALC * Update the ResolvedFromDifferentContext test Make it test that we throw an exception when the parent ALC is not collectible.
1 parent fc9a75c commit ff00ac7

File tree

6 files changed

+127
-77
lines changed

6 files changed

+127
-77
lines changed

src/coreclr/binder/assemblybindercommon.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
extern HRESULT RuntimeInvokeHostAssemblyResolver(INT_PTR pManagedAssemblyLoadContextToBindWithin,
3131
BINDER_SPACE::AssemblyName *pAssemblyName,
3232
DefaultAssemblyBinder *pDefaultBinder,
33+
AssemblyBinder *pBinder,
3334
BINDER_SPACE::Assembly **ppLoadedAssembly);
3435

3536
#endif // !defined(DACCESS_COMPILE)
@@ -1153,9 +1154,10 @@ namespace BINDER_SPACE
11531154

11541155
#if !defined(DACCESS_COMPILE)
11551156
HRESULT AssemblyBinderCommon::BindUsingHostAssemblyResolver(/* in */ INT_PTR pManagedAssemblyLoadContextToBindWithin,
1156-
/* in */ AssemblyName *pAssemblyName,
1157+
/* in */ AssemblyName *pAssemblyName,
11571158
/* in */ DefaultAssemblyBinder *pDefaultBinder,
1158-
/* out */ Assembly **ppAssembly)
1159+
/* in */ AssemblyBinder *pBinder,
1160+
/* out */ Assembly **ppAssembly)
11591161
{
11601162
HRESULT hr = E_FAIL;
11611163

@@ -1164,7 +1166,7 @@ HRESULT AssemblyBinderCommon::BindUsingHostAssemblyResolver(/* in */ INT_PTR pMa
11641166
// RuntimeInvokeHostAssemblyResolver will perform steps 2-4 of CustomAssemblyBinder::BindAssemblyByName.
11651167
BINDER_SPACE::Assembly *pLoadedAssembly = NULL;
11661168
hr = RuntimeInvokeHostAssemblyResolver(pManagedAssemblyLoadContextToBindWithin,
1167-
pAssemblyName, pDefaultBinder, &pLoadedAssembly);
1169+
pAssemblyName, pDefaultBinder, pBinder, &pLoadedAssembly);
11681170
if (SUCCEEDED(hr))
11691171
{
11701172
_ASSERTE(pLoadedAssembly != NULL);

src/coreclr/binder/customassemblybinder.cpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ HRESULT CustomAssemblyBinder::BindUsingAssemblyName(BINDER_SPACE::AssemblyName*
7272
// of what to do next. The host-overridden binder can either fail the bind or return reference to an existing assembly
7373
// that has been loaded.
7474
//
75-
hr = AssemblyBinderCommon::BindUsingHostAssemblyResolver(GetManagedAssemblyLoadContext(), pAssemblyName, m_pDefaultBinder, &pCoreCLRFoundAssembly);
75+
hr = AssemblyBinderCommon::BindUsingHostAssemblyResolver(GetManagedAssemblyLoadContext(), pAssemblyName,
76+
m_pDefaultBinder, this, &pCoreCLRFoundAssembly);
7677
if (SUCCEEDED(hr))
7778
{
7879
// We maybe returned an assembly that was bound to a different AssemblyBinder instance.
@@ -224,9 +225,13 @@ void CustomAssemblyBinder::PrepareForLoadContextRelease(INT_PTR ptrManagedStrong
224225

225226
// We cannot delete the binder here as it is used indirectly when comparing assemblies with the same binder
226227
// It will be deleted when the LoaderAllocator will be deleted
227-
// But we can release the LoaderAllocator as we are no longer using it here
228+
// We need to keep the LoaderAllocator pointer set as it still may be needed for creating references between the
229+
// native LoaderAllocators of two collectible contexts in case the AssemblyLoadContext.Unload was called on the current
230+
// context before returning from its AssemblyLoadContext.Load override or the context's Resolving event.
231+
// But we need to release the LoaderAllocator so that it doesn't prevent completion of the final phase of unloading in
232+
// some cases. It is safe to do as the AssemblyLoaderAllocator is guaranteed to be alive at least until the
233+
// CustomAssemblyBinder::ReleaseLoadContext is called, where we NULL this pointer.
228234
m_pAssemblyLoaderAllocator->Release();
229-
m_pAssemblyLoaderAllocator = NULL;
230235

231236
// Destroy the strong handle to the LoaderAllocator in order to let it reach its finalizer
232237
DestroyHandle(reinterpret_cast<OBJECTHANDLE>(m_loaderAllocatorHandle));
@@ -251,6 +256,10 @@ void CustomAssemblyBinder::ReleaseLoadContext()
251256
handle = reinterpret_cast<OBJECTHANDLE>(m_ptrManagedStrongAssemblyLoadContext);
252257
DestroyHandle(handle);
253258
SetManagedAssemblyLoadContext(NULL);
259+
260+
// The AssemblyLoaderAllocator is in a process of shutdown and should not be used
261+
// after this point.
262+
m_pAssemblyLoaderAllocator = NULL;
254263
}
255264

256265
#endif // !defined(DACCESS_COMPILE)

src/coreclr/binder/defaultassemblybinder.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ HRESULT DefaultAssemblyBinder::BindUsingAssemblyName(BINDER_SPACE::AssemblyName
8585
if (pManagedAssemblyLoadContext != NULL)
8686
{
8787
hr = AssemblyBinderCommon::BindUsingHostAssemblyResolver(pManagedAssemblyLoadContext, pAssemblyName,
88-
NULL, &pCoreCLRFoundAssembly);
88+
NULL, this, &pCoreCLRFoundAssembly);
8989
if (SUCCEEDED(hr))
9090
{
9191
// We maybe returned an assembly that was bound to a different AssemblyLoadContext instance.

src/coreclr/binder/inc/assemblybindercommon.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ namespace BINDER_SPACE
5252
static HRESULT BindUsingHostAssemblyResolver (/* in */ INT_PTR pManagedAssemblyLoadContextToBindWithin,
5353
/* in */ AssemblyName *pAssemblyName,
5454
/* in */ DefaultAssemblyBinder *pDefaultBinder,
55+
/* in */ AssemblyBinder *pBinder,
5556
/* out */ Assembly **ppAssembly);
5657

5758
static HRESULT BindUsingPEImage(/* in */ AssemblyBinder *pBinder,

src/coreclr/vm/appdomain.cpp

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3370,37 +3370,6 @@ BOOL AppDomain::AddFileToCache(AssemblySpec* pSpec, PEAssembly * pPEAssembly, BO
33703370
}
33713371
}
33723372

3373-
// If the ALC for the result (pPEAssembly) is collectible and not the same as the ALC for the request (pSpec),
3374-
// which can happen when extension points (AssemblyLoadContext.Load, AssemblyLoadContext.Resolving) resolve the
3375-
// assembly, we need to ensure the result ALC is not collected before the request ALC. We do this by adding an
3376-
// explicit reference between the LoaderAllocators corresponding to the ALCs.
3377-
//
3378-
// To get the LoaderAllocators, we rely on the DomainAssembly corresponding to:
3379-
// - Parent assembly for the request
3380-
// - For loads via explicit load or reflection, this will be NULL. In these cases, the request ALC should not
3381-
// implicitly (as in not detectable via managed references) depend on the result ALC, so it is fine to not
3382-
// add the reference.
3383-
// - For loads via assembly references, this will never be NULL.
3384-
// - Result assembly for the result
3385-
// - For dynamic assemblies, there is no host assembly, so we don't have the result assembly / LoaderAllocator.
3386-
// We currently block resolving to dynamic assemblies, so we simply assert that we have a host assembly.
3387-
// - For non-dynamic assemblies, we should be able to get the host assembly.
3388-
DomainAssembly *pParentAssembly = pSpec->GetParentAssembly();
3389-
BINDER_SPACE::Assembly *pBinderSpaceAssembly = pPEAssembly->GetHostAssembly();
3390-
_ASSERTE(pBinderSpaceAssembly != NULL);
3391-
DomainAssembly *pResultAssembly = pBinderSpaceAssembly->GetDomainAssembly();
3392-
if ((pParentAssembly != NULL) && (pResultAssembly != NULL))
3393-
{
3394-
LoaderAllocator *pParentAssemblyLoaderAllocator = pParentAssembly->GetLoaderAllocator();
3395-
LoaderAllocator *pResultAssemblyLoaderAllocator = pResultAssembly->GetLoaderAllocator();
3396-
_ASSERTE(pParentAssemblyLoaderAllocator);
3397-
_ASSERTE(pResultAssemblyLoaderAllocator);
3398-
if (pResultAssemblyLoaderAllocator->IsCollectible())
3399-
{
3400-
pParentAssemblyLoaderAllocator->EnsureReference(pResultAssemblyLoaderAllocator);
3401-
}
3402-
}
3403-
34043373
return TRUE;
34053374
}
34063375

@@ -4937,7 +4906,7 @@ AppDomain::AssemblyIterator::Next_Unlocked(
49374906
#if !defined(DACCESS_COMPILE)
49384907

49394908
// Returns S_OK if the assembly was successfully loaded
4940-
HRESULT RuntimeInvokeHostAssemblyResolver(INT_PTR pManagedAssemblyLoadContextToBindWithin, BINDER_SPACE::AssemblyName *pAssemblyName, DefaultAssemblyBinder *pDefaultBinder, BINDER_SPACE::Assembly **ppLoadedAssembly)
4909+
HRESULT RuntimeInvokeHostAssemblyResolver(INT_PTR pManagedAssemblyLoadContextToBindWithin, BINDER_SPACE::AssemblyName *pAssemblyName, DefaultAssemblyBinder *pDefaultBinder, AssemblyBinder *pBinder, BINDER_SPACE::Assembly **ppLoadedAssembly)
49414910
{
49424911
CONTRACTL
49434912
{
@@ -5115,6 +5084,22 @@ HRESULT RuntimeInvokeHostAssemblyResolver(INT_PTR pManagedAssemblyLoadContextToB
51155084
COMPlusThrowHR(COR_E_INVALIDOPERATION, IDS_HOST_ASSEMBLY_RESOLVER_DYNAMICALLY_EMITTED_ASSEMBLIES_UNSUPPORTED, name);
51165085
}
51175086

5087+
// For collectible assemblies, ensure that the parent loader allocator keeps the assembly's loader allocator
5088+
// alive for all its lifetime.
5089+
if (pDomainAssembly->IsCollectible())
5090+
{
5091+
LoaderAllocator *pResultAssemblyLoaderAllocator = pDomainAssembly->GetLoaderAllocator();
5092+
LoaderAllocator *pParentLoaderAllocator = pBinder->GetLoaderAllocator();
5093+
if (pParentLoaderAllocator == NULL)
5094+
{
5095+
// The AssemblyLoadContext for which we are resolving the the Assembly is not collectible.
5096+
COMPlusThrow(kNotSupportedException, W("NotSupported_CollectibleBoundNonCollectible"));
5097+
}
5098+
5099+
_ASSERTE(pResultAssemblyLoaderAllocator);
5100+
pParentLoaderAllocator->EnsureReference(pResultAssemblyLoaderAllocator);
5101+
}
5102+
51185103
pResolvedAssembly = pLoadedPEAssembly->GetHostAssembly();
51195104
}
51205105

src/tests/Loader/CollectibleAssemblies/ResolvedFromDifferentContext/ResolvedFromDifferentContext.cs

Lines changed: 91 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ protected override Assembly Load(AssemblyName assemblyName)
3030
if (assemblyName.Name == "TestInterface")
3131
{
3232
AssemblyLoadContext alc1 = new AssemblyLoadContext("Dependencies", true);
33-
Console.WriteLine($"Loading TestInterface by alc {alc1} for alc {this}");
34-
Assembly a = alc1.LoadFromAssemblyPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\TestInterface\TestInterface.dll"));
33+
Console.WriteLine($"Loading TestInterface by alc {alc1} for {(IsCollectible ? "collectible" : "non-collectible")} alc {this}");
34+
Assembly a = alc1.LoadFromAssemblyPath(Test.GetTestAssemblyPath(@"..\TestInterface\TestInterface.dll"));
3535
interfaceAssemblyRef = new WeakReference(a);
3636
return a;
3737
}
@@ -45,13 +45,18 @@ class Test
4545
static AssemblyLoadContext alc1 = null;
4646
static WeakReference interfaceAssemblyRef = null;
4747

48+
public static string GetTestAssemblyPath(string subPath)
49+
{
50+
return Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), subPath);
51+
}
52+
4853
[MethodImpl(MethodImplOptions.NoInlining)]
49-
public static Assembly LoadUsingResolvingEvent()
54+
private static Assembly LoadUsingResolvingEvent(bool collectibleParent)
5055
{
5156
alc1 = new AssemblyLoadContext("Dependencies", true);
52-
AssemblyLoadContext alc2 = new AssemblyLoadContext("Test1", true);
57+
AssemblyLoadContext alc2 = new AssemblyLoadContext("Test1", collectibleParent);
5358
alc2.Resolving += Alc2_Resolving;
54-
Assembly assembly = alc2.LoadFromAssemblyPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\TestClass\TestClass.dll"));
59+
Assembly assembly = alc2.LoadFromAssemblyPath(Test.GetTestAssemblyPath(@"..\TestClass\TestClass.dll"));
5560

5661
Type t = assembly.GetType("TestClass.Class");
5762
Console.WriteLine($"Type {t} obtained");
@@ -70,8 +75,8 @@ private static Assembly Alc2_Resolving(AssemblyLoadContext arg1, AssemblyName ar
7075
Console.WriteLine($"Resolving event by alc {alc1} for alc {arg1}");
7176
if (alc1 != null && arg2.Name == "TestInterface")
7277
{
73-
Console.WriteLine($"Loading TestInterface by alc {alc1} for alc {arg1}");
74-
Assembly a = alc1.LoadFromAssemblyPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\TestInterface\TestInterface.dll"));
78+
Console.WriteLine($"Loading TestInterface by alc {alc1} for {(arg1.IsCollectible ? "collectible" : "non-collectible")} alc {arg1}");
79+
Assembly a = alc1.LoadFromAssemblyPath(Test.GetTestAssemblyPath(@"..\TestInterface\TestInterface.dll"));
7580
interfaceAssemblyRef = new WeakReference(a);
7681
return a;
7782
}
@@ -80,10 +85,10 @@ private static Assembly Alc2_Resolving(AssemblyLoadContext arg1, AssemblyName ar
8085
}
8186

8287
[MethodImpl(MethodImplOptions.NoInlining)]
83-
public static Assembly LoadUsingLoadOverride()
88+
private static Assembly LoadUsingLoadOverride(bool collectibleParent)
8489
{
85-
TestAssemblyLoadContext alc2 = new TestAssemblyLoadContext("Test2", true);
86-
Assembly assembly = alc2.LoadFromAssemblyPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\TestClass\TestClass.dll"));
90+
TestAssemblyLoadContext alc2 = new TestAssemblyLoadContext("Test2", collectibleParent);
91+
Assembly assembly = alc2.LoadFromAssemblyPath(Test.GetTestAssemblyPath(@"..\TestClass\TestClass.dll"));
8792

8893
Type t = assembly.GetType("TestClass.Class");
8994

@@ -94,18 +99,33 @@ public static Assembly LoadUsingLoadOverride()
9499
return assembly;
95100
}
96101

102+
private enum TestCase
103+
{
104+
ResolvingEvent,
105+
LoadOverride,
106+
ResolvingEventInNonCollectible,
107+
LoadOverrideInNonCollectible
108+
}
109+
97110
[MethodImpl(MethodImplOptions.NoInlining)]
98-
private static WeakReference TestDependencies(string testCase)
111+
private static WeakReference TestDependencies(TestCase testCase)
99112
{
100-
Assembly assembly;
113+
Assembly assembly = null;
101114

102-
if (testCase == "ResolvingEvent")
103-
{
104-
assembly = LoadUsingResolvingEvent();
105-
}
106-
else
115+
switch (testCase)
107116
{
108-
assembly = LoadUsingLoadOverride();
117+
case TestCase.ResolvingEvent:
118+
assembly = LoadUsingResolvingEvent(collectibleParent: true);
119+
break;
120+
case TestCase.LoadOverride:
121+
assembly = LoadUsingLoadOverride(collectibleParent: true);
122+
break;
123+
case TestCase.ResolvingEventInNonCollectible:
124+
assembly = LoadUsingResolvingEvent(collectibleParent: false);
125+
break;
126+
case TestCase.LoadOverrideInNonCollectible:
127+
assembly = LoadUsingLoadOverride(collectibleParent: false);
128+
break;
109129
}
110130

111131
for (int i = 0; interfaceAssemblyRef.IsAlive && (i < 10); i++)
@@ -124,45 +144,78 @@ private static WeakReference TestDependencies(string testCase)
124144
return new WeakReference(assembly);
125145
}
126146

127-
public static int TestFullUnload(string testCase)
147+
private static bool ShouldThrow(TestCase testCase)
128148
{
129-
Console.WriteLine($"Running test case {testCase}");
149+
return (testCase == TestCase.LoadOverrideInNonCollectible) || (testCase == TestCase.ResolvingEventInNonCollectible);
150+
}
130151

131-
WeakReference assemblyRef = TestDependencies(testCase);
132-
if (assemblyRef == null)
133-
{
134-
return 101;
135-
}
152+
private static int TestFullUnload(TestCase testCase)
153+
{
154+
Console.WriteLine($"Running test case {testCase}");
136155

137-
for (int i = 0; assemblyRef.IsAlive && (i < 10); i++)
156+
try
138157
{
139-
GC.Collect();
140-
GC.WaitForPendingFinalizers();
158+
WeakReference assemblyRef = TestDependencies(testCase);
159+
if (assemblyRef == null)
160+
{
161+
return 101;
162+
}
163+
164+
for (int i = 0; assemblyRef.IsAlive && (i < 10); i++)
165+
{
166+
GC.Collect();
167+
GC.WaitForPendingFinalizers();
168+
}
169+
170+
if (assemblyRef.IsAlive)
171+
{
172+
Console.WriteLine("Failed to unload alc2");
173+
return 102;
174+
}
175+
176+
if (interfaceAssemblyRef.IsAlive)
177+
{
178+
Console.WriteLine("Failed to unload alc1");
179+
return 103;
180+
}
181+
182+
Console.WriteLine();
141183
}
142-
143-
if (assemblyRef.IsAlive)
184+
catch (System.IO.FileLoadException e)
144185
{
145-
Console.WriteLine("Failed to unload alc2");
146-
return 102;
186+
if (!ShouldThrow(testCase))
187+
{
188+
Console.WriteLine("Failure - unexpected exception");
189+
return 104;
190+
}
191+
if ((e.InnerException == null) || e.InnerException.GetType() != typeof(System.NotSupportedException))
192+
{
193+
Console.WriteLine($"Failure - unexpected exception type {e.InnerException}");
194+
return 105;
195+
}
196+
197+
return 100;
147198
}
148199

149-
if (interfaceAssemblyRef.IsAlive)
200+
if (ShouldThrow(testCase))
150201
{
151-
Console.WriteLine("Failed to unload alc1");
152-
return 103;
202+
Console.WriteLine("Failure - resolved collectible assembly into non-collectible context without throwing exception");
203+
return 106;
153204
}
154205

155-
Console.WriteLine();
156206
return 100;
157-
158207
}
159208

160209
public static int Main(string[] args)
161210
{
162211
int status = 100;
163-
foreach (string testCase in new string[] {"LoadOverride", "ResolvingEvent"})
212+
foreach (TestCase testCase in Enum.GetValues(typeof(TestCase)))
164213
{
165214
status = TestFullUnload(testCase);
215+
if (status != 100)
216+
{
217+
break;
218+
}
166219
}
167220

168221
return status;

0 commit comments

Comments
 (0)