Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 59 additions & 36 deletions legacy/src/main/java/de/robv/android/xposed/XposedHelpers.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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;
}
Expand All@@ -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;
}
Expand All@@ -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;
}
Expand All@@ -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;
}
Expand All@@ -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;
}
Expand All@@ -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;
}
Expand All@@ -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;
}
Expand All@@ -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;
}
Expand All@@ -1269,19 +1262,49 @@ 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;
}
}

//#################################################################################################

/**
* 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}.
*/
Expand Down
38 changes: 38 additions & 0 deletions native/src/jni/hook_bridge.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,6 +89,7 @@ SharedHashMap<jmethodID, std::unique_ptr<HookItem>> hooked_methods;

// Cached JNI method and field IDs for performance.
jmethodID invoke = nullptr;

} // namespace

namespace vector::native::jni {
Expand DownExpand Up@@ -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<uint32_t *>(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<uint32_t>(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.
Expand DownExpand Up@@ -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;"),
Expand All@@ -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
Original file line numberDiff line numberDiff line change
Expand Up@@ -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

Expand DownExpand Up@@ -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(
Expand Down
Loading