Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 {
<init>();
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 {
<init>();
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 {
<init>();
<init>(java.lang.String);
public void jiAddManagedReference(java.lang.Object);
public void jiClearManagedReferences();
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

-keep class net.dot.jni.** { *; <init>(...); }
-keep class net.dot.android.crypto.** { *; <init>(...); }
# 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*(...);
Expand Down
27 changes: 18 additions & 9 deletions src/Xamarin.Android.Build.Tasks/Tasks/R8.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand All @@ -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<string> lines)
{
var temp = Path.GetTempFileName ();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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, "")]
Expand Down Expand Up @@ -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) (?<name>[^\s{]+)", RegexOptions.Multiline)
.Cast<Match> ()
.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<string> GetNativeRuntimeJniTypeNames ()
{
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+ \{ ""(?<value>[^""]+)"" \};")
.Cast<Match> ()
.Select (match => match.Groups ["value"].Value)
.ToArray ();
var jniTypes = names
.Where (name => name.Contains ('/'))
.ToHashSet (StringComparer.Ordinal);

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)} = (?<type>[\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 (auditRoot, "Native", "CoreCLR"),
Path.Combine (auditRoot, "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 (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*""(?<name>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 ()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,26 @@
<Link>..\Expected\CheckPackageManagerAssemblyOrder.java</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(XamarinAndroidSourcePath)src\native\common\include\shared\runtime-jni-names.hh">
<Link>RuntimeJniAudit\runtime-jni-names.hh</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(XamarinAndroidSourcePath)src\java-runtime\java\mono\android\Runtime.java">
<Link>RuntimeJniAudit\Runtime.java</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(XamarinAndroidSourcePath)src\native\clr\**\*.cc">
<Link>RuntimeJniAudit\Native\CoreCLR\%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(XamarinAndroidSourcePath)src\native\nativeaot\**\*.cc">
<Link>RuntimeJniAudit\Native\NativeAOT\%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(XamarinAndroidSourcePath)external\Java.Interop\src\Java.Interop\Java.Interop\ManagedPeer.cs;$(XamarinAndroidSourcePath)external\Java.Interop\src\Java.Interop\Java.Interop\JavaProxyObject.cs;$(XamarinAndroidSourcePath)external\Java.Interop\src\Java.Interop\Java.Interop\JavaProxyThrowable.cs">
<Link>RuntimeJniAudit\Java.Interop\%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

<ItemGroup>
Expand Down
9 changes: 5 additions & 4 deletions src/native/clr/host/bridge-processing.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <host/runtime-util.hh>
#include <runtime-base/logger.hh>
#include <shared/helpers.hh>
#include <shared/runtime-jni-names.hh>

using namespace xamarin::android;

Expand Down Expand Up @@ -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, "<init>", "()V");
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions src/native/clr/host/host.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include <runtime-base/monodroid-state.hh>
#include <runtime-base/timing-internal.hh>
#include <shared/log_types.hh>
#include <shared/runtime-jni-names.hh>

using namespace xamarin::android;

Expand Down Expand Up @@ -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);

Expand Down
3 changes: 2 additions & 1 deletion src/native/clr/host/os-bridge.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <runtime-base/logger.hh>
#include <shared/cpp-util.hh>
#include <shared/helpers.hh>
#include <shared/runtime-jni-names.hh>

using namespace xamarin::android;

Expand Down Expand Up @@ -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, "<init>", "()V");
abort_unless (GCUserPeer_class != nullptr && GCUserPeer_ctor != nullptr, "Failed to load mono.android.GCUserPeer!");
}
Expand Down
21 changes: 21 additions & 0 deletions src/native/common/include/shared/runtime-jni-names.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once

#include <string_view>

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" };
};
}
Loading
Loading