Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ This project is made possible by the following open-source contributions:
* [XposedBridge](https://github.com/rovo89/XposedBridge): The standard Xposed APIs.
* [Dobby](https://github.com/JingMatrix/Dobby): Inline hooking implementation.
* [LSPosed](https://github.com/LSPosed/LSPosed): Upstream source.
* [EdXposed](https://github.com/ElderDrivers/EdXposed): Upstream source, before LSPosed.
* [xz-embedded](https://github.com/tukaani-project/xz-embedded): Library decompression utilities.

<details>
Expand Down
8 changes: 4 additions & 4 deletions daemon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ src/main/
└── kotlin/org/matrix/vector/daemon/
├── data/ # SQLite schema, immutable state cache, and file operations
├── env/ # UNIX domain socket servers and native process monitors
├── ipc/ # AIDL endpoints (Application, Manager, Module, SystemServer)
├── ipc/ # AIDL endpoints (Framework, Manager, ModuleApp, InjectedModule, SystemServer)
├── system/ # System binder delegates and Notification UI
├── utils/ # Context forgery, signature verification, and JNI bridges
├── Cli.kt # Command-line interface definitions
Expand Down Expand Up @@ -47,15 +47,15 @@ When a standard user application spawns, it requests framework access from the d
* The target application queries the `activity` service. The Zygisk module inside `system_server` intercepts this query.
* The `system_server` forwards the application's UID, PID, process name, and a newly created heartbeat `BBinder` to the daemon using the previously stored `VectorService` reference.
* The daemon verifies the request against its `ConfigCache` to determine if the application is within the scope of any enabled modules.
* If approved, the daemon returns an `ApplicationService` binder, which the `system_server` passes back to the target application.
* If approved, the daemon returns an `FrameworkService` binder, which the `system_server` passes back to the target application.
* The daemon links a `DeathRecipient` to the heartbeat binder to automatically clean up internal tracking maps when the application process dies.
* The target application uses the `ApplicationService` binder to fetch its specific module list, framework DEX, and obfuscation map.
* The target application uses the `FrameworkService` binder to fetch its specific module list, framework DEX, and obfuscation map.

### 3. Libxposed Module Injection
Unlike target applications which request access, the daemon actively pushes its API binder to module processes. This mechanism is strictly limited to modules utilizing the modern libxposed API.

* The daemon registers an `IUidObserver` with the Activity Manager to monitor process lifecycles.
* When a UID becomes active, `ModuleService` checks if the UID belongs to an enabled libxposed module.
* When a UID becomes active, `ModuleAppService` checks if the UID belongs to an enabled libxposed module.
* The daemon retrieves an `IXposedService` binder. To deliver it, the daemon calls `IActivityManager.getContentProviderExternal`, targeting a synthetic authority constructed from the module's package name.
* The daemon executes `IContentProvider.call` with the action `SEND_BINDER` and a `Bundle` containing the binder. This injects the binder into the module's process space before `Application.onCreate` executes, providing access to API verification, scope requests, and remote preferences.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ object VectorDaemon {
@Suppress("DEPRECATION") Looper.prepareMainLooper()

// Squat on the proxy service name immediately, which creates the early IPC channel of
// ApplicationService for our Zygisk module during system_server specialization.
// FrameworkService for our Zygisk module during system_server specialization.
SystemServerService.registerProxyService(proxyServiceName)

// Start Environmental Daemons
Expand Down Expand Up @@ -107,7 +107,7 @@ object VectorDaemon {
// to do so while we still have root. On a successful injection a binder thread opens it for
// us during specialization, but when the injection fails nothing else has, and the daemon
// used to die here on an unreadable preference.
val isVerboseLog = ManagerService.isVerboseLog()
val isVerboseLog = ManagerService.isVerboseLogEnabled()

// Setup IPC channel for applications by injecting DaemonService binder
sendToBridge(VectorService.asBinder(), false, systemServerMaxRetry)
Expand Down
26 changes: 13 additions & 13 deletions daemon/src/main/kotlin/org/matrix/vector/daemon/VectorService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ import android.util.Log
import hidden.HiddenApiBridge
import io.github.libxposed.service.IXposedScopeCallback
import kotlinx.coroutines.launch
import org.lsposed.lspd.models.Application
import org.matrix.vector.ipc.ScopeEntry
import org.matrix.vector.ipc.IVectorDaemon
import org.matrix.vector.ipc.IFrameworkService
import org.matrix.vector.daemon.data.ConfigCache
import org.matrix.vector.daemon.data.ModuleDatabase
import org.matrix.vector.daemon.data.PreferenceStore
import org.matrix.vector.daemon.data.ProcessScope
import org.matrix.vector.daemon.ipc.ApplicationService
import org.matrix.vector.daemon.ipc.FrameworkService
import org.matrix.vector.daemon.ipc.ManagerService
import org.matrix.vector.daemon.ipc.ModuleService
import org.matrix.vector.daemon.ipc.ModuleAppService
import org.matrix.vector.daemon.system.*

private const val TAG = "VectorService"
Expand Down Expand Up @@ -75,7 +75,7 @@ object VectorService : IVectorDaemon.Stub() {
Log.w(TAG, "Unauthorized attachProcess call")
return null
}
if (ApplicationService.hasRegister(uid, pid)) return null
if (FrameworkService.hasRegister(uid, pid)) return null

val scope = ProcessScope(processName, uid)
if (!ManagerService.tryRegisterManagerProcess(pid, uid, processName) &&
Expand All @@ -84,8 +84,8 @@ object VectorService : IVectorDaemon.Stub() {
return null
}

return if (ApplicationService.registerHeartBeat(uid, pid, processName, heartBeat)) {
ApplicationService
return if (FrameworkService.registerHeartBeat(uid, pid, processName, heartBeat)) {
FrameworkService
} else null
}

Expand Down Expand Up @@ -196,15 +196,15 @@ object VectorService : IVectorDaemon.Stub() {
// UID Observer
val uidObserver =
object : android.app.IUidObserver.Stub() {
override fun onUidActive(uid: Int) = ModuleService.uidStarts(uid)
override fun onUidActive(uid: Int) = ModuleAppService.uidStarts(uid)

override fun onUidCachedChanged(uid: Int, cached: Boolean) {
if (!cached) ModuleService.uidStarts(uid)
if (!cached) ModuleAppService.uidStarts(uid)
}

override fun onUidIdle(uid: Int, disabled: Boolean) = ModuleService.uidStarts(uid)
override fun onUidIdle(uid: Int, disabled: Boolean) = ModuleAppService.uidStarts(uid)

override fun onUidGone(uid: Int, disabled: Boolean) = ModuleService.uidGone(uid)
override fun onUidGone(uid: Int, disabled: Boolean) = ModuleAppService.uidGone(uid)
}

val which =
Expand Down Expand Up @@ -316,7 +316,7 @@ object VectorService : IVectorDaemon.Stub() {
val scopeList = ModuleDatabase.getModuleScope(xposedModule) ?: mutableListOf()

val newScope =
Application().apply {
ScopeEntry().apply {
this.packageName = moduleName
this.userId = userId
}
Expand Down Expand Up @@ -367,7 +367,7 @@ object VectorService : IVectorDaemon.Stub() {
if (moduleName != null && isXposedModule && !isRemovedAction && !isRemovedForAllUsers) {
val scopes = ModuleDatabase.getModuleScope(moduleName) ?: emptyList()
val isSystemModule = scopes.any { it.packageName == "system" }
val isEnabled = ManagerService.enabledModules().contains(moduleName)
val isEnabled = ManagerService.getEnabledModules().contains(moduleName)

NotificationManager.notifyModuleUpdated(moduleName, userId, isEnabled, isSystemModule)
}
Expand Down Expand Up @@ -455,7 +455,7 @@ object VectorService : IVectorDaemon.Stub() {
val storedUserId = if (scopePackageName == "system") 0 else userId
if (scopes.none { it.packageName == scopePackageName && it.userId == storedUserId }) {
scopes.add(
Application().apply {
ScopeEntry().apply {
this.packageName = scopePackageName
this.userId = storedUserId
})
Expand Down
21 changes: 10 additions & 11 deletions daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,13 @@ import java.nio.file.attribute.PosixFilePermissions
import java.util.UUID
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import org.lsposed.lspd.ILSPManagerService
import org.lsposed.lspd.models.Application
import org.matrix.vector.ipc.IManagerService
import org.matrix.vector.ipc.LoadedModule
import org.matrix.vector.daemon.BuildConfig
import org.matrix.vector.daemon.VectorDaemon
import org.matrix.vector.daemon.ipc.ApplicationService
import org.matrix.vector.daemon.ipc.FrameworkService
import org.matrix.vector.daemon.ipc.InjectedModuleService
import org.matrix.vector.daemon.ipc.ModuleService
import org.matrix.vector.daemon.ipc.ModuleAppService
import org.matrix.vector.daemon.system.*
import org.matrix.vector.daemon.utils.InstallerVerifier
import org.matrix.vector.daemon.utils.applySqliteHelperWorkaround
Expand Down Expand Up @@ -264,7 +263,7 @@ object ConfigCache {
// module the user did enable, and they would find the switch off with no reason given.
// The configuration stands; what could not be done is recorded and reported instead.
Log.w(TAG, "Failed to find path of $pkgName")
unloadable[pkgName] = ILSPManagerService.MODULE_LOAD_NO_APK
unloadable[pkgName] = IManagerService.MODULE_LOAD_NO_APK
return@forEach
}
apkPath = realApkPath
Expand All @@ -284,7 +283,7 @@ object ConfigCache {
// being first in the list and answering for packages it does not even hold.
// The resolution above now deliberately prefers a *holder*, so for a module only
// user 11 has this reads 1110136, and without the modulo the module would fail
// its own authentication in `ModuleService.ensureModule` against a caller's
// its own authentication in `ModuleAppService.ensureModule` against a caller's
// 10136 and never be sent its binder.
appId = appInfo.uid % PER_USER_RANGE
versionCode = pkgInfo.longVersionCode
Expand All @@ -302,11 +301,11 @@ object ConfigCache {
// than reported as "the framework could not load it" alongside a zip that will not parse.
ModuleLoad.UnsupportedApi -> {
Log.w(TAG, "Could not load $pkgName: it targets libxposed API 100; skipping.")
unloadable[pkgName] = ILSPManagerService.MODULE_LOAD_UNSUPPORTED_API
unloadable[pkgName] = IManagerService.MODULE_LOAD_UNSUPPORTED_API
}
ModuleLoad.Unusable -> {
Log.w(TAG, "Could not load $pkgName; skipping.")
unloadable[pkgName] = ILSPManagerService.MODULE_LOAD_UNUSABLE
unloadable[pkgName] = IManagerService.MODULE_LOAD_UNUSABLE
}
}
}
Expand Down Expand Up @@ -435,12 +434,12 @@ object ConfigCache {

// Targets are removed only after the module set has been published.
(oldState.modules.keys - newModules.keys).forEach {
ApplicationService.forgetHotReloadTargets(it)
FrameworkService.forgetHotReloadTargets(it)
}
ApplicationService.backfillLoadedVersions()
FrameworkService.backfillLoadedVersions()

// Ask stale opt-in targets to load the generation that was just installed.
newModules.values.forEach { ModuleService.autoHotReload(it) }
newModules.values.forEach { ModuleAppService.autoHotReload(it) }
// Log.d(TAG, "cached modules:")
// newModules.forEach { (pkg, mod) -> Log.d(TAG, "$pkg ${mod.apkPath}") }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ object FileSystem {
fun getPreloadDex(obfuscate: Boolean): SharedMemory? {
if (preloadDex == null) {
runCatching {
FileInputStream("framework/lspd.dex").use { preloadDex = readDex(it, obfuscate) }
FileInputStream("framework/vector.dex").use { preloadDex = readDex(it, obfuscate) }
}
.onFailure { Log.e(TAG, "Failed to load framework dex", it) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package org.matrix.vector.daemon.data
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import android.util.Log
import org.lsposed.lspd.models.Application
import org.matrix.vector.ipc.ScopeEntry
import org.matrix.vector.daemon.system.NotificationManager

private const val TAG = "VectorModuleDatabase"
Expand Down Expand Up @@ -39,9 +39,9 @@ object ModuleDatabase {
/** The one database handle. [ConfigCache], [PreferenceStore] and the CLI all borrow it. */
val dbHelper = Database()

fun getModuleScope(packageName: String): MutableList<Application>? {
fun getModuleScope(packageName: String): MutableList<ScopeEntry>? {
if (packageName == "lspd") return null
val result = mutableListOf<Application>()
val result = mutableListOf<ScopeEntry>()
dbHelper.readableDatabase
.query(
"scope INNER JOIN modules ON scope.mid = modules.mid",
Expand All @@ -54,7 +54,7 @@ object ModuleDatabase {
.use { cursor ->
while (cursor.moveToNext()) {
result.add(
Application().apply {
ScopeEntry().apply {
this.packageName = cursor.getString(0)
this.userId = cursor.getInt(1)
})
Expand Down Expand Up @@ -203,8 +203,11 @@ object ModuleDatabase {
put("apk_path", "") // defer to cache updating
put("enabled", 1)
}
db.insert("modules", null, values)
changed = true
// `insert` answers -1 rather than throwing: it catches the SQLException itself and logs one
// line. Taking that for granted reported a write that never landed as a success, and the
// caller acted on it — the manager left its switch on, and the shade's "not activated yet"
// notice was cancelled for a module the database had no row for.
changed = db.insert("modules", null, values) != -1L
} else {
val values = ContentValues().apply { put("enabled", 1) }
changed = db.update("modules", values, "module_pkg_name = ?", arrayOf(packageName)) > 0
Expand Down Expand Up @@ -247,7 +250,7 @@ object ModuleDatabase {
return changed
}

fun setModuleScope(packageName: String, scope: MutableList<Application>): Boolean {
fun setModuleScope(packageName: String, scope: MutableList<ScopeEntry>): Boolean {
// Last line of defence for staticScope. The manager, the socket CLI, a backup restore and a
// module's own requestScope all end up here, so refusing here covers every one of them.
ConfigCache.staticScopeOf(packageName)?.let { claimed ->
Expand All @@ -271,8 +274,8 @@ object ModuleDatabase {
for (app in scope) {
// A module is one package, one APK and one scope set for the whole device — Android cannot
// hold two different builds under one package name, so there is nothing here to key by
// user. What [Application.userId] names is the *target*: which installed instance of
// [Application.packageName] this row points at. `ConfigCache` refuses to expand a row whose
// user. What [ScopeEntry.userId] names is the *target*: which installed instance of
// [ScopeEntry.packageName] this row points at. `ConfigCache` refuses to expand a row whose
// user does not hold the module, which is what keeps a module installed for one user out of
// another user's processes.
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ import java.io.FileInputStream
import java.nio.file.Files
import java.nio.file.Paths
import kotlinx.coroutines.launch
import org.lsposed.lspd.ILSPManagerService
import org.matrix.vector.ipc.IManagerService
import org.matrix.vector.daemon.VectorDaemon

private const val TAG = "VectorDex2Oat"

// Compatibility states mirrored directly from the ILSPManagerService AIDL contract.
val DEX2OAT_OK = ILSPManagerService.DEX2OAT_OK
val DEX2OAT_MOUNT_FAILED = ILSPManagerService.DEX2OAT_MOUNT_FAILED
val DEX2OAT_SEPOLICY_INCORRECT = ILSPManagerService.DEX2OAT_SEPOLICY_INCORRECT
val DEX2OAT_SELINUX_PERMISSIVE = ILSPManagerService.DEX2OAT_SELINUX_PERMISSIVE
val DEX2OAT_CRASHED = ILSPManagerService.DEX2OAT_CRASHED
// Wrapper states mirrored directly from the IManagerService AIDL contract.
val DEX2OAT_OK = IManagerService.DEX2OAT_OK
val DEX2OAT_MOUNT_FAILED = IManagerService.DEX2OAT_MOUNT_FAILED
val DEX2OAT_SEPOLICY_INCORRECT = IManagerService.DEX2OAT_SEPOLICY_INCORRECT
val DEX2OAT_SELINUX_PERMISSIVE = IManagerService.DEX2OAT_SELINUX_PERMISSIVE
val DEX2OAT_CRASHED = IManagerService.DEX2OAT_CRASHED

object Dex2OatServer {
private const val WRAPPER32 = "bin/dex2oat32"
Expand Down
Loading
Loading