Skip to content

Refuse to hook Object.getClass - #807

Open
JingMatrix wants to merge 1 commit into
masterfrom
refuse-getclass
Open

Refuse to hook Object.getClass#807
JingMatrix wants to merge 1 commit into
masterfrom
refuse-getclass

Conversation

@JingMatrix

@JingMatrix JingMatrix commented Jul 30, 2026

Copy link
Copy Markdown
Owner

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 turns
Kotlin's null checks into obj.getClass(), and the dispatch calls it entering every hooked method —
so a hook on getClass makes the dispatch call itself until the stack runs out. lsplant marks a
hooked method non-compilable and our dex is never compiled, so there is no way out at runtime.

getClass is the third method that can't be hooked, next to Method.invoke and
Constructor.newInstance, already refused for the same reason: the dispatch itself uses them. The
only difference is that the compiler, not the framework, is what calls getClass — which is why it
slipped 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 named
getClass off Class.getMethods(), which always includes the inherited one, when it wanted a real
getClass override on another class. Refusing turns a boot loop into a HookFailedError the module
already 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 getClass is on the
path 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 getClass survivable instead — a dispatch guard, a native entry
point, VectorChain in Java — is #803. It is the general form but costs ~10% per dispatch and a
tricky invariant; left open for its analysis and its on-device test.

Verified on a Pixel 7a / Android 16: hooking getClass now fails at registration and every other
hook still runs with the process alive; on master the same module boot-loops.

Fixes #798.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hook dispatch re-enters itself since AGP 9 compiles null checks into Object.getClass()

1 participant