Skip to content

Commit

Permalink
AssemblyLoadContext fix (Azure#7113)
Browse files Browse the repository at this point in the history
  • Loading branch information
brettsam authored Feb 1, 2021
1 parent 1efdfec commit c026339
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -212,16 +212,51 @@ protected override Assembly Load(AssemblyName assemblyName)
// If this is a runtime restricted assembly, load it based on unification rules
if (TryGetRuntimeAssembly(assemblyName, out ScriptRuntimeAssembly scriptRuntimeAssembly))
{
assembly = LoadRuntimeAssembly(assemblyName, scriptRuntimeAssembly);

// If the policy evaluation failed because the AssemblyName was adjusted due to
// deps.json, we cannot allow the Default context to load it. Instead, load directly.
if (assembly == null && isNameAdjusted)
// There are several possible scenarios:
// 1. The assembly was found and the policy evaluator succeeded.
// - Return the assembly.
//
// 2. The assembly was not found (meaning the policy evaluator wasn't able to run).
// - Return null as there's not much else we can do. This will fail and come back to this context
// through the fallback logic for another attempt. This is likely an error case so let it fail.
//
// 3. The assembly was found but the policy evaluator failed.
// a. We did not adjust the requested assembly name via the deps file.
// - This means that the DefaultLoadContext is looking for the correct version. Return null and let
// it handle the load attempt.
// b. We adjusted the requested assembly name via the deps file. If we return null the DefaultLoadContext would attempt to
// load the original assembly version, which may be incorrect if we had to adjust it forward past the runtime's version.
// i. The adjusted assembly name is higher than the runtime's version.
// - Do not trust the DefaultLoadContext to handle this as it may resolve to the runtime's assembly. Instead,
// call LoadCore() to ensure the adjusted assembly is loaded.
// ii. The adjusted assembly name is still lower than or equal to the runtime's version (this likely means
// a lower major version).
// - Return null and let the DefaultLoadContext handle it. It may come back here due to fallback logic, but
// ultimately it will unify on the runtime version.

if (TryLoadRuntimeAssembly(assemblyName, scriptRuntimeAssembly, out assembly))
{
assembly = LoadCore(assemblyName);
// Scenarios 1 and 2(a).
return assembly;
}
else
{
if (assembly == null || !isNameAdjusted)
{
// Scenario 2 and 3(a).
return null;
}

return assembly;
AssemblyName runtimeAssemblyName = AssemblyNameCache.GetName(assembly);
if (assemblyName.Version > runtimeAssemblyName.Version)
{
// Scenario 3(b)(i).
return LoadCore(assemblyName);
}

// Scenario 3(b)(ii).
return null;
}
}

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

private Assembly LoadRuntimeAssembly(AssemblyName assemblyName, ScriptRuntimeAssembly scriptRuntimeAssembly)
/// <summary>
/// Loads the runtime's version of this assembly and runs it through the appropriate policy evaluator.
/// </summary>
/// <param name="assemblyName">The assembly name to load.</param>
/// <param name="scriptRuntimeAssembly">The policy evaluator details.</param>
/// <param name="runtimeAssembly">The runtime's assembly.</param>
/// <returns>true if the policy evaluation succeeded, otherwise, false.</returns>
private bool TryLoadRuntimeAssembly(AssemblyName assemblyName, ScriptRuntimeAssembly scriptRuntimeAssembly, out Assembly runtimeAssembly)
{
// If this is a private runtime assembly, return function dependency
if (string.Equals(scriptRuntimeAssembly.ResolutionPolicy, PrivateDependencyResolutionPolicy))
{
return LoadCore(assemblyName);
runtimeAssembly = LoadCore(assemblyName);
return true;
}

// Attempt to load the runtime version of the assembly based on the unification policy evaluation result.
if (TryLoadHostEnvironmentAssembly(assemblyName, allowPartialNameMatch: true, out Assembly assembly))
if (TryLoadHostEnvironmentAssembly(assemblyName, allowPartialNameMatch: true, out runtimeAssembly))
{
var policyEvaluator = GetResolutionPolicyEvaluator(scriptRuntimeAssembly.ResolutionPolicy);

if (policyEvaluator.Invoke(assemblyName, assembly))
{
return assembly;
}
return policyEvaluator.Invoke(assemblyName, runtimeAssembly);
}

return null;
runtimeAssembly = null;
return false;
}

private bool TryLoadDepsDependency(AssemblyName assemblyName, out Assembly assembly)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeDependencyOldSdk", "N
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeDependencyNoRuntimes", "NativeDependencyNoRuntimes\NativeDependencyNoRuntimes.csproj", "{928B573E-904B-4733-86A2-6CDBF78D24AA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultipleDependencyVersions", "MultipleDependencyVersions\MultipleDependencyVersions.csproj", "{D0AF8295-7CBF-45EB-914F-7105A9F53B69}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultipleDependencyVersions", "MultipleDependencyVersions\MultipleDependencyVersions.csproj", "{D0AF8295-7CBF-45EB-914F-7105A9F53B69}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{297CEDAC-BB8E-4875-A3FF-7BA7A4916E73}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dependency55", "DependencyA\Dependency55.csproj", "{BC456A9E-D140-4B1A-84D9-AB82556F5881}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dependency55", "DependencyA\Dependency55.csproj", "{BC456A9E-D140-4B1A-84D9-AB82556F5881}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dependency56", "Dependency56\Dependency56.csproj", "{B3B098F6-B2B8-4219-AA52-29F55427496A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dependency56", "Dependency56\Dependency56.csproj", "{B3B098F6-B2B8-4219-AA52-29F55427496A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReferenceOlderRuntimeAssembly", "ReferenceOlderRuntimeAssembly\ReferenceOlderRuntimeAssembly.csproj", "{C3E99727-F4A3-47D9-9DF6-8EE85AE0C29A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -53,6 +55,10 @@ Global
{B3B098F6-B2B8-4219-AA52-29F55427496A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B3B098F6-B2B8-4219-AA52-29F55427496A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B3B098F6-B2B8-4219-AA52-29F55427496A}.Release|Any CPU.Build.0 = Release|Any CPU
{C3E99727-F4A3-47D9-9DF6-8EE85AE0C29A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C3E99727-F4A3-47D9-9DF6-8EE85AE0C29A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3E99727-F4A3-47D9-9DF6-8EE85AE0C29A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3E99727-F4A3-47D9-9DF6-8EE85AE0C29A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"dependencies": {
"appInsights1": {
"type": "appInsights"
},
"storage1": {
"type": "storage",
"connectionId": "AzureWebJobsStorage"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"dependencies": {
"appInsights1": {
"type": "appInsights.sdk"
},
"storage1": {
"type": "storage.emulator",
"connectionId": "AzureWebJobsStorage"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Hosting;

namespace ReferenceOlderRuntimeAssembly
{
public class ReferenceOlderRuntimeAssembly
{
public static IHostingEnvironment StartupEnv;
private readonly IHostingEnvironment _env;

public ReferenceOlderRuntimeAssembly(IHostingEnvironment env)
{
_env = env;
}

[FunctionName("ReferenceOlderRuntimeAssembly")]
public IActionResult Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req)
{
if (_env == null)
{
throw new InvalidOperationException();
}

return new OkResult();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AzureFunctionsVersion>v3</AzureFunctionsVersion>
<_FunctionsSkipCleanOutput>true</_FunctionsSkipCleanOutput>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="4.0.3" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.11" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingExcludedTypes": "Request",
"samplingSettings": {
"isEnabled": true
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,27 @@ await RunTest(async () =>
});
}

[Fact]
public async Task ReferenceOlderRuntimeAssembly()
{
// Test that we still return host version, even if it's a major version below.
// The test project used repros the scenario because it references the Storage extension,
// which has references to Extensions.Hosting.Abstractions 2.1. The project itself directly
// references 2.2 of this assembly and before the fix, would throw an exception on Startup.

await RunTest(async () =>
{
_launcher = new HostProcessLauncher("ReferenceOlderRuntimeAssembly");
await _launcher.StartHostAsync();

var client = _launcher.HttpClient;
var response = await client.GetAsync($"api/ReferenceOlderRuntimeAssembly");

// The function does all the validation internally.
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
});
}

private async Task RunTest(Func<Task> test)
{
try
Expand Down

0 comments on commit c026339

Please sign in to comment.