[core] Preload module apk (#877)

This commit is contained in:
vvb2060 2021-08-09 19:27:58 +08:00 committed by GitHub
parent 4f3a615ba9
commit 3cd9fd1735
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 174 additions and 161 deletions

View File

@ -1,8 +1,8 @@
package org.lsposed.lspd.models;
import org.lsposed.lspd.models.ModuleConfig;
import org.lsposed.lspd.models.PreLoadedApk;
parcelable Module {
String name;
String apk;
ModuleConfig config;
String packageName;
String apkPath;
PreLoadedApk file;
}

View File

@ -1,6 +1,7 @@
package org.lsposed.lspd.models;
parcelable ModuleConfig {
parcelable PreLoadedApk {
List<SharedMemory> preLoadedDexes;
List<String> moduleClassNames;
List<String> moduleLibraryNames;
}

View File

@ -31,7 +31,6 @@ import static de.robv.android.xposed.XposedHelpers.getObjectField;
import static de.robv.android.xposed.XposedHelpers.getParameterIndexByType;
import static de.robv.android.xposed.XposedHelpers.setStaticObjectField;
import android.annotation.SuppressLint;
import android.content.pm.ApplicationInfo;
import android.content.res.Resources;
import android.content.res.ResourcesImpl;
@ -40,25 +39,20 @@ import android.content.res.XResources;
import android.os.Build;
import android.os.IBinder;
import android.os.Process;
import android.os.SharedMemory;
import android.util.ArraySet;
import android.util.Log;
import org.lsposed.lspd.models.PreLoadedApk;
import org.lsposed.lspd.nativebridge.NativeAPI;
import org.lsposed.lspd.nativebridge.ResourcesHook;
import org.lsposed.lspd.util.LspModuleClassLoader;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import de.robv.android.xposed.callbacks.XC_InitPackageResources;
@ -87,7 +81,7 @@ public final class XposedInit {
findAndHookMethod("android.app.ApplicationPackageManager", null, "getResourcesForApplication",
ApplicationInfo.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
protected void beforeHookedMethod(MethodHookParam param) {
ApplicationInfo app = (ApplicationInfo) param.args[0];
XResources.setPackageNameForResDir(app.packageName,
app.uid == Process.myUid() ? app.sourceDir : app.publicSourceDir);
@ -119,7 +113,7 @@ public final class XposedInit {
hookAllMethods(classGTLR, createResourceMethod, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
protected void afterHookedMethod(MethodHookParam param) {
// At least on OnePlus 5, the method has an additional parameter compared to AOSP.
final int activityTokenIdx = getParameterIndexByType(param.method, IBinder.class);
final int resKeyIdx = getParameterIndexByType(param.method, classResKey);
@ -131,15 +125,18 @@ public final class XposedInit {
}
Object activityToken = param.args[activityTokenIdx];
//noinspection SynchronizeOnNonFinalField
synchronized (param.thisObject) {
ArrayList<WeakReference<Resources>> resourceReferences;
if (activityToken != null) {
Object activityResources = callMethod(param.thisObject, "getOrCreateActivityResourcesStructLocked", activityToken);
//noinspection unchecked
resourceReferences = (ArrayList<WeakReference<Resources>>) getObjectField(activityResources, "activityResources");
} else {
//noinspection unchecked
resourceReferences = (ArrayList<WeakReference<Resources>>) getObjectField(param.thisObject, "mResourceReferences");
}
resourceReferences.add(new WeakReference(newRes));
resourceReferences.add(new WeakReference<>(newRes));
}
}
});
@ -214,23 +211,23 @@ public final class XposedInit {
}
}
public static boolean loadModules() throws IOException {
public static void loadModules() {
boolean hasLoaded = !modulesLoaded.compareAndSet(false, true);
if (hasLoaded) {
return false;
return;
}
synchronized (moduleLoadLock) {
var moduleList = serviceClient.getModulesList();
ArraySet<String> newLoadedApk = new ArraySet<>();
var newLoadedApk = new ArraySet<String>();
moduleList.forEach(module -> {
var apk = module.apk;
var name = module.name;
var dexes = module.config.preLoadedDexes;
var apk = module.apkPath;
var name = module.packageName;
var file = module.file;
if (loadedModules.contains(apk)) {
newLoadedApk.add(apk);
} else {
loadedModules.add(apk); // temporarily add it for XSharedPreference
boolean loadSuccess = loadModule(name, apk, dexes);
boolean loadSuccess = loadModule(name, apk, file);
if (loadSuccess) {
newLoadedApk.add(apk);
}
@ -240,14 +237,13 @@ public final class XposedInit {
loadedModules.addAll(newLoadedApk);
// refresh callback according to current loaded module list
pruneCallbacks(loadedModules);
pruneCallbacks();
});
}
return true;
}
// remove deactivated or outdated module callbacks
private static void pruneCallbacks(Set<String> loadedModules) {
private static void pruneCallbacks() {
synchronized (moduleLoadLock) {
Object[] loadedPkgSnapshot = sLoadedPackageCallbacks.getSnapshot();
Object[] initPkgResSnapshot = sInitPackageResourcesCallbacks.getSnapshot();
@ -280,37 +276,16 @@ public final class XposedInit {
* Load all so from an APK by reading <code>assets/native_init</code>.
* It will only store the so names but not doing anything.
*/
private static boolean initNativeModule(ClassLoader mcl, String name) {
try (InputStream is = mcl.getResourceAsStream("assets/native_init")) {
if (is == null) return true;
BufferedReader moduleLibraryReader = new BufferedReader(new InputStreamReader(is));
String moduleLibraryName;
while ((moduleLibraryName = moduleLibraryReader.readLine()) != null) {
if (!moduleLibraryName.startsWith("#")) {
NativeAPI.recordNativeEntrypoint(moduleLibraryName);
}
}
return true;
} catch (IOException e) {
Log.e(TAG, " Failed to load native library list from " + name, e);
return false;
}
private static void initNativeModule(List<String> moduleLibraryNames) {
moduleLibraryNames.forEach(NativeAPI::recordNativeEntrypoint);
}
private static boolean initModule(ClassLoader mcl, String name, String apk) {
InputStream is = mcl.getResourceAsStream("assets/xposed_init");
if (is == null) {
return true;
}
try (BufferedReader moduleClassesReader = new BufferedReader(new InputStreamReader(is))) {
String moduleClassName;
while ((moduleClassName = moduleClassesReader.readLine()) != null) {
moduleClassName = moduleClassName.trim();
if (moduleClassName.isEmpty() || moduleClassName.startsWith("#"))
continue;
private static boolean initModule(ClassLoader mcl, String apk, List<String> moduleClassNames) {
var count = 0;
for (var moduleClassName : moduleClassNames) {
try {
Log.i(TAG, " Loading class " + moduleClassName);
Class<?> moduleClass = mcl.loadClass(moduleClassName);
if (!IXposedMod.class.isAssignableFrom(moduleClass)) {
@ -322,6 +297,7 @@ public final class XposedInit {
}
final Object moduleInstance = moduleClass.newInstance();
if (moduleInstance instanceof IXposedHookZygoteInit) {
IXposedHookZygoteInit.StartupParam param = new IXposedHookZygoteInit.StartupParam();
param.modulePath = apk;
@ -330,60 +306,57 @@ public final class XposedInit {
XposedBridge.hookInitZygote(new IXposedHookZygoteInit.Wrapper(
(IXposedHookZygoteInit) moduleInstance, param));
((IXposedHookZygoteInit) moduleInstance).initZygote(param);
count++;
}
if (moduleInstance instanceof IXposedHookLoadPackage)
if (moduleInstance instanceof IXposedHookLoadPackage) {
XposedBridge.hookLoadPackage(new IXposedHookLoadPackage.Wrapper(
(IXposedHookLoadPackage) moduleInstance, apk));
count++;
}
if (moduleInstance instanceof IXposedHookInitPackageResources)
if (moduleInstance instanceof IXposedHookInitPackageResources) {
XposedBridge.hookInitPackageResources(new IXposedHookInitPackageResources.Wrapper(
(IXposedHookInitPackageResources) moduleInstance, apk));
count++;
}
} catch (Throwable t) {
Log.e(TAG, " Failed to load class " + moduleClassName, t);
return false;
}
}
return true;
} catch (IOException e) {
Log.e(TAG, " Failed to load module " + name + " from " + apk, e);
return false;
}
return count > 0;
}
/**
* Load a module from an APK by calling the init(String) method for all classes defined
* in <code>assets/xposed_init</code>.
*/
@SuppressLint("PrivateApi")
private static boolean loadModule(String name, String apk, List<SharedMemory> dexes) {
private static boolean loadModule(String name, String apk, PreLoadedApk file) {
Log.i(TAG, "Loading module " + name + " from " + apk);
if (!new File(apk).exists()) {
Log.e(TAG, " File does not exist");
return false;
}
var librarySearchPath = new StringBuilder();
var sb = new StringBuilder();
var abis = Process.is64Bit() ? Build.SUPPORTED_64_BIT_ABIS : Build.SUPPORTED_32_BIT_ABIS;
for (String abi : abis) {
librarySearchPath.append(apk).append("!/lib/").append(abi).append(File.pathSeparator);
sb.append(apk).append("!/lib/").append(abi).append(File.pathSeparator);
}
ClassLoader initLoader = XposedInit.class.getClassLoader();
ClassLoader mcl = LspModuleClassLoader.loadApk(new File(apk), dexes, librarySearchPath.toString(), initLoader);
var librarySearchPath = sb.toString();
var initLoader = XposedInit.class.getClassLoader();
var mcl = LspModuleClassLoader.loadApk(apk, file.preLoadedDexes, librarySearchPath, initLoader);
try {
if (mcl.loadClass(XposedBridge.class.getName()).getClassLoader() != initLoader) {
Log.e(TAG, " Cannot load module: " + name);
Log.e(TAG, " The Xposed API classes are compiled into the module's APK.");
Log.e(TAG, " This may cause strange issues and must be fixed by the module developer.");
Log.e(TAG, " For details, see: http://api.xposed.info/using.html");
Log.e(TAG, " For details, see: https://api.xposed.info/using.html");
return false;
}
} catch (ClassNotFoundException ignored) {
return false;
}
return initNativeModule(mcl, name) && initModule(mcl, name, apk);
initNativeModule(file.moduleLibraryNames);
return initModule(mcl, apk, file.moduleClassNames);
}
public final static HashSet<String> loadedPackagesInProcess = new HashSet<>(1);

View File

@ -78,14 +78,6 @@ public class Main {
}
}
private static void loadModulesSafely() {
try {
XposedInit.loadModules();
} catch (Exception exception) {
Utils.logE("error loading module list", exception);
}
}
public static void forkPostCommon(boolean isSystem, String appDataDir, String niceName) {
// init logger
YahfaHooker.init();
@ -95,7 +87,7 @@ public class Main {
PrebuiltMethodsDeopter.deoptBootMethods(); // do it once for secondary zygote
installBootstrapHooks(isSystem, appDataDir);
Utils.logI("Loading modules for " + niceName + "/" + Process.myUid());
loadModulesSafely();
XposedInit.loadModules();
}
public static void forkAndSpecializePost(String appDataDir, String niceName, IBinder binder) {

View File

@ -47,12 +47,14 @@ import org.apache.commons.lang3.SerializationUtils;
import org.lsposed.lspd.BuildConfig;
import org.lsposed.lspd.models.Application;
import org.lsposed.lspd.models.Module;
import org.lsposed.lspd.models.ModuleConfig;
import org.lsposed.lspd.models.PreLoadedApk;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.channels.Channels;
@ -200,8 +202,8 @@ public class ConfigManager {
private final Map<ProcessScope, List<Module>> cachedScope = new ConcurrentHashMap<>();
// apkPath, dexes
private final Map<String, List<SharedMemory>> cachedDexes = new ConcurrentHashMap<>();
// apkPath, PreLoadedApk
private final Map<String, PreLoadedApk> cachedApkFile = new ConcurrentHashMap<>();
// appId, packageName
private final Map<Integer, String> cachedModule = new ConcurrentHashMap<>();
@ -256,14 +258,17 @@ public class ConfigManager {
int apkPathIdx = cursor.getColumnIndex("apk_path");
int pkgNameIdx = cursor.getColumnIndex("module_pkg_name");
while (cursor.moveToNext()) {
var module = new Module();
var config = new ModuleConfig();
var path = cursor.getString(apkPathIdx);
config.preLoadedDexes = getModuleDexes(path);
module.name = cursor.getString(pkgNameIdx);
module.apk = path;
module.config = config;
var file = getCachedApkFile(path);
if (file != null) {
var module = new Module();
module.packageName = cursor.getString(pkgNameIdx);
module.apkPath = path;
module.file = file;
modules.add(module);
} else {
Log.w(TAG, "Can not load " + path + ", skip!");
}
}
}
return modules;
@ -476,7 +481,7 @@ public class ConfigManager {
for (var obsoleteScope : obsoleteScopes) {
removeModuleScopeWithoutCache(obsoleteScope);
}
cleanModuleDexes();
checkCachedApkFile();
}
Log.d(TAG, "cached modules");
for (int uid : cachedModule.keySet()) {
@ -507,26 +512,35 @@ public class ConfigManager {
app.userId = cursor.getInt(userIdIdx);
// system server always loads database
if (app.packageName.equals("android")) continue;
String apk_path = cursor.getString(apkPathIdx);
String module_pkg = cursor.getString(modulePkgNameIdx);
try {
List<ProcessScope> processesScope = getAssociatedProcesses(app);
if (processesScope.isEmpty()) {
obsoletePackages.add(app);
continue;
}
var apkPath = cursor.getString(apkPathIdx);
var modulePackageName = cursor.getString(modulePkgNameIdx);
var file = getCachedApkFile(apkPath);
Module module;
if (file != null) {
module = new Module();
module.packageName = modulePackageName;
module.apkPath = apkPath;
module.file = file;
} else {
Log.w(TAG, "Can not load " + apkPath + ", skip!");
continue;
}
for (ProcessScope processScope : processesScope) {
var module = new Module();
var config = new ModuleConfig();
config.preLoadedDexes = getModuleDexes(apk_path);
module.name = module_pkg;
module.apk = apk_path;
module.config = config;
cachedScope.computeIfAbsent(processScope, ignored -> new LinkedList<>()).add(module);
if (module_pkg.equals(app.packageName)) {
cachedScope.computeIfAbsent(processScope,
ignored -> new LinkedList<>()).add(module);
// Always allow the module to inject itself
if (modulePackageName.equals(app.packageName)) {
var appId = processScope.uid % PER_USER_RANGE;
for (var user : UserService.getUsers()) {
cachedScope.computeIfAbsent(new ProcessScope(processScope.processName, user.id * PER_USER_RANGE + appId),
var moduleUid = user.id * PER_USER_RANGE + appId;
var moduleSelf = new ProcessScope(processScope.processName, moduleUid);
cachedScope.computeIfAbsent(moduleSelf,
ignored -> new LinkedList<>()).add(module);
}
}
@ -543,13 +557,11 @@ public class ConfigManager {
Log.d(TAG, "cached Scope");
cachedScope.forEach((ps, modules) -> {
Log.d(TAG, ps.processName + "/" + ps.uid);
modules.forEach(module -> Log.d(TAG, "\t" + module.name));
modules.forEach(module -> Log.d(TAG, "\t" + module.packageName));
});
}
private List<SharedMemory> loadModuleDexes(String path) {
var sharedMemories = new ArrayList<SharedMemory>();
try (var apkFile = new ZipFile(path)) {
private void readDexes(ZipFile apkFile, List<SharedMemory> preLoadedDexes) {
int secondary = 2;
for (var dexFile = apkFile.getEntry("classes.dex"); dexFile != null;
dexFile = apkFile.getEntry("classes" + secondary + ".dex"), secondary++) {
@ -559,27 +571,62 @@ public class ConfigManager {
Channels.newChannel(in).read(byteBuffer);
SharedMemory.unmap(byteBuffer);
memory.setProtect(OsConstants.PROT_READ);
sharedMemories.add(memory);
preLoadedDexes.add(memory);
} catch (IOException | ErrnoException e) {
Log.w(TAG, "Can not load " + dexFile + " in " + path, e);
Log.w(TAG, "Can not load " + dexFile + " in " + apkFile, e);
}
}
}
private void readName(ZipFile apkFile, String initName, List<String> names) {
var initEntry = apkFile.getEntry(initName);
if (initEntry == null) return;
try (var in = apkFile.getInputStream(initEntry)) {
var reader = new BufferedReader(new InputStreamReader(in));
String name;
while ((name = reader.readLine()) != null) {
name = name.trim();
if (name.isEmpty() || name.startsWith("#")) continue;
names.add(name);
}
} catch (IOException e) {
Log.e(TAG, "Can not open " + initEntry, e);
}
}
@Nullable
private PreLoadedApk loadModule(String path) {
var file = new PreLoadedApk();
var preLoadedDexes = new ArrayList<SharedMemory>();
var moduleClassNames = new ArrayList<String>(1);
var moduleLibraryNames = new ArrayList<String>(1);
try (var apkFile = new ZipFile(path)) {
readDexes(apkFile, preLoadedDexes);
readName(apkFile, "assets/xposed_init", moduleClassNames);
readName(apkFile, "assets/native_init", moduleLibraryNames);
} catch (IOException e) {
Log.e(TAG, "Can not open " + path, e);
return null;
}
return sharedMemories;
if (preLoadedDexes.isEmpty()) return null;
if (moduleClassNames.isEmpty()) return null;
file.preLoadedDexes = preLoadedDexes;
file.moduleClassNames = moduleClassNames;
file.moduleLibraryNames = moduleLibraryNames;
return file;
}
private List<SharedMemory> getModuleDexes(String path) {
return cachedDexes.computeIfAbsent(path, this::loadModuleDexes);
@Nullable
private PreLoadedApk getCachedApkFile(String path) {
return cachedApkFile.computeIfAbsent(path, this::loadModule);
}
private void cleanModuleDexes() {
cachedDexes.entrySet().removeIf(entry -> {
private void checkCachedApkFile() {
cachedApkFile.entrySet().removeIf(entry -> {
var path = entry.getKey();
var dexes = entry.getValue();
var file = entry.getValue();
if (!new File(path).exists()) {
dexes.stream().parallel().forEach(SharedMemory::close);
file.preLoadedDexes.stream().parallel().forEach(SharedMemory::close);
return true;
}
return false;

View File

@ -176,7 +176,7 @@ public final class LspModuleClassLoader extends ByteBufferDexClassLoader {
super.toString() + "]";
}
public static LspModuleClassLoader loadApk(File apk,
public static LspModuleClassLoader loadApk(String apk,
List<SharedMemory> dexes,
String librarySearchPath,
ClassLoader parent) {
@ -191,9 +191,9 @@ public final class LspModuleClassLoader extends ByteBufferDexClassLoader {
LspModuleClassLoader cl;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
cl = new LspModuleClassLoader(dexBuffers, librarySearchPath,
parent, apk.getAbsolutePath());
parent, apk);
} else {
cl = new LspModuleClassLoader(dexBuffers, parent, apk.getAbsolutePath());
cl = new LspModuleClassLoader(dexBuffers, parent, apk);
cl.initNativeLibraryDirs(librarySearchPath);
}
Arrays.stream(dexBuffers).parallel().forEach(SharedMemory::unmap);