Skip to content

Write static final fields again on Android 17 - #821

Merged
JingMatrix merged 1 commit into
masterfrom
static-final-fields
Jul 30, 2026
Merged

Write static final fields again on Android 17#821
JingMatrix merged 1 commit into
masterfrom
static-final-fields

Conversation

@JingMatrix

Copy link
Copy Markdown
Owner

Android 17 refuses every reflective write to a static final field. Field_set calls ThrowIAEIfFieldIsNotOverwritable before it looks at the accessible flag, and ArtField::IsUnmodifiable lets a static final through 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's Unsafe has no static field accessors. Measured on a Pixel 6 on 17: setAccessible(true) is accepted, isAccessible() is true, and the write throws anyway, for android.os.Build.FINGERPRINT and for a static final in a test class alike. Instance finals are unaffected.

That leaves XposedHelpers.setStatic*Field dead for every legacy module on 17, spoofing android.os.Build included, which is what most of them use it for (#818).

HookBridge.makeFieldWritable clears 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 JNI SetStatic*Field is the other way in and is deliberately not taken: EnsureModifiable is LOG(FATAL) for a field it holds unmodifiable, and the carve-out that spares android.os.Build carries 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 the IllegalAccessError it already had. Below 17 the first reflective write succeeds and none of this is reached.

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.
@JingMatrixJingMatrix linked an issue Jul 30, 2026 that may be closed by this pull request
1 task
@JingMatrix
JingMatrix merged commit 4455239 into masterJul 30, 2026
1 check passed
@JingMatrix
JingMatrix deleted the static-final-fields branch August 1, 2026 06:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

(legacy) modules don't recieve root in Android 17

1 participant

@JingMatrix