From e3b15cf0de71afc869d4347e66a2837b104a067e Mon Sep 17 00:00:00 2001 From: lzup333 <3463541577@qq.com> Date: Sat, 22 Aug 2026 02:05:17 +0800 Subject: [PATCH] Add per-app ART inline hook compatibility mode Adds a per-package switch to disable Vector's ART inline hooks in selected apps for compatibility. Long-pressing an app in a module's scope now offers the toggle in the package action sheet, instead of a dedicated screen. The native routine restores libart.so's file-backed executable image and the process's pre-injection modifications after framework bootstrap, leaving LSPlant/Dobby metadata intact; it applies to the app the next time it starts. The daemon stores the configured set per package (including the system UI as "system"), keyed through a new preference, and the manager reads and writes it over AIDL. Included translations for all shipped languages. --- .../daemon/data/InlineHookProcessPolicy.kt | 28 + .../vector/daemon/data/PreferenceStore.kt | 56 ++ .../vector/daemon/ipc/FrameworkService.kt | 13 + .../vector/daemon/ipc/ManagerService.kt | 6 + .../vector/manager/demo/FakeManagerService.kt | 6 + .../matrix/vector/manager/ipc/DaemonClient.kt | 19 + .../ui/components/PackageActionMenu.kt | 45 ++ manager/src/main/res/values-ar/strings.xml | 5 + manager/src/main/res/values-de/strings.xml | 5 + manager/src/main/res/values-es/strings.xml | 5 + manager/src/main/res/values-fa/strings.xml | 5 + manager/src/main/res/values-fr/strings.xml | 5 + manager/src/main/res/values-in/strings.xml | 5 + manager/src/main/res/values-it/strings.xml | 5 + manager/src/main/res/values-iw/strings.xml | 5 + manager/src/main/res/values-ja/strings.xml | 5 + manager/src/main/res/values-ko/strings.xml | 5 + manager/src/main/res/values-pl/strings.xml | 5 + .../src/main/res/values-pt-rBR/strings.xml | 5 + manager/src/main/res/values-ru/strings.xml | 5 + manager/src/main/res/values-tr/strings.xml | 5 + manager/src/main/res/values-uk/strings.xml | 5 + manager/src/main/res/values-vi/strings.xml | 5 + .../src/main/res/values-zh-rCN/strings.xml | 5 + .../src/main/res/values-zh-rTW/strings.xml | 5 + manager/src/main/res/values/strings.xml | 6 + .../core/art_inline_hook_invalidation.h | 33 ++ native/src/core/art_inline_hook_cleanup.cpp | 504 ++++++++++++++++++ .../matrix/vector/ipc/IManagerService.aidl | 26 +- zygisk/src/main/cpp/include/ipc_bridge.h | 6 + zygisk/src/main/cpp/ipc_bridge.cpp | 26 + zygisk/src/main/cpp/module.cpp | 76 ++- 32 files changed, 922 insertions(+), 18 deletions(-) create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/data/InlineHookProcessPolicy.kt create mode 100644 native/include/core/art_inline_hook_invalidation.h create mode 100644 native/src/core/art_inline_hook_cleanup.cpp diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/InlineHookProcessPolicy.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/InlineHookProcessPolicy.kt new file mode 100644 index 000000000..4334f3847 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/InlineHookProcessPolicy.kt @@ -0,0 +1,28 @@ +package org.matrix.vector.daemon.data + +import android.os.Process + +/** Pure process matching rules shared by the daemon policy and local unit tests. */ +object InlineHookProcessPolicy { + fun matchesSystemUiVirtualPackage( + configuredPackages: Set, + processName: String, + uid: Int + ): Boolean = + SYSTEM_UI_VIRTUAL_PACKAGE in configuredPackages && + uid == Process.SYSTEM_UID && + processName == SYSTEM_UI_PROCESS + + fun matchesPackage( + expectedUid: Int, + actualUid: Int, + processName: String, + applicationProcessName: String?, + componentProcesses: Set + ): Boolean = + expectedUid == actualUid && + (processName == applicationProcessName || processName in componentProcesses) + + fun mayInvalidate(processName: String, uid: Int): Boolean = + uid != Process.SYSTEM_UID || processName != "system" +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt index bf1f4905f..e26b653a1 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt @@ -3,8 +3,12 @@ package org.matrix.vector.daemon.data import android.content.ContentValues import android.database.sqlite.SQLiteDatabase import org.apache.commons.lang3.SerializationUtilsX +import org.matrix.vector.daemon.system.* private const val TAG = "VectorPreferenceStore" +private const val INVALIDATE_ART_INLINE_HOOKS_KEY_PREFIX = "invalidate_art_inline_hooks:" +const val SYSTEM_UI_VIRTUAL_PACKAGE = "system" +const val SYSTEM_UI_PROCESS = "system:ui" object PreferenceStore { @@ -100,4 +104,56 @@ object PreferenceStore { fun isScopeRequestBlocked(pkg: String): Boolean = (getModulePrefs("lspd", 0, "config")["scope_request_blocked"] as? Set<*>)?.contains(pkg) == true + + fun getInvalidateArtInlineHookPackages(): Set { + return getModulePrefs("lspd", 0, "config") + .asSequence() + .filter { (key, value) -> + key.startsWith(INVALIDATE_ART_INLINE_HOOKS_KEY_PREFIX) && value == true + } + .map { (key, _) -> key.removePrefix(INVALIDATE_ART_INLINE_HOOKS_KEY_PREFIX) } + .filter { it.isNotBlank() } + .toSet() + } + + /** Updates one package without replacing another Manager client's choices. */ + fun setInvalidateArtInlineHooks(packageName: String, enabled: Boolean): Boolean { + val normalized = packageName.trim() + if (normalized.isEmpty()) return false + updateModulePref( + "lspd", + 0, + "config", + INVALIDATE_ART_INLINE_HOOKS_KEY_PREFIX + normalized, + if (enabled) true else null) + return true + } + + /** + * Resolves the configured package list against the actual process topology for this user. + * This deliberately avoids assuming that every Android process name starts with its package name. + */ + fun shouldInvalidateArtInlineHooks(processName: String, uid: Int): Boolean { + val configured = getInvalidateArtInlineHookPackages() + if (configured.isEmpty()) return false + + if (InlineHookProcessPolicy.matchesSystemUiVirtualPackage(configured, processName, uid)) { + return true + } + + val userId = uid / PER_USER_RANGE + return configured.any { packageName -> + if (packageName == SYSTEM_UI_VIRTUAL_PACKAGE) return@any false + val info = + packageManager?.getPackageInfoWithComponents(packageName, MATCH_ALL_FLAGS, userId) + ?: return@any false + val applicationInfo = info.applicationInfo ?: return@any false + InlineHookProcessPolicy.matchesPackage( + expectedUid = applicationInfo.uid, + actualUid = uid, + processName = processName, + applicationProcessName = applicationInfo.processName, + componentProcesses = info.fetchProcesses()) + } + } } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/FrameworkService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/FrameworkService.kt index 44ed1c650..165633d7a 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/FrameworkService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/FrameworkService.kt @@ -15,6 +15,8 @@ import org.matrix.vector.ipc.IProcessChannel import org.matrix.vector.ipc.IFrameworkService import org.matrix.vector.daemon.data.ConfigCache import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.data.InlineHookProcessPolicy +import org.matrix.vector.daemon.data.PreferenceStore import org.matrix.vector.daemon.system.FIRST_APPLICATION_UID import org.matrix.vector.daemon.system.PER_USER_RANGE import org.matrix.vector.daemon.utils.InstallerVerifier @@ -29,6 +31,8 @@ const val DEX_TRANSACTION_CODE = ('_'.code shl 24) or ('D'.code shl 16) or ('E'.code shl 8) or 'X'.code const val OBFUSCATION_MAP_TRANSACTION_CODE = ('_'.code shl 24) or ('O'.code shl 16) or ('B'.code shl 8) or 'F'.code +const val INVALIDATE_ART_INLINE_HOOKS_TRANSACTION_CODE = + ('_'.code shl 24) or ('I'.code shl 16) or ('N'.code shl 8) or 'L'.code /** * What an injected process asks the framework for — this project's `IFrameworkService`. @@ -241,6 +245,15 @@ object FrameworkService : IFrameworkService.Stub() { } return true } + INVALIDATE_ART_INLINE_HOOKS_TRANSACTION_CODE -> { + val info = ensureRegistered() + val invalidate = + InlineHookProcessPolicy.mayInvalidate(info.processName, info.key.uid) && + PreferenceStore.shouldInvalidateArtInlineHooks(info.processName, info.key.uid) + reply?.writeNoException() + reply?.writeInt(if (invalidate) 1 else 0) + return true + } } return super.onTransact(code, data, reply, flags) } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt index 96251dffc..4f2aee083 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt @@ -284,6 +284,12 @@ object ManagerService : IManagerService.Stub() { if (isVerboseLogEnabled()) LogcatMonitor.startVerbose() else LogcatMonitor.stopVerbose() } + override fun getInvalidateArtInlineHookPackages(): MutableList = + PreferenceStore.getInvalidateArtInlineHookPackages().sorted().toMutableList() + + override fun setInvalidateArtInlineHooks(packageName: String, enabled: Boolean): Boolean = + PreferenceStore.setInvalidateArtInlineHooks(packageName, enabled) + override fun getLogParts(verbose: Boolean): List = FileSystem.listLogParts(verbose) override fun getLogPart(verbose: Boolean, name: String): ParcelFileDescriptor? = diff --git a/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt index a0e7964f0..503568623 100644 --- a/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt +++ b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt @@ -224,6 +224,12 @@ class FakeManagerService( real?.setVerboseLogEnabled(enabled) } + override fun getInvalidateArtInlineHookPackages(): MutableList = + real?.invalidateArtInlineHookPackages.orEmpty().sorted().toMutableList() + + override fun setInvalidateArtInlineHooks(packageName: String, enabled: Boolean): Boolean = + real?.setInvalidateArtInlineHooks(packageName, enabled) ?: false + override fun getLiveLogPart(verbose: Boolean): ParcelFileDescriptor? = real?.getLiveLogPart(verbose) diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ipc/DaemonClient.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ipc/DaemonClient.kt index b4c0705f8..4637b75f7 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ipc/DaemonClient.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ipc/DaemonClient.kt @@ -218,6 +218,25 @@ class DaemonClient(private val serviceState: StateFlow) { suspend fun setVerboseLogEnabled(enabled: Boolean): Result = runIpc { it.setVerboseLogEnabled(enabled) } + /** + * Every package opted into ART inline-hook invalidation, sorted. + * + * Empty against a daemon too old to answer the call, in which case the manager shows none. + */ + suspend fun getInvalidateArtInlineHookPackages(): Result> = runIpc { + it.invalidateArtInlineHookPackages.orEmpty() + } + + /** + * Sets whether a package invalidates Vector's native ART inline hooks after injection. + * + * [Result] carries the daemon's own answer: it stores the choice and reports whether the write + * landed, so a blank package name or a refused write reaches the caller rather than reading as a + * silent success. + */ + suspend fun setInvalidateArtInlineHooks(packageName: String, enabled: Boolean): Result = + runIpc { it.setInvalidateArtInlineHooks(packageName, enabled) } + /** * The rotated parts the daemon still holds for one of the two logs, oldest first. * diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/PackageActionMenu.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/PackageActionMenu.kt index 6e64fc7be..e247f9418 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/PackageActionMenu.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/PackageActionMenu.kt @@ -52,6 +52,7 @@ import android.text.format.Formatter import androidx.compose.material.icons.rounded.ArrowCircleUp import androidx.compose.material.icons.rounded.CloudDownload import androidx.compose.material.icons.rounded.CloudOff +import androidx.compose.material.icons.rounded.FlashOff import androidx.compose.material.icons.rounded.NotificationsOff import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -363,6 +364,50 @@ LocalizedOverlay { } } + // The inverse of re-optimizing, also only for a hook target. Where re-optimizing clears + // the inlined-away hooks ART has baked in, this stops Vector from installing ART inline + // hooks in the first place — the same silence, but a compatibility escape hatch rather + // than a fix: it trades the hooks of every module against an app that otherwise breaks or + // crashes. Read and written per package through the daemon, so the switch starts as the + // stored value and flips only as far as the daemon agrees. + if (!isModule) { + var invalidateInlineHooks by remember(packageName) { mutableStateOf(null) } + LaunchedEffect(packageName) { + invalidateInlineHooks = + daemon.getInvalidateArtInlineHookPackages().getOrNull()?.contains(packageName) + } + ActionToggleRow( + icon = Icons.Rounded.FlashOff, + title = stringResource(R.string.action_invalidate_art_inline_hooks), + subtitle = stringResource(R.string.action_invalidate_art_inline_hooks_summary), + checked = invalidateInlineHooks == true, + onCheckedChange = { enabled -> + finish { + val ok = + daemon + .setInvalidateArtInlineHooks(packageName, enabled) + .onFailure { e -> + logE( + "actions: set ART inline hook invalidation for " + + "$packageName failed", + e, + ) + } + .getOrDefault(false) + PackageActionResult( + when { + !ok -> R.string.action_invalidate_art_inline_hooks_failed + enabled -> R.string.action_invalidate_art_inline_hooks_enabled + else -> R.string.action_invalidate_art_inline_hooks_disabled + }, + appName, + tone = if (ok) SnackbarTone.Success else SnackbarTone.Failure, + ) + } + }, + ) + } + if (isModule) { HorizontalDivider(Modifier.padding(horizontal = 24.dp, vertical = 4.dp)) ActionDrawerItem( diff --git a/manager/src/main/res/values-ar/strings.xml b/manager/src/main/res/values-ar/strings.xml index 932f431a3..a7dc4571d 100644 --- a/manager/src/main/res/values-ar/strings.xml +++ b/manager/src/main/res/values-ar/strings.xml @@ -464,4 +464,9 @@ ليس لـ Vector أيقونة بعد يعمل Vector داخل عملية أخرى بدل أن يكون مثبَّتًا، فلا يظهر شيء في المشغّل ولا توجد طريقة واضحة للعودة إليه. امنحه اختصارًا على الشاشة الرئيسية، أو ثبِّته كتطبيق عادي. عدم السؤال مجددًا + وضع توافق ربط ART المضمّن + تعطيل ربط ART المضمّن الخاص بـ Vector في هذا التطبيق لتحسين التوافق. قد تتوقف بعض الوحدات عن العمل هنا، وقد يتعطل التطبيق أو ينهار. يُطبّق عند تشغيل التطبيق في المرة القادمة. + تم تفعيل توافق ربط ART المضمّن لـ %1$s. + تم تعطيل توافق ربط ART المضمّن لـ %1$s. + تعذّر تغيير توافق ربط ART المضمّن لـ %1$s. diff --git a/manager/src/main/res/values-de/strings.xml b/manager/src/main/res/values-de/strings.xml index 55a712de7..7da3809f6 100644 --- a/manager/src/main/res/values-de/strings.xml +++ b/manager/src/main/res/values-de/strings.xml @@ -420,4 +420,9 @@ Vector hat noch kein Symbol Vector läuft in einem fremden Prozess, statt installiert zu sein — im Launcher erscheint also nichts, und es gibt keinen offensichtlichen Weg zurück. Gib ihm eine Verknüpfung auf dem Startbildschirm, oder installiere es als gewöhnliche App. Nicht mehr fragen + ART-Inline-Hook-Kompatibilitätsmodus + Deaktiviere Vector\'s ART-Inline-Hooks in dieser App, um die Kompatibilität zu verbessern. Einige Module funktionieren hier möglicherweise nicht mehr, und die App kann sich fehlverhalten oder abstürzen. Wirkt beim nächsten Start der App. + ART-Inline-Hook-Kompatibilität für %1$s aktiviert. + ART-Inline-Hook-Kompatibilität für %1$s deaktiviert. + ART-Inline-Hook-Kompatibilität für %1$s konnte nicht geändert werden. diff --git a/manager/src/main/res/values-es/strings.xml b/manager/src/main/res/values-es/strings.xml index d3fc8ad5c..5a70e8eb1 100644 --- a/manager/src/main/res/values-es/strings.xml +++ b/manager/src/main/res/values-es/strings.xml @@ -420,4 +420,9 @@ Vector todavía no tiene icono Vector se ejecuta dentro de otro proceso en lugar de estar instalado, así que no aparece nada en tu launcher y no hay una forma evidente de volver. Dale un acceso directo en la pantalla de inicio, o instálalo como una app normal. No volver a preguntar + Modo de compatibilidad de hooks inline de ART + Desactiva los hooks inline de ART de Vector en esta app para mejorar la compatibilidad. Algunos módulos pueden dejar de funcionar aquí, y la app puede comportarse mal o bloquearse. Se aplica la próxima vez que se inicie la app. + Compatibilidad de hooks inline de ART habilitada para %1$s. + Compatibilidad de hooks inline de ART deshabilitada para %1$s. + No se pudo cambiar la compatibilidad de hooks inline de ART para %1$s. diff --git a/manager/src/main/res/values-fa/strings.xml b/manager/src/main/res/values-fa/strings.xml index d4c8a0670..500bd710f 100644 --- a/manager/src/main/res/values-fa/strings.xml +++ b/manager/src/main/res/values-fa/strings.xml @@ -420,4 +420,9 @@ Vector هنوز نمادی ندارد Vector به‌جای آنکه نصب شده باشد درون فرایندی دیگر اجرا می‌شود، پس چیزی در لانچر پیدا نمی‌شود و راه روشنی برای بازگشت به آن نیست. به آن میان‌بری در صفحهٔ اصلی بدهید، یا آن را مانند برنامه‌ای معمولی نصب کنید. دیگر پرسیده نشود + حالت سازگاری هوک درون‌خطی ART + غیرفعال کردن هوک‌های درون‌خطی ART ویکتور در این برنامه برای بهبود سازگاری. برخی ماژول‌ها ممکن است در اینجا از کار بیفتند و برنامه ممکن است دچار مشکل یا کرش شود. در راه‌اندازی بعدی برنامه اعمال می‌شود. + حالت سازگاری هوک درون‌خطی ART برای %1$s فعال شد. + حالت سازگاری هوک درون‌خطی ART برای %1$s غیرفعال شد. + تغییر حالت سازگاری هوک درون‌خطی ART برای %1$s ممکن نبود. diff --git a/manager/src/main/res/values-fr/strings.xml b/manager/src/main/res/values-fr/strings.xml index 3a401d36b..220168f10 100644 --- a/manager/src/main/res/values-fr/strings.xml +++ b/manager/src/main/res/values-fr/strings.xml @@ -420,4 +420,9 @@ Vector n\'a pas encore d\'icône Vector s\'exécute dans un autre processus au lieu d\'être installé : rien n\'apparaît dans votre lanceur et il n\'y a pas de moyen évident d\'y revenir. Donnez-lui un raccourci sur l\'écran d\'accueil, ou installez-le comme une application ordinaire. Ne plus demander + Mode de compatibilité des hooks inline ART + Désactive les hooks inline ART de Vector dans cette application pour améliorer la compatibilité. Certains modules peuvent cesser de fonctionner ici et l\'application peut mal se comporter ou planter. S\'applique au prochain démarrage de l\'application. + Compatibilité des hooks inline ART activée pour %1$s. + Compatibilité des hooks inline ART désactivée pour %1$s. + Impossible de modifier la compatibilité des hooks inline ART pour %1$s. diff --git a/manager/src/main/res/values-in/strings.xml b/manager/src/main/res/values-in/strings.xml index 3c6c6390e..7a7573713 100644 --- a/manager/src/main/res/values-in/strings.xml +++ b/manager/src/main/res/values-in/strings.xml @@ -413,4 +413,9 @@ Vector belum punya ikon Vector berjalan di dalam proses lain alih-alih dipasang, jadi tidak ada yang muncul di launcher Anda dan tidak ada jalan kembali yang jelas. Beri dia pintasan di layar utama, atau pasang sebagai aplikasi biasa. Jangan tanya lagi + Mode kompatibilitas hook inline ART + Menonaktifkan hook inline ART Vector di aplikasi ini untuk meningkatkan kompatibilitas. Beberapa modul mungkin berhenti bekerja di sini, dan aplikasi mungkin berperilaku tidak normal atau mogok. Berlaku saat aplikasi dimulai berikutnya. + Kompatibilitas hook inline ART diaktifkan untuk %1$s. + Kompatibilitas hook inline ART dinonaktifkan untuk %1$s. + Tidak dapat mengubah kompatibilitas hook inline ART untuk %1$s. diff --git a/manager/src/main/res/values-it/strings.xml b/manager/src/main/res/values-it/strings.xml index e71e14e18..57e90c0f9 100644 --- a/manager/src/main/res/values-it/strings.xml +++ b/manager/src/main/res/values-it/strings.xml @@ -420,4 +420,9 @@ Vector non ha ancora un\'icona Vector gira dentro un altro processo invece di essere installato, quindi nel launcher non compare nulla e non c\'è un modo evidente per tornarci. Dagli una scorciatoia nella schermata Home, oppure installalo come una normale app. Non chiedere più + Modalità di compatibilità degli hook inline ART + Disattiva gli hook inline ART di Vector in questa app per migliorare la compatibilità. Alcuni moduli potrebbero smettere di funzionare qui e l\'app potrebbe comportarsi in modo anomalo o bloccarsi. Ha effetto al prossimo avvio dell\'app. + Compatibilità hook inline ART abilitata per %1$s. + Compatibilità hook inline ART disabilitata per %1$s. + Impossibile modificare la compatibilità degli hook inline ART per %1$s. diff --git a/manager/src/main/res/values-iw/strings.xml b/manager/src/main/res/values-iw/strings.xml index 385c87583..94d4b62b6 100644 --- a/manager/src/main/res/values-iw/strings.xml +++ b/manager/src/main/res/values-iw/strings.xml @@ -446,4 +446,9 @@ ל-Vector עדיין אין סמל Vector פועל בתוך תהליך אחר במקום להיות מותקן, ולכן שום דבר לא מופיע במסך הבית ואין דרך ברורה לחזור אליו. תנו לו קיצור דרך במסך הבית, או התקינו אותו כאפליקציה רגילה. לא לשאול שוב + מצב תאימות Hook קוויים של ART + השבתה של ה-hooks הקוויים של ART של Vector באפליקציה זו כדי לשפר את התאימות. חלק מהמודולים עשויים להפסיק לעסות כאן, והאפליקציה עשויה להתנהג באופן לא תקין או לקרוס. חל בפעם הבאה שהאפליקציה תופעל. + תאימות ה-hooks הקוויים של ART הופעלה עבור %1$s. + תאימות ה-hooks הקוויים של ART הושבתה עבור %1$s. + לא ניתן היה לשנות את תאימות ה-hooks הקוויים של ART עבור %1$s. diff --git a/manager/src/main/res/values-ja/strings.xml b/manager/src/main/res/values-ja/strings.xml index 3b75f0c3f..391a4d711 100644 --- a/manager/src/main/res/values-ja/strings.xml +++ b/manager/src/main/res/values-ja/strings.xml @@ -399,4 +399,9 @@ Vector のアイコンはまだありません Vector はインストールされるのではなく、別のプロセス内で実行されるため、ランチャーには何も表示されず、Vector に戻ってくる手段もありません。ホーム画面にショートカットを作成するか、通常のアプリとしてインストールしてください。 今後表示しない + ART インラインフック互換モード + このアプリで Vector の ART インラインフックを無効化して互換性を向上させます。一部のモジュールがここで動作しなくなり、アプリが誤動作したりクラッシュしたりする可能性があります。次回アプリを起動したときに適用されます。 + %1$s の ART インラインフック互換モードを有効にしました。 + %1$s の ART インラインフック互換モードを無効にしました。 + %1$s の ART インラインフック互換モードを変更できませんでした。 diff --git a/manager/src/main/res/values-ko/strings.xml b/manager/src/main/res/values-ko/strings.xml index 533f39735..ab06e2dc0 100644 --- a/manager/src/main/res/values-ko/strings.xml +++ b/manager/src/main/res/values-ko/strings.xml @@ -409,4 +409,9 @@ Vector에 아직 아이콘이 없습니다 Vector는 설치되는 대신 다른 프로세스 안에서 실행되므로 런처에 아무것도 나타나지 않고 다시 들어올 뚜렷한 방법도 없습니다. 홈 화면 바로가기를 만들거나, 일반 앱으로 설치하세요. 다시 묻지 않기 + ART 인라인 후크 호환 모드 + 이 앱에서 Vector의 ART 인라인 후크를 비활성화하여 호환성을 개선합니다. 일부 모듈이 여기서 작동하지 않을 수 있으며 앱이 오작동하거나 충돌할 수 있습니다. 다음에 앱을 시작할 때 적용됩니다. + %1$s에 ART 인라인 후크 호환 모드를 사용하도록 설정했습니다. + %1$s에 ART 인라인 후크 호환 모드를 사용하지 않도록 설정했습니다. + %1$s의 ART 인라인 후크 호환 모드를 변경할 수 없습니다. diff --git a/manager/src/main/res/values-pl/strings.xml b/manager/src/main/res/values-pl/strings.xml index e8f40a59c..8b00880d5 100644 --- a/manager/src/main/res/values-pl/strings.xml +++ b/manager/src/main/res/values-pl/strings.xml @@ -482,4 +482,9 @@ Vector nie ma jeszcze ikony Vector działa w ramach innego procesu i nie jest instalowany, więc nic nie pojawia się w launcherze, więc nie ma oczywistej drogi powrotu. Dodaj skrót na ekranie głównym lub zainstaluj go jako zwykłą aplikację. Nie pytaj ponownie + Tryb zgodności hooków inline ART + Wyłącza hooki inline ART Vectora w tej aplikacji, aby poprawić zgodność. Niektóre moduły mogą przestać tu działać, a aplikacja może działać nieprawidłowo lub się zawieszać. Działa przy następnym uruchomieniu aplikacji. + Włączono tryb zgodności hooków inline ART dla %1$s. + Wyłączono tryb zgodności hooków inline ART dla %1$s. + Nie udało się zmienić trybu zgodności hooków inline ART dla %1$s. diff --git a/manager/src/main/res/values-pt-rBR/strings.xml b/manager/src/main/res/values-pt-rBR/strings.xml index 3977bb60a..74968fc3d 100644 --- a/manager/src/main/res/values-pt-rBR/strings.xml +++ b/manager/src/main/res/values-pt-rBR/strings.xml @@ -420,4 +420,9 @@ O Vector ainda não tem ícone O Vector roda dentro de outro processo em vez de estar instalado, então nada aparece no seu launcher e não há um caminho óbvio de volta. Dê a ele um atalho na tela inicial, ou instale-o como um app comum. Não perguntar de novo + Modo de compatibilidade de hooks inline do ART + Desativa os hooks inline do ART do Vector neste app para melhorar a compatibilidade. Alguns módulos podem parar de funcionar aqui e o app pode se comportar mal ou travar. Aplica-se na próxima vez que o app for iniciado. + Compatibilidade de hooks inline do ART ativada para %1$s. + Compatibilidade de hooks inline do ART desativada para %1$s. + Não foi possível alterar a compatibilidade de hooks inline do ART para %1$s. diff --git a/manager/src/main/res/values-ru/strings.xml b/manager/src/main/res/values-ru/strings.xml index c0c7f5370..269100801 100644 --- a/manager/src/main/res/values-ru/strings.xml +++ b/manager/src/main/res/values-ru/strings.xml @@ -425,4 +425,9 @@ У Vector пока нет значка Vector работает внутри чужого процесса, а не установлен, поэтому в лаунчере ничего не появляется и очевидного пути обратно нет. Добавьте ярлык на главный экран или установите Vector как обычное приложение. Больше не спрашивать + Режим совместимости инлайн-хуков ART + Отключает инлайн-хуки ART Vector в этом приложении для повышения совместимости. Некоторые модули могут перестать здесь работать, а приложение может вести себя некорректно или аварийно завершаться. Применяется при следующем запуске приложения. + Режим совместимости инлайн-хуков ART включён для %1$s. + Режим совместимости инлайн-хуков ART выключён для %1$s. + Не удалось изменить режим совместимости инлайн-хуков ART для %1$s. diff --git a/manager/src/main/res/values-tr/strings.xml b/manager/src/main/res/values-tr/strings.xml index 701a438e6..cfd0e6370 100644 --- a/manager/src/main/res/values-tr/strings.xml +++ b/manager/src/main/res/values-tr/strings.xml @@ -420,4 +420,9 @@ Vector\'ün henüz bir simgesi yok Vector kurulmak yerine başka bir sürecin içinde çalışır; bu yüzden başlatıcınızda hiçbir şey görünmez ve geri dönmenin bariz bir yolu yoktur. Ona ana ekranda bir kısayol verin ya da sıradan bir uygulama olarak kurun. Bir daha sorma + ART satır içi kanca uyumluluk modu + Uyumluluğu artırmak için bu uygulamada Vector\'ın ART satır içi kancalarını devre dışı bırakır. Bazı modüller burada çalışmayı durdurabilir ve uygulama arızalanabilir veya çökebilir. Uygulamanın bir sonraki başlatılışında geçerli olur. + %1$s için ART satır içi kanca uyumluluğu etkinleştirildi. + %1$s için ART satır içi kanca uyumluluğu devre dışı bırakıldı. + %1$s için ART satır içi kanca uyumluluğu değiştirilemedi. diff --git a/manager/src/main/res/values-uk/strings.xml b/manager/src/main/res/values-uk/strings.xml index 29cb7052f..85dd1f9d3 100644 --- a/manager/src/main/res/values-uk/strings.xml +++ b/manager/src/main/res/values-uk/strings.xml @@ -462,4 +462,9 @@ У Vector ще немає значка Vector працює всередині чужого процесу, а не встановлений, тому в лаунчері нічого не з\'являється і очевидного шляху назад немає. Додайте ярлик на головний екран або встановіть Vector як звичайний застосунок. Більше не питати + Режим сумісності інлайн-хуків ART + Вимкнути інлайн-хуки ART Vector у цьому застосунку для покращення сумісності. Деякі модулі можуть перестати працювати тут, а застосунок може поводитися неправильно або аварійно завершуватися. Застосовується під час наступного запуску застосунка. + Режим сумісності інлайн-хуків ART увімкнено для %1$s. + Режим сумісності інлайн-хуків ART вимкнено для %1$s. + Не вдалося змінити режим сумісності інлайн-хуків ART для %1$s. diff --git a/manager/src/main/res/values-vi/strings.xml b/manager/src/main/res/values-vi/strings.xml index c592b94e7..70b6df7d8 100644 --- a/manager/src/main/res/values-vi/strings.xml +++ b/manager/src/main/res/values-vi/strings.xml @@ -409,4 +409,9 @@ Vector vẫn chưa có biểu tượng Vector chạy bên trong một tiến trình khác thay vì được cài đặt, nên không có gì hiện ra trong trình khởi chạy và cũng không có cách quay lại rõ ràng. Hãy tạo cho nó một lối tắt trên màn hình chính, hoặc cài nó như một ứng dụng bình thường. Đừng hỏi lại + Chế độ tương thích hook nội tuyến ART + Vô hiệu hóa hook nội tuyến ART của Vector trong ứng dụng này để cải thiện khả năng tương thích. Một số mô-đun có thể ngừng hoạt động ở đây và ứng dụng có thể hoạt động sai hoặc bị treo. Có hiệu lực khi ứng dụng khởi động lần tiếp theo. + Đã bật chế độ tương thích hook nội tuyến ART cho %1$s. + Đã tắt chế độ tương thích hook nội tuyến ART cho %1$s. + Không thể thay đổi chế độ tương thích hook nội tuyến ART cho %1$s. diff --git a/manager/src/main/res/values-zh-rCN/strings.xml b/manager/src/main/res/values-zh-rCN/strings.xml index 5068e3cf5..0e42ea6a6 100644 --- a/manager/src/main/res/values-zh-rCN/strings.xml +++ b/manager/src/main/res/values-zh-rCN/strings.xml @@ -410,4 +410,9 @@ Vector 还没有图标 Vector 运行在别的进程里,而不是被安装到系统中,所以桌面上看不到它,也没有明显的途径再打开它。给它一个桌面快捷方式,或者把它安装成一个普通应用。 不再询问 + ART 内联 HOOK 兼容模式 + 在此应用中禁用 Vector 的 ART 内联 HOOK 以提高兼容性。某些模块可能在此处失效,应用可能出现异常或崩溃。下次启动该应用时生效。 + 已为 %1$s 启用 ART 内联 HOOK 兼容模式。 + 已为 %1$s 禁用 ART 内联 HOOK 兼容模式。 + 无法更改 %1$s 的 ART 内联 HOOK 兼容模式。 diff --git a/manager/src/main/res/values-zh-rTW/strings.xml b/manager/src/main/res/values-zh-rTW/strings.xml index 76a243e73..ed8946460 100644 --- a/manager/src/main/res/values-zh-rTW/strings.xml +++ b/manager/src/main/res/values-zh-rTW/strings.xml @@ -410,4 +410,9 @@ Vector 還沒有圖示 Vector 執行在別的程序裡,而不是被安裝到系統中,所以啟動器上看不到它,也沒有明顯的途徑再開啟它。給它一個主畫面捷徑,或者把它安裝成一般的應用程式。 不再詢問 + ART 內聯 HOOK 相容模式 + 在此應用程式中停用 Vector 的 ART 內聯 HOOK 以提高相容性。某些模組可能在此處失效,應用程式可能出現異常或崩潰。下次啟動該應用程式時生效。 + 已為 %1$s 啟用 ART 內聯 HOOK 相容模式。 + 已為 %1$s 停用 ART 內聯 HOOK 相容模式。 + 無法變更 %1$s 的 ART 內聯 HOOK 相容模式。 diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index ec54516e1..f3af100b2 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -92,6 +92,12 @@ A persistent notification showing that Vector is running. Stop apps hiding their launcher icons Since Android 10, an app that hides its own launcher icon gets one back that opens its app info page. Turn this off to let it stay hidden — your launcher may not catch up until it restarts. Apps that never had a launcher icon, including most modules, are unaffected. + + ART inline hook compatibility mode + Disable Vector\'s ART inline hooks in this app to improve compatibility. Some modules may stop working here, and the app may misbehave or crash. Applies the next time the app starts. + Enabled ART inline hook compatibility for %1$s. + Disabled ART inline hook compatibility for %1$s. + Could not change ART inline hook compatibility for %1$s. Recent activity diff --git a/native/include/core/art_inline_hook_invalidation.h b/native/include/core/art_inline_hook_invalidation.h new file mode 100644 index 000000000..a95561885 --- /dev/null +++ b/native/include/core/art_inline_hook_invalidation.h @@ -0,0 +1,33 @@ +#pragma once + +namespace vector::native { + +/** + * Configure per-process ART inline-hook invalidation state before LSPlant initialization. + * + * This is reset for every specialized process. system_server and normal apps that are not on the + * compatibility list pass false, making the post-bootstrap invalidation call a no-op. + * + * Returns whether invalidation is armed. Enabling can fail when the pre-Vector libart executable + * state cannot be captured safely; in that case invalidation remains disabled so existing native + * hooks are never overwritten without a recoverable baseline. + */ +bool ConfigureArtInlineHookInvalidation(bool enabled); + +/** Record/forget native libart.so targets installed through LSPlant's InitInfo hook handler. */ +void RecordArtInlineHookInvalidationTarget(void *target); +void ForgetArtInlineHookInvalidationTarget(void *target); + +/** + * Run the one-shot compatibility invalidation after framework bootstrap and before app loading. + * + * For opted-in apps, replace executable libart.so segments containing recorded Vector/LSPlant + * targets with clean private mappings, then restore executable pages that were already modified + * before Vector installed its hooks. This intentionally does not perform normal Dobby hook teardown, + * so LSPlant/Dobby trampoline and interceptor metadata remain intact while Vector's patched libart + * entry code is removed. + * Disabled/already-invalidated states are successful no-ops. + */ +bool InvalidateArtInlineHooksIfEnabled(); + +} // namespace vector::native diff --git a/native/src/core/art_inline_hook_cleanup.cpp b/native/src/core/art_inline_hook_cleanup.cpp new file mode 100644 index 000000000..a080b6fe4 --- /dev/null +++ b/native/src/core/art_inline_hook_cleanup.cpp @@ -0,0 +1,504 @@ +#include "core/art_inline_hook_invalidation.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "common/logging.h" + +namespace vector::native { +namespace { + +std::mutex g_art_invalidation_mutex; +std::vector g_art_inline_hook_targets; +bool g_art_invalidation_enabled = false; +bool g_art_invalidation_completed = false; + +struct LibArtRestoreResult { + bool found = false; + bool success = false; + size_t executable_segments = 0; + size_t modified_pages = 0; + size_t restored_bytes = 0; +}; + +struct PreservedExecutablePage { + uintptr_t address = 0; + std::vector bytes; +}; + +struct LibArtSnapshotResult { + bool found = false; + bool success = false; + std::vector modified_pages; +}; + +std::vector g_preserved_art_pages; + +bool IsLibArtPath(const char *path) { + if (!path || *path == '\0') return false; + const char *name = std::strrchr(path, '/'); + name = name ? name + 1 : path; + return std::strcmp(name, "libart.so") == 0; +} + +int ProtectionFromFlags(ElfW(Word) flags) { + int protection = 0; + if ((flags & PF_R) != 0) protection |= PROT_READ; + if ((flags & PF_W) != 0) protection |= PROT_WRITE; + if ((flags & PF_X) != 0) protection |= PROT_EXEC; + return protection; +} + +bool GetPageLayout(size_t &page_size, uintptr_t &page_mask) { + const long value = sysconf(_SC_PAGESIZE); + if (value <= 0 || (value & (value - 1)) != 0) { + LOGE("Failed to determine a valid page size while processing libart.so"); + return false; + } + page_size = static_cast(value); + page_mask = static_cast(page_size - 1); + return true; +} + +bool SegmentContainsTrackedTarget(uintptr_t segment_start, uintptr_t segment_end) { + return std::any_of(g_art_inline_hook_targets.begin(), g_art_inline_hook_targets.end(), + [segment_start, segment_end](const void *target) { + const auto address = reinterpret_cast(target); + return address >= segment_start && address < segment_end; + }); +} + +bool CaptureExecutablePages(const dl_phdr_info *info, LibArtSnapshotResult &result) { + const char *path = info->dlpi_name; + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + PLOGE("Failed to open libart backing file '{}' while capturing existing hooks", path); + return false; + } + + struct stat file_stat {}; + if (fstat(fd, &file_stat) != 0 || file_stat.st_size <= 0) { + PLOGE("Failed to stat libart backing file '{}' while capturing existing hooks", path); + close(fd); + return false; + } + + const size_t file_size = static_cast(file_stat.st_size); + void *file_map = mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0); + if (file_map == MAP_FAILED) { + PLOGE("Failed to map libart backing file '{}' while capturing existing hooks", path); + close(fd); + return false; + } + + size_t page_size = 0; + uintptr_t page_mask = 0; + if (!GetPageLayout(page_size, page_mask)) { + munmap(file_map, file_size); + close(fd); + return false; + } + + bool success = true; + const auto *clean_file = static_cast(file_map); + for (ElfW(Half) i = 0; i < info->dlpi_phnum; ++i) { + const ElfW(Phdr) &phdr = info->dlpi_phdr[i]; + if (phdr.p_type != PT_LOAD || (phdr.p_flags & PF_X) == 0 || phdr.p_filesz == 0) continue; + + const size_t file_offset = static_cast(phdr.p_offset); + const size_t segment_size = static_cast(phdr.p_filesz); + const uintptr_t image_base = static_cast(info->dlpi_addr); + const uintptr_t virtual_address = static_cast(phdr.p_vaddr); + if (virtual_address > UINTPTR_MAX - image_base) { + LOGE("Executable libart segment {} snapshot address overflows", i); + success = false; + continue; + } + const uintptr_t segment_start = image_base + virtual_address; + if (segment_size > UINTPTR_MAX - segment_start) { + LOGE("Executable libart segment {} snapshot range overflows", i); + success = false; + continue; + } + const size_t first_page_prefix = static_cast(segment_start & page_mask); + if (file_offset < first_page_prefix || segment_size > SIZE_MAX - first_page_prefix) { + LOGE("Executable libart segment {} has invalid aligned snapshot bounds", i); + success = false; + continue; + } + + const size_t mapping_span = first_page_prefix + segment_size; + if (mapping_span > SIZE_MAX - page_mask) { + LOGE("Executable libart segment {} snapshot size overflows", i); + success = false; + continue; + } + const size_t mapping_size = (mapping_span + page_mask) & ~page_mask; + const size_t first_page_file_offset = file_offset - first_page_prefix; + if (first_page_file_offset > file_size || mapping_size > file_size - first_page_file_offset) { + LOGE("Executable libart segment {} aligned snapshot exceeds backing file bounds", i); + success = false; + continue; + } + + const uintptr_t mapping_start = segment_start & ~page_mask; + for (size_t offset = 0; offset < mapping_size; offset += page_size) { + auto *live = reinterpret_cast(mapping_start + offset); + const auto *clean = clean_file + first_page_file_offset + offset; + if (std::memcmp(live, clean, page_size) == 0) continue; + + PreservedExecutablePage page; + page.address = mapping_start + offset; + page.bytes.assign(live, live + page_size); + result.modified_pages.emplace_back(std::move(page)); + } + } + + munmap(file_map, file_size); + close(fd); + return success; +} + +int CaptureLibArtCallback(dl_phdr_info *info, size_t, void *data) { + if (!IsLibArtPath(info->dlpi_name)) return 0; + + auto &result = *static_cast(data); + result.found = true; + result.success = CaptureExecutablePages(info, result); + return 1; +} + +LibArtSnapshotResult CaptureLibArtExecutableState() { + LibArtSnapshotResult result; + dl_iterate_phdr(CaptureLibArtCallback, &result); + if (!result.found) LOGE("Unable to locate loaded libart.so before installing ART hooks"); + return result; +} + +bool PreparePreservedPagesForRewrite(uintptr_t mapping_start, uintptr_t mapping_end, + int original_protection, size_t page_size) { + for (const auto &page : g_preserved_art_pages) { + if (page.address < mapping_start || page.address >= mapping_end) continue; + if (page.bytes.size() != page_size) { + LOGE("Invalid preserved libart page size at {}", reinterpret_cast(page.address)); + return false; + } + + const int writable_protection = original_protection | PROT_WRITE; + if (mprotect(reinterpret_cast(page.address), page_size, writable_protection) != 0) { + PLOGE("Cannot make preserved libart page at {} writable", + reinterpret_cast(page.address)); + return false; + } + if (mprotect(reinterpret_cast(page.address), page_size, original_protection) != 0) { + PLOGE("Cannot restore protection for preserved libart page at {}", + reinterpret_cast(page.address)); + return false; + } + } + return true; +} + +bool RestorePreservedPages(uintptr_t mapping_start, uintptr_t mapping_end, int original_protection, + size_t page_size) { + bool success = true; + for (const auto &page : g_preserved_art_pages) { + if (page.address < mapping_start || page.address >= mapping_end) continue; + + const int writable_protection = original_protection | PROT_WRITE; + if (mprotect(reinterpret_cast(page.address), page_size, writable_protection) != 0) { + PLOGE("Failed to make preserved libart page at {} writable", + reinterpret_cast(page.address)); + success = false; + continue; + } + std::memcpy(reinterpret_cast(page.address), page.bytes.data(), page_size); + __builtin___clear_cache(reinterpret_cast(page.address), + reinterpret_cast(page.address + page_size)); + if (mprotect(reinterpret_cast(page.address), page_size, original_protection) != 0) { + PLOGE("Failed to restore protection for preserved libart page at {}", + reinterpret_cast(page.address)); + success = false; + } + } + return success; +} + +bool RestoreExecutableSegments(const dl_phdr_info *info, LibArtRestoreResult &result) { + const char *path = info->dlpi_name; + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + PLOGE("Failed to open libart backing file '{}'", path); + return false; + } + + struct stat file_stat {}; + if (fstat(fd, &file_stat) != 0 || file_stat.st_size <= 0) { + PLOGE("Failed to stat libart backing file '{}'", path); + close(fd); + return false; + } + + const size_t file_size = static_cast(file_stat.st_size); + void *file_map = mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0); + if (file_map == MAP_FAILED) { + PLOGE("Failed to map libart backing file '{}'", path); + close(fd); + return false; + } + + size_t page_size = 0; + uintptr_t page_mask = 0; + if (!GetPageLayout(page_size, page_mask)) { + munmap(file_map, file_size); + close(fd); + return false; + } + + bool success = true; + const auto *clean_file = static_cast(file_map); + + for (ElfW(Half) i = 0; i < info->dlpi_phnum; ++i) { + const ElfW(Phdr) &phdr = info->dlpi_phdr[i]; + if (phdr.p_type != PT_LOAD || (phdr.p_flags & PF_X) == 0 || phdr.p_filesz == 0) { + continue; + } + + const size_t file_offset = static_cast(phdr.p_offset); + const size_t segment_size = static_cast(phdr.p_filesz); + if (file_offset > file_size || segment_size > file_size - file_offset) { + LOGE("Executable libart segment {} exceeds backing file bounds", i); + success = false; + continue; + } + + const uintptr_t segment_start = + static_cast(info->dlpi_addr) + static_cast(phdr.p_vaddr); + if (segment_size > UINTPTR_MAX - segment_start) { + LOGE("Executable libart segment {} address range overflows", i); + success = false; + continue; + } + const uintptr_t segment_end = segment_start + segment_size; + if (!SegmentContainsTrackedTarget(segment_start, segment_end)) continue; + ++result.executable_segments; + const auto *clean_segment = clean_file + file_offset; + const int original_protection = ProtectionFromFlags(phdr.p_flags); + const size_t first_page_prefix = static_cast(segment_start & page_mask); + if (file_offset < first_page_prefix) { + LOGE("Executable libart segment {} has an invalid page-aligned file offset", i); + success = false; + continue; + } + const size_t first_page_file_offset = file_offset - first_page_prefix; + + // Count dirty pages first, then replace the complete executable PT_LOAD in one mmap. Mapping + // individual dirty pages leaves visible VMA boundaries around every former trampoline; + // protection libraries can treat that non-standard libart layout as tampering even when all + // instruction bytes have been restored. A segment-wide file mapping recreates the loader's + // normal contiguous VMA shape. Only pages containing preserved pre-Vector modifications are + // made transiently writable below, and only after writability has been preflighted. + const uintptr_t mapping_start = segment_start & ~page_mask; + uintptr_t page_start = mapping_start; + size_t segment_modified_pages = 0; + size_t segment_restored_bytes = 0; + while (page_start < segment_end) { + const uintptr_t next_page = page_start + page_size; + if (next_page < page_start) { + LOGE("Page range overflow while invalidating libart.so"); + success = false; + break; + } + + const uintptr_t copy_start = std::max(page_start, segment_start); + const uintptr_t copy_end = std::min(next_page, segment_end); + const size_t copy_size = static_cast(copy_end - copy_start); + const size_t segment_offset = static_cast(copy_start - segment_start); + auto *live = reinterpret_cast(copy_start); + const auto *clean = clean_segment + segment_offset; + + if (std::memcmp(live, clean, copy_size) != 0) { + ++segment_modified_pages; + segment_restored_bytes += copy_size; + } + + page_start = next_page; + } + + if (segment_modified_pages == 0) continue; + + const size_t mapping_span = first_page_prefix + segment_size; + if (mapping_span > SIZE_MAX - page_mask) { + LOGE("Executable libart segment {} mapping size overflows", i); + success = false; + continue; + } + const size_t mapping_size = (mapping_span + page_mask) & ~page_mask; + if (mapping_size > UINTPTR_MAX - mapping_start) { + LOGE("Executable libart segment {} aligned address range overflows", i); + success = false; + continue; + } + const uintptr_t mapping_end = mapping_start + mapping_size; + if (first_page_file_offset > file_size || + mapping_size > file_size - first_page_file_offset) { + LOGE("Executable libart segment {} aligned mapping exceeds backing file bounds", i); + success = false; + continue; + } + + // Verify that pre-Vector modifications in this segment can be rewritten before replacing + // its mapping. If this fails, leave the original mapping and every external hook intact. + if (!PreparePreservedPagesForRewrite(mapping_start, mapping_end, original_protection, + page_size)) { + success = false; + continue; + } + + void *mapped = mmap(reinterpret_cast(mapping_start), mapping_size, + original_protection, MAP_PRIVATE | MAP_FIXED, fd, + static_cast(first_page_file_offset)); + if (mapped == MAP_FAILED) { + PLOGE("Failed to invalidate executable libart segment {} at {} from backing offset {}", + i, reinterpret_cast(mapping_start), first_page_file_offset); + success = false; + continue; + } + if (!RestorePreservedPages(mapping_start, mapping_end, original_protection, page_size)) { + success = false; + continue; + } + __builtin___clear_cache(reinterpret_cast(mapping_start), + reinterpret_cast(mapping_end)); + result.modified_pages += segment_modified_pages; + result.restored_bytes += segment_restored_bytes; + } + + munmap(file_map, file_size); + close(fd); + return success && result.executable_segments > 0; +} + +int RestoreLibArtCallback(dl_phdr_info *info, size_t, void *data) { + if (!IsLibArtPath(info->dlpi_name)) return 0; + + auto &result = *static_cast(data); + result.found = true; + result.success = RestoreExecutableSegments(info, result); + return 1; // libart.so is unique in an app process; stop after handling it. +} + +LibArtRestoreResult RestoreLibArtExecutableBytes() { + LibArtRestoreResult result; + dl_iterate_phdr(RestoreLibArtCallback, &result); + if (!result.found) { + LOGE("Unable to locate loaded libart.so for executable-byte invalidation"); + } + return result; +} + +} // namespace + +bool ConfigureArtInlineHookInvalidation(bool enabled) { + std::lock_guard lock(g_art_invalidation_mutex); + g_art_inline_hook_targets.clear(); + g_preserved_art_pages.clear(); + g_art_invalidation_enabled = false; + g_art_invalidation_completed = false; + + if (!enabled) return false; + + auto snapshot = CaptureLibArtExecutableState(); + if (!snapshot.success) { + LOGW("ART inline-hook invalidation was not armed because the pre-Vector libart.so state " + "could not be captured safely."); + return false; + } + + g_preserved_art_pages = std::move(snapshot.modified_pages); + g_art_invalidation_enabled = true; + LOGI("Preserved {} pre-existing modified libart.so executable page(s) before installing " + "Vector hooks.", + g_preserved_art_pages.size()); + return true; +} + +void RecordArtInlineHookInvalidationTarget(void *target) { + if (!target) return; + + std::lock_guard lock(g_art_invalidation_mutex); + if (!g_art_invalidation_enabled || g_art_invalidation_completed) return; + + if (std::find(g_art_inline_hook_targets.begin(), g_art_inline_hook_targets.end(), target) == + g_art_inline_hook_targets.end()) { + g_art_inline_hook_targets.push_back(target); + } +} + +void ForgetArtInlineHookInvalidationTarget(void *target) { + if (!target) return; + + std::lock_guard lock(g_art_invalidation_mutex); + g_art_inline_hook_targets.erase( + std::remove(g_art_inline_hook_targets.begin(), g_art_inline_hook_targets.end(), target), + g_art_inline_hook_targets.end()); +} + +bool InvalidateArtInlineHooksIfEnabled() { + std::lock_guard lock(g_art_invalidation_mutex); + if (!g_art_invalidation_enabled || g_art_invalidation_completed) return true; + + const size_t tracked_targets = g_art_inline_hook_targets.size(); + LOGI("Running libart.so executable-byte invalidation after framework bootstrap " + "({} tracked LSPlant target(s)).", + tracked_targets); + + if (tracked_targets == 0) { + g_preserved_art_pages.clear(); + g_art_invalidation_completed = true; + g_art_invalidation_enabled = false; + LOGI("No Vector/LSPlant ART inline-hook targets were installed; invalidation is unnecessary."); + return true; + } + + // Deliberately do not call DobbyDestroy/UnhookInline here. LSPosed-style invalidation is a + // compatibility operation, not normal hook teardown: restore libart.so's file-backed executable + // image, reapply modifications captured before Vector initialized, and leave LSPlant/Dobby + // trampoline and interceptor metadata intact. Apps opting into this mode accept that Vector's + // ART maintenance hooks no longer execute afterwards. + const LibArtRestoreResult result = RestoreLibArtExecutableBytes(); + if (!result.success) { + g_art_inline_hook_targets.clear(); + g_preserved_art_pages.clear(); + g_art_invalidation_enabled = false; + LOGW("libart.so executable-byte invalidation failed; ART inline-hook invalidation mode " + "was not fully applied in this process."); + return false; + } + + g_art_inline_hook_targets.clear(); + g_preserved_art_pages.clear(); + g_art_invalidation_completed = true; + g_art_invalidation_enabled = false; + + if (result.modified_pages == 0) { + LOGI("libart.so executable segments already match the backing file ({} segment(s) checked).", + result.executable_segments); + } else { + LOGI("Invalidated libart.so executable pages from backing file: {} modified page(s), {} " + "file-backed byte(s) restored across {} executable segment(s).", + result.modified_pages, result.restored_bytes, result.executable_segments); + } + return true; +} + +} // namespace vector::native diff --git a/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl b/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl index 19319ba12..a33936b65 100644 --- a/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl +++ b/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl @@ -73,7 +73,7 @@ interface IManagerService { * transaction ids follow declaration order, this number is the only thing standing between a * mismatched pair and a call that lands on the wrong method.

*/ - const int PROTOCOL_VERSION = 1; + const int PROTOCOL_VERSION = 2; /** * Which generation of this interface the daemon implements, never below 1. @@ -406,6 +406,30 @@ interface IManagerService { */ void setVerboseLogEnabled(boolean enabled); + // ---- ART inline hook compatibility mode ----------------------------------------------------- + + /** + * Every package opted into ART inline-hook invalidation, sorted. + * + *

Also names {@code system} when the system UI (whose process is {@code system:ui}) is on + * the list, and names nothing else synthetic: the configured set is returned verbatim. The + * empty set is the ordinary answer on a device where nobody has touched the setting.

+ */ + List getInvalidateArtInlineHookPackages(); + + /** + * Sets whether a package invalidates Vector's native ART inline hooks after injection. + * + *

Opting in makes a process restore libart.so's file-backed executable image and its own + * pre-injection modifications after the framework bootstrap, leaving LSPlant/Dobby metadata + * intact. It is a compatibility operation for apps whose protection rejects the temporary + * patches; the framework's own maintenance hooks no longer run afterwards.

+ * + * @return whether the daemon stored it, which is not whether the call arrived. False means the + * package name was blank - nothing else is refused + */ + boolean setInvalidateArtInlineHooks(String packageName, boolean enabled); + // ---- logs ------------------------------------------------------------------------------------- /** diff --git a/zygisk/src/main/cpp/include/ipc_bridge.h b/zygisk/src/main/cpp/include/ipc_bridge.h index 748840671..6eaaee17e 100644 --- a/zygisk/src/main/cpp/include/ipc_bridge.h +++ b/zygisk/src/main/cpp/include/ipc_bridge.h @@ -81,6 +81,12 @@ class IPCBridge { */ std::map FetchObfuscationMap(JNIEnv *env, jobject binder); + /** + * @brief Queries whether this registered application process should invalidate Vector's + * native ART inline hooks after framework initialization. + */ + bool ShouldInvalidateArtInlineHooks(JNIEnv *env, jobject binder); + /** * @brief Sets up the JNI hook to intercept Binder transactions. * diff --git a/zygisk/src/main/cpp/ipc_bridge.cpp b/zygisk/src/main/cpp/ipc_bridge.cpp index bc19adfd8..0135d6497 100644 --- a/zygisk/src/main/cpp/ipc_bridge.cpp +++ b/zygisk/src/main/cpp/ipc_bridge.cpp @@ -89,6 +89,8 @@ constexpr auto kBridgeServiceName = "activity"sv; constexpr jint kBridgeTransactionCode = ('_' << 24) | ('V' << 16) | ('E' << 8) | 'C'; constexpr jint kDexTransactionCode = ('_' << 24) | ('D' << 16) | ('E' << 8) | 'X'; constexpr jint kObfuscationMapTransactionCode = ('_' << 24) | ('O' << 16) | ('B' << 8) | 'F'; +constexpr jint kInvalidateArtInlineHooksTransactionCode = + ('_' << 24) | ('I' << 16) | ('N' << 8) | 'L'; // Action codes sent within a kBridgeTransactionCode transaction. constexpr jint kActionGetBinder = 2; @@ -453,6 +455,30 @@ std::map IPCBridge::FetchObfuscationMap(JNIEnv *env, j return result_map; } +bool IPCBridge::ShouldInvalidateArtInlineHooks(JNIEnv *env, jobject binder) { + if (!initialized_ || !binder) { + return false; + } + + ParcelWrapper parcels(env, this); + bool success = lsplant::JNI_CallBooleanMethod( + env, binder, transact_method_, kInvalidateArtInlineHooksTransactionCode, parcels.data.get(), + parcels.reply.get(), 0); + if (!success) { + LOGW("ART inline hook invalidation policy query failed."); + return false; + } + + lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); + if (env->ExceptionCheck()) { + LOGW("Remote exception while querying ART inline hook invalidation policy."); + env->ExceptionClear(); + return false; + } + + return lsplant::JNI_CallIntMethod(env, parcels.reply.get(), read_int_method_) != 0; +} + jboolean IPCBridge::ExecTransact_Replace(jboolean *res, JNIEnv *env, jobject obj, va_list args) { va_list copy; va_copy(copy, args); diff --git a/zygisk/src/main/cpp/module.cpp b/zygisk/src/main/cpp/module.cpp index 84ff88312..25d45e1fc 100644 --- a/zygisk/src/main/cpp/module.cpp +++ b/zygisk/src/main/cpp/module.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -111,17 +112,39 @@ class VectorModule : public zygisk::ModuleBase, public vector::native::Context { */ void SetAllowUnload(bool unload); + /** + * @brief Creates LSPlant configuration while recording every native ART inline hook that + * LSPlant successfully installs in this process. + */ + lsplant::InitInfo MakeArtHookInitInfo(); + zygisk::Api *api_ = nullptr; JNIEnv *env_ = nullptr; - // --- ART Hooker Configuration --- - const lsplant::InitInfo init_info_{ + // State managed within the class instance for each forked process. + bool should_inject_ = false; + bool is_manager_app_ = false; +}; + +// ========================================================================================= +// Implementation of VectorModule +// ========================================================================================= + +lsplant::InitInfo VectorModule::MakeArtHookInitInfo() { + return lsplant::InitInfo{ .inline_hooker = - [](auto target, auto replace) { + [](auto target, auto replace) -> void * { void *backup = nullptr; - return HookInline(target, replace, &backup) == 0 ? backup : nullptr; + if (HookInline(target, replace, &backup) != 0) return nullptr; + RecordArtInlineHookInvalidationTarget(target); + return backup; + }, + .inline_unhooker = + [](auto target) { + if (UnhookInline(target) != 0) return false; + ForgetArtInlineHookInvalidationTarget(target); + return true; }, - .inline_unhooker = [](auto target) { return UnhookInline(target) == 0; }, .art_symbol_resolver = [](auto symbol) { return ElfSymbolCache::GetArt()->getSymbAddress(symbol); }, .art_symbol_prefix_resolver = @@ -129,15 +152,7 @@ class VectorModule : public zygisk::ModuleBase, public vector::native::Context { .generated_class_name = "Vector_", .generated_source_name = "Dobby", }; - - // State managed within the class instance for each forked process. - bool should_inject_ = false; - bool is_manager_app_ = false; -}; - -// ========================================================================================= -// Implementation of VectorModule -// ========================================================================================= +} void VectorModule::LoadDex(JNIEnv *env, PreloadedDex &&dex) { LOGV("Loading framework DEX into memory (size: {}).", dex.size()); @@ -347,14 +362,29 @@ void VectorModule::postAppSpecialize(const zygisk::AppSpecializeArgs *args) { auto obfs_map = ipc_bridge.FetchObfuscationMap(env_, binder.get()); ConfigBridge::GetInstance()->obfuscation_map(std::move(obfs_map)); + const bool invalidate_art_inline_hooks_requested = + !is_manager_app_ && ipc_bridge.ShouldInvalidateArtInlineHooks(env_, binder.get()); + const bool invalidate_art_inline_hooks = + ConfigureArtInlineHookInvalidation(invalidate_art_inline_hooks_requested); + if (invalidate_art_inline_hooks) { + LOGI("ART inline hook invalidation mode enabled for '{}'; invalidation will run " + "immediately after framework bootstrap.", + nice_name_str.get()); + } else if (invalidate_art_inline_hooks_requested) { + LOGW("ART inline hook invalidation mode could not be armed for '{}'.", + nice_name_str.get()); + } + { PreloadedDex dex(dex_fd, dex_size); this->LoadDex(env_, std::move(dex)); } close(dex_fd); // The FD is duplicated by mmap, we can close it now. - // Initialize ART hooks via the native library. - this->InitArtHooker(env_, init_info_); + // Initialize ART hooks via the native library. The compatibility path records this handler's + // libart.so targets so their executable pages can be restored after framework bootstrap. + auto art_hook_init_info = MakeArtHookInitInfo(); + this->InitArtHooker(env_, art_hook_init_info); // Initialize JNI hooks via the native library. this->InitHooks(env_); // Find the Java entrypoint. @@ -365,6 +395,14 @@ void VectorModule::postAppSpecialize(const zygisk::AppSpecializeArgs *args) { env_, "forkCommon", "(ZZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V", JNI_FALSE, JNI_FALSE, args->nice_name, args->app_data_dir, binder.get(), is_manager_app_); + // Run this before LoadedApk creates the application's class loader and before app protection + // libraries can observe or derive state from the temporary LSPlant/Dobby patches. forkCommon + // has already installed Vector's Java lifecycle hooks, so no later package-ready callback is + // required merely to bootstrap the framework. + if (invalidate_art_inline_hooks && !InvalidateArtInlineHooksIfEnabled()) { + LOGW("Early ART inline-hook invalidation failed in '{}'.", nice_name_str.get()); + } + if (entered) { LOGV("Injected Vector framework into '{}'.", nice_name_str.get()); } else { @@ -450,7 +488,11 @@ void VectorModule::postServerSpecialize(const zygisk::ServerSpecializeArgs *args ipc_bridge.HookBridge(env_); - this->InitArtHooker(env_, init_info_); + // system_server intentionally keeps the full LSPlant ART maintenance hooks. This preserves the + // existing soft-restart and late-reinjection recovery path. + (void)ConfigureArtInlineHookInvalidation(false); + auto art_hook_init_info = MakeArtHookInitInfo(); + this->InitArtHooker(env_, art_hook_init_info); this->InitHooks(env_); this->SetupEntryClass(env_);