From 7e79d593d1829e6ef49973c4254f36775749d4c8 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Sat, 18 Jul 2026 18:38:32 -0500 Subject: [PATCH 1/3] [wasm] Fix pinvoke with 64-bit enum argument on wasm A P/Invoke whose argument is an enum with a 64-bit underlying type (e.g. 'enum : ulong') crashed on wasm with 'RuntimeError: null function or function signature mismatch'. interp_type_as_ptr() (used by interp_get_icall_sig to decide whether a native call can take the fast MINT_CALLI_NAT_FAST / do_icall path, which passes every argument as a pointer-sized gpointer) treated every enum as pointer-compatible without looking at its underlying type. Raw long/ulong are already correctly excluded on 32-bit targets via the SIZEOF_VOID_P == 8 guard, but a 64-bit enum slipped through and do_icall then called the native 'void(int64_t)' as 'void(int32_t)' on wasm32, causing a call_indirect signature mismatch. Resolve the enum's underlying type so it inherits the same (correctly guarded) decision as the raw integer type. Behavior only changes for enums with a 64-bit underlying type on 32-bit targets such as wasm32. Adds enum:ulong and enum:long coverage to the WASM ABI pinvoke test. Fixes #112262 --- src/mono/mono/mini/interp/transform.c | 9 ++++++++- .../PInvokeTableGeneratorTests.cs | 3 +++ .../testassets/EntryPoints/PInvoke/AbiRules.cs | 17 +++++++++++++++++ src/mono/wasm/testassets/native-libs/wasm-abi.c | 10 ++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/mono/mono/mini/interp/transform.c b/src/mono/mono/mini/interp/transform.c index 2f33faa59bbc49..aaa1e0ff2ba122 100644 --- a/src/mono/mono/mini/interp/transform.c +++ b/src/mono/mono/mini/interp/transform.c @@ -2887,7 +2887,14 @@ interp_type_as_ptr (MonoType *tp) if ((tp)->type == MONO_TYPE_CHAR) return TRUE; if ((tp)->type == MONO_TYPE_VALUETYPE && m_class_is_enumtype (m_type_data_get_klass_unchecked (tp))) - return TRUE; + /* + * An enum is only pointer-compatible if its underlying type is. Checking the + * underlying type ensures a 64-bit enum (e.g. 'enum : ulong') is not treated as a + * pointer-sized icall argument on 32-bit targets such as wasm32, where it would + * otherwise be passed through do_icall's gpointer signature and cause a native + * call_indirect signature mismatch. + */ + return interp_type_as_ptr (mono_class_enum_basetype_internal (m_type_data_get_klass_unchecked (tp))); if (is_scalar_vtype (tp)) return TRUE; return FALSE; diff --git a/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs b/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs index 3be221bc216195..d8fa6339f5d2bb 100644 --- a/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs @@ -408,6 +408,9 @@ private async Task EnsureWasmAbiRulesAreFollowed(Configuration config, bool aot) Assert.Contains(result.TestOutput, m => m.Contains("iares[0]=32")); Assert.Contains(result.TestOutput, m => m.Contains("iares[1]=2")); Assert.Contains("fares.elements[1]=2", result.TestOutput); + // https://github.com/dotnet/runtime/issues/112262: 64-bit enum pinvoke args + Assert.Contains("eu (eu)=18374966859414961921", result.TestOutput); + Assert.Contains("ei (ei)=-2", result.TestOutput); } [Theory] diff --git a/src/mono/wasm/testassets/EntryPoints/PInvoke/AbiRules.cs b/src/mono/wasm/testassets/EntryPoints/PInvoke/AbiRules.cs index a51d5aaaf280d7..754b98aa88c7fc 100644 --- a/src/mono/wasm/testassets/EntryPoints/PInvoke/AbiRules.cs +++ b/src/mono/wasm/testassets/EntryPoints/PInvoke/AbiRules.cs @@ -26,6 +26,9 @@ public struct MyInlineArray { public int element0; } +public enum U64Enum : ulong { A = 0, B = 0xFF00FF00FF00FF00UL } +public enum I64Enum : long { A = 0, B = -3 } + public class Test { public static unsafe int Main(string[] argv) @@ -66,6 +69,14 @@ public static unsafe int Main(string[] argv) var fares = accept_and_return_fixedarray(fa); Console.WriteLine("TestOutput -> fares.elements[1]=" + fares.elements[1]); + // Regression test for https://github.com/dotnet/runtime/issues/112262: a pinvoke + // with a 64-bit enum argument must not be routed through the pointer-sized fast + // icall path on wasm, otherwise the native call traps with a signature mismatch. + var euRes = direct_enum_u64(U64Enum.B); + Console.WriteLine("TestOutput -> eu (eu)=" + (ulong)euRes); + var eiRes = direct_enum_i64(I64Enum.B); + Console.WriteLine("TestOutput -> ei (ei)=" + (long)eiRes); + int exitCode = (int)res.Value; return exitCode; } @@ -104,4 +115,10 @@ public static unsafe MyInlineArray InlineArrayTest2 (MyInlineArray ia) { [DllImport("wasm-abi", EntryPoint="accept_and_return_inlinearray")] public static extern MyInlineArray accept_and_return_inlinearray(MyInlineArray arg); + + [DllImport("wasm-abi", EntryPoint="accept_and_return_ulong")] + public static extern U64Enum direct_enum_u64(U64Enum arg); + + [DllImport("wasm-abi", EntryPoint="accept_and_return_long")] + public static extern I64Enum direct_enum_i64(I64Enum arg); } diff --git a/src/mono/wasm/testassets/native-libs/wasm-abi.c b/src/mono/wasm/testassets/native-libs/wasm-abi.c index 083bce6abe0c59..b08e09528558f0 100644 --- a/src/mono/wasm/testassets/native-libs/wasm-abi.c +++ b/src/mono/wasm/testassets/native-libs/wasm-abi.c @@ -69,3 +69,13 @@ MyInlineArray accept_and_return_inlinearray (MyInlineArray arg) { MyInlineArray accept_and_return_fixedarray (MyInlineArray arg) { return accept_and_return_inlinearray (arg); } + +// Regression coverage for https://github.com/dotnet/runtime/issues/112262: +// a pinvoke whose argument is a 64-bit enum must be passed as a real i64 on wasm. +unsigned long long accept_and_return_ulong (unsigned long long arg) { + return arg + 1; +} + +long long accept_and_return_long (long long arg) { + return arg + 1; +} From e5d1716518674d608b9ef2cc2a06871800c8fad3 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Sat, 18 Jul 2026 18:53:08 -0500 Subject: [PATCH 2/3] Scope the fix to 64-bit-underlying enums only Only enums with a 64-bit underlying type can be misclassified as pointer-sized on a 32-bit target, so defer to the (width-guarded) underlying type only for those and leave the classification of smaller enums unchanged. Verified that a pinvoke with a byte/sbyte/short/ushort/int/uint/long/ulong enum argument all round-trip correctly on wasm. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 62dcd5b3-7227-426b-a072-8812e9c4382d --- src/mono/mono/mini/interp/transform.c | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/mono/mono/mini/interp/transform.c b/src/mono/mono/mini/interp/transform.c index aaa1e0ff2ba122..38524c1c5826b1 100644 --- a/src/mono/mono/mini/interp/transform.c +++ b/src/mono/mono/mini/interp/transform.c @@ -2886,15 +2886,19 @@ interp_type_as_ptr (MonoType *tp) return TRUE; if ((tp)->type == MONO_TYPE_CHAR) return TRUE; - if ((tp)->type == MONO_TYPE_VALUETYPE && m_class_is_enumtype (m_type_data_get_klass_unchecked (tp))) + if ((tp)->type == MONO_TYPE_VALUETYPE && m_class_is_enumtype (m_type_data_get_klass_unchecked (tp))) { /* - * An enum is only pointer-compatible if its underlying type is. Checking the - * underlying type ensures a 64-bit enum (e.g. 'enum : ulong') is not treated as a - * pointer-sized icall argument on 32-bit targets such as wasm32, where it would - * otherwise be passed through do_icall's gpointer signature and cause a native - * call_indirect signature mismatch. + * A 64-bit enum (e.g. 'enum : ulong') must not be treated as a pointer-sized icall + * argument on 32-bit targets such as wasm32: it would otherwise be passed through + * do_icall's gpointer signature and cause a native call_indirect signature mismatch. + * Defer to the underlying type, which is width-guarded, for those cases; enums with + * a smaller underlying type keep their existing classification. */ - return interp_type_as_ptr (mono_class_enum_basetype_internal (m_type_data_get_klass_unchecked (tp))); + MonoType *base_type = mono_class_enum_basetype_internal (m_type_data_get_klass_unchecked (tp)); + if (base_type->type == MONO_TYPE_I8 || base_type->type == MONO_TYPE_U8) + return interp_type_as_ptr (base_type); + return TRUE; + } if (is_scalar_vtype (tp)) return TRUE; return FALSE; From d885c28b812da24dfe636706a34d2982b770fafa Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Sat, 18 Jul 2026 19:24:46 -0500 Subject: [PATCH 3/3] Address review: null-guard enum basetype and cache klass lookup Guard against mono_class_enum_basetype_internal returning NULL (SRE/broken types) before dereferencing, and read m_type_data_get_klass_unchecked once into a local instead of evaluating it twice. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 62dcd5b3-7227-426b-a072-8812e9c4382d --- src/mono/mono/mini/interp/transform.c | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/mono/mono/mini/interp/transform.c b/src/mono/mono/mini/interp/transform.c index 38524c1c5826b1..534acb4ad72a8f 100644 --- a/src/mono/mono/mini/interp/transform.c +++ b/src/mono/mono/mini/interp/transform.c @@ -2886,18 +2886,21 @@ interp_type_as_ptr (MonoType *tp) return TRUE; if ((tp)->type == MONO_TYPE_CHAR) return TRUE; - if ((tp)->type == MONO_TYPE_VALUETYPE && m_class_is_enumtype (m_type_data_get_klass_unchecked (tp))) { - /* - * A 64-bit enum (e.g. 'enum : ulong') must not be treated as a pointer-sized icall - * argument on 32-bit targets such as wasm32: it would otherwise be passed through - * do_icall's gpointer signature and cause a native call_indirect signature mismatch. - * Defer to the underlying type, which is width-guarded, for those cases; enums with - * a smaller underlying type keep their existing classification. - */ - MonoType *base_type = mono_class_enum_basetype_internal (m_type_data_get_klass_unchecked (tp)); - if (base_type->type == MONO_TYPE_I8 || base_type->type == MONO_TYPE_U8) - return interp_type_as_ptr (base_type); - return TRUE; + if ((tp)->type == MONO_TYPE_VALUETYPE) { + MonoClass *tp_klass = m_type_data_get_klass_unchecked (tp); + if (m_class_is_enumtype (tp_klass)) { + /* + * A 64-bit enum (e.g. 'enum : ulong') must not be treated as a pointer-sized icall + * argument on 32-bit targets such as wasm32: it would otherwise be passed through + * do_icall's gpointer signature and cause a native call_indirect signature mismatch. + * Defer to the underlying type, which is width-guarded, for those cases; enums with + * a smaller underlying type keep their existing classification. + */ + MonoType *base_type = mono_class_enum_basetype_internal (tp_klass); + if (base_type && (base_type->type == MONO_TYPE_I8 || base_type->type == MONO_TYPE_U8)) + return interp_type_as_ptr (base_type); + return TRUE; + } } if (is_scalar_vtype (tp)) return TRUE;