From 8afd179bb9b54b1d207e84a1f1f3462371a193c1 Mon Sep 17 00:00:00 2001 From: Qing <44231502+byemaxx@users.noreply.github.com> Date: Sat, 18 Apr 2026 18:21:18 -0400 Subject: [PATCH 1/3] Make soft-restart path idempotent for dex2oat and bridge services --- .../org/matrix/vector/daemon/VectorDaemon.kt | 9 +- .../matrix/vector/daemon/data/FileSystem.kt | 17 ++- .../matrix/vector/daemon/env/Dex2OatServer.kt | 109 ++++++++++++++---- .../vector/daemon/ipc/SystemServerService.kt | 33 ++++-- 4 files changed, 131 insertions(+), 37 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt index 9c5224d82..ae07c07b6 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt @@ -146,10 +146,11 @@ object VectorDaemon { Log.w(TAG, "System Server died! Clearing caches and re-injecting...") bridgeService.unlinkToDeath(this, 0) clearSystemCaches() - SystemServerService.binderDied() // Cleanup old references - // Re-claim the service name immediately to ensure that when system_server - // restarts, our proxy is already there for the Zygisk module to find. - ServiceManager.addService(proxyServiceName, SystemServerService) + // Ensure stale binder/proxy/socket state is dropped before the next round. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Dex2OatServer.restart() + } + SystemServerService.prepareForSystemServerRestart(proxyServiceName) ManagerService.guard = null // Remove dead guard Handler(Looper.getMainLooper()).post { sendToBridge(binder, true, restartRetry - 1) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt index 5eeb7a1b9..6ca14f809 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt @@ -78,15 +78,22 @@ object FileSystem { } val cliSocket: String = socketPath.toString() - val socketFile = File(cliSocket) - if (socketFile.exists()) { - Log.d(TAG, "Existing $cliSocket deleted") - socketFile.delete() - } + cleanupSocketFile(cliSocket) return cliSocket } + fun cleanupSocketFile(path: String) { + runCatching { + val socketFile = File(path) + if (socketFile.exists()) { + Log.d(TAG, "Existing socket $path deleted") + socketFile.delete() + } + } + .onFailure { Log.w(TAG, "Failed to cleanup stale socket $path", it) } + } + /** Tries to lock the daemon lockfile. Returns false if another daemon is running. */ fun tryLock(): Boolean { return runCatching { diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt index b144e62c1..c39920f83 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt @@ -11,8 +11,11 @@ import android.util.Log import java.io.File import java.io.FileDescriptor import java.io.FileInputStream +import java.io.IOException import java.nio.file.Files import java.nio.file.Paths +import java.util.concurrent.atomic.AtomicBoolean +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.matrix.vector.daemon.VectorDaemon @@ -33,6 +36,10 @@ object Dex2OatServer { private val dex2oatArray = arrayOfNulls(6) private val fdArray = arrayOfNulls(6) + private val stateLock = Any() + private var serverJob: Job? = null + private var serverSocket: LocalServerSocket? = null + private val running = AtomicBoolean(false) @Volatile var compatibility = DEX2OAT_OK @@ -183,20 +190,49 @@ object Dex2OatServer { } fun start() { - if (notMounted()) { - doMount(true) - if (notMounted()) { - doMount(false) - compatibility = DEX2OAT_MOUNT_FAILED + synchronized(stateLock) { + if (running.get()) { + Log.d(TAG, "Dex2oat wrapper daemon already running, skip duplicate start") return } + + cleanupSocketStateLocked() + + if (notMounted()) { + doMount(true) + if (notMounted()) { + doMount(false) + compatibility = DEX2OAT_MOUNT_FAILED + return + } + } + + compatibility = DEX2OAT_OK + selinuxObserver.startWatching() + selinuxObserver.onEvent(0, null) + + running.set(true) + // Run the socket accept loop in an IO coroutine + serverJob = VectorDaemon.scope.launch { runSocketLoop() } } + } - selinuxObserver.startWatching() - selinuxObserver.onEvent(0, null) + fun stop(disableMount: Boolean = false) { + synchronized(stateLock) { + running.set(false) + serverJob?.cancel() + serverJob = null + cleanupSocketStateLocked() + selinuxObserver.stopWatching() + if (disableMount && compatibility == DEX2OAT_OK) { + doMount(false) + } + } + } - // Run the socket accept loop in an IO coroutine - VectorDaemon.scope.launch { runSocketLoop() } + fun restart() { + stop() + start() } private fun runSocketLoop() { @@ -220,28 +256,61 @@ object Dex2OatServer { SELinux.setFileContext(HOOKER64, xposedFile) runCatching { - LocalServerSocket(sockPath).use { server -> - setSockCreateContext(null) - while (true) { - // This blocks until the C++ wrapper connects - server.accept().use { client -> - val input = client.inputStream - val output = client.outputStream - val id = input.read() - if (id in fdArray.indices && fdArray[id] != null) { - client.setFileDescriptorsForSend(arrayOf(fdArray[id]!!)) - output.write(1) + serverSocket = LocalServerSocket(sockPath) + setSockCreateContext(null) + serverSocket!!.use { server -> + while (running.get()) { + try { + // This blocks until the C++ wrapper connects + server.accept().use { client -> + val input = client.inputStream + val output = client.outputStream + val id = input.read() + if (id in fdArray.indices && fdArray[id] != null) { + client.setFileDescriptorsForSend(arrayOf(fdArray[id]!!)) + output.write(1) + } } + } catch (e: IOException) { + if (!running.get()) break + throw e } } } } .onFailure { Log.e(TAG, "Dex2oat wrapper daemon crashed", it) + setSockCreateContext(null) + synchronized(stateLock) { + running.set(false) + cleanupSocketStateLocked() + } if (compatibility == DEX2OAT_OK) { doMount(false) compatibility = DEX2OAT_CRASHED } } + .onSuccess { + synchronized(stateLock) { + running.set(false) + cleanupSocketStateLocked() + } + } + } + + private fun cleanupSocketStateLocked() { + runCatching { serverSocket?.close() } + serverSocket = null + + val sockPath = runCatching { getSockPath() }.getOrNull() + if (!sockPath.isNullOrBlank() && sockPath.startsWith("/")) { + runCatching { + val socketFile = File(sockPath) + if (socketFile.exists()) { + socketFile.delete() + } + } + .onFailure { Log.w(TAG, "Failed to clean stale dex2oat socket file: $sockPath", it) } + } } } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt index dc229ad22..0119c37e0 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt @@ -17,9 +17,11 @@ object SystemServerService : ILSPSystemServerService.Stub(), IBinder.DeathRecipi private var proxyServiceName: String? = null private var originService: IBinder? = null + private var callbackRegistered = false var systemServerRequested = false + @Synchronized fun registerProxyService(serviceName: String) { // Register as the service name early to setup an IPC for `system_server`. Log.d(TAG, "Registering bridge service for `system_server` with name `$serviceName`.") @@ -40,14 +42,31 @@ object SystemServerService : ILSPSystemServerService.Stub(), IBinder.DeathRecipi override fun asBinder(): IBinder = this } runCatching { - getSystemServiceManager().registerForNotifications(serviceName, callback) + if (!callbackRegistered || proxyServiceName != serviceName) { + getSystemServiceManager().registerForNotifications(serviceName, callback) + callbackRegistered = true + } ServiceManager.addService(serviceName, this) proxyServiceName = serviceName } .onFailure { Log.e(TAG, "Failed to register IServiceCallback", it) } + } else { + runCatching { + ServiceManager.addService(serviceName, this) + proxyServiceName = serviceName + } + .onFailure { Log.e(TAG, "Failed to register proxy service `$serviceName`", it) } } } + @Synchronized + fun prepareForSystemServerRestart(serviceName: String = proxyServiceName ?: return) { + binderDied() + systemServerRequested = false + runCatching { ServiceManager.addService(serviceName, this) } + .onFailure { Log.w(TAG, "Failed to re-claim proxy service `$serviceName`", it) } + } + override fun requestApplicationService( uid: Int, pid: Int, @@ -64,13 +83,6 @@ object SystemServerService : ILSPSystemServerService.Stub(), IBinder.DeathRecipi } override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { - originService?.let { - // This is unlikely to happen unless system_server restarts / crashes, since we intentionally - // discard our proxy upon later replacements in registerProxyService. - Log.d(TAG, "Forwarding request to real `$proxyServiceName` service.") - return it.transact(code, data, reply, flags) - } - when (code) { BRIDGE_TRANSACTION_CODE -> { val uid = data.readInt() @@ -91,6 +103,11 @@ object SystemServerService : ILSPSystemServerService.Stub(), IBinder.DeathRecipi return ApplicationService.onTransact(code, data, reply, flags) } else -> { + originService?.let { + // Keep bridge transactions handled locally; only proxy non-Vector calls. + Log.d(TAG, "Forwarding request to real `$proxyServiceName` service.") + return it.transact(code, data, reply, flags) + } return super.onTransact(code, data, reply, flags) } } From 4d0b5fc1438c4f960137bab40aed8c9fbb600c85 Mon Sep 17 00:00:00 2001 From: Qing <44231502+byemaxx@users.noreply.github.com> Date: Sat, 18 Apr 2026 18:31:48 -0400 Subject: [PATCH 2/3] Fix Kotlin compile error in restart helper signature --- .../org/matrix/vector/daemon/ipc/SystemServerService.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt index 0119c37e0..4d0dd1dec 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt @@ -60,11 +60,12 @@ object SystemServerService : ILSPSystemServerService.Stub(), IBinder.DeathRecipi } @Synchronized - fun prepareForSystemServerRestart(serviceName: String = proxyServiceName ?: return) { + fun prepareForSystemServerRestart(serviceName: String? = proxyServiceName) { + val name = serviceName ?: return binderDied() systemServerRequested = false - runCatching { ServiceManager.addService(serviceName, this) } - .onFailure { Log.w(TAG, "Failed to re-claim proxy service `$serviceName`", it) } + runCatching { ServiceManager.addService(name, this) } + .onFailure { Log.w(TAG, "Failed to re-claim proxy service `$name`", it) } } override fun requestApplicationService( From 506cf53d8990389a4aa2ac5243834b76f5632555 Mon Sep 17 00:00:00 2001 From: Qing Date: Sat, 18 Apr 2026 22:08:51 -0400 Subject: [PATCH 3/3] Fix concurrent reinjection --- .../org/matrix/vector/daemon/VectorDaemon.kt | 95 +++++- .../matrix/vector/daemon/data/FileSystem.kt | 285 +++++++++++++++++- .../vector/daemon/ipc/SystemServerService.kt | 11 +- 3 files changed, 369 insertions(+), 22 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt index ae07c07b6..521ecd0c0 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt @@ -38,6 +38,7 @@ object VectorDaemon { private val exceptionHandler = CoroutineExceptionHandler { context, throwable -> Log.e(TAG, "Caught fatal coroutine exception in background task!", throwable) } + private val daemonInstanceId = FileSystem.createDaemonInstanceId() // Dispatchers.IO: Uses the shared background thread pool. // SupervisorJob(): Ensures one failing task doesn't kill the whole daemon. @@ -51,6 +52,12 @@ object VectorDaemon { fun main(args: Array) { if (!FileSystem.tryLock()) kotlin.system.exitProcess(0) + val ownerState = FileSystem.ensureActiveReinjectionOwner(daemonInstanceId) + if (!ownerState.isOwner) { + terminateStaleDaemon( + "Daemon instance `$daemonInstanceId` is not the active reinjection owner. Active owner=`${ownerState.owner?.toLogString() ?: "unknown"}`.") + } + var systemServerMaxRetry = 1 for (arg in args) { if (arg.startsWith("--system-server-max-retry=")) { @@ -61,7 +68,9 @@ object VectorDaemon { } } - Log.i(TAG, "Vector daemon started: lateInject=$isLateInject, proxy=$proxyServiceName") + Log.i( + TAG, + "Vector daemon started: instance=$daemonInstanceId, owner=${ownerState.owner?.toLogString()}, lateInject=$isLateInject, proxy=$proxyServiceName") Log.i(TAG, "Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") Thread.setDefaultUncaughtExceptionHandler { _, e -> @@ -121,16 +130,30 @@ object VectorDaemon { binder: IBinder, isRestart: Boolean, restartRetry: Int, + reinjectionLease: FileSystem.ReinjectionLease? = null, ) { check(Looper.myLooper() == Looper.getMainLooper()) { "sendToBridge MUST run on the main thread!" } + val ownerState = FileSystem.ensureActiveReinjectionOwner(daemonInstanceId) + if (!ownerState.isOwner) { + reinjectionLease?.close() + terminateStaleDaemon( + "Daemon instance `$daemonInstanceId` lost reinjection ownership before bridge injection. Active owner=`${ownerState.owner?.toLogString() ?: "unknown"}`.") + } + Os.seteuid(0) - runCatching { + try { + runCatching { var bridgeService: IBinder? - if (isRestart) Log.w(TAG, "system_server restarted...") + if (isRestart) { + Log.w( + TAG, + "system_server restarted for owner `${ownerState.owner?.toLogString() ?: daemonInstanceId}`" + + reinjectionLease?.let { ", round=${it.round}" }.orEmpty()) + } while (true) { bridgeService = ServiceManager.getService(bridgeServiceName) @@ -143,17 +166,52 @@ object VectorDaemon { val deathRecipient = object : IBinder.DeathRecipient { override fun binderDied() { - Log.w(TAG, "System Server died! Clearing caches and re-injecting...") bridgeService.unlinkToDeath(this, 0) - clearSystemCaches() - // Ensure stale binder/proxy/socket state is dropped before the next round. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - Dex2OatServer.restart() - } - SystemServerService.prepareForSystemServerRestart(proxyServiceName) - ManagerService.guard = null // Remove dead guard - Handler(Looper.getMainLooper()).post { - sendToBridge(binder, true, restartRetry - 1) + + val leaseResult = FileSystem.tryAcquireReinjectionLease(daemonInstanceId) + when (leaseResult.status) { + FileSystem.ReinjectionLeaseStatus.BUSY -> { + Log.i( + TAG, + "Daemon `$daemonInstanceId` saw system_server death but reinjection is already in progress by `${leaseResult.owner?.toLogString() ?: "another daemon"}`. Ignoring duplicate callback.") + if (leaseResult.owner?.instanceId != daemonInstanceId) { + terminateStaleDaemon( + "Daemon `$daemonInstanceId` lost restart race to `${leaseResult.owner?.toLogString() ?: "another daemon"}`.") + } + } + FileSystem.ReinjectionLeaseStatus.NOT_OWNER -> { + terminateStaleDaemon( + "Stale daemon `$daemonInstanceId` ignored system_server death. Active owner=`${leaseResult.owner?.toLogString() ?: "unknown"}`.") + } + FileSystem.ReinjectionLeaseStatus.ACQUIRED -> { + val lease = leaseResult.lease ?: return + Log.w( + TAG, + "System Server died! Owner `${leaseResult.owner?.toLogString() ?: daemonInstanceId}` handling reinjection round=${lease.round}.") + try { + clearSystemCaches() + // Ensure stale binder/proxy/socket state is dropped before the next round. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Dex2OatServer.restart() + } + SystemServerService.prepareForSystemServerRestart( + proxyServiceName, daemonInstanceId, lease.round) + ManagerService.guard = null // Remove dead guard + val posted = + Handler(Looper.getMainLooper()).post { + sendToBridge(binder, true, restartRetry - 1, lease) + } + if (!posted) { + Log.e( + TAG, + "Failed to post reinjection round=${lease.round} to the main thread.") + lease.close() + } + } catch (t: Throwable) { + lease.close() + throw t + } + } } } } @@ -186,7 +244,10 @@ object VectorDaemon { } } .onFailure { Log.e(TAG, "Error during injecting DaemonService", it) } - Os.seteuid(1000) + } finally { + Os.seteuid(1000) + reinjectionLease?.close() + } } private fun clearSystemCaches() { @@ -229,4 +290,10 @@ object VectorDaemon { } SystemProperties.set("ctl.restart", restartTarget) } + + private fun terminateStaleDaemon(reason: String): Nothing { + Log.w(TAG, "$reason Terminating stale daemon instance.") + Process.killProcess(Process.myPid()) + kotlin.system.exitProcess(0) + } } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt index 6ca14f809..53128e1bd 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt @@ -19,11 +19,11 @@ import java.io.InputStream import java.nio.channels.Channels import java.nio.channels.FileChannel import java.nio.channels.FileLock +import java.nio.channels.OverlappingFileLockException import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths import java.nio.file.StandardOpenOption -import java.nio.file.attribute.PosixFilePermissions import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -37,6 +37,9 @@ import org.matrix.vector.daemon.BuildConfig import org.matrix.vector.daemon.utils.ObfuscationManager private const val TAG = "VectorFileSystem" +private const val PRIVATE_FILE_MODE = 0x180 // 0600 +private const val PRIVATE_DIR_MODE = 0x1C0 // 0700 +private const val SYSTEM_FILE_CONTEXT = "u:object_r:system_file:s0" object FileSystem { val basePath: Path = Paths.get("/data/adb/lspd") @@ -53,14 +56,58 @@ object FileSystem { private val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.systemDefault()) private val lockPath: Path = basePath.resolve("lock") + private val reinjectionOwnerPath: Path = basePath.resolve("vector_reinjection_owner") + private val reinjectionOwnerLockPath: Path = basePath.resolve("vector_reinjection_owner.lock") + private val reinjectionRestartLockPath: Path = basePath.resolve("vector_reinjection_restart.lock") + private val reinjectionRoundPath: Path = basePath.resolve("vector_reinjection_round") private var fileLock: FileLock? = null private var lockChannel: FileChannel? = null + data class ReinjectionOwner(val instanceId: String, val pid: Int, val claimedAtMillis: Long) { + fun toLogString(): String = "$instanceId(pid=$pid, claimedAt=$claimedAtMillis)" + } + + data class ReinjectionOwnerState( + val isOwner: Boolean, + val owner: ReinjectionOwner?, + val ownershipChanged: Boolean + ) + + enum class ReinjectionLeaseStatus { + ACQUIRED, + BUSY, + NOT_OWNER, + } + + class ReinjectionLease internal constructor( + val round: Long, + private val channel: FileChannel, + private val lock: FileLock + ) : AutoCloseable { + @Volatile private var closed = false + + override fun close() { + synchronized(this) { + if (closed) return + closed = true + runCatching { lock.release() } + runCatching { channel.close() } + } + } + } + + data class ReinjectionLeaseResult( + val status: ReinjectionLeaseStatus, + val owner: ReinjectionOwner?, + val lease: ReinjectionLease? = null, + val ownershipChanged: Boolean = false + ) + init { runCatching { Files.createDirectories(basePath) - Os.chmod(basePath.toString(), "700".toInt(8)) - SELinux.setFileContext(basePath.toString(), "u:object_r:system_file:s0") + Os.chmod(basePath.toString(), PRIVATE_DIR_MODE) + SELinux.setFileContext(basePath.toString(), SYSTEM_FILE_CONTEXT) Files.createDirectories(configDirPath) } .onFailure { Log.e(TAG, "Failed to initialize directories", it) } @@ -97,17 +144,79 @@ object FileSystem { /** Tries to lock the daemon lockfile. Returns false if another daemon is running. */ fun tryLock(): Boolean { return runCatching { - val permissions = - PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")) + preparePrivateFile(lockPath) lockChannel = - FileChannel.open( - lockPath, setOf(StandardOpenOption.CREATE, StandardOpenOption.WRITE), permissions) + FileChannel.open(lockPath, setOf(StandardOpenOption.CREATE, StandardOpenOption.WRITE)) fileLock = lockChannel?.tryLock() fileLock?.isValid == true } .getOrDefault(false) } + fun createDaemonInstanceId(): String = "${Process.myPid()}@${System.currentTimeMillis()}" + + fun ensureActiveReinjectionOwner(instanceId: String): ReinjectionOwnerState { + return withRootFileAccess { + withReinjectionMetadataLock { ensureActiveReinjectionOwnerLocked(instanceId) } + } + } + + fun tryAcquireReinjectionLease(instanceId: String): ReinjectionLeaseResult { + return withRootFileAccess { + preparePrivateFile(reinjectionRestartLockPath) + val channel = + runCatching { + FileChannel.open( + reinjectionRestartLockPath, + setOf(StandardOpenOption.CREATE, StandardOpenOption.WRITE)) + } + .onFailure { Log.e(TAG, "Failed to open reinjection restart lock", it) } + .getOrNull() + ?: return@withRootFileAccess ReinjectionLeaseResult( + ReinjectionLeaseStatus.BUSY, readActiveReinjectionOwner()) + + val lock = + try { + channel.tryLock() + } catch (_: OverlappingFileLockException) { + null + } catch (t: Throwable) { + Log.w(TAG, "Failed to acquire reinjection restart lock", t) + null + } + + if (lock == null) { + runCatching { channel.close() } + return@withRootFileAccess ReinjectionLeaseResult( + ReinjectionLeaseStatus.BUSY, readActiveReinjectionOwner()) + } + + lateinit var currentOwnerState: ReinjectionOwnerState + var round = 0L + withReinjectionMetadataLock { + currentOwnerState = ensureActiveReinjectionOwnerLocked(instanceId) + if (currentOwnerState.isOwner) { + round = nextReinjectionRoundLocked() + } + } + + if (!currentOwnerState.isOwner) { + runCatching { lock.release() } + runCatching { channel.close() } + return@withRootFileAccess ReinjectionLeaseResult( + ReinjectionLeaseStatus.NOT_OWNER, + currentOwnerState.owner, + ownershipChanged = currentOwnerState.ownershipChanged) + } + + ReinjectionLeaseResult( + ReinjectionLeaseStatus.ACQUIRED, + currentOwnerState.owner, + ReinjectionLease(round, channel, lock), + currentOwnerState.ownershipChanged) + } + } + /** Clears all special file attributes (like immutable) on a directory. */ fun chattr0(path: Path): Boolean { return runCatching { @@ -464,4 +573,166 @@ object FileSystem { createLogDirPath() return logDirPath.resolve(getNewLogFileName("modules")).toFile() } + + private fun readActiveReinjectionOwner(): ReinjectionOwner? { + return withRootFileAccess { withReinjectionMetadataLock { readReinjectionOwnerLocked() } } + } + + private inline fun withReinjectionMetadataLock(block: () -> T): T { + preparePrivateFile(reinjectionOwnerLockPath) + val channel = + FileChannel.open( + reinjectionOwnerLockPath, + setOf(StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE)) + channel.use { lockedChannel -> + val lock = lockedChannel.lock() + try { + return block() + } finally { + runCatching { lock.release() } + } + } + } + + private fun ensureActiveReinjectionOwnerLocked(instanceId: String): ReinjectionOwnerState { + val currentOwner = readReinjectionOwnerLocked() + val pid = Process.myPid() + if (currentOwner?.instanceId == instanceId && currentOwner.pid == pid) { + return ReinjectionOwnerState(true, currentOwner, false) + } + if (currentOwner != null && isPidAlive(currentOwner.pid)) { + return ReinjectionOwnerState(false, currentOwner, false) + } + + val newOwner = ReinjectionOwner(instanceId, pid, System.currentTimeMillis()) + if (!writeReinjectionOwnerLocked(newOwner)) { + Log.e(TAG, "Failed to claim reinjection ownership for `${newOwner.toLogString()}`") + return ReinjectionOwnerState(false, currentOwner, false) + } + Log.i( + TAG, + "Reinjection owner changed from `${currentOwner?.toLogString() ?: "none"}` to `${newOwner.toLogString()}`") + return ReinjectionOwnerState(true, newOwner, true) + } + + private fun readReinjectionOwnerLocked(): ReinjectionOwner? { + if (!reinjectionOwnerPath.exists()) return null + return runCatching { + Files.newBufferedReader(reinjectionOwnerPath).use { reader -> + parseReinjectionOwner(reader.readLine()) + } + } + .onFailure { Log.w(TAG, "Failed to read reinjection owner state", it) } + .getOrNull() + } + + private fun writeReinjectionOwnerLocked(owner: ReinjectionOwner): Boolean { + preparePrivateFile(reinjectionOwnerPath) + return runCatching { + Files.newBufferedWriter( + reinjectionOwnerPath, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE) + .use { writer -> + writer.write( + listOf(owner.instanceId, owner.pid.toString(), owner.claimedAtMillis.toString()) + .joinToString("|")) + } + true + } + .onFailure { Log.e(TAG, "Failed to persist reinjection owner state", it) } + .getOrDefault(false) + } + + private fun parseReinjectionOwner(raw: String?): ReinjectionOwner? { + val value = raw?.trim().orEmpty() + if (value.isEmpty()) return null + val parts = value.split('|') + if (parts.size != 3) return null + val pid = parts[1].toIntOrNull() ?: return null + val claimedAt = parts[2].toLongOrNull() ?: return null + return ReinjectionOwner(parts[0], pid, claimedAt) + } + + private fun isPidAlive(pid: Int): Boolean { + if (pid <= 0) return false + + val procVisible = Files.exists(Paths.get("/proc/$pid")) + return runCatching { + Os.kill(pid, 0) + true + } + .recover { e -> + if (e is ErrnoException) { + when (e.errno) { + OsConstants.ESRCH -> false + OsConstants.EPERM -> true + else -> procVisible + } + } else { + procVisible + } + } + .getOrDefault(procVisible) + } + + private fun nextReinjectionRoundLocked(): Long { + preparePrivateFile(reinjectionRoundPath) + val current = + runCatching { + if (!reinjectionRoundPath.exists()) { + 0L + } else { + Files.newBufferedReader(reinjectionRoundPath).use { it.readLine().toLongOrNull() ?: 0L } + } + } + .getOrDefault(0L) + val next = current + 1 + runCatching { + Files.newBufferedWriter( + reinjectionRoundPath, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE) + .use { it.write(next.toString()) } + } + .onFailure { Log.w(TAG, "Failed to persist reinjection round counter", it) } + return next + } + + private inline fun withRootFileAccess(block: () -> T): T { + val originalEuid = runCatching { Os.geteuid() }.getOrDefault(0) + val switched = originalEuid != 0 && runCatching { Os.seteuid(0) }.isSuccess + try { + return block() + } finally { + if (switched) { + runCatching { Os.seteuid(originalEuid) } + .onFailure { Log.w(TAG, "Failed to restore euid to $originalEuid", it) } + } + } + } + + private fun preparePrivateFile(path: Path) { + runCatching { + val parent = path.parent + if (parent != null && !parent.exists()) { + Files.createDirectories(parent) + Os.chmod(parent.toString(), PRIVATE_DIR_MODE) + SELinux.setFileContext(parent.toString(), SYSTEM_FILE_CONTEXT) + } + + val file = path.toFile() + if (!file.exists()) { + file.createNewFile() + } + + Os.chmod(path.toString(), PRIVATE_FILE_MODE) + if (SELinux.getFileContext(path.toString()) != SYSTEM_FILE_CONTEXT) { + SELinux.setFileContext(path.toString(), SYSTEM_FILE_CONTEXT) + } + } + .onFailure { Log.w(TAG, "Failed to prepare private file $path", it) } + } } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt index 4d0dd1dec..219dfe9c8 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt @@ -60,8 +60,17 @@ object SystemServerService : ILSPSystemServerService.Stub(), IBinder.DeathRecipi } @Synchronized - fun prepareForSystemServerRestart(serviceName: String? = proxyServiceName) { + fun prepareForSystemServerRestart( + serviceName: String? = proxyServiceName, + ownerInstanceId: String? = null, + round: Long? = null, + ) { val name = serviceName ?: return + Log.i( + TAG, + "Preparing proxy service `$name` for system_server restart" + + ownerInstanceId?.let { " owner=$it" }.orEmpty() + + round?.let { " round=$it" }.orEmpty()) binderDied() systemServerRequested = false runCatching { ServiceManager.addService(name, this) }