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
33 changes: 27 additions & 6 deletions daemon/src/main/kotlin/org/matrix/vector/daemon/VectorService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import android.os.Binder
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.os.Process
import android.os.SystemProperties
import android.telephony.TelephonyManager
import android.util.Log
import hidden.HiddenApiBridge
Expand All @@ -18,20 +20,23 @@ import kotlinx.coroutines.launch
import org.matrix.vector.ipc.ScopeEntry
import org.matrix.vector.ipc.IVectorDaemon
import org.matrix.vector.ipc.IFrameworkService
import org.matrix.vector.ipc.IModuleService
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.FrameworkService
import org.matrix.vector.daemon.ipc.ManagerService
import org.matrix.vector.daemon.ipc.ModuleAppService
import org.matrix.vector.daemon.ipc.SystemServerService
import org.matrix.vector.daemon.system.*

private const val TAG = "VectorService"

object VectorService : IVectorDaemon.Stub() {

private var bootCompleted = false
private var contextOwner: IBinder? = null

/**
* What the dialer broadcasts when a secret code is entered, which changed name in Q.
Expand All @@ -49,18 +54,22 @@ object VectorService : IVectorDaemon.Stub() {
/** Dial *#*#832867#*#* ("VECTOR" on the keypad) to open the manager. */
private const val SECRET_CODE = "832867"

@Synchronized
override fun dispatchSystemServerContext(
appThread: IBinder?,
activityToken: IBinder?,
) {
appThread?.let { SystemContext.appThread = IApplicationThread.Stub.asInterface(it) }
if (Binder.getCallingUid() != Process.SYSTEM_UID || appThread == null) return
if (contextOwner == appThread) return
SystemContext.appThread = IApplicationThread.Stub.asInterface(appThread)
SystemContext.token = activityToken

// Initialize OS Observers using Coroutines for the dispatch blocks
registerReceivers()
contextOwner = appThread

if (VectorDaemon.isLateInject) {
Log.i(TAG, "Late injection detected. Forcing boot completed event.")
if (VectorDaemon.isLateInject || SystemProperties.get("sys.boot_completed") == "1") {
Log.i(TAG, "Connected to an already booted system server.")
dispatchBootCompleted()
}
}
Expand All @@ -69,17 +78,22 @@ object VectorService : IVectorDaemon.Stub() {
uid: Int,
pid: Int,
processName: String,
heartBeat: IBinder
heartBeat: IBinder,
): IFrameworkService? {
if (Binder.getCallingUid() != 1000) {
Log.w(TAG, "Unauthorized attachProcess call")
return null
}
if (processName == "system") {
return SystemServerService.attachProcess(uid, pid, processName, heartBeat)
}
if (FrameworkService.hasRegister(uid, pid)) return null

val scope = ProcessScope(processName, uid)
if (!ManagerService.tryRegisterManagerProcess(pid, uid, processName) &&
ConfigCache.shouldSkipProcess(scope)) {
if (
!ManagerService.tryRegisterManagerProcess(pid, uid, processName) &&
ConfigCache.shouldSkipProcess(scope)
) {
Log.d(TAG, "Skipped $processName/$uid")
return null
}
Expand All @@ -91,6 +105,13 @@ object VectorService : IVectorDaemon.Stub() {

override fun preStartManager() = ManagerService.preStartManager()

override fun reconnectSystemModule(
packageName: String,
loadedVersionCode: Long,
hotReloadable: Boolean,
): IModuleService? =
FrameworkService.reconnectSystemModule(packageName, loadedVersionCode, hotReloadable)

private fun createReceiver() =
object : IIntentReceiver.Stub() {
override fun performReceive(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ object ConfigCache {
applySqliteHelperWorkaround()
}

private fun ensureCacheReady() {
internal fun ensureCacheReady() {
if (!state.isCacheReady && packageManager?.asBinder()?.isBinderAlive == true) {
// The rebuild lock, not `this`: this is the one place that starts a rebuild without going
// through the channel, so it has to queue behind a rebuild already in flight rather than
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import java.util.concurrent.atomic.AtomicLong
import org.matrix.vector.ipc.LoadedModule
import org.matrix.vector.ipc.IProcessChannel
import org.matrix.vector.ipc.IFrameworkService
import org.matrix.vector.ipc.IModuleService
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.system.FIRST_APPLICATION_UID
import org.matrix.vector.daemon.system.PER_USER_RANGE
import org.matrix.vector.daemon.utils.InstallerVerifier
Expand Down Expand Up @@ -45,6 +47,7 @@ object FrameworkService : IFrameworkService.Stub() {
data class ProcessKey(val uid: Int, val pid: Int)

private val processes = ConcurrentHashMap<ProcessKey, ProcessInfo>()
private val processRegistrationLock = Any()

/** One module generation loaded into one process: what a hot reload request addresses. */
class HotReloadTarget(
Expand All @@ -57,6 +60,7 @@ object FrameworkService : IFrameworkService.Stub() {
val hotReloadable: Boolean,
) {
val state = AtomicInteger(HookedProcess.TARGET_STATE_UP_TO_DATE)
@Volatile var mayBackfillVersion = true
}

private val hotReloadTargets = ConcurrentHashMap<Long, HotReloadTarget>()
Expand All @@ -70,15 +74,13 @@ object FrameworkService : IFrameworkService.Stub() {

@Volatile var hotReloadBinder: IProcessChannel? = null

init {
heartBeat.linkToDeath(this, 0)
processes[key] = this
}

override fun binderDied() {
heartBeat.unlinkToDeath(this, 0)
processes.remove(key)
targetIds.values.forEach { hotReloadTargets.remove(it) }
synchronized(processRegistrationLock) {
runCatching { heartBeat.unlinkToDeath(this, 0) }
if (processes.remove(key, this)) {
targetIds.values.forEach { hotReloadTargets.remove(it) }
}
}
}
}

Expand Down Expand Up @@ -156,7 +158,7 @@ object FrameworkService : IFrameworkService.Stub() {
// system_server records its targets before PMS exists, so they start without a version.
fun backfillLoadedVersions() {
hotReloadTargets.values
.filter { it.loadedVersionCode == 0L }
.filter { it.loadedVersionCode == 0L && it.mayBackfillVersion }
.forEach { target ->
ConfigCache.state.modules[target.modulePackageName]
?.versionCode
Expand Down Expand Up @@ -246,11 +248,67 @@ object FrameworkService : IFrameworkService.Stub() {
}

fun registerHeartBeat(uid: Int, pid: Int, processName: String, heartBeat: IBinder): Boolean {
return runCatching {
ProcessInfo(ProcessKey(uid, pid), processName, heartBeat)
true
}
.getOrDefault(false)
return synchronized(processRegistrationLock) {
val key = ProcessKey(uid, pid)
processes[key]?.let {
// A repeated system-server handshake must retain its process channel and module targets.
if (it.heartBeat.isBinderAlive) return@synchronized it.processName == processName
// Death delivery may be queued while a new process has already reused the PID.
it.binderDied()
}
runCatching {
val info = ProcessInfo(key, processName, heartBeat)
heartBeat.linkToDeath(info, 0)
if (!heartBeat.isBinderAlive) {
heartBeat.unlinkToDeath(info, 0)
false
} else {
processes[key] = info
true
}
}
.getOrDefault(false)
}
}

fun isSystemServerRegistered(): Boolean =
processes.values.any {
it.key.uid == Process.SYSTEM_UID && it.processName == "system" && it.heartBeat.isBinderAlive
}

fun reconnectSystemModule(
packageName: String,
loadedVersionCode: Long,
hotReloadable: Boolean,
): IModuleService? {
val info = ensureRegistered()
if (info.key.uid != Process.SYSTEM_UID || info.processName != "system") {
throw SecurityException("Only system_server may restore its module connection")
}
require(loadedVersionCode >= 0) { "Invalid loaded module version" }
ConfigCache.ensureCacheReady()
if (!ConfigCache.state.isCacheReady) throw RemoteException("Module cache is not ready")
if (ModuleDatabase.systemServerModuleRows().none { it.packageName == packageName }) return null
// Reuse the cache-owned service: it receives subsequent preference updates. Do not parse
// another APK or stage native libraries merely to reconnect code that is already loaded.
val module =
ConfigCache.state.modules[packageName]?.takeUnless { it.code.legacy } ?: return null
info.targetIds.computeIfAbsent(packageName) {
val id = nextHotReloadTargetId.getAndIncrement()
hotReloadTargets[id] =
HotReloadTarget(
id,
packageName,
info.processName,
info.key.uid,
info.key.pid,
loadedVersionCode,
hotReloadable,
)
.apply { mayBackfillVersion = false }
id
}
return module.service
}

fun hasRegister(uid: Int, pid: Int): Boolean = processes.containsKey(ProcessKey(uid, pid))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.os.IBinder
import android.os.IServiceCallback
import android.os.Parcel
import android.os.ServiceManager
import android.os.Process
import android.util.Log
import org.matrix.vector.ipc.IFrameworkService
import org.matrix.vector.daemon.*
Expand Down Expand Up @@ -59,7 +60,8 @@ object SystemServerService : Binder(), IBinder.DeathRecipient {
private var proxyServiceName: String? = null
private var originService: IBinder? = null

var systemServerRequested = false
val systemServerRequested: Boolean
get() = FrameworkService.isSystemServerRegistered()

fun registerProxyService(serviceName: String) {
// Register as the service name early to setup an IPC for `system_server`.
Expand Down Expand Up @@ -93,24 +95,26 @@ object SystemServerService : Binder(), IBinder.DeathRecipient {

/**
* Registers system_server and answers with its framework service, or null if this is not
* system_server. Only ever called from [onTransact] below.
* system_server. Used by both the initial rendezvous and the existing bridge after a daemon
* restart.
*/
private fun attachProcess(
internal fun attachProcess(
uid: Int,
pid: Int,
processName: String,
processLifeToken: IBinder?
processLifeToken: IBinder?,
): IFrameworkService? {
if (uid != 1000 || processLifeToken == null || processName != "system") return null

// Latched only once the registration has actually succeeded, not on the way in. It used to be
// set immediately after the gate above, so a registration that then failed — registerHeartBeat
// answers false when the life token cannot be linked to death — still read as attached. The
// symptom was the worst kind: the manager's status page reported the framework as present in
// system_server while no module hooking the system ever loaded, which sends a reader looking
// at their module instead of at the injection.
if (
uid != Process.SYSTEM_UID ||
processLifeToken == null ||
processName != "system" ||
getCallingUid() != uid ||
getCallingPid() != pid
)
return null

// Status is derived from the live registration, including failure and process death.
if (!FrameworkService.registerHeartBeat(uid, pid, processName, processLifeToken)) return null
systemServerRequested = true
return FrameworkService
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.matrix.vector.ipc;

import org.matrix.vector.ipc.IFrameworkService;
import org.matrix.vector.ipc.IModuleService;

/**
* The daemon's front door, reached from a newly specialized process through the zygisk bridge.
Expand All @@ -26,6 +27,9 @@ interface IVectorDaemon {
* this process, which is most of them; or the registration itself failed. A process that is
* answered null simply carries on unhooked.</p>
*
* <p>The authenticated system_server may repeat its own attachment after a daemon restart.
* An existing live registration keeps its process channel and module targets.</p>
*
* @param uid the process uid, as the daemon will re-derive it from the binder call
* @param pid the process id
* @param processName the Android process name, which is what module scope is keyed on
Expand All @@ -43,4 +47,12 @@ interface IVectorDaemon {

/** Asks the daemon to bring the manager up before it is needed. */
boolean preStartManager();

/**
* Rebinds an already loaded modern module after the daemon restarts. Only a registered
* system_server may call this. The reported version is the generation still in memory;
* this does not load code or claim that an updated APK has already been applied.
*/
@nullable IModuleService reconnectSystemModule(String packageName, long loadedVersionCode,
boolean hotReloadable);
}
29 changes: 29 additions & 0 deletions tests/reattach/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# System-server reconnection tests

Build `:daemon:compileReleaseKotlin` and `:zygisk:compileReleaseKotlin`, then run:

```sh
python3 tests/reattach/run_tests.py \
--jdk /path/to/jdk-21 \
--sdk /path/to/android-sdk \
--gradle-cache /path/to/gradle-user-home
```

Requires Python 3 and a C++17 compiler. The Java fixture loads the compiled
production classes with controlled Android/Binder doubles. It checks live
registration, caller authentication, duplicate handshakes, queued old death
notifications, PID reuse, partial restore retries, existing module contexts,
remote-preference snapshots and callbacks, and restored hot-reload targets.
The same assertions run for selected API branches from 26 through 37; this is
not an Android device or minimum-SDK compatibility matrix.

The native fixture extracts the production rendezvous and registration functions
from `ipc_bridge.cpp`. It checks absent, ordinary, and throwing services followed
by successful registration for both proxy names, plus bounded failure. Only
sleep and transport are replaced by deterministic doubles.

Device validation must additionally establish that restarting only the Vector
daemon changes its PID while preserving the kernel boot ID and the PID/start time
of `system_server` and zygote, then confirm application injection still works.
This recovery requires Vector's bridge to have already been injected; it cannot
inject an untouched running `system_server` or repair an external APEX mount failure.
Loading