Skip to content

Commit 3d831d0

Browse files
[r8-obfuscation] Keep runtime-owned JNI types
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent ec9422a commit 3d831d0

10 files changed

Lines changed: 257 additions & 24 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Runtime-owned JNI names consumed before managed R8 remapping is available.
2+
3+
# Native entry points and runtime initialization resolve this class and its fields by name.
4+
-keep class mono.android.Runtime { *; }
5+
6+
# The native GC bridge resolves these interface methods by name.
7+
-keep interface mono.android.IGCUserPeer {
8+
void monodroidAddReference(java.lang.Object);
9+
void monodroidClearReferences();
10+
}
11+
12+
# The native GC bridge creates this runtime helper and invokes its interface methods.
13+
-keep class mono.android.GCUserPeer {
14+
<init>();
15+
void monodroidAddReference(java.lang.Object);
16+
void monodroidClearReferences();
17+
}
18+
19+
# NativeAOT resolves this marker interface before the managed runtime is initialized.
20+
-keep interface net.dot.jni.GCUserPeerable {
21+
void jiAddManagedReference(java.lang.Object);
22+
void jiClearManagedReferences();
23+
}
24+
25+
# Java.Interop registers these prebuilt Java runtime types by their original JNI names.
26+
-keep class net.dot.jni.ManagedPeer {
27+
public static native void registerNativeMembers(java.lang.Class,java.lang.String);
28+
public static native void construct(java.lang.Object,java.lang.String,java.lang.Object[]);
29+
}
30+
-keep class net.dot.jni.internal.JavaProxyObject {
31+
<init>();
32+
public boolean equals(java.lang.Object);
33+
public int hashCode();
34+
public java.lang.String toString();
35+
public void jiAddManagedReference(java.lang.Object);
36+
public void jiClearManagedReferences();
37+
}
38+
-keep class net.dot.jni.internal.JavaProxyThrowable {
39+
<init>();
40+
<init>(java.lang.String);
41+
public void jiAddManagedReference(java.lang.Object);
42+
public void jiClearManagedReferences();
43+
}

src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
-keep class net.dot.jni.** { *; <init>(...); }
66
-keep class net.dot.android.crypto.** { *; <init>(...); }
7-
# The prebuilt native runtime resolves this class and its fields by JNI name.
8-
-keep class mono.android.Runtime { *; }
9-
# NativeAOT resolves these interface methods through JNI during startup.
10-
-keep class mono.android.IGCUserPeer { *; }
7+
# The managed NativeAOT entry point uses a fixed Java_* JNI export.
8+
-keep class net.dot.jni.nativeaot.JavaInteropRuntime {
9+
public static native void init(java.lang.ClassLoader,java.lang.String,java.lang.String,java.lang.String);
10+
}
11+
# These manifest startup providers, including per-process suffix variants, are runtime-owned.
12+
-keep class net.dot.jni.nativeaot.NativeAotRuntimeProvider* { *; }
1113

1214
-keepclassmembers class * extends android.view.View {
1315
*** set*(...);

src/Xamarin.Android.Build.Tasks/Tasks/R8.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -391,15 +391,8 @@ void GenerateCommonXamarinConfiguration ()
391391

392392
using var xamcfg = File.CreateText (ProguardCommonXamarinConfiguration);
393393
string resourceName = UseTrimmableNativeAotProguardConfiguration ? "proguard_trimmable_nativeaot.cfg" : "proguard_xamarin.cfg";
394-
using (Stream resource = GetEmbeddedResourceStream (resourceName))
395-
using (var reader = new StreamReader (resource)) {
396-
while (reader.ReadLine () is string line) {
397-
if (EnableObfuscation && String.Equals (line.Trim (), "-dontobfuscate", StringComparison.OrdinalIgnoreCase)) {
398-
continue;
399-
}
400-
xamcfg.WriteLine (line);
401-
}
402-
}
394+
WriteEmbeddedConfiguration (xamcfg, resourceName, filterLegacyObfuscationRules: EnableObfuscation);
395+
WriteEmbeddedConfiguration (xamcfg, "proguard_r8_jni_runtime.cfg", filterLegacyObfuscationRules: false);
403396
if (IgnoreWarnings) {
404397
xamcfg.WriteLine ("-ignorewarnings");
405398
}
@@ -410,6 +403,22 @@ void GenerateCommonXamarinConfiguration ()
410403
}
411404
}
412405

