Skip to content

Commit c026339

Browse files
authored
AssemblyLoadContext fix (#7113)
1 parent 1efdfec commit c026339

File tree

8 files changed

+172
-19
lines changed

8 files changed

+172
-19
lines changed

src/WebJobs.Script/Description/DotNet/FunctionAssemblyLoadContext.cs

+56-16
Original file line numberDiff line numberDiff line change
@@ -212,16 +212,51 @@ protected override Assembly Load(AssemblyName assemblyName)
212212
// If this is a runtime restricted assembly, load it based on unification rules
213213
if (TryGetRuntimeAssembly(assemblyName, out ScriptRuntimeAssembly scriptRuntimeAssembly))
214214
{
215-
assembly = LoadRuntimeAssembly(assemblyName, scriptRuntimeAssembly);
216-
217-
// If the policy evaluation failed because the AssemblyName was adjusted due to
218-
// deps.json, we cannot allow the Default context to load it. Instead, load directly.
219-
if (assembly == null && isNameAdjusted)
215+
// There are several possible scenarios:
216+
// 1. The assembly was found and the policy evaluator succeeded.
217+
// - Return the assembly.
218+
//
219+
// 2. The assembly was not found (meaning the policy evaluator wasn't able to run).
220+
// - Return null as there's not much else we can do. This will fail and come back to this context
221+
// through the fallback logic for another attempt. This is likely an error case so let it fail.
222+
//
223+
// 3. The assembly was found but the policy evaluator failed.
224+
// a. We did not adjust the requested assembly name via the deps file.
225+
// - This means that the DefaultLoadContext is looking for the correct version. Return null and let
226+
// it handle the load attempt.
227+
// b. We adjusted the requested assembly name via the deps file. If we return null the DefaultLoadContext would attempt to
228+
// load the original assembly version, which may be incorrect if we had to adjust it forward past the runtime's version.
229+
// i. The adjusted assembly name is higher than the runtime's version.
230+
// - Do not trust the DefaultLoadContext to handle this as it may resolve to the runtime's assembly. Instead,
231+
// call LoadCore() to ensure the adjusted assembly is loaded.
232+
// ii. The adjusted assembly name is still lower than or equal to the runtime's version (this likely means
233+
// a lower major version).
234+
// - Return null and let the DefaultLoadContext handle it. It may come back here due to fallback logic, but
235+
// ultimately it will unify on the runtime version.
236+
237+
if (TryLoadRuntimeAssembly(assemblyName, scriptRuntimeAssembly, out assembly))
220238
{
221-
assembly = LoadCore(assemblyName);
239+
// Scenarios 1 and 2(a).
240+
return assembly;
222241
}
242+
else
243+
{
244+
if (assembly == null || !isNameAdjusted)
245+
{
246+
// Scenario 2 and 3(a).
247+
return null;
248+
}
223249

224-
return assembly;
250+
AssemblyName runtimeAssemblyName = AssemblyNameCache.GetName(assembly);
251+
if (assemblyName.Version > runtimeAssemblyName.Version)
252+
{
253+
// Scenario 3(b)(i).
254+
return LoadCore(assemblyName);
255+
}
256+
257+
// Scenario 3(b)(ii).
258+
return null;
259+
}
225260
}
226261

227262
// If the assembly being requested matches a host assembly, we'll use
@@ -257,26 +292,31 @@ private bool TryAdjustRuntimeAssemblyFromDepsFile(AssemblyName assemblyName, out
257292
return false;
258293
}
259294

260-
private Assembly LoadRuntimeAssembly(AssemblyName assemblyName, ScriptRuntimeAssembly scriptRuntimeAssembly)
295+
/// <summary>
296+
/// Loads the runtime's version of this assembly and runs it through the appropriate policy evaluator.
297+
/// </summary>
298+
/// <param name="assemblyName">The assembly name to load.</param>
299+
/// <param name="scriptRuntimeAssembly">The policy evaluator details.</param>
300+
/// <param name="runtimeAssembly">The runtime's assembly.</param>
301+
/// <returns>true if the policy evaluation succeeded, otherwise, false.</returns>
302+
private bool TryLoadRuntimeAssembly(AssemblyName assemblyName, ScriptRuntimeAssembly scriptRuntimeAssembly, out Assembly runtimeAssembly)
261303
{
262304
// If this is a private runtime assembly, return function dependency
263305
if (string.Equals(scriptRuntimeAssembly.ResolutionPolicy, PrivateDependencyResolutionPolicy))
264306
{
265-
return LoadCore(assemblyName);
307+
runtimeAssembly = LoadCore(assemblyName);
308+
return true;
266309
}
267310

268311
// Attempt to load the runtime version of the assembly based on the unification policy evaluation result.
269-
if (TryLoadHostEnvironmentAssembly(assemblyName, allowPartialNameMatch: true, out Assembly assembly))
312+
if (TryLoadHostEnvironmentAssembly(assemblyName, allowPartialNameMatch: true, out runtimeAssembly))
270313
{
271314
var policyEvaluator = GetResolutionPolicyEvaluator(scriptRuntimeAssembly.ResolutionPolicy);
272-
273-
if (policyEvaluator.Invoke(assemblyName, assembly))
274-
{
275-
return assembly;
276-
}
315+
return policyEvaluator.Invoke(assemblyName, runtimeAssembly);
277316
}
278317

279-
return null;
318+
runtimeAssembly = null;
319+
return false;
280320
}
281321

282322
private bool TryLoadDepsDependency(AssemblyName assemblyName, out Assembly assembly)

test/CSharpPrecompiledTestProjects/CSharpPrecompiledTestProjects.sln

+9-3
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeDependencyOldSdk", "N
1111
EndProject
1212
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeDependencyNoRuntimes", "NativeDependencyNoRuntimes\NativeDependencyNoRuntimes.csproj", "{928B573E-904B-4733-86A2-6CDBF78D24AA}"
1313
EndProject
14-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultipleDependencyVersions", "MultipleDependencyVersions\MultipleDependencyVersions.csproj", "{D0AF8295-7CBF-45EB-914F-7105A9F53B69}"
14+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultipleDependencyVersions", "MultipleDependencyVersions\MultipleDependencyVersions.csproj", "{D0AF8295-7CBF-45EB-914F-7105A9F53B69}"
1515
EndProject
1616
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{297CEDAC-BB8E-4875-A3FF-7BA7A4916E73}"
1717
EndProject
18-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dependency55", "DependencyA\Dependency55.csproj", "{BC456A9E-D140-4B1A-84D9-AB82556F5881}"
18+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dependency55", "DependencyA\Dependency55.csproj", "{BC456A9E-D140-4B1A-84D9-AB82556F5881}"
1919
EndProject
20-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dependency56", "Dependency56\Dependency56.csproj", "{B3B098F6-B2B8-4219-AA52-29F55427496A}"
20+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dependency56", "Dependency56\Dependency56.csproj", "{B3B098F6-B2B8-4219-AA52-29F55427496A}"
21+
EndProject
22+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReferenceOlderRuntimeAssembly", "ReferenceOlderRuntimeAssembly\ReferenceOlderRuntimeAssembly.csproj", "{C3E99727-F4A3-47D9-9DF6-8EE85AE0C29A}"
2123
EndProject
2224
Global
2325
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -53,6 +55,10 @@ Global
5355
{B3B098F6-B2B8-4219-AA52-29F55427496A}.Debug|Any CPU.Build.0 = Debug|Any CPU
5456
{B3B098F6-B2B8-4219-AA52-29F55427496A}.Release|Any CPU.ActiveCfg = Release|Any CPU
5557
{B3B098F6-B2B8-4219-AA52-29F55427496A}.Release|Any CPU.Build.0 = Release|Any CPU
58+
{C3E99727-F4A3-47D9-9DF6-8EE85AE0C29A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
59+
{C3E99727-F4A3-47D9-9DF6-8EE85AE0C29A}.Debug|Any CPU.Build.0 = Debug|Any CPU
60+
{C3E99727-F4A3-47D9-9DF6-8EE85AE0C29A}.Release|Any CPU.ActiveCfg = Release|Any CPU
61+
{C3E99727-F4A3-47D9-9DF6-8EE85AE0C29A}.Release|Any CPU.Build.0 = Release|Any CPU
5662
EndGlobalSection
5763
GlobalSection(SolutionProperties) = preSolution
5864
HideSolutionNode = FALSE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"dependencies": {
3+
"appInsights1": {
4+
"type": "appInsights"
5+
},
6+
"storage1": {
7+
"type": "storage",
8+
"connectionId": "AzureWebJobsStorage"
9+
}
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"dependencies": {
3+
"appInsights1": {
4+
"type": "appInsights.sdk"
5+
},
6+
"storage1": {
7+
"type": "storage.emulator",
8+
"connectionId": "AzureWebJobsStorage"
9+
}
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using Microsoft.AspNetCore.Http;
3+
using Microsoft.AspNetCore.Mvc;
4+
using Microsoft.Azure.WebJobs;
5+
using Microsoft.Azure.WebJobs.Extensions.Http;
6+
using Microsoft.Extensions.Hosting;
7+
8+
namespace ReferenceOlderRuntimeAssembly
9+
{
10+
public class ReferenceOlderRuntimeAssembly
11+
{
12+
public static IHostingEnvironment StartupEnv;
13+
private readonly IHostingEnvironment _env;
14+
15+
public ReferenceOlderRuntimeAssembly(IHostingEnvironment env)
16+
{
17+
_env = env;
18+
}
19+
20+
[FunctionName("ReferenceOlderRuntimeAssembly")]
21+
public IActionResult Run(
22+
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req)
23+
{
24+
if (_env == null)
25+
{
26+
throw new InvalidOperationException();
27+
}
28+
29+
return new OkResult();
30+
}
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>netcoreapp3.1</TargetFramework>
4+
<AzureFunctionsVersion>v3</AzureFunctionsVersion>
5+
<_FunctionsSkipCleanOutput>true</_FunctionsSkipCleanOutput>
6+
</PropertyGroup>
7+
<ItemGroup>
8+
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="4.0.3" />
9+
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="2.2.0" />
10+
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.11" />
11+
</ItemGroup>
12+
<ItemGroup>
13+
<None Update="host.json">
14+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
15+
</None>
16+
<None Update="local.settings.json">
17+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
18+
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
19+
</None>
20+
</ItemGroup>
21+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"version": "2.0",
3+
"logging": {
4+
"applicationInsights": {
5+
"samplingExcludedTypes": "Request",
6+
"samplingSettings": {
7+
"isEnabled": true
8+
}
9+
}
10+
}
11+
}

test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/FunctionAssemblyLoadContextEndToEndTests.cs

+21
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,27 @@ await RunTest(async () =>
114114
});
115115
}
116116

117+
[Fact]
118+
public async Task ReferenceOlderRuntimeAssembly()
119+
{
120+
// Test that we still return host version, even if it's a major version below.
121+
// The test project used repros the scenario because it references the Storage extension,
122+
// which has references to Extensions.Hosting.Abstractions 2.1. The project itself directly
123+
// references 2.2 of this assembly and before the fix, would throw an exception on Startup.
124+
125+
await RunTest(async () =>
126+
{
127+
_launcher = new HostProcessLauncher("ReferenceOlderRuntimeAssembly");
128+
await _launcher.StartHostAsync();
129+
130+
var client = _launcher.HttpClient;
131+
var response = await client.GetAsync($"api/ReferenceOlderRuntimeAssembly");
132+
133+
// The function does all the validation internally.
134+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
135+
});
136+
}
137+
117138
private async Task RunTest(Func<Task> test)
118139
{
119140
try

0 commit comments

Comments
 (0)