diff --git a/core/proguard-rules.pro b/core/proguard-rules.pro index 940ee160..f23c8f7c 100644 --- a/core/proguard-rules.pro +++ b/core/proguard-rules.pro @@ -42,9 +42,6 @@ getFrameworkVersion(...); getFrameworkVersionCode(...); getFrameworkPrivilege(...); - featuredMethod(...); - hookBefore(...); - hookAfter(...); hook(...); deoptimize(...); invokeOrigin(...); diff --git a/core/src/main/java/de/robv/android/xposed/XC_MethodHook.java b/core/src/main/java/de/robv/android/xposed/XC_MethodHook.java index 3b61f6e1..798f47c9 100644 --- a/core/src/main/java/de/robv/android/xposed/XC_MethodHook.java +++ b/core/src/main/java/de/robv/android/xposed/XC_MethodHook.java @@ -20,20 +20,12 @@ package de.robv.android.xposed; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.lsposed.lspd.nativebridge.HookBridge; - import java.lang.reflect.Executable; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; -import java.util.ConcurrentModificationException; import java.util.HashMap; import de.robv.android.xposed.callbacks.IXUnhook; import de.robv.android.xposed.callbacks.XCallback; -import io.github.libxposed.api.XposedInterface; /** * Callback class for method hooks. @@ -103,7 +95,7 @@ public abstract class XC_MethodHook extends XCallback { /** * Wraps information about the method call and allows to influence it. */ - public static final class MethodHookParam extends XCallback.Param implements XposedInterface.BeforeHookCallback, XposedInterface.AfterHookCallback { + public static final class MethodHookParam extends XCallback.Param { /** * @hide */ @@ -158,11 +150,6 @@ public abstract class XC_MethodHook extends XCallback { return throwable; } - @Override - public boolean isSkipped() { - return returnEarly; - } - /** * Returns true if an exception was thrown by the method. */ @@ -189,62 +176,6 @@ public abstract class XC_MethodHook extends XCallback { throw throwable; return result; } - - @NonNull - @Override - public T getOrigin() { - return (T) method; - } - - @Nullable - @Override - public Object getThis() { - return thisObject; - } - - @NonNull - @Override - public Object[] getArgs() { - return args; - } - - @Nullable - @Override - public U getArg(int index) { - return (U) args[index]; - } - - @Override - public void setArg(int index, U value) { - args[index] = value; - } - - @Override - public void returnAndSkip(@Nullable Object returnValue) { - setResult(returnValue); - } - - @Override - public void throwAndSkip(@Nullable Throwable throwable) { - setThrowable(throwable); - } - - @Nullable - @Override - public Object invokeOrigin() throws InvocationTargetException, IllegalAccessException { - return HookBridge.invokeOriginalMethod((Executable) method, thisObject, args); - } - - @Nullable - @Override - public U getExtra(@NonNull String key) { - return (U) extras.get(key); - } - - @Override - public void setExtra(@NonNull String key, @Nullable U value) throws ConcurrentModificationException { - extras.put(key, value); - } } /** diff --git a/core/src/main/java/de/robv/android/xposed/XposedBridge.java b/core/src/main/java/de/robv/android/xposed/XposedBridge.java index 2be1e321..1c4cd75f 100644 --- a/core/src/main/java/de/robv/android/xposed/XposedBridge.java +++ b/core/src/main/java/de/robv/android/xposed/XposedBridge.java @@ -35,8 +35,10 @@ import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; +import java.util.ArrayDeque; import java.util.Arrays; import java.util.HashSet; +import java.util.Queue; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; @@ -219,7 +221,7 @@ public final class XposedBridge { throw new IllegalArgumentException("callback should not be null!"); } - if (!HookBridge.hookMethod((Executable) hookMethod, AdditionalHookInfo.class, callback.priority, callback)) { + if (!HookBridge.hookMethod(false, (Executable) hookMethod, AdditionalHookInfo.class, callback.priority, callback)) { log("Failed to hook " + hookMethod); return null; } @@ -238,7 +240,7 @@ public final class XposedBridge { @Deprecated public static void unhookMethod(Member hookMethod, XC_MethodHook callback) { if (hookMethod instanceof Executable) { - HookBridge.unhookMethod((Executable) hookMethod, callback); + HookBridge.unhookMethod(false, (Executable) hookMethod, callback); } } @@ -438,7 +440,7 @@ public final class XposedBridge { } } - Object[] callbacksSnapshot = HookBridge.callbackSnapshot(method); + Object[] callbacksSnapshot = HookBridge.callbackSnapshot(HookerCallback.class, method); if (callbacksSnapshot == null || callbacksSnapshot.length == 0) { try { return HookBridge.invokeOriginalMethod(method, param.thisObject, param.args); @@ -446,6 +448,7 @@ public final class XposedBridge { throw (Throwable) HookBridge.invokeOriginalMethod(getCause, ite); } } + Queue extras = new ArrayDeque<>(callbacksSnapshot.length); // call "before method" callbacks int beforeIdx = 0; @@ -454,8 +457,9 @@ public final class XposedBridge { var cb = callbacksSnapshot[beforeIdx]; if (HookBridge.instanceOf(cb, XC_MethodHook.class)) { ((XC_MethodHook) cb).beforeHookedMethod(param); - } else if (HookBridge.instanceOf(cb, XposedInterface.BeforeHooker.class)) { - ((XposedInterface.BeforeHooker) cb).before(param); + } else if (HookBridge.instanceOf(cb, HookerCallback.class)) { + var hooker = (HookerCallback) cb; + extras.add(hooker.beforeInvocation.invoke(null, method, param.thisObject, param.args)); } } catch (Throwable t) { XposedBridge.log(t); @@ -492,8 +496,9 @@ public final class XposedBridge { try { if (HookBridge.instanceOf(cb, XC_MethodHook.class)) { ((XC_MethodHook) cb).afterHookedMethod(param); - } else if (HookBridge.instanceOf(cb, XposedInterface.AfterHooker.class)) { - ((XposedInterface.AfterHooker) cb).after(param); + } else if (HookBridge.instanceOf(cb, HookerCallback.class)) { + var hooker = (HookerCallback) cb; + hooker.afterInvocation.invoke(null, extras.poll(), lastResult); } } catch (Throwable t) { XposedBridge.log(t); @@ -518,4 +523,14 @@ public final class XposedBridge { } } } + + public static class HookerCallback { + Method beforeInvocation; + Method afterInvocation; + + public HookerCallback(Method beforeInvocation, Method afterInvocation) { + this.beforeInvocation = beforeInvocation; + this.afterInvocation = afterInvocation; + } + } } diff --git a/core/src/main/java/org/lsposed/lspd/impl/LSPosedContext.java b/core/src/main/java/org/lsposed/lspd/impl/LSPosedContext.java index 75d39213..bd2ad4de 100644 --- a/core/src/main/java/org/lsposed/lspd/impl/LSPosedContext.java +++ b/core/src/main/java/org/lsposed/lspd/impl/LSPosedContext.java @@ -55,10 +55,12 @@ import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.nio.ByteBuffer; +import java.util.Arrays; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -66,8 +68,12 @@ import java.util.concurrent.ConcurrentHashMap; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedHelpers; import io.github.libxposed.api.XposedContext; +import io.github.libxposed.api.XposedInterface; import io.github.libxposed.api.XposedModule; import io.github.libxposed.api.XposedModuleInterface; +import io.github.libxposed.api.annotations.AfterInvocation; +import io.github.libxposed.api.annotations.BeforeInvocation; +import io.github.libxposed.api.annotations.XposedHooker; import io.github.libxposed.api.errors.HookFailedError; import io.github.libxposed.api.utils.DexParser; @@ -862,41 +868,68 @@ public class LSPosedContext extends XposedContext { } } - @Override - public Object featuredMethod(String name, Object... args) { - throw new UnsupportedOperationException(); - } - - private MethodUnhooker doHook(U hookMethod, int priority, T callback) { + private MethodUnhooker doHook(T hookMethod, int priority, Class hooker) { if (Modifier.isAbstract(hookMethod.getModifiers())) { throw new IllegalArgumentException("Cannot hook abstract methods: " + hookMethod); } else if (hookMethod.getDeclaringClass().getClassLoader() == LSPosedContext.class.getClassLoader()) { throw new IllegalArgumentException("Do not allow hooking inner methods"); } else if (hookMethod.getDeclaringClass() == Method.class && hookMethod.getName().equals("invoke")) { throw new IllegalArgumentException("Cannot hook Method.invoke"); - } - - if (callback == null) { + } else if (hooker == null) { throw new IllegalArgumentException("hooker should not be null!"); + } else if (hooker.getAnnotation(XposedHooker.class) == null) { + throw new IllegalArgumentException("Hooker should be annotated with @XposedHooker"); } - if (HookBridge.hookMethod(hookMethod, XposedBridge.AdditionalHookInfo.class, priority, callback)) { + Method beforeInvocation = null, afterInvocation = null; + var modifiers = Modifier.PUBLIC | Modifier.STATIC; + for (var method : hooker.getDeclaredMethods()) { + if (method.getAnnotation(BeforeInvocation.class) != null) { + if (beforeInvocation != null) { + throw new IllegalArgumentException("More than one method annotated with @BeforeInvocation"); + } + boolean valid; + valid = (method.getModifiers() & modifiers) == modifiers; + valid &= Arrays.equals(method.getParameterTypes(), new Class[]{Member.class, Object.class, Object[].class}); + valid &= method.getReturnType().equals(Hooker.class); + if (!valid) { + throw new IllegalArgumentException("BeforeInvocation method format is invalid"); + } + beforeInvocation = method; + } + if (method.getAnnotation(AfterInvocation.class) != null) { + if (afterInvocation != null) { + throw new IllegalArgumentException("More than one method annotated with @AfterInvocation"); + } + boolean valid; + valid = (method.getModifiers() & modifiers) == modifiers; + valid &= Arrays.equals(method.getParameterTypes(), new Class[]{Hooker.class, Object.class}); + valid &= method.getReturnType().equals(void.class); + if (!valid) { + throw new IllegalArgumentException("AfterInvocation method format is invalid"); + } + afterInvocation = method; + } + } + if (beforeInvocation == null) { + throw new IllegalArgumentException("No method annotated with @BeforeInvocation"); + } + if (afterInvocation == null) { + throw new IllegalArgumentException("No method annotated with @AfterInvocation"); + } + + var callback = new XposedBridge.HookerCallback(beforeInvocation, afterInvocation); + if (HookBridge.hookMethod(true, hookMethod, XposedBridge.AdditionalHookInfo.class, priority, callback)) { return new MethodUnhooker<>() { @NonNull @Override - public U getOrigin() { + public T getOrigin() { return hookMethod; } - @NonNull - @Override - public T getHooker() { - return callback; - } - @Override public void unhook() { - HookBridge.unhookMethod(hookMethod, callback); + HookBridge.unhookMethod(true, hookMethod, callback); } }; } @@ -905,73 +938,25 @@ public class LSPosedContext extends XposedContext { @Override @NonNull - public MethodUnhooker, Method> hookBefore(@NonNull Method origin, @NonNull BeforeHooker hooker) { + public MethodUnhooker hook(@NonNull Method origin, @NonNull Class hooker) { return doHook(origin, PRIORITY_DEFAULT, hooker); } @Override @NonNull - public MethodUnhooker, Method> hookAfter(@NonNull Method origin, @NonNull AfterHooker hooker) { - return doHook(origin, PRIORITY_DEFAULT, hooker); - } - - @Override - @NonNull - public MethodUnhooker, Method> hook(@NonNull Method origin, @NonNull Hooker hooker) { - return doHook(origin, PRIORITY_DEFAULT, hooker); - } - - @Override - @NonNull - public MethodUnhooker, Method> hookBefore(@NonNull Method origin, int priority, @NonNull BeforeHooker hooker) { + public MethodUnhooker hook(@NonNull Method origin, int priority, @NonNull Class hooker) { return doHook(origin, priority, hooker); } @Override @NonNull - public MethodUnhooker, Method> hookAfter(@NonNull Method origin, int priority, @NonNull AfterHooker hooker) { - return doHook(origin, priority, hooker); - } - - @Override - @NonNull - public MethodUnhooker, Method> hook(@NonNull Method origin, int priority, @NonNull Hooker hooker) { - return doHook(origin, priority, hooker); - } - - @Override - @NonNull - public MethodUnhooker>, Constructor> hookBefore(@NonNull Constructor origin, @NonNull BeforeHooker> hooker) { + public MethodUnhooker> hook(@NonNull Constructor origin, @NonNull Class hooker) { return doHook(origin, PRIORITY_DEFAULT, hooker); } @Override @NonNull - public MethodUnhooker>, Constructor> hookAfter(@NonNull Constructor origin, @NonNull AfterHooker> hooker) { - return doHook(origin, PRIORITY_DEFAULT, hooker); - } - - @Override - @NonNull - public MethodUnhooker>, Constructor> hook(@NonNull Constructor origin, @NonNull Hooker> hooker) { - return doHook(origin, PRIORITY_DEFAULT, hooker); - } - - @Override - @NonNull - public MethodUnhooker>, Constructor> hookBefore(@NonNull Constructor origin, int priority, @NonNull BeforeHooker> hooker) { - return doHook(origin, priority, hooker); - } - - @Override - @NonNull - public MethodUnhooker>, Constructor> hookAfter(@NonNull Constructor origin, int priority, @NonNull AfterHooker> hooker) { - return doHook(origin, priority, hooker); - } - - @Override - @NonNull - public MethodUnhooker>, Constructor> hook(@NonNull Constructor origin, int priority, @NonNull Hooker> hooker) { + public MethodUnhooker> hook(@NonNull Constructor origin, int priority, @NonNull Class hooker) { return doHook(origin, priority, hooker); } diff --git a/core/src/main/java/org/lsposed/lspd/nativebridge/HookBridge.java b/core/src/main/java/org/lsposed/lspd/nativebridge/HookBridge.java index 4c4c02d7..db733ea2 100644 --- a/core/src/main/java/org/lsposed/lspd/nativebridge/HookBridge.java +++ b/core/src/main/java/org/lsposed/lspd/nativebridge/HookBridge.java @@ -6,9 +6,9 @@ import java.lang.reflect.InvocationTargetException; import dalvik.annotation.optimization.FastNative; public class HookBridge { - public static native boolean hookMethod(Executable hookMethod, Class hooker, int priority, Object callback); + public static native boolean hookMethod(boolean useModernApi, Executable hookMethod, Class hooker, int priority, Object callback); - public static native boolean unhookMethod(Executable hookMethod, Object callback); + public static native boolean unhookMethod(boolean useModernApi, Executable hookMethod, Object callback); public static native boolean deoptimizeMethod(Executable method); @@ -24,5 +24,5 @@ public class HookBridge { @FastNative public static native boolean setTrusted(Object cookie); - public static native Object[] callbackSnapshot(Executable method); + public static native Object[] callbackSnapshot(Class hooker_callback, Executable method); } diff --git a/core/src/main/jni/src/jni/hook_bridge.cpp b/core/src/main/jni/src/jni/hook_bridge.cpp index e64a3850..e2f5663c 100644 --- a/core/src/main/jni/src/jni/hook_bridge.cpp +++ b/core/src/main/jni/src/jni/hook_bridge.cpp @@ -29,9 +29,19 @@ using namespace lsplant; namespace { +struct CallbackType { + bool use_modern_api; + union { + jobject callback_object; + struct { + jmethodID before_method; + jmethodID after_method; + }; + }; +}; struct HookItem { - std::multimap> callbacks {}; + std::multimap> callbacks; private: std::atomic backup {nullptr}; static_assert(decltype(backup)::is_always_lock_free); @@ -57,10 +67,13 @@ std::shared_mutex hooked_lock; absl::flat_hash_map> hooked_methods; jmethodID invoke = nullptr; +jmethodID callback_ctor = nullptr; +jfieldID before_method_field = nullptr; +jfieldID after_method_field = nullptr; } namespace lspd { -LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, hookMethod, jobject hookMethod, +LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, hookMethod, jboolean useModernApi, jobject hookMethod, jclass hooker, jint priority, jobject callback) { bool newHook = false; #ifndef NDEBUG @@ -108,11 +121,32 @@ LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, hookMethod, jobject hookMethod, jobject backup = hook_item->GetBackup(); if (!backup) return JNI_FALSE; JNIMonitor monitor(env, backup); - hook_item->callbacks.emplace(std::make_pair(priority, env->NewGlobalRef(callback))); + if (useModernApi) { + if (before_method_field == nullptr) { + auto callback_class = JNI_GetObjectClass(env, callback); + callback_ctor = JNI_GetMethodID(env, callback_class, "", "(Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V"); + before_method_field = JNI_GetFieldID(env, callback_class, "beforeInvocation", "Ljava/lang/reflect/Method;"); + after_method_field = JNI_GetFieldID(env, callback_class, "afterInvocation", "Ljava/lang/reflect/Method;"); + } + auto before_method = JNI_GetObjectField(env, callback, before_method_field); + auto after_method = JNI_GetObjectField(env, callback, after_method_field); + auto callback_type = CallbackType { + .use_modern_api = true, + .before_method = env->FromReflectedMethod(before_method), + .after_method = env->FromReflectedMethod(after_method), + }; + hook_item->callbacks.emplace(std::make_pair(priority, callback_type)); + } else { + auto callback_type = CallbackType { + .use_modern_api = false, + .callback_object = env->NewGlobalRef(callback), + }; + hook_item->callbacks.emplace(std::make_pair(priority, callback_type)); + } return JNI_TRUE; } -LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, unhookMethod, jobject hookMethod, jobject callback) { +LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, unhookMethod, jboolean useModernApi, jobject hookMethod, jobject callback) { auto target = env->FromReflectedMethod(hookMethod); HookItem * hook_item = nullptr; { @@ -125,10 +159,22 @@ LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, unhookMethod, jobject hookMethod, jo jobject backup = hook_item->GetBackup(); if (!backup) return JNI_FALSE; JNIMonitor monitor(env, backup); + jmethodID before = nullptr; + if (useModernApi) { + auto before_method = JNI_GetObjectField(env, callback, before_method_field); + before = env->FromReflectedMethod(before_method); + } for (auto i = hook_item->callbacks.begin(); i != hook_item->callbacks.end(); ++i) { - if (env->IsSameObject(i->second, callback)) { - hook_item->callbacks.erase(i); - return JNI_TRUE; + if (useModernApi) { + if (i->second.use_modern_api && before == i->second.before_method) { + hook_item->callbacks.erase(i); + return JNI_TRUE; + } + } else { + if (!i->second.use_modern_api && env->IsSameObject(i->second.callback_object, callback)) { + hook_item->callbacks.erase(i); + return JNI_TRUE; + } } } return JNI_FALSE; @@ -153,7 +199,7 @@ LSP_DEF_NATIVE_METHOD(jobject, HookBridge, invokeOriginalMethod, jobject hookMet } LSP_DEF_NATIVE_METHOD(jobject, HookBridge, allocateObject, jclass cls) { - return env->AllocObject(clazz); + return env->AllocObject(cls); } LSP_DEF_NATIVE_METHOD(jobject, HookBridge, invokeSpecialMethod, jobject method, jcharArray shorty, @@ -269,7 +315,7 @@ LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, setTrusted, jobject cookie) { return lsplant::MakeDexFileTrusted(env, cookie); } -LSP_DEF_NATIVE_METHOD(jobjectArray, HookBridge, callbackSnapshot, jobject method) { +LSP_DEF_NATIVE_METHOD(jobjectArray, HookBridge, callbackSnapshot, jclass callback_class, jobject method) { auto target = env->FromReflectedMethod(method); HookItem *hook_item = nullptr; { @@ -284,21 +330,28 @@ LSP_DEF_NATIVE_METHOD(jobjectArray, HookBridge, callbackSnapshot, jobject method JNIMonitor monitor(env, backup); auto res = env->NewObjectArray((jsize) hook_item->callbacks.size(), env->FindClass("java/lang/Object"), nullptr); for (jsize i = 0; auto callback: hook_item->callbacks) { - env->SetObjectArrayElement(res, i++, env->NewLocalRef(callback.second)); + if (callback.second.use_modern_api) { + auto before_method = JNI_ToReflectedMethod(env, clazz, callback.second.before_method, JNI_TRUE); + auto after_method = JNI_ToReflectedMethod(env, clazz, callback.second.after_method, JNI_TRUE); + auto callback_object = JNI_NewObject(env, callback_class, callback_ctor, before_method, after_method); + env->SetObjectArrayElement(res, i++, env->NewLocalRef(callback_object)); + } else { + env->SetObjectArrayElement(res, i++, env->NewLocalRef(callback.second.callback_object)); + } } return res; } static JNINativeMethod gMethods[] = { - LSP_NATIVE_METHOD(HookBridge, hookMethod, "(Ljava/lang/reflect/Executable;Ljava/lang/Class;ILjava/lang/Object;)Z"), - LSP_NATIVE_METHOD(HookBridge, unhookMethod, "(Ljava/lang/reflect/Executable;Ljava/lang/Object;)Z"), + LSP_NATIVE_METHOD(HookBridge, hookMethod, "(ZLjava/lang/reflect/Executable;Ljava/lang/Class;ILjava/lang/Object;)Z"), + LSP_NATIVE_METHOD(HookBridge, unhookMethod, "(ZLjava/lang/reflect/Executable;Ljava/lang/Object;)Z"), LSP_NATIVE_METHOD(HookBridge, deoptimizeMethod, "(Ljava/lang/reflect/Executable;)Z"), LSP_NATIVE_METHOD(HookBridge, invokeOriginalMethod, "(Ljava/lang/reflect/Executable;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"), LSP_NATIVE_METHOD(HookBridge, invokeSpecialMethod, "(Ljava/lang/reflect/Executable;[CLjava/lang/Class;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"), LSP_NATIVE_METHOD(HookBridge, allocateObject, "(Ljava/lang/Class;)Ljava/lang/Object;"), LSP_NATIVE_METHOD(HookBridge, instanceOf, "(Ljava/lang/Object;Ljava/lang/Class;)Z"), LSP_NATIVE_METHOD(HookBridge, setTrusted, "(Ljava/lang/Object;)Z"), - LSP_NATIVE_METHOD(HookBridge, callbackSnapshot, "(Ljava/lang/reflect/Executable;)[Ljava/lang/Object;"), + LSP_NATIVE_METHOD(HookBridge, callbackSnapshot, "(Ljava/lang/Class;Ljava/lang/reflect/Executable;)[Ljava/lang/Object;"), }; void RegisterHookBridge(JNIEnv *env) { diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java index fb6c0f88..6db093ec 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java @@ -149,11 +149,6 @@ public class LSPModuleService extends IXposedService.Stub { return IXposedService.FRAMEWORK_PRIVILEGE_ROOT; } - @Override - public Bundle featuredMethod(String name, Bundle args) { - throw new UnsupportedOperationException(); - } - @Override public List getScope() throws RemoteException { ensureModule();