Skip to content

A potential out-of-bound read in RuntimeInvokeHostAssemblyResolver #104466

Closed
@JJLovesLife

Description

@JJLovesLife

Description

In AssemblyLoadContext, there is a check to avoid the loaded assembly to be reflection emitted assembly. However, the native code checking this seems to be contains a potential out-of-bound memory read, caused by the mismatching between RuntimeAssembly and RuntimeAssemblyBuilder.

Here is the code analysis:

// We were able to get the assembly loaded. Now, get its name since the host could have
// performed the resolution using an assembly with different name.
DomainAssembly *pDomainAssembly = _gcRefs.oRefLoadedAssembly->GetDomainAssembly();

This code here _gcRefs.oRefLoadedAssembly could be returned from AssemblyLoadContext.Load which is controlled by user, so _gcRefs.oRefLoadedAssembly of type Assembly could actually be RuntimeAssemblyBuilder.
But ->GetDomainAssembly() access offset 0x20 of the Assembly object, where RuntimeAssemblyBuilder has only a size of 0x20(don't count header). So access domain assembly on RuntimeAssemblyBuilder cause it reads the end of the objects, which normally is the header of _internalAssembly of RuntimeAssemblyBuilder, but potentially be any object's header or even something else if a GC happens between this lines code.
If that happens, following access of DomainAssembly causes a memory access violation.
if (!pDomainAssembly)
{
// Reflection emitted assemblies will not have a domain assembly.
fFailLoad = true;
}
else
{
pLoadedPEAssembly = pDomainAssembly->GetPEAssembly();

Reproduction Steps

using System.Reflection;
using System.Runtime.Loader;
using System.Reflection.Emit;

new MyLoadContext().LoadFromAssemblyName(new("DynamicAssemblyExample"));

class MyLoadContext : AssemblyLoadContext
{
	protected override Assembly? Load(AssemblyName assemblyName)
	{
		var aName = new AssemblyName("DynamicAssemblyExample");
		AssemblyBuilder ab =
			AssemblyBuilder.DefineDynamicAssembly(
				aName,
				AssemblyBuilderAccess.Run);
		// Here I am forcing memory just after RuntimeAssemblyBuilder to be non-zero by cause _internalAssembly's obj header to be != 0.
		// But that memory could potentially be non-zero if this is a GC happens during `DefineDynamicAssembly`
		typeof(AssemblyBuilder).Assembly.GetType("System.Reflection.Emit.RuntimeAssemblyBuilder", true)!.GetField("_internalAssembly", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(ab)!.GetHashCode();
		return ab;
	}
}

Result:

Fatal error. Internal CLR error. (0x80131506)
   at System.Reflection.RuntimeAssembly.InternalLoad(System.Reflection.AssemblyName, System.Threading.StackCrawlMark ByRef, System.Runtime.Loader.AssemblyLoadContext, System.Reflection.RuntimeAssembly, Boolean)
   at System.Runtime.Loader.AssemblyLoadContext.LoadFromAssemblyName(System.Reflection.AssemblyName)
   at Program.<Main>$(System.String[])

Expected behavior

The check within RuntimeInvokeHostAssemblyResolver should aware that _gcRefs.oRefLoadedAssembly could be any type inherited from Assembly

Actual behavior

Code in RuntimeInvokeHostAssemblyResolver assume _gcRefs.oRefLoadedAssembly to be RuntimeAssembly or has a simlar memory layout.

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    No status

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions