Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<String>,
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<String>
): Boolean =
expectedUid == actualUid &&
(processName == applicationProcessName || processName in componentProcesses)

fun mayInvalidate(processName: String, uid: Int): Boolean =
uid != Process.SYSTEM_UID || processName != "system"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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<String> {
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())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`.
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,12 @@ object ManagerService : IManagerService.Stub() {
if (isVerboseLogEnabled()) LogcatMonitor.startVerbose() else LogcatMonitor.stopVerbose()
}

override fun getInvalidateArtInlineHookPackages(): MutableList<String> =
PreferenceStore.getInvalidateArtInlineHookPackages().sorted().toMutableList()

override fun setInvalidateArtInlineHooks(packageName: String, enabled: Boolean): Boolean =
PreferenceStore.setInvalidateArtInlineHooks(packageName, enabled)

override fun getLogParts(verbose: Boolean): List<String> = FileSystem.listLogParts(verbose)

override fun getLogPart(verbose: Boolean, name: String): ParcelFileDescriptor? =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,12 @@ class FakeManagerService(
real?.setVerboseLogEnabled(enabled)
}

override fun getInvalidateArtInlineHookPackages(): MutableList<String> =
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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,25 @@ class DaemonClient(private val serviceState: StateFlow<IManagerService?>) {
suspend fun setVerboseLogEnabled(enabled: Boolean): Result<Unit> = 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<List<String>> = 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<Boolean> =
runIpc { it.setInvalidateArtInlineHooks(packageName, enabled) }

/**
* The rotated parts the daemon still holds for one of the two logs, oldest first.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ import org.matrix.vector.manager.ui.navigation.StoreDetail
import org.matrix.vector.manager.ui.navigation.CrashTrace
import org.matrix.vector.manager.ui.navigation.LogTrace
import org.matrix.vector.manager.ui.navigation.SystemStatus
import org.matrix.vector.manager.ui.navigation.InvalidateArtInlineHooks
import org.matrix.vector.manager.ui.navigation.Web
import org.matrix.vector.manager.ui.navigation.TopLevelRoute
import org.matrix.vector.manager.ui.navigation.rememberNavigator
import org.matrix.vector.manager.ui.screens.home.HomeScreen
import org.matrix.vector.manager.ui.screens.home.CrashTraceScreen
import org.matrix.vector.manager.ui.screens.home.SystemStatusScreen
import org.matrix.vector.manager.ui.screens.home.InvalidateArtInlineHooksScreen
import org.matrix.vector.ui.logs.LogTraceScreen
import org.matrix.vector.manager.data.repository.VectorLogSource
import org.matrix.vector.manager.ui.theme.LocalizedOverlay
Expand Down Expand Up @@ -250,8 +252,12 @@ private fun EntryProviderScope<NavKey>.registerRoutes(navigator: Navigator) {
SystemStatusScreen(
onNavigateBack = { navigator.back() },
onOpenCrash = { navigator.go(CrashTrace) },
onOpenArtInlineHooks = { navigator.go(InvalidateArtInlineHooks) },
)
}
entry<InvalidateArtInlineHooks> {
InvalidateArtInlineHooksScreen(onNavigateBack = { navigator.back() })
}
entry<CrashTrace> { CrashTraceScreen(onNavigateBack = { navigator.back() }) }
entry<LogTrace> { route ->
LogTraceScreen(text = route.text, onNavigateBack = { navigator.back() })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ sealed interface TopLevelRoute : Route {

@Serializable data object SystemStatus : Route

/**
* The per-app ART inline hook compatibility picker.
*
* Read from the system status screen, next to the other framework behaviour toggles.
*/
@Serializable data object InvalidateArtInlineHooks : Route

/**
* The newest recorded crash, frame by frame.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package org.matrix.vector.manager.ui.screens.home

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Android
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.launch
import org.matrix.vector.manager.R
import org.matrix.vector.ui.PackageRow
import org.matrix.vector.ui.SearchField
import org.matrix.vector.manager.ui.components.SnackbarTone
import org.matrix.vector.manager.ui.components.VectorSnackbarHost
import org.matrix.vector.manager.ui.components.show

/**
* The per-app picker behind the "ART inline hook compatibility mode" row.
*
* Every installed app with a switch, plus the Android system UI at the top. Each switch records the
* package in the daemon's preference store; nothing takes effect until that app's process next
* starts, which is said at the top of the list rather than left to be discovered.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InvalidateArtInlineHooksScreen(
onNavigateBack: () -> Unit,
viewModel: InlineHookViewModel = viewModel(factory = InlineHookLocator.factory()),
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val query by viewModel.searchQuery.collectAsStateWithLifecycle()
val message by viewModel.message.collectAsStateWithLifecycle()
val snackbars = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
val saveFailed = stringResource(R.string.invalidate_art_inline_hooks_save_failed)

LaunchedEffect(message) {
if (message != null) {
scope.launch { snackbars.show(saveFailed, SnackbarTone.Failure) }
viewModel.consumeMessage()
}
}

Scaffold(
snackbarHost = { VectorSnackbarHost(snackbars) },
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.invalidate_art_inline_hooks)) },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = stringResource(R.string.back),
)
}
},
)
},
) { padding ->
LazyColumn(
modifier = Modifier.padding(padding),
contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 8.dp, bottom = 24.dp),
) {
item {
Text(
stringResource(R.string.invalidate_art_inline_hooks_summary),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.height(12.dp))
}
item {
SearchField(
query = query,
onQueryChange = { viewModel.searchQuery.value = it },
placeholder = stringResource(R.string.invalidate_art_inline_hooks_search_hint),
)
Spacer(Modifier.height(4.dp))
}
if (state.loading) {
item { CircularProgressIndicator() }
} else {
items(state.rows, key = { it.packageName }) { row ->
PackageRow(
icon = {
Icon(
imageVector = rowIcon(row.isSystemUi),
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
},
label =
if (row.isSystemUi) {
stringResource(R.string.invalidate_art_inline_hooks_system_ui)
} else {
row.label
},
packageName = row.appName,
trailing = {
Switch(
checked = row.enabled,
onCheckedChange = { enabled -> viewModel.setEnabled(row, enabled) },
)
},
)
}
}
}
}
}

private fun rowIcon(isSystemUi: Boolean): ImageVector = Icons.Rounded.Android
Loading