406+
void WriteEmbeddedConfiguration (StreamWriter writer, string resourceName, bool filterLegacyObfuscationRules)
407+
{
408+
using Stream resource = GetEmbeddedResourceStream (resourceName);
409+
using var reader = new StreamReader (resource);
410+
while (reader.ReadLine () is string line) {
411+
string trimmed = line.Trim ();
412+
if (filterLegacyObfuscationRules &&
413+
(String.Equals (trimmed, "-dontobfuscate", StringComparison.OrdinalIgnoreCase) ||
414+
trimmed.StartsWith ("-keep class net.dot.jni.** ", StringComparison.Ordinal) ||
415+
trimmed.StartsWith ("-keep class mono.android.** ", StringComparison.Ordinal))) {
416+
continue;
417+
}
418+
writer.WriteLine (line);
419+
}
420+
}
421+
413422
void WriteConfiguration (StreamWriter response, IEnumerable<string> lines)
414423
{
415424
var temp = Path.GetTempFileName ();

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
5+
using System.Text.RegularExpressions;
56
using Microsoft.Build.Framework;
67
using Microsoft.Build.Utilities;
78
using NUnit.Framework;
@@ -10,7 +11,7 @@
1011
namespace Xamarin.Android.Build.Tests
1112
{
1213
[TestFixture]
13-
public class R8Tests
14+
public class R8Tests : BaseTest
1415
{
1516
[TestCase ("-keep class com.example.Foo { *; }", false, "")]
1617
[TestCase ("-dontwarn com.example.**", false, "")]
@@ -129,6 +130,155 @@ public void GenerateSeedMappingAllowsAcwObfuscation ()
129130
}
130131
}
131132

133+
[TestCase (false)]
134+
[TestCase (true)]
135+
public void R8JniObfuscationExplicitlyKeepsRuntimeOwnedJniTypes (bool nativeAot)
136+
{
137+
string path = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ("N"));
138+
Directory.CreateDirectory (path);
139+
string responseFile = "";
140+
try {
141+
string acwMap = Path.Combine (path, "acw-map.txt");
142+
string applicationConfiguration = Path.Combine (path, "application.cfg");
143+
string commonConfiguration = Path.Combine (path, "xamarin.cfg");
144+
File.WriteAllText (acwMap, "Managed.GeneratedPeer;com.example.GeneratedPeer\n");
145+
var task = new R8TestTask {
146+
AcwMapFile = acwMap,
147+
BuildEngine = new MockBuildEngine (TestContext.Out),
148+
EnableObfuscation = true,
149+
EnableShrinking = true,
150+
JarPath = "r8.jar",
151+
JavaPlatformJarPath = "android.jar",
152+
OutputDirectory = path,
153+
ProguardCommonXamarinConfiguration = commonConfiguration,
154+
ProguardGeneratedApplicationConfiguration = applicationConfiguration,
155+
UseTrimmableNativeAotProguardConfiguration = nativeAot,
156+
};
157+
158+
task.TestGenerateCommandLineCommands ();
159+
responseFile = task.ResponseFilePath;
160+
string configuration = File.ReadAllText (commonConfiguration) + File.ReadAllText (applicationConfiguration);
161+
var keepTargets = Regex.Matches (configuration, @"^-keep (?:class|interface) (?<name>[^\s{]+)", RegexOptions.Multiline)
162+
.Cast<Match> ()
163+
.Select (match => match.Groups ["name"].Value)
164+
.ToHashSet (StringComparer.Ordinal);
165+
166+
foreach (string jniName in GetNativeRuntimeJniTypeNames ()) {
167+
string javaName = jniName.Replace ('/', '.');
168+
Assert.That (keepTargets, Does.Contain (javaName), $"Runtime JNI type `{javaName}` must have an explicit keep rule.");
169+
}
170+
171+
StringAssert.DoesNotContain ("-keep class net.dot.jni.**", configuration);
172+
StringAssert.DoesNotContain ("-keep class mono.android.**", configuration);
173+
StringAssert.Contains ("void monodroidAddReference(java.lang.Object);", configuration);
174+
StringAssert.Contains ("void monodroidClearReferences();", configuration);
175+
StringAssert.Contains ("public static native void registerNativeMembers(java.lang.Class,java.lang.String);", configuration);
176+
StringAssert.Contains ("public static native void construct(java.lang.Object,java.lang.String,java.lang.Object[]);", configuration);
177+
StringAssert.DoesNotContain ("com.example.GeneratedPeer", configuration,
178+
"An ordinary generated app peer must remain eligible for R8 obfuscation.");
179+
180+
if (nativeAot) {
181+
Assert.That (keepTargets, Does.Contain ("net.dot.jni.nativeaot.JavaInteropRuntime"));
182+
Assert.That (keepTargets, Does.Contain ("net.dot.jni.nativeaot.NativeAotRuntimeProvider*"));
183+
StringAssert.Contains ("public static native void init(java.lang.ClassLoader,java.lang.String,java.lang.String,java.lang.String);", configuration);
184+
} else {
185+
Assert.That (keepTargets, Does.Not.Contain ("net.dot.jni.nativeaot.JavaInteropRuntime"));
186+
Assert.That (keepTargets, Does.Not.Contain ("net.dot.jni.nativeaot.NativeAotRuntimeProvider*"));
187+
}
188+
} finally {
189+
if (File.Exists (responseFile)) {
190+
File.Delete (responseFile);
191+
}
192+
Directory.Delete (path, recursive: true);
193+
}
194+
}
195+
196+
[TestCase (false)]
197+
[TestCase (true)]
198+
public void R8WithoutJniObfuscationRetainsBroadRuntimeKeepRules (bool nativeAot)
199+
{
200+
string path = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ("N"));
201+
Directory.CreateDirectory (path);
202+
string responseFile = "";
203+
try {
204+
string commonConfiguration = Path.Combine (path, "xamarin.cfg");
205+
var task = new R8TestTask {
206+
BuildEngine = new MockBuildEngine (TestContext.Out),
207+
EnableShrinking = true,
208+
JarPath = "r8.jar",
209+
JavaPlatformJarPath = "android.jar",
210+
OutputDirectory = path,
211+
ProguardCommonXamarinConfiguration = commonConfiguration,
212+
UseTrimmableNativeAotProguardConfiguration = nativeAot,
213+
};
214+
215+
task.TestGenerateCommandLineCommands ();
216+
responseFile = task.ResponseFilePath;
217+
string configuration = File.ReadAllText (commonConfiguration);
218+
219+
StringAssert.Contains ("-dontobfuscate", configuration);
220+
StringAssert.Contains ("-keep class net.dot.jni.**", configuration);
221+
StringAssert.Contains ("-keep class mono.android.Runtime { *; }", configuration);
222+
if (nativeAot) {
223+
StringAssert.DoesNotContain ("-keep class mono.android.**", configuration);
224+
} else {
225+
StringAssert.Contains ("-keep class mono.android.**", configuration);
226+
}
227+
} finally {
228+
if (File.Exists (responseFile)) {
229+
File.Delete (responseFile);
230+
}
231+
Directory.Delete (path, recursive: true);
232+
}
233+
}
234+
235+
IEnumerable<string> GetNativeRuntimeJniTypeNames ()
236+
{
237+
string sourceRoot = GetAssemblyMetadataValue ("XamarinAndroidSourcePath");
238+
string headerPath = Path.Combine (sourceRoot, "src", "native", "common", "include", "shared", "runtime-jni-names.hh");
239+
string header = File.ReadAllText (headerPath);
240+
string [] names = Regex.Matches (header, @"std::string_view \w+ \{ ""(?<value>[^""]+)"" \};")
241+
.Cast<Match> ()
242+
.Select (match => match.Groups ["value"].Value)
243+
.ToArray ();
244+
var jniTypes = names
245+
.Where (name => name.Contains ('/'))
246+
.ToHashSet (StringComparer.Ordinal);
247+
248+
string runtimeJavaPath = Path.Combine (sourceRoot, "src", "java-runtime", "java", "mono", "android", "Runtime.java");
249+
string runtimeJava = File.ReadAllText (runtimeJavaPath);
250+
foreach (string fieldName in names.Where (name => name.StartsWith ("mono_android_", StringComparison.Ordinal) || name.StartsWith ("net_dot_jni_", StringComparison.Ordinal))) {
251+
Match field = Regex.Match (runtimeJava, $@"static java\.lang\.Class {Regex.Escape (fieldName)} = (?<type>[\w.]+)\.class;");
252+
Assert.That (field.Success, Is.True, $"Runtime field `{fieldName}` must resolve to a Java class.");
253+
jniTypes.Add (field.Groups ["type"].Value.Replace ('.', '/'));
254+
}
255+
256+
foreach (string directory in new [] {
257+
Path.Combine (sourceRoot, "src", "native", "clr"),
258+
Path.Combine (sourceRoot, "src", "native", "nativeaot"),
259+
}) {
260+
foreach (string file in Directory.EnumerateFiles (directory, "*.cc", SearchOption.AllDirectories)) {
261+
string source = File.ReadAllText (file);
262+
Assert.That (Regex.IsMatch (source, @"FindClass\s*\(\s*""(?:mono/|net/dot/)", RegexOptions.CultureInvariant), Is.False,
263+
$"SDK-owned FindClass names in `{file}` must use RuntimeJniNames and explicit keep coverage.");
264+
Assert.That (Regex.IsMatch (source, @"get_class_from_runtime_field\s*\([^;]*""(?:mono_android_|net_dot_jni_)", RegexOptions.CultureInvariant), Is.False,
265+
$"SDK-owned runtime fields in `{file}` must use RuntimeJniNames and explicit keep coverage.");
266+
}
267+
}
268+
269+
string javaInteropPath = Path.Combine (sourceRoot, "external", "Java.Interop", "src", "Java.Interop", "Java.Interop");
270+
foreach (string file in Directory.EnumerateFiles (javaInteropPath, "*.cs", SearchOption.TopDirectoryOnly)) {
271+
string source = File.ReadAllText (file);
272+
foreach (Match match in Regex.Matches (source, @"JniTypeName\s*=\s*""(?<name>net/dot/jni/(?:ManagedPeer|internal/JavaProxy(?:Object|Throwable)))""")) {
273+
jniTypes.Add (match.Groups ["name"].Value);
274+
}
275+
}
276+
277+
Assert.That (jniTypes, Does.Contain ("mono/android/Runtime"),
278+
"CoreCLR JNI exports and NativeAOT startup require mono.android.Runtime to remain stable.");
279+
return jniTypes;
280+
}
281+
132282
[Test]
133283
public void ValidateAppliedMappingUsesXA4327 ()
134284
{

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
<ProjectReference Include="..\..\..\AndroidBuildConfig\AndroidBuildConfig.csproj" />
3232
</ItemGroup>
3333

34+
<ItemGroup>
35+
<AssemblyMetadata Include="XamarinAndroidSourcePath" Value="$(XamarinAndroidSourcePath)" />
36+
</ItemGroup>
37+
3438
<ItemGroup>
3539
<Compile Remove="DebuggingTasksTests.cs" Condition="!Exists('$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Build.Debugging.Tasks.dll')" />
3640
<Compile Remove="Resources\ApacheHttpClient.cs" />

src/native/clr/host/bridge-processing.cc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <host/runtime-util.hh>
88
#include <runtime-base/logger.hh>
99
#include <shared/helpers.hh>
10+
#include <shared/runtime-jni-names.hh>
1011

1112
using namespace xamarin::android;
1213

@@ -74,7 +75,7 @@ void TemporaryPeerMap::initialize_on_runtime_init (JNIEnv *env, jclass runtimeCl
7475
abort_if_invalid_pointer_argument (env, "env");
7576
abort_if_invalid_pointer_argument (runtimeClass, "runtimeClass");
7677

77-
GCUserPeer_class = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, "mono_android_GCUserPeer", true);
78+
GCUserPeer_class = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, RuntimeJniNames::GCUserPeerRuntimeField, true);
7879
abort_unless (GCUserPeer_class != nullptr, "Failed to load mono.android.GCUserPeer!");
7980

