Refuse to hook Object.getClass - #807
Open
JingMatrix wants to merge 1 commit into
Open
Conversation
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.
JingMatrix
force-pushed
the
refuse-getclass
branch
from
July 30, 2026 07:38
78dcae3 to
57b133a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The minimal fix for #798, plus clearer messages for every refused hook.
A module hooked
Object.getClass()and the launcher boot-looped. Since AGP 9 (#792), R8 turnsKotlin's null checks into
obj.getClass(), and the dispatch calls it entering every hooked method —so a hook on
getClassmakes the dispatch call itself until the stack runs out. lsplant marks ahooked method non-compilable and our dex is never compiled, so there is no way out at runtime.
getClassis the third method that can't be hooked, next toMethod.invokeandConstructor.newInstance, already refused for the same reason: the dispatch itself uses them. Theonly difference is that the compiler, not the framework, is what calls
getClass— which is why itslipped through until AGP 9 put the call everywhere.
No module has a reason to hook
Object.getClass. The one that hit this hooked every method namedgetClassoffClass.getMethods(), which always includes the inherited one, when it wanted a realgetClassoverride on another class. Refusing turns a boot loop into aHookFailedErrorthe modulealready catches.
Everything else the dispatch touches (
Class.getName,Throwable.getCause, unboxing) stays hookable:those are cold paths, called rarely, so they never recursed even before — only
getClassis on thepath into every method.
This also rewrites the refusal messages so a developer sees why, not just that it failed, e.g.
"Object.getClass cannot be hooked: Vector's dispatch calls it entering every hooked method, so a
hook here would call itself forever."
An alternative that makes hooking
getClasssurvivable instead — a dispatch guard, a native entrypoint,
VectorChainin Java — is #803. It is the general form but costs ~10% per dispatch and atricky invariant; left open for its analysis and its on-device test.
Verified on a Pixel 7a / Android 16: hooking
getClassnow fails at registration and every otherhook still runs with the process alive; on master the same module boot-loops.
Fixes #798.