From 9efe5b938690d72c5f20ad1a7f67201aa0688fd6 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sun, 28 Dec 2025 02:47:47 +0900 Subject: [PATCH 01/13] Handle byref shared generics in UnsafeAccessor --- src/coreclr/vm/genericdict.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/coreclr/vm/genericdict.cpp b/src/coreclr/vm/genericdict.cpp index f32d4701db2f94..9f8b21f33c4eed 100644 --- a/src/coreclr/vm/genericdict.cpp +++ b/src/coreclr/vm/genericdict.cpp @@ -848,7 +848,8 @@ Dictionary::PopulateEntry( th = th.GetMethodTable()->GetMethodTableMatchingParentClass(declaringType.AsMethodTable()); } - th.GetMethodTable()->EnsureInstanceActive(); + PTR_MethodTable pMT = th.IsByRef() ? th.GetTypeParam().GetMethodTable() : th.GetMethodTable(); + pMT->EnsureInstanceActive(); result = (CORINFO_GENERIC_HANDLE)th.AsPtr(); break; @@ -991,7 +992,7 @@ Dictionary::PopulateEntry( if (nonExpansive) return NULL; - pOwnerMT = ownerType.GetMethodTable(); + pOwnerMT = ownerType.IsByRef() ? ownerType.GetTypeParam().GetMethodTable() : ownerType.GetMethodTable(); _ASSERTE(pOwnerMT != NULL); IfFailThrow(ptr.GetData(&methodFlags)); From a0e62164f826902bb6708a8a605869bd0d79df0f Mon Sep 17 00:00:00 2001 From: Steven He Date: Sun, 28 Dec 2025 03:09:26 +0900 Subject: [PATCH 02/13] Add tests --- .../UnsafeAccessorsTests.Generics.cs | 91 ++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs b/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs index df95ad1bb194a5..bf2d5ca2393d4e 100644 --- a/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs +++ b/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs @@ -9,7 +9,19 @@ using Xunit; -struct Struct { } +struct Struct +{ + public Type Value; + private void SetType(Type type) => Value = type; + private void SetType() => Value = typeof(T); +} + +struct GenericStruct +{ + public Type Value; + private void SetType(Type type) => Value = type; + private void SetType() => Value = typeof(U); +} interface I1 { } @@ -112,6 +124,12 @@ static class Accessors [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "CreateMessage")] public extern static string CreateMessage(GenericBase b, V v); + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "SetType")] + public extern static void SetType(ref GenericStruct s, Type type); + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "SetType")] + public extern static void SetType(ref GenericStruct s); + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ElementType")] public extern static Type ElementType(MyList l); @@ -128,6 +146,15 @@ static class Accessors public extern static ref V GetPrivateStaticField(MyList d); } + static class Accessors + { + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "SetType")] + public extern static void SetType(ref Struct s); + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "SetType")] + public extern static void SetType(ref Struct s, Type type); + } + [Fact] public static void Verify_Generic_AccessStaticFieldClass() { @@ -459,6 +486,68 @@ public static void Verify_Generic_GenericTypeGenericStaticMethod() } } + [Fact] + public static void Verify_Generic_Structs() + { + Console.WriteLine($"Running {nameof(Verify_Generic_Structs)}"); + + { + Struct s = new(); + Accessors.SetType(ref s, typeof(int)); + Assert.Equal(typeof(int), s.Value); + Accessors.SetType(ref s, typeof(string)); + Assert.Equal(typeof(string), s.Value); + Accessors.SetType(ref s, typeof(Struct)); + Assert.Equal(typeof(Struct), s.Value); + Accessors.SetType(ref s, typeof(GenericStruct)); + Assert.Equal(typeof(GenericStruct), s.Value); + Accessors.SetType(ref s, typeof(GenericStruct)); + Assert.Equal(typeof(GenericStruct), s.Value); + } + + { + Struct s = new(); + Accessors.SetType(ref s); + Assert.Equal(typeof(int), s.Value); + Accessors.SetType(ref s); + Assert.Equal(typeof(string), s.Value); + Accessors.SetType(ref s); + Assert.Equal(typeof(Struct), s.Value); + Accessors.SetType>(ref s); + Assert.Equal(typeof(GenericStruct), s.Value); + Accessors.SetType>(ref s); + Assert.Equal(typeof(GenericStruct), s.Value); + } + + { + GenericStruct s = new(); + Accessors.SetType(ref s, typeof(int)); + Assert.Equal(typeof(int), s.Value); + Accessors.SetType(ref s); + Assert.Equal(typeof(string), s.Value); + Accessors.SetType(ref s); + Assert.Equal(typeof(Struct), s.Value); + Accessors.SetType>(ref s); + Assert.Equal(typeof(GenericStruct), s.Value); + Accessors.SetType>(ref s); + Assert.Equal(typeof(GenericStruct), s.Value); + } + + { + GenericStruct s = new(); + Accessors.SetType(ref s, typeof(int)); + Assert.Equal(typeof(int), s.Value); + Accessors.SetType(ref s); + Assert.Equal(typeof(string), s.Value); + Accessors.SetType(ref s); + Assert.Equal(typeof(Struct), s.Value); + Accessors.SetType>(ref s); + Assert.Equal(typeof(GenericStruct), s.Value); + Accessors.SetType>(ref s); + Assert.Equal(typeof(GenericStruct), s.Value); + } + } + class MethodWithConstraints { private string M() where T : U, I2 From 247f91698d8f5f64fca2a2ca5372581f8aa2c035 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sun, 28 Dec 2025 03:37:16 +0900 Subject: [PATCH 03/13] Address feedbacks --- src/coreclr/vm/genericdict.cpp | 6 ++++-- .../UnsafeAccessors/UnsafeAccessorsTests.Generics.cs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/coreclr/vm/genericdict.cpp b/src/coreclr/vm/genericdict.cpp index 9f8b21f33c4eed..0dc08b49a68d3a 100644 --- a/src/coreclr/vm/genericdict.cpp +++ b/src/coreclr/vm/genericdict.cpp @@ -843,12 +843,14 @@ Dictionary::PopulateEntry( } IfFailThrow(ptr.SkipExactlyOne()); + PTR_MethodTable pMT = th.IsByRef() ? th.GetTypeParam().GetMethodTable() : th.GetMethodTable(); + if (!declaringType.IsNull()) { - th = th.GetMethodTable()->GetMethodTableMatchingParentClass(declaringType.AsMethodTable()); + th = pMT->GetMethodTableMatchingParentClass(declaringType.AsMethodTable()); + pMT = th.GetMethodTable(); } - PTR_MethodTable pMT = th.IsByRef() ? th.GetTypeParam().GetMethodTable() : th.GetMethodTable(); pMT->EnsureInstanceActive(); result = (CORINFO_GENERIC_HANDLE)th.AsPtr(); diff --git a/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs b/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs index bf2d5ca2393d4e..6ab617ed41e89f 100644 --- a/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs +++ b/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs @@ -126,7 +126,7 @@ static class Accessors [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "SetType")] public extern static void SetType(ref GenericStruct s, Type type); - + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "SetType")] public extern static void SetType(ref GenericStruct s); From daced4fec4661ea3ab95086dd265a4e84417f5bf Mon Sep 17 00:00:00 2001 From: Steven He Date: Mon, 29 Dec 2025 02:29:49 +0900 Subject: [PATCH 04/13] Partially revert changes to genericdict --- src/coreclr/vm/genericdict.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/coreclr/vm/genericdict.cpp b/src/coreclr/vm/genericdict.cpp index 0dc08b49a68d3a..b1016a616c06ac 100644 --- a/src/coreclr/vm/genericdict.cpp +++ b/src/coreclr/vm/genericdict.cpp @@ -843,15 +843,16 @@ Dictionary::PopulateEntry( } IfFailThrow(ptr.SkipExactlyOne()); - PTR_MethodTable pMT = th.IsByRef() ? th.GetTypeParam().GetMethodTable() : th.GetMethodTable(); - if (!declaringType.IsNull()) { - th = pMT->GetMethodTableMatchingParentClass(declaringType.AsMethodTable()); - pMT = th.GetMethodTable(); + th = th.GetMethodTable()->GetMethodTableMatchingParentClass(declaringType.AsMethodTable()); } - pMT->EnsureInstanceActive(); + MethodTable *pMT = th.GetMethodTable(); + if (pMT) + { + pMT->EnsureInstanceActive(); + } result = (CORINFO_GENERIC_HANDLE)th.AsPtr(); break; @@ -994,7 +995,7 @@ Dictionary::PopulateEntry( if (nonExpansive) return NULL; - pOwnerMT = ownerType.IsByRef() ? ownerType.GetTypeParam().GetMethodTable() : ownerType.GetMethodTable(); + pOwnerMT = ownerType.GetMethodTable(); _ASSERTE(pOwnerMT != NULL); IfFailThrow(ptr.GetData(&methodFlags)); From a6747120bc3caeafecc425a8af8d307a949815a4 Mon Sep 17 00:00:00 2001 From: Steven He Date: Mon, 29 Dec 2025 02:29:59 +0900 Subject: [PATCH 05/13] Fix unsafe accessor emitter --- src/coreclr/vm/unsafeaccessors.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/coreclr/vm/unsafeaccessors.cpp b/src/coreclr/vm/unsafeaccessors.cpp index b82c3e6800a3dc..3930d79f67d0b7 100644 --- a/src/coreclr/vm/unsafeaccessors.cpp +++ b/src/coreclr/vm/unsafeaccessors.cpp @@ -1076,6 +1076,19 @@ bool MethodDesc::TryGenerateUnsafeAccessor(DynamicResolver** resolver, COR_ILMET context.TargetTypeSig = context.DeclarationMetaSig.GetArgProps(); (void)context.TargetTypeSig.PeekElemType(&firstArgCorType); firstArgType = context.DeclarationMetaSig.GetLastTypeHandleThrowing(); + + // For instance member access on value types, the first argument (this) must be a byref. + // However, the target type must not be parameterized. Keeping the byref here can lead to + // incorrect shared generic context lookups at runtime. + if (firstArgCorType == ELEMENT_TYPE_BYREF) + { + SigPointer targetTypeSig = context.TargetTypeSig; + CorElementType elemType; + IfFailThrow(targetTypeSig.GetElemType(&elemType)); + _ASSERTE(elemType == ELEMENT_TYPE_BYREF); + + context.TargetTypeSig = targetTypeSig; + } } // Using the kind type, perform the following: From 89c1e9d5ef088499766e4551d8f42bd9a7ac7655 Mon Sep 17 00:00:00 2001 From: Steven He Date: Mon, 29 Dec 2025 02:32:23 +0900 Subject: [PATCH 06/13] Improve tests --- .../UnsafeAccessorsTests.Generics.cs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs b/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs index 6ab617ed41e89f..20dc977ad7ad4e 100644 --- a/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs +++ b/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs @@ -12,15 +12,33 @@ struct Struct { public Type Value; - private void SetType(Type type) => Value = type; - private void SetType() => Value = typeof(T); + private void SetType(Type type) + { + Value = type; + Console.WriteLine(type); + } + private void SetType() + { + Value = typeof(T); + Console.WriteLine(typeof(T)); + } } struct GenericStruct { public Type Value; - private void SetType(Type type) => Value = type; - private void SetType() => Value = typeof(U); + private void SetType(Type type) + { + Value = type; + Console.WriteLine(type); + Console.WriteLine(typeof(T)); + } + private void SetType() + { + Value = typeof(U); + Console.WriteLine(typeof(U)); + Console.WriteLine(typeof(T)); + } } interface I1 { } From 09d752fe271fe6cdd3552b2998129a18b2572777 Mon Sep 17 00:00:00 2001 From: Steven He Date: Mon, 29 Dec 2025 03:02:25 +0900 Subject: [PATCH 07/13] Only account for instance member access --- src/coreclr/vm/unsafeaccessors.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/coreclr/vm/unsafeaccessors.cpp b/src/coreclr/vm/unsafeaccessors.cpp index 3930d79f67d0b7..00cb328d92c61f 100644 --- a/src/coreclr/vm/unsafeaccessors.cpp +++ b/src/coreclr/vm/unsafeaccessors.cpp @@ -1078,9 +1078,10 @@ bool MethodDesc::TryGenerateUnsafeAccessor(DynamicResolver** resolver, COR_ILMET firstArgType = context.DeclarationMetaSig.GetLastTypeHandleThrowing(); // For instance member access on value types, the first argument (this) must be a byref. - // However, the target type must not be parameterized. Keeping the byref here can lead to - // incorrect shared generic context lookups at runtime. - if (firstArgCorType == ELEMENT_TYPE_BYREF) + // However, the target type used for token emission must not be parameterized. Keeping the + // byref here can lead to incorrect shared generic context lookups at runtime. + if ((context.Kind == UnsafeAccessorKind::Method || context.Kind == UnsafeAccessorKind::Field) + && firstArgCorType == ELEMENT_TYPE_BYREF) { SigPointer targetTypeSig = context.TargetTypeSig; CorElementType elemType; From 0bd4160f5184dbe05f9a2a56aee7cddd557813ed Mon Sep 17 00:00:00 2001 From: Steven He Date: Mon, 29 Dec 2025 04:04:11 +0900 Subject: [PATCH 08/13] Better fix --- src/coreclr/vm/genericdict.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/coreclr/vm/genericdict.cpp b/src/coreclr/vm/genericdict.cpp index b1016a616c06ac..7aa81bbc1a0a09 100644 --- a/src/coreclr/vm/genericdict.cpp +++ b/src/coreclr/vm/genericdict.cpp @@ -848,9 +848,10 @@ Dictionary::PopulateEntry( th = th.GetMethodTable()->GetMethodTableMatchingParentClass(declaringType.AsMethodTable()); } - MethodTable *pMT = th.GetMethodTable(); - if (pMT) + if (!th.IsTypeDesc()) { + MethodTable* pMT = th.AsMethodTable(); + _ASSERTE(pMT != NULL); pMT->EnsureInstanceActive(); } From d5828bd47e894cd525dd2afdd3c4d3936712fab0 Mon Sep 17 00:00:00 2001 From: Steve Date: Sat, 3 Jan 2026 23:18:59 +0900 Subject: [PATCH 09/13] Update src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs Co-authored-by: Aaron R Robinson --- .../UnsafeAccessors/UnsafeAccessorsTests.Generics.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs b/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs index 20dc977ad7ad4e..a13ccf851c748d 100644 --- a/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs +++ b/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs @@ -15,7 +15,6 @@ struct Struct private void SetType(Type type) { Value = type; - Console.WriteLine(type); } private void SetType() { From 289575b1969207f6cc29797cac155bc944d0c78b Mon Sep 17 00:00:00 2001 From: Steve Date: Sat, 3 Jan 2026 23:19:07 +0900 Subject: [PATCH 10/13] Update src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs Co-authored-by: Aaron R Robinson --- .../UnsafeAccessors/UnsafeAccessorsTests.Generics.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs b/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs index a13ccf851c748d..598e2d836cc11b 100644 --- a/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs +++ b/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs @@ -19,7 +19,6 @@ private void SetType(Type type) private void SetType() { Value = typeof(T); - Console.WriteLine(typeof(T)); } } From c82b07b3b6a1adc79963c312ecdc2e1556bcca81 Mon Sep 17 00:00:00 2001 From: Steve Date: Sat, 3 Jan 2026 23:19:14 +0900 Subject: [PATCH 11/13] Update src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs Co-authored-by: Aaron R Robinson --- .../UnsafeAccessors/UnsafeAccessorsTests.Generics.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs b/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs index 598e2d836cc11b..84d8636b9d6629 100644 --- a/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs +++ b/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs @@ -34,8 +34,6 @@ private void SetType(Type type) private void SetType() { Value = typeof(U); - Console.WriteLine(typeof(U)); - Console.WriteLine(typeof(T)); } } From b67e6543d230b9c82ac10112e08d05085700cab7 Mon Sep 17 00:00:00 2001 From: Steve Date: Sat, 3 Jan 2026 23:19:22 +0900 Subject: [PATCH 12/13] Update src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs Co-authored-by: Aaron R Robinson --- .../UnsafeAccessors/UnsafeAccessorsTests.Generics.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs b/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs index 84d8636b9d6629..0b46c2c0f09f77 100644 --- a/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs +++ b/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.Generics.cs @@ -28,8 +28,6 @@ struct GenericStruct private void SetType(Type type) { Value = type; - Console.WriteLine(type); - Console.WriteLine(typeof(T)); } private void SetType() { From c9f07fce94b42e507f3a44c0400dd1bf529b0f15 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sun, 4 Jan 2026 19:31:54 +0900 Subject: [PATCH 13/13] Update comments --- src/coreclr/vm/unsafeaccessors.cpp | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/coreclr/vm/unsafeaccessors.cpp b/src/coreclr/vm/unsafeaccessors.cpp index 00cb328d92c61f..5411a4fd0afed9 100644 --- a/src/coreclr/vm/unsafeaccessors.cpp +++ b/src/coreclr/vm/unsafeaccessors.cpp @@ -1077,18 +1077,21 @@ bool MethodDesc::TryGenerateUnsafeAccessor(DynamicResolver** resolver, COR_ILMET (void)context.TargetTypeSig.PeekElemType(&firstArgCorType); firstArgType = context.DeclarationMetaSig.GetLastTypeHandleThrowing(); - // For instance member access on value types, the first argument (this) must be a byref. - // However, the target type used for token emission must not be parameterized. Keeping the - // byref here can lead to incorrect shared generic context lookups at runtime. + // For instance member access, the first argument is the receiver ("this"). + // For value types the receiver must be passed byref. A byref receiver can also occur for reference + // types depending on the declaration signature. + // + // However, when emitting the metadata token for the accessed member, the owning type must not be + // a byref. Keeping the byref here can lead to incorrect shared generic context lookups at runtime. + // + // No special handling is required for other forms of type parameterization here: + // * Arrays and generic instantiations are part of the owning type identity and must not be stripped. + // * Other forms (eg. pointers) are not valid target types and will be rejected by ValidateTargetType(). if ((context.Kind == UnsafeAccessorKind::Method || context.Kind == UnsafeAccessorKind::Field) && firstArgCorType == ELEMENT_TYPE_BYREF) { - SigPointer targetTypeSig = context.TargetTypeSig; - CorElementType elemType; - IfFailThrow(targetTypeSig.GetElemType(&elemType)); - _ASSERTE(elemType == ELEMENT_TYPE_BYREF); - - context.TargetTypeSig = targetTypeSig; + // Consume byref token. + (void)context.TargetTypeSig.GetElemType(nullptr); } }