Fix Java.perform crash on Android 17 when attaching to a live VM#400
Open
Wes765 wants to merge 1 commit into
Open
Fix Java.perform crash on Android 17 when attaching to a live VM#400Wes765 wants to merge 1 commit into
Wes765 wants to merge 1 commit into
Conversation
On Android 17 (API 37), the first Java.use() after attaching to an
already-running ("live") VM can make Java.perform() appear to hang and then
abort the target process. It only happens on attach; a `-f` spawn is unaffected.
EnsurePluginLoaded -> Plugin::Load -> dlopen -> ArtPlugin_Initialize ->
DeoptManager::FinishSetup(). On a live VM FinishSetup runs the heavy debuggable
transition, ending in Instrumentation::UpdateEntrypointsForDebuggable(). That
walks every loaded class installing debug stubs, and for each method resolves
AOT code via ArtMethod::GetOatMethodQuickCode -> FindOatMethodFor ->
OatFile::OatClass::GetOatMethodOffsets, which asserts
CHECK_LT(method_index, num_methods_). For a class whose runtime method layout
diverges from its AOT odex (for example pairip-protected apps, where the shipped
base.odex is a stub and the real classes are loaded at runtime), the computed
index runs past the stub OatClass and ART aborts. The abort holds the mutator
lock exclusively and skips its all-thread dump, so it looks like a hang before
the tombstone lands. A `-f` spawn dodges it because the VM is still in
JVMTI_PHASE_ONLOAD, where FinishSetup takes a cheap early-return branch.
The bridge only needs the jvmtiEnv for GetLoadedClasses and object tagging,
neither of which needs the debuggable transition (method hooking uses the
bridge's own ArtMethodMangler). So on API >= 37 we no-op
DeoptManager::FinishSetup for the duration of the plugin load: hook
dlsym/__loader_dlsym, and when ART resolves ArtPlugin_Initialize, locate the
just-mapped module and Interceptor.replace its FinishSetup export with a no-op
before init() runs. The cheap DeoptManager::Setup() still runs. Resolved by
symbol name with no hardcoded offsets, and undone once EnsurePluginLoaded
returns.
Verified on a Pixel 10 Pro XL (Android 17, SDK 37, arm64): JVMTI loads on
attach, Java.perform completes across repeated fresh launches, GetLoadedClasses
enumerates via JVMTI, and .implementation hooks dispatch. On a normal app the
transition completes on its own in about 30 ms, so the workaround is a no-op
there; on a pairip-protected app it aborts without the workaround and succeeds
with it. Spawn path unchanged.
6785206 to
4174688
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.
On Android 17 (API 37), the first
Java.use()after attaching to analready-running ("live") VM can make
Java.perform()appear to hang and thenabort the target process. It reproduces on attach; a
-fspawn is unaffected.Root cause
tryGetEnvJvmti()loads the JVMTI plugin viaRuntime::EnsurePluginLoaded("libopenjdkjvmti.so"):On a live VM,
DeoptManager::FinishSetup()runs the heavy debuggabletransition (
WaitForBackgroundVerificationTasksToFinish,ScopedJitSuspend, afull
ThreadList::SuspendAll,InvalidateAllCompiledCode,UpdateEntrypointsForDebuggable). None of those blocks; the branch runs to theend and then aborts inside
Instrumentation::UpdateEntrypointsForDebuggable().That walks every loaded class installing debug stubs, and for each method
resolves AOT code:
For a class whose runtime method layout diverges from its AOT odex, the computed
method index runs past the class's
OatClassand theCHECKaborts the process.This is common on pairip-protected apps, where the shipped
base.odexis astub and the real classes are decrypted and loaded at runtime, so a class ART
sees with 17+ methods may have only 5 recorded in the odex. ART's abort holds the
mutator lock exclusively and skips its all-thread dump, so from the client side
it looks like a hang right before the tombstone.
A
-fspawn dodges all of this because the VM is still inJVMTI_PHASE_ONLOADwhen the plugin loads, where
FinishSetup()takes a cheap early-return branch.That
ONLOADvsLIVEsplit is the only spawn-vs-attach divergence in the loadpath. On an ordinary app the
LIVEtransition completes on its own (about 30 mshere) and the process is fine; the failure only surfaces on apps with the
odex/runtime divergence.
Fix
The bridge only uses the jvmtiEnv for
GetLoadedClassesand object tagging;neither needs the debuggable transition (method hooking uses the bridge's own
ArtMethodMangler, not JVMTI deopt). So on API >= 37 we no-opDeoptManager::FinishSetupfor the duration of the plugin load, then restore it:dlsym/__loader_dlsym. When ART resolvesArtPlugin_Initialize, itsreturn value locates the just-mapped
libopenjdkjvmti.so; resolveDeoptManager::FinishSetupby exported symbol name andInterceptor.replaceit with a no-op before
init()runs. The cheapDeoptManager::Setup()still runs.
EnsurePluginLoadedreturns,undo()reverts the replacement anddetaches the
dlsymhooks, so nothing is left installed.Resolved by symbol name, no hardcoded offsets. Gated to API >= 37; older ART is
unchanged. On apps where the transition would have completed anyway this is a
harmless no-op (the transition is not needed by the bridge); on apps where it
would have aborted, it keeps the target alive.
Verification
Pixel 10 Pro XL, Android 17 (SDK 37, arm64):
workaround the target aborts in
UpdateEntrypointsForDebuggable(
CHECK failed: method_index < num_methods_); with it,Java.performcompletes,
GetLoadedClassesenumerates ~31k classes via JVMTI,.implementationhooks install and dispatch, and the target stays alive.its own; behavior is unchanged with the workaround.
-fspawn (Settings and the same apps): unchanged.