diff --git a/app/src/main/java/org/lsposed/manager/ConfigManager.java b/app/src/main/java/org/lsposed/manager/ConfigManager.java index 79a1236a7..9be97ba5e 100644 --- a/app/src/main/java/org/lsposed/manager/ConfigManager.java +++ b/app/src/main/java/org/lsposed/manager/ConfigManager.java @@ -63,7 +63,7 @@ public static String getXposedVersionName() { } } - public static int getXposedVersionCode() { + public static long getXposedVersionCode() { try { return LSPManagerServiceHolder.getService().getXposedVersionCode(); } catch (RemoteException e) { diff --git a/core/.gitignore b/core/.gitignore deleted file mode 100644 index 035d07e58..000000000 --- a/core/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/build -/.cxx -/src/main/jni/src/config.cpp diff --git a/core/build.gradle.kts b/core/build.gradle.kts deleted file mode 100644 index 5e6695814..000000000 --- a/core/build.gradle.kts +++ /dev/null @@ -1,35 +0,0 @@ -val versionCodeProvider: Provider by rootProject.extra -val versionNameProvider: Provider by rootProject.extra - -plugins { alias(libs.plugins.agp.lib) } - -android { - namespace = "org.lsposed.lspd.core" - - androidResources { enable = false } - - defaultConfig { - consumerProguardFiles("proguard-rules.pro") - buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""") - buildConfigField("String", "VERSION_NAME", """"${versionCodeProvider.get()}"""") - buildConfigField("long", "VERSION_CODE", versionCodeProvider.get()) - } - - buildTypes { - release { - isMinifyEnabled = true - proguardFiles("proguard-rules.pro") - } - } -} - -dependencies { - api(projects.xposed) - implementation(projects.external.apache) - implementation(projects.external.axml) - implementation(projects.hiddenapi.bridge) - implementation(projects.services.daemonService) - implementation(projects.services.managerService) - compileOnly(libs.androidx.annotation) - compileOnly(projects.hiddenapi.stubs) -} diff --git a/core/proguard-rules.pro b/core/proguard-rules.pro deleted file mode 100644 index 844704e3c..000000000 --- a/core/proguard-rules.pro +++ /dev/null @@ -1,37 +0,0 @@ --keep class android.** { *; } --keep class de.robv.android.xposed.** {*;} --keep class io.github.libxposed.** {*;} --keep class org.lsposed.lspd.core.* {*;} --keep class org.lsposed.lspd.hooker.HandleSystemServerProcessHooker {*;} --keep class org.lsposed.lspd.hooker.HandleSystemServerProcessHooker$Callback {*;} --keep class org.lsposed.lspd.impl.LSPosedBridge$NativeHooker {*;} --keep class org.lsposed.lspd.impl.LSPosedBridge$HookerCallback {*;} --keep class org.lsposed.lspd.util.Hookers {*;} - --keepnames class org.lsposed.lspd.impl.LSPosedHelper { - public ; -} - --keepattributes RuntimeVisibleAnnotations --keepclasseswithmembers,includedescriptorclasses class * { - native ; -} --keepclassmembers class org.lsposed.lspd.impl.LSPosedContext { - public ; -} --keepclassmembers class org.lsposed.lspd.impl.LSPosedHookCallback { - public ; -} --keepclassmembers,allowoptimization class ** implements io.github.libxposed.api.XposedInterface$Hooker { - public static *** before(); - public static *** before(io.github.libxposed.api.XposedInterface$BeforeHookCallback); - public static void after(); - public static void after(io.github.libxposed.api.XposedInterface$AfterHookCallback); - public static void after(io.github.libxposed.api.XposedInterface$AfterHookCallback, ***); -} --assumenosideeffects class android.util.Log { - public static *** v(...); - public static *** d(...); -} --repackageclasses --allowaccessmodification diff --git a/core/src/main/java/de/robv/android/xposed/IXposedMod.java b/core/src/main/java/de/robv/android/xposed/IXposedMod.java deleted file mode 100644 index 319fa560f..000000000 --- a/core/src/main/java/de/robv/android/xposed/IXposedMod.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package de.robv.android.xposed; - -/** - * Marker interface for Xposed modules. Cannot be implemented directly. - */ -/* package */ interface IXposedMod { -} diff --git a/core/src/main/java/org/lsposed/lspd/core/ApplicationServiceClient.java b/core/src/main/java/org/lsposed/lspd/core/ApplicationServiceClient.java deleted file mode 100644 index 6eb01d0b3..000000000 --- a/core/src/main/java/org/lsposed/lspd/core/ApplicationServiceClient.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.core; - -import android.os.Bundle; -import android.os.IBinder; -import android.os.ParcelFileDescriptor; -import android.os.RemoteException; - -import androidx.annotation.NonNull; - -import org.lsposed.lspd.models.Module; -import org.lsposed.lspd.service.ILSPApplicationService; -import org.lsposed.lspd.util.Utils; - -import java.util.Collections; -import java.util.List; - -public class ApplicationServiceClient implements ILSPApplicationService, IBinder.DeathRecipient { - public static ApplicationServiceClient serviceClient = null; - - final ILSPApplicationService service; - - final String processName; - - private ApplicationServiceClient(@NonNull ILSPApplicationService service, @NonNull String processName) throws RemoteException { - this.service = service; - this.processName = processName; - this.service.asBinder().linkToDeath(this, 0); - } - - synchronized static void Init(ILSPApplicationService service, String niceName) { - var binder = service.asBinder(); - if (serviceClient == null && binder != null) { - try { - serviceClient = new ApplicationServiceClient(service, niceName); - } catch (RemoteException e) { - Utils.logE("link to death error: ", e); - } - } - } - - @Override - public boolean isLogMuted() { - try { - return service.isLogMuted(); - } catch (RemoteException | NullPointerException ignored) { - } - return false; - } - - @Override - public List getLegacyModulesList() { - try { - return service.getLegacyModulesList(); - } catch (RemoteException | NullPointerException ignored) { - } - return Collections.emptyList(); - } - - @Override - public List getModulesList() { - try { - return service.getModulesList(); - } catch (RemoteException | NullPointerException ignored) { - } - return Collections.emptyList(); - } - - @Override - public String getPrefsPath(String packageName) { - try { - return service.getPrefsPath(packageName); - } catch (RemoteException | NullPointerException ignored) { - } - return null; - } - - @Override - public ParcelFileDescriptor requestInjectedManagerBinder(List binder) { - try { - return service.requestInjectedManagerBinder(binder); - } catch (RemoteException | NullPointerException ignored) { - } - return null; - } - - @Override - public IBinder asBinder() { - return service.asBinder(); - } - - @Override - public void binderDied() { - service.asBinder().unlinkToDeath(this, 0); - serviceClient = null; - } -} diff --git a/core/src/main/java/org/lsposed/lspd/core/Startup.java b/core/src/main/java/org/lsposed/lspd/core/Startup.java deleted file mode 100644 index 3970de86d..000000000 --- a/core/src/main/java/org/lsposed/lspd/core/Startup.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -package org.lsposed.lspd.core; - -import android.app.ActivityThread; -import android.app.LoadedApk; -import android.content.pm.ApplicationInfo; -import android.content.res.CompatibilityInfo; -import android.os.IBinder; - -import com.android.internal.os.ZygoteInit; - -import org.lsposed.lspd.deopt.PrebuiltMethodsDeopter; -import org.lsposed.lspd.hooker.AttachHooker; -import org.lsposed.lspd.hooker.CrashDumpHooker; -import org.lsposed.lspd.hooker.HandleSystemServerProcessHooker; -import org.lsposed.lspd.hooker.LoadedApkCtorHooker; -import org.lsposed.lspd.hooker.LoadedApkCreateCLHooker; -import org.lsposed.lspd.hooker.OpenDexFileHooker; -import org.lsposed.lspd.hooker.StartBootstrapServicesHooker; -import org.lsposed.lspd.impl.LSPosedContext; -import org.lsposed.lspd.impl.LSPosedHelper; -import org.lsposed.lspd.service.ILSPApplicationService; -import org.lsposed.lspd.util.Utils; - -import java.util.List; - -import dalvik.system.DexFile; -import de.robv.android.xposed.XposedBridge; -import de.robv.android.xposed.XposedInit; - -public class Startup { - private static void startBootstrapHook(boolean isSystem) { - Utils.logD("startBootstrapHook starts: isSystem = " + isSystem); - LSPosedHelper.hookMethod(CrashDumpHooker.class, Thread.class, "dispatchUncaughtException", Throwable.class); - if (isSystem) { - LSPosedHelper.hookAllMethods(HandleSystemServerProcessHooker.class, ZygoteInit.class, "handleSystemServerProcess"); - } else { - LSPosedHelper.hookAllMethods(OpenDexFileHooker.class, DexFile.class, "openDexFile"); - LSPosedHelper.hookAllMethods(OpenDexFileHooker.class, DexFile.class, "openInMemoryDexFile"); - LSPosedHelper.hookAllMethods(OpenDexFileHooker.class, DexFile.class, "openInMemoryDexFiles"); - } - LSPosedHelper.hookConstructor(LoadedApkCtorHooker.class, LoadedApk.class, - ActivityThread.class, ApplicationInfo.class, CompatibilityInfo.class, - ClassLoader.class, boolean.class, boolean.class, boolean.class); - LSPosedHelper.hookMethod(LoadedApkCreateCLHooker.class, LoadedApk.class, "createOrUpdateClassLoaderLocked", List.class); - LSPosedHelper.hookAllMethods(AttachHooker.class, ActivityThread.class, "attach"); - } - - public static void bootstrapXposed(boolean systemServerStarted) { - // Initialize the Xposed framework - try { - startBootstrapHook(XposedInit.startsSystemServer); - XposedInit.loadLegacyModules(); - } catch (Throwable t) { - Utils.logE("error during Xposed initialization", t); - } - - if (systemServerStarted) { - Utils.logD("Manually triggering system_server module load for late injection"); - - IBinder activityService = android.os.ServiceManager.getService("activity"); - if (activityService == null) { - Utils.logE("Activity service not found! Cannot get SystemServer ClassLoader."); - return; - } - - // Maintain state consistency for the rest of the Vector framework - HandleSystemServerProcessHooker.systemServerCL = activityService.getClass().getClassLoader(); - HandleSystemServerProcessHooker.after(); - StartBootstrapServicesHooker.before(); - - Utils.logI("Late system_server injection successfully completed."); - } - } - - public static void initXposed(boolean isSystem, String processName, String appDir, ILSPApplicationService service) { - // init logger - ApplicationServiceClient.Init(service, processName); - XposedBridge.initXResources(); - XposedInit.startsSystemServer = isSystem; - LSPosedContext.isSystemServer = isSystem; - LSPosedContext.appDir = appDir; - LSPosedContext.processName = processName; - PrebuiltMethodsDeopter.deoptBootMethods(); // do it once for secondary zygote - } -} diff --git a/core/src/main/java/org/lsposed/lspd/deopt/InlinedMethodCallers.java b/core/src/main/java/org/lsposed/lspd/deopt/InlinedMethodCallers.java deleted file mode 100644 index 4b7f999b7..000000000 --- a/core/src/main/java/org/lsposed/lspd/deopt/InlinedMethodCallers.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.deopt; - -import android.app.Instrumentation; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.res.AssetManager; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.util.DisplayMetrics; -import android.util.TypedValue; - -import java.util.HashMap; - -/** - * Providing a whitelist of methods which are the callers of the target methods we want to hook. - * Because the target methods are inlined into the callers, we deoptimize the callers to - * run in intercept mode to make target methods hookable. - *

- * Only for methods which are included in pre-compiled framework codes. - * TODO recompile system apps and priv-apps since their original dex files are available - */ -public class InlinedMethodCallers { - - public static final String KEY_BOOT_IMAGE = "boot_image"; - public static final String KEY_BOOT_IMAGE_MIUI_RES = "boot_image_miui_res"; - public static final String KEY_SYSTEM_SERVER = "system_server"; - - /** - * Key should be {@link #KEY_BOOT_IMAGE}, {@link #KEY_SYSTEM_SERVER}, or a package name - * of system apps or priv-apps i.e. com.android.systemui - */ - private static final HashMap CALLERS = new HashMap<>(); - - /** - * format for each row: {className, methodName, methodSig} - */ - private static final Object[][] BOOT_IMAGE = { - // callers of Application#attach(Context) - {"android.app.Instrumentation", "newApplication", ClassLoader.class, String.class, Context.class}, - {"android.app.Instrumentation", "newApplication", ClassLoader.class, Context.class}, - - // callers of Instrumentation#newApplication(ClassLoader, String, Context) - {"android.app.LoadedApk", "makeApplicationInner", Boolean.TYPE, Instrumentation.class, Boolean.TYPE}, - {"android.app.LoadedApk", "makeApplicationInner", Boolean.TYPE, Instrumentation.class}, - {"android.app.LoadedApk", "makeApplication", Boolean.TYPE, Instrumentation.class}, - - {"android.app.ContextImpl", "getSharedPreferencesPath", String.class} - }; - - // TODO deprecate this - private static final Object[][] BOOT_IMAGE_FOR_MIUI_RES = { - // for MIUI resources hooking - {"android.content.res.MiuiResources", "init", String.class}, - {"android.content.res.MiuiResources", "updateMiuiImpl"}, - {"android.content.res.MiuiResources", "setImpl", "android.content.res.ResourcesImpl"}, - {"android.content.res.MiuiResources", "loadOverlayValue", TypedValue.class, int.class}, - {"android.content.res.MiuiResources", "getThemeString", CharSequence.class}, - {"android.content.res.MiuiResources", "", ClassLoader.class}, - {"android.content.res.MiuiResources", ""}, - {"android.content.res.MiuiResources", "", AssetManager.class, DisplayMetrics.class, Configuration.class}, - {"android.miui.ResourcesManager", "initMiuiResource", Resources.class, String.class}, - {"android.app.LoadedApk", "getResources", Resources.class}, - {"android.content.res.Resources", "getSystem", Resources.class}, - {"android.app.ApplicationPackageManager", "getResourcesForApplication", ApplicationInfo.class}, - {"android.app.ContextImpl", "setResources", Resources.class}, - }; - - private static final Object[][] SYSTEM_SERVER = {}; - - private static final Object[][] SYSTEM_UI = {}; - - static { - CALLERS.put(KEY_BOOT_IMAGE, BOOT_IMAGE); - CALLERS.put(KEY_BOOT_IMAGE_MIUI_RES, BOOT_IMAGE_FOR_MIUI_RES); - CALLERS.put(KEY_SYSTEM_SERVER, SYSTEM_SERVER); - CALLERS.put("com.android.systemui", SYSTEM_UI); - } - - public static HashMap getAll() { - return CALLERS; - } - - public static Object[][] get(String where) { - return CALLERS.get(where); - } -} diff --git a/core/src/main/java/org/lsposed/lspd/deopt/PrebuiltMethodsDeopter.java b/core/src/main/java/org/lsposed/lspd/deopt/PrebuiltMethodsDeopter.java deleted file mode 100644 index ad0b104cc..000000000 --- a/core/src/main/java/org/lsposed/lspd/deopt/PrebuiltMethodsDeopter.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.deopt; - -import static org.lsposed.lspd.deopt.InlinedMethodCallers.KEY_BOOT_IMAGE; -import static org.lsposed.lspd.deopt.InlinedMethodCallers.KEY_BOOT_IMAGE_MIUI_RES; -import static org.lsposed.lspd.deopt.InlinedMethodCallers.KEY_SYSTEM_SERVER; - -import org.matrix.vector.nativebridge.HookBridge; -import org.lsposed.lspd.util.Hookers; -import org.lsposed.lspd.util.Utils; - -import java.lang.reflect.Executable; -import java.util.Arrays; - -import de.robv.android.xposed.XposedHelpers; - -public class PrebuiltMethodsDeopter { - - public static void deoptMethods(String where, ClassLoader cl) { - Object[][] callers = InlinedMethodCallers.get(where); - if (callers == null) { - return; - } - for (Object[] caller : callers) { - try { - if (caller.length < 2) continue; - if (!(caller[0] instanceof String)) continue; - if (!(caller[1] instanceof String)) continue; - Executable method; - Object[] params = new Object[caller.length - 2]; - System.arraycopy(caller, 2, params, 0, params.length); - if ("".equals(caller[1])) { - method = XposedHelpers.findConstructorExactIfExists((String) caller[0], cl, params); - } else { - method = XposedHelpers.findMethodExactIfExists((String) caller[0], cl, (String) caller[1], params); - } - if (method != null) { - Hookers.logD("deoptimizing " + method); - HookBridge.deoptimizeMethod(method); - } - } catch (Throwable throwable) { - Utils.logE("error when deopting method: " + Arrays.toString(caller), throwable); - } - } - } - - public static void deoptBootMethods() { - // todo check if has been done before - deoptMethods(KEY_BOOT_IMAGE, null); - } - - public static void deoptResourceMethods() { - if (Utils.isMIUI) { - //deopt these only for MIUI - deoptMethods(KEY_BOOT_IMAGE_MIUI_RES, null); - } - } - - public static void deoptSystemServerMethods(ClassLoader sysCL) { - deoptMethods(KEY_SYSTEM_SERVER, sysCL); - } -} diff --git a/core/src/main/java/org/lsposed/lspd/hooker/AttachHooker.java b/core/src/main/java/org/lsposed/lspd/hooker/AttachHooker.java deleted file mode 100644 index 3e77ba156..000000000 --- a/core/src/main/java/org/lsposed/lspd/hooker/AttachHooker.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.lsposed.lspd.hooker; - -import android.app.ActivityThread; - -import de.robv.android.xposed.XposedInit; -import io.github.libxposed.api.XposedInterface; - -public class AttachHooker implements XposedInterface.Hooker { - - public static void after(XposedInterface.AfterHookCallback callback) { - XposedInit.loadModules((ActivityThread) callback.getThisObject()); - } -} diff --git a/core/src/main/java/org/lsposed/lspd/hooker/CrashDumpHooker.java b/core/src/main/java/org/lsposed/lspd/hooker/CrashDumpHooker.java deleted file mode 100644 index 53ab5c0c7..000000000 --- a/core/src/main/java/org/lsposed/lspd/hooker/CrashDumpHooker.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.lsposed.lspd.hooker; - -import org.lsposed.lspd.impl.LSPosedBridge; -import org.lsposed.lspd.util.Utils.Log; - -import io.github.libxposed.api.XposedInterface; - -public class CrashDumpHooker implements XposedInterface.Hooker { - - public static void before(XposedInterface.BeforeHookCallback callback) { - try { - var e = (Throwable) callback.getArgs()[0]; - LSPosedBridge.log("Crash unexpectedly: " + Log.getStackTraceString(e)); - } catch (Throwable ignored) { - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/hooker/HandleSystemServerProcessHooker.java b/core/src/main/java/org/lsposed/lspd/hooker/HandleSystemServerProcessHooker.java deleted file mode 100644 index ef93feb19..000000000 --- a/core/src/main/java/org/lsposed/lspd/hooker/HandleSystemServerProcessHooker.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.hooker; - -import android.annotation.SuppressLint; - -import org.lsposed.lspd.deopt.PrebuiltMethodsDeopter; -import org.lsposed.lspd.impl.LSPosedHelper; -import org.lsposed.lspd.util.Hookers; - -import io.github.libxposed.api.XposedInterface; - -// system_server initialization -public class HandleSystemServerProcessHooker implements XposedInterface.Hooker { - - public interface Callback { - void onSystemServerLoaded(ClassLoader classLoader); - } - - public static volatile ClassLoader systemServerCL = null; - public static volatile Callback callback = null; - - @SuppressLint("PrivateApi") - public static void after() { - Hookers.logD("ZygoteInit#handleSystemServerProcess() starts"); - try { - if (systemServerCL == null) { - // get system_server classLoader - systemServerCL = Thread.currentThread().getContextClassLoader(); - } - // deopt methods in SYSTEMSERVERCLASSPATH - PrebuiltMethodsDeopter.deoptSystemServerMethods(systemServerCL); - var clazz = Class.forName("com.android.server.SystemServer", false, systemServerCL); - LSPosedHelper.hookAllMethods(StartBootstrapServicesHooker.class, clazz, "startBootstrapServices"); - if (callback != null) callback.onSystemServerLoaded(systemServerCL); - } catch (Throwable t) { - Hookers.logE("error when hooking systemMain", t); - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/hooker/LoadedApkCreateCLHooker.java b/core/src/main/java/org/lsposed/lspd/hooker/LoadedApkCreateCLHooker.java deleted file mode 100644 index b5c1bec08..000000000 --- a/core/src/main/java/org/lsposed/lspd/hooker/LoadedApkCreateCLHooker.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.hooker; - -import static org.lsposed.lspd.core.ApplicationServiceClient.serviceClient; - -import android.annotation.SuppressLint; -import android.app.ActivityThread; -import android.app.LoadedApk; -import android.content.pm.ApplicationInfo; -import android.os.Build; - -import androidx.annotation.NonNull; - -import org.lsposed.lspd.impl.LSPosedContext; -import org.lsposed.lspd.util.Hookers; -import org.lsposed.lspd.util.MetaDataReader; -import org.lsposed.lspd.util.Utils; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Field; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import de.robv.android.xposed.XC_MethodHook; -import de.robv.android.xposed.XC_MethodReplacement; -import de.robv.android.xposed.XposedBridge; -import de.robv.android.xposed.XposedHelpers; -import de.robv.android.xposed.XposedInit; -import de.robv.android.xposed.callbacks.XC_LoadPackage; -import io.github.libxposed.api.XposedInterface; -import io.github.libxposed.api.XposedModuleInterface; - -@SuppressLint("BlockedPrivateApi") -public class LoadedApkCreateCLHooker implements XposedInterface.Hooker { - private final static Field defaultClassLoaderField; - - private final static Set loadedApks = ConcurrentHashMap.newKeySet(); - - static { - Field field = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - try { - field = LoadedApk.class.getDeclaredField("mDefaultClassLoader"); - field.setAccessible(true); - } catch (Throwable ignored) { - } - } - defaultClassLoaderField = field; - } - - static void addLoadedApk(LoadedApk loadedApk) { - loadedApks.add(loadedApk); - } - - public static void after(XposedInterface.AfterHookCallback callback) { - LoadedApk loadedApk = (LoadedApk) callback.getThisObject(); - - if (callback.getArgs()[0] != null || !loadedApks.contains(loadedApk)) { - return; - } - - try { - Hookers.logD("LoadedApk#createClassLoader starts"); - - String packageName = ActivityThread.currentPackageName(); - String processName = ActivityThread.currentProcessName(); - boolean isFirstPackage = packageName != null && processName != null && packageName.equals(loadedApk.getPackageName()); - if (!isFirstPackage) { - packageName = loadedApk.getPackageName(); - processName = ActivityThread.currentPackageName(); - } else if (packageName.equals("android")) { - packageName = "system"; - } - - Object mAppDir = XposedHelpers.getObjectField(loadedApk, "mAppDir"); - ClassLoader classLoader = (ClassLoader) XposedHelpers.getObjectField(loadedApk, "mClassLoader"); - Hookers.logD("LoadedApk#createClassLoader ends: " + mAppDir + " -> " + classLoader); - - if (classLoader == null) { - return; - } - - if (!isFirstPackage && !XposedHelpers.getBooleanField(loadedApk, "mIncludeCode")) { - Hookers.logD("LoadedApk# mIncludeCode == false: " + mAppDir); - return; - } - - if (!isFirstPackage && !XposedInit.getLoadedModules().getOrDefault(packageName, Optional.of("")).isPresent()) { - return; - } - - XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam( - XposedBridge.sLoadedPackageCallbacks); - lpparam.packageName = packageName; - lpparam.processName = processName; - lpparam.classLoader = classLoader; - lpparam.appInfo = loadedApk.getApplicationInfo(); - lpparam.isFirstApplication = isFirstPackage; - - if (isFirstPackage && XposedInit.getLoadedModules().getOrDefault(packageName, Optional.empty()).isPresent()) { - hookNewXSP(lpparam); - } - - Hookers.logD("Call handleLoadedPackage: packageName=" + lpparam.packageName + " processName=" + lpparam.processName + " isFirstPackage=" + isFirstPackage + " classLoader=" + lpparam.classLoader + " appInfo=" + lpparam.appInfo); - XC_LoadPackage.callAll(lpparam); - - LSPosedContext.callOnPackageLoaded(new XposedModuleInterface.PackageLoadedParam() { - @NonNull - @Override - public String getPackageName() { - return loadedApk.getPackageName(); - } - - @NonNull - @Override - public ApplicationInfo getApplicationInfo() { - return loadedApk.getApplicationInfo(); - } - - @NonNull - @Override - public ClassLoader getDefaultClassLoader() { - try { - return (ClassLoader) defaultClassLoaderField.get(loadedApk); - } catch (Throwable t) { - throw new IllegalStateException(t); - } - } - - @NonNull - @Override - public ClassLoader getClassLoader() { - return classLoader; - } - - @Override - public boolean isFirstPackage() { - return isFirstPackage; - } - }); - } catch (Throwable t) { - Hookers.logE("error when hooking LoadedApk#createClassLoader", t); - } finally { - loadedApks.remove(loadedApk); - } - } - - private static void hookNewXSP(XC_LoadPackage.LoadPackageParam lpparam) { - int xposedminversion = -1; - boolean xposedsharedprefs = false; - try { - Map metaData = MetaDataReader.getMetaData(new File(lpparam.appInfo.sourceDir)); - Object minVersionRaw = metaData.get("xposedminversion"); - if (minVersionRaw instanceof Integer) { - xposedminversion = (Integer) minVersionRaw; - } else if (minVersionRaw instanceof String) { - xposedminversion = MetaDataReader.extractIntPart((String) minVersionRaw); - } - xposedsharedprefs = metaData.containsKey("xposedsharedprefs"); - } catch (NumberFormatException | IOException e) { - Hookers.logE("ApkParser fails", e); - } - - if (xposedminversion > 92 || xposedsharedprefs) { - Utils.logI("New modules detected, hook preferences"); - XposedHelpers.findAndHookMethod("android.app.ContextImpl", lpparam.classLoader, "checkMode", int.class, new XC_MethodHook() { - @Override - protected void afterHookedMethod(MethodHookParam param) { - if (((int) param.args[0] & 1/*Context.MODE_WORLD_READABLE*/) != 0) { - param.setThrowable(null); - } - } - }); - XposedHelpers.findAndHookMethod("android.app.ContextImpl", lpparam.classLoader, "getPreferencesDir", new XC_MethodReplacement() { - @Override - protected Object replaceHookedMethod(MethodHookParam param) { - return new File(serviceClient.getPrefsPath(lpparam.packageName)); - } - }); - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/hooker/LoadedApkCtorHooker.java b/core/src/main/java/org/lsposed/lspd/hooker/LoadedApkCtorHooker.java deleted file mode 100644 index e0a4af96e..000000000 --- a/core/src/main/java/org/lsposed/lspd/hooker/LoadedApkCtorHooker.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.hooker; - -import android.app.LoadedApk; -import android.content.res.XResources; - -import org.lsposed.lspd.util.Hookers; -import org.lsposed.lspd.util.Utils.Log; - -import de.robv.android.xposed.XposedHelpers; -import de.robv.android.xposed.XposedInit; -import io.github.libxposed.api.XposedInterface; - -// when a package is loaded for an existing process, trigger the callbacks as well -public class LoadedApkCtorHooker implements XposedInterface.Hooker { - - public static void after(XposedInterface.AfterHookCallback callback) { - Hookers.logD("LoadedApk# starts"); - - try { - LoadedApk loadedApk = (LoadedApk) callback.getThisObject(); - assert loadedApk != null; - String packageName = loadedApk.getPackageName(); - Object mAppDir = XposedHelpers.getObjectField(loadedApk, "mAppDir"); - Hookers.logD("LoadedApk# ends: " + mAppDir); - - if (!XposedInit.disableResources) { - XResources.setPackageNameForResDir(packageName, loadedApk.getResDir()); - } - - if (packageName.equals("android")) { - if (XposedInit.startsSystemServer) { - Hookers.logD("LoadedApk# is android, skip: " + mAppDir); - return; - } else { - packageName = "system"; - } - } - - if (!XposedInit.loadedPackagesInProcess.add(packageName)) { - Hookers.logD("LoadedApk# has been loaded before, skip: " + mAppDir); - return; - } - - // OnePlus magic... - if (Log.getStackTraceString(new Throwable()). - contains("android.app.ActivityThread$ApplicationThread.schedulePreload")) { - Hookers.logD("LoadedApk# maybe oneplus's custom opt, skip"); - return; - } - - LoadedApkCreateCLHooker.addLoadedApk(loadedApk); - } catch (Throwable t) { - Hookers.logE("error when hooking LoadedApk.", t); - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/hooker/OpenDexFileHooker.java b/core/src/main/java/org/lsposed/lspd/hooker/OpenDexFileHooker.java deleted file mode 100644 index f50e14768..000000000 --- a/core/src/main/java/org/lsposed/lspd/hooker/OpenDexFileHooker.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.lsposed.lspd.hooker; - -import android.os.Build; - -import org.lsposed.lspd.impl.LSPosedBridge; -import org.matrix.vector.nativebridge.HookBridge; - -import io.github.libxposed.api.XposedInterface; - -public class OpenDexFileHooker implements XposedInterface.Hooker { - - public static void after(XposedInterface.AfterHookCallback callback) { - ClassLoader classLoader = null; - for (var arg : callback.getArgs()) { - if (arg instanceof ClassLoader) { - classLoader = (ClassLoader) arg; - } - } - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P && classLoader == null) { - classLoader = LSPosedBridge.class.getClassLoader(); - } - while (classLoader != null) { - if (classLoader == LSPosedBridge.class.getClassLoader()) { - HookBridge.setTrusted(callback.getResult()); - return; - } else { - classLoader = classLoader.getParent(); - } - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/hooker/StartBootstrapServicesHooker.java b/core/src/main/java/org/lsposed/lspd/hooker/StartBootstrapServicesHooker.java deleted file mode 100644 index 3594095fa..000000000 --- a/core/src/main/java/org/lsposed/lspd/hooker/StartBootstrapServicesHooker.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.hooker; - -import static org.lsposed.lspd.util.Utils.logD; - -import androidx.annotation.NonNull; - -import org.lsposed.lspd.impl.LSPosedContext; -import org.lsposed.lspd.util.Hookers; - -import de.robv.android.xposed.XposedBridge; -import de.robv.android.xposed.XposedInit; -import de.robv.android.xposed.callbacks.XC_LoadPackage; -import io.github.libxposed.api.XposedInterface; -import io.github.libxposed.api.XposedModuleInterface; - -public class StartBootstrapServicesHooker implements XposedInterface.Hooker { - - public static void before() { - logD("SystemServer#startBootstrapServices() starts"); - - try { - XposedInit.loadedPackagesInProcess.add("android"); - - XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks); - lpparam.packageName = "android"; - lpparam.processName = "android"; // it's actually system_server, but other functions return this as well - lpparam.classLoader = HandleSystemServerProcessHooker.systemServerCL; - lpparam.appInfo = null; - lpparam.isFirstApplication = true; - XC_LoadPackage.callAll(lpparam); - - LSPosedContext.callOnSystemServerLoaded(new XposedModuleInterface.SystemServerLoadedParam() { - @Override - @NonNull - public ClassLoader getClassLoader() { - return HandleSystemServerProcessHooker.systemServerCL; - } - }); - } catch (Throwable t) { - Hookers.logE("error when hooking startBootstrapServices", t); - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/impl/LSPosedBridge.java b/core/src/main/java/org/lsposed/lspd/impl/LSPosedBridge.java deleted file mode 100644 index 53e033af7..000000000 --- a/core/src/main/java/org/lsposed/lspd/impl/LSPosedBridge.java +++ /dev/null @@ -1,292 +0,0 @@ -package org.lsposed.lspd.impl; - -import androidx.annotation.NonNull; - -import org.matrix.vector.nativebridge.HookBridge; -import org.lsposed.lspd.util.Utils.Log; - -import java.lang.reflect.Executable; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; - -import de.robv.android.xposed.XposedBridge; -import io.github.libxposed.api.XposedInterface; -import io.github.libxposed.api.errors.HookFailedError; - -public class LSPosedBridge { - - private static final String TAG = "LSPosed-Bridge"; - - private static final String castException = "Return value's type from hook callback does not match the hooked method"; - - private static final Method getCause; - - static { - Method tmp; - try { - tmp = InvocationTargetException.class.getMethod("getCause"); - } catch (Throwable e) { - tmp = null; - } - getCause = tmp; - } - - public static class HookerCallback { - @NonNull - final Method beforeInvocation; - @NonNull - final Method afterInvocation; - - final int beforeParams; - final int afterParams; - - public HookerCallback(@NonNull Method beforeInvocation, @NonNull Method afterInvocation) { - this.beforeInvocation = beforeInvocation; - this.afterInvocation = afterInvocation; - this.beforeParams = beforeInvocation.getParameterCount(); - this.afterParams = afterInvocation.getParameterCount(); - } - } - - public static void log(String text) { - Log.i(TAG, text); - } - - public static void log(Throwable t) { - String logStr = Log.getStackTraceString(t); - Log.e(TAG, logStr); - } - - public static class NativeHooker { - private final Object params; - - private NativeHooker(Executable method) { - var isStatic = Modifier.isStatic(method.getModifiers()); - Object returnType; - if (method instanceof Method) { - returnType = ((Method) method).getReturnType(); - } else { - returnType = null; - } - params = new Object[]{ - method, - returnType, - isStatic, - }; - } - - // This method is quite critical. We should try not to use system methods to avoid - // endless recursive - public Object callback(Object[] args) throws Throwable { - LSPosedHookCallback callback = new LSPosedHookCallback<>(); - - var array = ((Object[]) params); - - var method = (T) array[0]; - var returnType = (Class) array[1]; - var isStatic = (Boolean) array[2]; - - callback.method = method; - - if (isStatic) { - callback.thisObject = null; - callback.args = args; - } else { - callback.thisObject = args[0]; - callback.args = new Object[args.length - 1]; - //noinspection ManualArrayCopy - for (int i = 0; i < args.length - 1; ++i) { - callback.args[i] = args[i + 1]; - } - } - - Object[][] callbacksSnapshot = HookBridge.callbackSnapshot(HookerCallback.class, method); - Object[] modernSnapshot = callbacksSnapshot[0]; - Object[] legacySnapshot = callbacksSnapshot[1]; - - if (modernSnapshot.length == 0 && legacySnapshot.length == 0) { - try { - return HookBridge.invokeOriginalMethod(method, callback.thisObject, callback.args); - } catch (InvocationTargetException ite) { - throw (Throwable) HookBridge.invokeOriginalMethod(getCause, ite); - } - } - - Object[] ctxArray = new Object[modernSnapshot.length]; - XposedBridge.LegacyApiSupport legacy = null; - - // call "before method" callbacks - int beforeIdx; - for (beforeIdx = 0; beforeIdx < modernSnapshot.length; beforeIdx++) { - try { - var hooker = (HookerCallback) modernSnapshot[beforeIdx]; - if (hooker.beforeParams == 0) { - ctxArray[beforeIdx] = hooker.beforeInvocation.invoke(null); - } else { - ctxArray[beforeIdx] = hooker.beforeInvocation.invoke(null, callback); - } - } catch (Throwable t) { - LSPosedBridge.log(t); - - // reset result (ignoring what the unexpectedly exiting callback did) - callback.setResult(null); - callback.isSkipped = false; - continue; - } - - if (callback.isSkipped) { - // skip remaining "before" callbacks and corresponding "after" callbacks - beforeIdx++; - break; - } - } - - if (!callback.isSkipped && legacySnapshot.length != 0) { - // TODO: Separate classloader - legacy = new XposedBridge.LegacyApiSupport<>(callback, legacySnapshot); - legacy.handleBefore(); - } - - // call original method if not requested otherwise - if (!callback.isSkipped) { - try { - var result = HookBridge.invokeOriginalMethod(method, callback.thisObject, callback.args); - callback.setResult(result); - } catch (InvocationTargetException e) { - var throwable = (Throwable) HookBridge.invokeOriginalMethod(getCause, e); - callback.setThrowable(throwable); - } - } - - // call "after method" callbacks - for (int afterIdx = beforeIdx - 1; afterIdx >= 0; afterIdx--) { - Object lastResult = callback.getResult(); - Throwable lastThrowable = callback.getThrowable(); - var hooker = (HookerCallback) modernSnapshot[afterIdx]; - try { - if (hooker.afterParams == 0) { - hooker.afterInvocation.invoke(null); - } else if (hooker.afterParams == 1) { - hooker.afterInvocation.invoke(null, callback); - } else { - hooker.afterInvocation.invoke(null, callback, ctxArray[afterIdx]); - } - } catch (Throwable t) { - LSPosedBridge.log(t); - - // reset to last result (ignoring what the unexpectedly exiting callback did) - if (lastThrowable == null) { - callback.setResult(lastResult); - } else { - callback.setThrowable(lastThrowable); - } - } - } - - if (legacy != null) { - legacy.handleAfter(); - } - - // return - var t = callback.getThrowable(); - if (t != null) { - throw t; - } else { - var result = callback.getResult(); - if (returnType != null && !returnType.isPrimitive() && !HookBridge.instanceOf(result, returnType)) { - throw new ClassCastException(castException); - } - return result; - } - } - } - - public static void dummyCallback() { - } - - public static XposedInterface.MethodUnhooker - doHook(T hookMethod, int priority, Class hooker) { - if (Modifier.isAbstract(hookMethod.getModifiers())) { - throw new IllegalArgumentException("Cannot hook abstract methods: " + hookMethod); - } else if (hookMethod.getDeclaringClass().getClassLoader() == LSPosedContext.class.getClassLoader()) { - throw new IllegalArgumentException("Do not allow hooking inner methods"); - } else if (hookMethod.getDeclaringClass() == Method.class && hookMethod.getName().equals("invoke")) { - throw new IllegalArgumentException("Cannot hook Method.invoke"); - } else if (hooker == null) { - throw new IllegalArgumentException("hooker should not be null!"); - } - - Method beforeInvocation = null, afterInvocation = null; - var modifiers = Modifier.PUBLIC | Modifier.STATIC; - for (var method : hooker.getDeclaredMethods()) { - if (method.getName().equals("before")) { - if (beforeInvocation != null) { - throw new IllegalArgumentException("More than one method named before"); - } - boolean valid = (method.getModifiers() & modifiers) == modifiers; - var params = method.getParameterTypes(); - if (params.length == 1) { - valid &= params[0].equals(XposedInterface.BeforeHookCallback.class); - } else if (params.length != 0) { - valid = false; - } - if (!valid) { - throw new IllegalArgumentException("before method format is invalid"); - } - beforeInvocation = method; - } else if (method.getName().equals("after")) { - if (afterInvocation != null) { - throw new IllegalArgumentException("More than one method named after"); - } - boolean valid = (method.getModifiers() & modifiers) == modifiers; - valid &= method.getReturnType().equals(void.class); - var params = method.getParameterTypes(); - if (params.length == 1 || params.length == 2) { - valid &= params[0].equals(XposedInterface.AfterHookCallback.class); - } else if (params.length != 0) { - valid = false; - } - if (!valid) { - throw new IllegalArgumentException("after method format is invalid"); - } - afterInvocation = method; - } - } - if (beforeInvocation == null && afterInvocation == null) { - throw new IllegalArgumentException("No method named before or after found in " + hooker.getName()); - } - try { - if (beforeInvocation == null) { - beforeInvocation = LSPosedBridge.class.getMethod("dummyCallback"); - } else if (afterInvocation == null) { - afterInvocation = LSPosedBridge.class.getMethod("dummyCallback"); - } else { - var ret = beforeInvocation.getReturnType(); - var params = afterInvocation.getParameterTypes(); - if (ret != void.class && params.length == 2 && !ret.equals(params[1])) { - throw new IllegalArgumentException("before and after method format is invalid"); - } - } - } catch (NoSuchMethodException e) { - throw new HookFailedError(e); - } - - var callback = new LSPosedBridge.HookerCallback(beforeInvocation, afterInvocation); - if (HookBridge.hookMethod(true, hookMethod, LSPosedBridge.NativeHooker.class, priority, callback)) { - return new XposedInterface.MethodUnhooker<>() { - @NonNull - @Override - public T getOrigin() { - return hookMethod; - } - - @Override - public void unhook() { - HookBridge.unhookMethod(true, hookMethod, callback); - } - }; - } - throw new HookFailedError("Cannot hook " + hookMethod); - } -} diff --git a/core/src/main/java/org/lsposed/lspd/impl/LSPosedContext.java b/core/src/main/java/org/lsposed/lspd/impl/LSPosedContext.java deleted file mode 100644 index 4f4fe7abd..000000000 --- a/core/src/main/java/org/lsposed/lspd/impl/LSPosedContext.java +++ /dev/null @@ -1,385 +0,0 @@ -package org.lsposed.lspd.impl; - -import android.annotation.SuppressLint; -import android.app.ActivityThread; -import android.content.SharedPreferences; -import android.content.pm.ApplicationInfo; -import android.os.Build; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.os.RemoteException; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.lsposed.lspd.core.BuildConfig; -import org.lsposed.lspd.models.Module; -import org.matrix.vector.nativebridge.HookBridge; -import org.matrix.vector.nativebridge.NativeAPI; -import org.lsposed.lspd.service.ILSPInjectedModuleService; -import org.lsposed.lspd.util.LspModuleClassLoader; -import org.lsposed.lspd.util.Utils.Log; - -import org.matrix.vector.impl.utils.VectorDexParser; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.Executable; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.Proxy; -import java.nio.ByteBuffer; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import io.github.libxposed.api.XposedInterface; -import io.github.libxposed.api.XposedModule; -import io.github.libxposed.api.XposedModuleInterface; -import io.github.libxposed.api.errors.XposedFrameworkError; -import io.github.libxposed.api.utils.DexParser; - - -@SuppressLint("NewApi") -public class LSPosedContext implements XposedInterface { - - private static final String TAG = "LSPosedContext"; - - public static boolean isSystemServer; - public static String appDir; - public static String processName; - - static final Set modules = ConcurrentHashMap.newKeySet(); - - private final String mPackageName; - private final ApplicationInfo mApplicationInfo; - private final ILSPInjectedModuleService service; - private final Map mRemotePrefs = new ConcurrentHashMap<>(); - - LSPosedContext(String packageName, ApplicationInfo applicationInfo, ILSPInjectedModuleService service) { - this.mPackageName = packageName; - this.mApplicationInfo = applicationInfo; - this.service = service; - } - - public static void callOnPackageLoaded(XposedModuleInterface.PackageLoadedParam param) { - for (XposedModule module : modules) { - try { - module.onPackageLoaded(param); - } catch (Throwable t) { - Log.e(TAG, "Error when calling onPackageLoaded of " + module.getApplicationInfo().packageName, t); - } - } - } - - public static void callOnSystemServerLoaded(XposedModuleInterface.SystemServerLoadedParam param) { - for (XposedModule module : modules) { - try { - module.onSystemServerLoaded(param); - } catch (Throwable t) { - Log.e(TAG, "Error when calling onSystemServerLoaded of " + module.getApplicationInfo().packageName, t); - } - } - } - - @SuppressLint("DiscouragedPrivateApi") - public static boolean loadModule(ActivityThread at, Module module) { - try { - Log.d(TAG, "Loading module " + module.packageName); - var sb = new StringBuilder(); - var abis = Process.is64Bit() ? Build.SUPPORTED_64_BIT_ABIS : Build.SUPPORTED_32_BIT_ABIS; - for (String abi : abis) { - sb.append(module.apkPath).append("!/lib/").append(abi).append(File.pathSeparator); - } - var librarySearchPath = sb.toString(); - var initLoader = XposedModule.class.getClassLoader(); - var mcl = LspModuleClassLoader.loadApk(module.apkPath, module.file.preLoadedDexes, librarySearchPath, initLoader); - if (mcl.loadClass(XposedModule.class.getName()).getClassLoader() != initLoader) { - Log.e(TAG, " Cannot load module: " + module.packageName); - 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."); - return false; - } - var ctx = new LSPosedContext(module.packageName, module.applicationInfo, module.service); - for (var entry : module.file.moduleClassNames) { - var moduleClass = mcl.loadClass(entry); - Log.d(TAG, " Loading class " + moduleClass); - if (!XposedModule.class.isAssignableFrom(moduleClass)) { - Log.e(TAG, " This class doesn't implement any sub-interface of XposedModule, skipping it"); - continue; - } - try { - var moduleEntry = moduleClass.getConstructor(XposedInterface.class, XposedModuleInterface.ModuleLoadedParam.class); - var moduleContext = (XposedModule) moduleEntry.newInstance(ctx, new XposedModuleInterface.ModuleLoadedParam() { - @Override - public boolean isSystemServer() { - return isSystemServer; - } - - @NonNull - @Override - public String getProcessName() { - return processName; - } - }); - modules.add(moduleContext); - } catch (Throwable e) { - Log.e(TAG, " Failed to load class " + moduleClass, e); - } - } - module.file.moduleLibraryNames.forEach(NativeAPI::recordNativeEntrypoint); - Log.d(TAG, "Loaded module " + module.packageName + ": " + ctx); - } catch (Throwable e) { - Log.d(TAG, "Loading module " + module.packageName, e); - return false; - } - return true; - } - - @NonNull - @Override - public String getFrameworkName() { - return BuildConfig.FRAMEWORK_NAME; - } - - @NonNull - @Override - public String getFrameworkVersion() { - return BuildConfig.VERSION_NAME; - } - - @Override - public long getFrameworkVersionCode() { - return BuildConfig.VERSION_CODE; - } - - @Override - public int getFrameworkPrivilege() { - try { - return service.getFrameworkPrivilege(); - } catch (RemoteException ignored) { - return -1; - } - } - - @Override - @NonNull - public MethodUnhooker hook(@NonNull Method origin, @NonNull Class hooker) { - return LSPosedBridge.doHook(origin, PRIORITY_DEFAULT, hooker); - } - - @Override - @NonNull - public MethodUnhooker hook(@NonNull Method origin, int priority, @NonNull Class hooker) { - return LSPosedBridge.doHook(origin, priority, hooker); - } - - @Override - @NonNull - public MethodUnhooker> hook(@NonNull Constructor origin, @NonNull Class hooker) { - return LSPosedBridge.doHook(origin, PRIORITY_DEFAULT, hooker); - } - - @Override - @NonNull - public MethodUnhooker> hook(@NonNull Constructor origin, int priority, @NonNull Class hooker) { - return LSPosedBridge.doHook(origin, priority, hooker); - } - - @Override - @NonNull - public MethodUnhooker> hookClassInitializer(@NonNull Class origin, @NonNull Class hooker) { - return hookClassInitializer(origin, PRIORITY_DEFAULT, hooker); - } - - @Override - @NonNull - @SuppressWarnings({"unchecked", "rawtypes"}) - public MethodUnhooker> hookClassInitializer(@NonNull Class origin, int priority, @NonNull Class hooker) { - Method staticInitializer = HookBridge.getStaticInitializer(origin); - - // The class might not have a static initializer block - if (staticInitializer == null) { - throw new IllegalArgumentException("Class " + origin.getName() + " has no static initializer"); - } - - // Use the existing doHook logic. It will return a MethodUnhooker. - return (MethodUnhooker) LSPosedBridge.doHook(staticInitializer, priority, hooker); - } - - private static boolean doDeoptimize(@NonNull Executable method) { - if (Modifier.isAbstract(method.getModifiers())) { - throw new IllegalArgumentException("Cannot deoptimize abstract methods: " + method); - } else if (Proxy.isProxyClass(method.getDeclaringClass())) { - throw new IllegalArgumentException("Cannot deoptimize methods from proxy class: " + method); - } - return HookBridge.deoptimizeMethod(method); - } - - @Override - public boolean deoptimize(@NonNull Method method) { - return doDeoptimize(method); - } - - @Override - public boolean deoptimize(@NonNull Constructor constructor) { - return doDeoptimize(constructor); - } - - @Nullable - @Override - public Object invokeOrigin(@NonNull Method method, @Nullable Object thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException { - return HookBridge.invokeOriginalMethod(method, thisObject, args); - } - - @Override - public void invokeOrigin(@NonNull Constructor constructor, @NonNull T thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException { - // The bridge returns an Object (null for void/constructors), which we discard. - HookBridge.invokeOriginalMethod(constructor, thisObject, args); - } - - private static char getTypeShorty(Class type) { - if (type == int.class) { - return 'I'; - } else if (type == long.class) { - return 'J'; - } else if (type == float.class) { - return 'F'; - } else if (type == double.class) { - return 'D'; - } else if (type == boolean.class) { - return 'Z'; - } else if (type == byte.class) { - return 'B'; - } else if (type == char.class) { - return 'C'; - } else if (type == short.class) { - return 'S'; - } else if (type == void.class) { - return 'V'; - } else { - return 'L'; - } - } - - private static char[] getExecutableShorty(Executable executable) { - var parameterTypes = executable.getParameterTypes(); - var shorty = new char[parameterTypes.length + 1]; - shorty[0] = getTypeShorty(executable instanceof Method ? ((Method) executable).getReturnType() : void.class); - for (int i = 1; i < shorty.length; i++) { - shorty[i] = getTypeShorty(parameterTypes[i - 1]); - } - return shorty; - } - - @Nullable - @Override - public Object invokeSpecial(@NonNull Method method, @NonNull Object thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException { - if (Modifier.isStatic(method.getModifiers())) { - throw new IllegalArgumentException("Cannot invoke special on static method: " + method); - } - return HookBridge.invokeSpecialMethod(method, getExecutableShorty(method), method.getDeclaringClass(), thisObject, args); - } - - @Override - public void invokeSpecial(@NonNull Constructor constructor, @NonNull T thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException { - HookBridge.invokeSpecialMethod(constructor, getExecutableShorty(constructor), constructor.getDeclaringClass(), thisObject, args); - } - - @NonNull - @Override - public T newInstanceOrigin(@NonNull Constructor constructor, Object... args) throws InvocationTargetException, IllegalAccessException, InstantiationException { - var obj = HookBridge.allocateObject(constructor.getDeclaringClass()); - HookBridge.invokeOriginalMethod(constructor, obj, args); - return obj; - } - - @NonNull - @Override - public U newInstanceSpecial(@NonNull Constructor constructor, @NonNull Class subClass, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException, InstantiationException { - var superClass = constructor.getDeclaringClass(); - if (!superClass.isAssignableFrom(subClass)) { - throw new IllegalArgumentException(subClass + " is not inherited from " + superClass); - } - var obj = HookBridge.allocateObject(subClass); - HookBridge.invokeSpecialMethod(constructor, getExecutableShorty(constructor), superClass, obj, args); - return obj; - } - - @Override - public void log(@NonNull String message) { - log(Log.INFO, null, message, null); - } - - @Override - public void log(@NonNull String message, @NonNull Throwable throwable) { - log(Log.ERROR, null, message, throwable); - } - - @Override - public void log(int priority, @Nullable String tag, @NonNull String msg, @Nullable Throwable tr) { - String finalTag = (tag != null) ? tag : TAG; - - // Format the message with the package name prefix - String prefix = (mPackageName != null) ? mPackageName + ": " : ""; - StringBuilder fullMsg = new StringBuilder(prefix).append(msg); - - // Handle the Throwable if present - if (tr != null) { - fullMsg.append("\n").append(Log.getStackTraceString(tr)); - } - - // Use the low-level println to handle dynamic priorities - Log.println(priority, finalTag, fullMsg.toString()); - } - - @Override - public DexParser parseDex(@NonNull ByteBuffer dexData, boolean includeAnnotations) throws IOException { - return new VectorDexParser(dexData, includeAnnotations); - } - - @NonNull - @Override - public ApplicationInfo getApplicationInfo() { - return mApplicationInfo; - } - - @NonNull - @Override - public SharedPreferences getRemotePreferences(String name) { - if (name == null) throw new IllegalArgumentException("name must not be null"); - return mRemotePrefs.computeIfAbsent(name, n -> { - try { - return new LSPosedRemotePreferences(service, n); - } catch (RemoteException e) { - log("Failed to get remote preferences", e); - throw new XposedFrameworkError(e); - } - }); - } - - @NonNull - @Override - public String[] listRemoteFiles() { - try { - return service.getRemoteFileList(); - } catch (RemoteException e) { - log("Failed to list remote files", e); - throw new XposedFrameworkError(e); - } - } - - @NonNull - @Override - public ParcelFileDescriptor openRemoteFile(String name) throws FileNotFoundException { - if (name == null) throw new IllegalArgumentException("name must not be null"); - try { - return service.openRemoteFile(name); - } catch (RemoteException e) { - throw new FileNotFoundException(e.getMessage()); - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/impl/LSPosedHelper.java b/core/src/main/java/org/lsposed/lspd/impl/LSPosedHelper.java deleted file mode 100644 index 60f115e4b..000000000 --- a/core/src/main/java/org/lsposed/lspd/impl/LSPosedHelper.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.lsposed.lspd.impl; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.HashSet; -import java.util.Set; - -import io.github.libxposed.api.XposedInterface; -import io.github.libxposed.api.errors.HookFailedError; - -public class LSPosedHelper { - - @SuppressWarnings("UnusedReturnValue") - public static XposedInterface.MethodUnhooker - hookMethod(Class hooker, Class clazz, String methodName, Class... parameterTypes) { - try { - var method = clazz.getDeclaredMethod(methodName, parameterTypes); - return LSPosedBridge.doHook(method, XposedInterface.PRIORITY_DEFAULT, hooker); - } catch (NoSuchMethodException e) { - throw new HookFailedError(e); - } - } - - @SuppressWarnings("UnusedReturnValue") - public static Set> - hookAllMethods(Class hooker, Class clazz, String methodName) { - var unhooks = new HashSet>(); - for (var method : clazz.getDeclaredMethods()) { - if (method.getName().equals(methodName)) { - unhooks.add(LSPosedBridge.doHook(method, XposedInterface.PRIORITY_DEFAULT, hooker)); - } - } - return unhooks; - } - - @SuppressWarnings("UnusedReturnValue") - public static XposedInterface.MethodUnhooker> - hookConstructor(Class hooker, Class clazz, Class... parameterTypes) { - try { - var constructor = clazz.getDeclaredConstructor(parameterTypes); - return LSPosedBridge.doHook(constructor, XposedInterface.PRIORITY_DEFAULT, hooker); - } catch (NoSuchMethodException e) { - throw new HookFailedError(e); - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/impl/LSPosedHookCallback.java b/core/src/main/java/org/lsposed/lspd/impl/LSPosedHookCallback.java deleted file mode 100644 index 0caa22df8..000000000 --- a/core/src/main/java/org/lsposed/lspd/impl/LSPosedHookCallback.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.lsposed.lspd.impl; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.lang.reflect.Executable; -import java.lang.reflect.Member; - -import io.github.libxposed.api.XposedInterface; - -public class LSPosedHookCallback implements XposedInterface.BeforeHookCallback, XposedInterface.AfterHookCallback { - - public Member method; - - public Object thisObject; - - public Object[] args; - - public Object result; - - public Throwable throwable; - - public boolean isSkipped; - - public LSPosedHookCallback() { - } - - // Both before and after - - @NonNull - @Override - public Member getMember() { - return this.method; - } - - @Nullable - @Override - public Object getThisObject() { - return this.thisObject; - } - - @NonNull - @Override - public Object[] getArgs() { - return this.args; - } - - // Before - - @Override - public void returnAndSkip(@Nullable Object result) { - this.result = result; - this.throwable = null; - this.isSkipped = true; - } - - @Override - public void throwAndSkip(@Nullable Throwable throwable) { - this.result = null; - this.throwable = throwable; - this.isSkipped = true; - } - - // After - - @Nullable - @Override - public Object getResult() { - return this.result; - } - - @Nullable - @Override - public Throwable getThrowable() { - return this.throwable; - } - - @Override - public boolean isSkipped() { - return this.isSkipped; - } - - @Override - public void setResult(@Nullable Object result) { - this.result = result; - this.throwable = null; - } - - @Override - public void setThrowable(@Nullable Throwable throwable) { - this.result = null; - this.throwable = throwable; - } -} diff --git a/core/src/main/java/org/lsposed/lspd/impl/LSPosedRemotePreferences.java b/core/src/main/java/org/lsposed/lspd/impl/LSPosedRemotePreferences.java deleted file mode 100644 index 5c5d3fa37..000000000 --- a/core/src/main/java/org/lsposed/lspd/impl/LSPosedRemotePreferences.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.lsposed.lspd.impl; - -import android.content.SharedPreferences; -import android.os.Bundle; -import android.os.RemoteException; -import android.util.ArraySet; - -import androidx.annotation.Nullable; - -import org.lsposed.lspd.service.ILSPInjectedModuleService; -import org.lsposed.lspd.service.IRemotePreferenceCallback; - -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; -import java.util.concurrent.ConcurrentHashMap; - -@SuppressWarnings("unchecked") -public class LSPosedRemotePreferences implements SharedPreferences { - - private final Map mMap = new ConcurrentHashMap<>(); - - final HashSet mListeners = new HashSet<>(); - - IRemotePreferenceCallback callback = new IRemotePreferenceCallback.Stub() { - @Override - synchronized public void onUpdate(Bundle bundle) { - Set changes = new ArraySet<>(); - if (bundle.containsKey("delete")) { - var deletes = (Set) bundle.getSerializable("delete"); - changes.addAll(deletes); - for (var key : deletes) { - mMap.remove(key); - } - } - if (bundle.containsKey("put")) { - var puts = (Map) bundle.getSerializable("put"); - mMap.putAll(puts); - changes.addAll(puts.keySet()); - } - synchronized (mListeners) { - for (var key : changes) { - mListeners.forEach(listener -> listener.onSharedPreferenceChanged(LSPosedRemotePreferences.this, key)); - } - } - } - }; - - public LSPosedRemotePreferences(ILSPInjectedModuleService service, String group) throws RemoteException { - Bundle output = service.requestRemotePreferences(group, callback); - if (output.containsKey("map")) { - mMap.putAll((Map) output.getSerializable("map")); - } - } - - @Override - public Map getAll() { - return new TreeMap<>(mMap); - } - - @Nullable - @Override - public String getString(String key, @Nullable String defValue) { - var v = (String) mMap.getOrDefault(key, defValue); - if (v != null) return v; - return defValue; - } - - @Nullable - @Override - public Set getStringSet(String key, @Nullable Set defValues) { - var v = (Set) mMap.getOrDefault(key, defValues); - if (v != null) return v; - return defValues; - } - - @Override - public int getInt(String key, int defValue) { - var v = (Integer) mMap.getOrDefault(key, defValue); - if (v != null) return v; - return defValue; - } - - @Override - public long getLong(String key, long defValue) { - var v = (Long) mMap.getOrDefault(key, defValue); - if (v != null) return v; - return defValue; - } - - @Override - public float getFloat(String key, float defValue) { - var v = (Float) mMap.getOrDefault(key, defValue); - if (v != null) return v; - return defValue; - } - - @Override - public boolean getBoolean(String key, boolean defValue) { - var v = (Boolean) mMap.getOrDefault(key, defValue); - if (v != null) return v; - return defValue; - } - - @Override - public boolean contains(String key) { - return mMap.containsKey(key); - } - - @Override - public Editor edit() { - throw new UnsupportedOperationException("Read only implementation"); - } - - @Override - public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { - synchronized (mListeners) { - mListeners.add(listener); - } - } - - @Override - public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { - synchronized (mListeners) { - mListeners.remove(listener); - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/util/ClassPathURLStreamHandler.java b/core/src/main/java/org/lsposed/lspd/util/ClassPathURLStreamHandler.java deleted file mode 100644 index 9e4346137..000000000 --- a/core/src/main/java/org/lsposed/lspd/util/ClassPathURLStreamHandler.java +++ /dev/null @@ -1,120 +0,0 @@ -package org.lsposed.lspd.util; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.JarURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLConnection; -import java.util.jar.JarFile; -import java.util.zip.ZipEntry; - -import sun.net.www.ParseUtil; -import sun.net.www.protocol.jar.Handler; - -final class ClassPathURLStreamHandler extends Handler { - private final String fileUri; - private final JarFile jarFile; - - ClassPathURLStreamHandler(String jarFileName) throws IOException { - jarFile = new JarFile(jarFileName); - fileUri = new File(jarFileName).toURI().toString(); - } - - URL getEntryUrlOrNull(String entryName) { - if (jarFile.getEntry(entryName) != null) { - try { - String encodedName = ParseUtil.encodePath(entryName, false); - return new URL("jar", null, -1, fileUri + "!/" + encodedName, this); - } catch (MalformedURLException e) { - throw new RuntimeException("Invalid entry name", e); - } - } - return null; - } - - @Override - protected URLConnection openConnection(URL url) throws IOException { - return new ClassPathURLConnection(url); - } - - @Override - protected void finalize() throws IOException { - jarFile.close(); - } - - private final class ClassPathURLConnection extends JarURLConnection { - private JarFile connectionJarFile = null; - private ZipEntry jarEntry = null; - private InputStream jarInput = null; - private boolean closed = false; - - private ClassPathURLConnection(URL url) throws MalformedURLException { - super(url); - setUseCaches(false); - } - - @Override - public void setUseCaches(boolean usecaches) { - super.setUseCaches(false); - } - - @Override - public void connect() throws IOException { - if (closed) { - throw new IllegalStateException("JarURLConnection has been closed"); - } - if (!connected) { - jarEntry = jarFile.getEntry(getEntryName()); - if (jarEntry == null) { - throw new FileNotFoundException("URL=" + url + ", zipfile=" + jarFile.getName()); - } - connected = true; - } - } - - @Override - public JarFile getJarFile() throws IOException { - connect(); - if (connectionJarFile != null) return connectionJarFile; - return connectionJarFile = new JarFile(jarFile.getName()); - } - - @Override - public InputStream getInputStream() throws IOException { - connect(); - if (jarInput != null) return jarInput; - return jarInput = new FilterInputStream(jarFile.getInputStream(jarEntry)) { - @Override - public void close() throws IOException { - super.close(); - closed = true; - jarFile.close(); - if (connectionJarFile != null) connectionJarFile.close(); - } - }; - } - - @Override - public String getContentType() { - String cType = guessContentTypeFromName(getEntryName()); - if (cType == null) { - cType = "content/unknown"; - } - return cType; - } - - @Override - public int getContentLength() { - try { - connect(); - return (int) getJarEntry().getSize(); - } catch (IOException ignored) { - } - return -1; - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/util/Hookers.java b/core/src/main/java/org/lsposed/lspd/util/Hookers.java deleted file mode 100644 index 6805e8e9b..000000000 --- a/core/src/main/java/org/lsposed/lspd/util/Hookers.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.util; - -import android.app.ActivityThread; - -public class Hookers { - - public static void logD(String prefix) { - Utils.logD(String.format("%s: pkg=%s, prc=%s", prefix, ActivityThread.currentPackageName(), - ActivityThread.currentProcessName())); - } - - public static void logE(String prefix, Throwable throwable) { - Utils.logE(String.format("%s: pkg=%s, prc=%s", prefix, ActivityThread.currentPackageName(), - ActivityThread.currentProcessName()), throwable); - } - -} diff --git a/core/src/main/java/org/lsposed/lspd/util/LspModuleClassLoader.java b/core/src/main/java/org/lsposed/lspd/util/LspModuleClassLoader.java deleted file mode 100644 index 56f60b7e6..000000000 --- a/core/src/main/java/org/lsposed/lspd/util/LspModuleClassLoader.java +++ /dev/null @@ -1,207 +0,0 @@ -package org.lsposed.lspd.util; - -import static de.robv.android.xposed.XposedBridge.TAG; - -import android.os.Build; -import android.os.SharedMemory; -import android.system.ErrnoException; -import android.system.Os; -import android.system.OsConstants; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.List; -import java.util.Objects; -import java.util.jar.JarFile; -import java.util.zip.ZipEntry; - -import org.lsposed.lspd.util.Utils.Log; - -import hidden.ByteBufferDexClassLoader; -import sun.misc.CompoundEnumeration; - -@SuppressWarnings("ConstantConditions") -public final class LspModuleClassLoader extends ByteBufferDexClassLoader { - private static final String zipSeparator = "!/"; - private static final List systemNativeLibraryDirs = - splitPaths(System.getProperty("java.library.path")); - private final String apk; - private final List nativeLibraryDirs = new ArrayList<>(); - - private static List splitPaths(String searchPath) { - var result = new ArrayList(); - if (searchPath == null) return result; - for (var path : searchPath.split(File.pathSeparator)) { - result.add(new File(path)); - } - return result; - } - - private LspModuleClassLoader(ByteBuffer[] dexBuffers, - ClassLoader parent, - String apk) { - super(dexBuffers, parent); - this.apk = apk; - } - - @RequiresApi(Build.VERSION_CODES.Q) - private LspModuleClassLoader(ByteBuffer[] dexBuffers, - String librarySearchPath, - ClassLoader parent, - String apk) { - super(dexBuffers, librarySearchPath, parent); - initNativeLibraryDirs(librarySearchPath); - this.apk = apk; - } - - private void initNativeLibraryDirs(String librarySearchPath) { - nativeLibraryDirs.addAll(splitPaths(librarySearchPath)); - nativeLibraryDirs.addAll(systemNativeLibraryDirs); - } - - @Override - protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - var cl = findLoadedClass(name); - if (cl != null) { - return cl; - } - try { - return Object.class.getClassLoader().loadClass(name); - } catch (ClassNotFoundException ignored) { - } - ClassNotFoundException fromSuper; - try { - return findClass(name); - } catch (ClassNotFoundException ex) { - fromSuper = ex; - } - try { - return getParent().loadClass(name); - } catch (ClassNotFoundException cnfe) { - throw fromSuper; - } - } - - @Override - public String findLibrary(String libraryName) { - var fileName = System.mapLibraryName(libraryName); - for (var file : nativeLibraryDirs) { - var path = file.getPath(); - if (path.contains(zipSeparator)) { - var split = path.split(zipSeparator, 2); - try (var jarFile = new JarFile(split[0])) { - var entryName = split[1] + '/' + fileName; - var entry = jarFile.getEntry(entryName); - if (entry != null && entry.getMethod() == ZipEntry.STORED) { - return split[0] + zipSeparator + entryName; - } - } catch (IOException e) { - Log.e(TAG, "Can not open " + split[0], e); - } - } else if (file.isDirectory()) { - var entryPath = new File(file, fileName).getPath(); - try { - var fd = Os.open(entryPath, OsConstants.O_RDONLY, 0); - Os.close(fd); - return entryPath; - } catch (ErrnoException ignored) { - } - } - } - return null; - } - - @Override - public String getLdLibraryPath() { - var result = new StringBuilder(); - for (var directory : nativeLibraryDirs) { - if (result.length() > 0) { - result.append(':'); - } - result.append(directory); - } - return result.toString(); - } - - @Override - protected URL findResource(String name) { - try { - var urlHandler = new ClassPathURLStreamHandler(apk); - var url = urlHandler.getEntryUrlOrNull(name); - if (url == null) { - // noinspection FinalizeCalledExplicitly - urlHandler.finalize(); - } - return url; - } catch (IOException e) { - return null; - } - } - - @Override - protected Enumeration findResources(String name) { - var result = new ArrayList(); - var url = findResource(name); - if (url != null) result.add(url); - return Collections.enumeration(result); - } - - @Override - public URL getResource(String name) { - var resource = Object.class.getClassLoader().getResource(name); - if (resource != null) return resource; - resource = findResource(name); - if (resource != null) return resource; - final var cl = getParent(); - return (cl == null) ? null : cl.getResource(name); - } - - @Override - public Enumeration getResources(String name) throws IOException { - @SuppressWarnings("unchecked") final var resources = (Enumeration[]) new Enumeration[]{ - Object.class.getClassLoader().getResources(name), - findResources(name), - getParent() == null ? null : getParent().getResources(name)}; - return new CompoundEnumeration<>(resources); - } - - @NonNull - @Override - public String toString() { - if (apk == null) return "LspModuleClassLoader[instantiating]"; - return "LspModuleClassLoader[module=" + apk + ", " + super.toString() + "]"; - } - - public static ClassLoader loadApk(String apk, - List dexes, - String librarySearchPath, - ClassLoader parent) { - var dexBuffers = dexes.stream().parallel().map(dex -> { - try { - return dex.mapReadOnly(); - } catch (ErrnoException e) { - Log.w(TAG, "Can not map " + dex, e); - return null; - } - }).filter(Objects::nonNull).toArray(ByteBuffer[]::new); - LspModuleClassLoader cl; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - cl = new LspModuleClassLoader(dexBuffers, librarySearchPath, parent, apk); - } else { - cl = new LspModuleClassLoader(dexBuffers, parent, apk); - cl.initNativeLibraryDirs(librarySearchPath); - } - Arrays.stream(dexBuffers).parallel().forEach(SharedMemory::unmap); - dexes.stream().parallel().forEach(SharedMemory::close); - return cl; - } -} diff --git a/core/src/main/java/org/lsposed/lspd/util/MetaDataReader.java b/core/src/main/java/org/lsposed/lspd/util/MetaDataReader.java deleted file mode 100644 index 8a9400ff1..000000000 --- a/core/src/main/java/org/lsposed/lspd/util/MetaDataReader.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.util; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.jar.JarFile; - -import pxb.android.axml.AxmlReader; -import pxb.android.axml.AxmlVisitor; -import pxb.android.axml.NodeVisitor; - -public class MetaDataReader { - private final HashMap metaData = new HashMap<>(); - - public static Map getMetaData(File apk) throws IOException { - return new MetaDataReader(apk).metaData; - } - - private MetaDataReader(File apk) throws IOException { - try (JarFile zip = new JarFile(apk); - var is = zip.getInputStream(zip.getEntry("AndroidManifest.xml"))) { - var reader = new AxmlReader(getBytesFromInputStream(is)); - reader.accept(new AxmlVisitor() { - @Override - public NodeVisitor child(String ns, String name) { - NodeVisitor child = super.child(ns, name); - return new ManifestTagVisitor(child); - } - }); - } - } - - public static byte[] getBytesFromInputStream(InputStream inputStream) throws IOException { - try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { - byte[] b = new byte[1024]; - int n; - while ((n = inputStream.read(b)) != -1) { - bos.write(b, 0, n); - } - return bos.toByteArray(); - } - } - - private class ManifestTagVisitor extends NodeVisitor { - public ManifestTagVisitor(NodeVisitor child) { - super(child); - } - - @Override - public NodeVisitor child(String ns, String name) { - NodeVisitor child = super.child(ns, name); - if ("application".equals(name)) { - return new ApplicationTagVisitor(child); - } - return child; - } - - private class ApplicationTagVisitor extends NodeVisitor { - public ApplicationTagVisitor(NodeVisitor child) { - super(child); - } - - @Override - public NodeVisitor child(String ns, String name) { - NodeVisitor child = super.child(ns, name); - if ("meta-data".equals(name)) { - return new MetaDataVisitor(child); - } - return child; - } - } - } - - private class MetaDataVisitor extends NodeVisitor { - public String name = null; - public Object value = null; - - public MetaDataVisitor(NodeVisitor child) { - super(child); - } - - @Override - public void attr(String ns, String name, int resourceId, int type, Object obj) { - if (type == 3 && "name".equals(name)) { - this.name = (String) obj; - } - if ("value".equals(name)) { - value = obj; - } - super.attr(ns, name, resourceId, type, obj); - } - - @Override - public void end() { - if (name != null && value != null) { - metaData.put(name, value); - } - super.end(); - } - } - - public static int extractIntPart(String str) { - int result = 0, length = str.length(); - for (int offset = 0; offset < length; offset++) { - char c = str.charAt(offset); - if ('0' <= c && c <= '9') - result = result * 10 + (c - '0'); - else - break; - } - return result; - } -} diff --git a/daemon/build.gradle.kts b/daemon/build.gradle.kts index 93abf08fd..cee0e4d42 100644 --- a/daemon/build.gradle.kts +++ b/daemon/build.gradle.kts @@ -1,40 +1,18 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - import com.android.build.api.dsl.ApplicationExtension import com.android.ide.common.signing.KeystoreHelper import java.io.PrintStream +val defaultManagerPackageName: String by rootProject.extra +val injectedPackageName: String by rootProject.extra +val injectedPackageUid: Int by rootProject.extra +val versionCodeProvider: Provider by rootProject.extra +val versionNameProvider: Provider by rootProject.extra + plugins { alias(libs.plugins.agp.app) alias(libs.plugins.lsplugin.resopt) } -val daemonName = "LSPosed" - -val injectedPackageName: String by rootProject.extra -val injectedPackageUid: Int by rootProject.extra - -val agpVersion: String by project - -val defaultManagerPackageName: String by rootProject.extra - android { buildFeatures { prefab = true @@ -49,8 +27,11 @@ android { "DEFAULT_MANAGER_PACKAGE_NAME", """"$defaultManagerPackageName"""", ) + buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""") buildConfigField("String", "MANAGER_INJECTED_PKG_NAME", """"$injectedPackageName"""") buildConfigField("int", "MANAGER_INJECTED_UID", """$injectedPackageUid""") + buildConfigField("String", "VERSION_NAME", """"${versionNameProvider.get()}"""") + buildConfigField("long", "VERSION_CODE", versionCodeProvider.get()) } buildTypes { diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java index ae1f6e80f..fa402923c 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java @@ -30,8 +30,12 @@ public class LSPInjectedModuleService extends ILSPInjectedModuleService.Stub { } @Override - public int getFrameworkPrivilege() { - return IXposedService.FRAMEWORK_PRIVILEGE_ROOT; + public long getFrameworkProperties() { + var prop = IXposedService.PROP_CAP_SYSTEM | IXposedService.PROP_CAP_REMOTE; + if (ConfigManager.getInstance().dexObfuscate()) { + prop = prop | IXposedService.PROP_RT_API_PROTECTION; + } + return prop; } @Override diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java index 46a3cd542..0ed6b21f7 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java @@ -268,11 +268,11 @@ public IBinder asBinder() { @Override public int getXposedApiVersion() { - return IXposedService.API; + return IXposedService.LIB_API; } @Override - public int getXposedVersionCode() { + public long getXposedVersionCode() { return BuildConfig.VERSION_CODE; } diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java index 6e1a3ed4a..1b770c3cb 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java @@ -123,15 +123,15 @@ private int ensureModule() throws RemoteException { } @Override - public int getAPIVersion() throws RemoteException { + public int getApiVersion() throws RemoteException { ensureModule(); - return API; + return IXposedService.LIB_API; } @Override public String getFrameworkName() throws RemoteException { ensureModule(); - return "LSPosed"; + return BuildConfig.FRAMEWORK_NAME; } @Override @@ -147,9 +147,13 @@ public long getFrameworkVersionCode() throws RemoteException { } @Override - public int getFrameworkPrivilege() throws RemoteException { + public long getFrameworkProperties() throws RemoteException { ensureModule(); - return IXposedService.FRAMEWORK_PRIVILEGE_ROOT; + var prop = IXposedService.PROP_CAP_SYSTEM | IXposedService.PROP_CAP_REMOTE; + if (ConfigManager.getInstance().dexObfuscate()) { + prop = prop | IXposedService.PROP_RT_API_PROTECTION; + } + return prop; } @Override @@ -165,26 +169,28 @@ public List getScope() throws RemoteException { } @Override - public void requestScope(String packageName, IXposedScopeCallback callback) throws RemoteException { + public void requestScope(List packages, IXposedScopeCallback callback) throws RemoteException { var userId = ensureModule(); - if (ConfigManager.getInstance().scopeRequestBlocked(loadedModule.packageName)) { - callback.onScopeRequestDenied(packageName); + if (!ConfigManager.getInstance().scopeRequestBlocked(loadedModule.packageName)) { + for (String packageName : packages) { + LSPNotificationManager.requestModuleScope(loadedModule.packageName, userId, packageName, callback); + } } else { - LSPNotificationManager.requestModuleScope(loadedModule.packageName, userId, packageName, callback); - callback.onScopeRequestPrompted(packageName); + callback.onScopeRequestFailed("Scope request blocked by user configuration"); } } @Override - public String removeScope(String packageName) throws RemoteException { + public void removeScope(List packages) throws RemoteException { var userId = ensureModule(); - try { - if (!ConfigManager.getInstance().removeModuleScope(loadedModule.packageName, packageName, userId)) { - return "Invalid request"; + for (String packageName : packages) { + try { + if (!ConfigManager.getInstance().removeModuleScope(loadedModule.packageName, packageName, userId)) { + Log.w(TAG, "Failed to remove scope: " + packageName + " (Invalid request)"); + } + } catch (Throwable e) { + Log.e(TAG, "Error removing scope for " + packageName, e); } - return null; - } catch (Throwable e) { - return e.getMessage(); } } diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java index 878837eee..e2ee5148e 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java @@ -24,6 +24,7 @@ import android.os.RemoteException; import android.util.Log; +import org.lsposed.daemon.BuildConfig; import org.lsposed.daemon.R; import org.lsposed.lspd.util.FakeContext; @@ -164,7 +165,7 @@ static void notifyStatusNotification() { .setOngoing(true) .setAutoCancel(false) .build(); - notification.extras.putString("android.substName", "LSPosed"); + notification.extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME); try { var nm = getNotificationManager(); createNotificationChannel(nm); @@ -251,7 +252,7 @@ static void notifyModuleUpdated(String modulePackageName, .setAutoCancel(true) .setStyle(style) .build(); - notification.extras.putString("android.substName", "LSPosed"); + notification.extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME); try { var nm = getNotificationManager(); nm.enqueueNotificationWithTag("android", opPkg, modulePackageName, @@ -297,7 +298,7 @@ static void requestModuleScope(String modulePackageName, int moduleUserId, Strin getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "block", callback)) .build() ).build(); - notification.extras.putString("android.substName", "LSPosed"); + notification.extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME); try { var nm = getNotificationManager(); nm.enqueueNotificationWithTag("android", opPkg, modulePackageName, @@ -305,7 +306,7 @@ static void requestModuleScope(String modulePackageName, int moduleUserId, Strin notification, 0); } catch (RemoteException e) { try { - callback.onScopeRequestFailed(scopePackageName, e.getMessage()); + callback.onScopeRequestFailed(e.getMessage()); } catch (RemoteException ignored) { } Log.e(TAG, "request module scope", e); diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java index 198957362..4af2632a4 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java @@ -49,6 +49,7 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.Consumer; @@ -301,26 +302,26 @@ private void dispatchModuleScope(Intent intent) { try { var applicationInfo = PackageService.getApplicationInfo(scopePackageName, 0, userId); if (applicationInfo == null) { - iCallback.onScopeRequestFailed(scopePackageName, "Package not found"); + iCallback.onScopeRequestFailed("Package not found"); return; } switch (action) { case "approve" -> { ConfigManager.getInstance().setModuleScope(packageName, scopePackageName, userId); - iCallback.onScopeRequestApproved(scopePackageName); + iCallback.onScopeRequestApproved(Collections.singletonList(scopePackageName)); } - case "deny" -> iCallback.onScopeRequestDenied(scopePackageName); - case "delete" -> iCallback.onScopeRequestTimeout(scopePackageName); + case "deny" -> iCallback.onScopeRequestFailed("Request denied by user"); + case "delete" -> iCallback.onScopeRequestFailed("Request timeout"); case "block" -> { ConfigManager.getInstance().blockScopeRequest(packageName); - iCallback.onScopeRequestDenied(scopePackageName); + iCallback.onScopeRequestFailed("Request blocked by configuration"); } } Log.i(TAG, action + " scope " + scopePackageName + " for " + packageName + " in user " + userId); } catch (RemoteException e) { try { - iCallback.onScopeRequestFailed(scopePackageName, e.getMessage()); + iCallback.onScopeRequestFailed(e.getMessage()); } catch (RemoteException ignored) { // callback died } diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index 96d7c2048..9965f42f1 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -221,6 +221,26 @@ extern "C" JNIEXPORT jobject JNICALL Java_org_lsposed_lspd_service_ObfuscationMa return nullptr; } + bool needs_obfuscation = false; + for (const auto &sig : signatures) { + if (memmem(mem, size, sig.first.c_str(), sig.first.length()) != nullptr) { + needs_obfuscation = true; + break; + } + } + + if (!needs_obfuscation) { + LOGD("No target signatures found in fd=%d, skipping slicer.", fd); + munmap(mem, size); + + // Wrap the duplicated FD into Java objects and return instantly + auto java_fd = + lsplant::JNI_NewObject(env, class_file_descriptor, method_file_descriptor_ctor, fd); + auto java_sm = + lsplant::JNI_NewObject(env, class_shared_memory, method_shared_memory_ctor, java_fd); + return java_sm.release(); + } + // Process the DEX and obtain a new file descriptor for the output int new_fd = obfuscateDexBuffer(mem, size); diff --git a/hiddenapi/stubs/src/main/java/sun/net/www/ParseUtil.java b/hiddenapi/stubs/src/main/java/sun/net/www/ParseUtil.java deleted file mode 100644 index a3755eeca..000000000 --- a/hiddenapi/stubs/src/main/java/sun/net/www/ParseUtil.java +++ /dev/null @@ -1,7 +0,0 @@ -package sun.net.www; - -public class ParseUtil { - public static String encodePath(String path, boolean flag) { - throw new RuntimeException("Stub!"); - } -} diff --git a/legacy/build.gradle.kts b/legacy/build.gradle.kts new file mode 100644 index 000000000..f28dec801 --- /dev/null +++ b/legacy/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { alias(libs.plugins.agp.lib) } + +android { + namespace = "org.matrix.vector.legacy" + + androidResources { enable = false } + + defaultConfig { consumerProguardFiles("consumer-rules.pro") } +} + +dependencies { + api(projects.xposed) + implementation(projects.external.apache) + implementation(projects.hiddenapi.bridge) + implementation(projects.services.daemonService) + compileOnly(libs.androidx.annotation) + compileOnly(projects.hiddenapi.stubs) +} diff --git a/legacy/consumer-rules.pro b/legacy/consumer-rules.pro new file mode 100644 index 000000000..7da43889d --- /dev/null +++ b/legacy/consumer-rules.pro @@ -0,0 +1,5 @@ +-keep class android.** { *; } +-keep class de.robv.android.xposed.** { *; } + +# Workaround to bypass verification of in-memory built class xposed.dummy.XResourcesSuperClass +-keepclassmembers class org.matrix.vector.legacy.LegacyDelegateImpl$ResourceProxy { *; } diff --git a/core/src/main/java/android/app/AndroidAppHelper.java b/legacy/src/main/java/android/app/AndroidAppHelper.java similarity index 88% rename from core/src/main/java/android/app/AndroidAppHelper.java rename to legacy/src/main/java/android/app/AndroidAppHelper.java index ed6636f7e..8d12fc5b7 100644 --- a/core/src/main/java/android/app/AndroidAppHelper.java +++ b/legacy/src/main/java/android/app/AndroidAppHelper.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package android.app; import static de.robv.android.xposed.XposedHelpers.findClass; diff --git a/core/src/main/java/android/content/res/XModuleResources.java b/legacy/src/main/java/android/content/res/XModuleResources.java similarity index 72% rename from core/src/main/java/android/content/res/XModuleResources.java rename to legacy/src/main/java/android/content/res/XModuleResources.java index e5d8cf694..67396b194 100644 --- a/core/src/main/java/android/content/res/XModuleResources.java +++ b/legacy/src/main/java/android/content/res/XModuleResources.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package android.content.res; import android.app.AndroidAppHelper; diff --git a/core/src/main/java/android/content/res/XResForwarder.java b/legacy/src/main/java/android/content/res/XResForwarder.java similarity index 55% rename from core/src/main/java/android/content/res/XResForwarder.java rename to legacy/src/main/java/android/content/res/XResForwarder.java index 9df971630..7d659052c 100644 --- a/core/src/main/java/android/content/res/XResForwarder.java +++ b/legacy/src/main/java/android/content/res/XResForwarder.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package android.content.res; /** diff --git a/core/src/main/java/android/content/res/XResources.java b/legacy/src/main/java/android/content/res/XResources.java similarity index 98% rename from core/src/main/java/android/content/res/XResources.java rename to legacy/src/main/java/android/content/res/XResources.java index 349a885f5..833ea926e 100644 --- a/core/src/main/java/android/content/res/XResources.java +++ b/legacy/src/main/java/android/content/res/XResources.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package android.content.res; import static org.matrix.vector.nativebridge.ResourcesHook.rewriteXmlReferencesNative; diff --git a/core/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java b/legacy/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java similarity index 56% rename from core/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java rename to legacy/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java index 655453b8d..0132c58be 100644 --- a/core/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java +++ b/legacy/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed; diff --git a/core/src/main/java/de/robv/android/xposed/IXposedHookInitPackageResources.java b/legacy/src/main/java/de/robv/android/xposed/IXposedHookInitPackageResources.java similarity index 100% rename from core/src/main/java/de/robv/android/xposed/IXposedHookInitPackageResources.java rename to legacy/src/main/java/de/robv/android/xposed/IXposedHookInitPackageResources.java diff --git a/core/src/main/java/de/robv/android/xposed/IXposedHookLoadPackage.java b/legacy/src/main/java/de/robv/android/xposed/IXposedHookLoadPackage.java similarity index 100% rename from core/src/main/java/de/robv/android/xposed/IXposedHookLoadPackage.java rename to legacy/src/main/java/de/robv/android/xposed/IXposedHookLoadPackage.java diff --git a/core/src/main/java/de/robv/android/xposed/IXposedHookZygoteInit.java b/legacy/src/main/java/de/robv/android/xposed/IXposedHookZygoteInit.java similarity index 100% rename from core/src/main/java/de/robv/android/xposed/IXposedHookZygoteInit.java rename to legacy/src/main/java/de/robv/android/xposed/IXposedHookZygoteInit.java diff --git a/legacy/src/main/java/de/robv/android/xposed/IXposedMod.java b/legacy/src/main/java/de/robv/android/xposed/IXposedMod.java new file mode 100644 index 000000000..50e6418d1 --- /dev/null +++ b/legacy/src/main/java/de/robv/android/xposed/IXposedMod.java @@ -0,0 +1,7 @@ +package de.robv.android.xposed; + +/** + * Marker interface for Xposed modules. Cannot be implemented directly. + */ +/* package */ interface IXposedMod { +} diff --git a/core/src/main/java/de/robv/android/xposed/SELinuxHelper.java b/legacy/src/main/java/de/robv/android/xposed/SELinuxHelper.java similarity index 67% rename from core/src/main/java/de/robv/android/xposed/SELinuxHelper.java rename to legacy/src/main/java/de/robv/android/xposed/SELinuxHelper.java index a5c8fe8cf..72fde6f77 100644 --- a/core/src/main/java/de/robv/android/xposed/SELinuxHelper.java +++ b/legacy/src/main/java/de/robv/android/xposed/SELinuxHelper.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed; import de.robv.android.xposed.services.BaseService; diff --git a/core/src/main/java/de/robv/android/xposed/XC_MethodHook.java b/legacy/src/main/java/de/robv/android/xposed/XC_MethodHook.java similarity index 88% rename from core/src/main/java/de/robv/android/xposed/XC_MethodHook.java rename to legacy/src/main/java/de/robv/android/xposed/XC_MethodHook.java index 12efd52a3..d9431bea9 100644 --- a/core/src/main/java/de/robv/android/xposed/XC_MethodHook.java +++ b/legacy/src/main/java/de/robv/android/xposed/XC_MethodHook.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed; import java.lang.reflect.Executable; diff --git a/core/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java b/legacy/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java similarity index 79% rename from core/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java rename to legacy/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java index a7839f8f9..d7f2b6508 100644 --- a/core/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java +++ b/legacy/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed; import de.robv.android.xposed.callbacks.XCallback; diff --git a/core/src/main/java/de/robv/android/xposed/XSharedPreferences.java b/legacy/src/main/java/de/robv/android/xposed/XSharedPreferences.java similarity index 93% rename from core/src/main/java/de/robv/android/xposed/XSharedPreferences.java rename to legacy/src/main/java/de/robv/android/xposed/XSharedPreferences.java index 3f645932c..47cd192ee 100644 --- a/core/src/main/java/de/robv/android/xposed/XSharedPreferences.java +++ b/legacy/src/main/java/de/robv/android/xposed/XSharedPreferences.java @@ -1,27 +1,5 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - package de.robv.android.xposed; -import static org.lsposed.lspd.core.ApplicationServiceClient.serviceClient; - import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; @@ -30,9 +8,10 @@ import com.android.internal.util.XmlUtils; -import org.lsposed.lspd.core.BuildConfig; -import org.lsposed.lspd.util.MetaDataReader; import org.lsposed.lspd.util.Utils.Log; +import org.matrix.vector.impl.core.VectorServiceClient; +import org.matrix.vector.impl.utils.VectorMetaDataReader; +import org.matrix.vector.legacy.BuildConfig; import org.xmlpull.v1.XmlPullParserException; import java.io.File; @@ -167,14 +146,14 @@ public XSharedPreferences(String packageName, String prefFileName) { int xposedminversion = -1; boolean xposedsharedprefs = false; try { - Map metaData = MetaDataReader.getMetaData(new File(m.get())); + Map metaData = VectorMetaDataReader.getMetaData(new File(m.get())); isModule = metaData.containsKey("xposedminversion"); if (isModule) { Object minVersionRaw = metaData.get("xposedminversion"); if (minVersionRaw instanceof Integer) { xposedminversion = (Integer) minVersionRaw; } else if (minVersionRaw instanceof String) { - xposedminversion = MetaDataReader.extractIntPart((String) minVersionRaw); + xposedminversion = VectorMetaDataReader.extractIntPart((String) minVersionRaw); } xposedsharedprefs = metaData.containsKey("xposedsharedprefs"); } @@ -184,7 +163,7 @@ public XSharedPreferences(String packageName, String prefFileName) { newModule = isModule && (xposedminversion > 92 || xposedsharedprefs); } if (newModule) { - mFile = new File(serviceClient.getPrefsPath(packageName), prefFileName + ".xml"); + mFile = new File(VectorServiceClient.INSTANCE.getPrefsPath(packageName), prefFileName + ".xml"); } else { mFile = new File(Environment.getDataDirectory(), "data/" + packageName + "/shared_prefs/" + prefFileName + ".xml"); } @@ -201,7 +180,7 @@ private void tryRegisterWatcher() { Path path = mFile.toPath(); try { if (sWatcher == null) { - sWatcher = new File(serviceClient.getPrefsPath("")).toPath().getFileSystem().newWatchService(); + sWatcher = new File(VectorServiceClient.INSTANCE.getPrefsPath("")).toPath().getFileSystem().newWatchService(); if (BuildConfig.DEBUG) Log.d(TAG, "Created WatchService instance"); } mWatchKey = path.getParent().register(sWatcher, StandardWatchEventKinds.ENTRY_CREATE, diff --git a/core/src/main/java/de/robv/android/xposed/XposedBridge.java b/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java similarity index 89% rename from core/src/main/java/de/robv/android/xposed/XposedBridge.java rename to legacy/src/main/java/de/robv/android/xposed/XposedBridge.java index 95f8b9ef6..4e37cf25b 100644 --- a/core/src/main/java/de/robv/android/xposed/XposedBridge.java +++ b/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - package de.robv.android.xposed; import android.app.ActivityThread; @@ -25,8 +5,8 @@ import android.content.res.TypedArray; import android.util.Log; -import org.lsposed.lspd.impl.LSPosedBridge; -import org.lsposed.lspd.impl.LSPosedHookCallback; +import org.matrix.vector.impl.hooks.VectorNativeHooker; +import org.matrix.vector.impl.hooks.VectorLegacyCallback; import org.matrix.vector.nativebridge.HookBridge; import org.matrix.vector.nativebridge.ResourcesHook; @@ -133,7 +113,7 @@ public static void initXResources() { * Returns the currently installed version of the Xposed framework. */ public static int getXposedVersion() { - return XposedInterface.API; + return XposedInterface.LIB_API; } /** @@ -207,7 +187,7 @@ public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook c throw new IllegalArgumentException("callback should not be null!"); } - if (!HookBridge.hookMethod(false, (Executable) hookMethod, LSPosedBridge.NativeHooker.class, callback.priority, callback)) { + if (!HookBridge.hookMethod(false, (Executable) hookMethod, VectorNativeHooker.class, callback.priority, callback)) { log("Failed to hook " + hookMethod); return null; } @@ -385,12 +365,12 @@ public synchronized void clear() { public static class LegacyApiSupport { private final XC_MethodHook.MethodHookParam param; - private final LSPosedHookCallback callback; + private final VectorLegacyCallback callback; private final Object[] snapshot; private int beforeIdx; - public LegacyApiSupport(LSPosedHookCallback callback, Object[] legacySnapshot) { + public LegacyApiSupport(VectorLegacyCallback callback, Object[] legacySnapshot) { this.param = new XC_MethodHook.MethodHookParam<>(); this.callback = callback; this.snapshot = legacySnapshot; @@ -404,15 +384,11 @@ public void handleBefore() { cb.beforeHookedMethod(param); } catch (Throwable t) { XposedBridge.log(t); - - // reset result (ignoring what the unexpectedly exiting callback did) param.setResult(null); param.returnEarly = false; - continue; } if (param.returnEarly) { - // skip remaining "before" callbacks and corresponding "after" callbacks beforeIdx++; break; } @@ -430,8 +406,6 @@ public void handleAfter() { cb.afterHookedMethod(param); } catch (Throwable t) { XposedBridge.log(t); - - // reset to last result (ignoring what the unexpectedly exiting callback did) if (lastThrowable == null) { param.setResult(lastResult); } else { @@ -442,21 +416,26 @@ public void handleAfter() { syncronizeApi(param, callback, false); } - private void syncronizeApi(XC_MethodHook.MethodHookParam param, LSPosedHookCallback callback, boolean forward) { + private void syncronizeApi(XC_MethodHook.MethodHookParam param, VectorLegacyCallback callback, boolean forward) { if (forward) { - param.method = callback.method; - param.thisObject = callback.thisObject; - param.args = callback.args; - param.result = callback.result; - param.throwable = callback.throwable; - param.returnEarly = callback.isSkipped; + param.method = callback.getMethod(); + param.thisObject = callback.getThisObject(); + param.args = callback.getArgs(); + param.result = callback.getResult(); + param.throwable = callback.getThrowable(); + param.returnEarly = callback.isSkipped(); } else { - callback.method = param.method; - callback.thisObject = param.thisObject; - callback.args = param.args; - callback.result = param.result; - callback.throwable = param.throwable; - callback.isSkipped = param.returnEarly; + callback.setThisObject(param.thisObject); + callback.setArgs(param.args); + + // Only write the result/throwable back if the legacy module explicitly skipped execution + if (param.returnEarly) { + if (param.throwable != null) { + callback.setThrowable(param.throwable); + } else { + callback.setResult(param.result); + } + } } } } diff --git a/core/src/main/java/de/robv/android/xposed/XposedHelpers.java b/legacy/src/main/java/de/robv/android/xposed/XposedHelpers.java similarity index 98% rename from core/src/main/java/de/robv/android/xposed/XposedHelpers.java rename to legacy/src/main/java/de/robv/android/xposed/XposedHelpers.java index 592be83d8..a3bf8dd26 100644 --- a/core/src/main/java/de/robv/android/xposed/XposedHelpers.java +++ b/legacy/src/main/java/de/robv/android/xposed/XposedHelpers.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed; import android.content.res.AssetManager; diff --git a/core/src/main/java/de/robv/android/xposed/XposedInit.java b/legacy/src/main/java/de/robv/android/xposed/XposedInit.java similarity index 90% rename from core/src/main/java/de/robv/android/xposed/XposedInit.java rename to legacy/src/main/java/de/robv/android/xposed/XposedInit.java index 39bbe4be5..9c23e3380 100644 --- a/core/src/main/java/de/robv/android/xposed/XposedInit.java +++ b/legacy/src/main/java/de/robv/android/xposed/XposedInit.java @@ -1,27 +1,5 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed; -import static org.lsposed.lspd.core.ApplicationServiceClient.serviceClient; -import static org.lsposed.lspd.deopt.PrebuiltMethodsDeopter.deoptResourceMethods; import static de.robv.android.xposed.XposedBridge.hookAllMethods; import static de.robv.android.xposed.XposedHelpers.callMethod; import static de.robv.android.xposed.XposedHelpers.findAndHookMethod; @@ -40,11 +18,13 @@ import android.os.Process; import android.util.ArrayMap; -import org.lsposed.lspd.impl.LSPosedContext; -import org.lsposed.lspd.models.PreLoadedApk; +import org.matrix.vector.impl.core.VectorDeopter; +import org.matrix.vector.impl.core.VectorModuleManager; +import org.matrix.vector.impl.core.VectorServiceClient; +import org.matrix.vector.impl.utils.VectorModuleClassLoader; import org.matrix.vector.nativebridge.NativeAPI; import org.matrix.vector.nativebridge.ResourcesHook; -import org.lsposed.lspd.util.LspModuleClassLoader; +import org.lsposed.lspd.models.PreLoadedApk; import org.lsposed.lspd.util.Utils.Log; import java.io.File; @@ -74,7 +54,7 @@ public static void hookResources() throws Throwable { return; } - deoptResourceMethods(); + VectorDeopter.deoptResourceMethods(); if (!ResourcesHook.initXResourcesNative()) { Log.e(TAG, "Cannot hook resources"); @@ -224,7 +204,7 @@ public static Map> getLoadedModules() { } public static void loadLegacyModules() { - var moduleList = serviceClient.getLegacyModulesList(); + var moduleList = VectorServiceClient.INSTANCE.getLegacyModulesList(); moduleList.forEach(module -> { var apk = module.apkPath; var name = module.packageName; @@ -238,9 +218,9 @@ public static void loadLegacyModules() { public static void loadModules(ActivityThread at) { var packages = (ArrayMap) XposedHelpers.getObjectField(at, "mPackages"); - serviceClient.getModulesList().forEach(module -> { + VectorServiceClient.INSTANCE.getModulesList().forEach(module -> { loadedModules.put(module.packageName, Optional.empty()); - if (!LSPosedContext.loadModule(at, module)) { + if (!VectorModuleManager.INSTANCE.loadModule(module, startsSystemServer, VectorServiceClient.INSTANCE.getProcessName())) { loadedModules.remove(module.packageName); } else { packages.remove(module.packageName); @@ -260,7 +240,7 @@ private static boolean initModule(ClassLoader mcl, String apk, List modu var count = 0; for (var moduleClassName : moduleClassNames) { try { - Log.i(TAG, " Loading class " + moduleClassName); + Log.v(TAG, " Loading class " + moduleClassName); Class moduleClass = mcl.loadClass(moduleClassName); @@ -301,7 +281,7 @@ private static boolean initModule(ClassLoader mcl, String apk, List modu * in assets/xposed_init. */ private static boolean loadModule(String name, String apk, PreLoadedApk file) { - Log.i(TAG, "Loading legacy module " + name + " from " + apk); + Log.v(TAG, "Loading legacy module " + name + " from " + apk); var sb = new StringBuilder(); var abis = Process.is64Bit() ? Build.SUPPORTED_64_BIT_ABIS : Build.SUPPORTED_32_BIT_ABIS; @@ -311,7 +291,7 @@ private static boolean loadModule(String name, String apk, PreLoadedApk file) { var librarySearchPath = sb.toString(); var initLoader = XposedInit.class.getClassLoader(); - var mcl = LspModuleClassLoader.loadApk(apk, file.preLoadedDexes, librarySearchPath, initLoader); + var mcl = VectorModuleClassLoader.loadApk(apk, file.preLoadedDexes, librarySearchPath, initLoader); try { if (mcl.loadClass(XposedBridge.class.getName()).getClassLoader() != initLoader) { diff --git a/core/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java b/legacy/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java similarity index 50% rename from core/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java rename to legacy/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java index e2fa76c93..9867153c9 100644 --- a/core/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java +++ b/legacy/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed.callbacks; import de.robv.android.xposed.IXposedHookZygoteInit; diff --git a/core/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java b/legacy/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java similarity index 71% rename from core/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java rename to legacy/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java index 5c39ec968..cb23c43ee 100644 --- a/core/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java +++ b/legacy/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed.callbacks; import android.content.res.XResources; diff --git a/core/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java b/legacy/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java similarity index 81% rename from core/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java rename to legacy/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java index 76de46c9a..790b565d7 100644 --- a/core/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java +++ b/legacy/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed.callbacks; import android.content.res.XResources; diff --git a/core/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java b/legacy/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java similarity index 74% rename from core/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java rename to legacy/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java index 848e3eeb3..a502a62b9 100644 --- a/core/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java +++ b/legacy/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed.callbacks; import android.content.pm.ApplicationInfo; diff --git a/core/src/main/java/de/robv/android/xposed/callbacks/XCallback.java b/legacy/src/main/java/de/robv/android/xposed/callbacks/XCallback.java similarity index 83% rename from core/src/main/java/de/robv/android/xposed/callbacks/XCallback.java rename to legacy/src/main/java/de/robv/android/xposed/callbacks/XCallback.java index 13958a65d..209d15279 100644 --- a/core/src/main/java/de/robv/android/xposed/callbacks/XCallback.java +++ b/legacy/src/main/java/de/robv/android/xposed/callbacks/XCallback.java @@ -1,28 +1,8 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed.callbacks; import android.os.Bundle; -import org.lsposed.lspd.deopt.PrebuiltMethodsDeopter; +import org.matrix.vector.impl.core.VectorDeopter; import java.io.Serializable; @@ -137,7 +117,7 @@ public static void callAll(Param param) { // deopt methods in system apps or priv-apps, this would be not necessary // only if we found out how to recompile their apks XC_LoadPackage.LoadPackageParam lpp = (XC_LoadPackage.LoadPackageParam) param; - PrebuiltMethodsDeopter.deoptMethods(lpp.packageName, lpp.classLoader); + VectorDeopter.deoptMethods(lpp.packageName, lpp.classLoader); } if (param.callbacks == null) diff --git a/core/src/main/java/de/robv/android/xposed/services/BaseService.java b/legacy/src/main/java/de/robv/android/xposed/services/BaseService.java similarity index 91% rename from core/src/main/java/de/robv/android/xposed/services/BaseService.java rename to legacy/src/main/java/de/robv/android/xposed/services/BaseService.java index 7c69ee079..b912f44d3 100644 --- a/core/src/main/java/de/robv/android/xposed/services/BaseService.java +++ b/legacy/src/main/java/de/robv/android/xposed/services/BaseService.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed.services; import java.io.ByteArrayInputStream; diff --git a/core/src/main/java/de/robv/android/xposed/services/DirectAccessService.java b/legacy/src/main/java/de/robv/android/xposed/services/DirectAccessService.java similarity index 83% rename from core/src/main/java/de/robv/android/xposed/services/DirectAccessService.java rename to legacy/src/main/java/de/robv/android/xposed/services/DirectAccessService.java index a8421c855..a56c14336 100644 --- a/core/src/main/java/de/robv/android/xposed/services/DirectAccessService.java +++ b/legacy/src/main/java/de/robv/android/xposed/services/DirectAccessService.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed.services; import java.io.BufferedInputStream; diff --git a/core/src/main/java/de/robv/android/xposed/services/FileResult.java b/legacy/src/main/java/de/robv/android/xposed/services/FileResult.java similarity index 69% rename from core/src/main/java/de/robv/android/xposed/services/FileResult.java rename to legacy/src/main/java/de/robv/android/xposed/services/FileResult.java index 415f5cb9c..699583962 100644 --- a/core/src/main/java/de/robv/android/xposed/services/FileResult.java +++ b/legacy/src/main/java/de/robv/android/xposed/services/FileResult.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed.services; import java.io.InputStream; diff --git a/legacy/src/main/java/org/matrix/vector/Startup.java b/legacy/src/main/java/org/matrix/vector/Startup.java new file mode 100644 index 000000000..f47fff407 --- /dev/null +++ b/legacy/src/main/java/org/matrix/vector/Startup.java @@ -0,0 +1,34 @@ +package org.matrix.vector; + +import org.lsposed.lspd.service.ILSPApplicationService; +import org.lsposed.lspd.util.Utils; +import org.matrix.vector.impl.core.VectorStartup; +import org.matrix.vector.impl.di.VectorBootstrap; +import org.matrix.vector.legacy.LegacyDelegateImpl; + +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedInit; + +public class Startup { + + public static void bootstrapXposed(boolean systemServerStarted) { + try { + VectorStartup.bootstrap(XposedInit.startsSystemServer, systemServerStarted); + XposedInit.loadLegacyModules(); + } catch (Throwable t) { + Utils.logE("Error during framework initialization", t); + } + } + + public static void initXposed(boolean isSystem, String processName, String appDir, ILSPApplicationService service) { + // Establish the Dependency Injection contract + VectorBootstrap.INSTANCE.init(new LegacyDelegateImpl()); + + // Initialize legacy resources and state + XposedBridge.initXResources(); + XposedInit.startsSystemServer = isSystem; + + // Hand off execution to the modern framework initialization + VectorStartup.init(isSystem, processName, appDir, service); + } +} diff --git a/legacy/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java b/legacy/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java new file mode 100644 index 000000000..50006d138 --- /dev/null +++ b/legacy/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java @@ -0,0 +1,148 @@ +package org.matrix.vector.legacy; + +import android.content.res.XResources; + +import org.lsposed.lspd.util.Utils; +import org.matrix.vector.impl.core.VectorServiceClient; +import org.matrix.vector.impl.di.LegacyFrameworkDelegate; +import org.matrix.vector.impl.di.LegacyPackageInfo; +import org.matrix.vector.impl.di.OriginalInvoker; +import org.matrix.vector.impl.hooks.VectorLegacyCallback; +import org.matrix.vector.impl.utils.VectorMetaDataReader; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Executable; +import java.util.Map; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XC_MethodReplacement; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.XposedInit; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +/** + * Implementation of the explicit dependency injection contract. + * Translates modern lifecycle events and hooks into legacy Xposed API operations. + */ +public class LegacyDelegateImpl implements LegacyFrameworkDelegate { + + @Override + public void loadModules(Object activityThread) { + XposedInit.loadModules((android.app.ActivityThread) activityThread); + } + + @Override + public void onPackageLoaded(LegacyPackageInfo info) { + XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks); + lpparam.packageName = info.getPackageName(); + lpparam.processName = info.getProcessName(); + lpparam.classLoader = info.getClassLoader(); + lpparam.appInfo = info.getAppInfo(); + lpparam.isFirstApplication = info.isFirstApplication(); + + if (info.isFirstApplication() && hasLegacyModule(info.getPackageName())) { + hookNewXSP(lpparam); + } + + XC_LoadPackage.callAll(lpparam); + } + + @Override + public void onSystemServerLoaded(ClassLoader classLoader) { + XposedInit.loadedPackagesInProcess.add("android"); + XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks); + lpparam.packageName = "android"; + lpparam.processName = "system_server"; + lpparam.classLoader = classLoader; + lpparam.isFirstApplication = true; + XC_LoadPackage.callAll(lpparam); + } + + @Override + public Object processLegacyHook(Executable executable, Object thisObject, Object[] args, Object[] legacyHooks, OriginalInvoker invokeOriginal) { + VectorLegacyCallback callback = new VectorLegacyCallback<>(executable, thisObject, args); + XposedBridge.LegacyApiSupport legacy = new XposedBridge.LegacyApiSupport<>(callback, legacyHooks); + + legacy.handleBefore(); + + if (!callback.isSkipped()) { + try { + Object result = invokeOriginal.invoke(); + callback.setResult(result); + } catch (Throwable t) { + callback.setThrowable(t); + } + } + + legacy.handleAfter(); + + if (callback.getThrowable() != null) { + sneakyThrow(callback.getThrowable()); + } + return callback.getResult(); + } + + @Override + public boolean isResourceHookingDisabled() { + return XposedInit.disableResources; + } + + @Override + public boolean hasLegacyModule(String packageName) { + return XposedInit.getLoadedModules().containsKey(packageName); + } + + @Override + public void setPackageNameForResDir(String packageName, String resDir) { + // Call a separate static inner class to prevent the verifier + // from looking at XResources when LegacyDelegateImpl is loaded. + ResourceProxy.set(packageName, resDir); + } + + // This class is only verified the FIRST time 'set' is called. + private static class ResourceProxy { + static void set(String p, String r) { + XResources.setPackageNameForResDir(p, r); + } + } + + private void hookNewXSP(XC_LoadPackage.LoadPackageParam lpparam) { + int xposedminversion = -1; + boolean xposedsharedprefs = false; + try { + Map metaData = VectorMetaDataReader.getMetaData(new File(lpparam.appInfo.sourceDir)); + Object minVersionRaw = metaData.get("xposedminversion"); + if (minVersionRaw instanceof Integer) { + xposedminversion = (Integer) minVersionRaw; + } else if (minVersionRaw instanceof String) { + xposedminversion = VectorMetaDataReader.extractIntPart((String) minVersionRaw); + } + xposedsharedprefs = metaData.containsKey("xposedsharedprefs"); + } catch (NumberFormatException | IOException ignored) { + } + + if (xposedminversion > 92 || xposedsharedprefs) { + XposedHelpers.findAndHookMethod("android.app.ContextImpl", lpparam.classLoader, "checkMode", int.class, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + if (((int) param.args[0] & 1) != 0) { + param.setThrowable(null); + } + } + }); + XposedHelpers.findAndHookMethod("android.app.ContextImpl", lpparam.classLoader, "getPreferencesDir", new XC_MethodReplacement() { + @Override + protected Object replaceHookedMethod(MethodHookParam param) { + return new File(VectorServiceClient.INSTANCE.getPrefsPath(lpparam.packageName)); + } + }); + } + } + + @SuppressWarnings("unchecked") + private static void sneakyThrow(Throwable t) throws T { + throw (T) t; + } +} diff --git a/native/README.md b/native/README.md index 3afe933ec..fdf947a02 100644 --- a/native/README.md +++ b/native/README.md @@ -31,7 +31,6 @@ This is the most significant module and represents the library's primary service - **`jni_bridge.h`**: Provides a set of helper macros (`VECTOR_NATIVE_METHOD`, `REGISTER_VECTOR_NATIVE_METHODS`, etc.) to standardize and simplify the tedious process of writing JNI boilerplate. - **`HookBridge`**: The engine for ART method hooking. It maintains a thread-safe map of all active hooks. It also includes some stability controls, such as using atomic operations to set the backup method trampoline and throwing a Java exception instead of causing a native crash if a user tries to invoke the original method of a failed hook. - **`ResourcesHook`**: Provides the functionality to intercept and rewrite Android's binary XML resources on the fly. It relies on non-public structures from `libandroidfw.so` and uses the `elf` module to find the necessary function symbols at runtime. -- **`DexParserBridge`**: Exposes a native DEX parser to the Java layer using a visitor pattern. This allows for analysis of an app's bytecode without the overhead of instantiating the entire DEX structure as Java objects. - **`NativeApiBridge`**: The JNI counterpart to the `core/native_api`. It exposes a method for the Java framework to register the filenames of third-party native modules. ### `common` & `framework` diff --git a/native/include/jni/jni_hooks.h b/native/include/jni/jni_hooks.h index 703ae0114..ff232f7cd 100644 --- a/native/include/jni/jni_hooks.h +++ b/native/include/jni/jni_hooks.h @@ -9,9 +9,6 @@ namespace vector::native::jni { -/// Registers the JNI methods for the DexParserBridge. -void RegisterDexParserBridge(JNIEnv *env); - /// Registers the JNI methods for the HookBridge. void RegisterHookBridge(JNIEnv *env); diff --git a/native/src/core/context.cpp b/native/src/core/context.cpp index 2db2e1032..0fa95790d 100644 --- a/native/src/core/context.cpp +++ b/native/src/core/context.cpp @@ -102,7 +102,6 @@ void Context::InitHooks(JNIEnv *env) { jni::RegisterResourcesHook(env); jni::RegisterHookBridge(env); jni::RegisterNativeApiBridge(env); - jni::RegisterDexParserBridge(env); } lsplant::ScopedLocalRef Context::FindClassFromLoader(JNIEnv *env, jobject class_loader, diff --git a/native/src/jni/dex_parser_bridge.cpp b/native/src/jni/dex_parser_bridge.cpp deleted file mode 100644 index 41549a7e0..000000000 --- a/native/src/jni/dex_parser_bridge.cpp +++ /dev/null @@ -1,960 +0,0 @@ -#include -#include - -#include - -#include "jni/jni_bridge.h" -#include "jni/jni_hooks.h" - -/** - * @file dex_parser_bridge.cpp - * @brief Implements a JNI bridge to a native DEX file parser. - * - * This bridge provides a memory-efficient way for Java code to parse Android DEX files. - * It avoids creating a complete object representation of the DEX file in memory, - * which can be very large. - * - * It employs a visitor pattern: - * 1. The `openDex` method performs an initial parse of the DEX file's main sections - * (strings, types, fields, methods, classes) and returns them - * to the Java caller as primitive arrays. - * It stores the detailed parsed data in a native `DexParser` object. - * 2. The `visitClass` method then iterates through the parsed classes and - * invokes callback methods on a Java "visitor" object for - * each class, field, and method. - * - * This approach minimizes JNI overhead and memory consumption by processing data - * in a streaming fashion and only creating Java objects as needed for the visitor callbacks. - */ - -namespace { -// Type aliases for representing DEX encoded values and annotations. -// These structures temporarily hold parsed annotation data before it's converted to Java objects. - -// A DEX encoded value, represented as a tuple of its type and raw byte data. -using Value = std::tuple /*data*/>; -// A DEX encoded array, which is a vector of encoded values. -using Array = std::vector; -// A list of encoded arrays. A list is used because its elements won't be -// reallocated, which is important when indices are stored. -using ArrayList = std::list; -// An element of an annotation, consisting of a name (index into string table) and a value. -using Element = std::tuple; -// A list of annotation elements. -using ElementList = std::vector; -// A DEX annotation, containing its visibility, type, and a list of its elements. -using Annotation = std::tuple; -// A list of annotations. -using AnnotationList = std::vector; - -/** - * @class DexParser - * @brief Extends slicer's dex::Reader to hold parsed class, method, and annotation data. - * - * This class serves as the main native handle for a parsed DEX file. - * It stores structured data that has been read from the DEX file, - * making it readily available for the `visitClass` function. - */ -class DexParser : public dex::Reader { -public: - DexParser(const dex::u1 *data, size_t size) : dex::Reader(data, size, nullptr, 0) {} - - /** - * @struct ClassData - * @brief Holds all relevant information for a single class definition. - * - * This structure is populated during the `openDex` phase and contains indices - * pointing to the DEX file's various data pools (types, fields, methods). - */ - struct ClassData { - std::vector interfaces; - std::vector static_fields; - std::vector static_fields_access_flags; - std::vector instance_fields; - std::vector instance_fields_access_flags; - std::vector direct_methods; - std::vector direct_methods_access_flags; - std::vector direct_methods_code; // Pointers to method bytecode - std::vector virtual_methods; - std::vector virtual_methods_access_flags; - std::vector virtual_methods_code; // Pointers to method bytecode - std::vector annotations; - }; - - /** - * @struct MethodBody - * @brief Lazily-parsed information from a method's bytecode. - * - * This data is only computed when a method is visited in `visitClass`, - * saving significant processing time if the caller is not interested in method body details. - */ - struct MethodBody { - bool loaded = false; // Flag to indicate if this body has been parsed yet. - std::vector referred_strings; - std::vector accessed_fields; // Fields read from (iget/sget) - std::vector assigned_fields; // Fields written to (iput/sput) - std::vector invoked_methods; - std::vector opcodes; - }; - - // Parsed data storage - std::vector class_data; // One entry per ClassDef in the DEX file. - // Mappings from an item's index to a list of annotation indices. - // Using phmap::flat_hash_map for fast lookups. - phmap::flat_hash_map> field_annotations; - phmap::flat_hash_map> method_annotations; - phmap::flat_hash_map> parameter_annotations; - - // Lazily populated map of method index to its parsed body. - phmap::flat_hash_map method_bodies; -}; - -/** - * @brief Parses a variable-length integer from the DEX byte stream. - * @tparam T The integral type to parse (e.g., int8_t, int32_t). - * @param pptr Pointer to the current position in the byte stream. - * @param size The number of bytes to read (1 to sizeof(T)). - * @return A vector of bytes containing the parsed value. - */ -template -static std::vector ParseIntValue(const dex::u1 **pptr, size_t size) { - static_assert(std::is_integral::value, "must be an integral type"); - std::vector ret(sizeof(T)); - // Use reinterpret_cast to type-pun the byte vector's data into the target integer type. - T &value = *reinterpret_cast(ret.data()); - value = 0; // Ensure starting from a clean state. - for (size_t i = 0; i < size; ++i) { - value |= T(*(*pptr)++) << (i * 8); - } - - // If the type is signed and we read fewer bytes than its full size, - // we need to manually sign-extend the value. - if constexpr (std::is_signed_v) { - size_t shift = (sizeof(T) - size) * 8; - if (shift > 0) { - value = T(value << shift) >> shift; - } - } - return ret; -} - -/** - * @brief Parses a variable-length float from the DEX byte stream. - * @tparam T The floating-point type to parse (float or double). - * @param pptr Pointer to the current position in the byte stream. - * @param size The number of bytes to read. - * @return A vector of bytes containing the parsed value. - */ -template -static std::vector ParseFloatValue(const dex::u1 **pptr, size_t size) { - std::vector ret(sizeof(T), 0); - T &value = *reinterpret_cast(ret.data()); - // The value is right-padded with zero bytes, so we copy into the higher-order bytes. - int start_byte = sizeof(T) - size; - for (dex::u1 *p = reinterpret_cast(&value) + start_byte; size > 0; --size) { - *p++ = *(*pptr)++; - } - return ret; -} - -// Forward declarations for recursive parsing functions. -Annotation ParseAnnotation(const dex::u1 **annotation, AnnotationList &annotation_list, - ArrayList &array_list); - -Array ParseArray(const dex::u1 **array, AnnotationList &annotation_list, ArrayList &array_list); - -/** - * @brief Parses a single `encoded_value` from the byte stream. - * This is the core of the annotation parsing logic and - * handles all possible value types recursively. - */ -Value ParseValue(const dex::u1 **value, AnnotationList &annotation_list, ArrayList &array_list) { - Value res; - auto &[type, value_content] = res; - auto header = *(*value)++; - type = header & dex::kEncodedValueTypeMask; - dex::u1 arg = header >> dex::kEncodedValueArgShift; - switch (type) { - // For numeric types, `arg` is `size - 1`. - case dex::kEncodedByte: - value_content = ParseIntValue(value, arg + 1); - break; - case dex::kEncodedShort: - value_content = ParseIntValue(value, arg + 1); - break; - case dex::kEncodedChar: - value_content = ParseIntValue(value, arg + 1); - break; - case dex::kEncodedInt: - value_content = ParseIntValue(value, arg + 1); - break; - case dex::kEncodedLong: - value_content = ParseIntValue(value, arg + 1); - break; - case dex::kEncodedFloat: - value_content = ParseFloatValue(value, arg + 1); - break; - case dex::kEncodedDouble: - value_content = ParseFloatValue(value, arg + 1); - break; - // For index types, the value is the index itself. - case dex::kEncodedMethodType: - case dex::kEncodedMethodHandle: - case dex::kEncodedString: - case dex::kEncodedType: - case dex::kEncodedField: - case dex::kEncodedMethod: - case dex::kEncodedEnum: - value_content = ParseIntValue(value, arg + 1); - break; - // For complex types, we parse them recursively and store an index to the - // parsed object. - case dex::kEncodedArray: - value_content.resize(sizeof(jint)); - *reinterpret_cast(value_content.data()) = static_cast(array_list.size()); - array_list.emplace_back(ParseArray(value, annotation_list, array_list)); - break; - case dex::kEncodedAnnotation: - value_content.resize(sizeof(jint)); - *reinterpret_cast(value_content.data()) = static_cast(annotation_list.size()); - annotation_list.emplace_back(ParseAnnotation(value, annotation_list, array_list)); - break; - case dex::kEncodedNull: - // No value content needed. - break; - case dex::kEncodedBoolean: - // The boolean value is stored in the `arg` part of the header. - value_content = {static_cast(arg == 1)}; - break; - default: - // This should never be reached for a valid DEX file. - __builtin_unreachable(); - } - return res; -} - -/** - * @brief Parses an `encoded_annotation` structure. - */ -Annotation ParseAnnotation(const dex::u1 **annotation, AnnotationList &annotation_list, - ArrayList &array_list) { - Annotation ret = {dex::kVisibilityEncoded, dex::ReadULeb128(annotation), ElementList{}}; - auto &[vis, type, element_list] = ret; - auto size = dex::ReadULeb128(annotation); - element_list.resize(size); - for (size_t j = 0; j < size; ++j) { - auto &[name, value] = element_list[j]; - name = static_cast(dex::ReadULeb128(annotation)); - value = ParseValue(annotation, annotation_list, array_list); - } - return ret; -} - -/** - * @brief Parses an `encoded_array` structure. - */ -Array ParseArray(const dex::u1 **array, AnnotationList &annotation_list, ArrayList &array_list) { - auto size = dex::ReadULeb128(array); - Array ret; - ret.reserve(size); - for (size_t i = 0; i < size; ++i) { - ret.emplace_back(ParseValue(array, annotation_list, array_list)); - } - return ret; -} - -/** - * @brief Parses an `AnnotationSetItem`, which is a collection of annotations. - */ -void ParseAnnotationSet(dex::Reader &dex, AnnotationList &annotation_list, ArrayList &array_list, - std::vector &indices, const dex::AnnotationSetItem *annotation_set) { - if (annotation_set == nullptr) { - return; - } - for (size_t i = 0; i < annotation_set->size; ++i) { - auto *item = dex.dataPtr(annotation_set->entries[i]); - auto *annotation_data = item->annotation; - // Store the index of the new annotation in the output list. - indices.emplace_back(annotation_list.size()); - // Parse the annotation and add it to the global list. - auto &[visibility, type, element_list] = annotation_list.emplace_back( - ParseAnnotation(&annotation_data, annotation_list, array_list)); - // The visibility is stored in the item, not the encoded annotation itself. - visibility = item->visibility; - } -} -} // namespace - -namespace vector::native::jni { -/** - * @brief JNI method to open a DEX file and perform initial parsing. - * @param data A direct java.nio.ByteBuffer containing the DEX file. - * @param args A jlongArray used for passing arguments. - * args[0] is an output parameter to store the native DexParser pointer (cookie). - * args[1] is an input flag to control whether to parse annotations. - * @return A java.lang.Object[] array containing the top-level DEX structures. - */ -VECTOR_DEF_NATIVE_METHOD(jobject, DexParserBridge, openDex, jobject data, jlongArray args) { - auto dex_size = env->GetDirectBufferCapacity(data); - if (dex_size == -1) { - env->ThrowNew(env->FindClass("java/io/IOException"), - "DEX data must be in a direct ByteBuffer"); - return nullptr; - } - auto *dex_data = env->GetDirectBufferAddress(data); - if (dex_data == nullptr) { - env->ThrowNew(env->FindClass("java/io/IOException"), "Failed to get direct buffer address"); - return nullptr; - } - - // Create the native parser object. - // This will be the handle for subsequent calls. - auto *dex_reader = new DexParser(reinterpret_cast(dex_data), dex_size); - auto *args_ptr = env->GetLongArrayElements(args, nullptr); - auto include_annotations = args_ptr[1]; - env->ReleaseLongArrayElements(args, args_ptr, JNI_ABORT); - // Store the pointer to the native object in the first element of the args array. - // This "cookie" will be passed back to other native methods. - env->SetLongArrayRegion(args, 0, 1, reinterpret_cast(&dex_reader)); - auto &dex = *dex_reader; - if (dex.IsCompact()) { - env->ThrowNew(env->FindClass("java/io/IOException"), "Compact dex is not supported"); - delete dex_reader; // Clean up before returning. - return nullptr; - } - - // Find classes needed for creating Java objects. - auto object_class = env->FindClass("java/lang/Object"); - auto string_class = env->FindClass("java/lang/String"); - auto int_array_class = env->FindClass("[I"); - // This is the main output array that will be returned to Java. - auto out = env->NewObjectArray(8, object_class, nullptr); - - // 1. Parse String IDs - auto out0 = - env->NewObjectArray(static_cast(dex.StringIds().size()), string_class, nullptr); - auto strings = dex.StringIds(); - for (size_t i = 0; i < strings.size(); ++i) { - const auto *ptr = dex.dataPtr(strings[i].string_data_off); - // The string data is MUTF-8 encoded. We skip the length prefix. - [[maybe_unused]] size_t len = dex::ReadULeb128(&ptr); - auto str = env->NewStringUTF(reinterpret_cast(ptr)); - env->SetObjectArrayElement(out0, static_cast(i), str); - env->DeleteLocalRef(str); - } - env->SetObjectArrayElement(out, 0, out0); - env->DeleteLocalRef(out0); - - // 2. Parse Type IDs - auto types = dex.TypeIds(); - auto out1 = env->NewIntArray(static_cast(types.size())); - auto *out1_ptr = env->GetIntArrayElements(out1, nullptr); - for (size_t i = 0; i < types.size(); ++i) { - out1_ptr[i] = static_cast(types[i].descriptor_idx); // Index into String table - } - env->ReleaseIntArrayElements(out1, out1_ptr, 0); - env->SetObjectArrayElement(out, 1, out1); - env->DeleteLocalRef(out1); - - // 3. Parse Proto IDs (Method Prototypes) - auto protos = dex.ProtoIds(); - auto out2 = env->NewObjectArray(static_cast(protos.size()), int_array_class, nullptr); - auto empty_type_list = dex::TypeList{.size = 0, .list = {}}; - for (size_t i = 0; i < protos.size(); ++i) { - auto &proto = protos[i]; - const auto ¶ms = proto.parameters_off - ? *dex.dataPtr(proto.parameters_off) - : empty_type_list; - - auto out2i = env->NewIntArray(static_cast(2 + params.size)); - auto *out2i_ptr = env->GetIntArrayElements(out2i, nullptr); - out2i_ptr[0] = static_cast(proto.shorty_idx); - out2i_ptr[1] = static_cast(proto.return_type_idx); - for (size_t j = 0; j < params.size; ++j) { - out2i_ptr[2 + j] = static_cast(params.list[j].type_idx); - } - env->ReleaseIntArrayElements(out2i, out2i_ptr, 0); - env->SetObjectArrayElement(out2, static_cast(i), out2i); - env->DeleteLocalRef(out2i); - } - env->SetObjectArrayElement(out, 2, out2); - env->DeleteLocalRef(out2); - - // 4. Parse Field IDs - auto fields = dex.FieldIds(); - auto out3 = env->NewIntArray(static_cast(3 * fields.size())); - auto *out3_ptr = env->GetIntArrayElements(out3, nullptr); - for (size_t i = 0; i < fields.size(); ++i) { - auto &field = fields[i]; - out3_ptr[3 * i] = static_cast(field.class_idx); // Defining class type index - out3_ptr[3 * i + 1] = static_cast(field.type_idx); // Field type index - out3_ptr[3 * i + 2] = static_cast(field.name_idx); // Field name string index - } - env->ReleaseIntArrayElements(out3, out3_ptr, 0); - env->SetObjectArrayElement(out, 3, out3); - env->DeleteLocalRef(out3); - - // 5. Parse Method IDs - auto methods = dex.MethodIds(); - auto out4 = env->NewIntArray(static_cast(3 * methods.size())); - auto *out4_ptr = env->GetIntArrayElements(out4, nullptr); - for (size_t i = 0; i < methods.size(); ++i) { - out4_ptr[3 * i] = static_cast(methods[i].class_idx); // Defining class type index - out4_ptr[3 * i + 1] = static_cast(methods[i].proto_idx); // Method prototype index - out4_ptr[3 * i + 2] = static_cast(methods[i].name_idx); // Method name string index - } - env->ReleaseIntArrayElements(out4, out4_ptr, 0); - env->SetObjectArrayElement(out, 4, out4); - env->DeleteLocalRef(out4); - - // 6. Parse Class Definitions and their data - auto classes = dex.ClassDefs(); - dex.class_data.resize(classes.size()); - - // These lists will store all annotations found in the DEX file. - AnnotationList annotation_list; - ArrayList array_list; - - for (size_t i = 0; i < classes.size(); ++i) { - auto &class_def = classes[i]; - - // Pointers to various parts of the class data. Initialize to safe defaults. - dex::u4 static_fields_count = 0; - dex::u4 instance_fields_count = 0; - dex::u4 direct_methods_count = 0; - dex::u4 virtual_methods_count = 0; - const dex::u1 *class_data_ptr = nullptr; - - const dex::AnnotationsDirectoryItem *annotations = nullptr; - const dex::AnnotationSetItem *class_annotation = nullptr; - dex::u4 field_annotations_count = 0; - dex::u4 method_annotations_count = 0; - dex::u4 parameter_annotations_count = 0; - - auto &class_data = dex.class_data[i]; - - // Parse implemented interfaces. - if (class_def.interfaces_off) { - auto defined_interfaces = dex.dataPtr(class_def.interfaces_off); - class_data.interfaces.resize(defined_interfaces->size); - for (size_t k = 0; k < class_data.interfaces.size(); ++k) { - class_data.interfaces[k] = defined_interfaces->list[k].type_idx; - } - } - - // Locate the annotations directory for this class, if it exists. - if (class_def.annotations_off != 0) { - annotations = dex.dataPtr(class_def.annotations_off); - if (annotations->class_annotations_off != 0) { - class_annotation = - dex.dataPtr(annotations->class_annotations_off); - } - field_annotations_count = annotations->fields_size; - method_annotations_count = annotations->methods_size; - parameter_annotations_count = annotations->parameters_size; - } - - // Read the core class data: fields and methods. - if (class_def.class_data_off != 0) { - class_data_ptr = dex.dataPtr(class_def.class_data_off); - static_fields_count = dex::ReadULeb128(&class_data_ptr); - instance_fields_count = dex::ReadULeb128(&class_data_ptr); - direct_methods_count = dex::ReadULeb128(&class_data_ptr); - virtual_methods_count = dex::ReadULeb128(&class_data_ptr); - // Pre-allocate vectors to improve performance. - class_data.static_fields.resize(static_fields_count); - class_data.static_fields_access_flags.resize(static_fields_count); - class_data.instance_fields.resize(instance_fields_count); - class_data.instance_fields_access_flags.resize(instance_fields_count); - class_data.direct_methods.resize(direct_methods_count); - class_data.direct_methods_access_flags.resize(direct_methods_count); - class_data.direct_methods_code.resize(direct_methods_count); - class_data.virtual_methods.resize(virtual_methods_count); - class_data.virtual_methods_access_flags.resize(virtual_methods_count); - class_data.virtual_methods_code.resize(virtual_methods_count); - } - - // Now, decode the field and method lists. - if (class_data_ptr) { - // Static fields - for (size_t k = 0, field_idx = 0; k < static_fields_count; ++k) { - field_idx += - dex::ReadULeb128(&class_data_ptr); // field_idx is a diff from previous - class_data.static_fields[k] = static_cast(field_idx); - class_data.static_fields_access_flags[k] = - static_cast(dex::ReadULeb128(&class_data_ptr)); - } - - // Instance fields - for (size_t k = 0, field_idx = 0; k < instance_fields_count; ++k) { - field_idx += dex::ReadULeb128(&class_data_ptr); - class_data.instance_fields[k] = static_cast(field_idx); - class_data.instance_fields_access_flags[k] = - static_cast(dex::ReadULeb128(&class_data_ptr)); - } - - // Direct methods (static, private, constructors) - for (size_t k = 0, method_idx = 0; k < direct_methods_count; ++k) { - method_idx += dex::ReadULeb128(&class_data_ptr); - class_data.direct_methods[k] = static_cast(method_idx); - class_data.direct_methods_access_flags[k] = - static_cast(dex::ReadULeb128(&class_data_ptr)); - auto code_off = dex::ReadULeb128(&class_data_ptr); - class_data.direct_methods_code[k] = - code_off ? dex.dataPtr(code_off) : nullptr; - } - - // Virtual methods - for (size_t k = 0, method_idx = 0; k < virtual_methods_count; ++k) { - method_idx += dex::ReadULeb128(&class_data_ptr); - class_data.virtual_methods[k] = static_cast(method_idx); - class_data.virtual_methods_access_flags[k] = - static_cast(dex::ReadULeb128(&class_data_ptr)); - auto code_off = dex::ReadULeb128(&class_data_ptr); - class_data.virtual_methods_code[k] = - code_off ? dex.dataPtr(code_off) : nullptr; - } - } - - // Optionally skip the expensive annotation parsing. - if (!include_annotations) continue; - - // Parse annotations for the class, its fields, methods, and parameters. - ParseAnnotationSet(dex, annotation_list, array_list, class_data.annotations, - class_annotation); - - auto *field_annotations = - annotations ? reinterpret_cast(annotations + 1) - : nullptr; - for (size_t k = 0; k < field_annotations_count; ++k) { - auto *field_annotation = - dex.dataPtr(field_annotations[k].annotations_off); - ParseAnnotationSet( - dex, annotation_list, array_list, - dex.field_annotations[static_cast(field_annotations[k].field_idx)], - field_annotation); - } - - auto *method_annotations = field_annotations - ? reinterpret_cast( - field_annotations + field_annotations_count) - : nullptr; - for (size_t k = 0; k < method_annotations_count; ++k) { - auto *method_annotation = - dex.dataPtr(method_annotations[k].annotations_off); - ParseAnnotationSet( - dex, annotation_list, array_list, - dex.method_annotations[static_cast(method_annotations[k].method_idx)], - method_annotation); - } - - auto *parameter_annotations = method_annotations - ? reinterpret_cast( - method_annotations + method_annotations_count) - : nullptr; - for (size_t k = 0; k < parameter_annotations_count; ++k) { - auto *parameter_annotation = - dex.dataPtr(parameter_annotations[k].annotations_off); - auto &indices = - dex.parameter_annotations[static_cast(parameter_annotations[k].method_idx)]; - for (size_t l = 0; l < parameter_annotation->size; ++l) { - if (parameter_annotation->list[l].annotations_off != 0) { - auto *parameter_annotation_item = dex.dataPtr( - parameter_annotation->list[l].annotations_off); - ParseAnnotationSet(dex, annotation_list, array_list, indices, - parameter_annotation_item); - } - // A kNoIndex entry serves as a separator between parameter annotation sets. - indices.emplace_back(dex::kNoIndex); - } - } - } - - // If annotations were skipped, we are done. - if (!include_annotations) return out; - - // 7. Convert parsed C++ annotation structures to Java objects. - auto out5 = env->NewIntArray(static_cast(2 * annotation_list.size())); - auto out6 = - env->NewObjectArray(static_cast(2 * annotation_list.size()), object_class, nullptr); - auto out5_ptr = env->GetIntArrayElements(out5, nullptr); - size_t i = 0; - for (auto &[visibility, type, items] : annotation_list) { - auto out6i0 = env->NewIntArray(static_cast(2 * items.size())); - auto out6i0_ptr = env->GetIntArrayElements(out6i0, nullptr); - auto out6i1 = env->NewObjectArray(static_cast(items.size()), object_class, nullptr); - size_t j = 0; - for (auto &[name, value] : items) { - auto &[value_type, value_data] = value; - // The raw value data is passed in a direct ByteBuffer. - auto java_value = value_data.empty() - ? nullptr - : env->NewDirectByteBuffer(value_data.data(), value_data.size()); - env->SetObjectArrayElement(out6i1, static_cast(j), java_value); - out6i0_ptr[2 * j] = name; - out6i0_ptr[2 * j + 1] = value_type; - env->DeleteLocalRef(java_value); - ++j; - } - env->ReleaseIntArrayElements(out6i0, out6i0_ptr, 0); - env->SetObjectArrayElement(out6, static_cast(2 * i), out6i0); - env->SetObjectArrayElement(out6, static_cast(2 * i + 1), out6i1); - out5_ptr[2 * i] = visibility; - out5_ptr[2 * i + 1] = type; - env->DeleteLocalRef(out6i0); - env->DeleteLocalRef(out6i1); - ++i; - } - env->ReleaseIntArrayElements(out5, out5_ptr, 0); - env->SetObjectArrayElement(out, 5, out5); - env->SetObjectArrayElement(out, 6, out6); - env->DeleteLocalRef(out5); - env->DeleteLocalRef(out6); - - // 8. Convert parsed C++ array values to Java objects. - auto out7 = - env->NewObjectArray(static_cast(2 * array_list.size()), object_class, nullptr); - i = 0; - for (auto &array : array_list) { - auto out7i0 = env->NewIntArray(static_cast(array.size())); - auto out7i0_ptr = env->GetIntArrayElements(out7i0, nullptr); - auto out7i1 = env->NewObjectArray(static_cast(array.size()), object_class, nullptr); - size_t j = 0; - for (auto &value : array) { - auto &[value_type, value_data] = value; - auto java_value = value_data.empty() - ? nullptr - : env->NewDirectByteBuffer(value_data.data(), value_data.size()); - out7i0_ptr[j] = value_type; - env->SetObjectArrayElement(out7i1, static_cast(j), java_value); - env->DeleteLocalRef(java_value); - ++j; - } - env->ReleaseIntArrayElements(out7i0, out7i0_ptr, 0); - env->SetObjectArrayElement(out7, static_cast(2 * i), out7i0); - env->SetObjectArrayElement(out7, static_cast(2 * i + 1), out7i1); - env->DeleteLocalRef(out7i0); - env->DeleteLocalRef(out7i1); - ++i; - } - env->SetObjectArrayElement(out, 7, out7); - env->DeleteLocalRef(out7); - - return out; -} - -/** - * @brief JNI method to release the native DexParser object. - * @param cookie The pointer to the DexParser object created by `openDex`. - */ -VECTOR_DEF_NATIVE_METHOD(void, DexParserBridge, closeDex, jlong cookie) { - if (cookie != 0) delete reinterpret_cast(cookie); -} - -/** - * @brief Iterates through classes, fields, and methods, calling back to a Java - * visitor. - * @param cookie The pointer to the DexParser object. - * @param visitor The main Java visitor object. - * @param ...visitor_class/.._method Java reflection objects used to - * get method IDs and perform type checks. - */ -VECTOR_DEF_NATIVE_METHOD(void, DexParserBridge, visitClass, jlong cookie, jobject visitor, - jclass field_visitor_class, jclass method_visitor_class, - jobject class_visit_method, jobject field_visit_method, - jobject method_visit_method, jobject method_body_visit_method, - jobject stop_method) { - // Constants for DEX opcodes used in method body parsing. - static constexpr dex::u1 kOpcodeMask = 0xff; - static constexpr dex::u1 kOpcodeNoOp = 0x00; - static constexpr dex::u1 kOpcodeConstString = 0x1a; - static constexpr dex::u1 kOpcodeConstStringJumbo = 0x1b; - static constexpr dex::u1 kOpcodeIGetStart = 0x52; - static constexpr dex::u1 kOpcodeIGetEnd = 0x58; - static constexpr dex::u1 kOpcodeSGetStart = 0x60; - static constexpr dex::u1 kOpcodeSGetEnd = 0x66; - static constexpr dex::u1 kOpcodeIPutStart = 0x59; - static constexpr dex::u1 kOpcodeIPutEnd = 0x5f; - static constexpr dex::u1 kOpcodeSPutStart = 0x67; - static constexpr dex::u1 kOpcodeSPutEnd = 0x6d; - static constexpr dex::u1 kOpcodeInvokeStart = 0x6e; - static constexpr dex::u1 kOpcodeInvokeEnd = 0x72; - static constexpr dex::u1 kOpcodeInvokeRangeStart = 0x74; - static constexpr dex::u1 kOpcodeInvokeRangeEnd = 0x78; - // Constants for special "payload" opcodes that follow a NOP instruction. - static constexpr dex::u2 kInstPackedSwitchPlayLoad = 0x0100; - static constexpr dex::u2 kInstSparseSwitchPlayLoad = 0x0200; - static constexpr dex::u2 kInstFillArrayDataPlayLoad = 0x0300; - - if (cookie == 0) { - return; - } - auto &dex = *reinterpret_cast(cookie); - // Get jmethodIDs from the reflected java.lang.reflect.Method objects. - auto *visit_class = env->FromReflectedMethod(class_visit_method); - auto *visit_field = env->FromReflectedMethod(field_visit_method); - auto *visit_method = env->FromReflectedMethod(method_visit_method); - auto *visit_method_body = env->FromReflectedMethod(method_body_visit_method); - auto *stop = env->FromReflectedMethod(stop_method); - - auto classes = dex.ClassDefs(); - - for (size_t i = 0; i < classes.size(); ++i) { - auto &class_def = classes[i]; - auto &class_data = dex.class_data[i]; - - // --- Prepare arguments for the visit_class callback --- - // This involves converting C++ vectors of integers into Java int arrays. - auto interfaces = env->NewIntArray(static_cast(class_data.interfaces.size())); - env->SetIntArrayRegion(interfaces, 0, static_cast(class_data.interfaces.size()), - class_data.interfaces.data()); - auto static_fields = env->NewIntArray(static_cast(class_data.static_fields.size())); - env->SetIntArrayRegion(static_fields, 0, static_cast(class_data.static_fields.size()), - class_data.static_fields.data()); - auto static_fields_access_flags = - env->NewIntArray(static_cast(class_data.static_fields_access_flags.size())); - env->SetIntArrayRegion(static_fields_access_flags, 0, - static_cast(class_data.static_fields_access_flags.size()), - class_data.static_fields_access_flags.data()); - auto instance_fields = - env->NewIntArray(static_cast(class_data.instance_fields.size())); - env->SetIntArrayRegion(instance_fields, 0, - static_cast(class_data.instance_fields.size()), - class_data.instance_fields.data()); - auto instance_fields_access_flags = - env->NewIntArray(static_cast(class_data.instance_fields_access_flags.size())); - env->SetIntArrayRegion(instance_fields_access_flags, 0, - static_cast(class_data.instance_fields_access_flags.size()), - class_data.instance_fields_access_flags.data()); - auto direct_methods = env->NewIntArray(static_cast(class_data.direct_methods.size())); - env->SetIntArrayRegion(direct_methods, 0, - static_cast(class_data.direct_methods.size()), - class_data.direct_methods.data()); - auto direct_methods_access_flags = - env->NewIntArray(static_cast(class_data.direct_methods_access_flags.size())); - env->SetIntArrayRegion(direct_methods_access_flags, 0, - static_cast(class_data.direct_methods_access_flags.size()), - class_data.direct_methods_access_flags.data()); - auto virtual_methods = - env->NewIntArray(static_cast(class_data.virtual_methods.size())); - env->SetIntArrayRegion(virtual_methods, 0, - static_cast(class_data.virtual_methods.size()), - class_data.virtual_methods.data()); - auto virtual_methods_access_flags = - env->NewIntArray(static_cast(class_data.virtual_methods_access_flags.size())); - env->SetIntArrayRegion(virtual_methods_access_flags, 0, - static_cast(class_data.virtual_methods_access_flags.size()), - class_data.virtual_methods_access_flags.data()); - auto class_annotations = env->NewIntArray(static_cast(class_data.annotations.size())); - env->SetIntArrayRegion(class_annotations, 0, - static_cast(class_data.annotations.size()), - class_data.annotations.data()); - - // --- Call back to the Java visitor for the class --- - jobject member_visitor = env->CallObjectMethod( - visitor, visit_class, static_cast(class_def.class_idx), - static_cast(class_def.access_flags), static_cast(class_def.superclass_idx), - interfaces, static_cast(class_def.source_file_idx), static_fields, - static_fields_access_flags, instance_fields, instance_fields_access_flags, - direct_methods, direct_methods_access_flags, virtual_methods, - virtual_methods_access_flags, class_annotations); - - // --- Clean up local JNI references --- - env->DeleteLocalRef(interfaces); - env->DeleteLocalRef(static_fields); - env->DeleteLocalRef(static_fields_access_flags); - env->DeleteLocalRef(instance_fields); - env->DeleteLocalRef(instance_fields_access_flags); - env->DeleteLocalRef(direct_methods); - env->DeleteLocalRef(direct_methods_access_flags); - env->DeleteLocalRef(virtual_methods); - env->DeleteLocalRef(virtual_methods_access_flags); - env->DeleteLocalRef(class_annotations); - - // --- Visit fields --- - if (member_visitor && env->IsInstanceOf(member_visitor, field_visitor_class)) { - jboolean stopped = JNI_FALSE; - // This structured binding provides a clean way to iterate over both - // static and instance field collections. - for (auto &[fields, fields_access_flags] : - {std::tie(class_data.static_fields, class_data.static_fields_access_flags), - std::tie(class_data.instance_fields, class_data.instance_fields_access_flags)}) { - for (size_t j = 0; j < fields.size(); j++) { - auto field_idx = fields[j]; - auto access_flags = fields_access_flags[j]; - auto &field_annotations = dex.field_annotations[field_idx]; - auto annotations = - env->NewIntArray(static_cast(field_annotations.size())); - env->SetIntArrayRegion(annotations, 0, - static_cast(field_annotations.size()), - field_annotations.data()); - // Call back to Java for this field. - env->CallVoidMethod(member_visitor, visit_field, field_idx, access_flags, - annotations); - env->DeleteLocalRef(annotations); - // Check if the visitor wants to stop iteration. - stopped = env->CallBooleanMethod(member_visitor, stop); - if (stopped == JNI_TRUE) break; - } - if (stopped == JNI_TRUE) break; - } - } - - // --- Visit methods --- - if (member_visitor && env->IsInstanceOf(member_visitor, method_visitor_class)) { - jboolean stopped = JNI_FALSE; - // Iterate over both direct and virtual methods. - for (auto &[methods, methods_access_flags, methods_code] : - {std::tie(class_data.direct_methods, class_data.direct_methods_access_flags, - class_data.direct_methods_code), - std::tie(class_data.virtual_methods, class_data.virtual_methods_access_flags, - class_data.virtual_methods_code)}) { - for (size_t j = 0; j < methods.size(); j++) { - auto method_idx = methods[j]; - auto access_flags = methods_access_flags[j]; - auto code = methods_code[j]; - auto method_annotation = dex.method_annotations[method_idx]; - auto method_annotations = - env->NewIntArray(static_cast(method_annotation.size())); - env->SetIntArrayRegion(method_annotations, 0, - static_cast(method_annotation.size()), - method_annotation.data()); - auto parameter_annotation = dex.parameter_annotations[method_idx]; - auto parameter_annotations = - env->NewIntArray(static_cast(parameter_annotation.size())); - env->SetIntArrayRegion(parameter_annotations, 0, - static_cast(parameter_annotation.size()), - parameter_annotation.data()); - // Call back to Java for this method. - // This may return a "body visitor". - auto body_visitor = env->CallObjectMethod( - member_visitor, visit_method, method_idx, access_flags, code != nullptr, - method_annotations, parameter_annotations); - env->DeleteLocalRef(method_annotations); - env->DeleteLocalRef(parameter_annotations); - // --- Lazily parse the method body if requested --- - if (body_visitor && code != nullptr) { - auto &body = dex.method_bodies[method_idx]; - if (!body.loaded) { - // Using hash sets for efficient collection of unique indices. - phmap::flat_hash_set referred_strings; - phmap::flat_hash_set assigned_fields; - phmap::flat_hash_set accessed_fields; - phmap::flat_hash_set invoked_methods; - - const dex::u2 *inst = code->insns; - const dex::u2 *end = inst + code->insns_size; - // Iterate through the bytecode instructions. - while (inst < end) { - dex::u1 opcode = *inst & kOpcodeMask; - body.opcodes.push_back(static_cast(opcode)); - // Check for opcodes of interest. - if (opcode == kOpcodeConstString) { - auto str_idx = inst[1]; - referred_strings.emplace(str_idx); - } - if (opcode == kOpcodeConstStringJumbo) { - auto str_idx = *reinterpret_cast(&inst[1]); - referred_strings.emplace(static_cast(str_idx)); - } - if ((opcode >= kOpcodeIGetStart && opcode <= kOpcodeIGetEnd) || - (opcode >= kOpcodeSGetStart && opcode <= kOpcodeSGetEnd)) { - auto field_idx = inst[1]; - accessed_fields.emplace(field_idx); - } - if ((opcode >= kOpcodeIPutStart && opcode <= kOpcodeIPutEnd) || - (opcode >= kOpcodeSPutStart && opcode <= kOpcodeSPutEnd)) { - auto field_idx = inst[1]; - assigned_fields.emplace(field_idx); - } - if ((opcode >= kOpcodeInvokeStart && opcode <= kOpcodeInvokeEnd) || - (opcode >= kOpcodeInvokeRangeStart && - opcode <= kOpcodeInvokeRangeEnd)) { - auto callee = inst[1]; - invoked_methods.emplace(callee); - } - // Handle special payload instructions which have variable - // length. - if (opcode == kOpcodeNoOp) { - if (*inst == kInstPackedSwitchPlayLoad) { - inst += inst[1] * 2 + 3; - } else if (*inst == kInstSparseSwitchPlayLoad) { - inst += inst[1] * 4 + 1; - } else if (*inst == kInstFillArrayDataPlayLoad) { - inst += (*reinterpret_cast(&inst[2]) * - inst[1] + - 1) / - 2 + - 3; - } - } - // Advance instruction pointer by the known length of - // the current opcode. - inst += dex::opcode_len[opcode]; - } - // Copy the collected unique indices into the body's vectors. - body.referred_strings.assign(referred_strings.begin(), - referred_strings.end()); - body.assigned_fields.assign(assigned_fields.begin(), - assigned_fields.end()); - body.accessed_fields.assign(accessed_fields.begin(), - accessed_fields.end()); - body.invoked_methods.assign(invoked_methods.begin(), - invoked_methods.end()); - body.loaded = true; - } - // --- Prepare arguments and call back for the method body --- - auto referred_strings = - env->NewIntArray(static_cast(body.referred_strings.size())); - env->SetIntArrayRegion(referred_strings, 0, - static_cast(body.referred_strings.size()), - body.referred_strings.data()); - auto accessed_fields = - env->NewIntArray(static_cast(body.accessed_fields.size())); - env->SetIntArrayRegion(accessed_fields, 0, - static_cast(body.accessed_fields.size()), - body.accessed_fields.data()); - auto assigned_fields = - env->NewIntArray(static_cast(body.assigned_fields.size())); - env->SetIntArrayRegion(assigned_fields, 0, - static_cast(body.assigned_fields.size()), - body.assigned_fields.data()); - auto invoked_methods = - env->NewIntArray(static_cast(body.invoked_methods.size())); - env->SetIntArrayRegion(invoked_methods, 0, - static_cast(body.invoked_methods.size()), - body.invoked_methods.data()); - auto opcodes = env->NewByteArray(static_cast(body.opcodes.size())); - env->SetByteArrayRegion(opcodes, 0, static_cast(body.opcodes.size()), - body.opcodes.data()); - env->CallVoidMethod(body_visitor, visit_method_body, referred_strings, - invoked_methods, accessed_fields, assigned_fields, - opcodes); - } - stopped = env->CallBooleanMethod(member_visitor, stop); - if (stopped == JNI_TRUE) break; - } - if (stopped == JNI_TRUE) break; - } - } - // Check if the top-level visitor wants to stop. - if (env->CallBooleanMethod(visitor, stop) == JNI_TRUE) break; - } -} - -// Array of native method descriptors for JNI registration. -static JNINativeMethod gMethods[] = { - VECTOR_NATIVE_METHOD(DexParserBridge, openDex, "(Ljava/nio/ByteBuffer;[J)Ljava/lang/Object;"), - VECTOR_NATIVE_METHOD(DexParserBridge, closeDex, "(J)V"), - VECTOR_NATIVE_METHOD(DexParserBridge, visitClass, - "(JLjava/lang/Object;Ljava/lang/Class;Ljava/lang/Class;Ljava/lang/" - "reflect/Method;Ljava/lang/reflect/Method;Ljava/lang/reflect/" - "Method;Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V"), -}; - -/** - * @brief Registers the native methods with the JVM. - */ -void RegisterDexParserBridge(JNIEnv *env) { REGISTER_VECTOR_NATIVE_METHODS(DexParserBridge); } - -} // namespace vector::native::jni diff --git a/native/src/jni/hook_bridge.cpp b/native/src/jni/hook_bridge.cpp index b4d841cbd..5bc19975f 100644 --- a/native/src/jni/hook_bridge.cpp +++ b/native/src/jni/hook_bridge.cpp @@ -9,15 +9,6 @@ #include "jni/jni_hooks.h" namespace { -/** - * @struct ModuleCallback - * @brief Stores the jmethodIDs for the "modern" callback API. - * This API separates the logic that runs before and after the original method. - */ -struct ModuleCallback { - jmethodID before_method; - jmethodID after_method; -}; /** * @struct HookItem @@ -33,7 +24,7 @@ struct HookItem { // Callbacks are stored in multimaps, keyed by priority. // std::greater<> ensures that higher priority numbers are processed first. std::multimap> legacy_callbacks; - std::multimap> modern_callbacks; + std::multimap> modern_callbacks; private: // The backup is an atomic jobject. @@ -92,11 +83,7 @@ using SharedHashMap = phmap::parallel_flat_hash_map> hooked_methods; // Cached JNI method and field IDs for performance. -// Looking these up frequently is slow, so they are cached on first use. jmethodID invoke = nullptr; -jmethodID callback_ctor = nullptr; -jfieldID before_method_field = nullptr; -jfieldID after_method_field = nullptr; } // namespace namespace vector::native::jni { @@ -168,28 +155,10 @@ VECTOR_DEF_NATIVE_METHOD(jboolean, HookBridge, hookMethod, jboolean useModernApi // ensuring thread-safe modification of the callback lists. lsplant::JNIMonitor monitor(env, backup); + // Store a global reference to the callback object itself. if (useModernApi) { - // Lazy initialization of JNI IDs for the modern API. - if (before_method_field == nullptr) { - auto callback_class = env->GetObjectClass(callback); - callback_ctor = - env->GetMethodID(callback_class, "", - "(Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V"); - before_method_field = - env->GetFieldID(callback_class, "beforeInvocation", "Ljava/lang/reflect/Method;"); - after_method_field = - env->GetFieldID(callback_class, "afterInvocation", "Ljava/lang/reflect/Method;"); - } - // Extract the before/after methods from the Java callback object. - auto before_method = env->GetObjectField(callback, before_method_field); - auto after_method = env->GetObjectField(callback, after_method_field); - auto callback_type = ModuleCallback{ - .before_method = env->FromReflectedMethod(before_method), - .after_method = env->FromReflectedMethod(after_method), - }; - hook_item->modern_callbacks.emplace(priority, callback_type); + hook_item->modern_callbacks.emplace(priority, env->NewGlobalRef(callback)); } else { - // For the legacy API, store a global reference to the callback object itself. hook_item->legacy_callbacks.emplace(priority, env->NewGlobalRef(callback)); } return JNI_TRUE; @@ -213,28 +182,18 @@ VECTOR_DEF_NATIVE_METHOD(jboolean, HookBridge, unhookMethod, jboolean useModernA // Lock to safely modify the callback list. lsplant::JNIMonitor monitor(env, backup); - if (useModernApi) { - auto before_method = env->GetObjectField(callback, before_method_field); - auto before = env->FromReflectedMethod(before_method); - // Find the callback by comparing the before_method's ID. - for (auto i = hook_item->modern_callbacks.begin(); i != hook_item->modern_callbacks.end(); - ++i) { - if (before == i->second.before_method) { - hook_item->modern_callbacks.erase(i); - return JNI_TRUE; - } - } - } else { - // Find the callback by comparing the jobject directly. - for (auto i = hook_item->legacy_callbacks.begin(); i != hook_item->legacy_callbacks.end(); - ++i) { - if (env->IsSameObject(i->second, callback)) { - env->DeleteGlobalRef(i->second); // Clean up the global reference. - hook_item->legacy_callbacks.erase(i); - return JNI_TRUE; - } + // Select the correct multimap + auto &callbacks = useModernApi ? hook_item->modern_callbacks : hook_item->legacy_callbacks; + + // Find the callback by comparing the jobject directly. + for (auto i = callbacks.begin(); i != callbacks.end(); ++i) { + if (env->IsSameObject(i->second, callback)) { + env->DeleteGlobalRef(i->second); // Clean up the global reference. + callbacks.erase(i); + return JNI_TRUE; } } + return JNI_FALSE; } @@ -561,33 +520,29 @@ VECTOR_DEF_NATIVE_METHOD(jobjectArray, HookBridge, callbackSnapshot, jclass call // Lock to ensure a consistent snapshot of the callback lists. lsplant::JNIMonitor monitor(env, backup); - auto res = env->NewObjectArray(2, env->FindClass("[Ljava/lang/Object;"), nullptr); - auto modern = env->NewObjectArray((jsize)hook_item->modern_callbacks.size(), - env->FindClass("java/lang/Object"), nullptr); - auto legacy = env->NewObjectArray((jsize)hook_item->legacy_callbacks.size(), - env->FindClass("java/lang/Object"), nullptr); + // Get the generic Object class + jclass obj_class = env->FindClass("java/lang/Object"); + + // Construct the result array Object[2][] + // Use an existing array to reliably get the Class for Object[] + jobjectArray dummy_array = env->NewObjectArray(0, obj_class, nullptr); + jclass obj_array_class = env->GetObjectClass(dummy_array); + jobjectArray res = env->NewObjectArray(2, obj_array_class, nullptr); + + // Create modern and legacy arrays + // Use 'callback_class' (VectorHookRecord) for the modern array for strict type safety + jobjectArray modern = + env->NewObjectArray((jsize)hook_item->modern_callbacks.size(), callback_class, nullptr); + jobjectArray legacy = + env->NewObjectArray((jsize)hook_item->legacy_callbacks.size(), obj_class, nullptr); jsize i = 0; for (const auto &callback_pair : hook_item->modern_callbacks) { - // The clazz argument refers to the Java class where the native method is - // declared, provided by the macro VECTOR_DEF_NATIVE_METHOD. - auto before_method = - env->ToReflectedMethod(clazz, callback_pair.second.before_method, JNI_FALSE); - auto after_method = - env->ToReflectedMethod(clazz, callback_pair.second.after_method, JNI_FALSE); - // Re-create the Java callback object from the stored method IDs. - auto callback_object = - env->NewObject(callback_class, callback_ctor, before_method, after_method); - env->SetObjectArrayElement(modern, i++, callback_object); - // Clean up local references created during object construction. - env->DeleteLocalRef(before_method); - env->DeleteLocalRef(after_method); - env->DeleteLocalRef(callback_object); + env->SetObjectArrayElement(modern, i++, callback_pair.second); } i = 0; for (const auto &callback_pair : hook_item->legacy_callbacks) { - // The legacy list already stores a global ref to the callback object. env->SetObjectArrayElement(legacy, i++, callback_pair.second); } diff --git a/native/src/jni/resources_hook.cpp b/native/src/jni/resources_hook.cpp index ea95e3953..52765bb87 100644 --- a/native/src/jni/resources_hook.cpp +++ b/native/src/jni/resources_hook.cpp @@ -300,7 +300,7 @@ static JNINativeMethod gMethods[] = { "(Ljava/lang/ClassLoader;Ljava/lang/String;Ljava/lang/" "String;)Ljava/lang/ClassLoader;"), VECTOR_NATIVE_METHOD(ResourcesHook, rewriteXmlReferencesNative, - "(JLxposed/dummy/XResourcesSuperClass;Landroid/content/res/Resources;)V")}; + "(JLjava/lang/Object;Landroid/content/res/Resources;)V")}; void RegisterResourcesHook(JNIEnv *env) { REGISTER_VECTOR_NATIVE_METHODS(ResourcesHook); } } // namespace vector::native::jni diff --git a/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPInjectedModuleService.aidl b/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPInjectedModuleService.aidl index e9df2f508..c77ece391 100644 --- a/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPInjectedModuleService.aidl +++ b/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPInjectedModuleService.aidl @@ -3,7 +3,7 @@ package org.lsposed.lspd.service; import org.lsposed.lspd.service.IRemotePreferenceCallback; interface ILSPInjectedModuleService { - int getFrameworkPrivilege(); + long getFrameworkProperties(); Bundle requestRemotePreferences(String group, IRemotePreferenceCallback callback); diff --git a/services/daemon-service/src/main/java/org/lsposed/lspd/util/Utils.java b/services/daemon-service/src/main/java/org/lsposed/lspd/util/Utils.java index 49807810f..cfdd92bea 100644 --- a/services/daemon-service/src/main/java/org/lsposed/lspd/util/Utils.java +++ b/services/daemon-service/src/main/java/org/lsposed/lspd/util/Utils.java @@ -109,6 +109,14 @@ public static void logD(String msg, Throwable throwable) { Log.d(LOG_TAG, msg, throwable); } + public static void logV(Object msg) { + Log.v(LOG_TAG, msg.toString()); + } + + public static void logV(String msg, Throwable throwable) { + Log.v(LOG_TAG, msg, throwable); + } + public static void logW(String msg) { Log.w(LOG_TAG, msg); } diff --git a/services/libxposed b/services/libxposed index 496b76fa3..11f8945de 160000 --- a/services/libxposed +++ b/services/libxposed @@ -1 +1 @@ -Subproject commit 496b76fa3e5af87958ebef97bd160319e05da79b +Subproject commit 11f8945de4e24efc0eb0e2e87a2dd8284d8f7b66 diff --git a/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl b/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl index aeded3c62..11e2385e9 100644 --- a/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl +++ b/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl @@ -34,7 +34,7 @@ interface ILSPManagerService { ParcelFileDescriptor getModulesLog() = 17; - int getXposedVersionCode() = 18; + long getXposedVersionCode() = 18; String getXposedVersionName() = 19; diff --git a/settings.gradle.kts b/settings.gradle.kts index abc95ae74..c623fd46f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,14 +20,13 @@ rootProject.name = "Vector" include( ":app", - ":core", ":daemon", ":dex2oat", ":external:axml", ":external:apache", ":hiddenapi:stubs", ":hiddenapi:bridge", - ":magisk-loader", + ":legacy", ":services:manager-service", ":services:daemon-service", ":xposed", diff --git a/xposed/README.md b/xposed/README.md index bbc76abaf..9c652d733 100644 --- a/xposed/README.md +++ b/xposed/README.md @@ -1,5 +1,38 @@ # Xposed API implementation of the Vector framework -LSPosed is being refactored into a new project `Vector`. +This module implements the [libxposed](https://github.com/libxposed/api) API for the Vector framework. It serves as the primary bridge between the native ART hooking engine (`lsplant`) and module developers, providing a type-safe, OkHttp-style interceptor chain architecture. -This sub-project `xposed`, written in Kotlin, will be refactored from the `core` sub-project written in `Java`. +## Architectural Overview + +The `xposed` module is designed with strict boundaries to ensure stability during the Android boot process and application lifecycles. It is written entirely in Kotlin and operates independently of the legacy Xposed API (`de.robv.android.xposed`). +It defines a Dependency Injection (DI) contract (`LegacyFrameworkDelegate`) which the `legacy` module must implement and inject during startup. + +## Core Components + +### 1. The Hooking Engine + +* **`VectorHookBuilder`**: Implements the `HookBuilder` API. It validates the target `Executable`, bundles the module's `Hooker`, `priority`, and `ExceptionMode` into a `VectorHookRecord`, and registers it natively via JNI. +* **`VectorNativeHooker`**: The JNI trampoline target. When a hooked method is executed, the C++ layer invokes `callback(Array)` on this class. It fetches the active hooks (both modern and legacy) from the native registry as global `jobject` references, constructs the root `VectorChain`, and initiates execution. +* **`VectorChain`**: Implements the recursive `proceed()` state machine. + * **Exception Handling**: It implements the logic for `ExceptionMode`. In `PROTECTIVE` mode, if an interceptor throws an exception *before* calling `proceed()`, the chain skips the interceptor. If it throws *after* calling `proceed()`, the chain catches the exception and restores the cached downstream result/throwable to protect the host process. + +### 2. The Invocation System + +The `Invoker` system allows modules to execute methods while bypassing standard JVM access checks, with granular control over hook execution. + +* **`Type.Origin`**: Dispatches directly to JNI (`HookBridge.invokeOriginalMethod`), bypassing all active hooks. +* **`Type.Chain`**: Constructs a localized `VectorChain` containing only hooks with a priority less than or equal to the requested `maxPriority`, allowing modules to execute partial hook chains. +* **`VectorCtorInvoker`**: Handles constructor invocation. It separates memory allocation (`HookBridge.allocateObject`) from initialization (`invokeOriginalMethod` / `invokeSpecialMethod`) to support safe `newInstanceSpecial` logic. + +### 3. Dependency Injection Contract + +To maintain the separation of concerns, the `xposed` module communicates with the legacy Xposed ecosystem via `VectorBootstrap` and `LegacyFrameworkDelegate`. + +When `xposed` intercepts an Android lifecycle event (e.g., `LoadedApk.createClassLoader`), it dispatches the event internally via `VectorLifecycleManager` and then delegates the raw parameters to `LegacyFrameworkDelegate` so the `legacy` module can construct and dispatch the legacy `XC_LoadPackage` callbacks. + +### 4. In-Memory ClassLoading & Isolation + +Modules are executed strictly from memory using an isolated ClassLoader, ensuring zero disk footprint and maximum stealth against anti-cheat mechanisms. +* The module APK is loaded into `SharedMemory` (ashmem) to bypass Java heap limitations. Once the Android Runtime (ART) ingests the DEX buffers, the ashmem is instantly unmapped, preventing memory leaks and leaving no residual file descriptors. +* The `VectorModuleClassLoader` is attached exclusively to the Xposed Framework's classloader branch, preventing the target app from discovering the module via reflection or `ClassLoader.getParent()` chain-walking. +* `VectorURLStreamHandler` intercepts standard `jar:` requests, reading assets and resources natively from the module path without triggering Android's global `JarFile` cache, preventing OS-level file locks. diff --git a/xposed/build.gradle.kts b/xposed/build.gradle.kts index 2d153bb91..8edcc5cc4 100644 --- a/xposed/build.gradle.kts +++ b/xposed/build.gradle.kts @@ -1,3 +1,6 @@ +val versionCodeProvider: Provider by rootProject.extra +val versionNameProvider: Provider by rootProject.extra + plugins { alias(libs.plugins.agp.lib) alias(libs.plugins.kotlin) @@ -7,14 +10,24 @@ plugins { ktfmt { kotlinLangStyle() } android { - namespace = "org.matrix.vector.xposed" + namespace = "org.matrix.vector.impl" androidResources { enable = false } + defaultConfig { + consumerProguardFiles("consumer-rules.pro") + buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""") + buildConfigField("String", "VERSION_NAME", """"${versionNameProvider.get()}"""") + buildConfigField("long", "VERSION_CODE", versionCodeProvider.get()) + } + sourceSets { named("main") { java.srcDirs("src/main/kotlin", "libxposed/api/src/main/java") } } } dependencies { + implementation(projects.external.axml) + implementation(projects.hiddenapi.bridge) + implementation(projects.services.daemonService) compileOnly(libs.androidx.annotation) compileOnly(projects.hiddenapi.stubs) } diff --git a/xposed/consumer-rules.pro b/xposed/consumer-rules.pro new file mode 100644 index 000000000..d18731d55 --- /dev/null +++ b/xposed/consumer-rules.pro @@ -0,0 +1,13 @@ +# Preserve the libxposed public API surface for module developers +-keep class io.github.libxposed.** { *; } + +# Preserve all native methods (HookBridge, ResourcesHook, NativeAPI, etc.) +-keepclasseswithmembers,includedescriptorclasses class * { + native ; +} + +# Preserve the JNI Hook Trampoline +-keepclassmembers class org.matrix.vector.impl.hooks.VectorNativeHooker { + public (java.lang.reflect.Executable); + public java.lang.Object callback(java.lang.Object[]); +} diff --git a/xposed/libxposed b/xposed/libxposed index 88cc0781e..eb63ec2c4 160000 --- a/xposed/libxposed +++ b/xposed/libxposed @@ -1 +1 @@ -Subproject commit 88cc0781e1138477c6b2a6570a0a27692f91f175 +Subproject commit eb63ec2c49a34a9c3cd08a7eee2b5cd8332f46d2 diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt new file mode 100644 index 000000000..d92faabb6 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt @@ -0,0 +1,95 @@ +package org.matrix.vector.impl + +import android.content.SharedPreferences +import android.content.pm.ApplicationInfo +import android.os.ParcelFileDescriptor +import io.github.libxposed.api.XposedInterface +import io.github.libxposed.api.XposedModuleInterface.* +import java.io.FileNotFoundException +import java.lang.reflect.Constructor +import java.lang.reflect.Executable +import java.lang.reflect.Method +import java.util.concurrent.ConcurrentHashMap +import org.lsposed.lspd.service.ILSPInjectedModuleService +import org.lsposed.lspd.util.Utils.Log +import org.matrix.vector.impl.hooks.VectorCtorInvoker +import org.matrix.vector.impl.hooks.VectorHookBuilder +import org.matrix.vector.impl.hooks.VectorMethodInvoker +import org.matrix.vector.nativebridge.HookBridge + +/** + * Main framework context implementation. Provides modules with capabilities to hook executables, + * request invokers, and interact with the system. + */ +class VectorContext( + private val packageName: String, + private val applicationInfo: ApplicationInfo, + private val service: ILSPInjectedModuleService, +) : XposedInterface { + + private val remotePrefs = ConcurrentHashMap() + + override fun getFrameworkName(): String = BuildConfig.FRAMEWORK_NAME + + override fun getFrameworkVersion(): String = BuildConfig.VERSION_NAME + + override fun getFrameworkVersionCode(): Long = BuildConfig.VERSION_CODE + + override fun getFrameworkProperties(): Long { + return service.getFrameworkProperties() + } + + override fun hook(origin: Executable): XposedInterface.HookBuilder { + return VectorHookBuilder(origin) + } + + override fun hookClassInitializer(origin: Class<*>): XposedInterface.HookBuilder { + val clinit = + HookBridge.getStaticInitializer(origin) + ?: throw IllegalArgumentException("Class ${origin.name} has no static initializer") + return VectorHookBuilder(clinit) + } + + override fun deoptimize(executable: Executable): Boolean { + return HookBridge.deoptimizeMethod(executable) + } + + override fun getInvoker(method: Method): XposedInterface.Invoker<*, Method> { + return VectorMethodInvoker(method) + } + + override fun getInvoker(constructor: Constructor): XposedInterface.CtorInvoker { + return VectorCtorInvoker(constructor) + } + + override fun getModuleApplicationInfo(): ApplicationInfo = applicationInfo + + override fun getRemotePreferences(name: String): SharedPreferences { + return remotePrefs.getOrPut(name) { VectorRemotePreferences(service, name) } + } + + override fun listRemoteFiles(): Array { + return service.remoteFileList + } + + override fun openRemoteFile(name: String): ParcelFileDescriptor { + return service.openRemoteFile(name) + ?: throw FileNotFoundException("Cannot open remote file: $name") + } + + override fun log(priority: Int, tag: String?, msg: String) { + log(priority, tag, msg, null) + } + + override fun log(priority: Int, tag: String?, msg: String, tr: Throwable?) { + val finalTag = tag ?: "VectorContext" + val prefix = if (packageName.isNotEmpty()) "$packageName: " else "" + val fullMsg = buildString { + append(prefix).append(msg) + if (tr != null) { + append("\n").append(android.util.Log.getStackTraceString(tr)) + } + } + Log.println(priority, finalTag, fullMsg) + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorLifecycleManager.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorLifecycleManager.kt new file mode 100644 index 000000000..e1d73ac92 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorLifecycleManager.kt @@ -0,0 +1,143 @@ +package org.matrix.vector.impl + +import android.content.pm.ApplicationInfo +import android.os.Build +import androidx.annotation.RequiresApi +import io.github.libxposed.api.XposedModule +import io.github.libxposed.api.XposedModuleInterface.* +import java.util.concurrent.ConcurrentHashMap +import org.lsposed.lspd.util.Utils.Log + +/** Manages the dispatching of modern lifecycle events to loaded modules. */ +object VectorLifecycleManager { + + private const val TAG = "VectorLifecycle" + + val activeModules: MutableSet = ConcurrentHashMap.newKeySet() + + fun dispatchPackageLoaded( + packageName: String, + appInfo: ApplicationInfo, + isFirst: Boolean, + defaultClassLoader: ClassLoader, + ) { + val param = + object : PackageLoadedParam { + override fun getPackageName(): String = packageName + + override fun getApplicationInfo(): ApplicationInfo = appInfo + + override fun isFirstPackage(): Boolean = isFirst + + override fun getDefaultClassLoader(): ClassLoader = defaultClassLoader + } + + activeModules.forEach { module -> + runCatching { module.onPackageLoaded(param) } + .onFailure { + Log.e( + TAG, + "Error in onPackageLoaded for ${module.moduleApplicationInfo.packageName}", + it, + ) + } + } + } + + fun dispatchPackageReady( + packageName: String, + appInfo: ApplicationInfo, + isFirst: Boolean, + defaultClassLoader: ClassLoader, + classLoader: ClassLoader, + appComponentFactory: Any?, // Abstracted for API compatibility + ) { + val param = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && appComponentFactory != null) { + PackageReadyParamImplP( + packageName, + appInfo, + isFirst, + defaultClassLoader, + classLoader, + appComponentFactory, + ) + } else { + // Fallback for API < 28 (or if factory is null). + object : PackageReadyParam { + override fun getPackageName() = packageName + + override fun getApplicationInfo() = appInfo + + override fun isFirstPackage() = isFirst + + override fun getDefaultClassLoader() = defaultClassLoader + + override fun getClassLoader() = classLoader + + override fun getAppComponentFactory(): android.app.AppComponentFactory { + throw UnsupportedOperationException( + "AppComponentFactory is not available on this API level" + ) + } + } + } + + activeModules.forEach { module -> + runCatching { + Log.d(TAG, "dispatchPackageReady $param") + module.onPackageReady(param) + } + .onFailure { + Log.e( + TAG, + "Error in onPackageReady for ${module.moduleApplicationInfo.packageName}", + it, + ) + } + } + } + + fun dispatchSystemServerStarting(classLoader: ClassLoader) { + val param = + object : SystemServerStartingParam { + override fun getClassLoader(): ClassLoader = classLoader + } + + activeModules.forEach { module -> + runCatching { module.onSystemServerStarting(param) } + .onFailure { + Log.e( + TAG, + "Error in onSystemServerStarting for ${module.moduleApplicationInfo.packageName}", + it, + ) + } + } + } +} + +// Isolate the class so the Verifier doesn't crash on Android 8.1 and below +@RequiresApi(Build.VERSION_CODES.P) +private class PackageReadyParamImplP( + private val packageName: String, + private val appInfo: ApplicationInfo, + private val isFirst: Boolean, + private val defaultClassLoader: ClassLoader, + private val classLoader: ClassLoader, + private val appComponentFactory: Any, +) : PackageReadyParam { + override fun getPackageName() = packageName + + override fun getApplicationInfo() = appInfo + + override fun isFirstPackage() = isFirst + + override fun getDefaultClassLoader() = defaultClassLoader + + override fun getClassLoader() = classLoader + + override fun getAppComponentFactory(): android.app.AppComponentFactory { + return appComponentFactory as android.app.AppComponentFactory + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt new file mode 100644 index 000000000..db50b0cc6 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt @@ -0,0 +1,115 @@ +package org.matrix.vector.impl + +import android.content.SharedPreferences +import android.os.Bundle +import android.os.RemoteException +import android.util.ArraySet +import io.github.libxposed.api.error.XposedFrameworkError +import java.util.TreeMap +import java.util.concurrent.ConcurrentHashMap +import org.lsposed.lspd.service.ILSPInjectedModuleService +import org.lsposed.lspd.service.IRemotePreferenceCallback +import org.lsposed.lspd.util.Utils.Log + +@Suppress("DEPRECATION", "UNCHECKED_CAST") +private inline fun Bundle.getSerializableCompat(key: String): T? { + return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + getSerializable(key, java.io.Serializable::class.java) as? T + } else { + getSerializable(key) as? T + } +} + +@Suppress("UNCHECKED_CAST") +internal class VectorRemotePreferences(service: ILSPInjectedModuleService, group: String) : + SharedPreferences { + + private val map = ConcurrentHashMap() + private val listeners = mutableSetOf() + + private val callback = + object : IRemotePreferenceCallback.Stub() { + @Synchronized + override fun onUpdate(bundle: Bundle) { + val changes = ArraySet() + + if (bundle.containsKey("delete")) { + val deletes = + bundle.getSerializableCompat>("delete") as? Set + if (deletes != null) { + changes.addAll(deletes) + for (key in deletes) { + map.remove(key) + } + } + } + + if (bundle.containsKey("put")) { + val puts = + bundle.getSerializableCompat>("put") as? Map + if (puts != null) { + map.putAll(puts) + changes.addAll(puts.keys) + } + } + + synchronized(listeners) { + for (key in changes) { + listeners.forEach { listener -> + listener.onSharedPreferenceChanged(this@VectorRemotePreferences, key) + } + } + } + } + } + + init { + try { + val output = service.requestRemotePreferences(group, callback) + if (output.containsKey("map")) { + val initialMap = + output.getSerializableCompat>("map") as? Map + if (initialMap != null) { + map.putAll(initialMap) + } + } + } catch (e: RemoteException) { + Log.e("VectorContext", "Failed to request remote preferences for group: $group", e) + throw XposedFrameworkError("Remote preferences IPC failure", e) + } + } + + override fun getAll(): Map = TreeMap(map) + + override fun getString(key: String, defValue: String?): String? = + map[key] as? String ?: defValue + + override fun getStringSet(key: String, defValues: Set?): Set? = + map[key] as? Set ?: defValues + + override fun getInt(key: String, defValue: Int): Int = map[key] as? Int ?: defValue + + override fun getLong(key: String, defValue: Long): Long = map[key] as? Long ?: defValue + + override fun getFloat(key: String, defValue: Float): Float = map[key] as? Float ?: defValue + + override fun getBoolean(key: String, defValue: Boolean): Boolean = + map[key] as? Boolean ?: defValue + + override fun contains(key: String): Boolean = map.containsKey(key) + + override fun edit(): SharedPreferences.Editor = + throw UnsupportedOperationException("Read only implementation") + + override fun registerOnSharedPreferenceChangeListener( + listener: SharedPreferences.OnSharedPreferenceChangeListener + ) { + synchronized(listeners) { listeners.add(listener) } + } + + override fun unregisterOnSharedPreferenceChangeListener( + listener: SharedPreferences.OnSharedPreferenceChangeListener + ) { + synchronized(listeners) { listeners.remove(listener) } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt new file mode 100644 index 000000000..4ca9f47a8 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt @@ -0,0 +1,59 @@ +package org.matrix.vector.impl.core + +import java.lang.reflect.Executable +import org.lsposed.lspd.util.Utils +import org.matrix.vector.nativebridge.HookBridge + +/** + * Engine responsible for scanning and deoptimizing prebuilt framework methods. Ensures that hooks + * placed on heavily inlined methods are respected by the ART runtime. + */ +object VectorDeopter { + + private const val TAG = "VectorDeopter" + + @JvmStatic + fun deoptMethods(where: String, cl: ClassLoader?) { + val targets = VectorInlinedCallers.get(where) + if (targets.isEmpty()) return + + val searchClassLoader = cl ?: ClassLoader.getSystemClassLoader() + + for (target in targets) { + runCatching { + val clazz = Class.forName(target.className, false, searchClassLoader) + val executable: Executable = + if (target.isConstructor) { + clazz.getDeclaredConstructor(*target.params) + } else { + clazz.getDeclaredMethod(target.methodName, *target.params) + } + + // Allow access if restricted and pass to the native bridge + executable.isAccessible = true + HookBridge.deoptimizeMethod(executable) + } + .onFailure { + Utils.Log.v( + TAG, + "Skipping deopt for ${target.className}#${target.methodName}: ${it.message}", + ) + } + } + } + + fun deoptBootMethods() { + deoptMethods(VectorInlinedCallers.KEY_BOOT_IMAGE, null) + } + + @JvmStatic + fun deoptResourceMethods() { + if (Utils.isMIUI) { + deoptMethods(VectorInlinedCallers.KEY_BOOT_IMAGE_MIUI_RES, null) + } + } + + fun deoptSystemServerMethods(sysCL: ClassLoader) { + deoptMethods(VectorInlinedCallers.KEY_SYSTEM_SERVER, sysCL) + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorInlinedCallers.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorInlinedCallers.kt new file mode 100644 index 000000000..839ecca84 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorInlinedCallers.kt @@ -0,0 +1,154 @@ +package org.matrix.vector.impl.core + +import android.app.Instrumentation +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.res.AssetManager +import android.content.res.Configuration +import android.content.res.Resources +import android.util.DisplayMetrics +import android.util.TypedValue + +/** Defines a strongly-typed signature for an executable that requires deoptimization. */ +data class TargetExecutable( + val className: String, + val methodName: String, + val params: Array>, +) { + val isConstructor: Boolean + get() = methodName == "" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as TargetExecutable + if (className != other.className) return false + if (methodName != other.methodName) return false + return params.contentEquals(other.params) + } + + override fun hashCode(): Int { + var result = className.hashCode() + result = 31 * result + methodName.hashCode() + result = 31 * result + params.contentHashCode() + return result + } +} + +/** Provides a registry of methods known to inline target framework hooks. */ +object VectorInlinedCallers { + const val KEY_BOOT_IMAGE = "boot_image" + const val KEY_BOOT_IMAGE_MIUI_RES = "boot_image_miui_res" + const val KEY_SYSTEM_SERVER = "system_server" + + private val callers = mutableMapOf>() + + init { + callers[KEY_BOOT_IMAGE] = + listOf( + TargetExecutable( + "android.app.Instrumentation", + "newApplication", + arrayOf(ClassLoader::class.java, String::class.java, Context::class.java), + ), + TargetExecutable( + "android.app.Instrumentation", + "newApplication", + arrayOf(ClassLoader::class.java, Context::class.java), + ), + TargetExecutable( + "android.app.LoadedApk", + "makeApplicationInner", + arrayOf( + Boolean::class.javaPrimitiveType!!, + Instrumentation::class.java, + Boolean::class.javaPrimitiveType!!, + ), + ), + TargetExecutable( + "android.app.LoadedApk", + "makeApplicationInner", + arrayOf(Boolean::class.javaPrimitiveType!!, Instrumentation::class.java), + ), + TargetExecutable( + "android.app.LoadedApk", + "makeApplication", + arrayOf(Boolean::class.javaPrimitiveType!!, Instrumentation::class.java), + ), + TargetExecutable( + "android.app.ContextImpl", + "getSharedPreferencesPath", + arrayOf(String::class.java), + ), + ) + + callers[KEY_BOOT_IMAGE_MIUI_RES] = + listOf( + TargetExecutable( + "android.content.res.MiuiResources", + "init", + arrayOf(String::class.java), + ), + TargetExecutable( + "android.content.res.MiuiResources", + "updateMiuiImpl", + emptyArray(), + ), + // Simplified string-based resolution for unavailable classes + TargetExecutable( + "android.content.res.MiuiResources", + "loadOverlayValue", + arrayOf(TypedValue::class.java, Int::class.javaPrimitiveType!!), + ), + TargetExecutable( + "android.content.res.MiuiResources", + "getThemeString", + arrayOf(CharSequence::class.java), + ), + TargetExecutable( + "android.content.res.MiuiResources", + "", + arrayOf(ClassLoader::class.java), + ), + TargetExecutable("android.content.res.MiuiResources", "", emptyArray()), + TargetExecutable( + "android.content.res.MiuiResources", + "", + arrayOf( + AssetManager::class.java, + DisplayMetrics::class.java, + Configuration::class.java, + ), + ), + TargetExecutable( + "android.miui.ResourcesManager", + "initMiuiResource", + arrayOf(Resources::class.java, String::class.java), + ), + TargetExecutable( + "android.app.LoadedApk", + "getResources", + arrayOf(Resources::class.java), + ), + TargetExecutable( + "android.content.res.Resources", + "getSystem", + arrayOf(Resources::class.java), + ), + TargetExecutable( + "android.app.ApplicationPackageManager", + "getResourcesForApplication", + arrayOf(ApplicationInfo::class.java), + ), + TargetExecutable( + "android.app.ContextImpl", + "setResources", + arrayOf(Resources::class.java), + ), + ) + + callers[KEY_SYSTEM_SERVER] = emptyList() + } + + fun get(where: String): List = callers[where] ?: emptyList() +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt new file mode 100644 index 000000000..3081789be --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt @@ -0,0 +1,112 @@ +package org.matrix.vector.impl.core + +import android.os.Build +import android.os.Process +import io.github.libxposed.api.XposedModule +import io.github.libxposed.api.XposedModuleInterface.ModuleLoadedParam +import java.io.File +import org.lsposed.lspd.models.Module +import org.lsposed.lspd.util.Utils.Log +import org.matrix.vector.impl.VectorContext +import org.matrix.vector.impl.VectorLifecycleManager +import org.matrix.vector.impl.utils.VectorModuleClassLoader +import org.matrix.vector.nativebridge.NativeAPI + +/** + * Responsible for loading modules into the target process. Handles ClassLoader isolation and + * injects the framework context into the module instances. + */ +object VectorModuleManager { + + private const val TAG = "VectorModuleManager" + + /** + * Loads a module APK, instantiates its entry classes, and binds them to the Vector framework. + */ + fun loadModule(module: Module, isSystemServer: Boolean, processName: String): Boolean { + try { + Log.d(TAG, "Loading module ${module.packageName}") + + // Construct the native library search path + val librarySearchPath = buildString { + val abis = + if (Process.is64Bit()) Build.SUPPORTED_64_BIT_ABIS + else Build.SUPPORTED_32_BIT_ABIS + for (abi in abis) { + append(module.apkPath).append("!/lib/").append(abi).append(File.pathSeparator) + } + } + + // Create the isolated ClassLoader for the module + val initLoader = XposedModule::class.java.classLoader + val moduleClassLoader = + VectorModuleClassLoader.loadApk( + module.apkPath, + module.file.preLoadedDexes, + librarySearchPath, + initLoader, + ) + + // Security/Integrity Check: Ensure the module isn't bundling its own API classes + if ( + moduleClassLoader.loadClass(XposedModule::class.java.name).classLoader !== + initLoader + ) { + Log.e(TAG, "The Xposed API classes are compiled into ${module.packageName}") + return false + } + + // Create the Context that will be injected into the module + val vectorContext = + VectorContext( + packageName = module.packageName, + applicationInfo = module.applicationInfo, + service = module.service, // Our IPC client + ) + + // Instantiate the module entry classes + for (className in module.file.moduleClassNames) { + runCatching { + val moduleClass = moduleClassLoader.loadClass(className) + Log.v(TAG, "Loading class $moduleClass") + + if (!XposedModule::class.java.isAssignableFrom(moduleClass)) { + Log.e(TAG, "Class does not extend XposedModule, skipping.") + return@runCatching + } + + val constructor = moduleClass.getDeclaredConstructor() + constructor.isAccessible = true + val moduleInstance = constructor.newInstance() as XposedModule + + // Attach the framework context to the module + moduleInstance.attachFramework(vectorContext) + + // Register the active module to receive future lifecycle events + VectorLifecycleManager.activeModules.add(moduleInstance) + + // Trigger the initial onModuleLoaded callback + moduleInstance.onModuleLoaded( + object : ModuleLoadedParam { + override fun isSystemServer(): Boolean = isSystemServer + + override fun getProcessName(): String = processName + } + ) + } + .onFailure { e -> Log.e(TAG, " Failed to instantiate class $className", e) } + } + + // Register any native JNI entrypoints declared by the module + module.file.moduleLibraryNames.forEach { libraryName -> + NativeAPI.recordNativeEntrypoint(libraryName) + } + + Log.d(TAG, "Loaded module ${module.packageName} successfully.") + return true + } catch (e: Throwable) { + Log.e(TAG, "Fatal error loading module ${module.packageName}", e) + return false + } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorServiceClient.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorServiceClient.kt new file mode 100644 index 000000000..fc70e9097 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorServiceClient.kt @@ -0,0 +1,65 @@ +package org.matrix.vector.impl.core + +import android.os.IBinder +import android.os.ParcelFileDescriptor +import org.lsposed.lspd.models.Module +import org.lsposed.lspd.service.ILSPApplicationService +import org.lsposed.lspd.util.Utils.Log + +/** + * Singleton client for managing IPC communication with the injected manager service. Handles Binder + * death gracefully and ensures safe remote execution. + */ +object VectorServiceClient : ILSPApplicationService, IBinder.DeathRecipient { + + private const val TAG = "VectorServiceClient" + + private var service: ILSPApplicationService? = null + var processName: String = "" + private set + + @Synchronized + fun init(appService: ILSPApplicationService?, niceName: String) { + val binder = appService?.asBinder() + if (service == null && binder != null) { + runCatching { + service = appService + processName = niceName + binder.linkToDeath(this, 0) + } + .onFailure { + Log.e(TAG, "Failed to link to death for service in process: $niceName", it) + service = null + } + } + } + + override fun isLogMuted(): Boolean { + return runCatching { service?.isLogMuted == true }.getOrDefault(false) + } + + override fun getLegacyModulesList(): List { + return runCatching { service?.legacyModulesList }.getOrNull() ?: emptyList() + } + + override fun getModulesList(): List { + return runCatching { service?.modulesList }.getOrNull() ?: emptyList() + } + + override fun getPrefsPath(packageName: String): String? { + return runCatching { service?.getPrefsPath(packageName) }.getOrNull() + } + + override fun requestInjectedManagerBinder(binder: List): ParcelFileDescriptor? { + return runCatching { service?.requestInjectedManagerBinder(binder) }.getOrNull() + } + + override fun asBinder(): IBinder? { + return service?.asBinder() + } + + override fun binderDied() { + service?.asBinder()?.unlinkToDeath(this, 0) + service = null + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt new file mode 100644 index 000000000..4020d5e63 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt @@ -0,0 +1,90 @@ +package org.matrix.vector.impl.core + +import android.os.Build +import android.os.IBinder +import dalvik.system.DexFile +import org.lsposed.lspd.service.ILSPApplicationService +import org.matrix.vector.impl.hookers.* +import org.matrix.vector.impl.hooks.VectorHookBuilder + +/** + * Modern framework initialization and bootstrap sequence. Deploys interceptors into the ART runtime + * and handles early process deoptimization. + */ +object VectorStartup { + + @JvmStatic + fun init( + isSystem: Boolean, + processName: String?, + appDir: String?, + service: ILSPApplicationService?, + ) { + VectorServiceClient.init(service, processName ?: "android") + VectorDeopter.deoptBootMethods() + } + + @JvmStatic + fun bootstrap(isSystem: Boolean, systemServerStarted: Boolean) { + // Crash Dump Interceptor + Thread::class + .java + .declaredMethods + .firstOrNull { it.name == "dispatchUncaughtException" } + ?.let { VectorHookBuilder(it).intercept(CrashDumpHooker) } + + // Process-specific Interceptors + if (isSystem) { + val zygoteInitClass = Class.forName("com.android.internal.os.ZygoteInit") + zygoteInitClass.declaredMethods + .filter { it.name == "handleSystemServerProcess" } + .forEach { VectorHookBuilder(it).intercept(HandleSystemServerProcessHooker) } + } else { + DexFile::class + .java + .declaredMethods + .filter { + it.name == "openDexFile" || + it.name == "openInMemoryDexFile" || + it.name == "openInMemoryDexFiles" + } + .forEach { VectorHookBuilder(it).intercept(DexTrustHooker) } + } + + // Application Load Interceptors + val loadedApkClass = Class.forName("android.app.LoadedApk") + loadedApkClass.declaredConstructors.forEach { + // Hook all constructors of LoadedApk to catch early instantiations securely + VectorHookBuilder(it).intercept(LoadedApkCtorHooker) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + loadedApkClass.declaredMethods + .filter { it.name == "createAppFactory" } + .forEach { VectorHookBuilder(it).intercept(LoadedApkCreateAppFactoryHooker) } + } + + loadedApkClass.declaredMethods + .filter { it.name == "createOrUpdateClassLoaderLocked" } + .forEach { VectorHookBuilder(it).intercept(LoadedApkCreateCLHooker) } + + // ActivityThread Attachment Interceptor + val activityThreadClass = Class.forName("android.app.ActivityThread") + activityThreadClass.declaredMethods + .filter { it.name == "attach" } + .forEach { VectorHookBuilder(it).intercept(AppAttachHooker) } + + // Late System Server Injection + if (systemServerStarted) { + val activityService: IBinder? = android.os.ServiceManager.getService("activity") + if (activityService != null) { + val classLoader = activityService.javaClass.classLoader + if (classLoader != null) { + // Manually trigger the routines that the hooks normally would + HandleSystemServerProcessHooker.initSystemServer(classLoader, isLate = true) + StartBootstrapServicesHooker.dispatchSystemServerLoaded(classLoader) + } + } + } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/di/VectorBootstrap.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/di/VectorBootstrap.kt new file mode 100644 index 000000000..b10b0c8f7 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/di/VectorBootstrap.kt @@ -0,0 +1,67 @@ +package org.matrix.vector.impl.di + +import android.content.pm.ApplicationInfo +import java.lang.reflect.Executable + +/** Data class representing package load information for the legacy bridge. */ +data class LegacyPackageInfo( + val packageName: String, + val processName: String, + val classLoader: ClassLoader, + val appInfo: ApplicationInfo, + val isFirstApplication: Boolean, +) + +/** Functional interface for executing the original method within a legacy hook bypass. */ +fun interface OriginalInvoker { + fun invoke(): Any? +} + +/** + * The explicit contract that the `legacy` module must fulfill. The modern framework will call these + * methods at the appropriate lifecycle moments. + */ +interface LegacyFrameworkDelegate { + /** Instructs the legacy bridge to load legacy modules. */ + fun loadModules(activityThread: Any) + + /** Dispatches a package load event to legacy XC_LoadPackage callbacks. */ + fun onPackageLoaded(info: LegacyPackageInfo) + + /** Dispatches the system server load event to legacy callbacks. */ + fun onSystemServerLoaded(classLoader: ClassLoader) + + /** Processes legacy hooks wrapped around the original method invocation. */ + fun processLegacyHook( + executable: Executable, + thisObject: Any?, + args: Array, + legacyHooks: Array, + invokeOriginal: OriginalInvoker, + ): Any? + + /** Checks if resource hooking is disabled by the legacy configuration. */ + val isResourceHookingDisabled: Boolean + + fun setPackageNameForResDir(packageName: String, resDir: String?) + + /** Checks if a legacy module is active for the given package name. */ + fun hasLegacyModule(packageName: String): Boolean +} + +/** The central registry for framework bootstrapping. */ +object VectorBootstrap { + @Volatile + var delegate: LegacyFrameworkDelegate? = null + private set + + fun init(frameworkDelegate: LegacyFrameworkDelegate) { + check(delegate == null) { "VectorBootstrap is already initialized!" } + delegate = frameworkDelegate + } + + /** Helper to safely execute operations requiring the legacy delegate. */ + inline fun withLegacy(block: (LegacyFrameworkDelegate) -> Unit) { + delegate?.let(block) + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/AppAttachHooker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/AppAttachHooker.kt new file mode 100644 index 000000000..afd1fbe71 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/AppAttachHooker.kt @@ -0,0 +1,23 @@ +package org.matrix.vector.impl.hookers + +import io.github.libxposed.api.XposedInterface +import org.matrix.vector.impl.di.VectorBootstrap + +/** + * Intercepts the early ApplicationThread attachment phase. Triggers the legacy compatibility layer + * to load modules into the process. + */ +object AppAttachHooker : XposedInterface.Hooker { + override fun intercept(chain: XposedInterface.Chain): Any? { + // Execute the actual attach() method first + val result = chain.proceed() + + // Delegate legacy module loading via DI + val activityThread = chain.thisObject + if (activityThread != null) { + VectorBootstrap.withLegacy { delegate -> delegate.loadModules(activityThread) } + } + + return result + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/CrashDumpHooker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/CrashDumpHooker.kt new file mode 100644 index 000000000..bf696175c --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/CrashDumpHooker.kt @@ -0,0 +1,20 @@ +package org.matrix.vector.impl.hookers + +import io.github.libxposed.api.XposedInterface +import org.lsposed.lspd.util.Utils + +/** + * Intercepts uncaught exceptions in the framework to provide diagnostic logging before the process + * completely terminates. + */ +object CrashDumpHooker : XposedInterface.Hooker { + override fun intercept(chain: XposedInterface.Chain): Any? { + try { + val throwable = chain.args.firstOrNull() as? Throwable + if (throwable != null) { + Utils.logE("Crash unexpectedly", throwable) + } + } catch (ignored: Throwable) {} + return chain.proceed() + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/DexTrustHooker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/DexTrustHooker.kt new file mode 100644 index 000000000..44ee159d3 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/DexTrustHooker.kt @@ -0,0 +1,31 @@ +package org.matrix.vector.impl.hookers + +import android.os.Build +import io.github.libxposed.api.XposedInterface +import org.matrix.vector.nativebridge.HookBridge + +/** + * Intercepts DEX file parsing to dynamically mark our framework's ClassLoader as trusted. Prevents + * ART from blocking reflective access by the hooking engine. + */ +object DexTrustHooker : XposedInterface.Hooker { + override fun intercept(chain: XposedInterface.Chain): Any? { + val result = chain.proceed() + + var classLoader = chain.args.filterIsInstance().firstOrNull() + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P && classLoader == null) { + classLoader = DexTrustHooker::class.java.classLoader + } + + while (classLoader != null) { + if (classLoader === DexTrustHooker::class.java.classLoader) { + // Inform the native bridge that this DEX cookie is safe + HookBridge.setTrusted(result) + break + } + classLoader = classLoader.parent + } + + return result + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt new file mode 100644 index 000000000..e71f076c6 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt @@ -0,0 +1,195 @@ +package org.matrix.vector.impl.hookers + +import android.content.pm.ApplicationInfo +import android.os.Build +import androidx.annotation.RequiresApi +import io.github.libxposed.api.XposedInterface +import java.util.Collections +import java.util.WeakHashMap +import java.util.concurrent.ConcurrentHashMap +import org.lsposed.lspd.util.Utils +import org.matrix.vector.impl.VectorLifecycleManager +import org.matrix.vector.impl.di.LegacyPackageInfo +import org.matrix.vector.impl.di.VectorBootstrap + +/** Safe reflection helper */ +private inline fun Any.getFieldValue(name: String): T? { + var clazz: Class<*>? = this.javaClass + while (clazz != null) { + try { + val field = clazz.getDeclaredField(name) + field.isAccessible = true + return field.get(this) as? T + } catch (ignored: NoSuchFieldException) { + clazz = clazz.superclass + } + } + return null +} + +/** Centralized helper for determining context details */ +private object PackageContextHelper { + private val activityThreadClass by lazy { Class.forName("android.app.ActivityThread") } + private val currentPkgMethod by lazy { + activityThreadClass.getDeclaredMethod("currentPackageName").apply { isAccessible = true } + } + private val currentProcMethod by lazy { + activityThreadClass.getDeclaredMethod("currentProcessName").apply { isAccessible = true } + } + + data class ContextInfo( + val packageName: String, + val processName: String, + val isFirstPackage: Boolean, + ) + + fun resolve(loadedApk: Any, apkPackageName: String): ContextInfo { + var packageName = currentPkgMethod.invoke(null) as? String + var processName = currentProcMethod.invoke(null) as? String + + val isFirstPackage = + packageName != null && processName != null && packageName == apkPackageName + + if (!isFirstPackage) { + packageName = apkPackageName + processName = currentPkgMethod.invoke(null) as? String ?: apkPackageName + } else if (packageName == "android") { + packageName = "system" + } + + return ContextInfo(packageName!!, processName!!, isFirstPackage) + } +} + +/** Identity-based tracking for LoadedApk instances. */ +private object LoadedApkTracker { + // Tracks LoadedApk instances that are currently in their initial bootstrap phase + val activeApks: MutableSet = + Collections.synchronizedSet(Collections.newSetFromMap(WeakHashMap())) +} + +/** Tracks and prepares Application instances when their LoadedApk is instantiated. */ +object LoadedApkCtorHooker : XposedInterface.Hooker { + val trackedApks = ConcurrentHashMap.newKeySet() + + override fun intercept(chain: XposedInterface.Chain): Any? { + val result = chain.proceed() + val loadedApk = chain.thisObject ?: return result + + val packageName = loadedApk.getFieldValue("mPackageName") ?: return result + + VectorBootstrap.withLegacy { delegate -> + if (!delegate.isResourceHookingDisabled) { + val resDir = loadedApk.getFieldValue("mResDir") + delegate.setPackageNameForResDir(packageName, resDir) + } + } + + // OnePlus workaround to avoid custom opt crashing + val isPreload = + Throwable().stackTrace.any { + it.className == "android.app.ActivityThread\$ApplicationThread" && + it.methodName == "schedulePreload" + } + if (!isPreload) { + LoadedApkTracker.activeApks.add(loadedApk) + } + + return result + } +} + +/** Modern API Phase: onPackageLoaded */ +@RequiresApi(Build.VERSION_CODES.P) +object LoadedApkCreateAppFactoryHooker : XposedInterface.Hooker { + override fun intercept(chain: XposedInterface.Chain): Any? { + val loadedApk = chain.thisObject ?: return chain.proceed() + + // Ensure we only dispatch for instances we are tracking + if (!LoadedApkTracker.activeApks.contains(loadedApk)) return chain.proceed() + + val appInfo = chain.args[0] as ApplicationInfo + val defaultClassLoader = + chain.args[1] as? ClassLoader + ?: return chain.proceed() // Skip dispatch if there's no ClassLoader + + // Only dispatch if on API 29+ per libxposed API specification + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val apkPackageName = + loadedApk.getFieldValue("mPackageName") ?: appInfo.packageName + val ctx = PackageContextHelper.resolve(loadedApk, apkPackageName) + + VectorLifecycleManager.dispatchPackageLoaded( + ctx.packageName, + appInfo, + ctx.isFirstPackage, + defaultClassLoader, + ) + } + + return chain.proceed() + } +} + +/** Modern API Phase: onPackageReady and Legacy Phase: handleLoadPackage */ +object LoadedApkCreateCLHooker : XposedInterface.Hooker { + // intercepting createOrUpdateClassLoaderLocked(List addedPaths) + override fun intercept(chain: XposedInterface.Chain): Any? { + val loadedApk = chain.thisObject ?: return chain.proceed() + + // Proceed: Modern modules need onPackageReady even for Split APKs (args[0] != null) + val isInitialLoad = + chain.args.firstOrNull() == null && LoadedApkTracker.activeApks.contains(loadedApk) + val result = chain.proceed() + + try { + val apkPackageName = loadedApk.getFieldValue("mPackageName") ?: return result + val appInfo = + loadedApk.getFieldValue("mApplicationInfo") ?: return result + val classLoader = loadedApk.getFieldValue("mClassLoader") ?: return result + val defaultClassLoader = + loadedApk.getFieldValue("mDefaultClassLoader") ?: classLoader + + val ctx = PackageContextHelper.resolve(loadedApk, apkPackageName) + + // Dispatch Modern Lifecycle: onPackageReady + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val appComponentFactory = loadedApk.getFieldValue("mAppComponentFactory") + VectorLifecycleManager.dispatchPackageReady( + ctx.packageName, + appInfo, + ctx.isFirstPackage, + defaultClassLoader, + classLoader, + appComponentFactory, + ) + } + + // Legacy API: Only dispatch once during initial load + if (isInitialLoad) { + val mIncludeCode = loadedApk.getFieldValue("mIncludeCode") ?: true + if (ctx.isFirstPackage || mIncludeCode) { + VectorBootstrap.withLegacy { delegate -> + delegate.onPackageLoaded( + LegacyPackageInfo( + ctx.packageName, + ctx.processName, + classLoader, + appInfo, + ctx.isFirstPackage, + ) + ) + } + } + } + } catch (t: Throwable) { + Utils.logE("LoadedApkCreateCLHooker failed in post-proceed phase", t) + } finally { + // Cleanup: Once the initial load is done, we remove it from activeApks. + // Subsequent calls (Split APKs) will now be recognized as non-initial loads. + LoadedApkTracker.activeApks.remove(loadedApk) + } + + return result + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt new file mode 100644 index 000000000..a52b56f05 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt @@ -0,0 +1,77 @@ +package org.matrix.vector.impl.hookers + +import io.github.libxposed.api.XposedInterface +import org.matrix.vector.impl.VectorLifecycleManager +import org.matrix.vector.impl.core.VectorDeopter +import org.matrix.vector.impl.di.VectorBootstrap +import org.matrix.vector.impl.hooks.VectorHookBuilder + +/** + * Intercepts the system server initialization process to deoptimize targets and attach bootstrap + * hookers dynamically. + */ +object HandleSystemServerProcessHooker : XposedInterface.Hooker { + + interface Callback { + fun onSystemServerLoaded(classLoader: ClassLoader) + } + + @Volatile var callback: Callback? = null + + @Volatile + var systemServerCL: ClassLoader? = null + private set + + override fun intercept(chain: XposedInterface.Chain): Any? { + val result = chain.proceed() + val classLoader = Thread.currentThread().contextClassLoader + if (classLoader != null) { + initSystemServer(classLoader) + } + return result + } + + /** Performs system server initialization. */ + fun initSystemServer(classLoader: ClassLoader, isLate: Boolean = false) { + if (systemServerCL != null) return // Ensure this only runs once + systemServerCL = classLoader + + // Deoptimize heavily inlined system server paths + VectorDeopter.deoptSystemServerMethods(classLoader) + + if (!isLate) { + // Dynamically locate and hook the bootstrap service initializer + val sysServerClass = + Class.forName("com.android.server.SystemServer", false, classLoader) + val startMethod = + sysServerClass.declaredMethods.find { it.name == "startBootstrapServices" } + ?: throw NoSuchMethodException( + "com.android.server.SystemServer.startBootstrapServices not found" + ) + + // Ensure we can hook the private method + startMethod.isAccessible = true + VectorHookBuilder(startMethod).intercept(StartBootstrapServicesHooker) + } + + callback?.onSystemServerLoaded(classLoader) + } +} + +/** Dispatches the core system server bootstrap event to the module engines. */ +object StartBootstrapServicesHooker : XposedInterface.Hooker { + + override fun intercept(chain: XposedInterface.Chain): Any? { + HandleSystemServerProcessHooker.systemServerCL?.let { dispatchSystemServerLoaded(it) } + return chain.proceed() + } + + /** Dispatches module loading events. */ + fun dispatchSystemServerLoaded(classLoader: ClassLoader) { + // Dispatch to modern framework modules + VectorLifecycleManager.dispatchSystemServerStarting(classLoader) + + // Dispatch to legacy framework modules + VectorBootstrap.withLegacy { delegate -> delegate.onSystemServerLoaded(classLoader) } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt new file mode 100644 index 000000000..013d57914 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt @@ -0,0 +1,163 @@ +package org.matrix.vector.impl.hooks + +import io.github.libxposed.api.XposedInterface.CtorInvoker +import io.github.libxposed.api.XposedInterface.Invoker +import java.lang.reflect.Constructor +import java.lang.reflect.Executable +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method +import org.matrix.vector.impl.di.VectorBootstrap +import org.matrix.vector.nativebridge.HookBridge + +/** + * Base implementation of the Invoker system. Handles the resolution of [Invoker.Type] to determine + * whether to execute the original method directly or to construct a partial interceptor chain. + */ +internal abstract class BaseInvoker, U : Executable>( + protected val executable: U +) : Invoker { + + protected var type: Invoker.Type = Invoker.Type.Chain.FULL + + @Suppress("UNCHECKED_CAST") + override fun setType(type: Invoker.Type): T { + this.type = type + return this as T + } + + /** Resolves the current [type] and executes the underlying method. */ + protected fun proceedInvocation(thisObject: Any?, args: Array): Any? { + return when (val currentType = type) { + is Invoker.Type.Origin -> { + try { + HookBridge.invokeOriginalMethod(executable, thisObject, args) + } catch (e: InvocationTargetException) { + throw e.cause ?: e + } + } + is Invoker.Type.Chain -> { + val snapshots = + HookBridge.callbackSnapshot(VectorHookRecord::class.java, executable) + + @Suppress("UNCHECKED_CAST") + val allModernHooks = snapshots[0] as Array + val legacyHooks = snapshots[1] + + // Filter hooks to respect the maxPriority requested by the module + val filteredHooks = + allModernHooks.filter { it.priority <= currentType.maxPriority }.toTypedArray() + + val terminal: (Any?, Array) -> Any? = { tObj, tArgs -> + val delegate = VectorBootstrap.delegate + if (legacyHooks.isNotEmpty() && delegate != null) { + delegate.processLegacyHook(executable, tObj, tArgs, legacyHooks) { + HookBridge.invokeOriginalMethod(executable, tObj, *tArgs) + } + } else { + HookBridge.invokeOriginalMethod(executable, tObj, *tArgs) + } + } + + val chain = + VectorChain(executable, thisObject, arrayOf(*args), filteredHooks, 0, terminal) + chain.proceed() + } + } + } + + /** Helper to generate the JNI shorty for non-virtual special invocations. */ + protected fun getExecutableShorty(): CharArray { + val parameterTypes = executable.parameterTypes + val shorty = CharArray(parameterTypes.size + 1) + shorty[0] = getTypeShorty(if (executable is Method) executable.returnType else Void.TYPE) + for (i in 1..shorty.lastIndex) { + shorty[i] = getTypeShorty(parameterTypes[i - 1]) + } + return shorty + } + + private fun getTypeShorty(type: Class<*>): Char = + when (type) { + Int::class.javaPrimitiveType -> 'I' + Long::class.javaPrimitiveType -> 'J' + Float::class.javaPrimitiveType -> 'F' + Double::class.javaPrimitiveType -> 'D' + Boolean::class.javaPrimitiveType -> 'Z' + Byte::class.javaPrimitiveType -> 'B' + Char::class.javaPrimitiveType -> 'C' + Short::class.javaPrimitiveType -> 'S' + Void.TYPE -> 'V' + else -> 'L' + } +} + +/** Invoker implementation specifically for [Method] types. */ +internal class VectorMethodInvoker(method: Method) : + BaseInvoker(method) { + + override fun invoke(thisObject: Any?, vararg args: Any?): Any? { + return proceedInvocation(thisObject, args) + } + + override fun invokeSpecial(thisObject: Any, vararg args: Any?): Any? { + return HookBridge.invokeSpecialMethod( + executable, + getExecutableShorty(), + executable.declaringClass, + thisObject, + *args, + ) + } +} + +/** + * Invoker implementation specifically for [Constructor] types. Extends capabilities to allocate and + * initialize objects safely. + */ +internal class VectorCtorInvoker(constructor: Constructor) : + BaseInvoker, Constructor>(constructor), CtorInvoker { + + override fun invoke(thisObject: Any?, vararg args: Any?): Any? { + // Invoking a constructor as a method returns nothing (void/null) + proceedInvocation(thisObject, args) + return null + } + + override fun invokeSpecial(thisObject: Any, vararg args: Any?): Any? { + HookBridge.invokeSpecialMethod( + executable, + getExecutableShorty(), + executable.declaringClass, + thisObject, + *args, + ) + return null + } + + @Suppress("UNCHECKED_CAST") + override fun newInstance(vararg args: Any?): T { + // Allocate memory without invoking + val obj = HookBridge.allocateObject(executable.declaringClass) + // Drive the invocation (origin or chain) utilizing the allocated object + proceedInvocation(obj, args) + return obj + } + + @Suppress("UNCHECKED_CAST") + override fun newInstanceSpecial(subClass: Class, vararg args: Any?): U { + if (!executable.declaringClass.isAssignableFrom(subClass)) { + throw IllegalArgumentException( + "$subClass is not inherited from ${executable.declaringClass}" + ) + } + val obj = HookBridge.allocateObject(subClass) + HookBridge.invokeSpecialMethod( + executable, + getExecutableShorty(), + executable.declaringClass, + obj, + *args, + ) + return obj + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt new file mode 100644 index 000000000..cd8b75058 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt @@ -0,0 +1,114 @@ +package org.matrix.vector.impl.hooks + +import io.github.libxposed.api.XposedInterface +import io.github.libxposed.api.XposedInterface.Chain +import io.github.libxposed.api.XposedInterface.ExceptionMode +import java.lang.reflect.Executable +import org.lsposed.lspd.util.Utils + +/** Represents a registered hook configuration, stored natively by [HookBridge]. */ +data class VectorHookRecord( + val hooker: XposedInterface.Hooker, + val priority: Int, + val exceptionMode: ExceptionMode, +) + +/** + * Core interceptor chain engine. Manages recursive hook execution and enforces [ExceptionMode] + * protections. + */ +class VectorChain( + private val executable: Executable, + private val thisObj: Any?, + private val args: Array, + private val hooks: Array, + private val index: Int, + private val terminal: (thisObj: Any?, args: Array) -> Any?, +) : Chain { + + // Tracks if this specific chain node has forwarded execution downstream + internal var proceedCalled: Boolean = false + private set + + // Stores the actual result/exception from the rest of the chain/original method + internal var downstreamResult: Any? = null + internal var downstreamThrowable: Throwable? = null + + override fun getExecutable(): Executable = executable + + override fun getThisObject(): Any? = thisObj + + override fun getArgs(): List = args.toList() + + override fun getArg(index: Int): Any? = args[index] + + override fun proceed(): Any? = proceedWith(thisObj ?: Any(), args) + + override fun proceed(args: Array): Any? = proceedWith(thisObj ?: Any(), args) + + override fun proceedWith(thisObject: Any): Any? = proceedWith(thisObject, args) + + override fun proceedWith(thisObject: Any, args: Array): Any? { + proceedCalled = true + + // Reached the end of the modern hooks; trigger the original executable (and legacy hooks) + if (index >= hooks.size) { + return executeDownstream { terminal(thisObject, args) } + } + + val record = hooks[index] + val nextChain = VectorChain(executable, thisObject, args, hooks, index + 1, terminal) + + return try { + executeDownstream { record.hooker.intercept(nextChain) } + } catch (t: Throwable) { + handleInterceptorException(t, record, nextChain, thisObject, args) + } + } + + /** + * Executes the block and caches the downstream state so parent chains can recover it if the + * current interceptor crashes during post-processing. + */ + private inline fun executeDownstream(block: () -> Any?): Any? { + return try { + val result = block() + downstreamResult = result + result + } catch (t: Throwable) { + downstreamThrowable = t + throw t + } + } + + /** Handles exceptions thrown by a hooker according to its [ExceptionMode]. */ + private fun handleInterceptorException( + t: Throwable, + record: VectorHookRecord, + nextChain: VectorChain, + recoveryThis: Any, + recoveryArgs: Array, + ): Any? { + // Check if the exception originated from downstream (lower hooks or original method) + if (nextChain.proceedCalled && t === nextChain.downstreamThrowable) { + throw t + } + + // Passthrough mode does not rescue the process from hooker crashes + if (record.exceptionMode == ExceptionMode.PASSTHROUGH) { + throw t + } + + val hookerName = record.hooker.javaClass.name + if (!nextChain.proceedCalled) { + // Crash occurred before calling proceed(); skip hooker and continue the chain + Utils.logD("Hooker [$hookerName] crashed before proceed. Skipping.", t) + return nextChain.proceedWith(recoveryThis, recoveryArgs) + } else { + // Crash occurred after calling proceed(); suppress and restore downstream state + Utils.logD("Hooker [$hookerName] crashed after proceed. Restoring state.", t) + nextChain.downstreamThrowable?.let { throw it } + return nextChain.downstreamResult + } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorLegacyCallback.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorLegacyCallback.kt new file mode 100644 index 000000000..73366a4a5 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorLegacyCallback.kt @@ -0,0 +1,34 @@ +package org.matrix.vector.impl.hooks + +import java.lang.reflect.Executable + +/** + * Adapter for backward compatibility with [XposedBridge.LegacyApiSupport]. Contains state mutations + * strictly for legacy module support. + */ +class VectorLegacyCallback( + val method: T, + var thisObject: Any?, + var args: Array, +) { + var result: Any? = null + private set + + var throwable: Throwable? = null + private set + + var isSkipped = false + private set + + fun setResult(res: Any?) { + result = res + throwable = null + isSkipped = true + } + + fun setThrowable(t: Throwable?) { + result = null + throwable = t + isSkipped = true + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt new file mode 100644 index 000000000..cbfdb7506 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt @@ -0,0 +1,149 @@ +package org.matrix.vector.impl.hooks + +import io.github.libxposed.api.XposedInterface +import io.github.libxposed.api.XposedInterface.ExceptionMode +import io.github.libxposed.api.XposedInterface.HookBuilder +import io.github.libxposed.api.XposedInterface.HookHandle +import io.github.libxposed.api.XposedInterface.Hooker +import io.github.libxposed.api.error.HookFailedError +import java.lang.reflect.Executable +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import org.lsposed.lspd.util.Utils +import org.matrix.vector.impl.di.VectorBootstrap +import org.matrix.vector.nativebridge.HookBridge + +/** Builder for configuring and registering hooks. */ +class VectorHookBuilder(private val origin: Executable) : HookBuilder { + + private var priority = XposedInterface.PRIORITY_DEFAULT + private var exceptionMode = ExceptionMode.DEFAULT + + override fun setPriority(priority: Int): HookBuilder = apply { this.priority = priority } + + override fun setExceptionMode(mode: ExceptionMode): HookBuilder = apply { + this.exceptionMode = mode + } + + override fun intercept(hooker: Hooker): HookHandle { + if (Modifier.isAbstract(origin.modifiers)) { + throw IllegalArgumentException("Cannot hook abstract methods: $origin") + } else if (origin.declaringClass.classLoader == VectorHookBuilder::class.java.classLoader) { + throw IllegalArgumentException("Do not allow hooking inner methods") + } else if ( + origin is Method && + origin.declaringClass == Method::class.java && + origin.name == "invoke" + ) { + throw IllegalArgumentException("Cannot hook Method.invoke") + } + + val record = VectorHookRecord(hooker, priority, exceptionMode) + + // Register natively. HookBridge now stores VectorHookRecord instead of HookerCallback. + if ( + !HookBridge.hookMethod(true, origin, VectorNativeHooker::class.java, priority, record) + ) { + throw HookFailedError("Cannot hook $origin") + } + + return object : HookHandle { + override fun getExecutable(): Executable = origin + + override fun unhook() { + HookBridge.unhookMethod(true, origin, record) + } + } + } +} + +/** + * The native callback entrypoint. Instantiated natively by [HookBridge] when a hooked method is + * hit. + */ +class VectorNativeHooker(private val method: T) { + + private val isStatic = Modifier.isStatic(method.modifiers) + private val returnType = if (method is Method) method.returnType else null + + /** Invoked by C++ via JNI. */ + fun callback(args: Array): Any? { + val thisObject = if (isStatic) null else args[0] + val actualArgs = if (isStatic) args else args.sliceArray(1 until args.size) + + // Retrieve the hook snapshots + val snapshots = HookBridge.callbackSnapshot(VectorHookRecord::class.java, method) + + @Suppress("UNCHECKED_CAST") val modernHooks = snapshots[0] as Array + val legacyHooks = snapshots[1] + + // Fast path: No hooks active + if (modernHooks.isEmpty() && legacyHooks.isEmpty()) { + return invokeOriginalSafely(thisObject, actualArgs) + } + + val terminal: (Any?, Array) -> Any? = { tObj, tArgs -> + val delegate = VectorBootstrap.delegate + if (legacyHooks.isNotEmpty() && delegate != null) { + delegate.processLegacyHook(method, tObj, tArgs, legacyHooks) { + invokeOriginalSafely(tObj, tArgs) + } + } else { + invokeOriginalSafely(tObj, tArgs) + } + } + + val rootChain = VectorChain(method, thisObject, actualArgs, modernHooks, 0, terminal) + + val result = rootChain.proceed() + + // Type safety validation before returning to C++ + if (returnType != null && returnType != Void.TYPE) { + if (result == null) { + if (returnType.isPrimitive) { + throw NullPointerException( + "Hook returned null for a primitive return type: $method" + ) + } + } else { + // Use the JNI bridge for the most reliable type check across ClassLoaders + if ( + !HookBridge.instanceOf(result, returnType) && + !isBoxingCompatible(result, returnType) + ) { + Utils.logD( + "Hook return type mismatch. Expected ${returnType.name}, got ${result.javaClass.name}" + ) + } + } + } + + return result + } + + /** Handles primitive boxing compatibility (e.g., Integer object vs int primitive). */ + private fun isBoxingCompatible(obj: Any, targetType: Class<*>): Boolean { + if (!targetType.isPrimitive) return false + return when (targetType) { + Int::class.javaPrimitiveType -> obj is Int + Long::class.javaPrimitiveType -> obj is Long + Boolean::class.javaPrimitiveType -> obj is Boolean + Double::class.javaPrimitiveType -> obj is Double + Float::class.javaPrimitiveType -> obj is Float + Byte::class.javaPrimitiveType -> obj is Byte + Char::class.javaPrimitiveType -> obj is Char + Short::class.javaPrimitiveType -> obj is Short + else -> false + } + } + + /** Safely invokes the original method, unwrapping InvocationTargetExceptions. */ + private fun invokeOriginalSafely(tObj: Any?, tArgs: Array): Any? { + return try { + HookBridge.invokeOriginalMethod(method, tObj, *tArgs) + } catch (ite: InvocationTargetException) { + throw ite.cause ?: ite + } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorDexParser.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorDexParser.kt deleted file mode 100644 index ac42e64ad..000000000 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorDexParser.kt +++ /dev/null @@ -1,316 +0,0 @@ -package org.matrix.vector.impl.utils - -import io.github.libxposed.api.utils.DexParser -import io.github.libxposed.api.utils.DexParser.* -import java.io.IOException -import java.nio.ByteBuffer -import org.matrix.vector.nativebridge.DexParserBridge - -/** - * Kotlin implementation of [DexParser] for Vector. - * - * This class acts as a high-level wrapper around the native C++ DexParser. It maps raw JNI data - * structures (integer arrays, flat buffers) into usable object graphs (StringId, TypeId, MethodId, - * etc.). - */ -@Suppress("UNCHECKED_CAST") -class VectorDexParser(buffer: ByteBuffer, includeAnnotations: Boolean) : DexParser { - - private var cookie: Long = 0 - private val data: ByteBuffer - - // Internal storage for parsed DEX structures. - // We use private properties and explicit getter methods as requested. - private val internalStrings: Array - private val internalTypeIds: Array - private val internalProtoIds: Array - private val internalFieldIds: Array - private val internalMethodIds: Array - private val internalAnnotations: Array - private val internalArrays: Array - - init { - // Ensure the buffer is Direct and accessible by native code - data = - if (!buffer.isDirect || !buffer.asReadOnlyBuffer().hasArray()) { - ByteBuffer.allocateDirect(buffer.capacity()).apply { - put(buffer) - // Ensure position is reset for reading if needed, - // though native uses address - flip() - } - } else { - buffer - } - - try { - val args = LongArray(2) - args[1] = if (includeAnnotations) 1 else 0 - - // Call Native Bridge - // Returns a raw Object[] containing headers and pools - val out = DexParserBridge.openDex(data, args) as Array - cookie = args[0] - - // --- Parse Strings (Index 0) --- - val rawStrings = out[0] as Array - internalStrings = Array(rawStrings.size) { i -> VectorStringId(i, rawStrings[i]) } - - // --- Parse Type IDs (Index 1) --- - val rawTypeIds = out[1] as IntArray - internalTypeIds = Array(rawTypeIds.size) { i -> VectorTypeId(i, rawTypeIds[i]) } - - // --- Parse Proto IDs (Index 2) --- - val rawProtoIds = out[2] as Array - internalProtoIds = Array(rawProtoIds.size) { i -> VectorProtoId(i, rawProtoIds[i]) } - - // --- Parse Field IDs (Index 3) --- - val rawFieldIds = out[3] as IntArray - // Each field is represented by 3 integers (class_idx, type_idx, name_idx) - internalFieldIds = - Array(rawFieldIds.size / 3) { i -> - VectorFieldId( - i, - rawFieldIds[3 * i], - rawFieldIds[3 * i + 1], - rawFieldIds[3 * i + 2], - ) - } - - // --- Parse Method IDs (Index 4) --- - val rawMethodIds = out[4] as IntArray - // Each method is represented by 3 integers (class_idx, proto_idx, name_idx) - internalMethodIds = - Array(rawMethodIds.size / 3) { i -> - VectorMethodId( - i, - rawMethodIds[3 * i], - rawMethodIds[3 * i + 1], - rawMethodIds[3 * i + 2], - ) - } - - // --- Parse Annotations (Index 5 & 6) --- - val rawAnnotationMetadata = out[5] as? IntArray - val rawAnnotationValues = out[6] as? Array - - internalAnnotations = - if (rawAnnotationMetadata != null && rawAnnotationValues != null) { - Array(rawAnnotationMetadata.size / 2) { i -> - // Metadata: [visibility, type_idx] - // Values: [name_indices[], values[]] - val elementsMeta = rawAnnotationValues[2 * i] as IntArray - val elementsData = rawAnnotationValues[2 * i + 1] as Array - VectorAnnotation( - rawAnnotationMetadata[2 * i], - rawAnnotationMetadata[2 * i + 1], - elementsMeta, - elementsData, - ) - } - } else { - emptyArray() - } - - // --- Parse Arrays (Index 7) --- - val rawArrays = out[7] as? Array - internalArrays = - if (rawArrays != null) { - Array(rawArrays.size / 2) { i -> - val types = rawArrays[2 * i] as IntArray - val values = rawArrays[2 * i + 1] as Array - VectorArray(types, values) - } - } else { - emptyArray() - } - } catch (e: Throwable) { - throw IOException("Invalid dex file", e) - } - } - - @Synchronized - override fun close() { - if (cookie != 0L) { - DexParserBridge.closeDex(cookie) - cookie = 0 - } - } - - override fun getStringId(): Array = internalStrings - - override fun getTypeId(): Array = internalTypeIds - - override fun getFieldId(): Array = internalFieldIds - - override fun getMethodId(): Array = internalMethodIds - - override fun getProtoId(): Array = internalProtoIds - - override fun getAnnotations(): Array = internalAnnotations - - override fun getArrays(): Array = internalArrays - - override fun visitDefinedClasses(visitor: ClassVisitor) { - if (cookie == 0L) { - throw IllegalStateException("Closed") - } - - // Accessing [0] is fragile - val classVisitMethod = ClassVisitor::class.java.declaredMethods[0] - val fieldVisitMethod = FieldVisitor::class.java.declaredMethods[0] - val methodVisitMethod = MethodVisitor::class.java.declaredMethods[0] - val methodBodyVisitMethod = MethodBodyVisitor::class.java.declaredMethods[0] - val stopMethod = EarlyStopVisitor::class.java.declaredMethods[0] - - DexParserBridge.visitClass( - cookie, - visitor, - FieldVisitor::class.java, - MethodVisitor::class.java, - classVisitMethod, - fieldVisitMethod, - methodVisitMethod, - methodBodyVisitMethod, - stopMethod, - ) - } - - /** Base implementation for all Dex IDs. */ - private open class VectorId>(private val id: Int) : Id { - override fun getId(): Int = id - - override fun compareTo(other: Self): Int = id - other.id - } - - private inner class VectorStringId(id: Int, private val string: String) : - VectorId(id), StringId { - override fun getString(): String = string - } - - private inner class VectorTypeId(id: Int, descriptorIdx: Int) : VectorId(id), TypeId { - private val descriptor: StringId = internalStrings[descriptorIdx] - - override fun getDescriptor(): StringId = descriptor - } - - private inner class VectorProtoId(id: Int, protoData: IntArray) : - VectorId(id), ProtoId { - - private val shorty: StringId = internalStrings[protoData[0]] - private val returnType: TypeId = internalTypeIds[protoData[1]] - private val parameters: Array? - - init { - if (protoData.size > 2) { - // protoData format: [shorty_idx, return_type_idx, param1_idx, param2_idx...] - parameters = Array(protoData.size - 2) { i -> internalTypeIds[protoData[i + 2]] } - } else { - parameters = null - } - } - - override fun getShorty(): StringId = shorty - - override fun getReturnType(): TypeId = returnType - - override fun getParameters(): Array? = parameters - } - - private inner class VectorFieldId(id: Int, classIdx: Int, typeIdx: Int, nameIdx: Int) : - VectorId(id), FieldId { - - private val declaringClass: TypeId = internalTypeIds[classIdx] - private val type: TypeId = internalTypeIds[typeIdx] - private val name: StringId = internalStrings[nameIdx] - - override fun getType(): TypeId = type - - override fun getDeclaringClass(): TypeId = declaringClass - - override fun getName(): StringId = name - } - - private inner class VectorMethodId(id: Int, classIdx: Int, protoIdx: Int, nameIdx: Int) : - VectorId(id), MethodId { - - private val declaringClass: TypeId = internalTypeIds[classIdx] - private val prototype: ProtoId = internalProtoIds[protoIdx] - private val name: StringId = internalStrings[nameIdx] - - override fun getDeclaringClass(): TypeId = declaringClass - - override fun getPrototype(): ProtoId = prototype - - override fun getName(): StringId = name - } - - private class VectorArray(elementsTypes: IntArray, valuesData: Array) : DexParser.Array { - - private val values: Array - - init { - values = - Array(valuesData.size) { i -> - VectorValue(elementsTypes[i], valuesData[i] as? ByteBuffer) - } - } - - override fun getValues(): Array = values - } - - private inner class VectorAnnotation( - private val visibility: Int, - typeIdx: Int, - elementNameIndices: IntArray, - elementValues: Array, - ) : DexParser.Annotation { - - private val type: TypeId = internalTypeIds[typeIdx] - private val elements: Array - - init { - elements = - Array(elementValues.size) { i -> - // Flattened structure from JNI: names are at 2*i, types at 2*i+1 - VectorElement( - elementNameIndices[i * 2], - elementNameIndices[i * 2 + 1], // valueType - elementValues[i] as? ByteBuffer, - ) - } - } - - override fun getVisibility(): Int = visibility - - override fun getType(): TypeId = type - - override fun getElements(): Array = elements - } - - private open class VectorValue(private val valueType: Int, buffer: ByteBuffer?) : Value { - - private val value: ByteArray? - - init { - if (buffer != null) { - value = ByteArray(buffer.remaining()) - buffer.get(value) - } else { - value = null - } - } - - override fun getValue(): ByteArray? = value - - override fun getValueType(): Int = valueType - } - - private inner class VectorElement(nameIdx: Int, valueType: Int, value: ByteBuffer?) : - VectorValue(valueType, value), Element { - - private val name: StringId = internalStrings[nameIdx] - - override fun getName(): StringId = name - } -} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorMetaDataReader.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorMetaDataReader.kt new file mode 100644 index 000000000..949416cc5 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorMetaDataReader.kt @@ -0,0 +1,112 @@ +package org.matrix.vector.impl.utils + +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.util.jar.JarFile +import pxb.android.axml.AxmlReader +import pxb.android.axml.AxmlVisitor +import pxb.android.axml.NodeVisitor + +/** + * Utility for parsing metadata configuration strictly from AndroidManifest.xml. Utilizes AXML + * reading to extract runtime constraints like minimum framework versions without requiring full APK + * extraction. + */ +class VectorMetaDataReader private constructor(apk: File) { + + val metaData = mutableMapOf() + + init { + JarFile(apk).use { zip -> + val entry = zip.getEntry("AndroidManifest.xml") + zip.getInputStream(entry).use { inputStream -> + val reader = AxmlReader(getBytesFromInputStream(inputStream)) + reader.accept( + object : AxmlVisitor() { + override fun child(ns: String?, name: String?): NodeVisitor? { + // We only care about the root tag. + // Returning ManifestTagVisitor() tells the parser to start + // looking at things inside . + return ManifestTagVisitor() + } + } + ) + } + } + } + + private inner class ManifestTagVisitor : NodeVisitor() { + override fun child(ns: String?, name: String?): NodeVisitor? { + // If we see , we return a visitor to go deeper. + if (name == "application") return ApplicationTagVisitor() + // If we see or , we return null to skip them entirely. + return null + } + } + + // Handles the inside of + private inner class ApplicationTagVisitor : NodeVisitor() { + override fun child(ns: String?, name: String?): NodeVisitor? { + // We only care about tags. + if (name == "meta-data") return MetaDataVisitor() + return null + } + } + + private inner class MetaDataVisitor : NodeVisitor() { + var attrName: String? = null + var attrValue: Any? = null + + override fun attr(ns: String?, name: String?, resourceId: Int, type: Int, obj: Any?) { + if (type == 3 && name == "name") { + attrName = obj as? String + } + if (name == "value") { + attrValue = obj + } + super.attr(ns, name, resourceId, type, obj) + } + + override fun end() { + if (attrName != null && attrValue != null) { + metaData[attrName!!] = attrValue!! + } + super.end() + } + } + + companion object { + @JvmStatic + @Throws(IOException::class) + fun getMetaData(apk: File): Map { + return VectorMetaDataReader(apk).metaData + } + + @Throws(IOException::class) + private fun getBytesFromInputStream(inputStream: InputStream): ByteArray { + ByteArrayOutputStream().use { bos -> + val b = ByteArray(1024) + var n: Int + while (inputStream.read(b).also { n = it } != -1) { + bos.write(b, 0, n) + } + return bos.toByteArray() + } + } + + @JvmStatic + fun extractIntPart(str: String): Int { + var result = 0 + for (c in str) { + if (c in '0'..'9') { + result = result * 10 + (c - '0') + } else { + break + } + } + return result + } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorModuleClassLoader.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorModuleClassLoader.kt new file mode 100644 index 000000000..eb930b0f4 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorModuleClassLoader.kt @@ -0,0 +1,179 @@ +package org.matrix.vector.impl.utils + +import android.os.Build +import android.os.SharedMemory +import android.system.ErrnoException +import android.system.Os +import android.system.OsConstants +import android.util.Log +import androidx.annotation.RequiresApi +import hidden.ByteBufferDexClassLoader +import java.io.File +import java.io.IOException +import java.net.URL +import java.nio.ByteBuffer +import java.util.Collections +import java.util.Enumeration +import java.util.jar.JarFile +import java.util.zip.ZipEntry + +/** + * Custom ClassLoader for module execution. Utilizes in-memory DEX loading to prevent the need to + * extract module code to the disk, enhancing both security and performance during the module + * lifecycle. + */ +class VectorModuleClassLoader : ByteBufferDexClassLoader { + + private val apkPath: String + private val nativeLibraryDirs = mutableListOf() + + @RequiresApi(Build.VERSION_CODES.Q) + private constructor( + dexBuffers: Array, + librarySearchPath: String?, + parent: ClassLoader?, + apkPath: String, + ) : super(dexBuffers, librarySearchPath, parent) { + this.apkPath = apkPath + initNativeDirs(librarySearchPath) + } + + private constructor( + dexBuffers: Array, + parent: ClassLoader?, + apkPath: String, + librarySearchPath: String?, + ) : super(dexBuffers, parent) { + this.apkPath = apkPath + initNativeDirs(librarySearchPath) + } + + private fun initNativeDirs(librarySearchPath: String?) { + val searchPath = librarySearchPath ?: "" + nativeLibraryDirs.addAll(splitPaths(searchPath)) + nativeLibraryDirs.addAll(SYSTEM_NATIVE_LIBRARY_DIRS) + } + + @Throws(ClassNotFoundException::class) + override fun loadClass(name: String, resolve: Boolean): Class<*> { + findLoadedClass(name)?.let { + return it + } + + try { + return Any::class.java.classLoader!!.loadClass(name) + } catch (ignored: ClassNotFoundException) {} + + var fromSuper: ClassNotFoundException? = null + try { + return findClass(name) + } catch (ex: ClassNotFoundException) { + fromSuper = ex + } + + try { + return parent?.loadClass(name) ?: throw fromSuper + } catch (cnfe: ClassNotFoundException) { + throw fromSuper + } + } + + override fun findLibrary(libraryName: String): String? { + val fileName = System.mapLibraryName(libraryName) + for (file in nativeLibraryDirs) { + val path = file.path + if (path.contains(ZIP_SEPARATOR)) { + val split = path.split(ZIP_SEPARATOR, limit = 2) + try { + JarFile(split[0]).use { jarFile -> + val entryName = "${split[1]}/$fileName" + val entry = jarFile.getEntry(entryName) + if (entry != null && entry.method == ZipEntry.STORED) { + return "${split[0]}$ZIP_SEPARATOR$entryName" + } + } + } catch (e: IOException) { + Log.e(TAG, "Cannot open ${split[0]}", e) + } + } else if (file.isDirectory) { + val entryPath = File(file, fileName).path + try { + val fd = Os.open(entryPath, OsConstants.O_RDONLY, 0) + Os.close(fd) + return entryPath + } catch (ignored: ErrnoException) {} + } + } + return null + } + + override fun findResource(name: String): URL? { + return try { + val urlHandler = VectorURLStreamHandler(apkPath) + urlHandler.getEntryUrlOrNull(name) + } catch (e: IOException) { + null + } + } + + override fun findResources(name: String): Enumeration { + val url = findResource(name) + val result = if (url != null) listOf(url) else emptyList() + return Collections.enumeration(result) + } + + override fun toString(): String { + return "VectorModuleClassLoader[module=$apkPath, ${super.toString()}]" + } + + companion object { + private const val TAG = "VectorModuleClassLoader" + private const val ZIP_SEPARATOR = "!/" + private val SYSTEM_NATIVE_LIBRARY_DIRS = splitPaths(System.getProperty("java.library.path")) + + private fun splitPaths(searchPath: String?): List { + if (searchPath.isNullOrEmpty()) return emptyList() + return searchPath.split(File.pathSeparator).map { File(it) } + } + + /** + * Loads an APK into memory securely. Maps the provided [SharedMemory] instances into + * read-only [ByteBuffer]s and cleans up the memory file descriptors once the ClassLoader is + * fully instantiated. + */ + @JvmStatic + fun loadApk( + apk: String, + dexes: List, + librarySearchPath: String, + parent: ClassLoader?, + ): ClassLoader { + val dexBuffers = + dexes + .parallelStream() + .map { dex -> + try { + dex.mapReadOnly() + } catch (e: ErrnoException) { + Log.w(TAG, "Cannot map $dex", e) + null + } + } + .toList() + .filterNotNull() + .toTypedArray() + + val cl = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + VectorModuleClassLoader(dexBuffers, librarySearchPath, parent, apk) + } else { + VectorModuleClassLoader(dexBuffers, parent, apk, librarySearchPath) + } + + dexBuffers.toList().parallelStream().forEach { SharedMemory.unmap(it) } + dexes.parallelStream().forEach { it.close() } + + return cl + } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt new file mode 100644 index 000000000..8f146a4da --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt @@ -0,0 +1,114 @@ +package org.matrix.vector.impl.utils + +import android.net.Uri +import java.io.File +import java.io.FileNotFoundException +import java.io.FilterInputStream +import java.io.IOException +import java.io.InputStream +import java.net.JarURLConnection +import java.net.MalformedURLException +import java.net.URL +import java.net.URLConnection +import java.util.jar.JarFile +import java.util.zip.ZipEntry +import sun.net.www.protocol.jar.Handler + +/** + * Custom URLStreamHandler for loading resources directly from the module's APK. Optimized to handle + * internal JAR/ZIP entries without extracting them to the filesystem. + */ +internal class VectorURLStreamHandler(jarFileName: String) : Handler() { + private val fileUri: String = File(jarFileName).toURI().toString() + private val jarFile: JarFile = JarFile(jarFileName) + + fun getEntryUrlOrNull(entryName: String): URL? { + if (jarFile.getEntry(entryName) != null) { + return try { + val encodedName = Uri.encode(entryName, "/") + URL("jar", null, -1, "$fileUri!/$encodedName", this) + } catch (e: MalformedURLException) { + throw RuntimeException("Invalid entry name: $entryName", e) + } + } + return null + } + + @Throws(IOException::class) + override fun openConnection(url: URL): URLConnection { + return ClassPathURLConnection(url) + } + + @Suppress("deprecation") + @Throws(IOException::class) + protected fun finalize() { + jarFile.close() + } + + private inner class ClassPathURLConnection(url: URL) : JarURLConnection(url) { + private var connectionJarFile: JarFile? = null + private var jarEntry: ZipEntry? = null + private var jarInput: InputStream? = null + private var isClosed = false + + init { + useCaches = false + } + + override fun setUseCaches(usecaches: Boolean) { + super.setUseCaches(false) + } + + @Throws(IOException::class) + override fun connect() { + check(!isClosed) { "JarURLConnection has been closed" } + if (!connected) { + jarEntry = + this@VectorURLStreamHandler.jarFile.getEntry(entryName) + ?: throw FileNotFoundException( + "URL=$url, zipfile=${this@VectorURLStreamHandler.jarFile.name}" + ) + connected = true + } + } + + @Throws(IOException::class) + override fun getJarFile(): JarFile { + connect() + return connectionJarFile + ?: JarFile(this@VectorURLStreamHandler.jarFile.name).also { connectionJarFile = it } + } + + @Throws(IOException::class) + override fun getInputStream(): InputStream { + connect() + return jarInput + ?: object : + FilterInputStream( + this@VectorURLStreamHandler.jarFile.getInputStream(jarEntry) + ) { + @Throws(IOException::class) + override fun close() { + super.close() + isClosed = true + this@VectorURLStreamHandler.jarFile.close() + connectionJarFile?.close() + } + } + .also { jarInput = it } + } + + override fun getContentType(): String { + return guessContentTypeFromName(entryName) ?: "content/unknown" + } + + override fun getContentLength(): Int { + return try { + connect() + jarEntry?.size?.toInt() ?: -1 + } catch (ignored: IOException) { + -1 + } + } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/DexParserBridge.kt b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/DexParserBridge.kt deleted file mode 100644 index 35d46ea5b..000000000 --- a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/DexParserBridge.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.matrix.vector.nativebridge - -import dalvik.annotation.optimization.FastNative -import io.github.libxposed.api.utils.DexParser -import java.io.IOException -import java.lang.reflect.Method -import java.nio.ByteBuffer - -object DexParserBridge { - @JvmStatic - @FastNative - @Throws(IOException::class) - external fun openDex(data: ByteBuffer, args: LongArray): Any - - @JvmStatic @FastNative external fun closeDex(cookie: Long) - - @JvmStatic - @FastNative - external fun visitClass( - cookie: Long, - visitor: Any, - fieldVisitorClass: Class, - methodVisitorClass: Class, - classVisitMethod: Method, - fieldVisitMethod: Method, - methodVisitMethod: Method, - methodBodyVisitMethod: Method, - stopMethod: Method, - ) -} diff --git a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt index 527fb8307..ea2a5bfd6 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt @@ -57,5 +57,5 @@ object HookBridge { @JvmStatic external fun callbackSnapshot(hooker_callback: Class<*>, method: Executable): Array> - @JvmStatic external fun getStaticInitializer(clazz: Class<*>): Method + @JvmStatic external fun getStaticInitializer(clazz: Class<*>): Method? } diff --git a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt index 42955ed72..a3f8ed057 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt @@ -2,7 +2,6 @@ package org.matrix.vector.nativebridge import android.content.res.Resources import dalvik.annotation.optimization.FastNative -import xposed.dummy.XResourcesSuperClass object ResourcesHook { @JvmStatic external fun initXResourcesNative(): Boolean @@ -18,9 +17,5 @@ object ResourcesHook { @JvmStatic @FastNative - external fun rewriteXmlReferencesNative( - parserPtr: Long, - origRes: XResourcesSuperClass, - repRes: Resources, - ) + external fun rewriteXmlReferencesNative(parserPtr: Long, origRes: Any, repRes: Resources) } diff --git a/zygisk/build.gradle.kts b/zygisk/build.gradle.kts index 2a3b31354..1cb090d84 100644 --- a/zygisk/build.gradle.kts +++ b/zygisk/build.gradle.kts @@ -54,8 +54,8 @@ abstract class Injected @Inject constructor(val moduleDir: String) { } dependencies { - implementation(projects.core) implementation(projects.hiddenapi.bridge) + implementation(projects.legacy) implementation(projects.services.managerService) implementation(projects.services.daemonService) compileOnly(libs.androidx.annotation) diff --git a/zygisk/proguard-rules.pro b/zygisk/proguard-rules.pro index f908e5529..b8166bd58 100644 --- a/zygisk/proguard-rules.pro +++ b/zygisk/proguard-rules.pro @@ -7,12 +7,5 @@ -keepclasseswithmembers class org.matrix.vector.service.BridgeService { public static boolean *(android.os.IBinder, int, long, long, int); } - --assumenosideeffects class android.util.Log { - public static *** v(...); - public static *** d(...); -} -repackageclasses -allowaccessmodification --dontwarn org.lsposed.lspd.core.* --dontwarn org.lsposed.lspd.util.Hookers diff --git a/zygisk/src/main/cpp/module.cpp b/zygisk/src/main/cpp/module.cpp index f35efcf69..d4c303f6c 100644 --- a/zygisk/src/main/cpp/module.cpp +++ b/zygisk/src/main/cpp/module.cpp @@ -35,7 +35,7 @@ constexpr int PER_USER_RANGE = 100000; // Defined via CMake generated marcos constexpr uid_t kHostPackageUid = INJECTED_PACKAGE_UID; const char *const kHostPackageName = INJECTED_PACKAGE_NAME; -const char *const kManagePackageName = MANAGER_PACKAGE_NAME; +const char *const kManagerPackageName = MANAGER_PACKAGE_NAME; constexpr uid_t GID_INET = 3003; // Android's Internet group ID. enum RuntimeFlags : uint32_t { @@ -126,6 +126,8 @@ class VectorModule : public zygisk::ModuleBase, public vector::native::Context { [](auto symbol) { return ElfSymbolCache::GetArt()->getSymbAddress(symbol); }, .art_symbol_prefix_resolver = [](auto symbol) { return ElfSymbolCache::GetArt()->getSymbPrefixFirstAddress(symbol); }, + .generated_class_name = "Vector_", + .generated_source_name = "Dobby", }; // State managed within the class instance for each forked process. @@ -238,7 +240,7 @@ void VectorModule::preAppSpecialize(zygisk::AppSpecializeArgs *args) { // grant it internet permissions by adding it to the INET group. if (args->uid == kHostPackageUid) { lsplant::JUTFString nice_name_str(env_, args->nice_name); - if (nice_name_str.get() == std::string(kManagePackageName)) { + if (nice_name_str.get() == std::string(kManagerPackageName)) { LOGI("Manager app detected. Granting internet permissions."); is_manager_app_ = true; @@ -303,7 +305,7 @@ void VectorModule::postAppSpecialize(const zygisk::AppSpecializeArgs *args) { } if (is_manager_app_) { - args->nice_name = env_->NewStringUTF(kManagePackageName); + args->nice_name = env_->NewStringUTF(kManagerPackageName); } // --- Framework Injection --- diff --git a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt index 5c06706de..b8037f12f 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt @@ -23,9 +23,8 @@ import java.io.FileOutputStream import java.lang.reflect.Method import java.util.concurrent.ConcurrentHashMap import org.lsposed.lspd.ILSPManagerService -import org.lsposed.lspd.core.ApplicationServiceClient.serviceClient -import org.lsposed.lspd.util.Hookers import org.lsposed.lspd.util.Utils +import org.matrix.vector.impl.core.VectorServiceClient /** The "Parasite" logic. Injects the LSPosed Manager APK into a host process (shell). */ @SuppressLint("StaticFieldLeak") @@ -39,6 +38,19 @@ object ParasiticManagerHooker { private val states = ConcurrentHashMap() private val persistentStates = ConcurrentHashMap() + private fun logD(msg: String) { + Utils.logD( + "ParasiticHooker: pkg=${ActivityThread.currentPackageName()}, prc=${ActivityThread.currentProcessName()} - $msg" + ) + } + + private fun logE(msg: String, t: Throwable) { + Utils.logE( + "ParasiticHooker: pkg=${ActivityThread.currentPackageName()}, prc=${ActivityThread.currentProcessName()} - $msg", + t, + ) + } + /** Constructs a hybrid PackageInfo. Combines the Manager's code with the Host's environment. */ @Synchronized private fun getManagerPkgInfo(appInfo: ApplicationInfo?): PackageInfo? { @@ -64,7 +76,7 @@ object ParasiticManagerHooker { } sourcePath = dstPath } - .onFailure { Hookers.logE("Failed to copy parasitic APK", it) } + .onFailure { logE("Failed to copy parasitic APK", it) } } val pkgInfo = @@ -140,7 +152,7 @@ object ParasiticManagerHooker { "android.app.ActivityThread\$AppBindData", object : XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam<*>) { - Hookers.logD("ActivityThread#handleBindApplication() starts") + logD("ActivityThread#handleBindApplication() starts") val bindData = param.args[0] val hostAppInfo = XposedHelpers.getObjectField(bindData, "appInfo") as ApplicationInfo @@ -165,7 +177,7 @@ object ParasiticManagerHooker { val dexPath = managerAppInfo.sourceDir val pathClassLoader = param.result as ClassLoader - Hookers.logD("Injecting DEX into LoadedApk ClassLoader: $pathClassLoader") + logD("Injecting DEX into LoadedApk ClassLoader: $pathClassLoader") val pathList = XposedHelpers.getObjectField(pathClassLoader, "pathList") val dexPaths = XposedHelpers.callMethod(pathList, "getDexPaths") as List<*> @@ -241,7 +253,7 @@ object ParasiticManagerHooker { override fun afterHookedMethod(param: MethodHookParam<*>) { if (!activityClientRecordClass.isInstance(param.thisObject)) return param.args.filterIsInstance().forEach { aInfo -> - Hookers.logD("Restoring state for Activity: ${aInfo.name}") + logD("Restoring state for Activity: ${aInfo.name}") states[aInfo.name]?.let { XposedHelpers.setObjectField(param.thisObject, "state", it) } @@ -307,7 +319,7 @@ object ParasiticManagerHooker { // Create a fake original context to satisfy internal package checks info.applicationInfo.packageName = "$managerPackage.origin" val compatibilityInfo = - HiddenApiBridge.Resources_getCompatibilityInfo(ctx!!.resources) + HiddenApiBridge.Resources_getCompatibilityInfo(ctx.resources) val originalPkgInfo = ActivityThread.currentActivityThread() .getPackageInfoNoCheck(info.applicationInfo, compatibilityInfo) @@ -368,10 +380,10 @@ object ParasiticManagerHooker { "sProviderInstance", instance, ) - Hookers.logD("WebView provider initialized: $instance") + logD("WebView provider initialized: $instance") instance } catch (e: Exception) { - Hookers.logE("WebView initialization failed", e) + logE("WebView initialization failed", e) throw AndroidRuntimeException(e) } } @@ -406,9 +418,9 @@ object ParasiticManagerHooker { state?.let { states[aInfo.name] = it } pState?.let { persistentStates[aInfo.name] = it } - Hookers.logD("Saved state for ${aInfo.name}") + logD("Saved state for ${aInfo.name}") } - .onFailure { Hookers.logE("Failed to save activity state", it) } + .onFailure { logE("Failed to save activity state", it) } } } XposedBridge.hookAllMethods( @@ -434,7 +446,7 @@ object ParasiticManagerHooker { fun start(): Boolean { val binderList = mutableListOf() return try { - serviceClient.requestInjectedManagerBinder(binderList).use { pfd -> + VectorServiceClient.requestInjectedManagerBinder(binderList)!!.use { pfd -> managerFd = pfd.detachFd() val managerService = ILSPManagerService.Stub.asInterface(binderList[0]) hookForManager(managerService) diff --git a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt index 8b3ce43d8..8e5bc18ce 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt @@ -1,20 +1,17 @@ package org.matrix.vector import android.annotation.SuppressLint -import android.app.ProfilerInfo import android.content.Intent import android.content.pm.ActivityInfo -import android.content.pm.ResolveInfo -import io.github.libxposed.api.XposedInterface -import org.lsposed.lspd.hooker.HandleSystemServerProcessHooker -import org.lsposed.lspd.impl.LSPosedHelper import org.lsposed.lspd.util.Utils +import org.matrix.vector.impl.hookers.HandleSystemServerProcessHooker +import org.matrix.vector.impl.hooks.VectorHookBuilder import org.matrix.vector.service.BridgeService /** * Handles System-Server side logic for the Parasitic Manager. * - * When a user tries to open the LSPosed Manager, the system normally wouldn't know how to handle it + * When a user tries to open the Vector Manager, the system normally wouldn't know how to handle it * because it isn't "installed." This class intercepts the activity resolution and tells the system * to launch it in a special process. */ @@ -23,51 +20,12 @@ class ParasiticManagerSystemHooker : HandleSystemServerProcessHooker.Callback { companion object { @JvmStatic fun start() { - // Register this class as the handler for system_server initialization + // Register this class as the handler for system_server initialization. + // This ensures the hook is deferred until the System Server ClassLoader is fully ready. HandleSystemServerProcessHooker.callback = ParasiticManagerSystemHooker() } } - /** Intercepts Activity resolution in the System Server. */ - object Hooker : XposedInterface.Hooker { - @JvmStatic - fun after(callback: XposedInterface.AfterHookCallback) { - val intent = callback.args[0] as? Intent ?: return - - // Check if this intent is meant for the LSPosed Manager - if (!intent.hasCategory(BuildConfig.ManagerPackageName + ".LAUNCH_MANAGER")) return - - val result = callback.result as? ActivityInfo ?: return - - // We only intercept if it's currently resolving to the shell/fallback - if (result.packageName != BuildConfig.InjectedPackageName) return - - // --- Redirection Logic --- - // We create a copy of the ActivityInfo to avoid polluting the system's cache. - val redirectedInfo = - ActivityInfo(result).apply { - // Force the manager to run in its own dedicated process name - processName = BuildConfig.ManagerPackageName - - // Set a standard theme so transition animations work correctly - theme = android.R.style.Theme_DeviceDefault_Settings - - // Ensure the activity isn't excluded from recents by host flags - flags = - flags and - (ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS or - ActivityInfo.FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS) - .inv() - } - - // Notify the bridge service that we are about to start the manager - BridgeService.getService()?.preStartManager() - - // Replace the original ResolveInfo with our parasitic one - callback.result = redirectedInfo - } - } - @SuppressLint("PrivateApi") override fun onSystemServerLoaded(classLoader: ClassLoader) { runCatching { @@ -100,16 +58,61 @@ class ParasiticManagerSystemHooker : HandleSystemServerProcessHooker.Callback { } } + // Locate the exact resolveActivity method + val resolveMethod = + supervisorClass.declaredMethods.first { it.name == "resolveActivity" } + // Hook the resolution method to inject our redirection logic - LSPosedHelper.hookMethod( - Hooker::class.java, - supervisorClass, - "resolveActivity", - Intent::class.java, - ResolveInfo::class.java, - Int::class.javaPrimitiveType, - ProfilerInfo::class.java, - ) + VectorHookBuilder(resolveMethod).intercept { chain -> + Utils.logD("inside resolveMethod, calling proceed") + // 1. Execute the original resolution first + val result = chain.proceed() + + val intent = chain.args[0] as? Intent ?: return@intercept result + Utils.logD("proceed called, intent ${intent}") + + // Check if this intent is meant for the LSPosed Manager + if (!intent.hasCategory(BuildConfig.ManagerPackageName + ".LAUNCH_MANAGER")) + return@intercept result + + val originalActivityInfo = + result as? ActivityInfo + ?: run { + Utils.logD( + "Redirection: result is not ActivityInfo (was ${result?.javaClass?.name})" + ) + return@intercept result + } + + // We only intercept if it's currently resolving to the shell/fallback + if (originalActivityInfo.packageName != BuildConfig.InjectedPackageName) + return@intercept result + + Utils.logD("creat redirectedInfo") + // --- Redirection Logic --- + // We create a copy of the ActivityInfo to avoid polluting the system's cache. + val redirectedInfo = + ActivityInfo(originalActivityInfo).apply { + // Force the manager to run in its own dedicated process name + processName = BuildConfig.ManagerPackageName + + // Set a standard theme so transition animations work correctly + theme = android.R.style.Theme_DeviceDefault_Settings + + // Ensure the activity isn't excluded from recents by host flags + flags = + flags and + (ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS or + ActivityInfo.FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS) + .inv() + } + + // Notify the bridge service that we are about to start the manager + BridgeService.getService()?.preStartManager() + + Utils.logD("returning redirectedInfo ${redirectedInfo}") + redirectedInfo + } Utils.logD("Successfully hooked Activity Supervisor for Manager redirection.") } diff --git a/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt index 164d86d84..279e1e90b 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt @@ -2,13 +2,13 @@ package org.matrix.vector.core import android.os.IBinder import android.os.Process -import org.lsposed.lspd.core.ApplicationServiceClient.serviceClient -import org.lsposed.lspd.core.Startup import org.lsposed.lspd.service.ILSPApplicationService import org.lsposed.lspd.util.Utils import org.matrix.vector.BuildConfig import org.matrix.vector.ParasiticManagerHooker import org.matrix.vector.ParasiticManagerSystemHooker +import org.matrix.vector.Startup +import org.matrix.vector.impl.core.VectorServiceClient /** Main entry point for the Java-side loader, invoked via JNI from the Vector Zygisk module. */ object Main { @@ -40,7 +40,7 @@ object Main { Startup.initXposed(isSystem, niceName, appDir, appService) // Configure logging levels from the service client - runCatching { Utils.Log.muted = serviceClient.isLogMuted } + runCatching { Utils.Log.muted = VectorServiceClient.isLogMuted } .onFailure { t -> Utils.logE("Failed to configure logs from service", t) } // Check if this process is the designated Vector Manager. @@ -52,7 +52,7 @@ object Main { } // Standard Xposed module loading for third-party apps - Utils.logI("Loading Vector/Xposed for $niceName (UID: ${Process.myUid()})") + Utils.logV("Loading Vector/Xposed for $niceName (UID: ${Process.myUid()})") Startup.bootstrapXposed(isSystem && isLateInject) } }