From bfa876449f8fef1c7db28fc2f0f4164b33c84f34 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 2 Sep 2026 18:28:45 +0200 Subject: [PATCH 1/2] [r8-obfuscation] Keep runtime-owned JNI types Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Resources/proguard_r8_jni_runtime.cfg | 43 +++++ .../proguard_trimmable_nativeaot.cfg | 10 +- src/Xamarin.Android.Build.Tasks/Tasks/R8.cs | 27 ++-- .../Tasks/R8Tests.cs | 152 +++++++++++++++++- .../Xamarin.Android.Build.Tests.csproj | 4 + src/native/clr/host/bridge-processing.cc | 9 +- src/native/clr/host/host.cc | 5 +- src/native/clr/host/os-bridge.cc | 3 +- .../include/shared/runtime-jni-names.hh | 21 +++ src/native/nativeaot/host/host.cc | 7 +- 10 files changed, 257 insertions(+), 24 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Resources/proguard_r8_jni_runtime.cfg create mode 100644 src/native/common/include/shared/runtime-jni-names.hh diff --git a/src/Xamarin.Android.Build.Tasks/Resources/proguard_r8_jni_runtime.cfg b/src/Xamarin.Android.Build.Tasks/Resources/proguard_r8_jni_runtime.cfg new file mode 100644 index 00000000000..50076f8470c --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Resources/proguard_r8_jni_runtime.cfg @@ -0,0 +1,43 @@ +# Runtime-owned JNI names consumed before managed R8 remapping is available. + +# Native entry points and runtime initialization resolve this class and its fields by name. +-keep class mono.android.Runtime { *; } + +# The native GC bridge resolves these interface methods by name. +-keep interface mono.android.IGCUserPeer { + void monodroidAddReference(java.lang.Object); + void monodroidClearReferences(); +} + +# The native GC bridge creates this runtime helper and invokes its interface methods. +-keep class mono.android.GCUserPeer { + (); + void monodroidAddReference(java.lang.Object); + void monodroidClearReferences(); +} + +# NativeAOT resolves this marker interface before the managed runtime is initialized. +-keep interface net.dot.jni.GCUserPeerable { + void jiAddManagedReference(java.lang.Object); + void jiClearManagedReferences(); +} + +# Java.Interop registers these prebuilt Java runtime types by their original JNI names. +-keep class net.dot.jni.ManagedPeer { + public static native void registerNativeMembers(java.lang.Class,java.lang.String); + public static native void construct(java.lang.Object,java.lang.String,java.lang.Object[]); +} +-keep class net.dot.jni.internal.JavaProxyObject { + (); + public boolean equals(java.lang.Object); + public int hashCode(); + public java.lang.String toString(); + public void jiAddManagedReference(java.lang.Object); + public void jiClearManagedReferences(); +} +-keep class net.dot.jni.internal.JavaProxyThrowable { + (); + (java.lang.String); + public void jiAddManagedReference(java.lang.Object); + public void jiClearManagedReferences(); +} diff --git a/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg b/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg index f4bd50cfd2c..b4f38634e8f 100644 --- a/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg +++ b/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg @@ -4,10 +4,12 @@ -keep class net.dot.jni.** { *; (...); } -keep class net.dot.android.crypto.** { *; (...); } -# The prebuilt native runtime resolves this class and its fields by JNI name. --keep class mono.android.Runtime { *; } -# NativeAOT resolves these interface methods through JNI during startup. --keep class mono.android.IGCUserPeer { *; } +# The managed NativeAOT entry point uses a fixed Java_* JNI export. +-keep class net.dot.jni.nativeaot.JavaInteropRuntime { + public static native void init(java.lang.ClassLoader,java.lang.String,java.lang.String,java.lang.String); +} +# These manifest startup providers, including per-process suffix variants, are runtime-owned. +-keep class net.dot.jni.nativeaot.NativeAotRuntimeProvider* { *; } -keepclassmembers class * extends android.view.View { *** set*(...); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs index 2a0f1ba0b88..e6f896a3166 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs @@ -391,15 +391,8 @@ void GenerateCommonXamarinConfiguration () using var xamcfg = File.CreateText (ProguardCommonXamarinConfiguration); string resourceName = UseTrimmableNativeAotProguardConfiguration ? "proguard_trimmable_nativeaot.cfg" : "proguard_xamarin.cfg"; - using (Stream resource = GetEmbeddedResourceStream (resourceName)) - using (var reader = new StreamReader (resource)) { - while (reader.ReadLine () is string line) { - if (EnableObfuscation && String.Equals (line.Trim (), "-dontobfuscate", StringComparison.OrdinalIgnoreCase)) { - continue; - } - xamcfg.WriteLine (line); - } - } + WriteEmbeddedConfiguration (xamcfg, resourceName, filterLegacyObfuscationRules: EnableObfuscation); + WriteEmbeddedConfiguration (xamcfg, "proguard_r8_jni_runtime.cfg", filterLegacyObfuscationRules: false); if (IgnoreWarnings) { xamcfg.WriteLine ("-ignorewarnings"); } @@ -410,6 +403,22 @@ void GenerateCommonXamarinConfiguration () } } + void WriteEmbeddedConfiguration (StreamWriter writer, string resourceName, bool filterLegacyObfuscationRules) + { + using Stream resource = GetEmbeddedResourceStream (resourceName); + using var reader = new StreamReader (resource); + while (reader.ReadLine () is string line) { + string trimmed = line.Trim (); + if (filterLegacyObfuscationRules && + (String.Equals (trimmed, "-dontobfuscate", StringComparison.OrdinalIgnoreCase) || + trimmed.StartsWith ("-keep class net.dot.jni.** ", StringComparison.Ordinal) || + trimmed.StartsWith ("-keep class mono.android.** ", StringComparison.Ordinal))) { + continue; + } + writer.WriteLine (line); + } + } + void WriteConfiguration (StreamWriter response, IEnumerable lines) { var temp = Path.GetTempFileName (); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs index fafbb967773..3cdc4060d0c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using NUnit.Framework; @@ -10,7 +11,7 @@ namespace Xamarin.Android.Build.Tests { [TestFixture] - public class R8Tests + public class R8Tests : BaseTest { [TestCase ("-keep class com.example.Foo { *; }", false, "")] [TestCase ("-dontwarn com.example.**", false, "")] @@ -129,6 +130,155 @@ public void GenerateSeedMappingAllowsAcwObfuscation () } } + [TestCase (false)] + [TestCase (true)] + public void R8JniObfuscationExplicitlyKeepsRuntimeOwnedJniTypes (bool nativeAot) + { + string path = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ("N")); + Directory.CreateDirectory (path); + string responseFile = ""; + try { + string acwMap = Path.Combine (path, "acw-map.txt"); + string applicationConfiguration = Path.Combine (path, "application.cfg"); + string commonConfiguration = Path.Combine (path, "xamarin.cfg"); + File.WriteAllText (acwMap, "Managed.GeneratedPeer;com.example.GeneratedPeer\n"); + var task = new R8TestTask { + AcwMapFile = acwMap, + BuildEngine = new MockBuildEngine (TestContext.Out), + EnableObfuscation = true, + EnableShrinking = true, + JarPath = "r8.jar", + JavaPlatformJarPath = "android.jar", + OutputDirectory = path, + ProguardCommonXamarinConfiguration = commonConfiguration, + ProguardGeneratedApplicationConfiguration = applicationConfiguration, + UseTrimmableNativeAotProguardConfiguration = nativeAot, + }; + + task.TestGenerateCommandLineCommands (); + responseFile = task.ResponseFilePath; + string configuration = File.ReadAllText (commonConfiguration) + File.ReadAllText (applicationConfiguration); + var keepTargets = Regex.Matches (configuration, @"^-keep (?:class|interface) (?[^\s{]+)", RegexOptions.Multiline) + .Cast () + .Select (match => match.Groups ["name"].Value) + .ToHashSet (StringComparer.Ordinal); + + foreach (string jniName in GetNativeRuntimeJniTypeNames ()) { + string javaName = jniName.Replace ('/', '.'); + Assert.That (keepTargets, Does.Contain (javaName), $"Runtime JNI type `{javaName}` must have an explicit keep rule."); + } + + StringAssert.DoesNotContain ("-keep class net.dot.jni.**", configuration); + StringAssert.DoesNotContain ("-keep class mono.android.**", configuration); + StringAssert.Contains ("void monodroidAddReference(java.lang.Object);", configuration); + StringAssert.Contains ("void monodroidClearReferences();", configuration); + StringAssert.Contains ("public static native void registerNativeMembers(java.lang.Class,java.lang.String);", configuration); + StringAssert.Contains ("public static native void construct(java.lang.Object,java.lang.String,java.lang.Object[]);", configuration); + StringAssert.DoesNotContain ("com.example.GeneratedPeer", configuration, + "An ordinary generated app peer must remain eligible for R8 obfuscation."); + + if (nativeAot) { + Assert.That (keepTargets, Does.Contain ("net.dot.jni.nativeaot.JavaInteropRuntime")); + Assert.That (keepTargets, Does.Contain ("net.dot.jni.nativeaot.NativeAotRuntimeProvider*")); + StringAssert.Contains ("public static native void init(java.lang.ClassLoader,java.lang.String,java.lang.String,java.lang.String);", configuration); + } else { + Assert.That (keepTargets, Does.Not.Contain ("net.dot.jni.nativeaot.JavaInteropRuntime")); + Assert.That (keepTargets, Does.Not.Contain ("net.dot.jni.nativeaot.NativeAotRuntimeProvider*")); + } + } finally { + if (File.Exists (responseFile)) { + File.Delete (responseFile); + } + Directory.Delete (path, recursive: true); + } + } + + [TestCase (false)] + [TestCase (true)] + public void R8WithoutJniObfuscationRetainsBroadRuntimeKeepRules (bool nativeAot) + { + string path = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ("N")); + Directory.CreateDirectory (path); + string responseFile = ""; + try { + string commonConfiguration = Path.Combine (path, "xamarin.cfg"); + var task = new R8TestTask { + BuildEngine = new MockBuildEngine (TestContext.Out), + EnableShrinking = true, + JarPath = "r8.jar", + JavaPlatformJarPath = "android.jar", + OutputDirectory = path, + ProguardCommonXamarinConfiguration = commonConfiguration, + UseTrimmableNativeAotProguardConfiguration = nativeAot, + }; + + task.TestGenerateCommandLineCommands (); + responseFile = task.ResponseFilePath; + string configuration = File.ReadAllText (commonConfiguration); + + StringAssert.Contains ("-dontobfuscate", configuration); + StringAssert.Contains ("-keep class net.dot.jni.**", configuration); + StringAssert.Contains ("-keep class mono.android.Runtime { *; }", configuration); + if (nativeAot) { + StringAssert.DoesNotContain ("-keep class mono.android.**", configuration); + } else { + StringAssert.Contains ("-keep class mono.android.**", configuration); + } + } finally { + if (File.Exists (responseFile)) { + File.Delete (responseFile); + } + Directory.Delete (path, recursive: true); + } + } + + IEnumerable GetNativeRuntimeJniTypeNames () + { + string sourceRoot = GetAssemblyMetadataValue ("XamarinAndroidSourcePath"); + string headerPath = Path.Combine (sourceRoot, "src", "native", "common", "include", "shared", "runtime-jni-names.hh"); + string header = File.ReadAllText (headerPath); + string [] names = Regex.Matches (header, @"std::string_view \w+ \{ ""(?[^""]+)"" \};") + .Cast () + .Select (match => match.Groups ["value"].Value) + .ToArray (); + var jniTypes = names + .Where (name => name.Contains ('/')) + .ToHashSet (StringComparer.Ordinal); + + string runtimeJavaPath = Path.Combine (sourceRoot, "src", "java-runtime", "java", "mono", "android", "Runtime.java"); + string runtimeJava = File.ReadAllText (runtimeJavaPath); + foreach (string fieldName in names.Where (name => name.StartsWith ("mono_android_", StringComparison.Ordinal) || name.StartsWith ("net_dot_jni_", StringComparison.Ordinal))) { + Match field = Regex.Match (runtimeJava, $@"static java\.lang\.Class {Regex.Escape (fieldName)} = (?[\w.]+)\.class;"); + Assert.That (field.Success, Is.True, $"Runtime field `{fieldName}` must resolve to a Java class."); + jniTypes.Add (field.Groups ["type"].Value.Replace ('.', '/')); + } + + foreach (string directory in new [] { + Path.Combine (sourceRoot, "src", "native", "clr"), + Path.Combine (sourceRoot, "src", "native", "nativeaot"), + }) { + foreach (string file in Directory.EnumerateFiles (directory, "*.cc", SearchOption.AllDirectories)) { + string source = File.ReadAllText (file); + Assert.That (Regex.IsMatch (source, @"FindClass\s*\(\s*""(?:mono/|net/dot/)", RegexOptions.CultureInvariant), Is.False, + $"SDK-owned FindClass names in `{file}` must use RuntimeJniNames and explicit keep coverage."); + Assert.That (Regex.IsMatch (source, @"get_class_from_runtime_field\s*\([^;]*""(?:mono_android_|net_dot_jni_)", RegexOptions.CultureInvariant), Is.False, + $"SDK-owned runtime fields in `{file}` must use RuntimeJniNames and explicit keep coverage."); + } + } + + string javaInteropPath = Path.Combine (sourceRoot, "external", "Java.Interop", "src", "Java.Interop", "Java.Interop"); + foreach (string file in Directory.EnumerateFiles (javaInteropPath, "*.cs", SearchOption.TopDirectoryOnly)) { + string source = File.ReadAllText (file); + foreach (Match match in Regex.Matches (source, @"JniTypeName\s*=\s*""(?net/dot/jni/(?:ManagedPeer|internal/JavaProxy(?:Object|Throwable)))""")) { + jniTypes.Add (match.Groups ["name"].Value); + } + } + + Assert.That (jniTypes, Does.Contain ("mono/android/Runtime"), + "CoreCLR JNI exports and NativeAOT startup require mono.android.Runtime to remain stable."); + return jniTypes; + } + [Test] public void ValidateAppliedMappingUsesXA4327 () { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj index 7989975de14..e2e543d7360 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj @@ -33,6 +33,10 @@ + + + + diff --git a/src/native/clr/host/bridge-processing.cc b/src/native/clr/host/bridge-processing.cc index e31c2b47a90..5d0066ae750 100644 --- a/src/native/clr/host/bridge-processing.cc +++ b/src/native/clr/host/bridge-processing.cc @@ -7,6 +7,7 @@ #include #include #include +#include using namespace xamarin::android; @@ -74,7 +75,7 @@ void TemporaryPeerMap::initialize_on_runtime_init (JNIEnv *env, jclass runtimeCl abort_if_invalid_pointer_argument (env, "env"); abort_if_invalid_pointer_argument (runtimeClass, "runtimeClass"); - GCUserPeer_class = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, "mono_android_GCUserPeer", true); + GCUserPeer_class = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, RuntimeJniNames::GCUserPeerRuntimeField, true); abort_unless (GCUserPeer_class != nullptr, "Failed to load mono.android.GCUserPeer!"); GCUserPeer_ctor = env->GetMethodID (GCUserPeer_class, "", "()V"); @@ -139,11 +140,11 @@ void BridgeProcessing::initialize_on_runtime_init (JNIEnv *env, jclass runtimeCl TemporaryPeerMap::initialize_on_runtime_init (env, runtimeClass); // Cache the IGCUserPeer interface method IDs once, instead of resolving them per reference edge. - IGCUserPeer_class = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, "mono_android_IGCUserPeer", true); + IGCUserPeer_class = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, RuntimeJniNames::IGCUserPeerRuntimeField, true); abort_unless (IGCUserPeer_class != nullptr, "Failed to load mono.android.IGCUserPeer!"); - IGCUserPeer_monodroidAddReference = env->GetMethodID (IGCUserPeer_class, "monodroidAddReference", "(Ljava/lang/Object;)V"); - IGCUserPeer_monodroidClearReferences = env->GetMethodID (IGCUserPeer_class, "monodroidClearReferences", "()V"); + IGCUserPeer_monodroidAddReference = env->GetMethodID (IGCUserPeer_class, RuntimeJniNames::IGCUserPeerAddReferenceMethod.data (), "(Ljava/lang/Object;)V"); + IGCUserPeer_monodroidClearReferences = env->GetMethodID (IGCUserPeer_class, RuntimeJniNames::IGCUserPeerClearReferencesMethod.data (), "()V"); abort_unless ( IGCUserPeer_monodroidAddReference != nullptr && IGCUserPeer_monodroidClearReferences != nullptr, diff --git a/src/native/clr/host/host.cc b/src/native/clr/host/host.cc index 8f6b339bbcd..165f400ff5e 100644 --- a/src/native/clr/host/host.cc +++ b/src/native/clr/host/host.cc @@ -31,6 +31,7 @@ #include #include #include +#include using namespace xamarin::android; @@ -468,8 +469,8 @@ void Host::Java_mono_android_Runtime_initInternal ( env->DeleteLocalRef (lrefLoaderClass); init.grefLoader = env->NewGlobalRef (loader); - init.grefIGCUserPeer = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, "mono_android_IGCUserPeer"sv, true); - init.grefGCUserPeerable = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, "net_dot_jni_GCUserPeerable"sv, true); + init.grefIGCUserPeer = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, RuntimeJniNames::IGCUserPeerRuntimeField, true); + init.grefGCUserPeerable = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, RuntimeJniNames::GCUserPeerableRuntimeField, true); log_info (LOG_GC, "GREF GC Threshold: {}"sv, init.grefGcThreshold); diff --git a/src/native/clr/host/os-bridge.cc b/src/native/clr/host/os-bridge.cc index 0037a7549f5..9c5a2ea7489 100644 --- a/src/native/clr/host/os-bridge.cc +++ b/src/native/clr/host/os-bridge.cc @@ -6,6 +6,7 @@ #include #include #include +#include using namespace xamarin::android; @@ -48,7 +49,7 @@ void OSBridge::initialize_on_onload (JavaVM *vm, JNIEnv *env) noexcept void OSBridge::initialize_on_runtime_init (JNIEnv *env, jclass runtimeClass) noexcept { abort_if_invalid_pointer_argument (env, "env"); - GCUserPeer_class = RuntimeUtil::get_class_from_runtime_field(env, runtimeClass, "mono_android_GCUserPeer"sv, true); + GCUserPeer_class = RuntimeUtil::get_class_from_runtime_field(env, runtimeClass, RuntimeJniNames::GCUserPeerRuntimeField, true); GCUserPeer_ctor = env->GetMethodID (GCUserPeer_class, "", "()V"); abort_unless (GCUserPeer_class != nullptr && GCUserPeer_ctor != nullptr, "Failed to load mono.android.GCUserPeer!"); } diff --git a/src/native/common/include/shared/runtime-jni-names.hh b/src/native/common/include/shared/runtime-jni-names.hh new file mode 100644 index 00000000000..731a08d0f00 --- /dev/null +++ b/src/native/common/include/shared/runtime-jni-names.hh @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace xamarin::android +{ + class RuntimeJniNames + { + public: + static inline constexpr std::string_view RuntimeClass { "mono/android/Runtime" }; + static inline constexpr std::string_view IGCUserPeerClass { "mono/android/IGCUserPeer" }; + static inline constexpr std::string_view GCUserPeerableClass { "net/dot/jni/GCUserPeerable" }; + + static inline constexpr std::string_view GCUserPeerRuntimeField { "mono_android_GCUserPeer" }; + static inline constexpr std::string_view IGCUserPeerRuntimeField { "mono_android_IGCUserPeer" }; + static inline constexpr std::string_view GCUserPeerableRuntimeField { "net_dot_jni_GCUserPeerable" }; + + static inline constexpr std::string_view IGCUserPeerAddReferenceMethod { "monodroidAddReference" }; + static inline constexpr std::string_view IGCUserPeerClearReferencesMethod { "monodroidClearReferences" }; + }; +} diff --git a/src/native/nativeaot/host/host.cc b/src/native/nativeaot/host/host.cc index 28830d7cef5..f752f44f2a4 100644 --- a/src/native/nativeaot/host/host.cc +++ b/src/native/nativeaot/host/host.cc @@ -4,6 +4,7 @@ #include #include #include +#include using namespace xamarin::android; @@ -52,7 +53,7 @@ void Host::OnInit (jstring language, jstring filesDir, jstring cacheDir, JnienvI abort_if_invalid_pointer_argument (initArgs, "initArgs"); JNIEnv *env = OSBridge::ensure_jnienv (); - jclass runtimeClass = env->FindClass ("mono/android/Runtime"); + jclass runtimeClass = env->FindClass (RuntimeJniNames::RuntimeClass.data ()); jstring_wrapper language_js (env, language); jstring_wrapper files_dir (env, filesDir); @@ -69,14 +70,14 @@ void Host::OnInit (jstring language, jstring filesDir, jstring cacheDir, JnienvI // NativeAOT initializes Mono.Android's common JNI state before creating the JniRuntime, // so the Java peer marker classes must be provided by the host instead of being looked // up later from mono.android.Runtime static fields like MonoVM/CoreCLR. - jclass lrefIGCUserPeer = env->FindClass ("mono/android/IGCUserPeer"); + jclass lrefIGCUserPeer = env->FindClass (RuntimeJniNames::IGCUserPeerClass.data ()); if (lrefIGCUserPeer == nullptr) [[unlikely]] { env->ExceptionDescribe (); env->ExceptionClear (); abort_unless (false, "Failed to load mono/android/IGCUserPeer class"); } - jclass lrefGCUserPeerable = env->FindClass ("net/dot/jni/GCUserPeerable"); + jclass lrefGCUserPeerable = env->FindClass (RuntimeJniNames::GCUserPeerableClass.data ()); if (lrefGCUserPeerable == nullptr) [[unlikely]] { env->ExceptionDescribe (); env->ExceptionClear (); From eb00af6715b7c20f1da9e58432662311d3a78492 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 2 Sep 2026 19:42:31 +0200 Subject: [PATCH 2/2] [r8-obfuscation] Package runtime JNI audit inputs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Tasks/R8Tests.cs | 12 +++++----- .../Xamarin.Android.Build.Tests.csproj | 24 +++++++++++++++---- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs index 3cdc4060d0c..0be0eb0a8b1 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs @@ -234,8 +234,8 @@ public void R8WithoutJniObfuscationRetainsBroadRuntimeKeepRules (bool nativeAot) IEnumerable GetNativeRuntimeJniTypeNames () { - string sourceRoot = GetAssemblyMetadataValue ("XamarinAndroidSourcePath"); - string headerPath = Path.Combine (sourceRoot, "src", "native", "common", "include", "shared", "runtime-jni-names.hh"); + string auditRoot = Path.Combine (TestContext.CurrentContext.TestDirectory, "RuntimeJniAudit"); + string headerPath = Path.Combine (auditRoot, "runtime-jni-names.hh"); string header = File.ReadAllText (headerPath); string [] names = Regex.Matches (header, @"std::string_view \w+ \{ ""(?[^""]+)"" \};") .Cast () @@ -245,7 +245,7 @@ IEnumerable GetNativeRuntimeJniTypeNames () .Where (name => name.Contains ('/')) .ToHashSet (StringComparer.Ordinal); - string runtimeJavaPath = Path.Combine (sourceRoot, "src", "java-runtime", "java", "mono", "android", "Runtime.java"); + string runtimeJavaPath = Path.Combine (auditRoot, "Runtime.java"); string runtimeJava = File.ReadAllText (runtimeJavaPath); foreach (string fieldName in names.Where (name => name.StartsWith ("mono_android_", StringComparison.Ordinal) || name.StartsWith ("net_dot_jni_", StringComparison.Ordinal))) { Match field = Regex.Match (runtimeJava, $@"static java\.lang\.Class {Regex.Escape (fieldName)} = (?[\w.]+)\.class;"); @@ -254,8 +254,8 @@ IEnumerable GetNativeRuntimeJniTypeNames () } foreach (string directory in new [] { - Path.Combine (sourceRoot, "src", "native", "clr"), - Path.Combine (sourceRoot, "src", "native", "nativeaot"), + Path.Combine (auditRoot, "Native", "CoreCLR"), + Path.Combine (auditRoot, "Native", "NativeAOT"), }) { foreach (string file in Directory.EnumerateFiles (directory, "*.cc", SearchOption.AllDirectories)) { string source = File.ReadAllText (file); @@ -266,7 +266,7 @@ IEnumerable GetNativeRuntimeJniTypeNames () } } - string javaInteropPath = Path.Combine (sourceRoot, "external", "Java.Interop", "src", "Java.Interop", "Java.Interop"); + string javaInteropPath = Path.Combine (auditRoot, "Java.Interop"); foreach (string file in Directory.EnumerateFiles (javaInteropPath, "*.cs", SearchOption.TopDirectoryOnly)) { string source = File.ReadAllText (file); foreach (Match match in Regex.Matches (source, @"JniTypeName\s*=\s*""(?net/dot/jni/(?:ManagedPeer|internal/JavaProxy(?:Object|Throwable)))""")) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj index e2e543d7360..a7dfa820fbd 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj @@ -33,10 +33,6 @@ - - - - @@ -59,6 +55,26 @@ ..\Expected\CheckPackageManagerAssemblyOrder.java PreserveNewest + + RuntimeJniAudit\runtime-jni-names.hh + PreserveNewest + + + RuntimeJniAudit\Runtime.java + PreserveNewest + + + RuntimeJniAudit\Native\CoreCLR\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + RuntimeJniAudit\Native\NativeAOT\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + RuntimeJniAudit\Java.Interop\%(Filename)%(Extension) + PreserveNewest +