Skip to content

Commit fe17a0b

Browse files
Copilotstephentoub
andcommitted
Replace test with one that constructs the exact problematic metadata blob
The C# compiler never generates a zero-length user-defined type name in the FieldMarshal blob - it omits the length byte entirely. The previous test using C# attributes therefore did not exercise the bug. This new test uses PersistedAssemblyBuilder to create a valid PE and then injects a FieldMarshal blob with the exact bytes that tlbimp produces: 0x1D (NATIVE_TYPE_SAFEARRAY) | 0x09 (VT_DISPATCH) | 0x00 (string len 0) The trailing zero-length string is the trigger for the bug. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent 0da8620 commit fe17a0b

File tree

1 file changed

+76
-20
lines changed
  • src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.UnitTests/System/Runtime/InteropServices

1 file changed

+76
-20
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Linq;
4+
using System.IO;
55
using System.Reflection;
6+
using System.Reflection.Emit;
7+
using System.Reflection.Metadata;
8+
using System.Reflection.Metadata.Ecma335;
9+
using System.Reflection.PortableExecutable;
10+
using System.Runtime.Loader;
611
using Xunit;
712

813
namespace System.Runtime.InteropServices.Tests
@@ -29,31 +34,82 @@ public void Ctor_ShortUnmanagedType(short umanagedType)
2934
Assert.Equal((UnmanagedType)umanagedType, attribute.Value);
3035
}
3136

32-
[Theory]
33-
[InlineData(nameof(ISafeArrayMarshallingTest.Method1))]
34-
[InlineData(nameof(ISafeArrayMarshallingTest.Method2))]
35-
public void SafeArrayParameter_NoUserDefinedSubType_CustomAttributesDoNotThrow(string methodName)
37+
[Fact]
38+
public void SafeArrayParameter_ZeroLengthUserDefinedSubType_DoesNotThrow()
3639
{
37-
MethodInfo method = typeof(ISafeArrayMarshallingTest).GetMethod(methodName);
38-
ParameterInfo parameter = method.GetParameters().Single();
40+
// Build a PE with a FieldMarshal blob that encodes
41+
// NATIVE_TYPE_SAFEARRAY + VT_DISPATCH + compressed-string-length-0.
42+
// tlbimp produces this format when there is no user-defined sub type name,
43+
// and it previously caused TypeLoadException because the native code returned
44+
// a non-null pointer past the blob boundary for the zero-length string.
45+
byte[] peImage = CreatePEWithSafeArrayFieldMarshal();
46+
47+
AssemblyLoadContext alc = new(nameof(SafeArrayParameter_ZeroLengthUserDefinedSubType_DoesNotThrow), isCollectible: true);
48+
try
49+
{
50+
Assembly asm = alc.LoadFromStream(new MemoryStream(peImage));
51+
Type iface = asm.GetType("TestInterface");
52+
MethodInfo method = iface.GetMethod("TestMethod");
53+
ParameterInfo param = method.GetParameters()[0];
3954

40-
// Accessing CustomAttributes should not throw TypeLoadException
41-
// when SafeArrayUserDefinedSubType is not specified.
42-
var attributes = parameter.CustomAttributes.ToList();
55+
// Accessing custom attributes must not throw TypeLoadException.
56+
_ = param.GetCustomAttributes(false);
4357

44-
MarshalAsAttribute marshalAs = (MarshalAsAttribute)Attribute.GetCustomAttribute(parameter, typeof(MarshalAsAttribute));
45-
Assert.NotNull(marshalAs);
46-
Assert.Equal(UnmanagedType.SafeArray, marshalAs.Value);
47-
Assert.Null(marshalAs.SafeArrayUserDefinedSubType);
58+
MarshalAsAttribute attr = (MarshalAsAttribute)Attribute.GetCustomAttribute(param, typeof(MarshalAsAttribute));
59+
Assert.NotNull(attr);
60+
Assert.Equal(UnmanagedType.SafeArray, attr.Value);
61+
Assert.Null(attr.SafeArrayUserDefinedSubType);
62+
}
63+
finally
64+
{
65+
alc.Unload();
66+
}
4867
}
4968

50-
[ComImport]
51-
[Guid("1FC06EAF-2B18-4D54-B7D4-E654A8BEEF5B")]
52-
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
53-
private interface ISafeArrayMarshallingTest
69+
/// <summary>
70+
/// Builds a minimal PE containing an interface with a method whose parameter
71+
/// has a FieldMarshal blob matching what tlbimp generates for SafeArray
72+
/// without a user-defined sub type:
73+
/// byte 0x1D (NATIVE_TYPE_SAFEARRAY), 0x09 (VT_DISPATCH), 0x00 (string len 0)
74+
/// The trailing zero-length string distinguishes this from blobs the C# compiler
75+
/// produces (which omit the length byte entirely).
76+
/// </summary>
77+
private static byte[] CreatePEWithSafeArrayFieldMarshal()
5478
{
55-
void Method1([MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_DISPATCH)] object[] args);
56-
void Method2([MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_UNKNOWN)] object[] args);
79+
PersistedAssemblyBuilder ab = new(new AssemblyName("SafeArrayTestAsm"), typeof(object).Assembly);
80+
ModuleBuilder moduleDef = ab.DefineDynamicModule("SafeArrayTestAsm.dll");
81+
TypeBuilder typeDef = moduleDef.DefineType("TestInterface",
82+
TypeAttributes.Interface | TypeAttributes.Abstract | TypeAttributes.Public);
83+
84+
MethodBuilder methodDef = typeDef.DefineMethod("TestMethod",
85+
MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual |
86+
MethodAttributes.NewSlot | MethodAttributes.HideBySig,
87+
typeof(void), new[] { typeof(object[]) });
88+
89+
methodDef.DefineParameter(1, ParameterAttributes.HasFieldMarshal, "args");
90+
typeDef.CreateType();
91+
92+
MetadataBuilder mdBuilder = ab.GenerateMetadata(out BlobBuilder ilStream, out _);
93+
94+
// Inject the problematic FieldMarshal descriptor directly:
95+
// 0x1D NATIVE_TYPE_SAFEARRAY
96+
// 0x09 VT_DISPATCH (compressed uint)
97+
// 0x00 compressed string length = 0 (empty user-defined type name)
98+
BlobBuilder marshalDescriptor = new();
99+
marshalDescriptor.WriteBytes(new byte[] { 0x1D, 0x09, 0x00 });
100+
mdBuilder.AddMarshallingDescriptor(
101+
MetadataTokens.ParameterHandle(1),
102+
mdBuilder.GetOrAddBlob(marshalDescriptor));
103+
104+
ManagedPEBuilder peAssembler = new(
105+
PEHeaderBuilder.CreateLibraryHeader(),
106+
new MetadataRootBuilder(mdBuilder),
107+
ilStream,
108+
flags: CorFlags.ILOnly);
109+
110+
BlobBuilder peOutput = new();
111+
peAssembler.Serialize(peOutput);
112+
return peOutput.ToArray();
57113
}
58114
}
59115
}

0 commit comments

Comments
 (0)