From d57d6024203197f3b0aa46d70f5ce20b1770ee55 Mon Sep 17 00:00:00 2001 From: solohsu Date: Fri, 25 Jan 2019 22:08:42 +0800 Subject: [PATCH] Fix memory issues when hooking too many methods in one process --- .../com/elderdrivers/riru/xposed/Main.java | 13 +++- .../riru/xposed/dexmaker/DynamicBridge.java | 54 ++++++++++----- .../riru/xposed/dexmaker/HookerDexMaker.java | 11 ++- .../entry/hooker/HandleBindAppHooker.java | 6 ++ .../hooker/LoadedApkConstructorHooker.java | 13 +++- .../entry/hooker/XposedBlackListHooker.java | 2 +- .../riru/xposed/util/FileUtils.java | 64 +++++++++++++++++ .../riru/xposed/util/ProcessUtils.java | 69 +++++++++++++++++++ Core/jni/main/yahfa/trampoline.h | 2 +- 9 files changed, 206 insertions(+), 28 deletions(-) create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/util/FileUtils.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/util/ProcessUtils.java diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/Main.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/Main.java index c5978284..348dbf4e 100644 --- a/Bridge/src/main/java/com/elderdrivers/riru/xposed/Main.java +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/Main.java @@ -9,13 +9,22 @@ import com.elderdrivers.riru.xposed.entry.Router; import java.lang.reflect.Method; +import static com.elderdrivers.riru.xposed.util.FileUtils.getDataPathPrefix; + @SuppressLint("DefaultLocale") public class Main implements KeepAll { + /** + * When set to true, install bootstrap hooks and loadModules + * for each process when it starts. + * This means you can deactivate or activate every module + * for the process you restart without rebooting. + */ + private static final boolean DYNAMIC_LOAD_MODULES = false; // private static String sForkAndSpecializePramsStr = ""; // private static String sForkSystemServerPramsStr = ""; public static String sAppDataDir = ""; - private static final boolean DYNAMIC_LOAD_MODULES = false; + public static String sAppProcessName = ""; static { init(Build.VERSION.SDK_INT); @@ -66,7 +75,7 @@ public class Main implements KeepAll { // Utils.logD(sForkSystemServerPramsStr + " = " + pid); if (pid == 0) { // in system_server process - sAppDataDir = "/data/data/android/"; + sAppDataDir = getDataPathPrefix() + "android/"; Router.onProcessForked(true); } else { // in zygote process, res is child zygote pid diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/DynamicBridge.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/DynamicBridge.java index b8411021..8246b3da 100644 --- a/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/DynamicBridge.java +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/DynamicBridge.java @@ -1,9 +1,9 @@ package com.elderdrivers.riru.xposed.dexmaker; import android.app.AndroidAppHelper; -import android.os.Build; import com.elderdrivers.riru.xposed.Main; +import com.elderdrivers.riru.xposed.util.FileUtils; import java.io.File; import java.lang.reflect.Constructor; @@ -12,12 +12,29 @@ import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.HashMap; +import java.util.concurrent.atomic.AtomicBoolean; import de.robv.android.xposed.XposedBridge; +import static com.elderdrivers.riru.xposed.dexmaker.HookerDexMaker.SHOULD_USE_IN_MEMORY_DEX; +import static com.elderdrivers.riru.xposed.util.FileUtils.getDataPathPrefix; + public final class DynamicBridge { - private static HashMap hookedInfo = new HashMap<>(); + private static final HashMap hookedInfo = new HashMap<>(); + private static final HookerDexMaker dexMaker = new HookerDexMaker(); + private static final AtomicBoolean dexPathInited = new AtomicBoolean(false); + private static final File dexDir; + private static final File dexOptDir; + + static { + // we always choose to use device encrypted storage data on android n and later + // in case some app is installing hooks before phone is unlocked + String fixedAppDataDir = getDataPathPrefix() + AndroidAppHelper.currentPackageName() + "/"; + dexDir = new File(fixedAppDataDir, "/cache/edhookers/" + + Main.sAppProcessName.replace(":", "_") + "/"); + dexOptDir = new File(dexDir, "oat"); + } public static synchronized void hookMethod(Member hookMethod, XposedBridge.AdditionalHookInfo additionalHookInfo) { @@ -33,21 +50,24 @@ public final class DynamicBridge { DexLog.d("start to generate class for: " + hookMethod); try { // for Android Oreo and later use InMemoryClassLoader - String dexDirPath = ""; - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + if (!SHOULD_USE_IN_MEMORY_DEX) { // under Android Oreo, using DexClassLoader - String dataDir = Main.sAppDataDir; - String processName = AndroidAppHelper.currentProcessName(); - File dexDir = new File(dataDir, "cache/edhookers/" + processName + "/"); - dexDir.mkdirs(); - dexDirPath = dexDir.getAbsolutePath(); + if (dexPathInited.compareAndSet(false, true)) { + // delete previous compiled dex to prevent potential crashing + // TODO find a way to reuse them in consideration of performance + try { + dexDir.mkdirs(); + DexLog.d(Main.sAppProcessName + " deleting dir: " + dexOptDir.getAbsolutePath()); + FileUtils.delete(dexOptDir); + } catch (Throwable throwable) { + } + } } - HookerDexMaker dexMaker = new HookerDexMaker(); dexMaker.start(hookMethod, additionalHookInfo, - hookMethod.getDeclaringClass().getClassLoader(), dexDirPath); - hookedInfo.put(hookMethod, dexMaker); + hookMethod.getDeclaringClass().getClassLoader(), dexDir.getAbsolutePath()); + hookedInfo.put(hookMethod, dexMaker.getCallBackupMethod()); } catch (Exception e) { - DexLog.e("error occur when generating dex", e); + DexLog.e("error occur when generating dex. dexDir=" + dexDir, e); } } @@ -71,13 +91,9 @@ public final class DynamicBridge { public static Object invokeOriginalMethod(Member method, Object thisObject, Object[] args) throws InvocationTargetException, IllegalAccessException { - HookerDexMaker dexMaker = hookedInfo.get(method); - if (dexMaker == null) { - throw new IllegalStateException("method not hooked, cannot call original method."); - } - Method callBackup = dexMaker.getCallBackupMethod(); + Method callBackup = hookedInfo.get(method); if (callBackup == null) { - throw new IllegalStateException("original method is null, something must be wrong!"); + throw new IllegalStateException("method not hooked, cannot call original method."); } if (!Modifier.isStatic(callBackup.getModifiers())) { throw new IllegalStateException("original method is not static, something must be wrong!"); diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/HookerDexMaker.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/HookerDexMaker.java index 97d9aba3..acfcbe1a 100644 --- a/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/HookerDexMaker.java +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/HookerDexMaker.java @@ -34,10 +34,17 @@ import static com.elderdrivers.riru.xposed.dexmaker.DexMakerUtils.getObjTypeIdIf public class HookerDexMaker { + public static final boolean IN_MEMORY_DEX_ELIGIBLE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + // using InMemoryDexClassLoader when too many methods (about >175 ?) + // are to hook might lead to large memory allocation and gc problems, forbid it for now + public static final boolean IN_MEMORY_DEX_FORBIDDEN = true; + public static final boolean SHOULD_USE_IN_MEMORY_DEX = IN_MEMORY_DEX_ELIGIBLE && !IN_MEMORY_DEX_FORBIDDEN; + public static final String METHOD_NAME_BACKUP = "backup"; public static final String METHOD_NAME_HOOK = "hook"; public static final String METHOD_NAME_CALL_BACKUP = "callBackup"; public static final String METHOD_NAME_SETUP = "setup"; + public static final TypeId objArrayTypeId = TypeId.get(Object[].class); private static final String CLASS_DESC_PREFIX = "L"; private static final String CLASS_NAME_PREFIX = "EdHooker"; private static final String FIELD_NAME_HOOK_INFO = "additionalHookInfo"; @@ -48,8 +55,6 @@ public class HookerDexMaker { private static final String CALLBACK_METHOD_NAME_BEFORE = "callBeforeHookedMethod"; private static final String CALLBACK_METHOD_NAME_AFTER = "callAfterHookedMethod"; private static final String PARAMS_METHOD_NAME_IS_EARLY_RETURN = "isEarlyReturn"; - - public static final TypeId objArrayTypeId = TypeId.get(Object[].class); private static final TypeId throwableTypeId = TypeId.get(Throwable.class); private static final TypeId memberTypeId = TypeId.get(Member.class); private static final TypeId callbackTypeId = TypeId.get(XC_MethodHook.class); @@ -194,7 +199,7 @@ public class HookerDexMaker { generateCallBackupMethod(); ClassLoader loader; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (SHOULD_USE_IN_MEMORY_DEX) { // in memory dex classloader byte[] dexBytes = mDexMaker.generate(); loader = new InMemoryDexClassLoader(ByteBuffer.wrap(dexBytes), mAppClassLoader); diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/HandleBindAppHooker.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/HandleBindAppHooker.java index 8663729a..6e9a1709 100644 --- a/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/HandleBindAppHooker.java +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/HandleBindAppHooker.java @@ -7,6 +7,8 @@ import android.content.pm.ApplicationInfo; import android.content.res.CompatibilityInfo; import com.elderdrivers.riru.common.KeepMembers; +import com.elderdrivers.riru.xposed.Main; +import com.elderdrivers.riru.xposed.util.Utils; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.callbacks.XC_LoadPackage; @@ -36,7 +38,11 @@ public class HandleBindAppHooker implements KeepMembers { logD("ActivityThread#handleBindApplication() starts"); ActivityThread activityThread = (ActivityThread) thiz; ApplicationInfo appInfo = (ApplicationInfo) getObjectField(bindData, "appInfo"); + // save app process name here for later use + Main.sAppProcessName = (String) getObjectField(bindData, "processName"); String reportedPackageName = appInfo.packageName.equals("android") ? "system" : appInfo.packageName; + Utils.logD("processName=" + Main.sAppProcessName + + ", packageName=" + reportedPackageName + ", appDataDir=" + Main.sAppDataDir); if (XposedBlackListHooker.shouldDisableHooks(reportedPackageName)) { return; diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/LoadedApkConstructorHooker.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/LoadedApkConstructorHooker.java index 9840a87a..01ccbab7 100644 --- a/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/LoadedApkConstructorHooker.java +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/LoadedApkConstructorHooker.java @@ -5,6 +5,7 @@ import android.app.AndroidAppHelper; import android.app.LoadedApk; import android.content.pm.ApplicationInfo; import android.content.res.CompatibilityInfo; +import android.util.Log; import com.elderdrivers.riru.common.KeepMembers; @@ -58,13 +59,21 @@ public class LoadedApkConstructorHooker implements KeepMembers { return; } + // mIncludeCode checking should go ahead of loadedPackagesInProcess added checking + if (!getBooleanField(loadedApk, "mIncludeCode")) { + logD("LoadedApk# mIncludeCode == false: " + mAppDir); + return; + } + if (!loadedPackagesInProcess.add(packageName)) { logD("LoadedApk# has been loaded before, skip: " + mAppDir); return; } - if (!getBooleanField(loadedApk, "mIncludeCode")) { - logD("LoadedApk# mIncludeCode == false: " + mAppDir); + // OnePlus magic... + if (Log.getStackTraceString(new Throwable()). + contains("android.app.ActivityThread$ApplicationThread.schedulePreload")) { + logD("LoadedApk# maybe oneplus's custom opt, skip"); return; } diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/XposedBlackListHooker.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/XposedBlackListHooker.java index 50c81f63..083a4a92 100644 --- a/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/XposedBlackListHooker.java +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/XposedBlackListHooker.java @@ -18,13 +18,13 @@ import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XSharedPreferences; import de.robv.android.xposed.XposedBridge; +import static com.elderdrivers.riru.xposed.util.FileUtils.IS_USING_PROTECTED_STORAGE; import static de.robv.android.xposed.XposedHelpers.findAndHookMethod; import static de.robv.android.xposed.XposedInit.INSTALLER_PACKAGE_NAME; public class XposedBlackListHooker { public static final String BLACK_LIST_PACKAGE_NAME = "com.flarejune.xposedblacklist"; - private static final boolean IS_USING_PROTECTED_STORAGE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; private static final String BLACK_LIST_PREF_NAME = "list"; private static final String PREF_KEY_BLACK_LIST = "blackList"; public static final String PREF_FILE_PATH = (IS_USING_PROTECTED_STORAGE ? "/data/user_de/0/" : "/data/data") diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/util/FileUtils.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/util/FileUtils.java new file mode 100644 index 00000000..e85a66d4 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/util/FileUtils.java @@ -0,0 +1,64 @@ +package com.elderdrivers.riru.xposed.util; + +import android.os.Build; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; + +public class FileUtils { + + public static final boolean IS_USING_PROTECTED_STORAGE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; + + /** + * Delete a file or a directory and its children. + * + * @param file The directory to delete. + * @throws IOException Exception when problem occurs during deleting the directory. + */ + public static void delete(File file) throws IOException { + + for (File childFile : file.listFiles()) { + + if (childFile.isDirectory()) { + delete(childFile); + } else { + if (!childFile.delete()) { + throw new IOException(); + } + } + } + + if (!file.delete()) { + throw new IOException(); + } + } + + public static String readLine(File file) { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + return reader.readLine(); + } catch (Throwable throwable) { + return ""; + } + } + + public static void writeLine(File file, String line) { + try { + file.createNewFile(); + } catch (IOException ex) { + } + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + writer.write(line); + writer.flush(); + } catch (Throwable throwable) { + Utils.logE("error writing line to file " + file + ": " + throwable.getMessage()); + } + } + + public static String getDataPathPrefix() { + return IS_USING_PROTECTED_STORAGE ? "/data/user_de/0/" : "/data/data/"; + } +} diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/util/ProcessUtils.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/util/ProcessUtils.java new file mode 100644 index 00000000..b7c474e2 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/util/ProcessUtils.java @@ -0,0 +1,69 @@ +package com.elderdrivers.riru.xposed.util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; + +public class ProcessUtils { + + /** + * a common solution from https://stackoverflow.com/a/21389402 + *

+ * use {@link com.elderdrivers.riru.xposed.Main#sAppProcessName} to get current process name + */ + public static String getProcessName(int pid) { + BufferedReader cmdlineReader = null; + try { + cmdlineReader = new BufferedReader(new InputStreamReader( + new FileInputStream( + "/proc/" + pid + "/cmdline"), + "iso-8859-1")); + int c; + StringBuilder processName = new StringBuilder(); + while ((c = cmdlineReader.read()) > 0) { + processName.append((char) c); + } + return processName.toString(); + } catch (Throwable throwable) { + Utils.logW("getProcessName: " + throwable.getMessage()); + } finally { + try { + if (cmdlineReader != null) { + cmdlineReader.close(); + } + } catch (Throwable throwable) { + Utils.logE("getProcessName: " + throwable.getMessage()); + } + } + return ""; + } + + public static boolean isLastPidAlive(File lastPidFile) { + String lastPidInfo = FileUtils.readLine(lastPidFile); + try { + String[] split = lastPidInfo.split(":", 2); + return checkProcessAlive(Integer.parseInt(split[0]), split[1]); + } catch (Throwable throwable) { + Utils.logW("error when check last pid " + lastPidFile + ": " + throwable.getMessage()); + return false; + } + } + + public static void saveLastPidInfo(File lastPidFile, int pid, String processName) { + try { + if (!lastPidFile.exists()) { + lastPidFile.getParentFile().mkdirs(); + lastPidFile.createNewFile(); + } + } catch (Throwable throwable) { + } + FileUtils.writeLine(lastPidFile, pid + ":" + processName); + } + + public static boolean checkProcessAlive(int pid, String processName) { + String existsPrcName = getProcessName(pid); + Utils.logW("checking pid alive: " + pid + ", " + processName + ", processName=" + existsPrcName); + return existsPrcName.equals(processName); + } +} diff --git a/Core/jni/main/yahfa/trampoline.h b/Core/jni/main/yahfa/trampoline.h index ba7a46b3..69fd7458 100644 --- a/Core/jni/main/yahfa/trampoline.h +++ b/Core/jni/main/yahfa/trampoline.h @@ -17,6 +17,6 @@ int doInitHookCap(unsigned int cap); void setupTrampoline(); void *genTrampoline(void *hookMethod); -#define DEFAULT_CAP 100 //size of each trampoline area would be no more than 4k Bytes(one page) +#define DEFAULT_CAP 1 //size of each trampoline area would be no more than 4k Bytes(one page) #endif //YAHFA_TAMPOLINE_H