From 6af9f1573656e3f4d1b6d1221321f3a517eb2627 Mon Sep 17 00:00:00 2001 From: Yunjie Ye Date: Thu, 30 Jul 2026 10:36:06 +0200 Subject: [PATCH 1/3] Evict a pre-cached LoadedApk before binding the parasitic manager Reported on EMUI and HarmonyOS: the manager never opens. Both are Android-based -- HarmonyOS 2 on Android 10-11, 3 through 4.2 on Android 12, while HarmonyOS 5 drops the AOSP layer and cannot run APKs at all -- and both are said to pre-warm the shell process. When a LoadedApk for the host package is already cached by the time bindApplication arrives, ActivityThread#getPackageInfo returns that instance and never looks at the ApplicationInfo we just swapped in. Its one repair path would have fixed everything: updateApplicationInfo overwrites mApplicationInfo and calls createOrUpdateClassLoaderLocked with the added path. But isLoadedApkResourceDirsUpToDate gates it on the resource and overlay directories alone, and getManagerPkgInfo copies both from the host, so sourceDir and className are never compared and the stale LoadedApk is declared current. The process then runs with mAppDir and mResDir pointing at the stock Shell.apk, and since its mApplicationInfo is a different object than ours, Hook 2's identity check skips the DEX injection and MainActivity cannot be found. So drop the entry from mPackages and mResourcePackages first. A freshly forked process has an empty cache, which makes this a no-op on every device that already worked, and the warning logged when something is actually evicted is the only evidence anyone has that the pre-warming is real -- the report carries no logcat. Hook 2 keeps comparing by object identity. #748 also relaxed it to match BuildConfig.InjectedPackageName, but that is the host package name, which getManagerPkgInfo assigns to the parasitic ApplicationInfo as well. The guard would then also match the stale LoadedApk, a resource-only one from mResourcePackages whose empty class loader accepts addDexPath happily, and the .origin LoadedApk this file builds for itself; paired with a one-shot latch, the first wrong match would inject into the wrong loader and unhook for good. It is unnecessary besides -- once the cache is evicted, the fresh LoadedApk holds our own ApplicationInfo by reference, which is what the identity check asks for. Untested on the affected hardware. --- .../matrix/vector/ParasiticManagerHooker.kt | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt index ab322a5c0..912c14338 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt @@ -146,6 +146,34 @@ object ParasiticManagerHooker { .onFailure { Utils.logW("Could not send binder to LSPosed Manager", it) } } + /** + * Drops any [LoadedApk] the process already holds for [packageName]. + * + * Some OEM ROMs, reported on EMUI and HarmonyOS, pre-warm the host process, so a `LoadedApk` + * built from the stock host APK can already be cached when `bindApplication` arrives. + * `ActivityThread#getPackageInfo` would then return that instance and keep its original + * `ApplicationInfo`: its only repair path is guarded by `isLoadedApkResourceDirsUpToDate`, + * which compares nothing but the resource and overlay directories, and those we copy from the + * host in [getManagerPkgInfo]. The manager's `sourceDir` would never be picked up. + * + * A freshly forked process has an empty cache, which makes this a no-op on most devices. The + * warning below is therefore also the only evidence that such pre-warming is real. + */ + private fun evictCachedLoadedApk(packageName: String) { + runCatching { + val at = ActivityThread.currentActivityThread() + for (field in arrayOf("mPackages", "mResourcePackages")) { + val cache = XposedHelpers.getObjectField(at, field) as? ArrayMap<*, *> + if (cache == null) { + Utils.logW("ActivityThread#$field is not an ArrayMap, not evicting") + } else if (cache.remove(packageName) != null) { + Utils.logW("Evicted a pre-cached LoadedApk of $packageName from $field") + } + } + } + .onFailure { logE("Failed to evict the cached LoadedApk of $packageName", it) } + } + private fun hookForManager(managerService: ILSPManagerService) { // Hook 1: Swap ApplicationInfo during host binding XposedHelpers.findAndHookMethod( @@ -158,7 +186,8 @@ object ParasiticManagerHooker { val bindData = param.args[0] val hostAppInfo = XposedHelpers.getObjectField(bindData, "appInfo") as ApplicationInfo - val parasiticInfo = getManagerPkgInfo(hostAppInfo)?.applicationInfo + val parasiticInfo = getManagerPkgInfo(hostAppInfo)?.applicationInfo ?: return + evictCachedLoadedApk(hostAppInfo.packageName) XposedHelpers.setObjectField(bindData, "appInfo", parasiticInfo) } }, From 2f8b40315a432fb6b98a702da75a3ffedb419d1d Mon Sep 17 00:00:00 2001 From: Yunjie Ye Date: Thu, 30 Jul 2026 21:31:27 +0800 Subject: [PATCH 2/3] Evict a pre-cached LoadedApk before binding the parasitic manager --- .../src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt index 912c14338..d76654381 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt @@ -149,7 +149,7 @@ object ParasiticManagerHooker { /** * Drops any [LoadedApk] the process already holds for [packageName]. * - * Some OEM ROMs, reported on EMUI and HarmonyOS, pre-warm the host process, so a `LoadedApk` + * Some OEM ROMs, reported on HarmonyOS, pre-warm the host process, so a `LoadedApk` * built from the stock host APK can already be cached when `bindApplication` arrives. * `ActivityThread#getPackageInfo` would then return that instance and keep its original * `ApplicationInfo`: its only repair path is guarded by `isLoadedApkResourceDirsUpToDate`, From 3a5ef654ac7ce4ae4e52f74868df76a8cb9f9dcb Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 30 Jul 2026 17:56:20 +0200 Subject: [PATCH 3/3] Rewrap the eviction KDoc after EMUI was dropped The paragraph no longer matched what ktfmt produces. CI would not have caught it: core.yml only runs zipAll. --- .../main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt index d76654381..79be41415 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt @@ -149,8 +149,8 @@ object ParasiticManagerHooker { /** * Drops any [LoadedApk] the process already holds for [packageName]. * - * Some OEM ROMs, reported on HarmonyOS, pre-warm the host process, so a `LoadedApk` - * built from the stock host APK can already be cached when `bindApplication` arrives. + * Some OEM ROMs, reported on HarmonyOS, pre-warm the host process, so a `LoadedApk` built from + * the stock host APK can already be cached when `bindApplication` arrives. * `ActivityThread#getPackageInfo` would then return that instance and keep its original * `ApplicationInfo`: its only repair path is guarded by `isLoadedApkResourceDirsUpToDate`, * which compares nothing but the resource and overlay directories, and those we copy from the