Skip to content

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

Open
Copilot wants to merge 4 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 4 commits intomainfrom
copilot/fix-customattributes-type-load-exception

Conversation

Copy link
Contributor

Copilot AI commented Feb 13, 2026

  • Fix native code (managedmdimport.cpp): Return NULL when byte count is 0 for safeArrayUserDefinedSubType, marshalType, and marshalCookie
  • Fix managed code (MdImport.cs): Add try/catch for safeArrayUserDefinedType resolution, update marshalTypeName to use IsNullOrEmpty for consistency (jkotas feedback from Fix TypeLoadException in GetMarshalAs when SafeArray has zero-length user-defined type name #124355)
  • Add regression test (MarshalAsAttributeTests.cs): Use poison bytes {0x1D, 0x09, 0x00, 0x58, 0x00} per stephentoub's feedback from Fix TypeLoadException in GetMarshalAs when SafeArray has zero-length user-defined type name #124355
  • Fix pre-existing Ctor_UmanagedTyeCtor_UnmanagedType typo
  • Address review feedback:
    • Update catch block comment to accurately describe behavior (comment 2806608098)
    • Simplify catch block comment — remove misleading "potential diagnostics" reference to local variable (jkotas feedback)
    • Add ConditionalFact guards for IsBuiltInComEnabled and IsReflectionEmitSupported (comment 2806608113)
    • Fix nullable handling with ! and ? annotations (comment 2806608127)
    • Document ParameterHandle row number assumption with named const (comment 2806608138)
    • Wrap MemoryStream in using declaration (comment 2806608147)
  • Verify build and tests pass
  • Run code-review skill and post results as PR comment
  • Run CodeQL security scan (no issues)
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.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated the catch block comment in commit 81224a0 to accurately describe the behavior — we swallow the exception and leave SafeArrayUserDefinedSubType unset.

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.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Switched to [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBuiltInComEnabled), nameof(PlatformDetection.IsReflectionEmitSupported))] in commit 81224a0, matching the existing convention used by tests in the same project (e.g., GetEndComSlotTests).

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.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed nullable handling in commit 81224a0 — added ! on GetType and GetMethod returns, and changed attr to MarshalAsAttribute? with the ? cast.

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.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in commit 81224a0 by extracting the hardcoded 1 into const int paramRowNumber = 1; with a comment explaining why it's correct. The portable emit layer's ParameterBuilder doesn't expose a metadata token, so we can't derive it programmatically, but the assumption is valid and now clearly documented.

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.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wrapped in using MemoryStream peStream = new(peBytes); in commit 81224a0.

@stephentoub
Copy link
Member

@copilot please address all relevant feedback. Also invoke the code-review skill and post the analysis/results as a comment on this PR

…ispose stream, and comment accuracy

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
auto-merge was automatically disabled February 16, 2026 00:12

Head branch was pushed to by a user without write access

Copy link
Contributor Author

Copilot AI commented Feb 16, 2026

@copilot please address all relevant feedback. Also invoke the code-review skill and post the analysis/results as a comment on this PR

All review feedback has been addressed in commit 81224a0. Here's the code-review skill analysis:

🤖 Code Review — PR #124408

Motivation: Justified regression fix. The native FCALL returned dangling pointers for zero-length strings after commit a3dc133 switched from managed strings to raw byte pointers. This caused CreateReadOnlySpanFromNullTerminated to read garbage and throw TypeLoadException.

Approach: Correct two-layer fix — native root cause fix (return NULL when byte count is 0) plus managed defense-in-depth (catch TypeLoadException). The string.IsNullOrEmpty consistency change for marshalTypeName is a good cleanup.

Summary: ✅ No blocking issues found. The native fix correctly guards all three string outputs (safeArrayUserDefinedSubType, marshalType, marshalCookie). The managed catch mirrors the existing marshalTypeRef pattern. The test uses poison bytes to reliably reproduce the dangling pointer read. NativeAOT doesn't use this code path so no parity concern. All builds and tests pass (test correctly skipped on non-COM platforms via ConditionalFact).

{
// 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.
Copy link
Member

Choose a reason for hiding this comment

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

safeArrayUserDefinedTypeName is a local variable. It is going to be gone once this method returns. What is the "potential diagnostics" that this comment talks about?

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch — removed the misleading "potential diagnostics" reference. The comment now matches the existing marshalTypeRef catch block style. Fixed in 3c8d570.

…back

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
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