Enable opt-in R8 JNI name obfuscation for CoreCLR and NativeAOT - #12575
Draft
simonrozsival wants to merge 14 commits into
Draft
Enable opt-in R8 JNI name obfuscation for CoreCLR and NativeAOT#12575simonrozsival wants to merge 14 commits into
simonrozsival wants to merge 14 commits into
Conversation
Rebuild managed PE metadata and IL with obfuscated JNI class, method, field, descriptor, RegisterNatives, and FieldRVA string data. Preserve compression descriptor ordering when rewritten assembly sizes change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Run a naming-only R8 pass before managed optimization, stage rewritten inputs for ILLink and ILC, and reuse the mapping during final Java shrinking. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use descriptor-aware reverse member mappings and regenerate the post-link ACW reachability map while retaining the original Java input set. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Record the exact seed mapping entries consumed while rewriting managed assemblies and verify that final R8 preserves every surviving name. Removed entries remain valid linker or R8 trimming outcomes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Generate post-link manifests from the CoreCLR and NativeAOT keep-rule inputs, then fail final R8 when a required Java class or member disappears. Cover registered fields and overload-aware methods in the CoreCLR rules. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Reuse the full JNI rewrite planner in reverse over linked CoreCLR assemblies so final R8 keep rules cover attributes, direct lookups, descriptors, interfaces, typemap metadata, and FieldRVA data. Preserve every ambiguous reverse member candidate and index exact lookups. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Normalize expected multiline output to the host line ending so the new test passes on Windows as well as Unix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add a supported opt-in, collision-safe staging, multi-ABI and project-reference coverage, identity-preserving strong-name handling, and deduplicated ACW keep rules. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Member
Author
|
/review |
Contributor
|
❌ Android PR Reviewer failed. Please review the logs for details.
|
Restrict reverse mapping during post-link scanning to exact JNI entries recorded by the managed rewrite pass. This prevents ordinary short string literals from being mistaken for R8-obfuscated synthetic classes while preserving strict final reachability validation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This was referenced Sep 1, 2026
simonrozsival
added a commit
that referenced
this pull request
Sep 2, 2026
## Summary Related to #12535 This is layer 1 of 6 in the replacement stack for PR #12575. It extracts the self-contained R8/JNI name-mapping primitives and their focused unit tests so later layers can add build integration and runtime-specific behavior independently. This layer intentionally changes no build behavior. Nothing invokes these new helpers yet. ## What this adds - `R8Mapping` - Parses R8/ProGuard mapping files, including class, field, method, constructor, overload, line-range, inline-frame, and class-merging entries. - Reports the actual source mapping path and line number when file parsing fails. - Buffers positional records by residual range and obfuscated name as R8 retrace stacks, indexing only the final residual frame as callable so same-class inline-only methods are not rewritten. - Preserves every original class candidate when R8 merges several classes into one residual class. - Uses rewrite-manifest class entries to disambiguate reverse class and member mappings, failing closed unless exactly one original class remains allowed. - Fails closed when reverse field or name-only method lookup remains ambiguous, while allowing a manifest filter to select one candidate safely. - Records the owning class together with every successful forward member lookup, allowing the resulting manifest to restrict its own reverse mapping safely. - Tracks accessed entries atomically across concurrent lookups and exposes stable snapshots to manifest consumers. - Reports a removed declaring class even when reachability input starts with only a required field or method entry, without emitting duplicate class conflicts. - Provides compatibility and reachability conflict detection used by later stack layers. - Produces and consumes compact, sorted manifest entries with deterministic `\n` newlines. - `JniDescriptorText` - Validates JNI field and method descriptors, including non-empty slash-separated object type names. - Rejects malformed object descriptors containing Java source-name dots, array markers inside class names, or empty path segments. - Rewrites embedded object type names while preserving primitive and unrelated types. - Converts validated JNI descriptor tokens into Java source type names without redundantly rescanning tokens parsed from a method descriptor. - Preserves nested binary-name `$` separators when converting descriptors to R8 mapping keys. - `LdstrRewriter` - Rewrites JNI-sensitive string constants using an `IJniNameMapping` implementation. - Handles encoded method and field IDs, constructor descriptors, `RegisterNatives` entries, multiline registration blocks, and exact JNI class-name strings. - Leaves unrelated or unresolved strings unchanged. ## Test coverage Adds focused tests for: - R8 mapping syntax, source-aware parse errors, positional inline stacks, qualified and same-class inline frames, residual method preservation, merged-class reverse lookup, manifest disambiguation, zero/multiple-candidate failure, concurrent access tracking and snapshot semantics, member-only owning-class tracking, forward and reverse lookups, overloads, constructors, nested classes, deterministic manifest output, and conflict detection. - JNI method/field descriptor validation, malformed object descriptors, object and array rewriting, Java type conversion, nested `$` descriptor-to-mapping-key conversion, and malformed standalone token handling. - JNI-related `ldstr` forms, including encoded member IDs, constructors, registration blocks, class names, unchanged inputs, and a forward/reverse field round trip restricted by the forward access manifest. Validation commands: ```sh ./dotnet-local.sh test src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj -v minimal --filter 'FullyQualifiedName~R8MappingTests|FullyQualifiedName~JniDescriptorTextTests|FullyQualifiedName~LdstrRewriterTests' ./dotnet-local.sh build src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj --no-restore -v minimal ``` Result: **82 passed, 0 failed, 0 skipped**; test project build succeeded with **0 warnings and 0 errors**. ## Intentionally out of scope The following remain for later layers in the replacement stack: - Build integration and MSBuild tasks/targets. - Metadata assembly rebuilding. - Typemap `FieldRVA` handling. - CoreCLR integration. - NativeAOT integration. - User-facing documentation. The initial implementation and tests in this layer were reconstructed from the verified PR #12575 source snapshot at `ac2b30593ff5afa0659fe35d3de515239c7540fe`, followed by descriptor-validation, ambiguity-safety, diagnostics, concurrency, merged-class, inline-frame, and owning-class access-tracking hardening from review. PR #12575 and its branch remain unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Advances #12535
Related to dotnet/maui#23027
Related to dotnet/docs-maui#2395
.NET for Android currently disables R8 name obfuscation because managed bindings embed Java/JNI class, method, field, and descriptor names. If R8 renames the Java side while managed metadata and IL retain the original names, JNI lookup and native registration fail.
This change implements build-time rewriting without a runtime lookup table. R8 remains the naming authority: a naming-only pass establishes names, managed assemblies are rewritten before ILLink or NativeAOT ILC, and final R8 performs normal shrinking and optimization with
-applymapping.The feature is opt-in through
AndroidEnableR8JniNameObfuscation. It requiresAndroidLinkTool=r8,AndroidTypeMapImplementation=trimmable, and either CoreCLR or NativeAOT.Pipeline
-applymapping.NativeAOT's prebuilt host performs fixed native lookups against
mono.android.Runtime, so that small bridge remains deliberately preserved.Production hardening
StrongNameSignedflag is cleared. These private rewritten inputs are intentionally delay-signed because the original private keys are unavailable.Validation coverage
Automated Release integration tests cover CoreCLR and NativeAOT and verify:
The focused hardening matrix passes all 7 CoreCLR, NativeAOT, multi-ABI, project-reference/custom-rule, incrementality, and strong-name tests. Existing JNI mapping, rewriting, scanning, mapping-consistency, and reachability coverage remains in place.
Measurements
Across 86 generated typemap assemblies, rewriting 13,031 real typemap entries reduced raw PE size by 1,016,832 bytes (9.42%) and gzip-equivalent size by 107,446 bytes (6.61%). NativeAOT's packaged saving came primarily from
classes.dex, because those managed assemblies are compiled into the application native library.Naming-only R8 remains the dominant added clean-build cost. Replacing it with a managed allocator would reproduce R8 inheritance, interface dispatch, override, keep-rule, and minifier semantics, so this design intentionally keeps R8 as the naming authority.
Remaining follow-up
Additional ACW class-name obfuscation is intentionally deferred. Allowing those classes to be renamed caused CoreCLR final R8 to remove reachable generated native declarations such as
nctor_0; the reachability validator correctly rejected the output. Supporting this safely requires a separate native registration/retention design rather than weaker validation or another superficial ProGuard modifier.