diff --git a/lib/android.js b/lib/android.js index d023166..5c28f99 100644 --- a/lib/android.js +++ b/lib/android.js @@ -556,7 +556,35 @@ function tryGetEnvJvmti (vm, runtime) { 'bool', ['pointer', 'pointer', 'pointer']); const errorPtr = Memory.alloc(pointerSize); - const success = ensurePluginLoaded(runtime, Memory.allocUtf8String('libopenjdkjvmti.so'), errorPtr); + // Android 17 (API >= 37): loading libopenjdkjvmti.so into an already-running + // ("live") VM makes the first Java.use() appear to hang and then aborts the + // target. EnsurePluginLoaded -> Plugin::Load -> dlopen -> ArtPlugin_Initialize + // -> DeoptManager::FinishSetup(); on a LIVE VM that runs the heavy debuggable + // transition, ending in Instrumentation::UpdateEntrypointsForDebuggable(). Its + // class walk (InstallStubsForClass -> GetOptimizedCodeFor -> + // ArtMethod::GetOatMethodQuickCode -> FindOatMethodFor -> + // OatFile::OatClass::GetOatMethodOffsets) hits a fatal + // CHECK_LT(method_index, num_methods_) on any class whose runtime method layout + // diverges from its AOT odex (e.g. pairip-protected apps, where the shipped + // base.odex is a stub and the real classes are loaded at runtime). ART's abort + // holds the mutator lock exclusively, so it presents as a hang before the + // process dies. A -f spawn dodges it: the VM is still in JVMTI_PHASE_ONLOAD, + // where FinishSetup takes a cheap early-return branch. The bridge only needs the + // jvmtiEnv for GetLoadedClasses + object tagging, neither of which needs the + // debuggable transition (method hooking uses the bridge's own ArtMethodMangler), + // so on 37+ we no-op FinishSetup for the duration of the load, then undo it. See + // installJvmtiFinishSetupWorkaround below. + const undoJvmtiLoadWorkaround = (getAndroidApiLevel() >= 37) + ? installJvmtiFinishSetupWorkaround() + : null; + let success; + try { + success = ensurePluginLoaded(runtime, Memory.allocUtf8String('libopenjdkjvmti.so'), errorPtr); + } finally { + if (undoJvmtiLoadWorkaround !== null) { + undoJvmtiLoadWorkaround(); + } + } if (!success) { // FIXME: Avoid leaking error return; @@ -580,6 +608,92 @@ function tryGetEnvJvmti (vm, runtime) { return env; } +// Installs the FinishSetup workaround and returns an undo() that removes it. Scoped +// to the plugin load: libopenjdkjvmti.so is loaded by art::Plugin::Load via +// dlopen -> dlsym("ArtPlugin_Initialize") -> init(). We can't hook +// ArtPlugin_Initialize before it exists, so we hook dlsym: when ART looks up +// ArtPlugin_Initialize the module is mapped, and dlsym's return value is that +// export's address. From it we locate the module and replace +// DeoptManager::FinishSetup() with a no-op before init() runs, so +// ArtPlugin_Initialize's cheap DeoptManager::Setup() still runs but the aborting +// FinishSetup() transition is skipped. Once the load has returned, undo() detaches +// the dlsym hooks and reverts FinishSetup, leaving nothing installed. +function installJvmtiFinishSetupWorkaround () { + const ARTPLUGIN_INIT = 'ArtPlugin_Initialize'; + const FINISH_SETUP = '_ZN12openjdkjvmti12DeoptManager11FinishSetupEv'; + + // Kept referenced for the lifetime of the replacement (until undo reverts it). + const noop = new NativeCallback(() => {}, 'void', ['pointer']); + const listeners = []; + let finishSetupAddr = null; + + const neuterFinishSetup = (pluginInitAddr) => { + if (finishSetupAddr !== null) { + return; + } + let mod = null; + try { mod = Process.findModuleByAddress(pluginInitAddr); } catch (e) {} + if (mod === null) { + try { mod = Process.getModuleByName('libopenjdkjvmti.so'); } catch (e) {} + } + if (mod === null) { + return; + } + let addr = null; + try { addr = mod.findExportByName(FINISH_SETUP); } catch (e) {} + if (addr === null || addr === undefined) { + return; + } + Interceptor.replace(addr, noop); + Interceptor.flush(); + finishSetupAddr = addr; + }; + + const hookDlsym = (name) => { + let addr = null; + for (const lib of ['libdl.so', 'libc.so']) { + try { addr = Process.getModuleByName(lib).findExportByName(name); } catch (e) {} + if (addr !== null && addr !== undefined) { + break; + } + } + if (addr === null || addr === undefined) { + try { addr = Module.getGlobalExportByName(name); } catch (e) {} + } + if (addr === null || addr === undefined) { + return; + } + listeners.push(Interceptor.attach(addr, { + onEnter (args) { + this.wanted = false; + try { + this.wanted = args[1].readCString() === ARTPLUGIN_INIT; + } catch (e) {} + }, + onLeave (retval) { + if (this.wanted && !retval.isNull()) { + neuterFinishSetup(retval); + } + } + })); + }; + + hookDlsym('dlsym'); + hookDlsym('__loader_dlsym'); + Interceptor.flush(); + + return function undo () { + for (const listener of listeners) { + listener.detach(); + } + if (finishSetupAddr !== null) { + Interceptor.revert(finishSetupAddr); + finishSetupAddr = null; + } + Interceptor.flush(); + }; +} + export function ensureClassInitialized (env, classRef) { const api = getApi(); if (api.flavor !== 'art') {