diff --git a/legacy/src/main/java/de/robv/android/xposed/XposedHelpers.java b/legacy/src/main/java/de/robv/android/xposed/XposedHelpers.java index a3bf8dd26..969bd7e90 100644 --- a/legacy/src/main/java/de/robv/android/xposed/XposedHelpers.java +++ b/legacy/src/main/java/de/robv/android/xposed/XposedHelpers.java @@ -8,6 +8,7 @@ import org.apache.commons.lang3.ClassUtilsX; import org.apache.commons.lang3.reflect.MemberUtilsX; +import org.matrix.vector.nativebridge.HookBridge; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; @@ -1149,12 +1150,11 @@ public static short getShortField(Object obj, String fieldName) { * Sets the value of a static object field in the given class. See also {@link #findField}. */ public static void setStaticObjectField(Class clazz, String fieldName, Object value) { + Field field = findField(clazz, fieldName); try { - findField(clazz, fieldName).set(null, value); + field.set(null, value); } catch (IllegalAccessException e) { - // should not happen - XposedBridge.log(e); - throw new IllegalAccessError(e.getMessage()); + setStaticFinalField(field, value, e); } catch (IllegalArgumentException e) { throw e; } @@ -1164,12 +1164,11 @@ public static void setStaticObjectField(Class clazz, String fieldName, Object * Sets the value of a static {@code boolean} field in the given class. See also {@link #findField}. */ public static void setStaticBooleanField(Class clazz, String fieldName, boolean value) { + Field field = findField(clazz, fieldName); try { - findField(clazz, fieldName).setBoolean(null, value); + field.setBoolean(null, value); } catch (IllegalAccessException e) { - // should not happen - XposedBridge.log(e); - throw new IllegalAccessError(e.getMessage()); + setStaticFinalField(field, value, e); } catch (IllegalArgumentException e) { throw e; } @@ -1179,12 +1178,11 @@ public static void setStaticBooleanField(Class clazz, String fieldName, boole * Sets the value of a static {@code byte} field in the given class. See also {@link #findField}. */ public static void setStaticByteField(Class clazz, String fieldName, byte value) { + Field field = findField(clazz, fieldName); try { - findField(clazz, fieldName).setByte(null, value); + field.setByte(null, value); } catch (IllegalAccessException e) { - // should not happen - XposedBridge.log(e); - throw new IllegalAccessError(e.getMessage()); + setStaticFinalField(field, value, e); } catch (IllegalArgumentException e) { throw e; } @@ -1194,12 +1192,11 @@ public static void setStaticByteField(Class clazz, String fieldName, byte val * Sets the value of a static {@code char} field in the given class. See also {@link #findField}. */ public static void setStaticCharField(Class clazz, String fieldName, char value) { + Field field = findField(clazz, fieldName); try { - findField(clazz, fieldName).setChar(null, value); + field.setChar(null, value); } catch (IllegalAccessException e) { - // should not happen - XposedBridge.log(e); - throw new IllegalAccessError(e.getMessage()); + setStaticFinalField(field, value, e); } catch (IllegalArgumentException e) { throw e; } @@ -1209,12 +1206,11 @@ public static void setStaticCharField(Class clazz, String fieldName, char val * Sets the value of a static {@code double} field in the given class. See also {@link #findField}. */ public static void setStaticDoubleField(Class clazz, String fieldName, double value) { + Field field = findField(clazz, fieldName); try { - findField(clazz, fieldName).setDouble(null, value); + field.setDouble(null, value); } catch (IllegalAccessException e) { - // should not happen - XposedBridge.log(e); - throw new IllegalAccessError(e.getMessage()); + setStaticFinalField(field, value, e); } catch (IllegalArgumentException e) { throw e; } @@ -1224,12 +1220,11 @@ public static void setStaticDoubleField(Class clazz, String fieldName, double * Sets the value of a static {@code float} field in the given class. See also {@link #findField}. */ public static void setStaticFloatField(Class clazz, String fieldName, float value) { + Field field = findField(clazz, fieldName); try { - findField(clazz, fieldName).setFloat(null, value); + field.setFloat(null, value); } catch (IllegalAccessException e) { - // should not happen - XposedBridge.log(e); - throw new IllegalAccessError(e.getMessage()); + setStaticFinalField(field, value, e); } catch (IllegalArgumentException e) { throw e; } @@ -1239,12 +1234,11 @@ public static void setStaticFloatField(Class clazz, String fieldName, float v * Sets the value of a static {@code int} field in the given class. See also {@link #findField}. */ public static void setStaticIntField(Class clazz, String fieldName, int value) { + Field field = findField(clazz, fieldName); try { - findField(clazz, fieldName).setInt(null, value); + field.setInt(null, value); } catch (IllegalAccessException e) { - // should not happen - XposedBridge.log(e); - throw new IllegalAccessError(e.getMessage()); + setStaticFinalField(field, value, e); } catch (IllegalArgumentException e) { throw e; } @@ -1254,12 +1248,11 @@ public static void setStaticIntField(Class clazz, String fieldName, int value * Sets the value of a static {@code long} field in the given class. See also {@link #findField}. */ public static void setStaticLongField(Class clazz, String fieldName, long value) { + Field field = findField(clazz, fieldName); try { - findField(clazz, fieldName).setLong(null, value); + field.setLong(null, value); } catch (IllegalAccessException e) { - // should not happen - XposedBridge.log(e); - throw new IllegalAccessError(e.getMessage()); + setStaticFinalField(field, value, e); } catch (IllegalArgumentException e) { throw e; } @@ -1269,12 +1262,11 @@ public static void setStaticLongField(Class clazz, String fieldName, long val * Sets the value of a static {@code short} field in the given class. See also {@link #findField}. */ public static void setStaticShortField(Class clazz, String fieldName, short value) { + Field field = findField(clazz, fieldName); try { - findField(clazz, fieldName).setShort(null, value); + field.setShort(null, value); } catch (IllegalAccessException e) { - // should not happen - XposedBridge.log(e); - throw new IllegalAccessError(e.getMessage()); + setStaticFinalField(field, value, e); } catch (IllegalArgumentException e) { throw e; } @@ -1282,6 +1274,37 @@ public static void setStaticShortField(Class clazz, String fieldName, short v //################################################################################################# + /** + * Writes a static field reflection has just refused to write. + * + * Android 17 rejects every reflective write to a static final field, whatever the Field's + * accessible flag says, so on that release the nine setters above would do nothing but throw + * for a module doing what modules have always done -- spoofing android.os.Build being the + * common one. The framework drops the field's final flag and lets reflection write it after + * all, which is the whole of the difference: the value, the conversions and the type checking + * are still reflection's. + * + * Only reached once reflection has thrown, so nothing changes on the releases that allow the + * write, and a genuine access failure -- or a runtime this cannot read -- still ends in the + * IllegalAccessError it always did. + */ + private static void setStaticFinalField(Field field, Object value, IllegalAccessException cause) { + if (HookBridge.makeFieldWritable(field, field.getModifiers())) { + try { + // Boxed, whatever the field's type: Field.set unboxes for a primitive field and + // widens like the typed setter that has just failed would have. + field.set(null, value); + return; + } catch (IllegalAccessException retried) { + cause = retried; + } + } + XposedBridge.log(cause); + throw new IllegalAccessError(cause.getMessage()); + } + + //################################################################################################# + /** * Returns the value of a static object field in the given class. See also {@link #findField}. */ diff --git a/native/src/jni/hook_bridge.cpp b/native/src/jni/hook_bridge.cpp index 2cbae6777..29ff2ec64 100644 --- a/native/src/jni/hook_bridge.cpp +++ b/native/src/jni/hook_bridge.cpp @@ -89,6 +89,7 @@ SharedHashMap> hooked_methods; // Cached JNI method and field IDs for performance. jmethodID invoke = nullptr; + } // namespace namespace vector::native::jni { @@ -504,6 +505,41 @@ VECTOR_DEF_NATIVE_METHOD(jboolean, HookBridge, setTrusted, jobject cookie) { return lsplant::MakeDexFileTrusted(env, cookie); } +/** + * @brief Clears ACC_FINAL on a field, so that reflection will write it again. + * + * Android 17 refuses every reflective write to a static final field + * (`ThrowIAEIfFieldIsNotOverwritable` in `runtime/native/java_lang_reflect_Field.cc`), whatever + * the Field's accessible flag says, and clearing the reflective copy's ACC_FINAL does not help + * because the check reads the ArtField. This clears it where the check looks. + * + * The runtime's own JNI SetStatic*Field is the other way in, and is not taken here: it is + * `LOG(FATAL)` for anything ART considers unmodifiable, and the carve-out that would spare + * android.os.Build carries a TODO to remove it. A field that is no longer final is unmodifiable + * to nobody, so this stays a write and never becomes an abort. + * + * [modifiers] is what java.lang.reflect.Field reports, and the ArtField's access flags have to + * agree with it before anything is written: that is what says this pointer is an ArtField laid + * out the way this expects, rather than a JNI index id or a layout that has moved. + * + * @return JNI_TRUE when the field is no longer final. + */ +VECTOR_DEF_NATIVE_METHOD(jboolean, HookBridge, makeFieldWritable, jobject field, jint modifiers) { + // jfieldID is the ArtField itself, and `access_flags_` follows the four-byte compressed + // `declaring_class_` root that starts it. + auto *art_field = reinterpret_cast(env->FromReflectedField(field)); + if (art_field == nullptr) return JNI_FALSE; + + constexpr uint32_t kAccJavaFlagsMask = 0xFFFFu; + constexpr uint32_t kAccFinal = 0x0010u; + + uint32_t flags = art_field[1]; + if ((flags & kAccJavaFlagsMask) != static_cast(modifiers)) return JNI_FALSE; + + art_field[1] = flags & ~kAccFinal; + return JNI_TRUE; +} + /** * @brief Creates a snapshot of all registered callbacks for a given method. * This is useful for debugging and introspection from the Java side. @@ -686,6 +722,7 @@ static JNINativeMethod gMethods[] = { VECTOR_NATIVE_METHOD(HookBridge, allocateObject, "(Ljava/lang/Class;)Ljava/lang/Object;"), VECTOR_NATIVE_METHOD(HookBridge, instanceOf, "(Ljava/lang/Object;Ljava/lang/Class;)Z"), VECTOR_NATIVE_METHOD(HookBridge, setTrusted, "(Ljava/lang/Object;)Z"), + VECTOR_NATIVE_METHOD(HookBridge, makeFieldWritable, "(Ljava/lang/reflect/Field;I)Z"), VECTOR_NATIVE_METHOD(HookBridge, callbackSnapshot, "(Ljava/lang/Class;Ljava/lang/reflect/" "Executable;)[[Ljava/lang/Object;"), @@ -702,6 +739,7 @@ void RegisterHookBridge(JNIEnv *env) { invoke = env->GetMethodID(method, "invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"); env->DeleteLocalRef(method); + REGISTER_VECTOR_NATIVE_METHODS(HookBridge); } } // namespace vector::native::jni diff --git a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt index d469eb4a5..9478806fb 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt @@ -2,6 +2,7 @@ package org.matrix.vector.nativebridge import dalvik.annotation.optimization.FastNative import java.lang.reflect.Executable +import java.lang.reflect.Field import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method @@ -54,6 +55,22 @@ object HookBridge { @JvmStatic @FastNative external fun setTrusted(cookie: Any?): Boolean + /** + * Clears the final flag ART reads, so that reflection will write [field] again. + * + * Android 17 refuses every reflective write to a static final field however accessible the + * [Field] is, and clearing the reflective copy's flag does not help because the check reads + * the ArtField. The write itself stays with reflection, which keeps its conversions and its + * exceptions; this only stops it being refused. + * + * [modifiers] must be `field.modifiers`. It is checked against the flags about to be written, + * so a runtime that lays an ArtField out differently is left alone rather than corrupted. + * + * Returns false when the field is still final, which is the caller's cue to report the + * failure it already had. The field stays writable afterwards. + */ + @JvmStatic external fun makeFieldWritable(field: Field, modifiers: Int): Boolean + /** Returns null when [method] carries no hooks at all. */ @JvmStatic external fun callbackSnapshot(