ensureClassInitialized() no longer initializes on ART since 7983e815 — hooks on uninitialized classes install successfully but never dispatch
A pull request for this has already been merged.
- #1 by @earlzdev — merged
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 52/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- java, javascript
- Domain
- devtools, mobile-dev
Research direction
Start in class-factory.js and trace ensureClassInitialized() immediately before ClassModel.build() during Java.use(). Compare the behavior introduced by 7983e815 with the v7.0.10 path, then reproduce the uninitialized-class hook case on the affected Android versions. Done means hooks on previously uninitialized classes dispatch reliably or fail loudly instead of reporting success.
Written by the indexing model from the issue text.
Description
Summary
Since 7983e815 (first released in v7.0.11), ensureClassInitialized() no longer initializes the class on ART. Hooks installed on a class that has not yet been initialized are accepted without error and then never dispatch — no exception, no warning, nothing in the console.
The change
export function ensureClassInitialized (env, classRef) {
const api = getApi();
if (api.flavor !== 'art') {
return;
}
- env.getFieldId(classRef, 'x', 'Z');
- env.exceptionClear();
+ env.getClassName(classRef);
}
android: Make ensureClassInitialized() less noisy
By performing an operation that never throws.
The intent was clearly to remove exception noise. But the noisy call was the only thing doing the work: GetFieldID for a field that does not exist reaches ClassLinker::EnsureInitialized, which is what actually initialized the class. GetClassName never does. The function still has the same name and is still called from class-factory.js immediately before ClassModel.build() on every Java.use() — it just no longer initializes anything.
Why it matters
entry_point_from_quick_compiled_code_ on an uninitialized class holds an ART trampoline rather than final code. The ArtMethod patch is applied over that placeholder with a raw pointer write and no verification, so it always reports success. The class is then initialized later, on first use, and the hook never fires.
The failure is completely silent: Java.use() succeeds, .implementation = succeeds, and the hook simply does nothing.
Affected versions
| version | contains 7983e815 |
hooks on uninitialized classes |
|---|---|---|
| v7.0.10 | no | work |
v7.0.11, v7.0.12, v7.0.13, master (b38a5b64) |
yes | silently dead |
Only three code commits landed between v7.0.10 and v7.0.11 — 4a4970bc, 8487d3d5, 7983e815.
How we isolated it
Four builds differing by a single commit each, with frida-gadget held byte-identical at 17.17.0 across all of them. Only reverting 7983e815 flips the behaviour, and two control hooks in the same build and same process keep working either way:
| class | hooked method | dispatches? |
|---|---|---|
android.hardware.biometrics.BiometricPrompt |
authenticate |
no |
android.hardware.camera2.CaptureRequest$Builder |
addTarget, build |
no |
android.hardware.camera2.impl.CameraCaptureSessionImpl |
setRepeatingRequest |
no |
android.hardware.biometrics.BiometricManager |
canAuthenticate |
yes |
android.hardware.camera2.impl.CameraDeviceImpl |
createCaptureSession |
yes |
The split matches class initialization state. oatdump on a failing device shows the dead class at SuperclassValidated and a working one at VisiblyInitialized in the same process. The working classes are ones the app touches early (so they are already initialized when we hook); the dead ones are first used later.
It is therefore device-dependent, since which framework classes are pre-initialized varies by vendor image. On Galaxy S21 the same app and same build passes on Android 11 and fails on Android 12.
A partially applied hook set can crash the app
Where several hooks cooperate, losing some of them is worse than losing all of them. We substitute camera surfaces across createCaptureSession + addTarget + build. On an affected device only createCaptureSession dispatches, so the session is configured with substituted surfaces while the request keeps the originals:
java.lang.IllegalArgumentException: CaptureRequest contains unconfigured Input/Output Surface!
at android.hardware.camera2.CaptureRequest.convertSurfaceToStreamId
at android.hardware.camera2.impl.CameraDeviceImpl.setRepeatingRequest
Minimal reproduction
The behaviour change should be observable without hooking anything, since ensureClassInitialized() runs on every Java.use(). Noting honestly that this standalone case is derived from the code path rather than executed — our own observations come from an embedded-gadget setup, so the smallest form we can vouch for is the dispatch failure below it:
public class Uninit {
static { android.util.Log.i("REPRO", "CLINIT RAN"); }
public static String foo() { return "real"; }
}
Uninit is never referenced during startup. Then:
Java.perform(() => { Java.use('com.example.repro.Uninit'); });
- v7.0.10 —
CLINIT RANappears in logcat - v7.0.11+ — nothing
Adding .foo.implementation = ... on top shows the consequence: the hook installs and never fires on v7.0.11+, and fires again if the class is initialized first via Class.forName(name, true, loader).
What we have not verified
We have not confirmed how the patch stops taking effect. Two possibilities are consistent with everything we measured:
- initialization rewrites the entry point, discarding the patch; or
- the patch is never read, because dispatch on an uninitialized class resolves through a path that does not consult the patched
ArtMethod.
Both are fixed by initializing before patching, but they imply different fixes upstream. Happy to instrument and report back if useful.
Suggested fix
Restore initialization without the exception noise, or handle the uninitialized case in the patching path so it fails loudly rather than silently. Either way, a hook that cannot take effect should not report success.
Happy to test any patch against the affected devices.
- Dominant language
- JavaScript
- Stars
- 413
- Forks
- 173
- PR merge metrics
- No merged PRs in 30d
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from frida/frida-java-bridge
-
Types is missing `deoptimizeMethod`Possibly taken @Electric1447 claimed this 172 days ago. Open
Difficulty 1/5 Under an hour Newbie friendliness 65/100
frida/frida-java-bridge#386 ·
-
[v7.0.11+] Process crash on accessing object["<init>"] with JVMTI availablePossibly taken A pull request linked to this issue is open or already merged. Open
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
frida/frida-java-bridge#384 · 2 comments · 2 reactions ·
-
Difficulty 3/5 1-2 days Newbie friendliness 42/100
frida/frida-java-bridge#409 ·
-
Difficulty 5/5 Over a week Newbie friendliness 20/100
frida/frida-java-bridge#404 ·
-
Difficulty 4/5 3-5 days Newbie friendliness 35/100
frida/frida-java-bridge#397 · 2 comments ·
All issues in frida/frida-java-bridge
Similar issues
-
[BUG] Multi-day events show "Ended" while still in progressPossibly taken @tarunagnihotri534 claimed this today. Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
data-umbrella/du-event-board#231 · 2 comments ·
-
Difficulty 1/5 Under an hour Newbie friendliness 78/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
NaturalIntelligence/fast-xml-parser#888 · 1 comment ·
Maintainers usually reply within 2 days
-
Difficulty 1/5 Under an hour Newbie friendliness 80/100
USACE/chart-docs#766 ·
-
bug callouts regression revealjs
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
quarto-dev/quarto-cli#15014 ·
Maintainers usually reply within 1 day