8081
GCUserPeer_ctor = env->GetMethodID (GCUserPeer_class, "<init>", "()V");
@@ -139,11 +140,11 @@ void BridgeProcessing::initialize_on_runtime_init (JNIEnv *env, jclass runtimeCl
139140
TemporaryPeerMap::initialize_on_runtime_init (env, runtimeClass);
140141

141142
// Cache the IGCUserPeer interface method IDs once, instead of resolving them per reference edge.
142-
IGCUserPeer_class = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, "mono_android_IGCUserPeer", true);
143+
IGCUserPeer_class = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, RuntimeJniNames::IGCUserPeerRuntimeField, true);
143144
abort_unless (IGCUserPeer_class != nullptr, "Failed to load mono.android.IGCUserPeer!");
144145

145-
IGCUserPeer_monodroidAddReference = env->GetMethodID (IGCUserPeer_class, "monodroidAddReference", "(Ljava/lang/Object;)V");
146-
IGCUserPeer_monodroidClearReferences = env->GetMethodID (IGCUserPeer_class, "monodroidClearReferences", "()V");
146+
IGCUserPeer_monodroidAddReference = env->GetMethodID (IGCUserPeer_class, RuntimeJniNames::IGCUserPeerAddReferenceMethod.data (), "(Ljava/lang/Object;)V");
147+
IGCUserPeer_monodroidClearReferences = env->GetMethodID (IGCUserPeer_class, RuntimeJniNames::IGCUserPeerClearReferencesMethod.data (), "()V");
147148

