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
Expand Up @@ -187,6 +187,7 @@ object ConfigCache {
packageName = pkgName
this.apkPath = apkPath
appId = appInfo.uid
versionCode = pkgInfo.longVersionCode
applicationInfo = appInfo
service = oldModule?.service ?: InjectedModuleService(pkgName)
file = preLoadedApk
Expand Down Expand Up @@ -340,6 +341,8 @@ object ConfigCache {
fun getModuleByUid(uid: Int): Module? =
state.modules.values.firstOrNull { it.appId == uid % PER_USER_RANGE }

fun getModuleByPackage(packageName: String): Module? = state.modules[packageName]

fun getModulesForSystemServer(): List<Module> {
val modules = mutableListOf<Module>()
if (!android.os.SELinux.checkSELinuxAccess(
Expand Down Expand Up @@ -376,6 +379,7 @@ object ConfigCache {
packageName = pkgName
this.apkPath = apkPath
appId = runCatching { Os.stat(statPath).st_uid }.getOrDefault(-1)
versionCode = 0
service = InjectedModuleService(pkgName)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import android.os.ParcelFileDescriptor
import android.os.Process
import android.os.RemoteException
import android.util.Log
import io.github.libxposed.service.HookedProcess
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong
import org.lsposed.lspd.models.Module
import org.lsposed.lspd.service.ILSPApplicationService
import org.lsposed.lspd.service.IHotReloadTarget
import org.matrix.vector.daemon.data.ConfigCache
import org.matrix.vector.daemon.data.FileSystem
import org.matrix.vector.daemon.utils.InstallerVerifier
Expand All @@ -24,11 +27,19 @@ const val DEX_TRANSACTION_CODE =
const val OBFUSCATION_MAP_TRANSACTION_CODE =
('_'.code shl 24) or ('O'.code shl 16) or ('B'.code shl 8) or 'F'.code

internal class HotReloadInProgressException(message: String) : IllegalStateException(message)

internal class HotReloadProcessDiedException(message: String) : IllegalStateException(message)

internal class HotReloadUnsupportedException(message: String) : IllegalStateException(message)

object ApplicationService : ILSPApplicationService.Stub() {

data class ProcessKey(val uid: Int, val pid: Int)

private val processes = ConcurrentHashMap<ProcessKey, ProcessInfo>()
private val nextHotReloadTargetId = AtomicLong(1)
private val hotReloadTargets = ConcurrentHashMap<Long, HotReloadTargetInfo>()

private class ProcessInfo(val key: ProcessKey, val processName: String, val heartBeat: IBinder) :
IBinder.DeathRecipient {
Expand All @@ -40,6 +51,45 @@ object ApplicationService : ILSPApplicationService.Stub() {
override fun binderDied() {
heartBeat.unlinkToDeath(this, 0)
processes.remove(key)
hotReloadTargets.entries.removeIf { it.value.process === this }
}
}

private class HotReloadTargetInfo(
val id: Long,
val modulePackageName: String,
val process: ProcessInfo,
@Volatile var loadedVersionCode: Long,
val target: IHotReloadTarget
) : IBinder.DeathRecipient {
@Volatile var state: Int = HookedProcess.TARGET_STATE_UP_TO_DATE

init {
target.asBinder().linkToDeath(this, 0)
hotReloadTargets[id] = this
}

override fun binderDied() {
target.asBinder().unlinkToDeath(this, 0)
hotReloadTargets.remove(id)
}

fun toHookedProcess(currentVersionCode: Long): HookedProcess {
val effectiveState =
if (state == HookedProcess.TARGET_STATE_UP_TO_DATE &&
loadedVersionCode != currentVersionCode) {
HookedProcess.TARGET_STATE_STALE
} else {
state
}
return HookedProcess().apply {
targetId = id
uid = process.key.uid
pid = process.key.pid
processName = process.processName
state = effectiveState
loadedVersionCode = this@HotReloadTargetInfo.loadedVersionCode
}
}
}

Expand Down Expand Up @@ -129,4 +179,66 @@ object ApplicationService : ILSPApplicationService.Stub() {
.onFailure { Log.e(TAG, "Failed to open or verify manager APK", it) }
.getOrNull()
}

override fun registerHotReloadTarget(
modulePackageName: String,
loadedVersionCode: Long,
target: IHotReloadTarget
): Long {
val info = ensureRegistered()
val module =
ConfigCache.getModuleByPackage(modulePackageName)
?: throw RemoteException("Unknown module: $modulePackageName")
if (!getAllModules().any { it.packageName == module.packageName }) {
throw RemoteException("Module $modulePackageName is not active in ${info.processName}")
}

val existing =
hotReloadTargets.values.firstOrNull {
it.modulePackageName == modulePackageName &&
it.process.key == info.key &&
it.target.asBinder() == target.asBinder()
}
if (existing != null) {
existing.loadedVersionCode = loadedVersionCode
existing.state = HookedProcess.TARGET_STATE_UP_TO_DATE
return existing.id
}

val id = nextHotReloadTargetId.getAndIncrement()
HotReloadTargetInfo(id, module.packageName, info, loadedVersionCode, target)
return id
}

fun getRunningTargets(module: Module): List<HookedProcess> {
return hotReloadTargets.values
.filter { it.modulePackageName == module.packageName }
.map { it.toHookedProcess(module.versionCode) }
}

fun hotReloadTarget(targetId: Long, module: Module, extras: android.os.Bundle?) {
val target =
hotReloadTargets[targetId] ?: throw SecurityException("Invalid hot reload target: $targetId")
if (target.modulePackageName != module.packageName) {
throw SecurityException("Target $targetId does not belong to ${module.packageName}")
}
if (target.state == HookedProcess.TARGET_STATE_RELOADING) {
throw HotReloadInProgressException("Target $targetId is already reloading")
}

target.state = HookedProcess.TARGET_STATE_RELOADING
runCatching {
target.target.hotReloadModule(module, extras)
target.loadedVersionCode = module.versionCode
target.state = HookedProcess.TARGET_STATE_UP_TO_DATE
}
.onFailure {
if (!target.target.asBinder().isBinderAlive) {
hotReloadTargets.remove(target.id, target)
throw HotReloadProcessDiedException("Target process died before hot reload completed")
}
target.state = HookedProcess.TARGET_STATE_FAILED
throw it
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,25 @@ import android.os.ParcelFileDescriptor
import android.os.RemoteException
import android.util.Log
import io.github.libxposed.service.IXposedService
import io.github.libxposed.service.HookedProcess
import io.github.libxposed.service.IHotReloadCallback
import io.github.libxposed.service.IXposedScopeCallback
import java.io.Serializable
import java.util.concurrent.ConcurrentHashMap
import org.lsposed.lspd.service.ILSPInjectedModuleService
import org.lsposed.lspd.service.IRemotePreferenceCallback
import org.matrix.vector.daemon.data.ConfigCache
import org.matrix.vector.daemon.data.FileSystem
import org.matrix.vector.daemon.data.ModuleDatabase
import org.matrix.vector.daemon.data.PreferenceStore
import org.matrix.vector.daemon.system.NotificationManager
import org.matrix.vector.daemon.system.PER_USER_RANGE

private const val TAG = "VectorInjectedModuleService"

class InjectedModuleService(private val packageName: String) : ILSPInjectedModuleService.Stub() {

// Tracks active RemotePreferenceCallbacks linked by config group
// Preference instances in hooked processes subscribe here for daemon-side updates.
private val callbacks = ConcurrentHashMap<String, MutableSet<IRemotePreferenceCallback>>()

override fun getFrameworkProperties(): Long {
Expand All @@ -30,6 +35,55 @@ class InjectedModuleService(private val packageName: String) : ILSPInjectedModul
return prop
}

override fun getScope(): List<String> =
ConfigCache.getModuleScope(packageName)?.map { it.packageName } ?: emptyList()

override fun requestScope(packages: List<String>, callback: IXposedScopeCallback?) {
val userId = Binder.getCallingUid() / PER_USER_RANGE
if (PreferenceStore.isScopeRequestBlocked(packageName)) {
callback?.onScopeRequestFailed("Scope request blocked by user configuration")
return
}
packages.forEach { pkg ->
if (callback != null) NotificationManager.requestModuleScope(packageName, userId, pkg, callback)
}
}

override fun removeScope(packages: List<String>) {
val userId = Binder.getCallingUid() / PER_USER_RANGE
packages.forEach { pkg ->
runCatching { ModuleDatabase.removeModuleScope(packageName, pkg, userId) }
.getOrElse { throw RemoteException(it.message) }
}
}

override fun getRunningTargets(): List<HookedProcess> {
val module = ConfigCache.getModuleByPackage(packageName) ?: return emptyList()
return ApplicationService.getRunningTargets(module)
}

override fun hotReloadModule(targetId: Long, data: Bundle?, callback: IHotReloadCallback?) {
runCatching {
val module = ConfigCache.getModuleByPackage(packageName)
?: throw HotReloadUnsupportedException("Module $packageName is not enabled")
if (module.file.moduleClassNames.size != 1) {
throw HotReloadUnsupportedException("Hot reload requires exactly one Java entry class")
}
ApplicationService.hotReloadTarget(targetId, module, data)
callback?.onHotReloadResult(IXposedService.HOT_RELOAD_SUCCEEDED, null)
}
.onFailure { throwable ->
if (throwable is SecurityException) throw throwable
val status = when (throwable) {
is HotReloadInProgressException -> IXposedService.HOT_RELOAD_IN_PROGRESS
is HotReloadProcessDiedException -> IXposedService.HOT_RELOAD_PROCESS_DIED
is HotReloadUnsupportedException -> IXposedService.HOT_RELOAD_UNSUPPORTED
else -> IXposedService.HOT_RELOAD_FAILED
}
callback?.onHotReloadResult(status, throwable.message)
}
}

override fun requestRemotePreferences(
group: String,
callback: IRemotePreferenceCallback?
Expand All @@ -48,12 +102,41 @@ class InjectedModuleService(private val packageName: String) : ILSPInjectedModul
return bundle
}

@Suppress("DEPRECATION")
override fun updateRemotePreferences(group: String, diff: Bundle) {
val userId = Binder.getCallingUid() / PER_USER_RANGE
val values = mutableMapOf<String, Any?>()
if (diff.getBoolean("clear", false)) {
PreferenceStore.deleteModulePrefs(packageName, userId, group)
}
(diff.getSerializable("delete") as? Set<*>)?.forEach { key ->
if (key is String) values[key] = null
}
(diff.getSerializable("put") as? Map<*, *>)?.forEach { (key, value) ->
if (key is String) values[key] = value
}
runCatching {
PreferenceStore.updateModulePrefs(packageName, userId, group, values)
onUpdateRemotePreferences(group, diff)
}
.getOrElse { throw RemoteException(it.message) }
}

override fun deleteRemotePreferences(group: String) {
val userId = Binder.getCallingUid() / PER_USER_RANGE
runCatching { PreferenceStore.deleteModulePrefs(packageName, userId, group) }
.getOrElse { throw RemoteException(it.message) }
onUpdateRemotePreferences(group, Bundle().apply { putBoolean("clear", true) })
}

override fun openRemoteFile(path: String): ParcelFileDescriptor {
FileSystem.ensureModuleFilePath(path)
val userId = Binder.getCallingUid() / PER_USER_RANGE
return runCatching {
val dir = FileSystem.resolveModuleDir(packageName, "files", userId, -1)
ParcelFileDescriptor.open(dir.resolve(path).toFile(), ParcelFileDescriptor.MODE_READ_ONLY)
ParcelFileDescriptor.open(
dir.resolve(path).toFile(),
ParcelFileDescriptor.MODE_CREATE or ParcelFileDescriptor.MODE_READ_WRITE)
}
.getOrElse { throw RemoteException(it.message) }
}
Expand All @@ -67,7 +150,19 @@ class InjectedModuleService(private val packageName: String) : ILSPInjectedModul
.getOrElse { throw RemoteException(it.message) }
}

// Called by ModuleService when prefs are updated globally
override fun deleteRemoteFile(path: String): Boolean {
FileSystem.ensureModuleFilePath(path)
val userId = Binder.getCallingUid() / PER_USER_RANGE
return runCatching {
FileSystem.resolveModuleDir(packageName, "files", userId, -1)
.resolve(path)
.toFile()
.delete()
}
.getOrElse { throw RemoteException(it.message) }
}

// Keep hooked-process preference caches in sync with updates from the module app.
fun onUpdateRemotePreferences(group: String, diff: Bundle) {
val groupCallbacks = callbacks[group] ?: return
for (callback in groupCallbacks) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import android.os.Bundle
import android.os.ParcelFileDescriptor
import android.os.RemoteException
import android.util.Log
import io.github.libxposed.service.HookedProcess
import io.github.libxposed.service.IHotReloadCallback
import io.github.libxposed.service.IXposedScopeCallback
import io.github.libxposed.service.IXposedService
import java.io.Serializable
Expand Down Expand Up @@ -140,6 +142,45 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() {
}
}

override fun getRunningTargets(): List<HookedProcess> {
ensureModule()
return ApplicationService.getRunningTargets(loadedModule)
}

override fun hotReloadModule(
targetId: Long,
data: Bundle?,
callback: IHotReloadCallback?
) {
ensureModule()
runCatching {
if (loadedModule.file.moduleClassNames.size != 1) {
throw HotReloadUnsupportedException("Hot reload requires exactly one Java entry class")
}
val latest =
ConfigCache.getModuleByPackage(loadedModule.packageName)
?: throw HotReloadUnsupportedException(
"Module ${loadedModule.packageName} is not enabled")
if (latest.file.moduleClassNames.size != 1) {
throw HotReloadUnsupportedException(
"Hot reload requires exactly one Java entry class")
}
ApplicationService.hotReloadTarget(targetId, latest, data)
callback?.onHotReloadResult(IXposedService.HOT_RELOAD_SUCCEEDED, null)
}
.onFailure { throwable ->
if (throwable is SecurityException) throw throwable
val status =
when (throwable) {
is HotReloadInProgressException -> IXposedService.HOT_RELOAD_IN_PROGRESS
is HotReloadProcessDiedException -> IXposedService.HOT_RELOAD_PROCESS_DIED
is HotReloadUnsupportedException -> IXposedService.HOT_RELOAD_UNSUPPORTED
else -> IXposedService.HOT_RELOAD_FAILED
}
callback?.onHotReloadResult(status, throwable.message)
}
}

override fun requestRemotePreferences(group: String): Bundle {
val userId = ensureModule()
return Bundle().apply {
Expand All @@ -154,6 +195,10 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() {
val userId = ensureModule()
val values = mutableMapOf<String, Any?>()

if (diff.getBoolean("clear", false)) {
PreferenceStore.deleteModulePrefs(loadedModule.packageName, userId, group)
}

diff.getSerializable("delete")?.let { deletes ->
(deletes as Set<*>).forEach { values[it as String] = null }
}
Expand All @@ -170,6 +215,8 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() {

override fun deleteRemotePreferences(group: String) {
PreferenceStore.deleteModulePrefs(loadedModule.packageName, ensureModule(), group)
(loadedModule.service as? InjectedModuleService)
?.onUpdateRemotePreferences(group, Bundle().apply { putBoolean("clear", true) })
}

override fun listRemoteFiles(): Array<String> {
Expand Down
1 change: 1 addition & 0 deletions services/daemon-service/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ android {

dependencies {
compileOnly(libs.androidx.annotation)
compileOnly(projects.shared.libxposedAnnotation)
compileOnly(projects.hiddenapi.stubs)
}
Loading