From 4657487cbc0d490d9c5a9dfadf9c75a7720307ba Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 15 Jul 2026 10:54:20 +0200 Subject: [PATCH 1/2] Guard primitive array value-manager routing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e043be9-9070-43bc-aea4-a48438690bdf --- .../JniRuntime.ReflectionJniValueManager.cs | 73 +++++++++++++++++-- .../src/Java.Interop/PublicAPI.Unshipped.txt | 1 + .../Android.Runtime/AndroidRuntime.cs | 8 ++ .../JavaMarshalValueManager.cs | 8 ++ .../PrimitiveArrayInfo.cs | 9 +++ .../Java.Interop/JavaConvertTest.cs | 27 ++++++- 6 files changed, 119 insertions(+), 7 deletions(-) diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniValueManager.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniValueManager.cs index 56f836bd2e3..23a646a252a 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniValueManager.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniValueManager.cs @@ -279,7 +279,7 @@ protected virtual bool TryConstructPeer ( return JavaPeerableValueMarshaler.Instance.CreateGenericValue (ref reference, options, targetType); } var marshaler = GetValueMarshaler (targetType); - return marshaler.CreateValue (ref reference, options, targetType); + return CreateMarshaledValue (ref reference, options, targetType, marshaler); } [return: MaybeNull] @@ -319,8 +319,14 @@ protected virtual bool TryConstructPeer ( #pragma warning restore CS8600,CS8601 // Possible null reference assignment. } - var marshaler = GetValueMarshaler (); - return marshaler.CreateGenericValue (ref reference, options, targetType); + var marshaler = GetValueMarshaler (targetType); + var value = CreateMarshaledValue (ref reference, options, targetType, marshaler); + if (value == null) { + #pragma warning disable 8653 + return default (T); + #pragma warning restore 8653 + } + return (T) value; } object? PeekBoxedObject (JniObjectReference reference) @@ -361,7 +367,7 @@ protected virtual bool TryConstructPeer ( return JavaPeerableValueMarshaler.Instance.CreateGenericValue (ref reference, options, targetType); } var marshaler = GetValueMarshaler (targetType); - return marshaler.CreateValue (ref reference, options, targetType); + return CreateMarshaledValue (ref reference, options, targetType, marshaler); } @@ -401,8 +407,63 @@ protected virtual bool TryConstructPeer ( #pragma warning restore CS8600,CS8601 // Possible null reference assignment. } - var marshaler = GetValueMarshaler (); - return marshaler.CreateGenericValue (ref reference, options, targetType); + var marshaler = GetValueMarshaler (targetType); + var value = CreateMarshaledValue (ref reference, options, targetType, marshaler); + if (value == null) { + #pragma warning disable 8653 + return default (T); + #pragma warning restore 8653 + } + return (T) value; + } + + object? CreateMarshaledValue ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + Type targetType, + JniValueMarshaler marshaler) + { + if (!IsPrimitiveArrayMarshaler (marshaler)) + return marshaler.CreateValue (ref reference, options, targetType); + + var jniType = JniEnvironment.Types.GetJniTypeNameFromInstance (reference); + if (IsMatchingPrimitiveArray (marshaler, jniType)) + return marshaler.CreateValue (ref reference, options, targetType); + + if (GetListType (targetType) != null && jniType != null && !jniType.StartsWith ("[", StringComparison.Ordinal)) + return CreateNonArrayListValue (ref reference, options, targetType); + + throw new InvalidCastException ($"JNI reference of type '{jniType}' is not the primitive array required by managed type '{targetType}'."); + } + + bool IsMatchingPrimitiveArray (JniValueMarshaler marshaler, string? jniType) + { + if (jniType == null || !JniTypeSignature.TryParse (jniType, out var actualType)) + return false; + + foreach (var candidate in JniPrimitiveArrayMarshalers.Value) { + if (!ReferenceEquals (candidate.Value, marshaler)) + continue; + return Runtime.TypeManager.GetTypeSignature (candidate.Key) == actualType; + } + return false; + } + + static bool IsPrimitiveArrayMarshaler (JniValueMarshaler marshaler) + { + foreach (var candidate in JniPrimitiveArrayMarshalers.Value) { + if (ReferenceEquals (candidate.Value, marshaler)) + return true; + } + return false; + } + + protected virtual object? CreateNonArrayListValue ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + Type targetType) + { + throw new InvalidCastException ($"JNI reference cannot be converted to managed list type '{targetType}'."); } Dictionary Marshalers = new Dictionary (); diff --git a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt index 2088a07dc59..48024f01427 100644 --- a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt +++ b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt @@ -117,3 +117,4 @@ override Java.Interop.JniRuntime.ReflectionJniTypeManager.GetTypesForSimpleRefer override Java.Interop.JniRuntime.ReflectionJniTypeManager.RegisterNativeMembers(Java.Interop.JniType! nativeClass, System.Type! type, string? methods) -> void override Java.Interop.JniRuntime.ReflectionJniTypeManager.RegisterNativeMembers(Java.Interop.JniType! nativeClass, System.Type! type, System.ReadOnlySpan methods) -> void virtual Java.Interop.JniRuntime.ReflectionJniValueManager.TryConstructPeer(Java.Interop.IJavaPeerable! self, ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions options, System.Type! type) -> bool +virtual Java.Interop.JniRuntime.ReflectionJniValueManager.CreateNonArrayListValue(ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions options, System.Type! targetType) -> object? diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 34bafabbe58..ee2772e654d 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -630,6 +630,14 @@ public override void WaitForGCBridgeProcessing () return peer; } + protected override object? CreateNonArrayListValue ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + Type targetType) + { + return JavaConvert.FromObjectReference (ref reference, options, targetType); + } + public override void AddPeer (IJavaPeerable value) { if (value == null) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index e83d20fd046..1c026b85e9d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -61,6 +61,14 @@ public override List GetSurfacedPeers () return JavaMarshalRegisteredPeers.GetSurfacedPeers (); } + protected override object? CreateNonArrayListValue ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + Type targetType) + { + return JavaConvert.FromObjectReference (ref reference, options, targetType); + } + protected override bool TryConstructPeer ( IJavaPeerable self, ref JniObjectReference reference, diff --git a/src/Mono.Android/Microsoft.Android.Runtime/PrimitiveArrayInfo.cs b/src/Mono.Android/Microsoft.Android.Runtime/PrimitiveArrayInfo.cs index 77cd34fc30f..760db4e97b8 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/PrimitiveArrayInfo.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/PrimitiveArrayInfo.cs @@ -74,6 +74,15 @@ public override bool TryCreateWrapper ( return false; } + var jniType = JniEnvironment.Types.GetJniTypeNameFromInstance (reference); + if (jniType != "[" + jniSimpleReference) { + if (IsCompatibleListType (targetType)) { + value = null; + return false; + } + throw new InvalidCastException ($"JNI reference of type '{jniType}' is not the primitive array required by managed type '{targetType}'."); + } + var array = createFromReference (ref reference, options); if (targetType == typeof (T[]) || IsCompatibleListType (targetType)) { try { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs index 92552cf6033..05e222d3c25 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs @@ -127,6 +127,31 @@ public void MarshalInt23Array () } } + [Test] + public void ValueManagerConvertsJavaListToPrimitiveIList () + { + using (var values = new Java.Util.ArrayList ()) + using (var first = new Java.Lang.Boolean (true)) + using (var second = new Java.Lang.Boolean (false)) { + values.Add (first); + values.Add (second); + + var reference = values.PeerReference; + var converted = JniEnvironment.Runtime.ValueManager.GetValue> (ref reference, JniObjectReferenceOptions.Copy); + + CollectionAssert.AreEqual (new [] { true, false }, converted); + } + } + + [Test] + public void ValueManagerConvertsPrimitiveArrayToIList () + { + var reference = new JniObjectReference (JNIEnv.NewArray (new [] { true, false }), JniObjectReferenceType.Local); + var converted = JniEnvironment.Runtime.ValueManager.GetValue> (ref reference, JniObjectReferenceOptions.CopyAndDispose); + + CollectionAssert.AreEqual (new [] { true, false }, converted); + } + static Java.Util.ArrayList CreateList (params int[][] items) { var list = new Java.Util.ArrayList (); @@ -139,5 +164,5 @@ static Java.Util.ArrayList CreateList (params int[][] items) return list; } } -} +} From a281eac8376f6b78695aea8b23c00a7573538d18 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 15 Jul 2026 11:09:16 +0200 Subject: [PATCH 2/2] Validate list routing and reference ownership Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e043be9-9070-43bc-aea4-a48438690bdf --- .../JniRuntime.ReflectionJniValueManager.cs | 15 ++++++++-- .../PrimitiveArrayInfo.cs | 6 +++- .../Java.Interop/JavaConvertTest.cs | 29 +++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniValueManager.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniValueManager.cs index 23a646a252a..dca2e43a2cf 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniValueManager.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniValueManager.cs @@ -430,12 +430,22 @@ protected virtual bool TryConstructPeer ( if (IsMatchingPrimitiveArray (marshaler, jniType)) return marshaler.CreateValue (ref reference, options, targetType); - if (GetListType (targetType) != null && jniType != null && !jniType.StartsWith ("[", StringComparison.Ordinal)) - return CreateNonArrayListValue (ref reference, options, targetType); + if (GetListType (targetType) != null && jniType != null && !jniType.StartsWith ("[", StringComparison.Ordinal)) { + if (JavaListType.IsInstanceOfType (reference)) + return CreateNonArrayListValue (ref reference, options, targetType); + JniObjectReference.Dispose (ref reference, options); + throw new InvalidCastException ($"JNI reference of type '{jniType}' does not implement java.util.List required by managed type '{targetType}'."); + } + JniObjectReference.Dispose (ref reference, options); throw new InvalidCastException ($"JNI reference of type '{jniType}' is not the primitive array required by managed type '{targetType}'."); } + static JniType? javaListType; + static JniType JavaListType { + get {return JniType.GetCachedJniType (ref javaListType, "java/util/List");} + } + bool IsMatchingPrimitiveArray (JniValueMarshaler marshaler, string? jniType) { if (jniType == null || !JniTypeSignature.TryParse (jniType, out var actualType)) @@ -463,6 +473,7 @@ static bool IsPrimitiveArrayMarshaler (JniValueMarshaler marshaler) JniObjectReferenceOptions options, Type targetType) { + JniObjectReference.Dispose (ref reference, options); throw new InvalidCastException ($"JNI reference cannot be converted to managed list type '{targetType}'."); } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/PrimitiveArrayInfo.cs b/src/Mono.Android/Microsoft.Android.Runtime/PrimitiveArrayInfo.cs index 760db4e97b8..c74b3bdb1af 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/PrimitiveArrayInfo.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/PrimitiveArrayInfo.cs @@ -76,10 +76,11 @@ public override bool TryCreateWrapper ( var jniType = JniEnvironment.Types.GetJniTypeNameFromInstance (reference); if (jniType != "[" + jniSimpleReference) { - if (IsCompatibleListType (targetType)) { + if (IsCompatibleListType (targetType) && JavaListType.IsInstanceOfType (reference)) { value = null; return false; } + JniObjectReference.Dispose (ref reference, options); throw new InvalidCastException ($"JNI reference of type '{jniType}' is not the primitive array required by managed type '{targetType}'."); } @@ -179,6 +180,9 @@ static bool IsCompatibleListType (Type targetType) list => new JavaDoubleArray (list)), ]; + static JniType? javaListType; + static JniType JavaListType => JniType.GetCachedJniType (ref javaListType, "java/util/List"); + public static bool TryGetArrayTypes (Type elementType, [NotNullWhen (true)] out Type[]? arrayTypes) { foreach (var handler in Handlers) { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs index 05e222d3c25..8f636bc3297 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs @@ -152,6 +152,35 @@ public void ValueManagerConvertsPrimitiveArrayToIList () CollectionAssert.AreEqual (new [] { true, false }, converted); } + [TestCase (JniObjectReferenceOptions.Copy, true)] + [TestCase (JniObjectReferenceOptions.CopyAndDispose, false)] + public void ValueManagerRejectsNonListForPrimitiveIListAndHonorsOwnership ( + JniObjectReferenceOptions options, + bool remainsValid) + { + var reference = new JniObjectReference (JNIEnv.NewString ("not a list"), JniObjectReferenceType.Local); + try { + Assert.Throws (() => + JniEnvironment.Runtime.ValueManager.GetValue> (ref reference, options)); + Assert.AreEqual (remainsValid, reference.IsValid); + } finally { + JniObjectReference.Dispose (ref reference); + } + } + + [Test] + public void ValueManagerRejectsMismatchedPrimitiveArrayAndDisposesReference () + { + var reference = new JniObjectReference (JNIEnv.NewArray (new [] { 1, 2 }), JniObjectReferenceType.Local); + try { + Assert.Throws (() => + JniEnvironment.Runtime.ValueManager.GetValue> (ref reference, JniObjectReferenceOptions.CopyAndDispose)); + Assert.IsFalse (reference.IsValid); + } finally { + JniObjectReference.Dispose (ref reference); + } + } + static Java.Util.ArrayList CreateList (params int[][] items) { var list = new Java.Util.ArrayList ();