Skip to content

DexFactory parent class loader injection (#1951) crashes release builds with "Attempt to register dex file ... with multiple class loaders" #1962

@adrian-niculescu

Description

@adrian-niculescu

Summary

On release (non debuggable) builds, the first time the runtime generates a proxy class on the main thread, the app crashes with:

java.lang.InternalError: Attempt to register dex file
  /data/user/0/<app>/code_cache/secondary-dexes/<proxy>.jar with multiple class loaders

This was introduced by #1951 (commit c9d41e62, currently on main). Debug builds are not affected, so it passes normal development and only appears once an app ships a release build. The runtime reports version 9.0.4 (the commit sits a few commits past the 9.0.4 tag).

What triggers it

Any JavaScript that implements or extends a native type whose proxy is produced at runtime, on the main thread (worker id 0). For example:

// Main thread. JS implements a native interface, so the runtime must
// generate a Java proxy class for it and load it through DexFactory.
const runnable = new java.lang.Runnable({
  run() {
    console.log("invoked from Java");
  }
});

new android.os.Handler(android.os.Looper.getMainLooper()).post(runnable);

Statically bound proxies are not affected, because resolveClass finds the pre-generated class and returns before reaching the new code path. If the static binding generator covers the example above, force the runtime path (for example by building the implementation dynamically) so that DexFactory.resolveClass actually performs generation. You can confirm the path is taken by logging inside injectDexIntoClassLoader.

Background: the class loader hierarchy

Android resolves classes through a chain of class loaders. Each one delegates to its parent before trying itself:

BootClassLoader     framework classes (android.*, java.*)
   ^ parent
PathClassLoader     the app's own classes, from the APK dex files
   ^ parent
DexClassLoader      a dex or jar loaded on demand from an arbitrary path

PathClassLoader is the app's primary loader, created by the system when the process starts. DexClassLoader is created by code at runtime to load a dex or jar from a chosen path. NativeScript uses it to load proxy classes it generates at runtime, with the PathClassLoader as the parent so the proxy can resolve the app and framework types it references.

Two runtime rules are relevant:

  1. A class's identity is the pair (name, defining class loader). The same bytecode loaded by two loaders produces two distinct types.
  2. A given dex file may be registered with only one class loader. ART enforces this and throws InternalError: Attempt to register dex file ... with multiple class loaders when a dex is claimed by a second loader.

Root cause

Before #1951, resolveClass loaded each runtime generated proxy through its own DexClassLoader: one dex, one owner.

#1951 added injectIntoParentClassLoader, enabled for the main thread runtime in Runtime.init. When enabled, resolveClass calls injectDexIntoClassLoader, which:

  1. creates a temporary DexClassLoader over the proxy jar, then
  2. splices the dex element into the app's PathClassLoader and loads the proxy through the PathClassLoader.

Both the temporary DexClassLoader and the app PathClassLoader now reference the same DexFile object (through the spliced element). Registering one DexFile against a second class loader is what ART rejects: the check is keyed on DexFile identity and is unconditional. ClassLinker::RegisterDexFile throws ThrowDexFileAlreadyRegisteredError with no android:debuggable guard. Whether a second registration actually happens depends on the build type, which is why only release crashes (see below).

Why debug builds are not affected

The single-owner rule is enforced identically in both modes; the throw is not gated on android:debuggable. Both loaders reference one DexFile (the spliced element), so the only build-dependent question is whether the temporary loader also registers it:

  • Release: the temporary loader ends up registering the proxy DexFile (a non-debuggable runtime verifies, and may AOT-compile, the dex in-process). The later load through the app PathClassLoader then registers the same DexFile against a second loader, and ART throws.
  • Debug: a debuggable runtime is interpret-only and does not use AOT code (class_linker.cc: "For debuggable runtimes we don't use AOT code"), and the temporary loader never claims the dex, so it is registered exactly once through the app loader and nothing throws.

The exact ART step that registers the dex against the temporary loader in release is not instrumented here; what is certain is that two loaders share one DexFile and that ClassLinker::RegisterDexFile is identity-keyed and unconditional, so the second registration throws. The release-only crash is consistent with the interpret-only vs verify/AOT split. (The odexDir passed as the DexClassLoader optimizedDirectory has no effect since API 26, so it is not the relevant factor.)

Expected behavior

Runtime generated proxies load without crashing in release, as they did before #1951.

Suggested direction

The goal of #1951 is to make generated proxies discoverable through the app's PathClassLoader (for example for Class.forName). The current implementation opens the dex with a separate loader first, which is what creates the second owner. Two options:

  1. Register the dex with the PathClassLoader as its only owner, for example by building the dex element through the PathClassLoader's own DexPathList instead of through a temporary DexClassLoader. The dex is then registered exactly once.
  2. Make the injection opt in and keep the previous isolated DexClassLoader path as the default, so apps that do not need Class.forName discovery are unaffected.

Environment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions