Skip to content

Fix TypeLoadException in GetMarshalAs when SafeArray has zero-length user-defined type name#124408

Open
Copilot wants to merge 2 commits intomainfrom
copilot/fix-customattributes-type-load-exception
Open

Fix TypeLoadException in GetMarshalAs when SafeArray has zero-length user-defined type name#124408
Copilot wants to merge 2 commits intomainfrom
copilot/fix-customattributes-type-load-exception

Conversation

Copy link
Contributor

Copilot AI commented Feb 13, 2026

Description

MetadataImport.GetMarshalAs returns raw byte pointers to managed code without associated lengths. When a MarshalAs blob contains NATIVE_TYPE_SAFEARRAY with a zero-length user-defined type name (common in tlbimp-generated COM interop assemblies), the native FCALL returned a non-null pointer past the blob data. CreateReadOnlySpanFromNullTerminated then read garbage memory, producing a garbled string that failed type resolution with TypeLoadException.

Regression from commit a3dc133 which switched from returning managed strings to returning raw byte pointers.

Native fix (managedmdimport.cpp)

  • Return NULL when byte count is 0 for safeArrayUserDefinedSubType, marshalType, and marshalCookie

Managed defense-in-depth (MdImport.cs)

  • Wrap safeArrayUserDefinedType resolution in try/catch(TypeLoadException), matching the existing pattern for marshalTypeRef
  • Update marshalTypeName check to string.IsNullOrEmpty for consistency