148149
abort_unless (
149150
IGCUserPeer_monodroidAddReference != nullptr && IGCUserPeer_monodroidClearReferences != nullptr,

src/native/clr/host/host.cc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include <runtime-base/monodroid-state.hh>
3232
#include <runtime-base/timing-internal.hh>
3333
#include <shared/log_types.hh>
34+
#include <shared/runtime-jni-names.hh>
3435

3536
using namespace xamarin::android;
3637

@@ -468,8 +469,8 @@ void Host::Java_mono_android_Runtime_initInternal (
468469
env->DeleteLocalRef (lrefLoaderClass);
469470

470471
init.grefLoader = env->NewGlobalRef (loader);
471-
init.grefIGCUserPeer = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, "mono_android_IGCUserPeer"sv, true);
472-
init.grefGCUserPeerable = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, "net_dot_jni_GCUserPeerable"sv, true);
472+
init.grefIGCUserPeer = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, RuntimeJniNames::IGCUserPeerRuntimeField, true);
473+
init.grefGCUserPeerable = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, RuntimeJniNames::GCUserPeerableRuntimeField, true);
473474

474475
log_info (LOG_GC, "GREF GC Threshold: {}"sv, init.grefGcThreshold);
475476

src/native/clr/host/os-bridge.cc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <runtime-base/logger.hh>
77
#include <shared/cpp-util.hh>
88
#include <shared/helpers.hh>
9+
#include <shared/runtime-jni-names.hh>
910

