Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 115 additions & 1 deletion lib/android.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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') {
Expand Down
Loading