Write static final fields again on Android 17 - #821
Merged
Conversation
Android 17 refuses every reflective write to a static final field: `Field_set` now calls `ThrowIAEIfFieldIsNotOverwritable` before it looks at the accessible flag, and that throws unless the process targets SDK 36 or lower. Clearing the reflective copy's ACC_FINAL does not help, since the check reads the ArtField; a VarHandle unreflected from the same field is read-only, and Android's Unsafe has no static field accessors at all. So `XposedHelpers.setStatic*Field` is dead for every legacy module on that release -- spoofing android.os.Build, which is what most of them use it for, included. HookBridge.makeFieldWritable clears ACC_FINAL where the check reads it, and the setters retry the write through reflection, which keeps the conversions, the type checking and the exceptions where they were. The runtime's own JNI SetStatic*Field is the other way in and is not taken: it is LOG(FATAL) for any field ART holds unmodifiable, and the carve-out sparing android.os.Build is a TODO to be removed. The ArtField's access flags are checked against Field.getModifiers() before anything is written, so a runtime that lays them out differently, or hands out JNI index ids rather than pointers, is left alone and the caller keeps the IllegalAccessError it already had. Nothing changes below 17, where the first reflective write succeeds and none of this is reached.
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 freeto 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.
Android 17 refuses every reflective write to a static final field.
Field_setcallsThrowIAEIfFieldIsNotOverwritablebefore it looks at the accessible flag, andArtField::IsUnmodifiablelets astatic finalthrough only for a process targeting SDK 36 or lower. Clearing the reflective copy's ACC_FINAL does not help — the check reads the ArtField — a VarHandle unreflected from the same field is read-only, and Android'sUnsafehas no static field accessors. Measured on a Pixel 6 on 17:setAccessible(true)is accepted,isAccessible()is true, and the write throws anyway, forandroid.os.Build.FINGERPRINTand for a static final in a test class alike. Instance finals are unaffected.That leaves
XposedHelpers.setStatic*Fielddead for every legacy module on 17, spoofingandroid.os.Buildincluded, which is what most of them use it for (#818).HookBridge.makeFieldWritableclears ACC_FINAL where the check reads it and the setters retry through reflection, so the value, the conversions and the exceptions stay reflection's. ART's own JNISetStatic*Fieldis the other way in and is deliberately not taken:EnsureModifiableisLOG(FATAL)for a field it holds unmodifiable, and the carve-out that sparesandroid.os.Buildcarries a TODO to remove it — a JNI write to any other class's static final aborts the process, which I confirmed on device.The ArtField's access flags are checked against
Field.getModifiers()before anything is written, so a runtime that lays them out differently, or hands out JNI index ids rather than pointers, is left alone and the caller keeps theIllegalAccessErrorit already had. Below 17 the first reflective write succeeds and none of this is reached.