Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -269,13 +269,24 @@ internal static unsafe MarshalAsAttribute GetMarshalAs(ConstArray nativeType, Ru
? null
: Text.Encoding.UTF8.GetString(MemoryMarshal.CreateReadOnlySpanFromNullTerminated(marshalCookieRaw));

RuntimeType? safeArrayUserDefinedType = string.IsNullOrEmpty(safeArrayUserDefinedTypeName) ? null :
TypeNameResolver.GetTypeReferencedByCustomAttribute(safeArrayUserDefinedTypeName, scope);
RuntimeType? safeArrayUserDefinedType = null;

try
{
safeArrayUserDefinedType = string.IsNullOrEmpty(safeArrayUserDefinedTypeName) ? null :
TypeNameResolver.GetTypeReferencedByCustomAttribute(safeArrayUserDefinedTypeName, scope);
}
catch (TypeLoadException)
{
// The user may have supplied a bad type name string causing this TypeLoadException
Debug.Assert(safeArrayUserDefinedTypeName is not null);
}

RuntimeType? marshalTypeRef = null;

try
{
marshalTypeRef = marshalTypeName is null ? null : TypeNameResolver.GetTypeReferencedByCustomAttribute(marshalTypeName, scope);
marshalTypeRef = string.IsNullOrEmpty(marshalTypeName) ? null : TypeNameResolver.GetTypeReferencedByCustomAttribute(marshalTypeName, scope);
}
catch (TypeLoadException)
{
Expand Down
6 changes: 3 additions & 3 deletions src/coreclr/vm/managedmdimport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ FCIMPL11(FC_BOOL_RET, MetaDataImport::GetMarshalAs,

*safeArraySubType = info.m_SafeArrayElementVT;

*safeArrayUserDefinedSubType = info.m_strSafeArrayUserDefTypeName;
*safeArrayUserDefinedSubType = info.m_cSafeArrayUserDefTypeNameBytes > 0 ? info.m_strSafeArrayUserDefTypeName : NULL;
#else
*iidParamIndex = 0;

Expand All @@ -59,9 +59,9 @@ FCIMPL11(FC_BOOL_RET, MetaDataImport::GetMarshalAs,
*safeArrayUserDefinedSubType = NULL;
#endif

*marshalType = info.m_strCMMarshalerTypeName;
*marshalType = info.m_cCMMarshalerTypeNameBytes > 0 ? info.m_strCMMarshalerTypeName : NULL;

*marshalCookie = info.m_strCMCookie;
*marshalCookie = info.m_cCMCookieStrBytes > 0 ? info.m_strCMCookie : NULL;

FC_RETURN_BOOL(TRUE);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO;
using System.Reflection;
using System.Reflection.Emit;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
using System.Runtime.Loader;
using Xunit;

namespace System.Runtime.InteropServices.Tests
Expand All @@ -11,7 +18,7 @@ public class MarshalAsAttributeTests
[InlineData((UnmanagedType)(-1))]
[InlineData(UnmanagedType.HString)]
[InlineData((UnmanagedType)int.MaxValue)]
public void Ctor_UmanagedTye(UnmanagedType unmanagedType)
public void Ctor_UnmanagedType(UnmanagedType unmanagedType)
{
var attribute = new MarshalAsAttribute(unmanagedType);
Assert.Equal(unmanagedType, attribute.Value);
Expand All @@ -26,5 +33,92 @@ public void Ctor_ShortUnmanagedType(short umanagedType)
var attribute = new MarshalAsAttribute(umanagedType);
Assert.Equal((UnmanagedType)umanagedType, attribute.Value);
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBuiltInComEnabled), nameof(PlatformDetection.IsReflectionEmitSupported))]
public void SafeArrayParameter_ZeroLengthUserDefinedSubType_DoesNotThrow()
{
byte[] peBytes = BuildPEWithSafeArrayMarshalBlob();

AssemblyLoadContext alc = new(nameof(SafeArrayParameter_ZeroLengthUserDefinedSubType_DoesNotThrow), isCollectible: true);
try
{
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);
}
finally
{
alc.Unload();
}
}

/// <summary>
/// Creates a PE whose parameter has a FieldMarshal blob that
/// reproduces the native bug: NATIVE_TYPE_SAFEARRAY (0x1D),
/// VT_DISPATCH (0x09), zero-length string (0x00), followed by
/// poison byte 'X' (0x58) and null terminator (0x00).
/// Without the native fix the FCALL returns a dangling pointer
/// into the poison byte region causing TypeLoadException.
/// </summary>
private static byte[] BuildPEWithSafeArrayMarshalBlob()
{
PersistedAssemblyBuilder ab = new(
new AssemblyName("SafeArrayTestAsm"), typeof(object).Assembly);
ModuleBuilder mod = ab.DefineDynamicModule("SafeArrayTestAsm.dll");
TypeBuilder td = mod.DefineType("TestInterface",
TypeAttributes.Interface | TypeAttributes.Abstract | TypeAttributes.Public);

MethodBuilder md = td.DefineMethod("TestMethod",
MethodAttributes.Public | MethodAttributes.Abstract |
MethodAttributes.Virtual | MethodAttributes.NewSlot |
MethodAttributes.HideBySig,
typeof(void), new[] { typeof(object[]) });

md.DefineParameter(1, ParameterAttributes.HasFieldMarshal, "args");
td.CreateType();

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

// This is the only parameter defined on the only method, so it
// occupies row 1 in the Param table. PersistedAssemblyBuilder
// emits parameters in definition order deterministically.
const int paramRowNumber = 1;

// 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(paramRowNumber),
mdb.GetOrAddBlob(marshalBlob));
Comment on lines 87 to 111
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.


ManagedPEBuilder pe = new(
PEHeaderBuilder.CreateLibraryHeader(),
new MetadataRootBuilder(mdb),
ilBlob,
flags: CorFlags.ILOnly);

BlobBuilder output = new();
pe.Serialize(output);
return output.ToArray();
}
}
}
Loading