1011
using namespace xamarin::android;
1112

@@ -36,7 +37,7 @@ void OSBridge::initialize_on_onload (JavaVM *vm, JNIEnv *env) noexcept
3637
void OSBridge::initialize_on_runtime_init (JNIEnv *env, jclass runtimeClass) noexcept
3738
{
3839
abort_if_invalid_pointer_argument (env, "env");
39-
GCUserPeer_class = RuntimeUtil::get_class_from_runtime_field(env, runtimeClass, "mono_android_GCUserPeer"sv, true);
40+
GCUserPeer_class = RuntimeUtil::get_class_from_runtime_field(env, runtimeClass, RuntimeJniNames::GCUserPeerRuntimeField, true);
4041
GCUserPeer_ctor = env->GetMethodID (GCUserPeer_class, "<init>", "()V");
4142
abort_unless (GCUserPeer_class != nullptr && GCUserPeer_ctor != nullptr, "Failed to load mono.android.GCUserPeer!");
4243
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#pragma once
2+
3+
#include <string_view>
4+
5+
namespace xamarin::android
6+
{
7+
class RuntimeJniNames
8+
{
9+
public:
10+
static inline constexpr std::string_view RuntimeClass { "mono/android/Runtime" };
11+
static inline constexpr std::string_view IGCUserPeerClass { "mono/android/IGCUserPeer" };
12+
static inline constexpr std::string_view GCUserPeerableClass { "net/dot/jni/GCUserPeerable" };
13+
14+
static inline constexpr std::string_view GCUserPeerRuntimeField { "mono_android_GCUserPeer" };
15+
static inline constexpr std::string_view IGCUserPeerRuntimeField { "mono_android_IGCUserPeer" };
16+
static inline constexpr std::string_view GCUserPeerableRuntimeField { "net_dot_jni_GCUserPeerable" };
17+
18+
static inline constexpr std::string_view IGCUserPeerAddReferenceMethod { "monodroidAddReference" };
19+
static inline constexpr std::string_view IGCUserPeerClearReferencesMethod { "monodroidClearReferences" };
20+
};
21+
}

0 commit comments

Comments
 (0)