From 4421e6d1e2ca8bc400ce62f42a504fdb9a0a412f Mon Sep 17 00:00:00 2001 From: bartasc4 <210133930+bartasc4@users.noreply.github.com> Date: Sat, 17 Oct 2026 10:35:49 +0000 Subject: [PATCH] Add a central app list for ART inline-hook compatibility Carry forward the cleanup and process-policy implementation from JingMatrix/Vector#921 by lzup333 and expose the application-wide setting through the existing local central selector. Keep the feature opt-in, preserve the native cleanup implementation, and include only the associated Manager, daemon and IPC changes. --- .../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 + .../manager/ui/screens/home/HomeViewModel.kt | 175 +++++- .../home/InlineHookInvalidationPicker.kt | 250 +++++++++ .../ui/screens/home/SystemStatusScreen.kt | 38 +- 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 ++- 34 files changed, 1338 insertions(+), 20 deletions(-) create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/data/InlineHookProcessPolicy.kt create mode 100644 manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/InlineHookInvalidationPicker.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/screens/home/HomeViewModel.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt index 725ff534d..fd403ba3e 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt @@ -3,6 +3,7 @@ import kotlinx.coroutines.CancellationException import org.matrix.vector.manager.data.repository.FrameworkUpdateState import org.matrix.vector.manager.data.repository.LaunchShortcut import org.matrix.vector.manager.data.repository.ManagerInstallStep +import android.content.pm.ApplicationInfo import android.os.Build import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -21,6 +22,7 @@ import kotlinx.coroutines.flow.update import kotlin.random.Random import kotlinx.coroutines.launch import org.matrix.vector.ipc.IManagerService +import org.matrix.vector.manager.BuildConfig import org.matrix.vector.manager.data.github.CommunityFeed import org.matrix.vector.manager.data.github.GitHubRepository import org.matrix.vector.manager.data.model.ManagerCopy @@ -30,6 +32,8 @@ import org.matrix.vector.manager.logE import org.matrix.vector.manager.logW import org.matrix.vector.manager.ui.components.FrameworkState +private const val SYSTEM_UI_VIRTUAL_PACKAGE = "system" + /** A specific reason the framework is degraded, so the UI never has to say merely "something". */ enum class HealthIssue { SepolicyNotLoaded, @@ -167,6 +171,24 @@ data class DeviceInfo( val abi: String = Build.SUPPORTED_ABIS.firstOrNull() ?: "", ) +/** One package in the global ART inline-hook compatibility selector. */ +data class InlineHookAppOption( + val packageName: String, + val appName: String, + val applicationInfo: ApplicationInfo?, +) + +/** Everything the selector needs, kept together so a refresh cannot expose a half-new list. */ +data class InlineHookInvalidationState( + val apps: List = emptyList(), + val selectedPackages: Set = emptySet(), + val savingPackages: Set = emptySet(), + val loading: Boolean = false, + val loaded: Boolean = false, + val loadFailed: Boolean = false, + val saveErrorPackage: String? = null, +) + class HomeViewModel( private val daemon: DaemonClient, private val github: GitHubRepository, @@ -215,6 +237,10 @@ class HomeViewModel( private val _hiddenIcon = MutableStateFlow(true) val hiddenIcon: StateFlow = _hiddenIcon.asStateFlow() + private val _inlineHookInvalidation = MutableStateFlow(InlineHookInvalidationState()) + val inlineHookInvalidation: StateFlow = + _inlineHookInvalidation.asStateFlow() + /** * How this manager can be opened, and how it currently is. * @@ -344,7 +370,10 @@ class HomeViewModel( // such moment: `refreshPresence` asks again whenever a screen that shows them // is opened, since this flow does not emit a second time while one binder // stays alive. - if (service != null) refreshToggles() + if (service != null) { + refreshToggles() + refreshInlineHookInvalidation() + } } } // Returning to Home is not a reason to talk to GitHub. The page renders from disk every @@ -585,6 +614,150 @@ class HomeViewModel( } } + /** + * Rebuilds the one package-wide compatibility list shared by every module. + * + * Installed copies from multiple Android users collapse to one row because the daemon stores + * this setting by package name, not by module or user. The synthetic `system` row follows the + * daemon's own convention and targets `system:ui`; system_server itself is deliberately never + * eligible for executable-byte invalidation. + */ + fun refreshInlineHookInvalidation() { + val flow = _inlineHookInvalidation + val current = flow.value + if (!daemon.isAlive || current.loading || current.savingPackages.isNotEmpty()) return + + flow.update { + it.copy(loading = true, loadFailed = false, saveErrorPackage = null) + } + viewModelScope.launch { + val selectedResult = daemon.getInvalidateArtInlineHookPackages() + val selected = selectedResult.getOrNull()?.toSet() + if (selected == null) { + selectedResult.exceptionOrNull()?.let { + logW("framework: process compatibility list unavailable", it) + } + flow.update { + it.copy(loading = false, loaded = false, loadFailed = true) + } + return@launch + } + + val installed = runCatching { + ServiceLocator.apps.getInstalledApps() + } + .onFailure { logW("framework: installed apps unavailable", it) } + .getOrDefault(emptyList()) + + // Lowest user id wins only as the representative icon and label. The choice itself is + // package-wide, and PreferenceStore resolves it against whichever user's process is + // being specialized. + val byPackage = linkedMapOf() + installed + .sortedBy { it.userId } + .forEach { app -> + byPackage.putIfAbsent(app.packageName, app) + } + val android = byPackage.remove("android") + byPackage.remove(SYSTEM_UI_VIRTUAL_PACKAGE) + byPackage.remove(BuildConfig.MANAGER_PACKAGE_NAME) + + val options = buildList { + add( + InlineHookAppOption( + packageName = SYSTEM_UI_VIRTUAL_PACKAGE, + appName = android?.appName.orEmpty(), + applicationInfo = android?.applicationInfo, + ) + ) + byPackage.values.forEach { app -> + add( + InlineHookAppOption( + packageName = app.packageName, + appName = app.appName, + applicationInfo = app.applicationInfo, + ) + ) + } + + // Keep an uninstalled but configured package visible so it can still be unticked. + // Otherwise its preference would survive forever with no UI capable of removing + // it after the package disappeared. + val visible = asSequence().map { it.packageName }.toSet() + selected + .filterNot { it in visible } + .forEach { packageName -> + add( + InlineHookAppOption( + packageName = packageName, + appName = packageName, + applicationInfo = null, + ) + ) + } + } + .sortedWith( + compareBy { it.appName.lowercase() } + .thenBy { it.packageName } + ) + + flow.value = + InlineHookInvalidationState( + apps = options, + selectedPackages = selected, + loaded = true, + ) + } + } + + /** Persists one checkbox immediately; a rejected write leaves the old selection intact. */ + fun setInvalidateArtInlineHooks(packageName: String, enabled: Boolean) { + val flow = _inlineHookInvalidation + val current = flow.value + if ( + !current.loaded || + packageName in current.savingPackages || + (packageName in current.selectedPackages) == enabled + ) { + return + } + + flow.update { + it.copy( + savingPackages = it.savingPackages + packageName, + saveErrorPackage = null, + ) + } + viewModelScope.launch { + val result = daemon.setInvalidateArtInlineHooks(packageName, enabled) + val stored = result.getOrDefault(false) + result.exceptionOrNull()?.let { + logE( + "framework: setting process compatibility for $packageName failed", + it, + ) + } + if (!stored && result.exceptionOrNull() == null) { + logW("framework: daemon refused process compatibility for $packageName") + } + + flow.update { state -> + state.copy( + selectedPackages = + if (!stored) state.selectedPackages + else if (enabled) state.selectedPackages + packageName + else state.selectedPackages - packageName, + savingPackages = state.savingPackages - packageName, + saveErrorPackage = if (stored) null else packageName, + ) + } + } + } + + fun clearInlineHookSaveError() { + _inlineHookInvalidation.update { it.copy(saveErrorPackage = null) } + } + fun refreshFeed(freshness: GitHubRepository.Freshness) { // Claimed before the coroutine starts rather than inside it. A pull-to-refresh that lands // while init's own load is still running would otherwise fetch the same window twice, and diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/InlineHookInvalidationPicker.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/InlineHookInvalidationPicker.kt new file mode 100644 index 000000000..e214c483d --- /dev/null +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/InlineHookInvalidationPicker.kt @@ -0,0 +1,250 @@ +package org.matrix.vector.manager.ui.screens.home + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.selection.toggleable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Apps +import androidx.compose.material.icons.rounded.ChevronRight +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetValue +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import org.matrix.vector.manager.R +import org.matrix.vector.ui.AppIcon +import org.matrix.vector.ui.SearchField +import org.matrix.vector.ui.theme.Mono + +/** The global selector entry placed immediately above the status-notification switch. */ +@Composable +fun InlineHookInvalidationRow( + state: InlineHookInvalidationState, + enabled: Boolean, + onClick: () -> Unit, + titleRes: Int = R.string.action_invalidate_art_inline_hooks, + summaryRes: Int = R.string.action_invalidate_art_inline_hooks_summary, +) { + val colors = MaterialTheme.colorScheme + Row( + modifier = + Modifier.fillMaxWidth() + .clickable( + enabled = enabled, + role = Role.Button, + onClick = onClick, + ) + .padding(vertical = 10.dp) + .alpha(if (enabled) 1f else 0.38f), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(Modifier.weight(1f)) { + Text( + stringResource(titleRes), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + stringResource(summaryRes), + style = MaterialTheme.typography.bodySmall, + color = colors.onSurfaceVariant, + ) + if (state.loadFailed) { + Text( + stringResource(R.string.modules_no_daemon), + style = MaterialTheme.typography.labelMedium, + color = colors.error, + ) + } + } + Spacer(Modifier.width(12.dp)) + if (state.loading) { + CircularProgressIndicator(modifier = Modifier.size(22.dp), strokeWidth = 2.dp) + } else { + Icon( + Icons.Rounded.ChevronRight, + contentDescription = stringResource(R.string.scope_search_hint), + tint = colors.onSurfaceVariant, + ) + } + } +} + +/** A searchable, package-wide list. Every checkbox is persisted immediately through the daemon. */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun InlineHookInvalidationPicker( + state: InlineHookInvalidationState, + onDismiss: () -> Unit, + onRetry: () -> Unit, + onToggle: (String, Boolean) -> Unit, + titleRes: Int = R.string.action_invalidate_art_inline_hooks, + summaryRes: Int = R.string.action_invalidate_art_inline_hooks_summary, +) { + val sheetState = rememberBottomSheetState(initialValue = SheetValue.Hidden) + var query by rememberSaveable { mutableStateOf("") } + val needle = query.trim().lowercase() + val visibleApps = + remember(state.apps, needle) { + if (needle.isEmpty()) { + state.apps + } else { + state.apps.filter { app -> + val label = app.appName.ifBlank { app.packageName } + label.lowercase().contains(needle) || + app.packageName.lowercase().contains(needle) + } + } + } + + ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(0.9f)) { + Column(modifier = Modifier.padding(horizontal = 20.dp)) { + Text( + stringResource(titleRes), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + ) + Text( + stringResource(summaryRes), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 4.dp, bottom = 12.dp), + ) + SearchField( + query = query, + onQueryChange = { query = it }, + placeholder = stringResource(R.string.scope_search_hint), + clearContentDescription = stringResource(R.string.modules_clear_search), + ) + Text( + "(${state.selectedPackages.size})", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(vertical = 12.dp), + ) + } + HorizontalDivider() + + when { + state.loading && !state.loaded -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + + state.loadFailed -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + stringResource(R.string.modules_no_daemon), + color = MaterialTheme.colorScheme.error, + ) + TextButton(onClick = onRetry) { + Text(stringResource(R.string.retry)) + } + } + } + } + + visibleApps.isEmpty() -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + stringResource(R.string.scope_no_match), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + + else -> { + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(visibleApps, key = { it.packageName }) { app -> + val checked = app.packageName in state.selectedPackages + val saving = app.packageName in state.savingPackages + val label = app.appName.ifBlank { app.packageName } + ListItem( + modifier = + Modifier.toggleable( + value = checked, + enabled = !saving, + role = Role.Checkbox, + onValueChange = { onToggle(app.packageName, it) }, + ) + .alpha(if (saving) 0.6f else 1f), + supportingContent = { + Text(app.packageName, style = Mono) + }, + leadingContent = { + val info = app.applicationInfo + if (info != null) { + AppIcon( + applicationInfo = info, + contentDescription = null, + size = 36.dp, + ) + } else { + Icon( + Icons.Rounded.Apps, + contentDescription = null, + modifier = Modifier.size(36.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + }, + trailingContent = { + if (saving) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 2.dp, + ) + } else { + Checkbox( + checked = checked, + onCheckedChange = null, + ) + } + }, + colors = + ListItemDefaults.colors(containerColor = Color.Transparent), + ) { + Text(label) + } + } + } + } + } + } + } +} diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt index f63f0dd9c..6e94ce6d6 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt @@ -39,6 +39,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.remember import androidx.compose.runtime.getValue +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -97,13 +98,18 @@ fun SystemStatusScreen( val context = LocalContext.current val statusNotification by viewModel.statusNotification.collectAsStateWithLifecycle() val hiddenIcon by viewModel.hiddenIcon.collectAsStateWithLifecycle() + val inlineHookInvalidation by viewModel.inlineHookInvalidation.collectAsStateWithLifecycle() + var inlineHookPickerOpen by rememberSaveable { mutableStateOf(false) } val presence by viewModel.presence.collectAsStateWithLifecycle() val managerInstall by viewModel.managerInstall.collectAsStateWithLifecycle() // Both the shortcut and the install can be undone from outside the app while it is open — // dragged off the home screen, uninstalled from Settings — so what the rows offer is re-read on // arrival rather than trusted from whenever the ViewModel was built. - LaunchedEffect(Unit) { viewModel.refreshPresence() } + LaunchedEffect(Unit) { + viewModel.refreshPresence() + viewModel.refreshInlineHookInvalidation() + } val sections = buildSections(status, device, context) // The same page again, in English, for the clipboard. @@ -134,6 +140,17 @@ fun SystemStatusScreen( val shortcutRefused = stringResource(R.string.launcher_shortcut_refused) val installDone = stringResource(R.string.launcher_install_done) + LaunchedEffect(inlineHookInvalidation.saveErrorPackage) { + val packageName = inlineHookInvalidation.saveErrorPackage ?: return@LaunchedEffect + val option = inlineHookInvalidation.apps.firstOrNull { it.packageName == packageName } + val label = option?.appName?.takeIf { it.isNotBlank() } ?: packageName + snackbars.show( + context.getString(R.string.action_invalidate_art_inline_hooks_failed, label), + SnackbarTone.Failure, + ) + viewModel.clearInlineHookSaveError() + } + Scaffold( snackbarHost = { SharedSnackbarHost(snackbars) }, topBar = { @@ -210,6 +227,16 @@ fun SystemStatusScreen( HorizontalDivider() Spacer(Modifier.height(4.dp)) } + item { + InlineHookInvalidationRow( + state = inlineHookInvalidation, + enabled = daemonAlive, + onClick = { + inlineHookPickerOpen = true + viewModel.refreshInlineHookInvalidation() + }, + ) + } item { FrameworkToggle( title = stringResource(R.string.status_notification), @@ -254,6 +281,15 @@ fun SystemStatusScreen( } } + if (inlineHookPickerOpen) { + InlineHookInvalidationPicker( + state = inlineHookInvalidation, + onDismiss = { inlineHookPickerOpen = false }, + onRetry = viewModel::refreshInlineHookInvalidation, + onToggle = viewModel::setInvalidateArtInlineHooks, + ) + } + // Success only. A failure stays on the card, where it can still be read by someone who was not // looking at this screen when it happened — which is the common case, since the install runs // while they are free to go elsewhere. Acknowledged first, and shown on the screen's own scope diff --git a/manager/src/main/res/values-ar/strings.xml b/manager/src/main/res/values-ar/strings.xml index 932f431a3..6014c10e5 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..3116a08a9 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 + Invalidiert Vectors ART-Inline-Hooks global in jeder ausgewählten App, unabhängig davon, wie viele Module oder Hooks auf sie zielen. Einige Module funktionieren möglicherweise nicht mehr, und Apps können sich fehlerhaft verhalten oder abstürzen. Wirkt beim nächsten Start der jeweiligen 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..41ceab4f6 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 + Invalida globalmente los hooks inline de ART de Vector en cada app seleccionada, independientemente de cuántos módulos o hooks la utilicen. Algunos módulos pueden dejar de funcionar, y las apps pueden comportarse mal o bloquearse. Se aplica la próxima vez que se inicie cada 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..921709885 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..b22dcf5f5 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 + Invalide globalement les hooks inline ART de Vector dans chaque application sélectionnée, quel que soit le nombre de modules ou de hooks qui la ciblent. Certains modules peuvent cesser de fonctionner et les applications peuvent mal fonctionner ou planter. S\'applique au prochain démarrage de chaque 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..35089d7fc 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 + Membatalkan hook inline ART Vector secara global di setiap aplikasi yang dipilih, berapa pun jumlah modul atau hook yang menargetkannya. Beberapa modul mungkin berhenti bekerja, dan aplikasi dapat bermasalah atau mogok. Berlaku saat setiap 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..5a827c1d5 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 + Invalida globalmente gli hook inline ART di Vector in ogni app selezionata, indipendentemente dal numero di moduli o hook che la interessano. Alcuni moduli potrebbero smettere di funzionare e le app potrebbero comportarsi in modo anomalo o bloccarsi. Ha effetto al successivo avvio di ogni 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..9f9175a97 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 שמכוונים אליה. חלק מהמודולים עשויים להפסיק לעבוד, ואפליקציות עלולות לפעול באופן שגוי או לקרוס. השינוי חל בהפעלה הבאה של כל אפליקציה. + תאימות ה-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..b473183da 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..2627a0daf 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..383b6cff3 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 + Globalnie unieważnia hooki inline ART Vectora w każdej wybranej aplikacji, niezależnie od liczby modułów i zastosowanych hooków. Niektóre moduły mogą przestać działać, a aplikacje mogą działać nieprawidłowo lub ulegać awarii. Zmiany zaczną działać przy następnym uruchomieniu każdej 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..542dfe133 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 + Invalida globalmente os hooks inline do ART do Vector em cada app selecionado, independentemente de quantos módulos ou hooks o atinjam. Alguns módulos podem parar de funcionar e os apps podem apresentar falhas ou travar. Aplica-se na próxima inicialização de cada app. + 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..6c7e54cad 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..9b73a4585 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 + Vector\'ın ART satır içi kancalarını, kaç modül veya kanca hedeflerse hedeflesin, seçilen her uygulamada genel olarak geçersiz kılar. Bazı modüller çalışmayı durdurabilir ve uygulamalar arızalanabilir veya çökebilir. Her 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..398a8dc75 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..687cee4d3 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 toàn cục hook nội tuyến ART của Vector trong từng ứng dụng đã chọn, bất kể có bao nhiêu mô-đun hoặc hook nhắm tới ứng dụng đó. Một số mô-đun có thể ngừng hoạt động và ứng dụng có thể hoạt động sai hoặc bị treo. Có hiệu lực vào lần khởi động tiếp theo của từng ứng dụng. + Đã 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..00bbe1fdb 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 失效,不受作用于该应用的模块或 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..be17d53f4 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 失效,不受作用於該應用程式的模組或 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..362ef8039 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 + Invalidate Vector\'s ART inline hooks globally in each selected app, regardless of how many modules or hooks target it. Some modules may stop working, and apps may misbehave or crash. Applies the next time each 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_);