Regression test (MarshalAsAttributeTests.cs)

  • Build a PE via PersistedAssemblyBuilder with FieldMarshal blob {0x1D, 0x09, 0x00, 0x58, 0x00} — the trailing poison byte 'X' ensures the dangling pointer read is observable (plain {0x1D, 0x09, 0x00} doesn't reliably repro because the blob typically lands near null padding in the #Blob heap)
  • Fix pre-existing Ctor_UmanagedTyeCtor_UnmanagedType typo

This PR carries forward the changes from #124355 with review feedback from @jkotas and @stephentoub applied.

Original prompt

This section details on the original issue you should resolve

<issue_title>Getting CustomAttributes of ParameterInfo throws TypeLoadException</issue_title>
<issue_description>### Description

When getting the CustomAttributes of the parameter of the setter for the property Icon of the interface MFilesAPI.IObjectType from the NuGet package Interop.MFilesAPI version 26.2.2, the following TypeLoadException is thrown:

Unhandled exception. System.TypeLoadException: Could not resolve type ' in assembly 'Interop.MFilesAPI, Version=7.0.0.0, Culture=neutral, PublicKeyToken=f1b4733f444f7ad0'.
   at System.Reflection.TypeNameResolver.GetTypeFromDefaultAssemblies(String typeName, ReadOnlySpan`1 nestedTypeNames, TypeName parsedName)
   at System.Reflection.TypeNameResolver.GetType(String escapedTypeName, ReadOnlySpan`1 nestedTypeNames, TypeName parsedName)
   at System.Reflection.TypeNameResolver.GetSimpleType(TypeName typeName)
   at System.Reflection.TypeNameResolver.Resolve(TypeName typeName)
   at System.Reflection.TypeNameResolver.GetTypeReferencedByCustomAttribute(String typeName, RuntimeModule scope)
   at System.Reflection.MetadataImport.GetMarshalAs(ConstArray nativeType, RuntimeModule scope)
   at System.Reflection.PseudoCustomAttribute.GetMarshalAsCustomAttribute(Int32 token, RuntimeModule scope)
   at System.Reflection.PseudoCustomAttribute.GetCustomAttributes(RuntimeParameterInfo parameter, RuntimeType caType, ListBuilder`1& pcas)
   at System.Reflection.RuntimeCustomAttributeData.GetCustomAttributesInternal(RuntimeParameterInfo target)

Reproduction Steps

Execute this Program.cs

using MFilesAPI;

var parameterInfo = typeof(IObjectType).GetMethod("set_Icon").GetParameters().Single();
_ = parameterInfo.CustomAttributes;

together with this .csproj

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net10.0-windows</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="Interop.MFilesAPI" Version="26.2.2" />
    </ItemGroup>
</Project>

Expected behavior

No exception thrown.

Actual behavior

TypeLoadException is thrown.

Regression?

Yes: Using <TargetFramework>net8.0-windows</TargetFramework>, no exception is thrown.

Known Workarounds

No response

Configuration

Which version of .NET is the code running on?
Microsoft.NETCore.App 10.0.2
.NET SDK 10.0.102

What OS and version, and what distro if applicable?
Windows 11

What is the architecture (x64, x86, ARM, ARM64)?
x64

Do you know whether it is specific to that configuration?
The problem also happens with Windows 10 x64, so is likely not specific to the configuration.

If you're using Blazor, which web browser(s) do you see this issue in?
N/A

Other information

Debugging shows that System.Reflection.MetadataImport.GetMarshalAs used to return an empty string in safeArrayUserDefinedSubType under .NET 8, but now (.NET 9 and .NET 10) decodes safeArrayUserDefinedSubTypeRaw into a garbled string in safeArrayUserDefinedTypeName in this instance. The method changed in a3dc133#diff-ca5f2236fd8d5cc746ac054393ca381a074ca52e5d7712a65b5a47cb804cfa46.</issue_description>

<agent_instructions>#124355 was close to addressing this, but copilot coding agent got into a bad state, so we need to create a new PR. Please open a new PR, copying all the changes from that PR and apply the feedback still pending in comments on that PR. Then also run the code-review skill and post the analysis/results as a comment on the PR.</agent_instructions>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@github-actions github-actions bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Feb 13, 2026
@jkotas jkotas added area-Interop-coreclr and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Feb 13, 2026
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @dotnet/interop-contrib
See info in area-owners.md if you want to be subscribed.

…user-defined type name

Return NULL in native code when byte count is 0 for safeArrayUserDefinedSubType,
marshalType, and marshalCookie. Add try/catch for safeArrayUserDefinedType resolution
in managed code. Use IsNullOrEmpty consistently for marshalTypeName. Add regression
test with poison bytes to reliably reproduce the bug.

Fixes #124346

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix TypeLoadException when getting CustomAttributes of ParameterInfo Fix TypeLoadException in GetMarshalAs when SafeArray has zero-length user-defined type name Feb 13, 2026
Copilot AI requested a review from stephentoub February 13, 2026 23:50
@stephentoub stephentoub marked this pull request as ready for review February 14, 2026 00:08
Copilot AI review requested due to automatic review settings February 14, 2026 00:08
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a regression in MetadataImport.GetMarshalAs where certain MarshalAs blobs (notably NATIVE_TYPE_SAFEARRAY with a zero-length UDT name produced by tlbimp) could result in reading past blob boundaries and triggering TypeLoadException during type resolution.

Changes:

  • CoreCLR native: return NULL for SafeArray UDT name / custom marshaler type / cookie when the corresponding byte count is 0.
  • CoreCLR managed: catch TypeLoadException when resolving SafeArrayUserDefinedSubType and align marshalTypeName checks with string.IsNullOrEmpty.
  • Libraries tests: add a regression test that constructs a PE with a crafted FieldMarshal blob to reproduce the bug; fix an existing test method name typo.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.UnitTests/System/Runtime/InteropServices/MarshalAsAttributeTests.cs Adds a regression test building a PE with a crafted SAFEARRAY marshal blob; fixes a test method name typo.
src/coreclr/vm/managedmdimport.cpp Prevents dangling pointers by returning NULL when marshaler-related string byte counts are 0.
src/coreclr/System.Private.CoreLib/src/System/Reflection/MdImport.cs Adds managed defense-in-depth against invalid type name strings and uses string.IsNullOrEmpty consistently.

Comment on lines +281 to +282
// The user may have supplied a bad type name string causing this TypeLoadException
// Regardless, we return the bad type name
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This catch-block comment says "we return the bad type name", but MarshalAsAttribute doesn’t expose the SafeArray user-defined subtype name string anywhere (only the resolved Type via SafeArrayUserDefinedSubType). Please adjust the comment to reflect the actual behavior (e.g., that we swallow TypeLoadException and leave SafeArrayUserDefinedSubType unset) to avoid misleading future maintainers.

Suggested change
// The user may have supplied a bad type name string causing this TypeLoadException
// Regardless, we return the bad type name
// The user may have supplied a bad type name string causing this TypeLoadException.
// Swallow the exception and leave SafeArrayUserDefinedSubType unset; the original
// type name remains in safeArrayUserDefinedTypeName for potential diagnostics.

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +39
[Fact]
public void SafeArrayParameter_ZeroLengthUserDefinedSubType_DoesNotThrow()
{
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test relies on COM interop behavior (FEATURE_COMINTEROP) and Reflection.Emit (PersistedAssemblyBuilder). As a plain [Fact], it may run on non-Windows / browser / NativeAOT configurations where it either can’t exercise the regression or may throw PlatformNotSupportedException. Consider switching this to a ConditionalFact guarded by PlatformDetection.IsBuiltInComEnabled and PlatformDetection.IsReflectionEmitSupported (and any other relevant guards used in this test project).

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +53
Type iface = asm.GetType("TestInterface");
MethodInfo method = iface.GetMethod("TestMethod");
ParameterInfo param = method.GetParameters()[0];

// Must not throw TypeLoadException.
_ = param.GetCustomAttributes(false);

MarshalAsAttribute attr = (MarshalAsAttribute)Attribute.GetCustomAttribute(param, typeof(MarshalAsAttribute));
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These reflection calls return nullable types (e.g., Assembly.GetType / Type.GetMethod / Attribute.GetCustomAttribute). As written, this likely introduces nullable warnings (and potential NullReferenceExceptions if metadata changes). Please add explicit assertions / null-forgiveness where appropriate (or use the overloads that throw on failure) so the test compiles cleanly under nullable and fails with a clear message if the expected members aren’t found.

Suggested change
Type iface = asm.GetType("TestInterface");
MethodInfo method = iface.GetMethod("TestMethod");
ParameterInfo param = method.GetParameters()[0];
// Must not throw TypeLoadException.
_ = param.GetCustomAttributes(false);
MarshalAsAttribute attr = (MarshalAsAttribute)Attribute.GetCustomAttribute(param, typeof(MarshalAsAttribute));
Type iface = asm.GetType("TestInterface") ?? throw new InvalidOperationException("Test interface 'TestInterface' not found in generated assembly.");
MethodInfo method = iface.GetMethod("TestMethod") ?? throw new InvalidOperationException("Test method 'TestMethod' not found on 'TestInterface'.");
ParameterInfo param = method.GetParameters()[0];
// Must not throw TypeLoadException.
_ = param.GetCustomAttributes(false);
MarshalAsAttribute attr = (MarshalAsAttribute?)Attribute.GetCustomAttribute(param, typeof(MarshalAsAttribute))
?? throw new InvalidOperationException("Expected MarshalAsAttribute not found on parameter.");

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +105
md.DefineParameter(1, ParameterAttributes.HasFieldMarshal, "args");
td.CreateType();

MetadataBuilder mdb = ab.GenerateMetadata(out BlobBuilder ilBlob, out _);

// Blob bytes:
// 0x1D NATIVE_TYPE_SAFEARRAY
// 0x09 VT_DISPATCH
// 0x00 compressed string length 0
// 0x58 'X' poison (not consumed by parser)
// 0x00 null terminator
BlobBuilder marshalBlob = new();
marshalBlob.WriteByte(0x1D);
marshalBlob.WriteByte(0x09);
marshalBlob.WriteByte(0x00);
marshalBlob.WriteByte(0x58);
marshalBlob.WriteByte(0x00);
mdb.AddMarshallingDescriptor(
MetadataTokens.ParameterHandle(1),
mdb.GetOrAddBlob(marshalBlob));
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid hardcoding ParameterHandle(1) here; the row id depends on how the Params table is emitted and could change (e.g., if a return parameter row gets introduced). Capture the ParameterBuilder returned from DefineParameter and derive the correct ParameterHandle from its metadata token when calling AddMarshallingDescriptor, so the test is resilient to metadata layout changes.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +56
Assembly asm = alc.LoadFromStream(new MemoryStream(peBytes));
Type iface = asm.GetType("TestInterface");
MethodInfo method = iface.GetMethod("TestMethod");
ParameterInfo param = method.GetParameters()[0];

// Must not throw TypeLoadException.
_ = param.GetCustomAttributes(false);

MarshalAsAttribute attr = (MarshalAsAttribute)Attribute.GetCustomAttribute(param, typeof(MarshalAsAttribute));
Assert.NotNull(attr);
Assert.Equal(UnmanagedType.SafeArray, attr.Value);
Assert.Null(attr.SafeArrayUserDefinedSubType);
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MemoryStream passed to AssemblyLoadContext.LoadFromStream isn’t disposed. Please wrap it in a using/try-finally so the stream is closed promptly (this can also help collectible ALC unloading behavior).

Suggested change
Assembly asm = alc.LoadFromStream(new MemoryStream(peBytes));
Type iface = asm.GetType("TestInterface");
MethodInfo method = iface.GetMethod("TestMethod");
ParameterInfo param = method.GetParameters()[0];
// Must not throw TypeLoadException.
_ = param.GetCustomAttributes(false);
MarshalAsAttribute attr = (MarshalAsAttribute)Attribute.GetCustomAttribute(param, typeof(MarshalAsAttribute));
Assert.NotNull(attr);
Assert.Equal(UnmanagedType.SafeArray, attr.Value);
Assert.Null(attr.SafeArrayUserDefinedSubType);
using (MemoryStream peStream = new(peBytes))
{
Assembly asm = alc.LoadFromStream(peStream);
Type iface = asm.GetType("TestInterface");
MethodInfo method = iface.GetMethod("TestMethod");
ParameterInfo param = method.GetParameters()[0];
// Must not throw TypeLoadException.
_ = param.GetCustomAttributes(false);
MarshalAsAttribute attr = (MarshalAsAttribute)Attribute.GetCustomAttribute(param, typeof(MarshalAsAttribute));
Assert.NotNull(attr);
Assert.Equal(UnmanagedType.SafeArray, attr.Value);
Assert.Null(attr.SafeArrayUserDefinedSubType);
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Getting CustomAttributes of ParameterInfo throws TypeLoadException

4 participants