From 57b133a2cfbcb357299b40b9204aec2f3460d8b1 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 30 Jul 2026 09:28:15 +0200 Subject: [PATCH] Refuse to hook Object.getClass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since AGP 9, R8 compiles Kotlin's parameter null checks into obj.getClass(), and one of them is the first instruction of the hook dispatch. The dispatch therefore calls Object.getClass() on the way in to every hooked method, before any hooker runs, so a module hooking it makes the dispatch re-enter itself from its own prologue until the stack is gone. lsplant marks a hooked method non-compilable and the framework dex is never AOT-compiled, so there is no recovery at runtime. The launcher boot-looped on the reporter's device; see #798. Refuse it at both registration paths, next to the existing refusals of Method.invoke and Constructor.newInstance, which exist for the same reason: a method the dispatch cannot avoid touching must not carry a hook. Object.getClass is the third of that kind, and the only one the compiler rather than the framework introduces. No module has a reason to hook it; the one that triggered this hooked every method named getClass over Class.getMethods(), which always lists the one inherited from Object, when it meant a real getClass override on another class. Also rewrites the refusal messages for the other blocked hooks — abstract methods, framework-internal methods, Method.invoke, Constructor.newInstance — so each says why rather than only that it failed. --- .../de/robv/android/xposed/XposedBridge.java | 12 +++++--- .../vector/impl/hooks/VectorNativeHooker.kt | 30 +++++++++++++++---- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java b/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java index 5e85d36c6..0f59feaa9 100644 --- a/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java +++ b/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java @@ -174,13 +174,17 @@ public static void deoptimizeMethod(Member deoptimizedMethod) { */ public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook callback) { if (!(hookMethod instanceof Executable)) { - throw new IllegalArgumentException("Only methods and constructors can be hooked: " + hookMethod); + throw new IllegalArgumentException("Only methods and constructors can be hooked, not " + hookMethod); } else if (Modifier.isAbstract(hookMethod.getModifiers())) { - throw new IllegalArgumentException("Cannot hook abstract methods: " + hookMethod); + throw new IllegalArgumentException(hookMethod + " is abstract: it has no body to hook. Hook the concrete override instead."); } else if (hookMethod.getDeclaringClass().getClassLoader() == XposedBridge.class.getClassLoader()) { - throw new IllegalArgumentException("Do not allow hooking inner methods"); + throw new IllegalArgumentException(hookMethod + " belongs to Vector itself. Hooking the framework would hook the code running your hook."); } else if (hookMethod.getDeclaringClass() == Method.class && hookMethod.getName().equals("invoke")) { - throw new IllegalArgumentException("Cannot hook Method.invoke"); + throw new IllegalArgumentException("Method.invoke cannot be hooked: Vector calls it to run the original of every hooked method, so a hook here would call itself forever."); + } else if (hookMethod.getDeclaringClass() == Object.class && hookMethod.getName().equals("getClass")) { + // Since AGP 9, R8 compiles Kotlin null checks into Object.getClass(), so the dispatch + // calls it entering every hooked method; a hook here re-enters the dispatch. See #798. + throw new IllegalArgumentException("Object.getClass cannot be hooked: Vector's dispatch calls it entering every hooked method, so a hook here would call itself forever."); } if (callback == null) { diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt index c8b65fe78..920018bcd 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt @@ -34,23 +34,41 @@ class VectorHookBuilder( override fun intercept(hooker: Hooker): HookHandle { if (Modifier.isAbstract(origin.modifiers)) { - throw IllegalArgumentException("Cannot hook abstract methods: $origin") + throw IllegalArgumentException( + "$origin is abstract: it has no body to hook. Hook the concrete override instead." + ) } else if (origin.declaringClass.classLoader == VectorHookBuilder::class.java.classLoader) { - throw IllegalArgumentException("Do not allow hooking inner methods") + throw IllegalArgumentException( + "$origin belongs to Vector itself. Hooking the framework would hook the code running your hook." + ) } else if ( origin is Method && origin.declaringClass == Method::class.java && origin.name == "invoke" ) { - throw IllegalArgumentException("Cannot hook Method.invoke") + throw IllegalArgumentException( + "Method.invoke cannot be hooked: Vector calls it to run the original of every hooked method, so a hook here would call itself forever." + ) } else if ( origin is Method && origin.declaringClass == Constructor::class.java && origin.name == "newInstance" ) { - // Named alongside Method.invoke by the API: the framework reflects through both, so - // hooking either recurses into the hook dispatch. - throw IllegalArgumentException("Cannot hook Constructor.newInstance") + throw IllegalArgumentException( + "Constructor.newInstance cannot be hooked: Vector reflects through it the same way it does Method.invoke, so a hook here would recurse." + ) + } else if ( + origin is Method && + origin.declaringClass == Any::class.java && + origin.name == "getClass" + ) { + // The compiler, not the framework, is what makes this recurse: since AGP 9 R8 compiles + // Kotlin null checks into Object.getClass(), so the dispatch calls it entering every + // hooked method. See #798. If you meant a getClass override on another class, hook that + // class; Class.getMethods() lists the inherited one, which is not what you want. + throw IllegalArgumentException( + "Object.getClass cannot be hooked: Vector's dispatch calls it entering every hooked method, so a hook here would call itself forever." + ) } // Resolve DEFAULT here rather than at throw time: the record is stored natively and