Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

ensureClassInitialized() no longer initializes on ART since 7983e815 — hooks on uninitialized classes install successfully but never dispatch

Open
#408 0 comments 0 reactions 0 assignees View on GitHub

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

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 RAN appears 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:

  1. initialization rewrites the entry point, discarding the patch; or
  2. 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from frida/frida-java-bridge

All issues in frida/frida-java-bridge

Similar issues

More JavaScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.