From 7526bc84203810cfa275f72a4760040525e87759 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 24 Mar 2026 09:36:57 +0100 Subject: [PATCH 01/38] test binder --- .../org/lsposed/lspd/service/ConfigManager.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java index 4cdc43903..085198d03 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java @@ -328,8 +328,15 @@ public synchronized void updateManager(boolean uninstalled) { } static ConfigManager getInstance() { - if (instance == null) + long enterTime = SystemClock.elapsedRealtime(); + int tid = Process.myTid(); + int uid = Binder.getCallingUid(); + Log.d(TAG, "getInstance [TID:" + tid + " UID:" + uid + "]: entered"); + + if (instance == null) { instance = new ConfigManager(); + Log.d(TAG, "new instance created for [TID:" + tid + " UID:" + uid + "]"); + } boolean needCached; synchronized (instance.cacheHandler) { needCached = instance.lastModuleCacheTime == 0 || instance.lastScopeCacheTime == 0; @@ -339,7 +346,7 @@ static ConfigManager getInstance() { Log.d(TAG, "pm & um are ready, updating cache"); // must ensure cache is valid for later usage instance.updateCaches(true); - instance.updateManager(false); + // instance.updateManager(false); } } return instance; @@ -1223,7 +1230,7 @@ public void exportScopes(ZipOutputStream os) throws IOException { os.closeEntry(); } - synchronized SharedMemory getPreloadDex() { + SharedMemory getPreloadDex() { return ConfigFileManager.getPreloadDex(dexObfuscate); } From 5c8bed1e6afac163eef6dd08c0803174a741228a Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 25 Mar 2026 21:45:25 +0100 Subject: [PATCH 02/38] Start refactoring daemon into Kotlin --- app/build.gradle.kts | 1 - build.gradle.kts | 1 + daemon/build.gradle.kts | 31 +++++++++---------- .../lsposed/lspd/service/BridgeService.java | 2 +- .../lspd/service/ConfigFileManager.java | 2 +- .../lsposed/lspd/service/ConfigManager.java | 2 +- .../lspd/service/LSPManagerService.java | 2 +- .../lspd/service/LSPModuleService.java | 2 +- .../lspd/service/LSPNotificationManager.java | 4 +-- .../lsposed/lspd/service/LSPosedService.java | 2 +- .../lsposed/lspd/service/ServiceManager.java | 2 +- .../lsposed/lspd/util/InstallerVerifier.java | 2 +- gradle/libs.versions.toml | 5 +-- 13 files changed, 29 insertions(+), 29 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9ad7a2288..b883c23b7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,7 +24,6 @@ plugins { alias(libs.plugins.nav.safeargs) alias(libs.plugins.autoresconfig) alias(libs.plugins.materialthemebuilder) - alias(libs.plugins.lsplugin.resopt) alias(libs.plugins.lsplugin.apksign) } diff --git a/build.gradle.kts b/build.gradle.kts index d1d1c12e4..bb697ba2c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -161,6 +161,7 @@ tasks.register("format") { "hiddenapi/*/build.gradle.kts", "services/*-service/build.gradle.kts", ) + dependsOn(":daemon:ktfmtFormat") dependsOn(":xposed:ktfmtFormat") dependsOn(":zygisk:ktfmtFormat") } diff --git a/daemon/build.gradle.kts b/daemon/build.gradle.kts index cee0e4d42..4eeed13f2 100644 --- a/daemon/build.gradle.kts +++ b/daemon/build.gradle.kts @@ -10,18 +10,12 @@ val versionNameProvider: Provider by rootProject.extra plugins { alias(libs.plugins.agp.app) - alias(libs.plugins.lsplugin.resopt) + alias(libs.plugins.kotlin) + alias(libs.plugins.ktfmt) } android { - buildFeatures { - prefab = true - buildConfig = true - } - defaultConfig { - applicationId = "org.lsposed.daemon" - buildConfigField( "String", "DEFAULT_MANAGER_PACKAGE_NAME", @@ -40,14 +34,13 @@ android { } release { isMinifyEnabled = true - isShrinkResources = true proguardFiles("proguard-rules.pro") } } externalNativeBuild { cmake { path("src/main/jni/CMakeLists.txt") } } - namespace = "org.lsposed.daemon" + namespace = "org.matrix.vector.daemon" } android.applicationVariants.all { @@ -67,7 +60,7 @@ android.applicationVariants.all { .named(variantLowered) .get() .signingConfig - val outSrc = file("$outSrcDir/org/lsposed/lspd/util/SignInfo.java") + val outSrc = file("$outSrcDir/org/matrix/vector/daemon/utils/SignInfo.kt") outputs.file(outSrc) doLast { outSrc.parentFile.mkdirs() @@ -79,24 +72,30 @@ android.applicationVariants.all { sign?.keyPassword, sign?.keyAlias, ) + PrintStream(outSrc) .print( """ - |package org.lsposed.lspd.util; - |public final class SignInfo { - | public static final byte[] CERTIFICATE = {${ + |package org.matrix.vector.daemon.utils + | + |object SignInfo { + | @JvmField + | val CERTIFICATE = byteArrayOf(${ certificateInfo.certificate.encoded.joinToString(",") - }}; + }) |}""" .trimMargin() ) } } - registerJavaGeneratingTask(signInfoTask, outSrcDir.asFile) + // registeoJavaGeneratingTask(signInfoTask, outSrcDir.asFile) + + kotlin.sourceSets.getByName(variantLowered) { kotlin.srcDir(signInfoTask.map { outSrcDir }) } } dependencies { implementation(libs.agp.apksig) + implementation(libs.kotlinx.coroutines.android) implementation(projects.external.apache) implementation(projects.hiddenapi.bridge) implementation(projects.services.daemonService) diff --git a/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java b/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java index 850cc4d08..564f6130f 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java @@ -12,7 +12,7 @@ import android.system.Os; import android.util.Log; -import org.lsposed.daemon.BuildConfig; +import org.matrix.vector.daemon.BuildConfig; import java.lang.reflect.Field; import java.util.Map; diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java index 1f4d08e09..77f15f2d0 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java @@ -37,7 +37,7 @@ import androidx.annotation.Nullable; -import org.lsposed.daemon.BuildConfig; +import org.matrix.vector.daemon.BuildConfig; import org.lsposed.lspd.models.PreLoadedApk; import org.lsposed.lspd.util.InstallerVerifier; import org.lsposed.lspd.util.Utils; diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java index 085198d03..220f9b88b 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java @@ -53,7 +53,7 @@ import androidx.annotation.Nullable; import org.apache.commons.lang3.SerializationUtilsX; -import org.lsposed.daemon.BuildConfig; +import org.matrix.vector.daemon.BuildConfig; import org.lsposed.lspd.models.Application; import org.lsposed.lspd.models.Module; diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java index 0ed6b21f7..1f76154b9 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java @@ -47,7 +47,7 @@ import androidx.annotation.NonNull; -import org.lsposed.daemon.BuildConfig; +import org.matrix.vector.daemon.BuildConfig; import org.lsposed.lspd.ILSPManagerService; import org.lsposed.lspd.models.Application; import org.lsposed.lspd.models.UserInfo; diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java index 1b770c3cb..f5980139d 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java @@ -32,7 +32,7 @@ import androidx.annotation.NonNull; -import org.lsposed.daemon.BuildConfig; +import org.matrix.vector.daemon.BuildConfig; import org.lsposed.lspd.models.Module; import java.io.IOException; diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java index e2ee5148e..54cbcb215 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java @@ -24,8 +24,8 @@ import android.os.RemoteException; import android.util.Log; -import org.lsposed.daemon.BuildConfig; -import org.lsposed.daemon.R; +import org.matrix.vector.daemon.BuildConfig; +import org.matrix.vector.daemon.R; import org.lsposed.lspd.util.FakeContext; import java.util.ArrayList; diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java index 4af2632a4..6c0aec58d 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java @@ -44,7 +44,7 @@ import android.telephony.TelephonyManager; import android.util.Log; -import org.lsposed.daemon.BuildConfig; +import org.matrix.vector.daemon.BuildConfig; import org.lsposed.lspd.models.Application; import java.io.IOException; diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java index e0fbc16af..c2c066b09 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java @@ -39,7 +39,7 @@ import com.android.internal.os.BinderInternal; -import org.lsposed.daemon.BuildConfig; +import org.matrix.vector.daemon.BuildConfig; import org.lsposed.lspd.util.FakeContext; import java.io.File; diff --git a/daemon/src/main/java/org/lsposed/lspd/util/InstallerVerifier.java b/daemon/src/main/java/org/lsposed/lspd/util/InstallerVerifier.java index 1d6b81d15..d682a5c95 100644 --- a/daemon/src/main/java/org/lsposed/lspd/util/InstallerVerifier.java +++ b/daemon/src/main/java/org/lsposed/lspd/util/InstallerVerifier.java @@ -1,6 +1,6 @@ package org.lsposed.lspd.util; -import static org.lsposed.lspd.util.SignInfo.CERTIFICATE; +import static org.matrix.vector.daemon.utils.SignInfo.CERTIFICATE; import com.android.apksig.ApkVerifier; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9ec38ab0f..43ee6a6e4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ appcenter = "5.0.5" glide = "5.0.5" okhttp = "5.3.2" ktfmt = "0.25.0" +coroutines = "1.10.2" [plugins] agp-lib = { id = "com.android.library", version.ref = "agp" } @@ -15,7 +16,6 @@ nav-safeargs = { id = "androidx.navigation.safeargs", version.ref = "nav" } autoresconfig = { id = "dev.rikka.tools.autoresconfig", version = "1.2.2" } ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" } materialthemebuilder = { id = "dev.rikka.tools.materialthemebuilder", version = "1.5.1" } -lsplugin-resopt = { id = "org.lsposed.lsplugin.resopt", version = "1.6" } lsplugin-apksign = { id = "org.lsposed.lsplugin.apksign", version = "1.4" } [libraries] @@ -55,4 +55,5 @@ material = { module = "com.google.android.material:material", version = "1.12.0" gson = { module = "com.google.code.gson:gson", version = "2.13.2" } hiddenapibypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version = "6.1" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } -kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.10.2" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } From 635f73936d4c6a9bd9f7866a2c123f2c9b0948d6 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 25 Mar 2026 22:15:34 +0100 Subject: [PATCH 03/38] Refactor phase 1 --- .../vector/daemon/system/SystemBinders.kt | 59 +++++++++++++ .../vector/daemon/system/SystemExtensions.kt | 78 +++++++++++++++++ .../matrix/vector/daemon/utils/FakeContext.kt | 57 ++++++++++++ .../matrix/vector/daemon/utils/Workarounds.kt | 86 +++++++++++++++++++ 4 files changed, 280 insertions(+) create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemBinders.kt create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemBinders.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemBinders.kt new file mode 100644 index 000000000..ce5eac996 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemBinders.kt @@ -0,0 +1,59 @@ +package org.matrix.vector.daemon.system + +import android.app.IActivityManager +import android.content.Context +import android.content.pm.IPackageManager +import android.os.IBinder +import android.os.IPowerManager +import android.os.IUserManager +import android.os.RemoteException +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/** + * A thread-safe, lazy property delegate that fetches an Android system service Binder. + * Automatically links a DeathRecipient to clear the cache if the service dies. + */ +class SystemService(private val name: String, private val asInterface: (IBinder) -> T) : + ReadOnlyProperty { + @Volatile private var instance: T? = null + + private val deathRecipient = IBinder.DeathRecipient { instance = null } + + override fun getValue(thisRef: Any?, property: KProperty<*>): T? { + instance?.let { + return it + } + return synchronized(this) { + instance?.let { + return it + } + val binder = android.os.ServiceManager.getService(name) ?: return null + try { + binder.linkToDeath(deathRecipient, 0) + instance = asInterface(binder) + instance + } catch (e: RemoteException) { + null + } + } + } +} + +// --- Top-level System Binders --- +val activityManager: IActivityManager? by + SystemService(Context.ACTIVITY_SERVICE, IActivityManager.Stub::asInterface) +val packageManager: IPackageManager? by SystemService("package", IPackageManager.Stub::asInterface) +val userManager: IUserManager? by + SystemService(Context.USER_SERVICE, IUserManager.Stub::asInterface) +val powerManager: IPowerManager? by + SystemService(Context.POWER_SERVICE, IPowerManager.Stub::asInterface) + +/** + * Holds global state received from system_server during the late injection phase. Used for forging + * calls to ActivityManager that require a valid caller context. + */ +object SystemContext { + @Volatile var appThread: android.app.IApplicationThread? = null + @Volatile var token: IBinder? = null +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt new file mode 100644 index 000000000..8bc9ee15f --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt @@ -0,0 +1,78 @@ +package org.matrix.vector.daemon.system + +import android.content.pm.IPackageManager +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build + +private const val TAG = "VectorSystem" +const val PER_USER_RANGE = 100000 +const val MATCH_ANY_USER = 0x00400000 // PackageManager.MATCH_ANY_USER +const val MATCH_ALL_FLAGS = + PackageManager.MATCH_DISABLED_COMPONENTS or + PackageManager.MATCH_DIRECT_BOOT_AWARE or + PackageManager.MATCH_DIRECT_BOOT_UNAWARE or + PackageManager.MATCH_UNINSTALLED_PACKAGES or + MATCH_ANY_USER + +/** Safely fetches PackageInfo, handling API level differences. */ +fun IPackageManager.getPackageInfoCompat( + packageName: String, + flags: Int, + userId: Int +): PackageInfo? { + return try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getPackageInfo(packageName, flags.toLong(), userId) + } else { + getPackageInfo(packageName, flags, userId) + } + } catch (e: Exception) { + null + } +} + +/** + * Fetches PackageInfo alongside its components (Activities, Services, Receivers, Providers). + * Includes a fallback mechanism to prevent TransactionTooLargeException on massive apps. + */ +fun IPackageManager.getPackageInfoWithComponents( + packageName: String, + flags: Int, + userId: Int +): PackageInfo? { + val fullFlags = + flags or + PackageManager.GET_ACTIVITIES or + PackageManager.GET_SERVICES or + PackageManager.GET_RECEIVERS or + PackageManager.GET_PROVIDERS + + // Fast path: Try fetching everything at once + getPackageInfoCompat(packageName, fullFlags, userId)?.let { + return it + } + + // Fallback path: Fetch sequentially to avoid Binder Transaction limits + val baseInfo = getPackageInfoCompat(packageName, flags, userId) ?: return null + + runCatching { + baseInfo.activities = + getPackageInfoCompat(packageName, flags or PackageManager.GET_ACTIVITIES, userId) + ?.activities + } + runCatching { + baseInfo.services = + getPackageInfoCompat(packageName, flags or PackageManager.GET_SERVICES, userId)?.services + } + runCatching { + baseInfo.receivers = + getPackageInfoCompat(packageName, flags or PackageManager.GET_RECEIVERS, userId)?.receivers + } + runCatching { + baseInfo.providers = + getPackageInfoCompat(packageName, flags or PackageManager.GET_PROVIDERS, userId)?.providers + } + + return baseInfo +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt new file mode 100644 index 000000000..939523279 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt @@ -0,0 +1,57 @@ +package org.matrix.vector.daemon.utils + +import android.content.ContentResolver +import android.content.ContextWrapper +import android.content.pm.ApplicationInfo +import android.content.res.Resources +import android.os.Build +import org.matrix.vector.daemon.system.packageManager as sysPackageManager + +/** + * A stub context used by the daemon to forge intents and notifications without triggering + * system_server strict mode violations. + */ +class FakeContext(private val fakePackageName: String = "android") : ContextWrapper(null) { + + companion object { + @Volatile var nullProvider = false + private var systemAppInfo: ApplicationInfo? = null + private var fakeTheme: Resources.Theme? = null + } + + override fun getPackageName(): String = fakePackageName + + override fun getOpPackageName(): String = "android" + + override fun getApplicationInfo(): ApplicationInfo { + if (systemAppInfo == null) { + systemAppInfo = + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + sysPackageManager?.getApplicationInfo("android", 0L, 0) + } else { + sysPackageManager?.getApplicationInfo("android", 0, 0) + } + } + .getOrNull() + } + return systemAppInfo ?: ApplicationInfo() + } + + override fun getContentResolver(): ContentResolver? { + return if (nullProvider) null else object : ContentResolver(this) {} + } + + override fun getTheme(): Resources.Theme { + if (fakeTheme == null) fakeTheme = resources.newTheme() + return fakeTheme!! + } + + // Resources fetching will be implemented in Phase 2 (FileSystem/Config) + override fun getResources(): Resources { + throw NotImplementedError("Resources will be provided by FileManager in Phase 2") + } + + // Required for Android 12+ + override fun getAttributionTag(): String? = null +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt new file mode 100644 index 000000000..eef5c51a1 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt @@ -0,0 +1,86 @@ +package org.matrix.vector.daemon.utils + +import android.app.Notification +import android.content.pm.UserInfo +import android.os.Build +import android.os.IUserManager +import android.util.Log +import java.io.BufferedReader +import java.io.InputStreamReader +import java.lang.ClassNotFoundException +import org.matrix.vector.daemon.system.packageManager + +private const val TAG = "VectorWorkarounds" +private val isLenovo = Build.MANUFACTURER.equals("lenovo", ignoreCase = true) + +/** Retrieves all users, applying Lenovo's app cloning workaround (hides users 900-909). */ +fun IUserManager.getRealUsers(): List { + val users = + runCatching { getUsers(true, true, true) } + .getOrElse { + getUsers(true) // Fallback for older Android versions + } + ?.toMutableList() ?: mutableListOf() + + if (isLenovo) { + val existingIds = users.map { it.id }.toSet() + for (i in 900..909) { + if (i !in existingIds) { + runCatching { getUserInfo(i) }.getOrNull()?.let { users.add(it) } + } + } + } + return users +} + +/** Android 16 DP1 SystemUI FeatureFlag and Notification Builder workaround. */ +fun applyNotificationWorkaround() { + if (Build.VERSION.SDK_INT == 36) { + runCatching { + val feature = Class.forName("android.app.FeatureFlagsImpl") + val field = feature.getDeclaredField("systemui_is_cached").apply { isAccessible = true } + field.set(null, true) + } + .onFailure { + if (it !is ClassNotFoundException) + Log.e(TAG, "Failed to bypass systemui_is_cached flag", it) + } + } + + runCatching { Notification.Builder(FakeContext(), "notification_workaround").build() } + .onFailure { + if (it is AbstractMethodError) { + FakeContext.nullProvider = !FakeContext.nullProvider + } else { + Log.e(TAG, "Failed to build dummy notification", it) + } + } +} + +/** + * UpsideDownCake (Android 14) requires executing a shell command for dexopt, whereas older versions + * use reflection/IPC. + */ +fun performDexOptMode(packageName: String): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return runCatching { + val process = + Runtime.getRuntime().exec("cmd package compile -m speed-profile -f $packageName") + val output = BufferedReader(InputStreamReader(process.inputStream)).use { it.readText() } + val exitCode = process.waitFor() + exitCode == 0 && output.contains("Success") + } + .getOrDefault(false) + } else { + return runCatching { + packageManager?.performDexOptMode( + packageName, + false, // useJitProfiles + "speed-profile", + true, + true, + null) == true + } + .getOrDefault(false) + } +} From 4e2900d665e75171775d4db4ae5062c89048c9ba Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 25 Mar 2026 23:09:32 +0100 Subject: [PATCH 04/38] Refactor phase 2 --- .../matrix/vector/daemon/data/ConfigCache.kt | 163 +++++++++++++++ .../org/matrix/vector/daemon/data/Database.kt | 91 ++++++++ .../matrix/vector/daemon/data/FileSystem.kt | 196 ++++++++++++++++++ .../vector/daemon/system/SystemExtensions.kt | 20 ++ .../matrix/vector/daemon/utils/FakeContext.kt | 6 +- 5 files changed, 472 insertions(+), 4 deletions(-) create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/data/Database.kt create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt new file mode 100644 index 000000000..7f3a2309a --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt @@ -0,0 +1,163 @@ +package org.matrix.vector.daemon.data + +import android.util.Log +import java.util.concurrent.ConcurrentHashMap +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import org.lsposed.lspd.models.Module +import org.matrix.vector.daemon.system.MATCH_ALL_FLAGS +import org.matrix.vector.daemon.system.PER_USER_RANGE +import org.matrix.vector.daemon.system.fetchProcesses +import org.matrix.vector.daemon.system.getPackageInfoWithComponents +import org.matrix.vector.daemon.system.packageManager +import org.matrix.vector.daemon.system.userManager +import org.matrix.vector.daemon.utils.getRealUsers + +private const val TAG = "VectorConfigCache" + +data class ProcessScope(val processName: String, val uid: Int) + +object ConfigCache { + val dbHelper = Database() + + // Thread-safe maps for IPC readers + val cachedModules = ConcurrentHashMap() + val cachedScopes = ConcurrentHashMap>() + + // Coroutine Scope for background DB tasks + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + // A conflated channel automatically drops older pending events if a new one arrives. + // This perfectly replaces the manual `lastModuleCacheTime` timestamp logic! + private val cacheUpdateChannel = Channel(Channel.CONFLATED) + + init { + // Start the background consumer + scope.launch { + for (request in cacheUpdateChannel) { + performCacheUpdate() + } + } + } + + /** + * Triggers an asynchronous cache update. Multiple rapid calls are naturally coalesced by the + * Conflated Channel. + */ + fun requestCacheUpdate() { + cacheUpdateChannel.trySend(Unit) + } + + /** Blocks and forces an immediate cache update (Used during system_server boot). */ + fun forceCacheUpdateSync() { + performCacheUpdate() + } + + private fun performCacheUpdate() { + if (packageManager == null) return // Wait for PM to be ready + + Log.d(TAG, "Executing Cache Update...") + val db = dbHelper.readableDatabase + + // 1. Fetch enabled modules + val newModules = mutableMapOf() + db.query( + "modules", + arrayOf("module_pkg_name", "apk_path"), + "enabled = 1", + null, + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + val pkgName = cursor.getString(0) + val apkPath = cursor.getString(1) + if (pkgName == "lspd") continue + + // TODO: Fetch real obfuscate pref from configs table later + val isObfuscateEnabled = true + val preLoadedApk = FileSystem.loadModule(apkPath, isObfuscateEnabled) + + if (preLoadedApk != null) { + val module = Module() + module.packageName = pkgName + module.apkPath = apkPath + module.file = preLoadedApk + // Note: module.appId, module.applicationInfo, and module.service + // will be populated in Phase 4 when we implement InjectedModuleService + newModules[pkgName] = module + } else { + Log.w(TAG, "Failed to parse DEX/ZIP for $pkgName, skipping.") + } + } + } + + // 2. Fetch scopes and map heavy PM logic + val newScopes = ConcurrentHashMap>() + db.query( + "scope INNER JOIN modules ON scope.mid = modules.mid", + arrayOf("app_pkg_name", "module_pkg_name", "user_id"), + "enabled = 1", + null, + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + val appPkg = cursor.getString(0) + val modPkg = cursor.getString(1) + val userId = cursor.getInt(2) + + // system_server it fetches its own modules + if (appPkg == "system") continue + + // Ensure the module is actually valid and loaded + val module = newModules[modPkg] ?: continue + + // Heavy logic: Fetch associated processes + val pkgInfo = + packageManager?.getPackageInfoWithComponents(appPkg, MATCH_ALL_FLAGS, userId) + if (pkgInfo?.applicationInfo == null) continue + + val processNames = pkgInfo.fetchProcesses() + if (processNames.isEmpty()) continue + + val appUid = pkgInfo.applicationInfo!!.uid + + for (processName in processNames) { + val processScope = ProcessScope(processName, appUid) + newScopes.getOrPut(processScope) { mutableListOf() }.add(module) + + // Always allow the module to inject itself across all users + if (modPkg == appPkg) { + val appId = appUid % PER_USER_RANGE + userManager?.getRealUsers()?.forEach { user -> + val moduleUid = user.id * PER_USER_RANGE + appId + if (moduleUid != appUid) { // Skip duplicate + val moduleSelf = ProcessScope(processName, moduleUid) + newScopes.getOrPut(moduleSelf) { mutableListOf() }.add(module) + } + } + } + } + } + } + + // 3. Atomically swap the memory cache + cachedModules.clear() + cachedModules.putAll(newModules) + + cachedScopes.clear() + cachedScopes.putAll(newScopes) + + Log.d(TAG, "Cache Update Complete. Modules: ${cachedModules.size}") + } + + fun getModulesForProcess(processName: String, uid: Int): List { + return cachedScopes[ProcessScope(processName, uid)] ?: emptyList() + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/Database.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/Database.kt new file mode 100644 index 000000000..d806e5d5b --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/Database.kt @@ -0,0 +1,91 @@ +package org.matrix.vector.daemon.data + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.util.Log + +private const val TAG = "VectorDatabase" +private const val DB_VERSION = 4 + +class Database(context: Context? = null) : + SQLiteOpenHelper(context, FileSystem.dbPath.absolutePath, null, DB_VERSION) { + override fun onConfigure(db: SQLiteDatabase) { + super.onConfigure(db) + db.setForeignKeyConstraintsEnabled(true) + db.enableWriteAheadLogging() + // Improve write performance + db.execSQL("PRAGMA synchronous=NORMAL;") + } + + override fun onCreate(db: SQLiteDatabase) { + Log.i(TAG, "Creating new Vector database") + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS modules ( + mid integer PRIMARY KEY AUTOINCREMENT, + module_pkg_name text NOT NULL UNIQUE, + apk_path text NOT NULL, + enabled BOOLEAN DEFAULT 0 CHECK (enabled IN (0, 1)), + auto_include BOOLEAN DEFAULT 0 CHECK (auto_include IN (0, 1)) + ); + """ + .trimIndent()) + + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS scope ( + mid integer, + app_pkg_name text NOT NULL, + user_id integer NOT NULL, + PRIMARY KEY (mid, app_pkg_name, user_id), + CONSTRAINT scope_module_constraint FOREIGN KEY (mid) REFERENCES modules (mid) ON DELETE CASCADE + ); + """ + .trimIndent()) + + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS configs ( + module_pkg_name text NOT NULL, + user_id integer NOT NULL, + `group` text NOT NULL, + `key` text NOT NULL, + data blob NOT NULL, + PRIMARY KEY (module_pkg_name, user_id, `group`, `key`), + CONSTRAINT config_module_constraint FOREIGN KEY (module_pkg_name) REFERENCES modules (module_pkg_name) ON DELETE CASCADE + ); + """ + .trimIndent()) + + db.execSQL("CREATE INDEX IF NOT EXISTS configs_idx ON configs (module_pkg_name, user_id);") + + // Insert self + db.execSQL( + "INSERT OR IGNORE INTO modules (module_pkg_name, apk_path) VALUES ('lspd', ?)", + arrayOf(FileSystem.managerApkPath.toString())) + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + Log.i(TAG, "Upgrading database from $oldVersion to $newVersion") + if (oldVersion < 2) { + db.execSQL("DROP INDEX IF EXISTS configs_idx;") + db.execSQL("ALTER TABLE scope RENAME TO old_scope;") + db.execSQL("ALTER TABLE configs RENAME TO old_configs;") + onCreate(db) // Recreate tables with strict constraints + runCatching { db.execSQL("INSERT INTO scope SELECT * FROM old_scope;") } + runCatching { db.execSQL("INSERT INTO configs SELECT * FROM old_configs;") } + db.execSQL("DROP TABLE old_scope;") + db.execSQL("DROP TABLE old_configs;") + } + if (oldVersion < 3) { + db.execSQL("UPDATE scope SET app_pkg_name = 'system' WHERE app_pkg_name = 'android';") + } + if (oldVersion < 4) { + runCatching { + db.execSQL( + "ALTER TABLE modules ADD COLUMN auto_include BOOLEAN DEFAULT 0 CHECK (auto_include IN (0, 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 new file mode 100644 index 000000000..27b242cd0 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt @@ -0,0 +1,196 @@ +package org.matrix.vector.daemon.data + +import android.content.res.AssetManager +import android.content.res.Resources +import android.os.Process +import android.os.SELinux +import android.os.SharedMemory +import android.system.ErrnoException +import android.system.Os +import android.system.OsConstants +import android.util.Log +import hidden.HiddenApiBridge +import java.io.File +import java.io.InputStream +import java.nio.channels.Channels +import java.nio.channels.FileChannel +import java.nio.channels.FileLock +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.util.zip.ZipFile +import kotlin.io.path.exists +import kotlin.io.path.isDirectory +import org.lsposed.lspd.models.PreLoadedApk +import org.matrix.vector.daemon.utils.ObfuscationManager + +private const val TAG = "VectorFileSystem" + +object FileSystem { + val basePath: Path = Paths.get("/data/adb/lspd") + val modulePath: Path = basePath.resolve("modules") + val daemonApkPath: Path = Paths.get(System.getProperty("java.class.path", "")) + val managerApkPath: Path = daemonApkPath.parent.resolve("manager.apk") + val configDirPath: Path = basePath.resolve("config") + val dbPath: File = configDirPath.resolve("modules_config.db").toFile() + val magiskDbPath = File("/data/adb/magisk.db") + + private val lockPath: Path = basePath.resolve("lock") + private var fileLock: FileLock? = null + private var lockChannel: FileChannel? = null + + init { + runCatching { + Files.createDirectories(basePath) + SELinux.setFileContext(basePath.toString(), "u:object_r:system_file:s0") + Files.createDirectories(configDirPath) + } + .onFailure { Log.e(TAG, "Failed to initialize directories", it) } + } + + /** 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-------")) + lockChannel = + FileChannel.open( + lockPath, setOf(StandardOpenOption.CREATE, StandardOpenOption.WRITE), permissions) + fileLock = lockChannel?.tryLock() + fileLock?.isValid == true + } + .getOrDefault(false) + } + + /** Clears all special file attributes (like immutable) on a directory. */ + fun chattr0(path: Path): Boolean { + return runCatching { + val fd = Os.open(path.toString(), OsConstants.O_RDONLY, 0) + // 0x40086602 for 64-bit, 0x40046602 for 32-bit (FS_IOC_SETFLAGS) + val req = if (Process.is64Bit()) 0x40086602 else 0x40046602 + HiddenApiBridge.Os_ioctlInt(fd, req, 0) + Os.close(fd) + true + } + .recover { e -> if (e is ErrnoException && e.errno == OsConstants.ENOTSUP) true else false } + .getOrDefault(false) + } + + /** Recursively sets SELinux context. Crucial for modules to read their data. */ + fun setSelinuxContextRecursive(path: Path, context: String) { + runCatching { + SELinux.setFileContext(path.toString(), context) + if (path.isDirectory()) { + Files.list(path).use { stream -> + stream.forEach { setSelinuxContextRecursive(it, context) } + } + } + } + .onFailure { Log.e(TAG, "Failed to set SELinux context for $path", it) } + } + + /** + * Lazily loads resources from the daemon's APK path via reflection. This allows FakeContext to + * access strings/drawables without a real application context. + */ + val resources: Resources by lazy { + val am = AssetManager::class.java.newInstance() + val addAssetPath = + AssetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java).apply { + isAccessible = true + } + addAssetPath.invoke(am, daemonApkPath.toString()) + @Suppress("DEPRECATION") Resources(am, null, null) + } + + /** Loads a single DEX file into SharedMemory, optionally applying obfuscation. */ + private fun readDex(inputStream: InputStream, obfuscate: Boolean): SharedMemory { + var memory = SharedMemory.create(null, inputStream.available()) + val byteBuffer = memory.mapReadWrite() + Channels.newChannel(inputStream).read(byteBuffer) + SharedMemory.unmap(byteBuffer) + + if (obfuscate) { + val newMemory = ObfuscationManager.obfuscateDex(memory) + if (memory !== newMemory) { + memory.close() + memory = newMemory + } + } + memory.setProtect(OsConstants.PROT_READ) + return memory + } + + /** Parses the module APK, extracts init lists, and loads DEXes into SharedMemory. */ + fun loadModule(apkPath: String, obfuscate: Boolean): PreLoadedApk? { + val file = File(apkPath) + if (!file.exists()) return null + + val preLoadedApk = PreLoadedApk() + val preLoadedDexes = mutableListOf() + val moduleClassNames = mutableListOf() + val moduleLibraryNames = mutableListOf() + var isLegacy = false + + runCatching { + ZipFile(file).use { zip -> + // 1. Read all classes*.dex files + var secondary = 1 + while (true) { + val entryName = if (secondary == 1) "classes.dex" else "classes$secondary.dex" + val dexEntry = zip.getEntry(entryName) ?: break + zip.getInputStream(dexEntry).use { preLoadedDexes.add(readDex(it, obfuscate)) } + secondary++ + } + + // 2. Read initialization lists + fun readList(name: String, dest: MutableList) { + zip.getEntry(name)?.let { entry -> + zip.getInputStream(entry).bufferedReader().useLines { lines -> + lines + .map { it.trim() } + .filter { it.isNotEmpty() && !it.startsWith("#") } + .forEach { dest.add(it) } + } + } + } + + readList("META-INF/xposed/java_init.list", moduleClassNames) + if (moduleClassNames.isEmpty()) { + isLegacy = true + readList("assets/xposed_init", moduleClassNames) + readList("assets/native_init", moduleLibraryNames) + } else { + readList("META-INF/xposed/native_init.list", moduleLibraryNames) + } + } + } + .onFailure { + Log.e(TAG, "Failed to load module $apkPath", it) + return null + } + + if (preLoadedDexes.isEmpty() || moduleClassNames.isEmpty()) return null + + // 3. Apply obfuscation to class names if required + if (obfuscate) { + // TODO + val signatures = ObfuscationManager.getSignatures() + for (i in moduleClassNames.indices) { + val s = moduleClassNames[i] + signatures.entries + .firstOrNull { s.startsWith(it.key) } + ?.let { moduleClassNames[i] = s.replace(it.key, it.value) } + } + } + + preLoadedApk.preLoadedDexes = preLoadedDexes + preLoadedApk.moduleClassNames = moduleClassNames + preLoadedApk.moduleLibraryNames = moduleLibraryNames + preLoadedApk.legacy = isLegacy + + return preLoadedApk + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt index 8bc9ee15f..0d59985b8 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt @@ -3,6 +3,7 @@ package org.matrix.vector.daemon.system import android.content.pm.IPackageManager import android.content.pm.PackageInfo import android.content.pm.PackageManager +import android.content.pm.ServiceInfo import android.os.Build private const val TAG = "VectorSystem" @@ -76,3 +77,22 @@ fun IPackageManager.getPackageInfoWithComponents( return baseInfo } + +/** Extracts all unique process names associated with a package's components. */ +fun PackageInfo.fetchProcesses(): Set { + val processNames = mutableSetOf() + + val componentArrays = arrayOf(activities, receivers, providers) + for (components in componentArrays) { + components?.forEach { processNames.add(it.processName) } + } + + services?.forEach { service -> + // Ignore isolated processes as they shouldn't be hooked in the same way + if ((service.flags and ServiceInfo.FLAG_ISOLATED_PROCESS) == 0) { + processNames.add(service.processName) + } + } + + return processNames +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt index 939523279..e60ed4bd0 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt @@ -5,6 +5,7 @@ import android.content.ContextWrapper import android.content.pm.ApplicationInfo import android.content.res.Resources import android.os.Build +import org.matrix.vector.daemon.data.FileSystem import org.matrix.vector.daemon.system.packageManager as sysPackageManager /** @@ -47,10 +48,7 @@ class FakeContext(private val fakePackageName: String = "android") : ContextWrap return fakeTheme!! } - // Resources fetching will be implemented in Phase 2 (FileSystem/Config) - override fun getResources(): Resources { - throw NotImplementedError("Resources will be provided by FileManager in Phase 2") - } + override fun getResources(): Resources = FileSystem.resources // Required for Android 12+ override fun getAttributionTag(): String? = null From cd4a36b0c97ee3cc67cd5bc71f9124e4d647b49b Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 25 Mar 2026 23:40:41 +0100 Subject: [PATCH 05/38] Refactor phase 3 and 4 (part done) --- .../matrix/vector/daemon/data/FileSystem.kt | 38 ++ .../matrix/vector/daemon/env/Dex2OatServer.kt | 245 +++++++++++++ .../matrix/vector/daemon/env/LogcatMonitor.kt | 139 ++++++++ .../vector/daemon/ipc/ApplicationService.kt | 130 +++++++ .../daemon/ipc/InjectedModuleService.kt | 76 ++++ .../vector/daemon/ipc/ManagerService.kt | 337 ++++++++++++++++++ .../matrix/vector/daemon/ipc/ModuleService.kt | 200 +++++++++++ .../vector/daemon/ipc/SystemServerService.kt | 110 ++++++ 8 files changed, 1275 insertions(+) create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt 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 27b242cd0..bc42402ac 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 @@ -30,6 +30,8 @@ private const val TAG = "VectorFileSystem" object FileSystem { val basePath: Path = Paths.get("/data/adb/lspd") + val logDirPath: Path = basePath.resolve("log") + val oldLogDirPath: Path = basePath.resolve("log.old") val modulePath: Path = basePath.resolve("modules") val daemonApkPath: Path = Paths.get(System.getProperty("java.class.path", "")) val managerApkPath: Path = daemonApkPath.parent.resolve("manager.apk") @@ -193,4 +195,40 @@ object FileSystem { return preLoadedApk } + + /** Safely creates the log directory. If a file exists with the same name, it deletes it first. */ + private fun createLogDirPath() { + if (!Files.isDirectory(logDirPath, java.nio.file.LinkOption.NOFOLLOW_LINKS)) { + logDirPath.toFile().deleteRecursively() + } + Files.createDirectories(logDirPath) + } + + /** + * Rotates the log directory by clearing file attributes (chattr 0), deleting the old backup, and + * renaming the current log directory to the backup. + */ + fun moveLogDir() { + runCatching { + if (Files.exists(logDirPath)) { + if (chattr0(logDirPath)) { + // Kotlin's deleteRecursively replaces the verbose Java SimpleFileVisitor + oldLogDirPath.toFile().deleteRecursively() + Files.move(logDirPath, oldLogDirPath) + } + } + Files.createDirectories(logDirPath) + } + .onFailure { Log.e(TAG, "Failed to move log directory", it) } + } + + fun getPropsPath(): File { + createLogDirPath() + return logDirPath.resolve("props.txt").toFile() + } + + fun getKmsgPath(): File { + createLogDirPath() + return logDirPath.resolve("kmsg.log").toFile() + } } 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 new file mode 100644 index 000000000..0540c240d --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt @@ -0,0 +1,245 @@ +package org.matrix.vector.daemon.env + +import android.net.LocalServerSocket +import android.os.Build +import android.os.FileObserver +import android.os.SELinux +import android.system.ErrnoException +import android.system.Os +import android.system.OsConstants +import android.util.Log +import java.io.File +import java.io.FileDescriptor +import java.io.FileInputStream +import java.nio.file.Files +import java.nio.file.Paths +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +private const val TAG = "VectorDex2Oat" + +// Compatibility states matching Manager expectations +const val DEX2OAT_OK = 0 +const val DEX2OAT_MOUNT_FAILED = 1 +const val DEX2OAT_SEPOLICY_INCORRECT = 2 +const val DEX2OAT_SELINUX_PERMISSIVE = 3 +const val DEX2OAT_CRASHED = 4 + +object Dex2OatServer { + private const val WRAPPER32 = "bin/dex2oat32" + private const val WRAPPER64 = "bin/dex2oat64" + private const val HOOKER32 = "bin/liboat_hook32.so" + private const val HOOKER64 = "bin/liboat_hook64.so" + + private val dex2oatArray = arrayOfNulls(6) + private val fdArray = arrayOfNulls(6) + + @Volatile + var compatibility = DEX2OAT_OK + private set + + private external fun doMountNative( + enabled: Boolean, + r32: String?, + d32: String?, + r64: String?, + d64: String? + ) + + private external fun setSockCreateContext(context: String?): Boolean + + private external fun getSockPath(): String + + private val selinuxObserver = + object : + FileObserver( + listOf(File("/sys/fs/selinux/enforce"), File("/sys/fs/selinux/policy")), + CLOSE_WRITE) { + override fun onEvent(event: Int, path: String?) { + synchronized(this) { + if (compatibility == DEX2OAT_CRASHED) { + stopWatching() + return + } + + val enforcing = + runCatching { + Files.newInputStream(Paths.get("/sys/fs/selinux/enforce")).use { + it.read() == '1'.code + } + } + .getOrDefault(false) + + when { + !enforcing -> { + if (compatibility == DEX2OAT_OK) doMount(false) + compatibility = DEX2OAT_SELINUX_PERMISSIVE + } + hasSePolicyErrors() -> { + if (compatibility == DEX2OAT_OK) doMount(false) + compatibility = DEX2OAT_SEPOLICY_INCORRECT + } + compatibility != DEX2OAT_OK -> { + doMount(true) + if (notMounted()) { + doMount(false) + compatibility = DEX2OAT_MOUNT_FAILED + stopWatching() + } else { + compatibility = DEX2OAT_OK + } + } + } + } + } + } + + init { + // Android 10 vs 11+ path differences + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { + checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oat") + checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oatd") + checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oat64") + checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oatd64") + } else { + checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oat32") + checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oatd32") + checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oat64") + checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oatd64") + } + + openDex2oat(4, "/data/adb/modules/zygisk_vector/bin/liboat_hook32.so") + openDex2oat(5, "/data/adb/modules/zygisk_vector/bin/liboat_hook64.so") + } + + private fun hasSePolicyErrors(): Boolean { + return SELinux.checkSELinuxAccess( + "u:r:untrusted_app:s0", "u:object_r:dex2oat_exec:s0", "file", "execute") || + SELinux.checkSELinuxAccess( + "u:r:untrusted_app:s0", "u:object_r:dex2oat_exec:s0", "file", "execute_no_trans") + } + + private fun openDex2oat(id: Int, path: String) { + runCatching { + fdArray[id] = Os.open(path, OsConstants.O_RDONLY, 0) + dex2oatArray[id] = path + } + } + + private fun checkAndAddDex2Oat(path: String) { + val file = File(path) + if (!file.exists()) return + + runCatching { + FileInputStream(file).use { fis -> + val header = ByteArray(5) + if (fis.read(header) != 5) return + // Verify ELF Magic: 0x7F 'E' 'L' 'F' + if (header[0] != 0x7F.toByte() || + header[1] != 'E'.toByte() || + header[2] != 'L'.toByte() || + header[3] != 'F'.toByte()) + return + + val is32Bit = header[4] == 1.toByte() + val is64Bit = header[4] == 2.toByte() + val isDebug = path.contains("dex2oatd") + + val index = + when { + is32Bit -> if (isDebug) 1 else 0 + is64Bit -> if (isDebug) 3 else 2 + else -> -1 + } + + if (index != -1 && dex2oatArray[index] == null) { + dex2oatArray[index] = path + fdArray[index] = Os.open(path, OsConstants.O_RDONLY, 0) + Log.i(TAG, "Detected $path -> Assigned Index $index") + } + } + } + .onFailure { dex2oatArray[dex2oatArray.indexOf(path)] = null } + } + + private fun notMounted(): Boolean { + for (i in 0 until 4) { + val bin = dex2oatArray[i] ?: continue + try { + val apex = Os.stat("/proc/1/root$bin") + val wrapper = Os.stat(if (i < 2) WRAPPER32 else WRAPPER64) + if (apex.st_dev != wrapper.st_dev || apex.st_ino != wrapper.st_ino) { + return true + } + } catch (e: ErrnoException) { + return true + } + } + return false + } + + private fun doMount(enabled: Boolean) { + doMountNative(enabled, dex2oatArray[0], dex2oatArray[1], dex2oatArray[2], dex2oatArray[3]) + } + + fun start() { + if (notMounted()) { + doMount(true) + if (notMounted()) { + doMount(false) + compatibility = DEX2OAT_MOUNT_FAILED + return + } + } + + selinuxObserver.startWatching() + selinuxObserver.onEvent(0, null) + + // Run the socket accept loop in an IO coroutine + CoroutineScope(Dispatchers.IO).launch { runSocketLoop() } + } + + private fun runSocketLoop() { + val sockPath = getSockPath() + val xposedFile = "u:object_r:xposed_file:s0" + val dex2oatExec = "u:object_r:dex2oat_exec:s0" + + if (SELinux.checkSELinuxAccess("u:r:dex2oat:s0", dex2oatExec, "file", "execute_no_trans")) { + SELinux.setFileContext(WRAPPER32, dex2oatExec) + SELinux.setFileContext(WRAPPER64, dex2oatExec) + setSockCreateContext("u:r:dex2oat:s0") + } else { + SELinux.setFileContext(WRAPPER32, xposedFile) + SELinux.setFileContext(WRAPPER64, xposedFile) + setSockCreateContext("u:r:installd:s0") + } + SELinux.setFileContext(HOOKER32, xposedFile) + 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) + } + } + } + } + } + .onFailure { + Log.e(TAG, "Dex2oat wrapper daemon crashed", it) + if (compatibility == DEX2OAT_OK) { + doMount(false) + compatibility = DEX2OAT_CRASHED + } + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt new file mode 100644 index 000000000..3569f7cdc --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt @@ -0,0 +1,139 @@ +package org.matrix.vector.daemon.env + +import android.annotation.SuppressLint +import android.os.Build +import android.os.ParcelFileDescriptor +import android.os.Process +import android.os.SELinux +import android.os.SystemProperties +import android.system.Os +import android.util.Log +import java.io.File +import java.io.FileDescriptor +import java.nio.file.Files +import java.nio.file.LinkOption +import java.nio.file.Paths +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.matrix.vector.daemon.data.FileSystem + +private const val TAG = "VectorLogcat" +private const val FD_MODE = + ParcelFileDescriptor.MODE_WRITE_ONLY or + ParcelFileDescriptor.MODE_CREATE or + ParcelFileDescriptor.MODE_TRUNCATE or + ParcelFileDescriptor.MODE_APPEND + +object LogcatMonitor { + private var modulesFd = -1 + private var verboseFd = -1 + @Volatile private var isRunning = false + + private external fun runLogcat() + + // Thread-safe LRU implementation for log files + private class ThreadSafeLRU(private val maxEntries: Int = 10) { + private val map = LinkedHashMap(maxEntries, 1f, false) + + @Synchronized + fun add(file: File) { + map[file] = Unit + if (map.size > maxEntries) { + val eldest = map.keys.first() + if (eldest.delete()) { + map.remove(eldest) + } + } + } + } + + private val moduleLogs = ThreadSafeLRU() + private val verboseLogs = ThreadSafeLRU() + + init { + loadNativeLibrary() + FileSystem.moveLogDir() // Defined in FileSystem + + // Meizu log_reject_level workaround + if (SystemProperties.getInt("persist.sys.log_reject_level", 0) > 0) { + SystemProperties.set("persist.sys.log_reject_level", "0") + } + + dumpPropsAndDmesg() + } + + @SuppressLint("UnsafeDynamicallyLoadedCode") + private fun loadNativeLibrary() { + val classPath = System.getProperty("java.class.path", "") + val abi = + if (Process.is64Bit()) Build.SUPPORTED_64_BIT_ABIS[0] else Build.SUPPORTED_32_BIT_ABIS[0] + System.load("$classPath!/lib/$abi/${System.mapLibraryName("daemon")}") + } + + private fun dumpPropsAndDmesg() { + CoroutineScope(Dispatchers.IO).launch { + // Filter privacy props by temporarily assuming an untrusted context + runCatching { + SELinux.setFSCreateContext("u:object_r:app_data_file:s0") + ProcessBuilder( + "sh", + "-c", + "echo -n u:r:untrusted_app:s0 > /proc/thread-self/attr/current; getprop") + .redirectOutput(FileSystem.getPropsPath()) // Ensure this exists in FileSystem + .start() + } + .onFailure { Log.e(TAG, "getprop failed", it) } + .also { SELinux.setFSCreateContext(null) } + + runCatching { ProcessBuilder("dmesg").redirectOutput(FileSystem.getKmsgPath()).start() } + .onFailure { Log.e(TAG, "dmesg failed", it) } + } + } + + fun start() { + if (isRunning) return + isRunning = true + CoroutineScope(Dispatchers.IO).launch { + runCatching { + Log.i(TAG, "Logcat daemon starting") + runLogcat() // Blocks until the native logcat process dies + Log.i(TAG, "Logcat daemon stopped") + } + .onFailure { Log.e(TAG, "Logcat crashed", it) } + isRunning = false + } + } + + fun getVerboseLog(): File? = fdToPath(verboseFd)?.toFile() + + fun getModulesLog(): File? = fdToPath(modulesFd)?.toFile() + + private fun fdToPath(fd: Int) = if (fd == -1) null else Paths.get("/proc/self/fd", fd.toString()) + + /** Resurrects deleted log files from /proc/self/fd if an external process deletes them. */ + private fun checkFd(fd: Int) { + if (fd == -1) return + runCatching { + val jfd = FileDescriptor() + jfd.javaClass + .getDeclaredMethod("setInt\$", Int::class.java) + .apply { isAccessible = true } + .invoke(jfd, fd) + val stat = Os.fstat(jfd) + + // st_nlink == 0 means the file was deleted but the FD is still held open + if (stat.st_nlink == 0L) { + val file = Files.readSymbolicLink(fdToPath(fd)!!) + val parent = file.parent + if (!Files.isDirectory(parent, LinkOption.NOFOLLOW_LINKS)) { + if (FileSystem.chattr0(parent)) Files.deleteIfExists(parent) + } + val name = file.fileName.toString() + val originName = name.substring(0, name.lastIndexOf(' ')) + Files.copy(file, parent.resolve(originName)) + } + } + .onFailure { Log.w(TAG, "checkFd failed for $fd", it) } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt new file mode 100644 index 000000000..0040475ed --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt @@ -0,0 +1,130 @@ +package org.matrix.vector.daemon.ipc + +import android.os.IBinder +import android.os.Parcel +import android.os.ParcelFileDescriptor +import android.os.Process +import android.os.RemoteException +import android.util.Log +import java.util.concurrent.ConcurrentHashMap +import org.lsposed.lspd.models.Module +import org.lsposed.lspd.service.ILSPApplicationService +import org.matrix.vector.daemon.data.ConfigCache +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.data.ProcessScope + +private const val TAG = "VectorAppService" +private const val DEX_TRANSACTION_CODE = + ('_'.code shl 24) or ('D'.code shl 16) or ('E'.code shl 8) or 'X'.code +private const val OBFUSCATION_MAP_TRANSACTION_CODE = + ('_'.code shl 24) or ('O'.code shl 16) or ('B'.code shl 8) or 'F'.code + +object ApplicationService : ILSPApplicationService.Stub() { + + // Tracks active processes linked to their heartbeat binders + private val processes = ConcurrentHashMap() + + private class ProcessInfo(val scope: ProcessScope, val heartBeat: IBinder) : + IBinder.DeathRecipient { + init { + heartBeat.linkToDeath(this, 0) + processes[scope] = this + } + + override fun binderDied() { + heartBeat.unlinkToDeath(this, 0) + processes.remove(scope) + } + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { + when (code) { + DEX_TRANSACTION_CODE -> { + // TODO: FileSystem needs getPreloadDex() implemented + val shm = FileSystem.getPreloadDex(ConfigCache.isDexObfuscateEnabled()) ?: return false + reply?.writeNoException() + shm.writeToParcel(reply, 0) + reply?.writeLong(shm.size.toLong()) + return true + } + OBFUSCATION_MAP_TRANSACTION_CODE -> { + val obfuscation = ConfigCache.isDexObfuscateEnabled() + val signatures = org.matrix.vector.daemon.utils.ObfuscationManager.getSignatures() + reply?.writeNoException() + reply?.writeInt(signatures.size * 2) + for ((key, value) in signatures) { + reply?.writeString(key) + reply?.writeString(if (obfuscation) value else key) + } + return true + } + } + return super.onTransact(code, data, reply, flags) + } + + fun registerHeartBeat(uid: Int, pid: Int, processName: String, heartBeat: IBinder): Boolean { + return runCatching { + ProcessInfo(ProcessScope(processName, uid), heartBeat) + true + } + .getOrDefault(false) + } + + fun hasRegister(uid: Int, pid: Int): Boolean { + // We only check UID here as the map key is ProcessScope, but PID is implied by the active + // heartbeat. + return processes.keys.any { it.uid == uid } + } + + private fun ensureRegistered(): ProcessScope { + val uid = getCallingUid() + val scope = processes.keys.firstOrNull { it.uid == uid } + if (scope == null) { + Log.w(TAG, "Unauthorized IPC call from uid=$uid") + throw RemoteException("Not registered") + } + return scope + } + + override fun getModulesList(): List { + val scope = ensureRegistered() + if (scope.uid == Process.SYSTEM_UID && scope.processName == "system") { + return ConfigCache.getModulesForSystemServer() // Needs implementation in ConfigCache + } + if (ManagerService.isRunningManager(getCallingPid(), scope.uid)) { + return emptyList() + } + return ConfigCache.getModulesForProcess(scope.processName, scope.uid).filter { !it.file.legacy } + } + + override fun getLegacyModulesList(): List { + val scope = ensureRegistered() + return ConfigCache.getModulesForProcess(scope.processName, scope.uid).filter { it.file.legacy } + } + + override fun isLogMuted(): Boolean = !ManagerService.isVerboseLog + + override fun getPrefsPath(packageName: String): String { + val scope = ensureRegistered() + return ConfigCache.getPrefsPath(packageName, scope.uid) // Needs implementation in ConfigCache + } + + override fun requestInjectedManagerBinder( + binderList: MutableList + ): ParcelFileDescriptor? { + val scope = ensureRegistered() + val pid = getCallingPid() + + if (ManagerService.postStartManager(pid, scope.uid) || ConfigCache.isManager(scope.uid)) { + val heartBeat = processes[scope]?.heartBeat ?: throw RemoteException("No heartbeat") + binderList.add(ManagerService.obtainManagerBinder(heartBeat, pid, scope.uid)) + } + + return runCatching { + ParcelFileDescriptor.open( + FileSystem.managerApkPath.toFile(), ParcelFileDescriptor.MODE_READ_ONLY) + } + .onFailure { Log.e(TAG, "Failed to open manager APK", it) } + .getOrNull() + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt new file mode 100644 index 000000000..95475904b --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt @@ -0,0 +1,76 @@ +package org.matrix.vector.daemon.ipc + +import android.os.Binder +import android.os.Bundle +import android.os.ParcelFileDescriptor +import android.os.RemoteException +import android.util.Log +import io.github.libxposed.service.IXposedService +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.system.PER_USER_RANGE + +private const val TAG = "VectorInjectedModuleService" + +class InjectedModuleService(private val packageName: String) : ILSPInjectedModuleService.Stub() { + + // Tracks active RemotePreferenceCallbacks linked by config group + private val callbacks = ConcurrentHashMap>() + + override fun getFrameworkProperties(): Long { + var prop = IXposedService.PROP_CAP_SYSTEM or IXposedService.PROP_CAP_REMOTE + if (ConfigCache.isDexObfuscateEnabled()) { + prop = prop or IXposedService.PROP_RT_API_PROTECTION + } + return prop + } + + override fun requestRemotePreferences( + group: String, + callback: IRemotePreferenceCallback? + ): Bundle { + val bundle = Bundle() + val userId = Binder.getCallingUid() / PER_USER_RANGE + bundle.putSerializable( + "map", ConfigCache.getModulePrefs(packageName, userId, group) as Serializable) + + if (callback != null) { + val groupCallbacks = callbacks.getOrPut(group) { ConcurrentHashMap.newKeySet() } + groupCallbacks.add(callback) + runCatching { callback.asBinder().linkToDeath({ groupCallbacks.remove(callback) }, 0) } + .onFailure { Log.w(TAG, "requestRemotePreferences linkToDeath failed", it) } + } + return bundle + } + + 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) + } + .getOrElse { throw RemoteException(it.message) } + } + + override fun getRemoteFileList(): Array { + val userId = Binder.getCallingUid() / PER_USER_RANGE + return runCatching { + val dir = FileSystem.resolveModuleDir(packageName, "files", userId, -1) + dir.toFile().list() ?: emptyArray() + } + .getOrElse { throw RemoteException(it.message) } + } + + // Called by ModuleService when prefs are updated globally + fun onUpdateRemotePreferences(group: String, diff: Bundle) { + val groupCallbacks = callbacks[group] ?: return + for (callback in groupCallbacks) { + runCatching { callback.onUpdate(diff) }.onFailure { groupCallbacks.remove(callback) } + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt new file mode 100644 index 000000000..7a3a1a340 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt @@ -0,0 +1,337 @@ +package org.matrix.vector.daemon.ipc + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.content.pm.PackageInfo +import android.content.pm.ResolveInfo +import android.os.Build +import android.os.Bundle +import android.os.IBinder +import android.os.ParcelFileDescriptor +import android.os.SystemProperties +import android.util.Log +import android.view.IWindowManager +import io.github.libxposed.service.IXposedService +import java.io.File +import java.io.FileOutputStream +import java.util.concurrent.TimeUnit +import org.lsposed.lspd.ILSPManagerService +import org.lsposed.lspd.models.Application +import org.lsposed.lspd.models.UserInfo +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.data.ConfigCache +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.env.Dex2OatServer +import org.matrix.vector.daemon.env.LogcatMonitor +import org.matrix.vector.daemon.system.* +import org.matrix.vector.daemon.utils.getRealUsers +import org.matrix.vector.daemon.utils.performDexOptMode +import rikka.parcelablelist.ParcelableListSlice + +private const val TAG = "VectorManagerService" + +object ManagerService : ILSPManagerService.Stub() { + + @Volatile var isVerboseLog = false + @Volatile private var managerPid = -1 + @Volatile private var pendingManager = false + @Volatile private var isEnabled = true + + var guard: ManagerGuard? = null + private set + + class ManagerGuard(private val binder: IBinder, val pid: Int, val uid: Int) : + IBinder.DeathRecipient { + private val connection = + object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) {} + + override fun onServiceDisconnected(name: ComponentName?) {} + } + + init { + ManagerService.guard = this + runCatching { + binder.linkToDeath(this, 0) + // MIUI XSpace Workaround + if (Build.MANUFACTURER.equals("xiaomi", ignoreCase = true)) { + val intent = + Intent().apply { + component = + ComponentName.unflattenFromString( + "com.miui.securitycore/com.miui.xspace.service.XSpaceService") + } + activityManager?.bindService( + null, + null, + intent, + intent.type, + connection, + Context.BIND_AUTO_CREATE.toLong(), + "android", + 0) + } + } + .onFailure { + Log.e(TAG, "ManagerGuard initialization failed", it) + ManagerService.guard = null + } + } + + override fun binderDied() { + runCatching { + binder.unlinkToDeath(this, 0) + activityManager?.unbindService(connection) + } + ManagerService.guard = null + } + } + + @Synchronized + fun preStartManager(): Boolean { + pendingManager = true + managerPid = -1 + return true + } + + @Synchronized + fun shouldStartManager(pid: Int, uid: Int, processName: String): Boolean { + if (!isEnabled || + uid != BuildConfig.MANAGER_INJECTED_UID || + processName != BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME || + !pendingManager) + return false + pendingManager = false + managerPid = pid + return true + } + + fun postStartManager(pid: Int, uid: Int): Boolean = + isEnabled && uid == BuildConfig.MANAGER_INJECTED_UID && pid == managerPid + + fun obtainManagerBinder(heartbeat: IBinder, pid: Int, uid: Int): IBinder { + ManagerGuard(heartbeat, pid, uid) + return this + } + + fun isRunningManager(pid: Int, uid: Int): Boolean = false + + override fun getXposedApiVersion() = IXposedService.LIB_API + + override fun getXposedVersionCode() = BuildConfig.VERSION_CODE + + override fun getXposedVersionName() = BuildConfig.VERSION_NAME + + override fun getApi() = "Zygisk" // To be removed + + override fun getInstalledPackagesFromAllUsers( + flags: Int, + filterNoProcess: Boolean + ): ParcelableListSlice { + return ParcelableListSlice( + packageManager?.getInstalledPackagesForAllUsers(flags, filterNoProcess) ?: emptyList()) + } + + override fun enabledModules() = ConfigCache.getEnabledModules().toTypedArray() + + override fun enableModule(packageName: String) = ConfigCache.enableModule(packageName) + + override fun disableModule(packageName: String) = ConfigCache.disableModule(packageName) + + override fun setModuleScope(packageName: String, scope: MutableList) = + ConfigCache.setModuleScope(packageName, scope) + + override fun getModuleScope(packageName: String) = ConfigCache.getModuleScope(packageName) + + override fun isVerboseLog() = isVerboseLog || BuildConfig.DEBUG + + override fun setVerboseLog(enabled: Boolean) { + isVerboseLog = enabled + if (enabled) LogcatMonitor.startVerbose() else LogcatMonitor.stopVerbose() + ConfigCache.updateModulePref("lspd", 0, "config", "enable_verbose_log", enabled) + } + + override fun getVerboseLog() = + LogcatMonitor.getVerboseLog()?.let { + ParcelFileDescriptor.open(it, ParcelFileDescriptor.MODE_READ_ONLY) + } + + override fun getModulesLog(): ParcelFileDescriptor? { + LogcatMonitor.checkLogFile() + return LogcatMonitor.getModulesLog()?.let { + ParcelFileDescriptor.open(it, ParcelFileDescriptor.MODE_READ_ONLY) + } + } + + override fun clearLogs(verbose: Boolean): Boolean { + LogcatMonitor.refresh(verbose) + return true + } + + override fun getPackageInfo(packageName: String, flags: Int, uid: Int) = + packageManager?.getPackageInfoCompat(packageName, flags, uid) + + override fun forceStopPackage(packageName: String, userId: Int) { + activityManager?.forceStopPackage(packageName, userId) + } + + override fun reboot() { + powerManager?.reboot(false, null, false) + } + + override fun uninstallPackage(packageName: String, userId: Int): Boolean { + // ... omitted standard PM uninstall wrapper ... + return true + } + + override fun isSepolicyLoaded() = + android.os.SELinux.checkSELinuxAccess( + "u:r:dex2oat:s0", "u:object_r:dex2oat_exec:s0", "file", "execute_no_trans") + + override fun getUsers(): List { + return userManager?.getRealUsers()?.map { + UserInfo().apply { + id = it.id + name = it.name + } + } ?: emptyList() + } + + override fun installExistingPackageAsUser(packageName: String, userId: Int): Int { + return runCatching { + packageManager?.installExistingPackageAsUser(packageName, userId, 0, 0, null) ?: -110 + } + .getOrDefault(-110) + } + + override fun systemServerRequested() = SystemServerService.isRequested() + + override fun startActivityAsUserWithFeature(intent: Intent, userId: Int): Int { + if (!intent.getBooleanExtra("lsp_no_switch_to_user", false)) { + intent.removeExtra("lsp_no_switch_to_user") + val currentUser = activityManager?.currentUser + val parent = userManager?.getProfileParent(userId)?.id ?: userId + if (currentUser != null && currentUser.id != parent) { + if (activityManager?.switchUser(parent) == false) return -1 + val wm = + IWindowManager.Stub.asInterface( + android.os.ServiceManager.getService(Context.WINDOW_SERVICE)) + wm?.lockNow(null) + } + } + return activityManager?.startActivityAsUserWithFeature( + SystemContext.appThread, + "android", + null, + intent, + intent.type, + null, + null, + 0, + 0, + null, + null, + userId) ?: -1 + } + + override fun queryIntentActivitiesAsUser( + intent: Intent, + flags: Int, + userId: Int + ): ParcelableListSlice { + return ParcelableListSlice( + packageManager?.queryIntentActivitiesCompat(intent, intent.type, flags, userId) + ?: emptyList()) + } + + override fun dex2oatFlagsLoaded() = + SystemProperties.get("dalvik.vm.dex2oat-flags").contains("--inline-max-code-units=0") + + override fun setHiddenIcon(hide: Boolean) { + val args = + Bundle().apply { + putString("value", if (hide) "0" else "1") + putString("_user", "0") + } + runCatching { + val provider = + activityManager + ?.getContentProviderExternal("settings", 0, SystemContext.token, null) + ?.provider + provider?.call("android", "settings", "PUT_global", "show_hidden_icon_apps_enabled", args) + } + .onFailure { Log.w(TAG, "setHiddenIcon failed", it) } + } + + override fun getLogs(zipFd: ParcelFileDescriptor) { + FileSystem.getLogs(zipFd) + } + + override fun restartFor(intent: Intent) {} // No-op matching original + + override fun getDenyListPackages() = ConfigCache.getDenyListPackages() + + /** + * Executes Magisk via ProcessBuilder and redirects output directly to the passed + * ParcelFileDescriptor using the /proc/self/fd/ pseudo-filesystem. + */ + override fun flashZip(zipPath: String, outputStream: ParcelFileDescriptor) { + val fdFile = File("/proc/self/fd/${outputStream.fd}") + val processBuilder = + ProcessBuilder("magisk", "--install-module", zipPath) + .redirectOutput(ProcessBuilder.Redirect.appendTo(fdFile)) + + runCatching { + outputStream.use { _ -> + FileOutputStream(fdFile, true).use { fdw -> + val proc = processBuilder.start() + if (proc.waitFor(10, TimeUnit.SECONDS)) { + if (proc.exitValue() == 0) { + fdw.write("- Reboot after 5s\n".toByteArray()) + Thread.sleep(5000) + reboot() + } else { + fdw.write("! Flash failed, exit with ${proc.exitValue()}\n".toByteArray()) + } + } else { + proc.destroy() + fdw.write("! Timeout, abort\n".toByteArray()) + } + } + } + } + .onFailure { Log.e(TAG, "flashZip failed", it) } + } + + override fun clearApplicationProfileData(packageName: String) { + packageManager?.clearApplicationProfileData(packageName) + } + + override fun enableStatusNotification() = ConfigCache.enableStatusNotification + + override fun setEnableStatusNotification(enable: Boolean) { + ConfigCache.enableStatusNotification = enable + // NotificationManager.notifyStatusNotification() handled via observers later + } + + override fun performDexOptMode(packageName: String) = + org.matrix.vector.daemon.utils.performDexOptMode(packageName) + + override fun getDexObfuscate() = ConfigCache.isDexObfuscateEnabled() + + override fun setDexObfuscate(enabled: Boolean) = ConfigCache.setDexObfuscate(enabled) + + override fun getDex2OatWrapperCompatibility() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) Dex2OatServer.compatibility else 0 + + override fun setLogWatchdog(enabled: Boolean) = ConfigCache.setLogWatchdog(enabled) + + override fun isLogWatchdogEnabled() = ConfigCache.isLogWatchdogEnabled() + + override fun setAutoInclude(packageName: String, enabled: Boolean) = + ConfigCache.setAutoInclude(packageName, enabled) + + override fun getAutoInclude(packageName: String) = ConfigCache.getAutoInclude(packageName) +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt new file mode 100644 index 000000000..f2606f123 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt @@ -0,0 +1,200 @@ +package org.matrix.vector.daemon.ipc + +import android.os.Binder +import android.os.Build +import android.os.Bundle +import android.os.ParcelFileDescriptor +import android.os.RemoteException +import android.util.Log +import io.github.libxposed.service.IXposedScopeCallback +import io.github.libxposed.service.IXposedService +import java.io.Serializable +import java.util.Collections +import java.util.WeakHashMap +import java.util.concurrent.ConcurrentHashMap +import org.lsposed.lspd.models.Module +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.data.ConfigCache +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.system.PER_USER_RANGE +import org.matrix.vector.daemon.system.activityManager + +private const val TAG = "VectorModuleService" +private const val AUTHORITY_SUFFIX = ".lsposed" +private const val SEND_BINDER = "send_binder" + +class ModuleService(private val loadedModule: Module) : IXposedService.Stub() { + + companion object { + private val uidSet = ConcurrentHashMap.newKeySet() + private val serviceMap = Collections.synchronizedMap(WeakHashMap()) + + fun uidClear() { + uidSet.clear() + } + + fun uidStarts(uid: Int) { + if (uidSet.add(uid)) { + val module = ConfigCache.getModuleByUid(uid) // Needs impl in ConfigCache + if (module?.file?.legacy == false) { + val service = serviceMap.getOrPut(module) { ModuleService(module) } + service.sendBinder(uid) + } + } + } + + fun uidGone(uid: Int) { + uidSet.remove(uid) + } + } + + /** + * Forges a ContentProvider call to force the module's target app process to receive this Binder + * IPC endpoint without standard Context.bindService() limits. + */ + private fun sendBinder(uid: Int) { + val name = loadedModule.packageName + runCatching { + val userId = uid / PER_USER_RANGE + val authority = name + AUTHORITY_SUFFIX + val provider = + activityManager?.getContentProviderExternal(authority, userId, null, null)?.provider + + if (provider == null) { + Log.d(TAG, "No service provider for $name") + return + } + + val extra = Bundle().apply { putBinder("binder", asBinder()) } + val reply: Bundle? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + provider.call("android", authority, SEND_BINDER, null, extra) + } else { + provider.call("android", SEND_BINDER, null, extra) + } + + if (reply != null) Log.d(TAG, "Sent module binder to $name") + else Log.w(TAG, "Failed to send module binder to $name") + } + .onFailure { Log.w(TAG, "Failed to send module binder for uid $uid", it) } + } + + private fun ensureModule(): Int { + val appId = Binder.getCallingUid() % PER_USER_RANGE + if (loadedModule.appId != appId) { + throw RemoteException( + "Module ${loadedModule.packageName} is not for uid ${Binder.getCallingUid()}") + } + return Binder.getCallingUid() / PER_USER_RANGE + } + + override fun getApiVersion() = ensureModule().let { IXposedService.LIB_API } + + override fun getFrameworkName() = ensureModule().let { BuildConfig.FRAMEWORK_NAME } + + override fun getFrameworkVersion() = ensureModule().let { BuildConfig.VERSION_NAME } + + override fun getFrameworkVersionCode() = ensureModule().let { BuildConfig.VERSION_CODE.toLong() } + + override fun getFrameworkProperties(): Long { + ensureModule() + var prop = IXposedService.PROP_CAP_SYSTEM or IXposedService.PROP_CAP_REMOTE + if (ConfigCache.isDexObfuscateEnabled()) prop = prop or IXposedService.PROP_RT_API_PROTECTION + return prop + } + + override fun getScope(): List { + ensureModule() + return ConfigCache.getModuleScope(loadedModule.packageName)?.map { it.packageName } + ?: emptyList() + } + + override fun requestScope(packages: List, callback: IXposedScopeCallback) { + val userId = ensureModule() + if (!ConfigCache.isScopeRequestBlocked(loadedModule.packageName)) { + packages.forEach { pkg -> + // Handled in Phase 5: NotificationManager.requestModuleScope() + } + } else { + callback.onScopeRequestFailed("Scope request blocked by user configuration") + } + } + + override fun removeScope(packages: List) { + val userId = ensureModule() + packages.forEach { pkg -> + runCatching { ConfigCache.removeModuleScope(loadedModule.packageName, pkg, userId) } + .onFailure { Log.e(TAG, "Error removing scope for $pkg", it) } + } + } + + override fun requestRemotePreferences(group: String): Bundle { + val userId = ensureModule() + return Bundle().apply { + putSerializable( + "map", + ConfigCache.getModulePrefs(loadedModule.packageName, userId, group) as Serializable) + } + } + + override fun updateRemotePreferences(group: String, diff: Bundle) { + val userId = ensureModule() + val values = mutableMapOf() + + diff.getSerializable("delete")?.let { deletes -> + (deletes as Set<*>).forEach { values[it as String] = null } + } + diff.getSerializable("put")?.let { puts -> + (puts as Map<*, *>).forEach { (k, v) -> values[k as String] = v } + } + + runCatching { + ConfigCache.updateModulePrefs(loadedModule.packageName, userId, group, values) + (loadedModule.service as? InjectedModuleService)?.onUpdateRemotePreferences(group, diff) + } + .getOrElse { throw RemoteException(it.message) } + } + + override fun deleteRemotePreferences(group: String) { + ConfigCache.deleteModulePrefs(loadedModule.packageName, ensureModule(), group) + } + + override fun listRemoteFiles(): Array { + val userId = ensureModule() + return runCatching { + FileSystem.resolveModuleDir( + loadedModule.packageName, "files", userId, Binder.getCallingUid()) + .toFile() + .list() ?: emptyArray() + } + .getOrElse { throw RemoteException(it.message) } + } + + override fun openRemoteFile(path: String): ParcelFileDescriptor { + val userId = ensureModule() + FileSystem.ensureModuleFilePath(path) + return runCatching { + val file = + FileSystem.resolveModuleDir( + loadedModule.packageName, "files", userId, Binder.getCallingUid()) + .resolve(path) + .toFile() + ParcelFileDescriptor.open( + file, ParcelFileDescriptor.MODE_CREATE or ParcelFileDescriptor.MODE_READ_WRITE) + } + .getOrElse { throw RemoteException(it.message) } + } + + override fun deleteRemoteFile(path: String): Boolean { + val userId = ensureModule() + FileSystem.ensureModuleFilePath(path) + return runCatching { + FileSystem.resolveModuleDir( + loadedModule.packageName, "files", userId, Binder.getCallingUid()) + .resolve(path) + .toFile() + .delete() + } + .getOrElse { throw RemoteException(it.message) } + } +} 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 new file mode 100644 index 000000000..e043cdcf8 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt @@ -0,0 +1,110 @@ +package org.matrix.vector.daemon.ipc + +import android.os.Build +import android.os.IBinder +import android.os.IServiceCallback +import android.os.Parcel +import android.os.SystemProperties +import android.util.Log +import org.lsposed.lspd.service.ILSPApplicationService +import org.lsposed.lspd.service.ILSPSystemServerService + +private const val TAG = "VectorSystemServer" + +class SystemServerService(private val maxRetry: Int, private val proxyServiceName: String) : + ILSPSystemServerService.Stub(), IBinder.DeathRecipient { + + private var originService: IBinder? = null + private var requestedRetryCount = -maxRetry + + // Hardcoded transaction code from BridgeService + private val BRIDGE_TRANSACTION_CODE = + ('_'.code shl 24) or ('V'.code shl 16) or ('E'.code shl 8) or 'C'.code + + init { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val callback = + object : IServiceCallback.Stub() { + override fun onRegistration(name: String, binder: IBinder?) { + if (name == proxyServiceName && + binder != null && + binder !== this@SystemServerService) { + Log.d(TAG, "Intercepted system service registration: $name") + originService = binder + runCatching { binder.linkToDeath(this@SystemServerService, 0) } + } + } + } + runCatching { + android.os.ServiceManager.getIServiceManager() + .registerForNotifications(proxyServiceName, callback) + } + .onFailure { Log.e(TAG, "Failed to register IServiceCallback", it) } + } + } + + fun putBinderForSystemServer() { + android.os.ServiceManager.addService(proxyServiceName, this) + binderDied() + } + + override fun requestApplicationService( + uid: Int, + pid: Int, + processName: String, + heartBeat: IBinder? + ): ILSPApplicationService? { + requestedRetryCount = 1 + if (uid != 1000 || heartBeat == null || processName != "system") return null + + // Return the ApplicationService singleton if successfully registered + return if (ApplicationService.registerHeartBeat(uid, pid, processName, heartBeat)) { + ApplicationService + } else null + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { + originService?.let { + return it.transact(code, data, reply, flags) + } + + when (code) { + BRIDGE_TRANSACTION_CODE -> { + val uid = data.readInt() + val pid = data.readInt() + val processName = data.readString() + val heartBeat = data.readStrongBinder() + + val service = requestApplicationService(uid, pid, processName, heartBeat) + if (service != null) { + reply?.writeNoException() + reply?.writeStrongBinder(service.asBinder()) + return true + } + return false + } + // Route DEX and OBFUSCATION transactions to ApplicationService + else -> return ApplicationService.onTransact(code, data, reply, flags) + } + } + + override fun binderDied() { + originService?.unlinkToDeath(this, 0) + originService = null + } + + fun maybeRetryInject() { + if (requestedRetryCount < 0) { + Log.w(TAG, "System server injection fails, triggering restart...") + requestedRetryCount++ + val restartTarget = + if (Build.SUPPORTED_64_BIT_ABIS.isNotEmpty() && + Build.SUPPORTED_32_BIT_ABIS.isNotEmpty()) { + "zygote_secondary" + } else { + "zygote" + } + SystemProperties.set("ctl.restart", restartTarget) + } + } +} From 7747f07af9fa25d7307be223c96805a7843eed94 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 25 Mar 2026 23:50:08 +0100 Subject: [PATCH 06/38] Refactor phase 5 --- .../vector/daemon/core/SystemServerBridge.kt | 78 ++++++++++++++ .../matrix/vector/daemon/core/VectorDaemon.kt | 93 ++++++++++++++++ .../vector/daemon/core/VectorService.kt | 102 ++++++++++++++++++ 3 files changed, 273 insertions(+) create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/core/SystemServerBridge.kt create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/SystemServerBridge.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/SystemServerBridge.kt new file mode 100644 index 000000000..6c626f469 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/SystemServerBridge.kt @@ -0,0 +1,78 @@ +package org.matrix.vector.daemon.core + +import android.os.IBinder +import android.os.Parcel +import android.system.Os +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.ipc.ManagerService +import org.matrix.vector.daemon.ipc.SystemServerService + +private const val TAG = "VectorBridge" +private const val TRANSACTION_CODE = + ('_'.code shl 24) or ('V'.code shl 16) or ('E'.code shl 8) or 'C'.code + +object SystemServerBridge { + + fun sendToBridge(binder: IBinder, isRestart: Boolean, systemServerService: SystemServerService) { + CoroutineScope(Dispatchers.IO).launch { + runCatching { + Os.seteuid(0) + + var bridgeService: IBinder? + while (true) { + bridgeService = android.os.ServiceManager.getService("activity") + if (bridgeService?.pingBinder() == true) break + Log.i(TAG, "activity service not ready, waiting 1s...") + delay(1000) + } + + if (isRestart) Log.w(TAG, "System Server restarted...") + + // Setup death recipient to handle system_server crashes + val deathRecipient = + object : IBinder.DeathRecipient { + override fun binderDied() { + Log.w(TAG, "System Server died! Clearing caches and re-injecting...") + bridgeService?.unlinkToDeath(this, 0) + systemServerService.putBinderForSystemServer() + ManagerService.guard = null // ManagerGuard binderDied + sendToBridge(binder, isRestart = true, systemServerService) + } + } + bridgeService?.linkToDeath(deathRecipient, 0) + + // Try sending the Binder payload (up to 3 times) + var success = false + for (i in 0 until 3) { + val data = Parcel.obtain() + val reply = Parcel.obtain() + try { + data.writeInt(1) // ACTION_SEND_BINDER + data.writeStrongBinder(binder) + success = bridgeService?.transact(TRANSACTION_CODE, data, reply, 0) == true + reply.readException() + if (success) break + } finally { + data.recycle() + reply.recycle() + } + Log.w(TAG, "No response from bridge, retrying...") + delay(1000) + } + + if (success) Log.i(TAG, "Successfully injected Vector into system_server") + else { + Log.e(TAG, "Failed to inject Vector into system_server") + systemServerService.maybeRetryInject() + } + } + .onFailure { Log.e(TAG, "Error during System Server bridging", it) } + .also { if (!BuildConfig.DEBUG) runCatching { Os.seteuid(1000) } } + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt new file mode 100644 index 000000000..433acc00b --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt @@ -0,0 +1,93 @@ +package org.matrix.vector.daemon.core + +import android.app.ActivityThread +import android.content.Context +import android.ddm.DdmHandleAppName +import android.os.Looper +import android.os.Process +import android.util.Log +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.data.ConfigCache +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.env.Dex2OatServer +import org.matrix.vector.daemon.env.LogcatMonitor +import org.matrix.vector.daemon.ipc.ManagerService +import org.matrix.vector.daemon.ipc.SystemServerService +import org.matrix.vector.daemon.utils.applyNotificationWorkaround + +private const val TAG = "VectorDaemon" + +object VectorDaemon { + var isLateInject = false + var proxyServiceName = "serial" + + @JvmStatic + fun main(args: Array) { + if (!FileSystem.tryLock()) kotlin.system.exitProcess(0) + + var systemServerMaxRetry = 1 + for (arg in args) { + if (arg.startsWith("--system-server-max-retry=")) { + systemServerMaxRetry = arg.substringAfter('=').toIntOrNull() ?: 1 + } else if (arg == "--late-inject") { + isLateInject = true + proxyServiceName = "serial_vector" + } + } + + Log.i(TAG, "Vector daemon started: lateInject=$isLateInject, proxy=$proxyServiceName") + Log.i(TAG, "Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") + + Thread.setDefaultUncaughtExceptionHandler { _, e -> + Log.e(TAG, "Uncaught exception in Daemon", e) + kotlin.system.exitProcess(1) + } + + // 1. Start Environmental Daemons + LogcatMonitor.start() + if (ConfigCache.isLogWatchdogEnabled()) + LogcatMonitor.enableWatchdog() // Needs impl in LogcatMonitor + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + Dex2OatServer.start() + } + + // 2. Setup Main Looper & System Services + Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND) + Looper.prepareMainLooper() + + val systemServerService = SystemServerService(systemServerMaxRetry, proxyServiceName) + systemServerService.putBinderForSystemServer() + + // Initializes system frameworks inside the daemon process + ActivityThread.systemMain() + DdmHandleAppName.setAppName("org.matrix.vector.daemon", 0) + + // 3. Wait for Android Core Services + waitForSystemService("package") + waitForSystemService("activity") + waitForSystemService(Context.USER_SERVICE) + waitForSystemService(Context.APP_OPS_SERVICE) + + applyNotificationWorkaround() + + // 4. Inject Vector into system_server + SystemServerBridge.sendToBridge(VectorService, isRestart = false, systemServerService) + + if (!ManagerService.isVerboseLog()) { + LogcatMonitor.stopVerbose() // Needs impl in LogcatMonitor + } + + Looper.loop() + throw RuntimeException("Main thread loop unexpectedly exited") + } + + private fun waitForSystemService(name: String) = runBlocking { + while (android.os.ServiceManager.getService(name) == null) { + Log.i(TAG, "Waiting for system service: $name") + delay(1000) + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt new file mode 100644 index 000000000..96464ae20 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt @@ -0,0 +1,102 @@ +package org.matrix.vector.daemon.core + +import android.content.Intent +import android.os.Binder +import android.os.IBinder +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.lsposed.lspd.ILSPApplicationService +import org.lsposed.lspd.ILSPosedService +import org.matrix.vector.daemon.data.ConfigCache +import org.matrix.vector.daemon.ipc.ApplicationService +import org.matrix.vector.daemon.ipc.ManagerService +import org.matrix.vector.daemon.system.SystemContext + +private const val TAG = "VectorService" + +object VectorService : ILSPosedService.Stub() { + + private val ioScope = CoroutineScope(Dispatchers.IO) + private var bootCompleted = false + + override fun dispatchSystemServerContext( + appThread: IBinder, + activityToken: IBinder, + api: String + ) { + Log.d(TAG, "Received System Server Context (API: $api)") + + SystemContext.appThread = android.app.IApplicationThread.Stub.asInterface(appThread) + SystemContext.token = activityToken + ConfigCache.setApi(api) // Needs impl in ConfigCache + + // Initialize OS Observers using Coroutines for the dispatch blocks + registerReceivers() + + if (VectorDaemon.isLateInject) { + Log.i(TAG, "Late injection detected. Forcing boot completed event.") + dispatchBootCompleted() + } + } + + override fun requestApplicationService( + uid: Int, + pid: Int, + processName: String, + heartBeat: IBinder + ): ILSPApplicationService? { + if (Binder.getCallingUid() != 1000) { + Log.w(TAG, "Unauthorized requestApplicationService call") + return null + } + if (ApplicationService.hasRegister(uid, pid)) return null + + // ConfigCache ProcessScope check omitted for brevity, handled in Phase 6 + return if (ApplicationService.registerHeartBeat(uid, pid, processName, heartBeat)) { + ApplicationService + } else null + } + + override fun preStartManager() = ManagerService.preStartManager() + + override fun setManagerEnabled(enabled: Boolean) = true // Omitted specific toggle logic + + private fun registerReceivers() { + // Implementation logic for ActivityManagerService.registerReceiver is moved to Phase 6 + // SystemExtensions + Log.d(TAG, "Registered all OS Receivers and UID Observers") + } + + private fun dispatchBootCompleted() { + bootCompleted = true + if (ConfigCache.enableStatusNotification) { + // NotificationManager.notifyStatusNotification() // Needs impl in Phase 6 + } + } + + private fun dispatchPackageChanged(intent: Intent) { + ioScope.launch { + val uid = intent.getIntExtra(Intent.EXTRA_UID, -1) + val action = intent.action ?: return@launch + + Log.d(TAG, "Package changed: action=$action, uid=$uid") + + // Logic mimicking the massive switch statement in LSPosedService.java + when (action) { + Intent.ACTION_PACKAGE_FULLY_REMOVED -> { + // ConfigCache.removeModule(pkg) + } + Intent.ACTION_PACKAGE_ADDED, + Intent.ACTION_PACKAGE_CHANGED -> { + // ConfigCache.updateModuleApkPath(pkg) + } + Intent.ACTION_UID_REMOVED -> { + ConfigCache.requestCacheUpdate() + } + } + // Broadcast intent back to manager app... + } + } +} From 480e4f35a3ebd4600adc5242c868031f03d4de43 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 00:27:31 +0100 Subject: [PATCH 07/38] Fix compilation errors --- .../vector/daemon/core/SystemServerBridge.kt | 7 +- .../matrix/vector/daemon/core/VectorDaemon.kt | 4 +- .../vector/daemon/core/VectorService.kt | 6 +- .../matrix/vector/daemon/data/ConfigCache.kt | 208 ++++++++++++++++++ .../matrix/vector/daemon/data/FileSystem.kt | 64 +++++- .../matrix/vector/daemon/env/Dex2OatServer.kt | 6 +- .../matrix/vector/daemon/env/LogcatMonitor.kt | 17 ++ .../vector/daemon/ipc/ApplicationService.kt | 2 +- .../vector/daemon/ipc/ManagerService.kt | 47 ++-- .../matrix/vector/daemon/ipc/ModuleService.kt | 3 +- .../vector/daemon/ipc/SystemServerService.kt | 17 +- .../daemon/system/NotificationManager.kt | 144 ++++++++++++ .../vector/daemon/system/SystemBinders.kt | 8 + .../vector/daemon/system/SystemExtensions.kt | 68 ++++++ .../vector/daemon/utils/ObfuscationManager.kt | 9 + 15 files changed, 571 insertions(+), 39 deletions(-) create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/utils/ObfuscationManager.kt diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/SystemServerBridge.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/SystemServerBridge.kt index 6c626f469..3192d8cd2 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/SystemServerBridge.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/SystemServerBridge.kt @@ -18,6 +18,7 @@ private const val TRANSACTION_CODE = object SystemServerBridge { + @Suppress("DEPRECATION") fun sendToBridge(binder: IBinder, isRestart: Boolean, systemServerService: SystemServerService) { CoroutineScope(Dispatchers.IO).launch { runCatching { @@ -38,13 +39,13 @@ object SystemServerBridge { object : IBinder.DeathRecipient { override fun binderDied() { Log.w(TAG, "System Server died! Clearing caches and re-injecting...") - bridgeService?.unlinkToDeath(this, 0) + bridgeService.unlinkToDeath(this, 0) systemServerService.putBinderForSystemServer() ManagerService.guard = null // ManagerGuard binderDied sendToBridge(binder, isRestart = true, systemServerService) } } - bridgeService?.linkToDeath(deathRecipient, 0) + bridgeService.linkToDeath(deathRecipient, 0) // Try sending the Binder payload (up to 3 times) var success = false @@ -54,7 +55,7 @@ object SystemServerBridge { try { data.writeInt(1) // ACTION_SEND_BINDER data.writeStrongBinder(binder) - success = bridgeService?.transact(TRANSACTION_CODE, data, reply, 0) == true + success = bridgeService.transact(TRANSACTION_CODE, data, reply, 0) == true reply.readException() if (success) break } finally { diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt index 433acc00b..dc01fff93 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt @@ -56,6 +56,7 @@ object VectorDaemon { // 2. Setup Main Looper & System Services Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND) + @Suppress("DEPRECATION") Looper.prepareMainLooper() val systemServerService = SystemServerService(systemServerMaxRetry, proxyServiceName) @@ -74,7 +75,8 @@ object VectorDaemon { applyNotificationWorkaround() // 4. Inject Vector into system_server - SystemServerBridge.sendToBridge(VectorService, isRestart = false, systemServerService) + SystemServerBridge.sendToBridge( + VectorService.asBinder(), isRestart = false, systemServerService) if (!ManagerService.isVerboseLog()) { LogcatMonitor.stopVerbose() // Needs impl in LogcatMonitor diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt index 96464ae20..e7ebe9373 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt @@ -7,8 +7,8 @@ import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.lsposed.lspd.ILSPApplicationService -import org.lsposed.lspd.ILSPosedService +import org.lsposed.lspd.service.ILSPApplicationService +import org.lsposed.lspd.service.ILSPosedService import org.matrix.vector.daemon.data.ConfigCache import org.matrix.vector.daemon.ipc.ApplicationService import org.matrix.vector.daemon.ipc.ManagerService @@ -30,7 +30,7 @@ object VectorService : ILSPosedService.Stub() { SystemContext.appThread = android.app.IApplicationThread.Stub.asInterface(appThread) SystemContext.token = activityToken - ConfigCache.setApi(api) // Needs impl in ConfigCache + ConfigCache.api = api // Initialize OS Observers using Coroutines for the dispatch blocks registerReceivers() diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt index 7f3a2309a..1b644a65c 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt @@ -1,5 +1,6 @@ package org.matrix.vector.daemon.data +import android.content.ContentValues import android.util.Log import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.CoroutineScope @@ -7,7 +8,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch +import org.lsposed.lspd.models.Application import org.lsposed.lspd.models.Module +import org.matrix.vector.daemon.BuildConfig import org.matrix.vector.daemon.system.MATCH_ALL_FLAGS import org.matrix.vector.daemon.system.PER_USER_RANGE import org.matrix.vector.daemon.system.fetchProcesses @@ -21,6 +24,9 @@ private const val TAG = "VectorConfigCache" data class ProcessScope(val processName: String, val uid: Int) object ConfigCache { + @Volatile var api: String = "(???)" + @Volatile var enableStatusNotification = true + val dbHelper = Database() // Thread-safe maps for IPC readers @@ -160,4 +166,206 @@ object ConfigCache { fun getModulesForProcess(processName: String, uid: Int): List { return cachedScopes[ProcessScope(processName, uid)] ?: emptyList() } + + // --- Preferences & Settings --- + fun isDexObfuscateEnabled(): Boolean = + getModulePrefs("lspd", 0, "config")["enable_dex_obfuscate"] as? Boolean ?: true + + fun setDexObfuscate(enabled: Boolean) = + updateModulePref("lspd", 0, "config", "enable_dex_obfuscate", enabled) + + fun isLogWatchdogEnabled(): Boolean = + getModulePrefs("lspd", 0, "config")["enable_log_watchdog"] as? Boolean ?: true + + fun setLogWatchdog(enabled: Boolean) = + updateModulePref("lspd", 0, "config", "enable_log_watchdog", enabled) + + // --- Modules & Scope DB Operations --- + fun getEnabledModules(): List = cachedModules.keys.toList() + + fun enableModule(packageName: String): Boolean { + if (packageName == "lspd") return false + val values = ContentValues().apply { put("enabled", 1) } + val changed = + dbHelper.writableDatabase.update( + "modules", values, "module_pkg_name = ?", arrayOf(packageName)) > 0 + if (changed) requestCacheUpdate() + return changed + } + + fun disableModule(packageName: String): Boolean { + if (packageName == "lspd") return false + val values = ContentValues().apply { put("enabled", 0) } + val changed = + dbHelper.writableDatabase.update( + "modules", values, "module_pkg_name = ?", arrayOf(packageName)) > 0 + if (changed) requestCacheUpdate() + return changed + } + + fun getModuleScope(packageName: String): MutableList? { + if (packageName == "lspd") return null + val result = mutableListOf() + dbHelper.readableDatabase + .query( + "scope INNER JOIN modules ON scope.mid = modules.mid", + arrayOf("app_pkg_name", "user_id"), + "modules.module_pkg_name = ?", + arrayOf(packageName), + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + result.add( + Application().apply { + this.packageName = cursor.getString(0) + this.userId = cursor.getInt(1) + }) + } + } + return result + } + + fun setModuleScope(packageName: String, scope: MutableList): Boolean { + enableModule(packageName) + val db = dbHelper.writableDatabase + db.beginTransaction() + try { + val mid = + db.compileStatement("SELECT mid FROM modules WHERE module_pkg_name = ?") + .apply { bindString(1, packageName) } + .simpleQueryForLong() + db.delete("scope", "mid = ?", arrayOf(mid.toString())) + + val values = ContentValues().apply { put("mid", mid) } + for (app in scope) { + if (app.packageName == "system" && app.userId != 0) continue + values.put("app_pkg_name", app.packageName) + values.put("user_id", app.userId) + db.insertWithOnConflict( + "scope", null, values, android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE) + } + db.setTransactionSuccessful() + } catch (e: Exception) { + Log.e(TAG, "Failed to set scope", e) + return false + } finally { + db.endTransaction() + } + requestCacheUpdate() + return true + } + + // --- Configs Table Operations --- + fun getModulePrefs(packageName: String, userId: Int, group: String): Map { + val result = mutableMapOf() + dbHelper.readableDatabase + .query( + "configs", + arrayOf("`key`", "data"), + "module_pkg_name = ? AND user_id = ? AND `group` = ?", + arrayOf(packageName, userId.toString(), group), + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + val key = cursor.getString(0) + val blob = cursor.getBlob(1) + val obj = org.apache.commons.lang3.SerializationUtilsX.deserialize(blob) + if (obj != null) result[key] = obj + } + } + return result + } + + fun updateModulePref(moduleName: String, userId: Int, group: String, key: String, value: Any?) { + updateModulePrefs(moduleName, userId, group, mapOf(key to value)) + } + + fun updateModulePrefs(moduleName: String, userId: Int, group: String, diff: Map) { + val db = dbHelper.writableDatabase + db.beginTransaction() + try { + for ((key, value) in diff) { + if (value is java.io.Serializable) { + val values = + ContentValues().apply { + put("`group`", group) + put("`key`", key) + put("data", org.apache.commons.lang3.SerializationUtilsX.serialize(value)) + put("module_pkg_name", moduleName) + put("user_id", userId.toString()) + } + db.insertWithOnConflict( + "configs", null, values, android.database.sqlite.SQLiteDatabase.CONFLICT_REPLACE) + } else { + db.delete( + "configs", + "module_pkg_name=? AND user_id=? AND `group`=? AND `key`=?", + arrayOf(moduleName, userId.toString(), group, key)) + } + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + fun deleteModulePrefs(moduleName: String, userId: Int, group: String) { + dbHelper.writableDatabase.delete( + "configs", + "module_pkg_name=? AND user_id=? AND `group`=?", + arrayOf(moduleName, userId.toString(), group)) + } + + // --- Helpers --- + fun isManager(uid: Int): Boolean = uid == BuildConfig.MANAGER_INJECTED_UID + + fun getModuleByUid(uid: Int): Module? = + cachedModules.values.firstOrNull { it.appId == uid % PER_USER_RANGE } + + fun isScopeRequestBlocked(pkg: String): Boolean = + (getModulePrefs("lspd", 0, "config")["scope_request_blocked"] as? Set<*>)?.contains(pkg) == + true + + fun getDenyListPackages(): List = + emptyList() // Needs Magisk DB parsing logic if Api == "Zygisk" + + fun getAutoInclude(pkg: String): Boolean = false // Query modules table for auto_include flag + + fun setAutoInclude(pkg: String, enabled: Boolean): Boolean = false + + // Generates application info for system server since it doesn't have an APK + fun getModulesForSystemServer(): List { + val result = mutableListOf() + // Fetch from DB where scope app_pkg_name = 'system', populate Fake ApplicationInfo, and return. + // Omitted DB boilerplate for brevity. + return result + } + + fun getPrefsPath(packageName: String, uid: Int): String { + val userId = uid / PER_USER_RANGE + val path = + FileSystem.basePath.resolve( + "misc/prefs${if (userId == 0) "" else userId.toString()}/$packageName") + // Apply Os.chown to path here + return path.toString() + } + + fun removeModuleScope(packageName: String, scopePackageName: String, userId: Int): Boolean { + if (packageName == "lspd" || (scopePackageName == "system" && userId != 0)) return false + val db = dbHelper.writableDatabase + val mid = + db.compileStatement("SELECT mid FROM modules WHERE module_pkg_name = ?") + .apply { bindString(1, packageName) } + .simpleQueryForLong() + db.delete( + "scope", + "mid = ? AND app_pkg_name = ? AND user_id = ?", + arrayOf(mid.toString(), scopePackageName, userId.toString())) + requestCacheUpdate() + return true + } } 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 bc42402ac..6eff9b3ec 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 @@ -2,7 +2,9 @@ package org.matrix.vector.daemon.data import android.content.res.AssetManager import android.content.res.Resources +import android.os.ParcelFileDescriptor import android.os.Process +import android.os.RemoteException import android.os.SELinux import android.os.SharedMemory import android.system.ErrnoException @@ -11,6 +13,7 @@ import android.system.OsConstants import android.util.Log import hidden.HiddenApiBridge import java.io.File +import java.io.FileInputStream import java.io.InputStream import java.nio.channels.Channels import java.nio.channels.FileChannel @@ -20,7 +23,9 @@ import java.nio.file.Path import java.nio.file.Paths import java.nio.file.StandardOpenOption import java.nio.file.attribute.PosixFilePermissions +import java.util.zip.ZipEntry import java.util.zip.ZipFile +import java.util.zip.ZipOutputStream import kotlin.io.path.exists import kotlin.io.path.isDirectory import org.lsposed.lspd.models.PreLoadedApk @@ -39,6 +44,8 @@ object FileSystem { val dbPath: File = configDirPath.resolve("modules_config.db").toFile() val magiskDbPath = File("/data/adb/magisk.db") + @Volatile private var preloadDex: SharedMemory? = null + private val lockPath: Path = basePath.resolve("lock") private var fileLock: FileLock? = null private var lockChannel: FileChannel? = null @@ -98,7 +105,7 @@ object FileSystem { * access strings/drawables without a real application context. */ val resources: Resources by lazy { - val am = AssetManager::class.java.newInstance() + val am = AssetManager::class.java.getDeclaredConstructor().newInstance() val addAssetPath = AssetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java).apply { isAccessible = true @@ -231,4 +238,59 @@ object FileSystem { createLogDirPath() return logDirPath.resolve("kmsg.log").toFile() } + + @Synchronized + fun getPreloadDex(obfuscate: Boolean): SharedMemory? { + if (preloadDex == null) { + runCatching { + FileInputStream("framework/lspd.dex").use { preloadDex = readDex(it, obfuscate) } + } + .onFailure { Log.e(TAG, "Failed to load framework dex", it) } + } + return preloadDex + } + + fun ensureModuleFilePath(path: String?) { + if (path == null || path.contains(File.separatorChar) || path == "." || path == "..") { + throw RemoteException("Invalid path: $path") + } + } + + fun resolveModuleDir(packageName: String, dir: String, userId: Int, uid: Int): Path { + val path = modulePath.resolve(userId.toString()).resolve(packageName).resolve(dir).normalize() + path.toFile().mkdirs() + + if (SELinux.getFileContext(path.toString()) != "u:object_r:xposed_data:s0") { + runCatching { + setSelinuxContextRecursive(path, "u:object_r:xposed_data:s0") + if (uid != -1) Os.chown(path.toString(), uid, uid) + Os.chmod(path.toString(), 0x1ed) // 0755 + } + .onFailure { throw RemoteException("Failed to set SELinux context: ${it.message}") } + } + return path + } + + fun getLogs(zipFd: ParcelFileDescriptor) { + runCatching { + ZipOutputStream(java.io.FileOutputStream(zipFd.fileDescriptor)).use { os -> + os.setComment("Vector Daemon Logs") + os.setLevel(java.util.zip.Deflater.BEST_COMPRESSION) + + fun addFile(name: String, file: File) { + if (!file.exists() || !file.isFile) return + os.putNextEntry(ZipEntry(name)) + file.inputStream().use { it.copyTo(os) } + os.closeEntry() + } + + addFile("modules_config.db", dbPath) + addFile("props.txt", getPropsPath()) + addFile("kmsg.log", getKmsgPath()) + // Omitted full directory walks for brevity, but you can use File.walk() here. + } + } + .onFailure { Log.e(TAG, "Failed to export logs", it) } + .also { runCatching { zipFd.close() } } + } } 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 0540c240d..a7dabbaab 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 @@ -136,11 +136,7 @@ object Dex2OatServer { val header = ByteArray(5) if (fis.read(header) != 5) return // Verify ELF Magic: 0x7F 'E' 'L' 'F' - if (header[0] != 0x7F.toByte() || - header[1] != 'E'.toByte() || - header[2] != 'L'.toByte() || - header[3] != 'F'.toByte()) - return + if (header[0] != 0x7F.toByte() || header[1] != 'E'.code.toByte() || header[2] != 'L'.code.toByte() || header[3] != 'F'.code.toByte()) return val is32Bit = header[4] == 1.toByte() val is64Bit = header[4] == 2.toByte() diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt index 3569f7cdc..5d098c50a 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt @@ -136,4 +136,21 @@ object LogcatMonitor { } .onFailure { Log.w(TAG, "checkFd failed for $fd", it) } } + + fun startVerbose() = Log.i(TAG, "!!start_verbose!!") + + fun stopVerbose() = Log.i(TAG, "!!stop_verbose!!") + + fun enableWatchdog() = Log.i(TAG, "!!start_watchdog!!") + + fun disableWatchdog() = Log.i(TAG, "!!stop_watchdog!!") + + fun refresh(isVerboseLog: Boolean) { + Log.i(TAG, if (isVerboseLog) "!!refresh_verbose!!" else "!!refresh_modules!!") + } + + fun checkLogFile() { + if (modulesFd == -1) refresh(false) + if (verboseFd == -1) refresh(true) + } } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt index 0040475ed..e75b1e416 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt @@ -43,7 +43,7 @@ object ApplicationService : ILSPApplicationService.Stub() { // TODO: FileSystem needs getPreloadDex() implemented val shm = FileSystem.getPreloadDex(ConfigCache.isDexObfuscateEnabled()) ?: return false reply?.writeNoException() - shm.writeToParcel(reply, 0) + reply?.let { shm.writeToParcel(it, 0) } reply?.writeLong(shm.size.toLong()) return true } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt index 7a3a1a340..7a1af6d45 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt @@ -3,7 +3,6 @@ package org.matrix.vector.daemon.ipc import android.content.ComponentName import android.content.Context import android.content.Intent -import android.content.ServiceConnection import android.content.pm.PackageInfo import android.content.pm.ResolveInfo import android.os.Build @@ -34,21 +33,19 @@ private const val TAG = "VectorManagerService" object ManagerService : ILSPManagerService.Stub() { - @Volatile var isVerboseLog = false + @Volatile var _isVerboseLog = false @Volatile private var managerPid = -1 @Volatile private var pendingManager = false @Volatile private var isEnabled = true var guard: ManagerGuard? = null - private set + internal set class ManagerGuard(private val binder: IBinder, val pid: Int, val uid: Int) : IBinder.DeathRecipient { private val connection = - object : ServiceConnection { - override fun onServiceConnected(name: ComponentName?, service: IBinder?) {} - - override fun onServiceDisconnected(name: ComponentName?) {} + object : android.app.IServiceConnection.Stub() { + override fun connected(name: ComponentName?, service: IBinder?, dead: Boolean) {} } init { @@ -63,15 +60,27 @@ object ManagerService : ILSPManagerService.Stub() { ComponentName.unflattenFromString( "com.miui.securitycore/com.miui.xspace.service.XSpaceService") } - activityManager?.bindService( - null, - null, - intent, - intent.type, - connection, - Context.BIND_AUTO_CREATE.toLong(), - "android", - 0) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + activityManager?.bindService( + SystemContext.appThread, + SystemContext.token, + intent, + intent.type, + connection, + Context.BIND_AUTO_CREATE.toLong(), + "android", + 0) + } else { + activityManager?.bindService( + SystemContext.appThread, + SystemContext.token, + intent, + intent.type, + connection, + Context.BIND_AUTO_CREATE, + "android", + 0) + } } } .onFailure { @@ -145,10 +154,10 @@ object ManagerService : ILSPManagerService.Stub() { override fun getModuleScope(packageName: String) = ConfigCache.getModuleScope(packageName) - override fun isVerboseLog() = isVerboseLog || BuildConfig.DEBUG + override fun isVerboseLog() = _isVerboseLog || BuildConfig.DEBUG override fun setVerboseLog(enabled: Boolean) { - isVerboseLog = enabled + _isVerboseLog = enabled if (enabled) LogcatMonitor.startVerbose() else LogcatMonitor.stopVerbose() ConfigCache.updateModulePref("lspd", 0, "config", "enable_verbose_log", enabled) } @@ -206,7 +215,7 @@ object ManagerService : ILSPManagerService.Stub() { .getOrDefault(-110) } - override fun systemServerRequested() = SystemServerService.isRequested() + override fun systemServerRequested() = SystemServerService.systemServerRequested() override fun startActivityAsUserWithFeature(intent: Intent, userId: Int): Int { if (!intent.getBooleanExtra("lsp_no_switch_to_user", false)) { diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt index f2606f123..c2f641346 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt @@ -94,7 +94,7 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() { override fun getFrameworkVersion() = ensureModule().let { BuildConfig.VERSION_NAME } - override fun getFrameworkVersionCode() = ensureModule().let { BuildConfig.VERSION_CODE.toLong() } + override fun getFrameworkVersionCode() = ensureModule().let { BuildConfig.VERSION_CODE } override fun getFrameworkProperties(): Long { ensureModule() @@ -137,6 +137,7 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() { } } + @Suppress("DEPRECATION") override fun updateRemotePreferences(group: String, diff: Bundle) { val userId = ensureModule() val values = mutableMapOf() 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 e043cdcf8..7a5c4012d 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 @@ -8,6 +8,7 @@ import android.os.SystemProperties import android.util.Log import org.lsposed.lspd.service.ILSPApplicationService import org.lsposed.lspd.service.ILSPSystemServerService +import org.matrix.vector.daemon.system.getSystemServiceManager private const val TAG = "VectorSystemServer" @@ -21,7 +22,14 @@ class SystemServerService(private val maxRetry: Int, private val proxyServiceNam private val BRIDGE_TRANSACTION_CODE = ('_'.code shl 24) or ('V'.code shl 16) or ('E'.code shl 8) or 'C'.code + companion object { + @Volatile var requestedRetryCount = 0 + + fun systemServerRequested() = requestedRetryCount > 0 + } + init { + requestedRetryCount = -maxRetry if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val callback = object : IServiceCallback.Stub() { @@ -34,11 +42,10 @@ class SystemServerService(private val maxRetry: Int, private val proxyServiceNam runCatching { binder.linkToDeath(this@SystemServerService, 0) } } } + + override fun asBinder(): IBinder = this } - runCatching { - android.os.ServiceManager.getIServiceManager() - .registerForNotifications(proxyServiceName, callback) - } + runCatching { getSystemServiceManager().registerForNotifications(proxyServiceName, callback) } .onFailure { Log.e(TAG, "Failed to register IServiceCallback", it) } } } @@ -72,7 +79,7 @@ class SystemServerService(private val maxRetry: Int, private val proxyServiceNam BRIDGE_TRANSACTION_CODE -> { val uid = data.readInt() val pid = data.readInt() - val processName = data.readString() + val processName = data.readString() ?: "" val heartBeat = data.readStrongBinder() val service = requestApplicationService(uid, pid, processName, heartBeat) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt new file mode 100644 index 000000000..7aa9c7e83 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt @@ -0,0 +1,144 @@ +package org.matrix.vector.daemon.system + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.util.Log +import io.github.libxposed.service.IXposedScopeCallback +import java.util.UUID +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.utils.FakeContext + +private const val TAG = "VectorNotifManager" +private const val STATUS_CHANNEL_ID = "lsposed_status" +private const val UPDATED_CHANNEL_ID = "lsposed_module_updated" +private const val SCOPE_CHANNEL_ID = "lsposed_module_scope" +private const val STATUS_NOTIF_ID = 2000 + +object NotificationManager { + val openManagerAction = UUID.randomUUID().toString() + val moduleScopeAction = UUID.randomUUID().toString() + + private val nm: android.app.INotificationManager? by + SystemService( + Context.NOTIFICATION_SERVICE, android.app.INotificationManager.Stub::asInterface) + private val opPkg = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) "android" else "com.android.settings" + + private fun createChannels() { + val context = FakeContext() + val list = + listOf( + NotificationChannel( + STATUS_CHANNEL_ID, + "Vector Status", + android.app.NotificationManager.IMPORTANCE_MIN) + .apply { setShowBadge(false) }, + NotificationChannel( + UPDATED_CHANNEL_ID, + "Module Updated", + android.app.NotificationManager.IMPORTANCE_HIGH) + .apply { setShowBadge(false) }, + NotificationChannel( + SCOPE_CHANNEL_ID, + "Scope Request", + android.app.NotificationManager.IMPORTANCE_HIGH) + .apply { setShowBadge(false) }) + + runCatching { + // ParceledListSlice is required for system_server IPC + nm?.createNotificationChannelsForPackage( + "android", 1000, android.content.pm.ParceledListSlice(list)) + } + .onFailure { Log.e(TAG, "Failed to create notification channels", it) } + } + + fun notifyStatusNotification() { + val context = FakeContext() + val intent = Intent(openManagerAction).apply { setPackage("android") } + val pi = + PendingIntent.getBroadcast( + context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + + val notif = + Notification.Builder(context, STATUS_CHANNEL_ID) + .setContentTitle("Vector is active") + .setContentText("The daemon is running.") + .setSmallIcon(android.R.drawable.ic_dialog_info) // Fallback icon + .setContentIntent(pi) + .setVisibility(Notification.VISIBILITY_SECRET) + .setOngoing(true) + .build() + .apply { extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME) } + + createChannels() + runCatching { + nm?.enqueueNotificationWithTag("android", opPkg, null, STATUS_NOTIF_ID, notif, 0) + } + } + + fun cancelStatusNotification() { + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + nm?.cancelNotificationWithTag("android", "android", null, STATUS_NOTIF_ID, 0) + } else { + nm?.cancelNotificationWithTag("android", null, STATUS_NOTIF_ID, 0) + } + } + } + + fun requestModuleScope( + modulePkg: String, + moduleUserId: Int, + scopePkg: String, + callback: IXposedScopeCallback + ) { + val context = FakeContext() + val intent = + Intent(moduleScopeAction).apply { + setPackage("android") + data = + Uri.Builder() + .scheme("module") + .encodedAuthority("$modulePkg:$moduleUserId") + .encodedPath(scopePkg) + .appendQueryParameter("action", "approve") + .build() + putExtras(Bundle().apply { putBinder("callback", callback.asBinder()) }) + } + val pi = + PendingIntent.getBroadcast( + context, 4, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + + val notif = + Notification.Builder(context, SCOPE_CHANNEL_ID) + .setContentTitle("Scope Request") + .setContentText("Module $modulePkg requested injection into $scopePkg.") + .setSmallIcon(android.R.drawable.ic_dialog_alert) + .addAction(Notification.Action.Builder(null, "Approve", pi).build()) + .setAutoCancel(true) + .build() + .apply { extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME) } + + createChannels() + runCatching { + nm?.enqueueNotificationWithTag("android", opPkg, modulePkg, modulePkg.hashCode(), notif, 0) + } + } + + fun cancelNotification(channel: String, modulePkg: String, moduleUserId: Int) { + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + nm?.cancelNotificationWithTag("android", "android", modulePkg, modulePkg.hashCode(), 0) + } else { + nm?.cancelNotificationWithTag("android", modulePkg, modulePkg.hashCode(), 0) + } + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemBinders.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemBinders.kt index ce5eac996..dd2a66439 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemBinders.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemBinders.kt @@ -5,8 +5,11 @@ import android.content.Context import android.content.pm.IPackageManager import android.os.IBinder import android.os.IPowerManager +import android.os.IServiceManager import android.os.IUserManager import android.os.RemoteException +import com.android.internal.os.BinderInternal +import hidden.HiddenApiBridge import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty @@ -57,3 +60,8 @@ object SystemContext { @Volatile var appThread: android.app.IApplicationThread? = null @Volatile var token: IBinder? = null } + +fun getSystemServiceManager(): IServiceManager { + return IServiceManager.Stub.asInterface( + HiddenApiBridge.Binder_allowBlocking(BinderInternal.getContextObject())) +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt index 0d59985b8..e9a9f2c30 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt @@ -1,10 +1,15 @@ package org.matrix.vector.daemon.system +import android.content.Intent import android.content.pm.IPackageManager import android.content.pm.PackageInfo import android.content.pm.PackageManager +import android.content.pm.ResolveInfo import android.content.pm.ServiceInfo import android.os.Build +import android.util.Log +import org.matrix.vector.daemon.system.userManager +import org.matrix.vector.daemon.utils.getRealUsers private const val TAG = "VectorSystem" const val PER_USER_RANGE = 100000 @@ -96,3 +101,66 @@ fun PackageInfo.fetchProcesses(): Set { return processNames } + +fun IPackageManager.queryIntentActivitiesCompat( + intent: Intent, + resolvedType: String?, + flags: Int, + userId: Int +): List { + return runCatching { + val slice = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + queryIntentActivities(intent, resolvedType, flags.toLong(), userId) + } else { + queryIntentActivities(intent, resolvedType, flags, userId) + } + slice?.list ?: emptyList() + } + .getOrElse { + Log.e(TAG, "queryIntentActivitiesCompat failed", it) + emptyList() + } +} + +fun IPackageManager.clearApplicationProfileDataCompat(packageName: String) { + runCatching { clearApplicationProfileData(packageName) } +} + +fun IPackageManager.getInstalledPackagesForAllUsers( + flags: Int, + filterNoProcess: Boolean +): List { + val result = mutableListOf() + val users = + userManager?.getRealUsers() + ?: emptyList() + + for (user in users) { + val infos = + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getInstalledPackages(flags.toLong(), user.id) + } else { + getInstalledPackages(flags, user.id) + } + } + .getOrNull() + ?.list ?: continue + + result.addAll( + infos.filter { + it.applicationInfo != null && it.applicationInfo!!.uid / PER_USER_RANGE == user.id + }) + } + + if (filterNoProcess) { + return result.filter { + getPackageInfoWithComponents( + it.packageName, MATCH_ALL_FLAGS, it.applicationInfo!!.uid / PER_USER_RANGE) + ?.fetchProcesses() + ?.isNotEmpty() == true + } + } + return result +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/ObfuscationManager.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/ObfuscationManager.kt new file mode 100644 index 000000000..ca6400bd4 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/ObfuscationManager.kt @@ -0,0 +1,9 @@ +package org.matrix.vector.daemon.utils + +import android.os.SharedMemory + +object ObfuscationManager { + @JvmStatic external fun obfuscateDex(memory: SharedMemory): SharedMemory + + @JvmStatic external fun getSignatures(): Map +} From ca26afbb247b33a367ec6d78742847d53e0f219e Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 00:49:21 +0100 Subject: [PATCH 08/38] Refactor phase 6 --- .../vector/daemon/core/SystemServerBridge.kt | 2 +- .../matrix/vector/daemon/core/VectorDaemon.kt | 3 +- .../vector/daemon/core/VectorService.kt | 134 +++++++++++++++--- .../matrix/vector/daemon/data/ConfigCache.kt | 57 ++++++++ .../matrix/vector/daemon/data/FileSystem.kt | 2 +- .../matrix/vector/daemon/env/Dex2OatServer.kt | 6 +- .../vector/daemon/ipc/ManagerService.kt | 65 ++++++++- .../vector/daemon/system/SystemExtensions.kt | 78 +++++++++- 8 files changed, 317 insertions(+), 30 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/SystemServerBridge.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/SystemServerBridge.kt index 3192d8cd2..e4b159cad 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/SystemServerBridge.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/SystemServerBridge.kt @@ -18,7 +18,7 @@ private const val TRANSACTION_CODE = object SystemServerBridge { - @Suppress("DEPRECATION") + @Suppress("DEPRECATION") fun sendToBridge(binder: IBinder, isRestart: Boolean, systemServerService: SystemServerService) { CoroutineScope(Dispatchers.IO).launch { runCatching { diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt index dc01fff93..d0adea587 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt @@ -56,8 +56,7 @@ object VectorDaemon { // 2. Setup Main Looper & System Services Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND) - @Suppress("DEPRECATION") - Looper.prepareMainLooper() + @Suppress("DEPRECATION") Looper.prepareMainLooper() val systemServerService = SystemServerService(systemServerMaxRetry, proxyServiceName) systemServerService.putBinderForSystemServer() diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt index e7ebe9373..d93055378 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt @@ -1,18 +1,26 @@ package org.matrix.vector.daemon.core +import android.content.IIntentReceiver import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager import android.os.Binder +import android.os.Bundle import android.os.IBinder import android.util.Log +import hidden.HiddenApiBridge import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.lsposed.lspd.service.ILSPApplicationService import org.lsposed.lspd.service.ILSPosedService +import org.matrix.vector.daemon.BuildConfig import org.matrix.vector.daemon.data.ConfigCache +import org.matrix.vector.daemon.data.ProcessScope import org.matrix.vector.daemon.ipc.ApplicationService import org.matrix.vector.daemon.ipc.ManagerService -import org.matrix.vector.daemon.system.SystemContext +import org.matrix.vector.daemon.ipc.ModuleService +import org.matrix.vector.daemon.system.* private const val TAG = "VectorService" @@ -53,7 +61,13 @@ object VectorService : ILSPosedService.Stub() { } if (ApplicationService.hasRegister(uid, pid)) return null - // ConfigCache ProcessScope check omitted for brevity, handled in Phase 6 + val scope = ProcessScope(processName, uid) + if (!ManagerService.shouldStartManager(pid, uid, processName) && + ConfigCache.shouldSkipProcess(scope)) { + Log.d(TAG, "Skipped $processName/$uid") + return null + } + return if (ApplicationService.registerHeartBeat(uid, pid, processName, heartBeat)) { ApplicationService } else null @@ -64,8 +78,50 @@ object VectorService : ILSPosedService.Stub() { override fun setManagerEnabled(enabled: Boolean) = true // Omitted specific toggle logic private fun registerReceivers() { - // Implementation logic for ActivityManagerService.registerReceiver is moved to Phase 6 - // SystemExtensions + val packageFilter = + IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_ADDED) + addAction(Intent.ACTION_PACKAGE_CHANGED) + addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED) + addDataScheme("package") + } + val uidFilter = IntentFilter(Intent.ACTION_UID_REMOVED) + + val receiver = + object : IIntentReceiver.Stub() { + override fun performReceive( + intent: Intent, + resultCode: Int, + data: String?, + extras: Bundle?, + ordered: Boolean, + sticky: Boolean, + sendingUser: Int + ) { + ioScope.launch { dispatchPackageChanged(intent) } + } + } + activityManager?.registerReceiverCompat(receiver, packageFilter, null, -1, 0) + activityManager?.registerReceiverCompat(receiver, uidFilter, null, -1, 0) + + // UID Observer + val uidObserver = + object : android.app.IUidObserver.Stub() { + override fun onUidActive(uid: Int) = ModuleService.uidStarts(uid) + + override fun onUidCachedChanged(uid: Int, cached: Boolean) { + if (!cached) ModuleService.uidStarts(uid) + } + + override fun onUidIdle(uid: Int, disabled: Boolean) = ModuleService.uidStarts(uid) + + override fun onUidGone(uid: Int, disabled: Boolean) = ModuleService.uidGone(uid) + } + + // UID_OBSERVER_ACTIVE | UID_OBSERVER_GONE | UID_OBSERVER_IDLE | UID_OBSERVER_CACHED + val which = 1 or 2 or 4 or 8 + activityManager?.registerUidObserverCompat( + uidObserver, which, HiddenApiBridge.ActivityManager_PROCESS_STATE_UNKNOWN()) Log.d(TAG, "Registered all OS Receivers and UID Observers") } @@ -77,26 +133,66 @@ object VectorService : ILSPosedService.Stub() { } private fun dispatchPackageChanged(intent: Intent) { - ioScope.launch { - val uid = intent.getIntExtra(Intent.EXTRA_UID, -1) - val action = intent.action ?: return@launch - - Log.d(TAG, "Package changed: action=$action, uid=$uid") + val uid = intent.getIntExtra(Intent.EXTRA_UID, -1) + val action = intent.action ?: return + val userId = intent.getIntExtra("android.intent.extra.user_handle", uid % PER_USER_RANGE) + val uri = intent.data + val moduleName = uri?.schemeSpecificPart ?: ConfigCache.getModuleByUid(uid)?.packageName + + var isXposedModule = false + if (moduleName != null) { + val appInfo = + packageManager + ?.getPackageInfoCompat(moduleName, MATCH_ALL_FLAGS or PackageManager.GET_META_DATA, 0) + ?.applicationInfo + isXposedModule = + appInfo != null && + ((appInfo.metaData?.containsKey("xposedminversion") == true) || + ConfigCache.getModuleApkPath(appInfo) != null) + } - // Logic mimicking the massive switch statement in LSPosedService.java - when (action) { - Intent.ACTION_PACKAGE_FULLY_REMOVED -> { - // ConfigCache.removeModule(pkg) - } - Intent.ACTION_PACKAGE_ADDED, - Intent.ACTION_PACKAGE_CHANGED -> { - // ConfigCache.updateModuleApkPath(pkg) + when (action) { + Intent.ACTION_PACKAGE_FULLY_REMOVED -> { + if (moduleName != null && + intent.getBooleanExtra("android.intent.extra.REMOVED_FOR_ALL_USERS", false)) { + if (ConfigCache.removeModule(moduleName)) isXposedModule = true } - Intent.ACTION_UID_REMOVED -> { + } + Intent.ACTION_PACKAGE_ADDED, + Intent.ACTION_PACKAGE_CHANGED -> { + if (isXposedModule && moduleName != null) { + val appInfo = + packageManager?.getPackageInfoCompat(moduleName, MATCH_ALL_FLAGS, 0)?.applicationInfo + if (appInfo != null) { + isXposedModule = + ConfigCache.updateModuleApkPath( + moduleName, ConfigCache.getModuleApkPath(appInfo), false) + } + } else if (ConfigCache.cachedScopes.keys.any { it.uid == uid }) { ConfigCache.requestCacheUpdate() } } - // Broadcast intent back to manager app... + Intent.ACTION_UID_REMOVED -> { + if (isXposedModule) ConfigCache.requestCacheUpdate() + else if (ConfigCache.cachedScopes.keys.any { it.uid == uid }) + ConfigCache.requestCacheUpdate() + } + } + + // Broadcast back to Manager + if (moduleName != null) { + val notifyIntent = + Intent("org.lsposed.manager.NOTIFICATION").apply { + putExtra(Intent.EXTRA_INTENT, intent) + putExtra("android.intent.extra.PACKAGES", moduleName) + putExtra(Intent.EXTRA_USER, userId) + putExtra("isXposedModule", isXposedModule) + addFlags( + 0x01000000 or + 0x00400000) // FLAG_RECEIVER_INCLUDE_BACKGROUND | FLAG_RECEIVER_FROM_SHELL + setPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME) + } + activityManager?.broadcastIntentCompat(notifyIntent) } } } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt index 1b644a65c..20162f148 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt @@ -1,6 +1,7 @@ package org.matrix.vector.daemon.data import android.content.ContentValues +import android.content.pm.ApplicationInfo import android.util.Log import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.CoroutineScope @@ -368,4 +369,60 @@ object ConfigCache { requestCacheUpdate() return true } + + fun updateModuleApkPath(packageName: String, apkPath: String?, force: Boolean): Boolean { + if (apkPath == null || packageName == "lspd") return false + val values = + ContentValues().apply { + put("module_pkg_name", packageName) + put("apk_path", apkPath) + } + val db = dbHelper.writableDatabase + var count = + db.insertWithOnConflict( + "modules", null, values, android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE) + .toInt() + if (count < 0) { + val cached = cachedModules[packageName] + if (force || cached == null || cached.apkPath != apkPath) { + count = + db.updateWithOnConflict( + "modules", + values, + "module_pkg_name=?", + arrayOf(packageName), + android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE) + } else count = 0 + } + if (!force && count > 0) requestCacheUpdate() + return count > 0 + } + + fun removeModule(packageName: String): Boolean { + if (packageName == "lspd") return false + val res = + dbHelper.writableDatabase.delete("modules", "module_pkg_name = ?", arrayOf(packageName)) > 0 + if (res) requestCacheUpdate() + return res + } + + fun getModuleApkPath(info: ApplicationInfo): String? { + val apks = mutableListOf() + info.sourceDir?.let { apks.add(it) } + info.splitSourceDirs?.let { apks.addAll(it) } + + return apks.firstOrNull { apk -> + runCatching { + java.util.zip.ZipFile(apk).use { zip -> + zip.getEntry("META-INF/xposed/java_init.list") != null || + zip.getEntry("assets/xposed_init") != null + } + } + .getOrDefault(false) + } + } + + fun shouldSkipProcess(scope: ProcessScope): Boolean { + return !cachedScopes.containsKey(scope) && !isManager(scope.uid) + } } 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 6eff9b3ec..ac53560dd 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 @@ -105,7 +105,7 @@ object FileSystem { * access strings/drawables without a real application context. */ val resources: Resources by lazy { - val am = AssetManager::class.java.getDeclaredConstructor().newInstance() + val am = AssetManager::class.java.getDeclaredConstructor().newInstance() val addAssetPath = AssetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java).apply { isAccessible = true 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 a7dabbaab..1b5669265 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 @@ -136,7 +136,11 @@ object Dex2OatServer { val header = ByteArray(5) if (fis.read(header) != 5) return // Verify ELF Magic: 0x7F 'E' 'L' 'F' - if (header[0] != 0x7F.toByte() || header[1] != 'E'.code.toByte() || header[2] != 'L'.code.toByte() || header[3] != 'F'.code.toByte()) return + if (header[0] != 0x7F.toByte() || + header[1] != 'E'.code.toByte() || + header[2] != 'L'.code.toByte() || + header[3] != 'F'.code.toByte()) + return val is32Bit = header[4] == 1.toByte() val is64Bit = header[4] == 2.toByte() diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt index 7a1af6d45..7d93cf5e4 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt @@ -2,9 +2,14 @@ package org.matrix.vector.daemon.ipc import android.content.ComponentName import android.content.Context +import android.content.IIntentSender import android.content.Intent +import android.content.IntentSender import android.content.pm.PackageInfo +import android.content.pm.PackageInstaller +import android.content.pm.PackageManager import android.content.pm.ResolveInfo +import android.content.pm.VersionedPackage import android.os.Build import android.os.Bundle import android.os.IBinder @@ -15,6 +20,7 @@ import android.view.IWindowManager import io.github.libxposed.service.IXposedService import java.io.File import java.io.FileOutputStream +import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import org.lsposed.lspd.ILSPManagerService import org.lsposed.lspd.models.Application @@ -191,8 +197,63 @@ object ManagerService : ILSPManagerService.Stub() { } override fun uninstallPackage(packageName: String, userId: Int): Boolean { - // ... omitted standard PM uninstall wrapper ... - return true + val latch = CountDownLatch(1) + var result = false + + val sender = + object : IIntentSender.Stub() { + override fun send( + code: Int, + intent: Intent, + resolvedType: String?, + whitelistToken: IBinder?, + finishedReceiver: android.content.IIntentReceiver?, + requiredPermission: String?, + options: Bundle? + ) { + val status = + intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) + result = status == PackageInstaller.STATUS_SUCCESS + latch.countDown() + } + + override fun send( + code: Int, + intent: Intent, + resolvedType: String?, + finishedReceiver: android.content.IIntentReceiver?, + requiredPermission: String?, + options: Bundle? + ): Int { + send(code, intent, resolvedType, null, finishedReceiver, requiredPermission, options) + return 0 + } + } + + // Using reflection to wrap the AIDL stub into an Android IntentSender + val intentSender = + runCatching { + val constructor = + IntentSender::class.java.getDeclaredConstructor(IIntentSender::class.java) + constructor.isAccessible = true + constructor.newInstance(sender) + } + .getOrNull() ?: return false + + val pkg = VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST) + val flag = if (userId == -1) 0x00000002 else 0 // DELETE_ALL_USERS flag + + runCatching { + packageManager + ?.packageInstaller + ?.uninstall(pkg, "android", flag, intentSender, if (userId == -1) 0 else userId) + } + .onFailure { + return false + } + + latch.await() + return result } override fun isSepolicyLoaded() = diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt index e9a9f2c30..97f97f488 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt @@ -1,6 +1,10 @@ package org.matrix.vector.daemon.system +import android.app.IActivityManager +import android.app.IUidObserver +import android.content.IIntentReceiver import android.content.Intent +import android.content.IntentFilter import android.content.pm.IPackageManager import android.content.pm.PackageInfo import android.content.pm.PackageManager @@ -8,7 +12,6 @@ import android.content.pm.ResolveInfo import android.content.pm.ServiceInfo import android.os.Build import android.util.Log -import org.matrix.vector.daemon.system.userManager import org.matrix.vector.daemon.utils.getRealUsers private const val TAG = "VectorSystem" @@ -132,9 +135,7 @@ fun IPackageManager.getInstalledPackagesForAllUsers( filterNoProcess: Boolean ): List { val result = mutableListOf() - val users = - userManager?.getRealUsers() - ?: emptyList() + val users = userManager?.getRealUsers() ?: emptyList() for (user in users) { val infos = @@ -164,3 +165,72 @@ fun IPackageManager.getInstalledPackagesForAllUsers( } return result } + +fun IActivityManager.registerReceiverCompat( + receiver: IIntentReceiver, + filter: IntentFilter, + requiredPermission: String?, + userId: Int, + flags: Int +): Intent? { + val appThread = SystemContext.appThread ?: return null + return runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + registerReceiverWithFeature( + appThread, + "android", + null, + "null", + receiver, + filter, + requiredPermission, + userId, + flags) + } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + registerReceiverWithFeature( + appThread, "android", null, receiver, filter, requiredPermission, userId, flags) + } else { + registerReceiver( + appThread, "android", receiver, filter, requiredPermission, userId, flags) + } + } + .onFailure { Log.e(TAG, "registerReceiver failed", it) } + .getOrNull() +} + +fun IActivityManager.registerUidObserverCompat(observer: IUidObserver, which: Int, cutpoint: Int) { + runCatching { registerUidObserver(observer, which, cutpoint, "android") } + .onFailure { Log.e(TAG, "registerUidObserver failed", it) } +} + +fun IActivityManager.broadcastIntentCompat(intent: Intent) { + val appThread = SystemContext.appThread + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + broadcastIntentWithFeature( + appThread, + null, + intent, + null, + null, + 0, + null, + null, + null, + null, + null, + -1, + null, + true, + false, + 0) + } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + broadcastIntentWithFeature( + appThread, null, intent, null, null, 0, null, null, null, -1, null, true, false, 0) + } else { + broadcastIntent( + appThread, intent, null, null, 0, null, null, null, -1, null, true, false, 0) + } + } + .onFailure { Log.e(TAG, "broadcastIntent failed", it) } +} From d8557919505ed87440934e432f74a14c590dfe95 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 01:00:43 +0100 Subject: [PATCH 09/38] Complete implementation --- .../matrix/vector/daemon/data/ConfigCache.kt | 74 +++++++++++++-- .../matrix/vector/daemon/data/FileSystem.kt | 94 +++++++++++++++++-- .../vector/daemon/ipc/ApplicationService.kt | 1 - 3 files changed, 154 insertions(+), 15 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt index 20162f148..d7cc1a58d 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt @@ -2,7 +2,11 @@ package org.matrix.vector.daemon.data import android.content.ContentValues import android.content.pm.ApplicationInfo +import android.content.pm.PackageParser +import android.system.Os import android.util.Log +import hidden.HiddenApiBridge +import java.io.File import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -12,6 +16,7 @@ import kotlinx.coroutines.launch import org.lsposed.lspd.models.Application import org.lsposed.lspd.models.Module import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.ipc.InjectedModuleService import org.matrix.vector.daemon.system.MATCH_ALL_FLAGS import org.matrix.vector.daemon.system.PER_USER_RANGE import org.matrix.vector.daemon.system.fetchProcesses @@ -85,7 +90,6 @@ object ConfigCache { val apkPath = cursor.getString(1) if (pkgName == "lspd") continue - // TODO: Fetch real obfuscate pref from configs table later val isObfuscateEnabled = true val preLoadedApk = FileSystem.loadModule(apkPath, isObfuscateEnabled) @@ -338,12 +342,70 @@ object ConfigCache { fun setAutoInclude(pkg: String, enabled: Boolean): Boolean = false - // Generates application info for system server since it doesn't have an APK fun getModulesForSystemServer(): List { - val result = mutableListOf() - // Fetch from DB where scope app_pkg_name = 'system', populate Fake ApplicationInfo, and return. - // Omitted DB boilerplate for brevity. - return result + val modules = mutableListOf() + + // system_server must have specific SELinux execmem capabilities to hook properly + if (!android.os.SELinux.checkSELinuxAccess( + "u:r:system_server:s0", "u:r:system_server:s0", "process", "execmem")) { + Log.e(TAG, "Skipping system_server injection: sepolicy execmem denied") + return modules + } + + dbHelper.readableDatabase + .query( + "scope INNER JOIN modules ON scope.mid = modules.mid", + arrayOf("module_pkg_name", "apk_path"), + "app_pkg_name=? AND enabled=1", + arrayOf("system"), + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + val pkgName = cursor.getString(0) + val apkPath = cursor.getString(1) + + // Reuse memory cache if available + val cached = cachedModules[pkgName] + if (cached != null) { + modules.add(cached) + continue + } + + val statPath = FileSystem.toGlobalNamespace("/data/user_de/0/$pkgName").absolutePath + + val module = + Module().apply { + packageName = pkgName + this.apkPath = apkPath + appId = runCatching { Os.stat(statPath).st_uid }.getOrDefault(-1) + service = InjectedModuleService(pkgName) + } + + // Parse the APK locally to simulate ApplicationInfo without ActivityManager running + runCatching { + @Suppress("DEPRECATION") + val pkg = PackageParser().parsePackage(File(apkPath), 0, false) + module.applicationInfo = + pkg.applicationInfo.apply { + sourceDir = apkPath + dataDir = statPath + deviceProtectedDataDir = statPath + HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(this, statPath) + processName = pkgName + } + } + .onFailure { Log.w(TAG, "Failed to parse $apkPath", it) } + + FileSystem.loadModule(apkPath, isDexObfuscateEnabled())?.let { + module.file = it + cachedModules.putIfAbsent(pkgName, module) + modules.add(module) + } + } + } + return modules } fun getPrefsPath(packageName: String, uid: Int): String { 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 ac53560dd..659481cbf 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 @@ -2,6 +2,7 @@ package org.matrix.vector.daemon.data import android.content.res.AssetManager import android.content.res.Resources +import android.os.Binder import android.os.ParcelFileDescriptor import android.os.Process import android.os.RemoteException @@ -29,6 +30,7 @@ import java.util.zip.ZipOutputStream import kotlin.io.path.exists import kotlin.io.path.isDirectory import org.lsposed.lspd.models.PreLoadedApk +import org.matrix.vector.daemon.BuildConfig import org.matrix.vector.daemon.utils.ObfuscationManager private const val TAG = "VectorFileSystem" @@ -185,7 +187,6 @@ object FileSystem { // 3. Apply obfuscation to class names if required if (obfuscate) { - // TODO val signatures = ObfuscationManager.getSignatures() for (i in moduleClassNames.indices) { val s = moduleClassNames[i] @@ -271,23 +272,100 @@ object FileSystem { return path } + fun toGlobalNamespace(path: String): File { + return if (path.startsWith("/")) File("/proc/1/root", path) else File("/proc/1/root/$path") + } + fun getLogs(zipFd: ParcelFileDescriptor) { runCatching { ZipOutputStream(java.io.FileOutputStream(zipFd.fileDescriptor)).use { os -> - os.setComment("Vector Daemon Logs") + val comment = + "Vector ${BuildConfig.BUILD_TYPE} ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" + os.setComment(comment) os.setLevel(java.util.zip.Deflater.BEST_COMPRESSION) fun addFile(name: String, file: File) { if (!file.exists() || !file.isFile) return - os.putNextEntry(ZipEntry(name)) - file.inputStream().use { it.copyTo(os) } - os.closeEntry() + runCatching { + os.putNextEntry(ZipEntry(name)) + file.inputStream().use { it.copyTo(os) } + os.closeEntry() + } + } + + fun addDir(basePath: String, dir: File) { + if (!dir.exists() || !dir.isDirectory) return + // Kotlin's walkTopDown elegantly replaces Files.walkFileTree + dir.walkTopDown() + .filter { it.isFile } + .forEach { file -> + val relativePath = dir.toPath().relativize(file.toPath()).toString() + val entryName = + if (basePath.isEmpty()) relativePath else "$basePath/$relativePath" + addFile(entryName, file) + } + } + + fun addProcOutput(name: String, vararg cmd: String) { + runCatching { + val proc = ProcessBuilder(*cmd).start() + os.putNextEntry(ZipEntry(name)) + proc.inputStream.use { it.copyTo(os) } + os.closeEntry() + } + } + + // 1. Gather daemon logs and system crash traces + addDir("log", logDirPath.toFile()) + addDir("log.old", oldLogDirPath.toFile()) + addDir("tombstones", File("/data/tombstones")) + addDir("anr", File("/data/anr")) + addDir( + "crash1", File("/data/data/${BuildConfig.MANAGER_INJECTED_PKG_NAME}/cache/crash")) + addDir( + "crash2", + File("/data/data/${BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME}/cache/crash")) + + // 2. Gather system logs directly via shell + addProcOutput("full.log", "logcat", "-b", "all", "-d") + addProcOutput("dmesg.log", "dmesg") + + // 3. Gather Magisk module states safely + val magiskDataDir = File("/data/adb/modules") + if (magiskDataDir.exists() && magiskDataDir.isDirectory) { + magiskDataDir.listFiles()?.forEach { moduleDir -> + val modName = moduleDir.name + listOf("module.prop", "remove", "disable", "update", "sepolicy.rule").forEach { + addFile("modules/$modName/$it", File(moduleDir, it)) + } + } + } + + // 4. Gather memory/mount info for daemon and caller + val proc = File("/proc") + arrayOf("self", Binder.getCallingPid().toString()).forEach { pid -> + val pidPath = File(proc, pid) + listOf("maps", "mountinfo", "status").forEach { + addFile("proc/$pid/$it", File(pidPath, it)) + } } + // 5. Gather Database and Scopes addFile("modules_config.db", dbPath) - addFile("props.txt", getPropsPath()) - addFile("kmsg.log", getKmsgPath()) - // Omitted full directory walks for brevity, but you can use File.walk() here. + runCatching { + os.putNextEntry(ZipEntry("scopes.txt")) + ConfigCache.cachedScopes.forEach { (scope, modules) -> + os.write("${scope.processName}/${scope.uid}\n".toByteArray()) + modules.forEach { mod -> + os.write("\t${mod.packageName}\n".toByteArray()) + mod.file?.moduleClassNames?.forEach { cn -> os.write("\t\t$cn\n".toByteArray()) } + mod.file?.moduleLibraryNames?.forEach { ln -> + os.write("\t\t$ln\n".toByteArray()) + } + } + } + os.closeEntry() + } } } .onFailure { Log.e(TAG, "Failed to export logs", it) } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt index e75b1e416..1b02f4fb0 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt @@ -40,7 +40,6 @@ object ApplicationService : ILSPApplicationService.Stub() { override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { when (code) { DEX_TRANSACTION_CODE -> { - // TODO: FileSystem needs getPreloadDex() implemented val shm = FileSystem.getPreloadDex(ConfigCache.isDexObfuscateEnabled()) ?: return false reply?.writeNoException() reply?.let { shm.writeToParcel(it, 0) } From 5a9f0ef31d1f7188a032059067774f8c8a8b3422 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 01:20:11 +0100 Subject: [PATCH 10/38] Fix i18n support --- .../lspd/service/LSPNotificationManager.java | 6 +- .../daemon/system/NotificationManager.kt | 138 ++++++++++++++---- .../vector/daemon/system/SystemExtensions.kt | 5 + daemon/src/main/res/values-af/strings.xml | 8 +- daemon/src/main/res/values-ar/strings.xml | 8 +- daemon/src/main/res/values-bg/strings.xml | 6 +- daemon/src/main/res/values-bn/strings.xml | 6 +- daemon/src/main/res/values-ca/strings.xml | 8 +- daemon/src/main/res/values-cs/strings.xml | 8 +- daemon/src/main/res/values-da/strings.xml | 8 +- daemon/src/main/res/values-de/strings.xml | 8 +- daemon/src/main/res/values-el/strings.xml | 8 +- daemon/src/main/res/values-es/strings.xml | 6 +- daemon/src/main/res/values-et/strings.xml | 8 +- daemon/src/main/res/values-fa/strings.xml | 8 +- daemon/src/main/res/values-fi/strings.xml | 8 +- daemon/src/main/res/values-fr/strings.xml | 10 +- daemon/src/main/res/values-hi/strings.xml | 6 +- daemon/src/main/res/values-hr/strings.xml | 8 +- daemon/src/main/res/values-hu/strings.xml | 8 +- daemon/src/main/res/values-in/strings.xml | 8 +- daemon/src/main/res/values-it/strings.xml | 8 +- daemon/src/main/res/values-iw/strings.xml | 10 +- daemon/src/main/res/values-ja/strings.xml | 8 +- daemon/src/main/res/values-ko/strings.xml | 6 +- daemon/src/main/res/values-ku/strings.xml | 6 +- daemon/src/main/res/values-lt/strings.xml | 6 +- daemon/src/main/res/values-nl/strings.xml | 12 +- daemon/src/main/res/values-no/strings.xml | 6 +- daemon/src/main/res/values-pl/strings.xml | 8 +- daemon/src/main/res/values-pt-rBR/strings.xml | 8 +- daemon/src/main/res/values-pt/strings.xml | 8 +- daemon/src/main/res/values-ro/strings.xml | 8 +- daemon/src/main/res/values-ru/strings.xml | 8 +- daemon/src/main/res/values-si/strings.xml | 6 +- daemon/src/main/res/values-sk/strings.xml | 6 +- daemon/src/main/res/values-sv/strings.xml | 8 +- daemon/src/main/res/values-th/strings.xml | 8 +- daemon/src/main/res/values-tr/strings.xml | 8 +- daemon/src/main/res/values-uk/strings.xml | 8 +- daemon/src/main/res/values-ur/strings.xml | 6 +- daemon/src/main/res/values-vi/strings.xml | 6 +- daemon/src/main/res/values-zh-rCN/strings.xml | 8 +- daemon/src/main/res/values-zh-rHK/strings.xml | 8 +- daemon/src/main/res/values-zh-rTW/strings.xml | 8 +- daemon/src/main/res/values/strings.xml | 14 +- 46 files changed, 284 insertions(+), 199 deletions(-) diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java index 54cbcb215..c422ffe70 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java @@ -156,8 +156,8 @@ static void notifyStatusNotification() { var context = new FakeContext(); int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; var notification = new Notification.Builder(context, STATUS_CHANNEL_ID) - .setContentTitle(context.getString(R.string.lsposed_running_notification_title)) - .setContentText(context.getString(R.string.lsposed_running_notification_content)) + .setContentTitle(context.getString(R.string.vector_running_notification_title)) + .setContentText(context.getString(R.string.vector_running_notification_content)) .setSmallIcon(getNotificationIcon()) .setContentIntent(PendingIntent.getBroadcast(context, 1, intent, flags)) .setVisibility(Notification.VISIBILITY_SECRET) @@ -294,7 +294,7 @@ static void requestModuleScope(String modulePackageName, int moduleUserId, Strin .build(), new Notification.Action.Builder( Icon.createWithResource(context, R.drawable.ic_baseline_block_24), - context.getString(R.string.nerver_ask_again), + context.getString(R.string.never_ask_again), getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "block", callback)) .build() ).build(); diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt index 7aa9c7e83..acc837ab2 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt @@ -13,6 +13,7 @@ import android.util.Log import io.github.libxposed.service.IXposedScopeCallback import java.util.UUID import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.R import org.matrix.vector.daemon.utils.FakeContext private const val TAG = "VectorNotifManager" @@ -37,22 +38,20 @@ object NotificationManager { listOf( NotificationChannel( STATUS_CHANNEL_ID, - "Vector Status", + context.getString(R.string.status_channel_name), android.app.NotificationManager.IMPORTANCE_MIN) .apply { setShowBadge(false) }, NotificationChannel( UPDATED_CHANNEL_ID, - "Module Updated", + context.getString(R.string.module_updated_channel_name), android.app.NotificationManager.IMPORTANCE_HIGH) .apply { setShowBadge(false) }, NotificationChannel( SCOPE_CHANNEL_ID, - "Scope Request", + context.getString(R.string.scope_channel_name), android.app.NotificationManager.IMPORTANCE_HIGH) .apply { setShowBadge(false) }) - runCatching { - // ParceledListSlice is required for system_server IPC nm?.createNotificationChannelsForPackage( "android", 1000, android.content.pm.ParceledListSlice(list)) } @@ -68,8 +67,8 @@ object NotificationManager { val notif = Notification.Builder(context, STATUS_CHANNEL_ID) - .setContentTitle("Vector is active") - .setContentText("The daemon is running.") + .setContentTitle(context.getString(R.string.vector_running_notification_title)) + .setContentText(context.getString(R.string.vector_running_notification_content)) .setSmallIcon(android.R.drawable.ic_dialog_info) // Fallback icon .setContentIntent(pi) .setVisibility(Notification.VISIBILITY_SECRET) @@ -100,45 +99,126 @@ object NotificationManager { callback: IXposedScopeCallback ) { val context = FakeContext() + val userName = userManager?.getUserName(moduleUserId) ?: moduleUserId.toString() + + fun createActionIntent(actionParams: String, requestCode: Int): PendingIntent { + val intent = + Intent(moduleScopeAction).apply { + setPackage("android") + data = + Uri.Builder() + .scheme("module") + .encodedAuthority("$modulePkg:$moduleUserId") + .encodedPath(scopePkg) + .appendQueryParameter("action", actionParams) + .build() + putExtras(Bundle().apply { putBinder("callback", callback.asBinder()) }) + } + return PendingIntent.getBroadcast( + context, + requestCode, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + } + + val notif = + Notification.Builder(context, SCOPE_CHANNEL_ID) + .setContentTitle(context.getString(R.string.xposed_module_request_scope_title)) + .setContentText( + context.getString( + R.string.xposed_module_request_scope_content, modulePkg, userName, scopePkg)) + .setSmallIcon(android.R.drawable.ic_dialog_alert) + .addAction( + Notification.Action.Builder( + null, + context.getString(R.string.scope_approve), + createActionIntent("approve", 4)) + .build()) + .addAction( + Notification.Action.Builder( + null, context.getString(R.string.scope_deny), createActionIntent("deny", 5)) + .build()) + .addAction( + Notification.Action.Builder( + null, + context.getString(R.string.never_ask_again), + createActionIntent("block", 6)) + .build()) + .setAutoCancel(true) + .setStyle( + Notification.BigTextStyle() + .bigText( + context.getString( + R.string.xposed_module_request_scope_content, + modulePkg, + userName, + scopePkg))) + .build() + .apply { extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME) } + + createChannels() + runCatching { + nm?.enqueueNotificationWithTag("android", opPkg, modulePkg, modulePkg.hashCode(), notif, 0) + } + } + + fun notifyModuleUpdated( + modulePackageName: String, + moduleUserId: Int, + enabled: Boolean, + systemModule: Boolean + ) { + val context = FakeContext() + val userName = userManager?.getUserName(moduleUserId) ?: moduleUserId.toString() + + val title = + context.getString( + if (enabled) { + if (systemModule) R.string.xposed_module_updated_notification_title_system + else R.string.xposed_module_updated_notification_title + } else R.string.module_is_not_activated_yet) + + val content = + context.getString( + if (enabled) { + if (systemModule) R.string.xposed_module_updated_notification_content_system + else R.string.xposed_module_updated_notification_content + } else { + if (moduleUserId == 0) R.string.module_is_not_activated_yet_main_user_detailed + else R.string.module_is_not_activated_yet_multi_user_detailed + }, + modulePackageName, + userName) + val intent = - Intent(moduleScopeAction).apply { + Intent(openManagerAction).apply { setPackage("android") data = Uri.Builder() .scheme("module") - .encodedAuthority("$modulePkg:$moduleUserId") - .encodedPath(scopePkg) - .appendQueryParameter("action", "approve") + .encodedAuthority("$modulePackageName:$moduleUserId") .build() - putExtras(Bundle().apply { putBinder("callback", callback.asBinder()) }) } val pi = PendingIntent.getBroadcast( - context, 4, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + context, 3, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) val notif = - Notification.Builder(context, SCOPE_CHANNEL_ID) - .setContentTitle("Scope Request") - .setContentText("Module $modulePkg requested injection into $scopePkg.") - .setSmallIcon(android.R.drawable.ic_dialog_alert) - .addAction(Notification.Action.Builder(null, "Approve", pi).build()) + Notification.Builder(context, UPDATED_CHANNEL_ID) + .setContentTitle(title) + .setContentText(content) + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentIntent(pi) + .setVisibility(Notification.VISIBILITY_SECRET) .setAutoCancel(true) + .setStyle(Notification.BigTextStyle().bigText(content)) .build() .apply { extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME) } createChannels() runCatching { - nm?.enqueueNotificationWithTag("android", opPkg, modulePkg, modulePkg.hashCode(), notif, 0) - } - } - - fun cancelNotification(channel: String, modulePkg: String, moduleUserId: Int) { - runCatching { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - nm?.cancelNotificationWithTag("android", "android", modulePkg, modulePkg.hashCode(), 0) - } else { - nm?.cancelNotificationWithTag("android", modulePkg, modulePkg.hashCode(), 0) - } + nm?.enqueueNotificationWithTag( + "android", opPkg, modulePackageName, modulePackageName.hashCode(), notif, 0) } } } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt index 97f97f488..47403be73 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt @@ -11,6 +11,7 @@ import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.content.pm.ServiceInfo import android.os.Build +import android.os.IUserManager import android.util.Log import org.matrix.vector.daemon.utils.getRealUsers @@ -234,3 +235,7 @@ fun IActivityManager.broadcastIntentCompat(intent: Intent) { } .onFailure { Log.e(TAG, "broadcastIntent failed", it) } } + +fun IUserManager.getUserName(userId: Int): String { + return runCatching { getUserInfo(userId)?.name }.getOrNull() ?: userId.toString() +} diff --git a/daemon/src/main/res/values-af/strings.xml b/daemon/src/main/res/values-af/strings.xml index 38e9953c4..8b9d290e5 100644 --- a/daemon/src/main/res/values-af/strings.xml +++ b/daemon/src/main/res/values-af/strings.xml @@ -9,13 +9,13 @@ Xposed-module is opgedateer, stelselherlaai vereis %s is opgedateer, aangesien die omvang System Framework bevat, vereis herlaai om veranderinge toe te pas Module-opdatering voltooi - LSPosed status - LSPosed gelaai - Tik die kennisgewing om bestuurder oop te maak + Vector status + Vector gelaai + Tik die kennisgewing om bestuurder oop te maak Omvang Versoek %1$s op gebruiker %2$s versoek om %3$s by sy omvang te voeg. Omvang versoek Keur goed Ontken - Moet nooit vra nie + Moet nooit vra nie diff --git a/daemon/src/main/res/values-ar/strings.xml b/daemon/src/main/res/values-ar/strings.xml index 013f1e30a..153b6a658 100644 --- a/daemon/src/main/res/values-ar/strings.xml +++ b/daemon/src/main/res/values-ar/strings.xml @@ -9,13 +9,13 @@ تم تحديث وحدة Xposed، مطلوب إعادة تشغيل النظام %s تم تحديثه، نظراً لأن النطاق يحتوي على إطار النظام، يتطلب إعادة التشغيل لتطبيق التغييرات اكتمل تحديث الوحدة - حالة LSPosed - تم تحميل LSPosed - اضغط على الإشعار لفتح المدير + حالة Vector + تم تحميل Vector + اضغط على الإشعار لفتح المدير Scope Request %1$s على %2$s طلبات المستخدم لإضافة %3$s إلى نطاقه. طلب النطاق اوافق رفض - لا تسأل أبدا + لا تسأل أبدا diff --git a/daemon/src/main/res/values-bg/strings.xml b/daemon/src/main/res/values-bg/strings.xml index 1d931004a..af52dbf0e 100644 --- a/daemon/src/main/res/values-bg/strings.xml +++ b/daemon/src/main/res/values-bg/strings.xml @@ -10,12 +10,12 @@ %s е актуализиран, тъй като обхватът съдържа System Framework, необходимо е рестартиране, за да се приложат промените Актуализация на модула е завършена Предложен статус на LSP - LSPosed заредени - Докоснете известието, за да отворите мениджъра + Vector заредени + Докоснете известието, за да отворите мениджъра Заявка за обхват %1$s при заявки от страна на потребителя %2$s за добавяне на %3$s към неговия обхват. Искане за обхват Одобряване на Отказ - Никога не питайте + Никога не питайте diff --git a/daemon/src/main/res/values-bn/strings.xml b/daemon/src/main/res/values-bn/strings.xml index ff94cfddf..36914d24a 100644 --- a/daemon/src/main/res/values-bn/strings.xml +++ b/daemon/src/main/res/values-bn/strings.xml @@ -10,12 +10,12 @@ %s আপডেট করা হয়েছে, যেহেতু সুযোগে সিস্টেম ফ্রেমওয়ার্ক রয়েছে, পরিবর্তনগুলি প্রয়োগ করার জন্য রিবুট প্রয়োজন মডিউল আপডেট সম্পূর্ণ LSPপোজড স্ট্যাটাস - LSPosed লোড - ম্যানেজার খুলতে বিজ্ঞপ্তিতে ট্যাপ করুন + Vector লোড + ম্যানেজার খুলতে বিজ্ঞপ্তিতে ট্যাপ করুন সুযোগ অনুরোধ ব্যবহারকারী %2$s এর সুযোগে %3$s যোগ করার অনুরোধে %1$s। সুযোগ অনুরোধ অনুমোদন করুন অস্বীকার করুন - কখনো জিজ্ঞাসা করবেন না + কখনো জিজ্ঞাসা করবেন না diff --git a/daemon/src/main/res/values-ca/strings.xml b/daemon/src/main/res/values-ca/strings.xml index 961589d2d..b5d53bc16 100644 --- a/daemon/src/main/res/values-ca/strings.xml +++ b/daemon/src/main/res/values-ca/strings.xml @@ -9,13 +9,13 @@ Mòdul Xposed actualitzat, cal reiniciar el sistema %s s\'ha actualitzat, ja que l\'abast conté System Framework, cal reiniciar per aplicar els canvis S\'ha completat l\'actualització del mòdul - Estat LSPosed - LSPosed carregat - Toqueu la notificació per obrir el gestor + Estat Vector + Vector carregat + Toqueu la notificació per obrir el gestor Sol·licitud d\'abast %1$s a l\'usuari %2$s demana afegir %3$s al seu abast. Sol·licitud d\'abast Aprovar Negar - Mai pregunteu + Mai pregunteu diff --git a/daemon/src/main/res/values-cs/strings.xml b/daemon/src/main/res/values-cs/strings.xml index 89c6aab41..fd96e36b9 100644 --- a/daemon/src/main/res/values-cs/strings.xml +++ b/daemon/src/main/res/values-cs/strings.xml @@ -9,13 +9,13 @@ Xposed modul aktualizován, je vyžadován restart systému %s byl aktualizován, a protože se provedly změny v souvislosti se Systémovým Frameworkem, je vyžadován restart pro aplikaci změn Aktualizace modulu dokončena - Stav LSPosed - LPosed načten - Klepnutím na oznámení otevřete správce + Stav Vector + LPosed načten + Klepnutím na oznámení otevřete správce Žádost o rozsah %1$s pro uživatele %2$s požaduje přidání %3$s do jeho rozsahu. Žádost o rozsah Schválit Odmítnout - Nikdy se neptat + Nikdy se neptat diff --git a/daemon/src/main/res/values-da/strings.xml b/daemon/src/main/res/values-da/strings.xml index 2a8485100..4bd015211 100644 --- a/daemon/src/main/res/values-da/strings.xml +++ b/daemon/src/main/res/values-da/strings.xml @@ -9,13 +9,13 @@ Xposed modul opdateret, system genstart kræves %s er blevet opdateret, da anvendelsesområdet indeholder System Framework, krævede genstart for at anvende ændringer Modulopdatering afsluttet - LSPosed status - LSPosed indlæst - Tryk på meddelelsen for at åbne administratoren + Vector status + Vector indlæst + Tryk på meddelelsen for at åbne administratoren Anmodning om anvendelsesområde %1$s på anmodninger fra brugeren %2$s om at tilføje %3$s til sit anvendelsesområde. Anmodning om anvendelsesområde Godkend Afvis - Spørg aldrig + Spørg aldrig diff --git a/daemon/src/main/res/values-de/strings.xml b/daemon/src/main/res/values-de/strings.xml index df200b793..f2e35f8e8 100644 --- a/daemon/src/main/res/values-de/strings.xml +++ b/daemon/src/main/res/values-de/strings.xml @@ -9,13 +9,13 @@ Xposed-Modul aktualisiert, Systemneustart erforderlich %s wurde aktualisiert, da der Geltungsbereich System-Framework enthält, ist ein Neustart erforderlich, damit die Änderungen übernommen werden Modulaktualisierung abgeschlossen - LSPosed-Status - LSPosed geladen - Auf die Benachrichtigung tippen, um den Manager zu öffnen + Vector-Status + Vector geladen + Auf die Benachrichtigung tippen, um den Manager zu öffnen Scope-Anfrage %1$s von Benutzer %2$s fordert an, %3$s zu seinem Scope-Bereich hinzuzufügen. Scope-Anfrage Genehmigen Verweigern - Niemals fragen + Niemals fragen diff --git a/daemon/src/main/res/values-el/strings.xml b/daemon/src/main/res/values-el/strings.xml index 85797fc8f..a2e25dd6b 100644 --- a/daemon/src/main/res/values-el/strings.xml +++ b/daemon/src/main/res/values-el/strings.xml @@ -9,13 +9,13 @@ Το πρόσθετο Xposed ενημερώθηκε, απαιτείται επανεκκίνηση συστήματος %s έχει ενημερωθεί, δεδομένου ότι το πεδίο εφαρμογής περιέχει Πλαίσιο Συστήματος, απαιτείται επανεκκίνηση για να εφαρμοστούν οι αλλαγές Η ενημέρωση πρόσθετου ολοκληρώθηκε - Κατάσταση LSPosed - LSPosed φορτώθηκε - Πατήστε την ειδοποίηση για άνοιγμα διαχειριστή + Κατάσταση Vector + Vector φορτώθηκε + Πατήστε την ειδοποίηση για άνοιγμα διαχειριστή Αίτηση για το πεδίο εφαρμογής %1$s στις αιτήσεις του χρήστη %2$s για την προσθήκη του %3$s στο πεδίο εφαρμογής του. Αίτημα πεδίου εφαρμογής Έγκριση Άρνηση - Ποτέ μην ρωτάς + Ποτέ μην ρωτάς diff --git a/daemon/src/main/res/values-es/strings.xml b/daemon/src/main/res/values-es/strings.xml index 5a4c91961..f0e39ee78 100644 --- a/daemon/src/main/res/values-es/strings.xml +++ b/daemon/src/main/res/values-es/strings.xml @@ -10,12 +10,12 @@ %s ha sido actualizado, ya que el ámbito contiene la estructura del sistema, requiere reiniciar para aplicar cambios Módulo de actualización completo LSPosición de estado - LSPosed cargado - Toca la notificación para abrir el gestor + Vector cargado + Toca la notificación para abrir el gestor Solicitud de alcance %1$s cuando el usuario %2$s solicita añadir %3$s a su ámbito. Solicitud de alcance Aprobar Denegar - Nunca preguntes + Nunca preguntes diff --git a/daemon/src/main/res/values-et/strings.xml b/daemon/src/main/res/values-et/strings.xml index 5e3a66898..da3e617f2 100644 --- a/daemon/src/main/res/values-et/strings.xml +++ b/daemon/src/main/res/values-et/strings.xml @@ -9,13 +9,13 @@ Xposed moodul uuendatud, süsteemi taaskäivitamine vajalik %s on uuendatud, kuna reguleerimisala sisaldab System Framework, vajalik taaskäivitamine, et rakendada muudatusi Mooduli uuendamine lõpetatud - LSPosedi staatus - LSPosed laaditud - Halduri avamiseks puudutage märguannet + Vectori staatus + Vector laaditud + Halduri avamiseks puudutage märguannet Ulatuse Taotlus %1$s kasutajal %2$s taotleb %3$s lisamist oma ulatusse. Ulatuse taotlus Kinnita Keela - Ära Enam Küsi + Ära Enam Küsi diff --git a/daemon/src/main/res/values-fa/strings.xml b/daemon/src/main/res/values-fa/strings.xml index 339c0e5de..cda35c5fe 100644 --- a/daemon/src/main/res/values-fa/strings.xml +++ b/daemon/src/main/res/values-fa/strings.xml @@ -9,13 +9,13 @@ ماژول Xposed به‌روزرسانی شد، نیاز به راه‌اندازی مجدد سیستم %s به‌روزرسانی شده است؛ از آنجا که محدوده شامل چارچوب سیستم است، برای اعمال تغییرات نیاز به راه‌اندازی مجدد سیستم است به‌روزرسانی ماژول کامل شد - وضعیت LSPosed - LSPosed بارگذاری شد - برای باز کردن مدیر روی اعلان ضربه بزنید + وضعیت Vector + Vector بارگذاری شد + برای باز کردن مدیر روی اعلان ضربه بزنید درخواست محدوده %1$s روی کاربر %2$s درخواست افزودن %3$s به محدوده خود را دارد. درخواست محدوده تأیید رد - هرگز نپرس + هرگز نپرس diff --git a/daemon/src/main/res/values-fi/strings.xml b/daemon/src/main/res/values-fi/strings.xml index b591d36b3..b21473c9e 100644 --- a/daemon/src/main/res/values-fi/strings.xml +++ b/daemon/src/main/res/values-fi/strings.xml @@ -9,13 +9,13 @@ Xposed moduuli päivitetty, järjestelmän uudelleenkäynnistys vaaditaan %s on päivitetty, koska soveltamisala sisältää järjestelmän kehyksen, vaaditaan uudelleenkäynnistys muutosten käyttöön Moduulin päivitys valmis - LSPosed status - LSPosed ladattu - Avaa manager napauttamalla ilmoitusta + Vector status + Vector ladattu + Avaa manager napauttamalla ilmoitusta Soveltamisalaa koskeva pyyntö %1$s käyttäjän %2$s pyynnöistä lisätä %3$s sen toimialueeseen. Laajuuspyyntö Hyväksy Kiellä - Älä koskaan kysy + Älä koskaan kysy diff --git a/daemon/src/main/res/values-fr/strings.xml b/daemon/src/main/res/values-fr/strings.xml index 2ca96bea9..0c3564c4b 100644 --- a/daemon/src/main/res/values-fr/strings.xml +++ b/daemon/src/main/res/values-fr/strings.xml @@ -1,7 +1,7 @@ - Le module LSPosed n\’est pas encore actif + Le module Vector n\’est pas encore actif %1$s a été installé, mais n\'a pas été encore activé %1$s a été installé pour l\'utilisateur %2$s, mais n\'a pas été encore activé Module Xposed mis à jour @@ -9,13 +9,13 @@ Module Xposed mis à jour, redémarrage du système requis %s a été mis à jour, étant donné que le champ d\'application est étendu au sous système, un redémarrage est nécessaire pour appliquer les changements Mise à jour du module terminée - Statut LSPosed - LSPosed chargé - Appuyer sur la notification pour ouvrir le gestionnaire + Statut Vector + Vector chargé + Appuyer sur la notification pour ouvrir le gestionnaire Demande d\'Extension de Portée %1$s sur l\'utilisateur %2$s demande d\'ajouter %3$s à son périmètre d\'action. Demande d\'Extension de Portée Approuver Refuser - Ne jamais demander + Ne jamais demander diff --git a/daemon/src/main/res/values-hi/strings.xml b/daemon/src/main/res/values-hi/strings.xml index 5d0eae764..b18a1f757 100644 --- a/daemon/src/main/res/values-hi/strings.xml +++ b/daemon/src/main/res/values-hi/strings.xml @@ -10,12 +10,12 @@ %s को अपडेट कर दिया गया है, क्योंकि स्कोप में सिस्टम फ्रेमवर्क है, परिवर्तनों को लागू करने के लिए रीबूट की आवश्यकता है मॉड्यूल अद्यतन पूर्ण एलएसपोस्ड स्थिति - LSPosed लोड किया गया - मैनेजर खोलने के लिए नोटिफिकेशन पर टैप करें + Vector लोड किया गया + मैनेजर खोलने के लिए नोटिफिकेशन पर टैप करें गुंजाइश अनुरोध उपयोगकर्ता %2$s पर %1$s इसके दायरे में %3$s जोड़ने का अनुरोध करता है। दायरा अनुरोध मंज़ूरी देना अस्वीकार करना - कभी मत पूछो + कभी मत पूछो diff --git a/daemon/src/main/res/values-hr/strings.xml b/daemon/src/main/res/values-hr/strings.xml index fa1f789f9..963882940 100644 --- a/daemon/src/main/res/values-hr/strings.xml +++ b/daemon/src/main/res/values-hr/strings.xml @@ -9,13 +9,13 @@ Xposed modul je ažuriran, potrebno je ponovno pokretanje sustava %s je ažuriran, budući da opseg sadrži System Framework, potrebno je ponovno pokretanje za primjenu promjena Ažuriranje modula dovršeno - LSPosed status - LSPosed je učitan - Dodirnite obavijest da otvorite upravitelja + Vector status + Vector je učitan + Dodirnite obavijest da otvorite upravitelja Zahtjev za opseg %1$s na korisniku %2$s zahtijeva dodavanje %3$s svom opsegu. Zahtjev za opseg Odobriti Poreći - Nikad ne pitaj + Nikad ne pitaj diff --git a/daemon/src/main/res/values-hu/strings.xml b/daemon/src/main/res/values-hu/strings.xml index bebddf236..f834c508e 100644 --- a/daemon/src/main/res/values-hu/strings.xml +++ b/daemon/src/main/res/values-hu/strings.xml @@ -9,13 +9,13 @@ Xposed modul frissítve, rendszer újraindítás szükséges %s frissítve lett, mivel a hatókör tartalmazza a System Framework-et, a változások alkalmazásához szükséges újraindítás szükséges. A modul frissítése befejeződött - LSPosed állapot - LSPosed betöltve - Érintse meg az értesítést a menedzser megnyitásához + Vector állapot + Vector betöltve + Érintse meg az értesítést a menedzser megnyitásához Hatókör kérés %1$s a felhasználó %2$s kérésére a %3$s hozzáadására a hatóköréhez. Hatókör kérés Jóváhagyás Megtagadás - Soha ne kérdezz rá + Soha ne kérdezz rá diff --git a/daemon/src/main/res/values-in/strings.xml b/daemon/src/main/res/values-in/strings.xml index 1f178a95d..f061972db 100644 --- a/daemon/src/main/res/values-in/strings.xml +++ b/daemon/src/main/res/values-in/strings.xml @@ -9,13 +9,13 @@ Modul Xposed diperbarui, diperlukan memulai ulang sistem %s telah diperbarui, karena cakupannya berisi Kerangka Sistem, diperlukan mulai ulang untuk menerapkan perubahan Pembaruan modul selesai - Status LSPosed - LSPosed dimuat - Ketuk notifikasi untuk membuka pengelola + Status Vector + Vector dimuat + Ketuk notifikasi untuk membuka pengelola Permintaan Cakupan %1$s pada pengguna %2$s meminta untuk menambahkan %3$s ke dalam cakupannya. Permintaan cakupan Menyetujui Menolak - Jangan Pernah Bertanya + Jangan Pernah Bertanya diff --git a/daemon/src/main/res/values-it/strings.xml b/daemon/src/main/res/values-it/strings.xml index 9646afd2d..9c611e04f 100644 --- a/daemon/src/main/res/values-it/strings.xml +++ b/daemon/src/main/res/values-it/strings.xml @@ -9,13 +9,13 @@ Modulo Xposed aggiornato, è necessario il riavvio del sistema %s è stato aggiornato. Poiché è abilitato per il framework di sistema, è necessario riavviare per applicare le modifiche Aggiornamento del modulo completato - Stato LSPosed - LSPosed caricato - Tocca la notifica per aprire il manager + Stato Vector + Vector caricato + Tocca la notifica per aprire il manager Richiesta attivazione %1$s sull\'utente %2$s richiede di aggiungere %3$s alle sue attivazioni. Richiesta attivazione Approva Nega - Non chiedere mai + Non chiedere mai diff --git a/daemon/src/main/res/values-iw/strings.xml b/daemon/src/main/res/values-iw/strings.xml index c9bb62b6a..a37e77231 100644 --- a/daemon/src/main/res/values-iw/strings.xml +++ b/daemon/src/main/res/values-iw/strings.xml @@ -1,21 +1,21 @@ - מודול LSPosed עדיין לא הופעל + מודול Vector עדיין לא הופעל %1$s הותקן, אך אינו מופעל עדיין %1$s הותקן למשתמש %2$s, אך אינו מופעל עדיין - מודול LSPosed עודכן + מודול Vector עודכן %s עודכן מודול Xposed עודכן, נדרש אתחול המערכת %s עודכן, מכיוון שההיקף מכיל System Framework, נדרש אתחול כדי להחיל שינויים עדכון המודול הושלם סטטוס LSPost - LSPosed נטען - הקש על ההודעה כדי לפתוח את המנהל + Vector נטען + הקש על ההודעה כדי לפתוח את המנהל Xposed_מודול_מבקש_כותרת_תחום %1$s על משתמש %2$s מבקש להוסיף %3$s להיקף שלו. תחום_שם_ערוץ אישור_תחום לְהַכּחִישׁ - לעולם אל תשאל + לעולם אל תשאל diff --git a/daemon/src/main/res/values-ja/strings.xml b/daemon/src/main/res/values-ja/strings.xml index 3b99590ab..9a567bbfd 100644 --- a/daemon/src/main/res/values-ja/strings.xml +++ b/daemon/src/main/res/values-ja/strings.xml @@ -9,13 +9,13 @@ Xposed モジュールが更新されました。システムの再起動が必要です %s が更新されました。スコープにシステムフレームワークが含まれているため、変更を適用するには再起動が必要です モジュールの更新完了通知 - LSPosed のステータス通知 - LSPosed の読み込み完了通知 - 通知をタップしてマネージャーを開きます + Vector のステータス通知 + Vector の読み込み完了通知 + 通知をタップしてマネージャーを開きます スコープのリクエスト ユーザー %2$s の %1$s が %3$s をそのスコープに追加するようリクエストしています。 スコープのリクエスト 許可 拒否 - 再度表示しない + 再度表示しない diff --git a/daemon/src/main/res/values-ko/strings.xml b/daemon/src/main/res/values-ko/strings.xml index 82a50715d..25be4ca5a 100644 --- a/daemon/src/main/res/values-ko/strings.xml +++ b/daemon/src/main/res/values-ko/strings.xml @@ -10,12 +10,12 @@ 범위에 시스템 프레임워크가 포함되어 있으므로 %s 이 업데이트되었습니다. 변경 사항을 적용하려면 재부팅해야 합니다. 모듈 업데이트 완료 LS포즈 상태 - LSPosed 로드됨 - 알림을 탭하여 관리자 열기 + Vector 로드됨 + 알림을 탭하여 관리자 열기 범위 요청 사용자 %2$s 의 %1$s 은 해당 범위에 %3$s 을 추가하도록 요청합니다. 범위 요청 승인 거부 - 다시 묻지 않음 + 다시 묻지 않음 diff --git a/daemon/src/main/res/values-ku/strings.xml b/daemon/src/main/res/values-ku/strings.xml index d442b1ae4..aef735d9b 100644 --- a/daemon/src/main/res/values-ku/strings.xml +++ b/daemon/src/main/res/values-ku/strings.xml @@ -10,12 +10,12 @@ %s hate nûve kirin, ji ber ku çarçove Çarçoveya Pergalê dihewîne, ji bo sepandina guhertinan ji nû ve destpêkirinê hewce dike Nûvekirina modulê qediya statûya LSP - LSP hate barkirin - Daxuyaniyê bikirtînin da ku rêveberê vekin + LSP hate barkirin + Daxuyaniyê bikirtînin da ku rêveberê vekin Scope Daxwaza %1$s li ser bikarhêner %2$s daxwaz dike ku %3$s li qada xwe zêde bike. Daxwaza Scope Destûrdan Înkarkirin - Never Ask + Never Ask diff --git a/daemon/src/main/res/values-lt/strings.xml b/daemon/src/main/res/values-lt/strings.xml index cd2045bca..60a491ffd 100644 --- a/daemon/src/main/res/values-lt/strings.xml +++ b/daemon/src/main/res/values-lt/strings.xml @@ -10,12 +10,12 @@ %s buvo atnaujintas, nes srityje yra System Framework, reikalingas perkrovimas, kad būtų galima taikyti pakeitimus Modulio atnaujinimas baigtas LSPatvirtintas statusas - LSPpateiktas pakrautas - Bakstelėkite pranešimą, kad atidarytumėte tvarkytuvę + LSPpateiktas pakrautas + Bakstelėkite pranešimą, kad atidarytumėte tvarkytuvę Apimties prašymas %1$s pagal naudotojo %2$s užklausas įtraukti %3$s į jo taikymo sritį. Apimties prašymas Patvirtinti Atsisakyti - Niekada neklauskite + Niekada neklauskite diff --git a/daemon/src/main/res/values-nl/strings.xml b/daemon/src/main/res/values-nl/strings.xml index 6e68eb0d0..333730fa9 100644 --- a/daemon/src/main/res/values-nl/strings.xml +++ b/daemon/src/main/res/values-nl/strings.xml @@ -1,21 +1,21 @@ - LSPosed module is nog niet geactiveerd + Vector module is nog niet geactiveerd %1$s is geïnstalleerd, maar nog niet geactiveerd %1$s is geïnstalleerd bij gebruiker %2$s, maar is nog niet geactiveerd - LSPosed module bijgewerkt + Vector module bijgewerkt %1$s is geupdate Xposed-module bijgewerkt, systeem opnieuw opstarten vereist %s is bijgewerkt, omdat het bereik een systeemkader bevat, moet je opnieuw opstarten om wijzigingen toe te passen Module update voltooid - LSPosed status - LSPosed geladen - Tik op de melding om manager te openen + Vector status + Vector geladen + Tik op de melding om manager te openen Scope verzoek %1$s op gebruiker %2$s verzoeken om %3$s toe te voegen aan het toepassingsgebied. Scope verzoek Goedkeuren Weiger - Nooit vragen + Nooit vragen diff --git a/daemon/src/main/res/values-no/strings.xml b/daemon/src/main/res/values-no/strings.xml index d275ef53a..c30428507 100644 --- a/daemon/src/main/res/values-no/strings.xml +++ b/daemon/src/main/res/values-no/strings.xml @@ -10,12 +10,12 @@ %s er blitt oppdatert, siden omfanget inneholder systemramme, nødvendig for omstart av endringene Moduloppdatering fullført LSPosert status - LSPosert lastet - Trykk på varselet for å åpne administrator + LSPosert lastet + Trykk på varselet for å åpne administrator Forespørsel om omfang %1$s på bruker %2$s ber om å legge til %3$s i omfanget. Forespørsel om omfang Vedta Benekte - Spør aldri + Spør aldri diff --git a/daemon/src/main/res/values-pl/strings.xml b/daemon/src/main/res/values-pl/strings.xml index a63aa91d8..07c40cec7 100644 --- a/daemon/src/main/res/values-pl/strings.xml +++ b/daemon/src/main/res/values-pl/strings.xml @@ -9,13 +9,13 @@ Zaktualizowano moduł Xposed, wymagane ponowne uruchomienie systemu %s został zaktualizowany, ponieważ zakres zawiera System Framework, wymagany restart aby zastosować zmiany Aktualizowanie modułu zakończone - Status LSPosed - LSPosed załadowany - Kliknij powiadomienie, by włączyć menadżer + Status Vector + Vector załadowany + Kliknij powiadomienie, by włączyć menadżer Żądanie Zakresu %1$s w użytkowniku %2$s żąda dodania %3$s do jego zakresu. Żądanie zakresu Zatwierdź Odrzuć - Nigdy nie pytaj + Nigdy nie pytaj diff --git a/daemon/src/main/res/values-pt-rBR/strings.xml b/daemon/src/main/res/values-pt-rBR/strings.xml index ff5357dc2..844219a7a 100644 --- a/daemon/src/main/res/values-pt-rBR/strings.xml +++ b/daemon/src/main/res/values-pt-rBR/strings.xml @@ -9,13 +9,13 @@ Módulo atualizado. É necessário reiniciar o sistema. %s foi atualizado. Como o escopo contém o Framework do Sistema, é necessário reiniciar para aplicar as mudanças. Atualização do módulo concluída - Status do LSPosed - LSPosed carregado - Toque na notificação para abrir o gerenciador + Status do Vector + Vector carregado + Toque na notificação para abrir o gerenciador Solicitação de escopo %1$s do usuário %2$s está solicitando para adicionar %3$s no seu escopo. Solicitação de escopo Permitir Negar - Nunca perguntar + Nunca perguntar diff --git a/daemon/src/main/res/values-pt/strings.xml b/daemon/src/main/res/values-pt/strings.xml index c036f9f32..680c9213b 100644 --- a/daemon/src/main/res/values-pt/strings.xml +++ b/daemon/src/main/res/values-pt/strings.xml @@ -9,13 +9,13 @@ Módulo Atualizado. É necessário reiniciar o sistema %s foi atualizado. Como o escopo contém o Framework do Sistema, é necessário reiniciar para aplicar as mudanças Atualização do módulo concluída - Estado do LSPosed - LSPosed carregado - Toque na notificação para abrir o gerenciador + Estado do Vector + Vector carregado + Toque na notificação para abrir o gerenciador Pedido de âmbito de aplicação %1$s sobre o utilizador %2$s pede para acrescentar %3$s ao seu âmbito de aplicação. Pedido de âmbito de aplicação Aprovar Negar - Nunca Pergunte + Nunca Pergunte diff --git a/daemon/src/main/res/values-ro/strings.xml b/daemon/src/main/res/values-ro/strings.xml index 0e7151ac6..e50349466 100644 --- a/daemon/src/main/res/values-ro/strings.xml +++ b/daemon/src/main/res/values-ro/strings.xml @@ -9,13 +9,13 @@ Modulul Xposed a fost actualizat, este necesară repornirea sistemului Modulul %s a fost actualizat. Este necesară repornirea dispozitivului, deoarece Sistemul Android face parte din configurația modulului. Actualizarea modulelor este completă - Stare LSPosed - LSPosed încărcat - Apăsați pentru a deschide managerul + Stare Vector + Vector încărcat + Apăsați pentru a deschide managerul Cerere de modificare configurație Modulul %1$s, instalat pentru utilizatorul %2$s, dorește să adauge %3$s în configurația sa. Cerere de modificare configurație Aprobați Refuzați - Nu afișați din nou + Nu afișați din nou diff --git a/daemon/src/main/res/values-ru/strings.xml b/daemon/src/main/res/values-ru/strings.xml index 75691c8d8..bca097f38 100644 --- a/daemon/src/main/res/values-ru/strings.xml +++ b/daemon/src/main/res/values-ru/strings.xml @@ -9,13 +9,13 @@ Модуль Xposed обновлён, требуется перезагрузка устройства %s обновлён; ввиду того, что системный фреймворк (System Framework) в его «охвате», требуется перезагрузка для применения изменений Обновление модуля завершено - Статус LSPosed - LSPosed загружен - Нажмите уведомление, чтобы открыть LSPosed Manager + Статус Vector + Vector загружен + Нажмите уведомление, чтобы открыть Vector Manager Запрос «охвата» %1$s (пользователь %2$s): запрашивается добавление %3$s в «охват». Запрос «охвата» Принять Отклонить - Больше не спрашивать + Больше не спрашивать diff --git a/daemon/src/main/res/values-si/strings.xml b/daemon/src/main/res/values-si/strings.xml index 14af856a0..5a2bf8bbe 100644 --- a/daemon/src/main/res/values-si/strings.xml +++ b/daemon/src/main/res/values-si/strings.xml @@ -10,12 +10,12 @@ %s යාවත්කාලීන කර ඇත, විෂය පථයේ පද්ධති රාමුව අඩංගු බැවින්, වෙනස්කම් යෙදීමට නැවත පණගැන්වීම අවශ්‍ය වේ මොඩියුල යාවත්කාලීන කිරීම සම්පූර්ණයි එල්එස්පී තත්ත්වය - LSPposed පටවා ඇත - කළමනාකරු විවෘත කිරීමට දැනුම්දීම තට්ටු කරන්න + LSPposed පටවා ඇත + කළමනාකරු විවෘත කිරීමට දැනුම්දීම තට්ටු කරන්න විෂය පථය ඉල්ලීම පරිශීලක %2$s හි %1$s එහි විෂය පථයට %3$s එකතු කරන ලෙස ඉල්ලා සිටී. විෂය පථය ඉල්ලීම අනුමත කරන්න ප්රතික්ෂේප කරන්න - කවදාවත් අහන්න එපා + කවදාවත් අහන්න එපා diff --git a/daemon/src/main/res/values-sk/strings.xml b/daemon/src/main/res/values-sk/strings.xml index bc8f4db64..9e71934c0 100644 --- a/daemon/src/main/res/values-sk/strings.xml +++ b/daemon/src/main/res/values-sk/strings.xml @@ -10,12 +10,12 @@ %s bola aktualizovaná, pretože rozsah obsahuje System Framework, potrebný reštart na uplatnenie zmien Aktualizácia modulu dokončená LSPonúkaný stav - LSPosed naložené - Ťuknutím na oznámenie otvorte správcu + Vector naložené + Ťuknutím na oznámenie otvorte správcu Žiadosť o rozsah %1$s na žiadosti používateľa %2$s o pridanie stránky %3$s do jej rozsahu. Žiadosť o rozsah Schváliť Odmietnuť - Nikdy sa nepýtajte + Nikdy sa nepýtajte diff --git a/daemon/src/main/res/values-sv/strings.xml b/daemon/src/main/res/values-sv/strings.xml index 6ba7be981..e5b63bc52 100644 --- a/daemon/src/main/res/values-sv/strings.xml +++ b/daemon/src/main/res/values-sv/strings.xml @@ -9,13 +9,13 @@ Xposed modul uppdaterad, systemomstart krävs %s har uppdaterats, eftersom omfattningen innehåller Systemramverk, krävs omstart för att tillämpa ändringar Uppdatering av modulen slutförd - LSPosed status - LSPosed laddad - Tryck på meddelandet för att öppna administratören + Vector status + Vector laddad + Tryck på meddelandet för att öppna administratören Begäran om tillämpningsområde %1$s om användaren %2$s begär att %3$s ska läggas till i dess räckvidd. Begäran om tillämpningsområde Godkänna Förneka - Fråga aldrig + Fråga aldrig diff --git a/daemon/src/main/res/values-th/strings.xml b/daemon/src/main/res/values-th/strings.xml index a00562388..38ac73703 100644 --- a/daemon/src/main/res/values-th/strings.xml +++ b/daemon/src/main/res/values-th/strings.xml @@ -9,13 +9,13 @@ อัปเดตโมดูล Xposed จำเป็นต้องรีสตาร์ทเครื่อง %s ได้รับการอัปเดตแล้ว เนื่องจาก Scope มี System Framework จึงจำเป็นต้องรีสตาร์ทเครื่องเพื่อใช้การเปลี่ยนแปลง การอัปเดตโมดูลเสร็จสมบูรณ์ - สถานะ LSPosed - LSPosed โหลดแล้ว - แตะการแจ้งเตือนเพื่อเปิดตัวจัดการ + สถานะ Vector + Vector โหลดแล้ว + แตะการแจ้งเตือนเพื่อเปิดตัวจัดการ คำขอ Scope %1$s ผูัใช้นี้ %2$s ขอให้เพิ่ม %3$s ใน List ของ scope. คำขอ Scope. อนุมัติ ปฎิเสธ - ไม่เคยถาม + ไม่เคยถาม diff --git a/daemon/src/main/res/values-tr/strings.xml b/daemon/src/main/res/values-tr/strings.xml index ebea2a4f6..e28cbb835 100644 --- a/daemon/src/main/res/values-tr/strings.xml +++ b/daemon/src/main/res/values-tr/strings.xml @@ -9,13 +9,13 @@ Xposed modülü güncellendi, sistemin yeniden başlatılması gerekiyor %s Güncelleme kapsamı Sistem Çerçevesi içerdiğinden, değişiklikleri uygulamak için yeniden başlatma gereklidir Modül güncellemesi tamamlandı - LSPosed durumu - LSPosed yüklendi - Yöneticiyi açmak için bildirime dokunun + Vector durumu + Vector yüklendi + Yöneticiyi açmak için bildirime dokunun Kapsam Talebi Kullanıcı %2$s %1$s kapsamına %3$s eklemek ister. Kapsam talebi Onayla Reddet - Asla Sorma + Asla Sorma diff --git a/daemon/src/main/res/values-uk/strings.xml b/daemon/src/main/res/values-uk/strings.xml index e4e6037df..73daa8ed7 100644 --- a/daemon/src/main/res/values-uk/strings.xml +++ b/daemon/src/main/res/values-uk/strings.xml @@ -9,13 +9,13 @@ Модуль Xposed оновлено, потрібно перезавантаження системи %s було оновлено, оскільки область містить System Framework, необхідне перезавантаження для застосування змін Оновлення модуля завершено - Статус LSPosed - LSPosed завантажено - Натисніть на повідомлення, щоб відкрити менеджер + Статус Vector + Vector завантажено + Натисніть на повідомлення, щоб відкрити менеджер Запит на визначення обсягу робіт %1$s на запит користувача %2$s з проханням додати %3$s до своєї області видимості. Запит обсягу робіт Затвердити Відхилити - Ніколи не питай + Ніколи не питай diff --git a/daemon/src/main/res/values-ur/strings.xml b/daemon/src/main/res/values-ur/strings.xml index 485184ea0..fc9739ed4 100644 --- a/daemon/src/main/res/values-ur/strings.xml +++ b/daemon/src/main/res/values-ur/strings.xml @@ -10,12 +10,12 @@ %s کو اپ ڈیٹ کر دیا گیا ہے، چونکہ دائرہ کار میں سسٹم فریم ورک ہے، تبدیلیاں لاگو کرنے کے لیے ریبوٹ کی ضرورت ہے۔ ماڈیول اپ ڈیٹ مکمل ہو گیا۔ ایل ایس پیز کی حیثیت - ایل ایس پیز لوڈ شدہ - مینیجر کو کھولنے کے لیے نوٹیفکیشن کو تھپتھپائیں۔ + ایل ایس پیز لوڈ شدہ + مینیجر کو کھولنے کے لیے نوٹیفکیشن کو تھپتھپائیں۔ دائرہ کار کی درخواست صارف %2$s پر %1$s اپنے دائرہ کار میں %3$s شامل کرنے کی درخواست کرتا ہے۔ دائرہ کار کی درخواست منظور کرو انکار کرنا - کبھی نہ پوچھیں۔ + کبھی نہ پوچھیں۔ diff --git a/daemon/src/main/res/values-vi/strings.xml b/daemon/src/main/res/values-vi/strings.xml index d33acebe9..f8f6e96f1 100644 --- a/daemon/src/main/res/values-vi/strings.xml +++ b/daemon/src/main/res/values-vi/strings.xml @@ -10,12 +10,12 @@ %s đã được cập nhật, vì phạm vi bao gồm Framework Hệ thống, thì khởi động lại là cần thiết để áp dụng các thay đổi Tiện ích bổ sung cập nhật hoàn tất Trạng thái hoạt động - Ứng dụng đã được tải - Nhấn để mở trình quản lý + Ứng dụng đã được tải + Nhấn để mở trình quản lý Yêu cầu phạm vi %1$s khi người dùng %2$s yêu cầu thêm %3$s vào phạm vi của nó. Phạm vi yêu cầu Chấp thuận Từ chối - Không hỏi lại + Không hỏi lại diff --git a/daemon/src/main/res/values-zh-rCN/strings.xml b/daemon/src/main/res/values-zh-rCN/strings.xml index 0642027b6..ace294bba 100644 --- a/daemon/src/main/res/values-zh-rCN/strings.xml +++ b/daemon/src/main/res/values-zh-rCN/strings.xml @@ -9,13 +9,13 @@ Xposed 模块已更新,需要重新启动 %s 已更新,由于作用域包含系统框架,需重启以应用更改 模块更新完成 - LSPosed 状态 - LSPosed 已加载 - 点按通知以打开管理器 + Vector 状态 + Vector 已加载 + 点按通知以打开管理器 作用域请求 用户 %2$s 上的 %1$s 请求将 %3$s 添加至其作用域。 作用域请求 允许 拒绝 - 不再询问 + 不再询问 diff --git a/daemon/src/main/res/values-zh-rHK/strings.xml b/daemon/src/main/res/values-zh-rHK/strings.xml index 5919b74e3..02b34d381 100644 --- a/daemon/src/main/res/values-zh-rHK/strings.xml +++ b/daemon/src/main/res/values-zh-rHK/strings.xml @@ -9,13 +9,13 @@ Xposed 模組已更新,需要重新啟動。 %s 已更新,由於作用範圍包含系統架構,需要重新啟動以套用修改。 模块更新完成 - LSPosed 狀態 - LSPosed 已載入 - 輕觸通知以開啟管理員 + Vector 狀態 + Vector 已載入 + 輕觸通知以開啟管理員 作用範圍要求 用戶 %2$s 上的 %1$s 要求將 %3$s 新增至其作用範圍。 作用範圍要求 核准 拒絕 - 永不詢問 + 永不詢問 diff --git a/daemon/src/main/res/values-zh-rTW/strings.xml b/daemon/src/main/res/values-zh-rTW/strings.xml index dd2f463be..d085f2620 100644 --- a/daemon/src/main/res/values-zh-rTW/strings.xml +++ b/daemon/src/main/res/values-zh-rTW/strings.xml @@ -9,13 +9,13 @@ Xposed 模組已更新,需要重新啟動。 %s 已更新,由於作用域包含系統框架,需要重新啟動以套用修改。 模組更新完成 - LSPosed 狀態 - LSPosed 已載入 - 輕觸通知以開啟管理員 + Vector 狀態 + Vector 已載入 + 輕觸通知以開啟管理員 作用範圍要求 使用者 %2$s 上的 %1$s 要求將 %3$s 新增至其作用範圍。 作用範圍要求 核准 拒絕 - 永不詢問 + 永不詢問 diff --git a/daemon/src/main/res/values/strings.xml b/daemon/src/main/res/values/strings.xml index 0d195d2dd..1b68ced85 100644 --- a/daemon/src/main/res/values/strings.xml +++ b/daemon/src/main/res/values/strings.xml @@ -1,20 +1,20 @@ - Xposed module is not activated yet + Module is not activated yet %1$s has been installed, but is not activated yet %1$s has been installed to user %2$s, but is not activated yet - Xposed module updated + Module updated %s has been updated, please force stop and restart apps in its scope - Xposed module updated, system reboot required + Module updated, system reboot required %s has been updated, since the scope contains System Framework, required reboot to apply changes Module update complete - LSPosed status - LSPosed loaded - Tap the notification to open manager + Vector status + Vector loaded + Tap the notification to open manager Scope Request %1$s on user %2$s requests to add %3$s to its scope. Scope request Approve Deny - Never Ask + Never Ask From ea23b5c2fa2b859e0546c81095f396642b7d6551 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 01:57:33 +0100 Subject: [PATCH 11/38] Correct JNI names --- daemon/src/main/jni/dex2oat.cpp | 95 ++++++++++--------- daemon/src/main/jni/logcat.cpp | 30 +++--- daemon/src/main/jni/logcat.h | 9 +- .../matrix/vector/daemon/data/FileSystem.kt | 18 ++++ .../matrix/vector/daemon/env/LogcatMonitor.kt | 31 ++++++ .../de/robv/android/xposed/XposedBridge.java | 2 +- zygisk/module/daemon | 2 +- 7 files changed, 122 insertions(+), 65 deletions(-) diff --git a/daemon/src/main/jni/dex2oat.cpp b/daemon/src/main/jni/dex2oat.cpp index 062e41343..32cf77d07 100644 --- a/daemon/src/main/jni/dex2oat.cpp +++ b/daemon/src/main/jni/dex2oat.cpp @@ -1,21 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2023 LSPosed Contributors - */ #include #include #include @@ -28,50 +10,71 @@ #include "logging.h" -extern "C" JNIEXPORT void JNICALL Java_org_lsposed_lspd_service_Dex2OatService_doMountNative( +// Lightweight RAII wrapper to prevent FD leaks +struct UniqueFd { + int fd; + explicit UniqueFd(int fd) : fd(fd) {} + ~UniqueFd() { + if (fd >= 0) close(fd); + } + operator int() const { return fd; } +}; + +extern "C" JNIEXPORT void JNICALL Java_org_matrix_vector_daemon_env_Dex2OatServer_doMountNative( JNIEnv *env, jobject, jboolean enabled, jstring r32, jstring d32, jstring r64, jstring d64) { char dex2oat32[PATH_MAX], dex2oat64[PATH_MAX]; - realpath("bin/dex2oat32", dex2oat32); - realpath("bin/dex2oat64", dex2oat64); + if (realpath("bin/dex2oat32", dex2oat32) == nullptr) { + PLOGE("resolve realpath for bin/dex2oat32"); + } + if (realpath("bin/dex2oat64", dex2oat64) == nullptr) { + PLOGE("resolve realpath for bin/dex2oat64"); + } - if (pid_t pid = fork(); pid > 0) { // parent + const char *r32p = r32 ? env->GetStringUTFChars(r32, nullptr) : nullptr; + const char *d32p = d32 ? env->GetStringUTFChars(d32, nullptr) : nullptr; + const char *r64p = r64 ? env->GetStringUTFChars(r64, nullptr) : nullptr; + const char *d64p = d64 ? env->GetStringUTFChars(d64, nullptr) : nullptr; + + pid_t pid = fork(); + if (pid > 0) { // Parent process waitpid(pid, nullptr, 0); - } else { // child - int ns = open("/proc/1/ns/mnt", O_RDONLY); - setns(ns, CLONE_NEWNS); - close(ns); - const char *r32p, *d32p, *r64p, *d64p; - if (r32) r32p = env->GetStringUTFChars(r32, nullptr); - if (d32) d32p = env->GetStringUTFChars(d32, nullptr); - if (r64) r64p = env->GetStringUTFChars(r64, nullptr); - if (d64) d64p = env->GetStringUTFChars(d64, nullptr); + // Safely release JNI strings in the parent + if (r32p) env->ReleaseStringUTFChars(r32, r32p); + if (d32p) env->ReleaseStringUTFChars(d32, d32p); + if (r64p) env->ReleaseStringUTFChars(r64, r64p); + if (d64p) env->ReleaseStringUTFChars(d64, d64p); + } else if (pid == 0) { // Child process + UniqueFd ns(open("/proc/1/ns/mnt", O_RDONLY)); + if (ns >= 0) { + setns(ns, CLONE_NEWNS); + } if (enabled) { LOGI("Enable dex2oat wrapper"); - if (r32) { + if (r32p) { mount(dex2oat32, r32p, nullptr, MS_BIND, nullptr); mount(nullptr, r32p, nullptr, MS_BIND | MS_REMOUNT | MS_RDONLY, nullptr); } - if (d32) { + if (d32p) { mount(dex2oat32, d32p, nullptr, MS_BIND, nullptr); mount(nullptr, d32p, nullptr, MS_BIND | MS_REMOUNT | MS_RDONLY, nullptr); } - if (r64) { + if (r64p) { mount(dex2oat64, r64p, nullptr, MS_BIND, nullptr); mount(nullptr, r64p, nullptr, MS_BIND | MS_REMOUNT | MS_RDONLY, nullptr); } - if (d64) { + if (d64p) { mount(dex2oat64, d64p, nullptr, MS_BIND, nullptr); mount(nullptr, d64p, nullptr, MS_BIND | MS_REMOUNT | MS_RDONLY, nullptr); } execlp("resetprop", "resetprop", "--delete", "dalvik.vm.dex2oat-flags", nullptr); } else { LOGI("Disable dex2oat wrapper"); - if (r32) umount(r32p); - if (d32) umount(d32p); - if (r64) umount(r64p); - if (d64) umount(d64p); + if (r32p) umount(r32p); + if (d32p) umount(d32p); + if (r64p) umount(r64p); + if (d64p) umount(d64p); execlp("resetprop", "resetprop", "dalvik.vm.dex2oat-flags", "--inline-max-code-units=0", nullptr); } @@ -83,8 +86,9 @@ extern "C" JNIEXPORT void JNICALL Java_org_lsposed_lspd_service_Dex2OatService_d static int setsockcreatecon_raw(const char *context) { std::string path = "/proc/self/task/" + std::to_string(gettid()) + "/attr/sockcreate"; - int fd = open(path.c_str(), O_RDWR | O_CLOEXEC); + UniqueFd fd(open(path.c_str(), O_RDWR | O_CLOEXEC)); if (fd < 0) return -1; + int ret; if (context) { do { @@ -95,20 +99,19 @@ static int setsockcreatecon_raw(const char *context) { ret = write(fd, nullptr, 0); // clear } while (ret < 0 && errno == EINTR); } - close(fd); return ret < 0 ? -1 : 0; } extern "C" JNIEXPORT jboolean JNICALL -Java_org_lsposed_lspd_service_Dex2OatService_setSockCreateContext(JNIEnv *env, jclass, - jstring contextStr) { - const char *context = env->GetStringUTFChars(contextStr, nullptr); +Java_org_matrix_vector_daemon_env_Dex2OatServer_setSockCreateContext(JNIEnv *env, jclass, + jstring contextStr) { + const char *context = contextStr ? env->GetStringUTFChars(contextStr, nullptr) : nullptr; int ret = setsockcreatecon_raw(context); - env->ReleaseStringUTFChars(contextStr, context); + if (context) env->ReleaseStringUTFChars(contextStr, context); return ret == 0; } extern "C" JNIEXPORT jstring JNICALL -Java_org_lsposed_lspd_service_Dex2OatService_getSockPath(JNIEnv *env, jobject) { +Java_org_matrix_vector_daemon_env_Dex2OatServer_getSockPath(JNIEnv *env, jobject) { return env->NewStringUTF("5291374ceda0aef7c5d86cd2a4f6a3ac\0"); } diff --git a/daemon/src/main/jni/logcat.cpp b/daemon/src/main/jni/logcat.cpp index 2e5897a39..6041efd30 100644 --- a/daemon/src/main/jni/logcat.cpp +++ b/daemon/src/main/jni/logcat.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -217,23 +218,32 @@ void Logcat::ProcessBuffer(struct log_msg *buf) { if (android_log_processLogBuffer(&buf->entry, &entry) < 0) return; entry.tagLen--; - std::string_view tag(entry.tag, entry.tagLen); bool shortcut = false; - if (tag == "LSPosed-Bridge"sv || tag == "XSharedPreferences"sv || tag == "LSPosedContext") + + if (tag == "VectorLegacyBridge"sv || tag == "XSharedPreferences"sv || tag == "VectorContext"sv) [[unlikely]] { modules_print_count_ += PrintLogLine(entry, modules_file_.get()); shortcut = true; } + + constexpr std::array exact_tags = { + "APatchD"sv, "Dobby"sv, "KernelSU"sv, "LSPlant"sv, + "LSPlt"sv, "Magisk"sv, "SELinux"sv, "TEESimulator"sv}; + constexpr std::array prefix_tags = {"dex2oat"sv, "Vector"sv, "LSPosed"sv, + "zygisk"sv}; + + bool match_exact = + std::any_of(exact_tags.begin(), exact_tags.end(), [&](auto t) { return tag == t; }); + bool match_prefix = std::any_of(prefix_tags.begin(), prefix_tags.end(), + [&](auto t) { return tag.starts_with(t); }); + if (verbose_ && (shortcut || buf->id() == log_id::LOG_ID_CRASH || entry.pid == my_pid_ || - tag == "APatchD"sv || tag == "Dobby"sv || tag.starts_with("dex2oat"sv) || - tag == "KernelSU"sv || tag == "LSPlant"sv || tag == "LSPlt"sv || - tag.starts_with("LSPosed"sv) || tag == "Magisk"sv || tag == "SELinux"sv || - tag == "TEESimulator"sv || tag.starts_with("Vector"sv) || - tag.starts_with("zygisk"sv))) [[unlikely]] { + match_exact || match_prefix)) [[unlikely]] { verbose_print_count_ += PrintLogLine(entry, verbose_file_.get()); } - if (entry.pid == my_pid_ && tag == "LSPosedLogcat"sv) [[unlikely]] { + + if (entry.pid == my_pid_ && tag == "VectorLogcat"sv) [[unlikely]] { std::string_view msg(entry.message, entry.messageLen); if (msg == "!!start_verbose!!"sv) { verbose_ = true; @@ -354,14 +364,12 @@ void Logcat::Run() { if (modules_print_count_ >= kMaxLogSize) [[unlikely]] RefreshFd(false); } - OnCrash(errno); } } extern "C" JNIEXPORT void JNICALL -// NOLINTNEXTLINE -Java_org_lsposed_lspd_service_LogcatService_runLogcat(JNIEnv *env, jobject thiz) { +Java_org_matrix_vector_daemon_env_LogcatMonitor_runLogcat(JNIEnv *env, jobject thiz) { jclass clazz = env->GetObjectClass(thiz); jmethodID method = env->GetMethodID(clazz, "refreshFd", "(Z)I"); Logcat logcat(env, thiz, method); diff --git a/daemon/src/main/jni/logcat.h b/daemon/src/main/jni/logcat.h index cc39b58a1..e2560ccc3 100644 --- a/daemon/src/main/jni/logcat.h +++ b/daemon/src/main/jni/logcat.h @@ -1,10 +1,9 @@ #pragma once +#include #include #include -#include - #define NS_PER_SEC 1000000000L #define MS_PER_NSEC 1000000 #define LOGGER_ENTRY_MAX_LEN (5 * 1024) @@ -43,15 +42,13 @@ struct log_msg { struct logger_entry entry; }; #ifdef __cplusplus - log_id_t id() { - return static_cast(entry.lid); - } + log_id_t id() { return static_cast(entry.lid); } #endif }; struct logger; struct logger_list; -long android_logger_get_log_size(struct logger* logger); +long android_logger_get_log_size(struct logger *logger); int android_logger_set_log_size(struct logger *logger, unsigned long size); struct logger_list *android_logger_list_alloc(int mode, unsigned int tail, pid_t pid); void android_logger_list_free(struct logger_list *logger_list); 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 659481cbf..6b3c7a9ad 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 @@ -24,6 +24,9 @@ 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 import java.util.zip.ZipEntry import java.util.zip.ZipFile import java.util.zip.ZipOutputStream @@ -48,6 +51,7 @@ object FileSystem { @Volatile private var preloadDex: SharedMemory? = null + private val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.systemDefault()) private val lockPath: Path = basePath.resolve("lock") private var fileLock: FileLock? = null private var lockChannel: FileChannel? = null @@ -371,4 +375,18 @@ object FileSystem { .onFailure { Log.e(TAG, "Failed to export logs", it) } .also { runCatching { zipFd.close() } } } + + private fun getNewLogFileName(prefix: String): String { + return "${prefix}_${formatter.format(Instant.now())}.log" + } + + fun getNewVerboseLogPath(): File { + createLogDirPath() + return logDirPath.resolve(getNewLogFileName("verbose")).toFile() + } + + fun getNewModulesLogPath(): File { + createLogDirPath() + return logDirPath.resolve(getNewLogFileName("modules")).toFile() + } } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt index 5d098c50a..1fd4f932d 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt @@ -153,4 +153,35 @@ object LogcatMonitor { if (modulesFd == -1) refresh(false) if (verboseFd == -1) refresh(true) } + + @JvmStatic + @Suppress("unused") // Called via JNI + fun refreshFd(isVerboseLog: Boolean): Int { + return runCatching { + val logFile = + if (isVerboseLog) { + checkFd(verboseFd) + val f = FileSystem.getNewVerboseLogPath() + verboseLogs.add(f) + f + } else { + checkFd(modulesFd) + val f = FileSystem.getNewModulesLogPath() + moduleLogs.add(f) + f + } + + Log.i(TAG, "New log file: $logFile") + FileSystem.chattr0(logFile.toPath().parent) + val fd = ParcelFileDescriptor.open(logFile, FD_MODE).detachFd() + + if (isVerboseLog) verboseFd = fd else modulesFd = fd + fd + } + .onFailure { + if (isVerboseLog) verboseFd = -1 else modulesFd = -1 + Log.w(TAG, "refreshFd failed", it) + } + .getOrDefault(-1) + } } diff --git a/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java b/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java index 4e37cf25b..5e85d36c6 100644 --- a/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java +++ b/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java @@ -42,7 +42,7 @@ public final class XposedBridge { /** * @hide */ - public static final String TAG = "LSPosed-Bridge"; + public static final String TAG = "VectorLegacyBridge"; /** * @deprecated Use {@link #getXposedVersion()} instead. diff --git a/zygisk/module/daemon b/zygisk/module/daemon index 514eac7c0..cad6e9d46 100644 --- a/zygisk/module/daemon +++ b/zygisk/module/daemon @@ -43,4 +43,4 @@ fi [ "$debug" = "true" ] && log -p d -t "Vector" "Starting daemon $*" # Launch the daemon -exec /system/bin/app_process $java_options /system/bin --nice-name=lspd org.lsposed.lspd.Main "$@" >/dev/null 2>&1 +exec /system/bin/app_process $java_options /system/bin --nice-name=lspd org.matrix.vector.daemon.core.VectorDaemon "$@" >/dev/null 2>&1 From 497af3519c96ad25ee04edf0185e699cc1777106 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 02:25:03 +0100 Subject: [PATCH 12/38] Fix databasehelper initilization --- .../org/matrix/vector/daemon/data/Database.kt | 4 ++- .../matrix/vector/daemon/env/LogcatMonitor.kt | 1 - .../matrix/vector/daemon/utils/FakeContext.kt | 25 +++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/Database.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/Database.kt index d806e5d5b..b3d21e60d 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/Database.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/Database.kt @@ -4,12 +4,14 @@ import android.content.Context import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import android.util.Log +import org.matrix.vector.daemon.utils.FakeContext private const val TAG = "VectorDatabase" private const val DB_VERSION = 4 -class Database(context: Context? = null) : +class Database(context: Context? = FakeContext()) : SQLiteOpenHelper(context, FileSystem.dbPath.absolutePath, null, DB_VERSION) { + override fun onConfigure(db: SQLiteDatabase) { super.onConfigure(db) db.setForeignKeyConstraintsEnabled(true) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt index 1fd4f932d..6900a41d7 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt @@ -154,7 +154,6 @@ object LogcatMonitor { if (verboseFd == -1) refresh(true) } - @JvmStatic @Suppress("unused") // Called via JNI fun refreshFd(isVerboseLog: Boolean): Int { return runCatching { diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt index e60ed4bd0..a2ded5232 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt @@ -4,7 +4,11 @@ import android.content.ContentResolver import android.content.ContextWrapper import android.content.pm.ApplicationInfo import android.content.res.Resources +import android.database.DatabaseErrorHandler +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteDatabase.CursorFactory import android.os.Build +import java.io.File import org.matrix.vector.daemon.data.FileSystem import org.matrix.vector.daemon.system.packageManager as sysPackageManager @@ -52,4 +56,25 @@ class FakeContext(private val fakePackageName: String = "android") : ContextWrap // Required for Android 12+ override fun getAttributionTag(): String? = null + + override fun getDatabasePath(name: String): File { + return java.io.File(name) // We pass absolute paths, so just return it directly + } + + override fun openOrCreateDatabase( + name: String, + mode: Int, + factory: CursorFactory? + ): SQLiteDatabase { + return SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), factory) + } + + override fun openOrCreateDatabase( + name: String, + mode: Int, + factory: CursorFactory, + errorHandler: DatabaseErrorHandler? + ): SQLiteDatabase { + return SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name).path, factory, errorHandler) + } } From 6a9bad4c7f6a568e0c6626b64d2bf396dffec9cc Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 02:59:53 +0100 Subject: [PATCH 13/38] Fix runtime bugs --- daemon/src/main/jni/obfuscation.cpp | 9 ++- .../vector/daemon/core/VectorService.kt | 78 +++++++++++++------ 2 files changed, 62 insertions(+), 25 deletions(-) diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index 9965f42f1..01d34b749 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -145,7 +145,8 @@ static jobject stringMapToJavaHashMap(JNIEnv *env, const std::map= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + activityManager?.finishReceiver( + appThread?.asBinder(), resultCode, data, extras, false, intent.flags) + } else { + activityManager?.finishReceiver( + this, resultCode, data, extras, false, intent.flags) + } + } + .onFailure { Log.e(TAG, "finishReceiver failed", it) } + } + } + private fun registerReceivers() { val packageFilter = IntentFilter().apply { @@ -86,23 +124,15 @@ object VectorService : ILSPosedService.Stub() { addDataScheme("package") } val uidFilter = IntentFilter(Intent.ACTION_UID_REMOVED) - - val receiver = - object : IIntentReceiver.Stub() { - override fun performReceive( - intent: Intent, - resultCode: Int, - data: String?, - extras: Bundle?, - ordered: Boolean, - sticky: Boolean, - sendingUser: Int - ) { - ioScope.launch { dispatchPackageChanged(intent) } - } + val bootFilter = + IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED).apply { + priority = IntentFilter.SYSTEM_HIGH_PRIORITY } - activityManager?.registerReceiverCompat(receiver, packageFilter, null, -1, 0) - activityManager?.registerReceiverCompat(receiver, uidFilter, null, -1, 0) + + // Use a receiver instance for each registration to prevent AMS conflicts + activityManager?.registerReceiverCompat(createReceiver(), packageFilter, null, -1, 0) + activityManager?.registerReceiverCompat(createReceiver(), uidFilter, null, -1, 0) + activityManager?.registerReceiverCompat(createReceiver(), bootFilter, null, 0, 0) // UID Observer val uidObserver = @@ -118,8 +148,12 @@ object VectorService : ILSPosedService.Stub() { override fun onUidGone(uid: Int, disabled: Boolean) = ModuleService.uidGone(uid) } - // UID_OBSERVER_ACTIVE | UID_OBSERVER_GONE | UID_OBSERVER_IDLE | UID_OBSERVER_CACHED - val which = 1 or 2 or 4 or 8 + val which = + HiddenApiBridge.ActivityManager_UID_OBSERVER_ACTIVE() or + HiddenApiBridge.ActivityManager_UID_OBSERVER_GONE() or + HiddenApiBridge.ActivityManager_UID_OBSERVER_IDLE() or + HiddenApiBridge.ActivityManager_UID_OBSERVER_CACHED() + activityManager?.registerUidObserverCompat( uidObserver, which, HiddenApiBridge.ActivityManager_PROCESS_STATE_UNKNOWN()) Log.d(TAG, "Registered all OS Receivers and UID Observers") @@ -128,7 +162,7 @@ object VectorService : ILSPosedService.Stub() { private fun dispatchBootCompleted() { bootCompleted = true if (ConfigCache.enableStatusNotification) { - // NotificationManager.notifyStatusNotification() // Needs impl in Phase 6 + NotificationManager.notifyStatusNotification() } } From 73b47163d1722b25c884008d85245a6cdb8caf08 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 03:45:54 +0100 Subject: [PATCH 14/38] Fix notification --- daemon/src/main/jni/logging.h | 2 +- .../vector/daemon/core/VectorService.kt | 86 +++++++++++++++++-- .../vector/daemon/ipc/ApplicationService.kt | 3 +- .../vector/daemon/ipc/ManagerService.kt | 67 ++++++++++++++- .../daemon/system/NotificationManager.kt | 53 +++++++++++- .../matrix/vector/daemon/utils/FakeContext.kt | 9 ++ 6 files changed, 208 insertions(+), 12 deletions(-) diff --git a/daemon/src/main/jni/logging.h b/daemon/src/main/jni/logging.h index 86a7220ac..ca9e36e06 100644 --- a/daemon/src/main/jni/logging.h +++ b/daemon/src/main/jni/logging.h @@ -25,7 +25,7 @@ #include #ifndef LOG_TAG -#define LOG_TAG "LSPosed" +#define LOG_TAG "VectorDaemon" #endif #ifdef LOG_DISABLED diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt index 0fac3f9c5..209b30799 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt @@ -11,6 +11,7 @@ import android.os.Bundle import android.os.IBinder import android.util.Log import hidden.HiddenApiBridge +import io.github.libxposed.service.IXposedScopeCallback import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -23,7 +24,6 @@ import org.matrix.vector.daemon.ipc.ApplicationService import org.matrix.vector.daemon.ipc.ManagerService import org.matrix.vector.daemon.ipc.ModuleService import org.matrix.vector.daemon.system.* -import org.matrix.vector.daemon.system.NotificationManager private const val TAG = "VectorService" @@ -92,10 +92,11 @@ object VectorService : ILSPosedService.Stub() { sendingUser: Int ) { ioScope.launch { - if (intent.action == Intent.ACTION_LOCKED_BOOT_COMPLETED) { - dispatchBootCompleted() - } else { - dispatchPackageChanged(intent) + when (intent.action) { + Intent.ACTION_LOCKED_BOOT_COMPLETED -> dispatchBootCompleted() + NotificationManager.openManagerAction -> ManagerService.openManager(intent.data) + NotificationManager.moduleScopeAction -> dispatchModuleScope(intent) + else -> dispatchPackageChanged(intent) } } @@ -123,16 +124,38 @@ object VectorService : ILSPosedService.Stub() { addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED) addDataScheme("package") } + val uidFilter = IntentFilter(Intent.ACTION_UID_REMOVED) + val bootFilter = IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED).apply { priority = IntentFilter.SYSTEM_HIGH_PRIORITY } + val openManagerFilter = + IntentFilter(NotificationManager.openManagerAction).apply { + addDataScheme("module") + addDataScheme("android_secret_code") + } + + val scopeFilter = + IntentFilter(NotificationManager.moduleScopeAction).apply { addDataScheme("module") } + + // For the secret dialer code (*#*#5776733#*#*) + val secretCodeFilter = + IntentFilter("android.provider.Telephony.SECRET_CODE").apply { + addDataScheme("android_secret_code") + addDataAuthority("5776733", null) + } + // Use a receiver instance for each registration to prevent AMS conflicts activityManager?.registerReceiverCompat(createReceiver(), packageFilter, null, -1, 0) activityManager?.registerReceiverCompat(createReceiver(), uidFilter, null, -1, 0) activityManager?.registerReceiverCompat(createReceiver(), bootFilter, null, 0, 0) + activityManager?.registerReceiverCompat(createReceiver(), openManagerFilter, null, 0, 0) + activityManager?.registerReceiverCompat(createReceiver(), scopeFilter, null, 0, 0) + activityManager?.registerReceiverCompat( + createReceiver(), secretCodeFilter, "android.permission.CONTROL_INCALL_EXPERIENCE", 0, 0) // UID Observer val uidObserver = @@ -229,4 +252,57 @@ object VectorService : ILSPosedService.Stub() { activityManager?.broadcastIntentCompat(notifyIntent) } } + + private fun dispatchModuleScope(intent: Intent) { + val data = intent.data ?: return + val extras = intent.extras ?: return + val callbackBinder = extras.getBinder("callback") ?: return + if (!callbackBinder.isBinderAlive) return + + val authority = data.encodedAuthority ?: return + val parts = authority.split(":", limit = 2) + if (parts.size != 2) return + val packageName = parts[0] + val userId = parts[1].toIntOrNull() ?: return + + val scopePackageName = data.path?.substring(1) ?: return // remove leading '/' + val action = data.getQueryParameter("action") ?: return + + val iCallback = IXposedScopeCallback.Stub.asInterface(callbackBinder) + runCatching { + val appInfo = packageManager?.getPackageInfoCompat(scopePackageName, 0, userId) + if (appInfo == null) { + iCallback.onScopeRequestFailed("Package not found") + return + } + when (action) { + "approve" -> { + val scopes = ConfigCache.getModuleScope(packageName) ?: mutableListOf() + if (scopes.none { it.packageName == scopePackageName && it.userId == userId }) { + scopes.add( + org.lsposed.lspd.models.Application().apply { + this.packageName = scopePackageName + this.userId = userId + }) + ConfigCache.setModuleScope(packageName, scopes) + } + iCallback.onScopeRequestApproved(listOf(scopePackageName)) + } + "deny" -> iCallback.onScopeRequestFailed("Request denied by user") + "delete" -> iCallback.onScopeRequestFailed("Request timeout") + "block" -> { + val blocked = + ConfigCache.getModulePrefs("lspd", 0, "config")["scope_request_blocked"] + as? Set ?: emptySet() + ConfigCache.updateModulePref( + "lspd", 0, "config", "scope_request_blocked", blocked + packageName) + iCallback.onScopeRequestFailed("Request blocked by configuration") + } + } + } + .onFailure { runCatching { iCallback.onScopeRequestFailed(it.message) } } + + NotificationManager.cancelNotification( + NotificationManager.SCOPE_CHANNEL_ID, packageName, userId) + } } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt index 1b02f4fb0..4ceac069b 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt @@ -12,6 +12,7 @@ import org.lsposed.lspd.service.ILSPApplicationService import org.matrix.vector.daemon.data.ConfigCache import org.matrix.vector.daemon.data.FileSystem import org.matrix.vector.daemon.data.ProcessScope +import org.matrix.vector.daemon.utils.ObfuscationManager private const val TAG = "VectorAppService" private const val DEX_TRANSACTION_CODE = @@ -48,7 +49,7 @@ object ApplicationService : ILSPApplicationService.Stub() { } OBFUSCATION_MAP_TRANSACTION_CODE -> { val obfuscation = ConfigCache.isDexObfuscateEnabled() - val signatures = org.matrix.vector.daemon.utils.ObfuscationManager.getSignatures() + val signatures = ObfuscationManager.getSignatures() reply?.writeNoException() reply?.writeInt(signatures.size * 2) for ((key, value) in signatures) { diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt index 7d93cf5e4..238cf6dd5 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt @@ -10,6 +10,7 @@ import android.content.pm.PackageInstaller import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.content.pm.VersionedPackage +import android.net.Uri import android.os.Build import android.os.Bundle import android.os.IBinder @@ -32,7 +33,6 @@ import org.matrix.vector.daemon.env.Dex2OatServer import org.matrix.vector.daemon.env.LogcatMonitor import org.matrix.vector.daemon.system.* import org.matrix.vector.daemon.utils.getRealUsers -import org.matrix.vector.daemon.utils.performDexOptMode import rikka.parcelablelist.ParcelableListSlice private const val TAG = "VectorManagerService" @@ -44,6 +44,8 @@ object ManagerService : ILSPManagerService.Stub() { @Volatile private var pendingManager = false @Volatile private var isEnabled = true + private var managerIntent: Intent? = null + var guard: ManagerGuard? = null internal set @@ -123,6 +125,69 @@ object ManagerService : ILSPManagerService.Stub() { return true } + private fun getManagerIntent(): Intent? { + if (managerIntent != null) return managerIntent + runCatching { + var intent = + Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_INFO) + setPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME) + } + var ris = packageManager?.queryIntentActivitiesCompat(intent, intent.type, 0, 0) + + if (ris.isNullOrEmpty()) { + intent.removeCategory(Intent.CATEGORY_INFO) + intent.addCategory(Intent.CATEGORY_LAUNCHER) + ris = packageManager?.queryIntentActivitiesCompat(intent, intent.type, 0, 0) + } + + if (ris.isNullOrEmpty()) { + val pkgInfo = + packageManager?.getPackageInfoCompat( + BuildConfig.MANAGER_INJECTED_PKG_NAME, PackageManager.GET_ACTIVITIES, 0) + val activity = pkgInfo?.activities?.firstOrNull { it.processName == it.packageName } + if (activity != null) { + intent = + Intent(Intent.ACTION_MAIN).apply { + component = ComponentName(activity.packageName, activity.name) + } + } else return null + } else { + val activity = ris.first().activityInfo + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + intent.component = ComponentName(activity.packageName, activity.name) + } + + intent.categories?.clear() + intent.addCategory("org.lsposed.manager.LAUNCH_MANAGER") + intent.setPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME) + managerIntent = Intent(intent) + } + .onFailure { Log.e(TAG, "Failed to build manager intent", it) } + return managerIntent + } + + fun openManager(withData: Uri?) { + val intent = getManagerIntent() ?: return + val launchIntent = Intent(intent).apply { data = withData } + runCatching { + activityManager?.startActivityAsUserWithFeature( + SystemContext.appThread, + "android", + null, + launchIntent, + launchIntent.type, + null, + null, + 0, + 0, + null, + null, + 0) + } + .onFailure { Log.e(TAG, "Failed to open manager", it) } + } + fun postStartManager(pid: Int, uid: Int): Boolean = isEnabled && uid == BuildConfig.MANAGER_INJECTED_UID && pid == managerPid diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt index acc837ab2..36d5ebcf7 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt @@ -6,6 +6,12 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.AdaptiveIconDrawable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Icon +import android.graphics.drawable.LayerDrawable import android.net.Uri import android.os.Build import android.os.Bundle @@ -14,18 +20,20 @@ import io.github.libxposed.service.IXposedScopeCallback import java.util.UUID import org.matrix.vector.daemon.BuildConfig import org.matrix.vector.daemon.R +import org.matrix.vector.daemon.data.FileSystem import org.matrix.vector.daemon.utils.FakeContext private const val TAG = "VectorNotifManager" private const val STATUS_CHANNEL_ID = "lsposed_status" private const val UPDATED_CHANNEL_ID = "lsposed_module_updated" -private const val SCOPE_CHANNEL_ID = "lsposed_module_scope" private const val STATUS_NOTIF_ID = 2000 object NotificationManager { val openManagerAction = UUID.randomUUID().toString() val moduleScopeAction = UUID.randomUUID().toString() + val SCOPE_CHANNEL_ID = "vector_module_scope" + private val nm: android.app.INotificationManager? by SystemService( Context.NOTIFICATION_SERVICE, android.app.INotificationManager.Stub::asInterface) @@ -58,6 +66,28 @@ object NotificationManager { .onFailure { Log.e(TAG, "Failed to create notification channels", it) } } + private fun getBitmap(id: Int): Bitmap { + val r = FileSystem.resources + var res = r.getDrawable(id, r.newTheme()) + if (res is BitmapDrawable) { + return res.bitmap + } else { + if (res is AdaptiveIconDrawable) { + res = LayerDrawable(arrayOf(res.background, res.foreground)) + } + val bitmap = + Bitmap.createBitmap(res.intrinsicWidth, res.intrinsicHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + res.setBounds(0, 0, canvas.width, canvas.height) + res.draw(canvas) + return bitmap + } + } + + private fun getNotificationIcon(): Icon { + return Icon.createWithBitmap(getBitmap(R.drawable.ic_notification)) + } + fun notifyStatusNotification() { val context = FakeContext() val intent = Intent(openManagerAction).apply { setPackage("android") } @@ -69,7 +99,7 @@ object NotificationManager { Notification.Builder(context, STATUS_CHANNEL_ID) .setContentTitle(context.getString(R.string.vector_running_notification_title)) .setContentText(context.getString(R.string.vector_running_notification_content)) - .setSmallIcon(android.R.drawable.ic_dialog_info) // Fallback icon + .setSmallIcon(getNotificationIcon()) .setContentIntent(pi) .setVisibility(Notification.VISIBILITY_SECRET) .setOngoing(true) @@ -92,6 +122,21 @@ object NotificationManager { } } + fun cancelNotification(channel: String, modulePkg: String, moduleUserId: Int) { + runCatching { + // We use the module package name's hash code as the notification ID + // to match how we enqueued it in requestModuleScope and notifyModuleUpdated. + val notifId = modulePkg.hashCode() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + nm?.cancelNotificationWithTag("android", "android", modulePkg, notifId, 0) + } else { + nm?.cancelNotificationWithTag("android", modulePkg, notifId, 0) + } + } + .onFailure { Log.e(TAG, "Failed to cancel notification", it) } + } + fun requestModuleScope( modulePkg: String, moduleUserId: Int, @@ -127,7 +172,7 @@ object NotificationManager { .setContentText( context.getString( R.string.xposed_module_request_scope_content, modulePkg, userName, scopePkg)) - .setSmallIcon(android.R.drawable.ic_dialog_alert) + .setSmallIcon(getNotificationIcon()) .addAction( Notification.Action.Builder( null, @@ -207,7 +252,7 @@ object NotificationManager { Notification.Builder(context, UPDATED_CHANNEL_ID) .setContentTitle(title) .setContentText(content) - .setSmallIcon(android.R.drawable.ic_dialog_info) + .setSmallIcon(getNotificationIcon()) .setContentIntent(pi) .setVisibility(Notification.VISIBILITY_SECRET) .setAutoCancel(true) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt index a2ded5232..0013d8808 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt @@ -8,6 +8,7 @@ import android.database.DatabaseErrorHandler import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase.CursorFactory import android.os.Build +import hidden.HiddenApiBridge import java.io.File import org.matrix.vector.daemon.data.FileSystem import org.matrix.vector.daemon.system.packageManager as sysPackageManager @@ -28,6 +29,14 @@ class FakeContext(private val fakePackageName: String = "android") : ContextWrap override fun getOpPackageName(): String = "android" + fun getUserId(): Int { + return 0 + } + + fun getUser(): android.os.UserHandle { + return HiddenApiBridge.UserHandle(0) + } + override fun getApplicationInfo(): ApplicationInfo { if (systemAppInfo == null) { systemAppInfo = From 396f738cdf7c01158fd44afaf40858fb8a3729f6 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 04:52:27 +0100 Subject: [PATCH 15/38] Fix manager --- daemon/build.gradle.kts | 1 + .../matrix/vector/daemon/core/VectorDaemon.kt | 31 ++- .../vector/daemon/core/VectorService.kt | 45 +++- .../matrix/vector/daemon/data/ConfigCache.kt | 201 +++++++++++++++--- .../matrix/vector/daemon/env/Dex2OatServer.kt | 3 + .../vector/daemon/ipc/ApplicationService.kt | 67 +++--- .../vector/daemon/ipc/ManagerService.kt | 2 +- .../matrix/vector/daemon/ipc/ModuleService.kt | 3 +- .../vector/daemon/ipc/SystemServerService.kt | 2 + .../vector/daemon/utils/InstallerVerifier.kt | 28 +++ 10 files changed, 303 insertions(+), 80 deletions(-) create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/utils/InstallerVerifier.kt diff --git a/daemon/build.gradle.kts b/daemon/build.gradle.kts index 4eeed13f2..bc5b9275a 100644 --- a/daemon/build.gradle.kts +++ b/daemon/build.gradle.kts @@ -96,6 +96,7 @@ android.applicationVariants.all { dependencies { implementation(libs.agp.apksig) implementation(libs.kotlinx.coroutines.android) + implementation(libs.kotlinx.coroutines.core) implementation(projects.external.apache) implementation(projects.hiddenapi.bridge) implementation(projects.services.daemonService) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt index d0adea587..721523a0a 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt @@ -3,10 +3,15 @@ package org.matrix.vector.daemon.core import android.app.ActivityThread import android.content.Context import android.ddm.DdmHandleAppName +import android.os.Build import android.os.Looper import android.os.Process +import android.os.ServiceManager import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.matrix.vector.daemon.BuildConfig import org.matrix.vector.daemon.data.ConfigCache @@ -45,16 +50,20 @@ object VectorDaemon { kotlin.system.exitProcess(1) } - // 1. Start Environmental Daemons + // Start Environmental Daemons LogcatMonitor.start() - if (ConfigCache.isLogWatchdogEnabled()) - LogcatMonitor.enableWatchdog() // Needs impl in LogcatMonitor - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { Dex2OatServer.start() } - // 2. Setup Main Looper & System Services + // Accessing the object triggers the `init` block, reading SQLite instantly. + if (ConfigCache.isLogWatchdogEnabled()) LogcatMonitor.enableWatchdog() + // Preload Framework DEX in the background + CoroutineScope(Dispatchers.IO).launch { + FileSystem.getPreloadDex(ConfigCache.isDexObfuscateEnabled()) + } + + // Setup Main Looper & System Services Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND) @Suppress("DEPRECATION") Looper.prepareMainLooper() @@ -65,7 +74,7 @@ object VectorDaemon { ActivityThread.systemMain() DdmHandleAppName.setAppName("org.matrix.vector.daemon", 0) - // 3. Wait for Android Core Services + // Wait for Android Core Services waitForSystemService("package") waitForSystemService("activity") waitForSystemService(Context.USER_SERVICE) @@ -73,12 +82,12 @@ object VectorDaemon { applyNotificationWorkaround() - // 4. Inject Vector into system_server + // Inject Vector into system_server SystemServerBridge.sendToBridge( VectorService.asBinder(), isRestart = false, systemServerService) if (!ManagerService.isVerboseLog()) { - LogcatMonitor.stopVerbose() // Needs impl in LogcatMonitor + LogcatMonitor.stopVerbose() } Looper.loop() @@ -86,8 +95,8 @@ object VectorDaemon { } private fun waitForSystemService(name: String) = runBlocking { - while (android.os.ServiceManager.getService(name) == null) { - Log.i(TAG, "Waiting for system service: $name") + while (ServiceManager.getService(name) == null) { + Log.i(TAG, "Waiting system service: $name for 1s") delay(1000) } } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt index 209b30799..3cf43a6d8 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt @@ -1,6 +1,7 @@ package org.matrix.vector.daemon.core import android.app.IApplicationThread +import android.content.Context import android.content.IIntentReceiver import android.content.Intent import android.content.IntentFilter @@ -132,7 +133,9 @@ object VectorService : ILSPosedService.Stub() { priority = IntentFilter.SYSTEM_HIGH_PRIORITY } - val openManagerFilter = + val openManagerNoDataFilter = IntentFilter(NotificationManager.openManagerAction) + + val openManagerDataFilter = IntentFilter(NotificationManager.openManagerAction).apply { addDataScheme("module") addDataScheme("android_secret_code") @@ -140,22 +143,36 @@ object VectorService : ILSPosedService.Stub() { val scopeFilter = IntentFilter(NotificationManager.moduleScopeAction).apply { addDataScheme("module") } - - // For the secret dialer code (*#*#5776733#*#*) val secretCodeFilter = IntentFilter("android.provider.Telephony.SECRET_CODE").apply { addDataScheme("android_secret_code") addDataAuthority("5776733", null) } - // Use a receiver instance for each registration to prevent AMS conflicts - activityManager?.registerReceiverCompat(createReceiver(), packageFilter, null, -1, 0) - activityManager?.registerReceiverCompat(createReceiver(), uidFilter, null, -1, 0) - activityManager?.registerReceiverCompat(createReceiver(), bootFilter, null, 0, 0) - activityManager?.registerReceiverCompat(createReceiver(), openManagerFilter, null, 0, 0) - activityManager?.registerReceiverCompat(createReceiver(), scopeFilter, null, 0, 0) + // Define strict Android 14+ flags and the system-only BRICK permission + val notExported = Context.RECEIVER_NOT_EXPORTED + val exported = Context.RECEIVER_EXPORTED + val brickPerm = "android.permission.BRICK" + activityManager?.registerReceiverCompat( - createReceiver(), secretCodeFilter, "android.permission.CONTROL_INCALL_EXPERIENCE", 0, 0) + createReceiver(), packageFilter, brickPerm, -1, notExported) + activityManager?.registerReceiverCompat(createReceiver(), uidFilter, brickPerm, -1, notExported) + activityManager?.registerReceiverCompat(createReceiver(), bootFilter, brickPerm, 0, notExported) + + activityManager?.registerReceiverCompat( + createReceiver(), openManagerNoDataFilter, brickPerm, 0, notExported) + activityManager?.registerReceiverCompat( + createReceiver(), openManagerDataFilter, brickPerm, 0, notExported) + activityManager?.registerReceiverCompat( + createReceiver(), scopeFilter, brickPerm, 0, notExported) + + // Only the secret dialer code needs to be exported so the phone app can trigger it + activityManager?.registerReceiverCompat( + createReceiver(), + secretCodeFilter, + "android.permission.CONTROL_INCALL_EXPERIENCE", + 0, + exported) // UID Observer val uidObserver = @@ -236,6 +253,13 @@ object VectorService : ILSPosedService.Stub() { } } + val removed = + action == Intent.ACTION_PACKAGE_FULLY_REMOVED || action == Intent.ACTION_UID_REMOVED + if (moduleName == BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME && userId == 0) { + Log.d(TAG, "Manager updated") + ConfigCache.updateManager(removed) + } + // Broadcast back to Manager if (moduleName != null) { val notifyIntent = @@ -253,6 +277,7 @@ object VectorService : ILSPosedService.Stub() { } } + @Suppress("UNCHECKED_CAST") private fun dispatchModuleScope(intent: Intent) { val data = intent.data ?: return val extras = intent.extras ?: return diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt index d7cc1a58d..589bab04f 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt @@ -7,6 +7,11 @@ import android.system.Os import android.util.Log import hidden.HiddenApiBridge import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.attribute.PosixFilePermissions +import java.util.UUID import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -17,12 +22,8 @@ import org.lsposed.lspd.models.Application import org.lsposed.lspd.models.Module import org.matrix.vector.daemon.BuildConfig import org.matrix.vector.daemon.ipc.InjectedModuleService -import org.matrix.vector.daemon.system.MATCH_ALL_FLAGS -import org.matrix.vector.daemon.system.PER_USER_RANGE -import org.matrix.vector.daemon.system.fetchProcesses -import org.matrix.vector.daemon.system.getPackageInfoWithComponents -import org.matrix.vector.daemon.system.packageManager -import org.matrix.vector.daemon.system.userManager +import org.matrix.vector.daemon.ipc.ManagerService +import org.matrix.vector.daemon.system.* import org.matrix.vector.daemon.utils.getRealUsers private const val TAG = "VectorConfigCache" @@ -33,6 +34,10 @@ object ConfigCache { @Volatile var api: String = "(???)" @Volatile var enableStatusNotification = true + @Volatile private var managerUid = -1 + @Volatile private var isCacheReady = false + @Volatile private var miscPath: Path? = null + val dbHelper = Database() // Thread-safe maps for IPC readers @@ -43,16 +48,81 @@ object ConfigCache { private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) // A conflated channel automatically drops older pending events if a new one arrives. - // This perfectly replaces the manual `lastModuleCacheTime` timestamp logic! private val cacheUpdateChannel = Channel(Channel.CONFLATED) init { - // Start the background consumer scope.launch { for (request in cacheUpdateChannel) { performCacheUpdate() } } + + initializeConfig() + } + + private fun initializeConfig() { + val config = getModulePrefs("lspd", 0, "config") + + ManagerService.isVerboseLog = config["enable_verbose_log"] as? Boolean ?: true + enableStatusNotification = config["enable_status_notification"] as? Boolean ?: true + + // Clean up legacy setting + if (config["enable_auto_add_shortcut"] != null) { + updateModulePref("lspd", 0, "config", "enable_auto_add_shortcut", null) + } + + // Initialize miscPath + val pathStr = config["misc_path"] as? String + if (pathStr == null) { + val newPath = Paths.get("/data/misc", UUID.randomUUID().toString()) + updateModulePref("lspd", 0, "config", "misc_path", newPath.toString()) + miscPath = newPath + } else { + miscPath = Paths.get(pathStr) + } + + runCatching { + val perms = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx--x--x")) + java.nio.file.Files.createDirectories(miscPath!!, perms) + FileSystem.setSelinuxContextRecursive(miscPath!!, "u:object_r:xposed_data:s0") + } + .onFailure { Log.e(TAG, "Failed to create misc directory", it) } + } + + private fun ensureCacheReady() { + // Lazy Execution (Wait for PackageManager) + if (!isCacheReady && packageManager?.asBinder()?.isBinderAlive == true) { + synchronized(this) { + if (!isCacheReady) { + Log.i(TAG, "System services are ready. Mapping modules and scopes.") + updateManager(false) + forceCacheUpdateSync() + isCacheReady = true + } + } + } + } + + fun updateManager(uninstalled: Boolean) { + if (uninstalled) { + managerUid = -1 + return + } + if (packageManager?.asBinder()?.isBinderAlive == true) { + runCatching { + val info = + packageManager?.getPackageInfoCompat(BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME, 0, 0) + managerUid = info?.applicationInfo?.uid ?: -1 + if (managerUid == -1) Log.i(TAG, "Manager is not installed") + } + .onFailure { managerUid = -1 } + } + } + + fun isManager(uid: Int): Boolean { + ensureCacheReady() + return uid == managerUid || uid == BuildConfig.MANAGER_INJECTED_UID } /** @@ -74,8 +144,11 @@ object ConfigCache { Log.d(TAG, "Executing Cache Update...") val db = dbHelper.readableDatabase - // 1. Fetch enabled modules + // Fetch enabled modules val newModules = mutableMapOf() + val obsoleteModules = mutableSetOf() + val obsoletePaths = mutableMapOf() + db.query( "modules", arrayOf("module_pkg_name", "apk_path"), @@ -87,27 +160,84 @@ object ConfigCache { .use { cursor -> while (cursor.moveToNext()) { val pkgName = cursor.getString(0) - val apkPath = cursor.getString(1) + var apkPath = cursor.getString(1) if (pkgName == "lspd") continue - val isObfuscateEnabled = true - val preLoadedApk = FileSystem.loadModule(apkPath, isObfuscateEnabled) + val oldModule = cachedModules[pkgName] + + // Find the PackageInfo across all users to get UID and ApplicationInfo + var pkgInfo: android.content.pm.PackageInfo? = null + val users = userManager?.getRealUsers() ?: emptyList() + for (user in users) { + pkgInfo = packageManager?.getPackageInfoCompat(pkgName, MATCH_ALL_FLAGS, user.id) + if (pkgInfo?.applicationInfo != null) break + } + + if (pkgInfo?.applicationInfo == null) { + Log.w(TAG, "Failed to find package info of $pkgName") + obsoleteModules.add(pkgName) + continue + } + + val appInfo = pkgInfo.applicationInfo + + // Optimization: Check if the APK has changed, skip parsing if identical + if (oldModule != null && + appInfo?.sourceDir != null && + apkPath != null && + oldModule.apkPath != null && + FileSystem.toGlobalNamespace(apkPath).exists() && + apkPath == oldModule.apkPath && + File(appInfo.sourceDir).parent == File(apkPath).parent) { + + if (oldModule.appId != -1) { + Log.d(TAG, "$pkgName did not change, skip caching it") + } else { + // System server edge case: update app info + oldModule.applicationInfo = appInfo + } + newModules[pkgName] = oldModule + continue + } + + // Update APK path if it shifted during an update + val realApkPath = getModuleApkPath(appInfo!!) + if (realApkPath == null) { + Log.w(TAG, "Failed to find path of $pkgName") + obsoleteModules.add(pkgName) + continue + } else { + apkPath = realApkPath + obsoletePaths[pkgName] = realApkPath + } + + // Load the actual DEX and construct the Module + val preLoadedApk = FileSystem.loadModule(apkPath, isDexObfuscateEnabled()) if (preLoadedApk != null) { val module = Module() module.packageName = pkgName module.apkPath = apkPath + module.appId = appInfo.uid + module.applicationInfo = appInfo + module.service = oldModule?.service ?: InjectedModuleService(pkgName) module.file = preLoadedApk - // Note: module.appId, module.applicationInfo, and module.service - // will be populated in Phase 4 when we implement InjectedModuleService + newModules[pkgName] = module } else { Log.w(TAG, "Failed to parse DEX/ZIP for $pkgName, skipping.") + obsoleteModules.add(pkgName) } } } - // 2. Fetch scopes and map heavy PM logic + // Clean up obsolete data to keep the database perfectly synced with Android + if (packageManager?.asBinder()?.isBinderAlive == true) { + obsoleteModules.forEach { removeModule(it) } + obsoletePaths.forEach { (pkg, path) -> updateModuleApkPath(pkg, path, true) } + } + + // Fetch scopes and map heavy PM logic val newScopes = ConcurrentHashMap>() db.query( "scope INNER JOIN modules ON scope.mid = modules.mid", @@ -129,7 +259,7 @@ object ConfigCache { // Ensure the module is actually valid and loaded val module = newModules[modPkg] ?: continue - // Heavy logic: Fetch associated processes + // Fetch associated processes val pkgInfo = packageManager?.getPackageInfoWithComponents(appPkg, MATCH_ALL_FLAGS, userId) if (pkgInfo?.applicationInfo == null) continue @@ -158,17 +288,24 @@ object ConfigCache { } } - // 3. Atomically swap the memory cache + // Atomically swap the memory cache cachedModules.clear() cachedModules.putAll(newModules) cachedScopes.clear() cachedScopes.putAll(newScopes) - Log.d(TAG, "Cache Update Complete. Modules: ${cachedModules.size}") + Log.d(TAG, "cached modules") + cachedModules.forEach { (pkg, mod) -> Log.d(TAG, "$pkg ${mod.apkPath}") } + Log.d(TAG, "cached scope") + cachedScopes.forEach { (ps, modules) -> + Log.d(TAG, "${ps.processName}/${ps.uid}") + modules.forEach { mod -> Log.d(TAG, "\t${mod.packageName}") } + } } fun getModulesForProcess(processName: String, uid: Int): List { + ensureCacheReady() return cachedScopes[ProcessScope(processName, uid)] ?: emptyList() } @@ -325,9 +462,6 @@ object ConfigCache { arrayOf(moduleName, userId.toString(), group)) } - // --- Helpers --- - fun isManager(uid: Int): Boolean = uid == BuildConfig.MANAGER_INJECTED_UID - fun getModuleByUid(uid: Int): Module? = cachedModules.values.firstOrNull { it.appId == uid % PER_USER_RANGE } @@ -409,11 +543,27 @@ object ConfigCache { } fun getPrefsPath(packageName: String, uid: Int): String { + ensureCacheReady() + + // Strictly enforce that miscPath exists. + val basePath = + miscPath + ?: throw IllegalStateException("Fatal: miscPath was not initialized from the database!") + val userId = uid / PER_USER_RANGE - val path = - FileSystem.basePath.resolve( - "misc/prefs${if (userId == 0) "" else userId.toString()}/$packageName") - // Apply Os.chown to path here + val userSuffix = if (userId == 0) "" else userId.toString() + val path = basePath.resolve("prefs$userSuffix").resolve(packageName) + + val module = cachedModules[packageName] + if (module != null && module.appId == uid % PER_USER_RANGE) { + runCatching { + val perms = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx--x--x")) + Files.createDirectories(path, perms) + Files.walk(path).forEach { p -> Os.chown(p.toString(), uid, uid) } + } + .onFailure { Log.e(TAG, "Failed to prepare prefs path", it) } + } return path.toString() } @@ -485,6 +635,7 @@ object ConfigCache { } fun shouldSkipProcess(scope: ProcessScope): Boolean { + ensureCacheReady() return !cachedScopes.containsKey(scope) && !isManager(scope.uid) } } 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 1b5669265..f8563b1ec 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 @@ -201,7 +201,10 @@ object Dex2OatServer { } private fun runSocketLoop() { + Log.i(TAG, "Dex2oat wrapper daemon start") val sockPath = getSockPath() + Log.d(TAG, "wrapper path: $sockPath") + val xposedFile = "u:object_r:xposed_file:s0" val dex2oatExec = "u:object_r:dex2oat_exec:s0" diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt index 4ceac069b..db0b2db64 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt @@ -11,7 +11,7 @@ import org.lsposed.lspd.models.Module import org.lsposed.lspd.service.ILSPApplicationService import org.matrix.vector.daemon.data.ConfigCache import org.matrix.vector.daemon.data.FileSystem -import org.matrix.vector.daemon.data.ProcessScope +import org.matrix.vector.daemon.utils.InstallerVerifier import org.matrix.vector.daemon.utils.ObfuscationManager private const val TAG = "VectorAppService" @@ -22,19 +22,20 @@ private const val OBFUSCATION_MAP_TRANSACTION_CODE = object ApplicationService : ILSPApplicationService.Stub() { - // Tracks active processes linked to their heartbeat binders - private val processes = ConcurrentHashMap() + data class ProcessKey(val uid: Int, val pid: Int) - private class ProcessInfo(val scope: ProcessScope, val heartBeat: IBinder) : + private val processes = ConcurrentHashMap() + + private class ProcessInfo(val key: ProcessKey, val processName: String, val heartBeat: IBinder) : IBinder.DeathRecipient { init { heartBeat.linkToDeath(this, 0) - processes[scope] = this + processes[key] = this } override fun binderDied() { heartBeat.unlinkToDeath(this, 0) - processes.remove(scope) + processes.remove(key) } } @@ -64,67 +65,69 @@ object ApplicationService : ILSPApplicationService.Stub() { fun registerHeartBeat(uid: Int, pid: Int, processName: String, heartBeat: IBinder): Boolean { return runCatching { - ProcessInfo(ProcessScope(processName, uid), heartBeat) + ProcessInfo(ProcessKey(uid, pid), processName, heartBeat) true } .getOrDefault(false) } - fun hasRegister(uid: Int, pid: Int): Boolean { - // We only check UID here as the map key is ProcessScope, but PID is implied by the active - // heartbeat. - return processes.keys.any { it.uid == uid } - } + fun hasRegister(uid: Int, pid: Int): Boolean = processes.containsKey(ProcessKey(uid, pid)) - private fun ensureRegistered(): ProcessScope { - val uid = getCallingUid() - val scope = processes.keys.firstOrNull { it.uid == uid } - if (scope == null) { - Log.w(TAG, "Unauthorized IPC call from uid=$uid") + private fun ensureRegistered(): ProcessInfo { + val key = ProcessKey(getCallingUid(), getCallingPid()) + val info = processes[key] + if (info == null) { + Log.w(TAG, "Unauthorized IPC call from uid=${key.uid} pid=${key.pid}") throw RemoteException("Not registered") } - return scope + return info } override fun getModulesList(): List { - val scope = ensureRegistered() - if (scope.uid == Process.SYSTEM_UID && scope.processName == "system") { + val info = ensureRegistered() + if (info.key.uid == Process.SYSTEM_UID && info.processName == "system") { return ConfigCache.getModulesForSystemServer() // Needs implementation in ConfigCache } - if (ManagerService.isRunningManager(getCallingPid(), scope.uid)) { + if (ManagerService.isRunningManager(getCallingPid(), info.key.uid)) { return emptyList() } - return ConfigCache.getModulesForProcess(scope.processName, scope.uid).filter { !it.file.legacy } + return ConfigCache.getModulesForProcess(info.processName, info.key.uid).filter { + !it.file.legacy + } } override fun getLegacyModulesList(): List { - val scope = ensureRegistered() - return ConfigCache.getModulesForProcess(scope.processName, scope.uid).filter { it.file.legacy } + val info = ensureRegistered() + return ConfigCache.getModulesForProcess(info.processName, info.key.uid).filter { + it.file.legacy + } } override fun isLogMuted(): Boolean = !ManagerService.isVerboseLog override fun getPrefsPath(packageName: String): String { - val scope = ensureRegistered() - return ConfigCache.getPrefsPath(packageName, scope.uid) // Needs implementation in ConfigCache + val info = ensureRegistered() + return ConfigCache.getPrefsPath(packageName, info.key.uid) } override fun requestInjectedManagerBinder( binderList: MutableList ): ParcelFileDescriptor? { - val scope = ensureRegistered() - val pid = getCallingPid() + val info = ensureRegistered() + val pid = info.key.pid + val uid = info.key.uid - if (ManagerService.postStartManager(pid, scope.uid) || ConfigCache.isManager(scope.uid)) { - val heartBeat = processes[scope]?.heartBeat ?: throw RemoteException("No heartbeat") - binderList.add(ManagerService.obtainManagerBinder(heartBeat, pid, scope.uid)) + if (ManagerService.postStartManager(pid, uid) || ConfigCache.isManager(uid)) { + binderList.add(ManagerService.obtainManagerBinder(info.heartBeat, pid, uid)) } return runCatching { + // Verify the APK signature before serving it + InstallerVerifier.verifyInstallerSignature(FileSystem.managerApkPath.toString()) ParcelFileDescriptor.open( FileSystem.managerApkPath.toFile(), ParcelFileDescriptor.MODE_READ_ONLY) } - .onFailure { Log.e(TAG, "Failed to open manager APK", it) } + .onFailure { Log.e(TAG, "Failed to open or verify manager APK", it) } .getOrNull() } } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt index 238cf6dd5..c1ef31b98 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt @@ -204,7 +204,7 @@ object ManagerService : ILSPManagerService.Stub() { override fun getXposedVersionName() = BuildConfig.VERSION_NAME - override fun getApi() = "Zygisk" // To be removed + override fun getApi() = ConfigCache.api override fun getInstalledPackagesFromAllUsers( flags: Int, diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt index c2f641346..cbd3fd091 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt @@ -16,6 +16,7 @@ import org.lsposed.lspd.models.Module import org.matrix.vector.daemon.BuildConfig import org.matrix.vector.daemon.data.ConfigCache import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.system.NotificationManager import org.matrix.vector.daemon.system.PER_USER_RANGE import org.matrix.vector.daemon.system.activityManager @@ -113,7 +114,7 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() { val userId = ensureModule() if (!ConfigCache.isScopeRequestBlocked(loadedModule.packageName)) { packages.forEach { pkg -> - // Handled in Phase 5: NotificationManager.requestModuleScope() + NotificationManager.requestModuleScope(loadedModule.packageName, userId, pkg, callback) } } else { callback.onScopeRequestFailed("Scope request blocked by user configuration") 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 7a5c4012d..ff0b67110 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 @@ -29,6 +29,8 @@ class SystemServerService(private val maxRetry: Int, private val proxyServiceNam } init { + Log.d(TAG, "SystemServerService::SystemServerService with proxy $proxyServiceName") + requestedRetryCount = -maxRetry if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val callback = diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/InstallerVerifier.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/InstallerVerifier.kt new file mode 100644 index 000000000..dc7d00596 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/InstallerVerifier.kt @@ -0,0 +1,28 @@ +package org.matrix.vector.daemon.utils + +import com.android.apksig.ApkVerifier +import java.io.File +import java.io.IOException + +object InstallerVerifier { + + @Throws(IOException::class) + fun verifyInstallerSignature(path: String) { + val verifier = ApkVerifier.Builder(File(path)).setMinCheckedPlatformVersion(27).build() + + try { + val result = verifier.verify() + if (!result.isVerified) { + throw IOException("APK signature not verified") + } + + val mainCert = result.signerCertificates[0] + if (!mainCert.encoded.contentEquals(SignInfo.CERTIFICATE)) { + val dname = mainCert.subjectX500Principal.name + throw IOException("APK signature mismatch: $dname") + } + } catch (e: Exception) { + throw IOException(e) + } + } +} From f67ef06bdec3f46b96227adeb78574652f30de93 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 11:35:21 +0100 Subject: [PATCH 16/38] Fix manager status --- .../vector/daemon/ipc/ApplicationService.kt | 8 +++++-- .../vector/daemon/ipc/ManagerService.kt | 2 +- .../vector/daemon/ipc/SystemServerService.kt | 22 +++++++++---------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt index db0b2db64..ec9b4cf78 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt @@ -15,9 +15,13 @@ import org.matrix.vector.daemon.utils.InstallerVerifier import org.matrix.vector.daemon.utils.ObfuscationManager private const val TAG = "VectorAppService" -private const val DEX_TRANSACTION_CODE = + +// Hardcoded transaction code from BridgeService +const val BRIDGE_TRANSACTION_CODE = + ('_'.code shl 24) or ('V'.code shl 16) or ('E'.code shl 8) or 'C'.code +const val DEX_TRANSACTION_CODE = ('_'.code shl 24) or ('D'.code shl 16) or ('E'.code shl 8) or 'X'.code -private const val OBFUSCATION_MAP_TRANSACTION_CODE = +const val OBFUSCATION_MAP_TRANSACTION_CODE = ('_'.code shl 24) or ('O'.code shl 16) or ('B'.code shl 8) or 'F'.code object ApplicationService : ILSPApplicationService.Stub() { diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt index c1ef31b98..045ca3e03 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt @@ -341,7 +341,7 @@ object ManagerService : ILSPManagerService.Stub() { .getOrDefault(-110) } - override fun systemServerRequested() = SystemServerService.systemServerRequested() + override fun systemServerRequested() = SystemServerService.systemServerRequested override fun startActivityAsUserWithFeature(intent: Intent, userId: Int): Int { if (!intent.getBooleanExtra("lsp_no_switch_to_user", false)) { 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 ff0b67110..27a80e131 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 @@ -18,20 +18,13 @@ class SystemServerService(private val maxRetry: Int, private val proxyServiceNam private var originService: IBinder? = null private var requestedRetryCount = -maxRetry - // Hardcoded transaction code from BridgeService - private val BRIDGE_TRANSACTION_CODE = - ('_'.code shl 24) or ('V'.code shl 16) or ('E'.code shl 8) or 'C'.code - companion object { - @Volatile var requestedRetryCount = 0 - - fun systemServerRequested() = requestedRetryCount > 0 + var systemServerRequested = false } init { - Log.d(TAG, "SystemServerService::SystemServerService with proxy $proxyServiceName") + Log.d(TAG, "registering via proxy $proxyServiceName") - requestedRetryCount = -maxRetry if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val callback = object : IServiceCallback.Stub() { @@ -63,8 +56,8 @@ class SystemServerService(private val maxRetry: Int, private val proxyServiceNam processName: String, heartBeat: IBinder? ): ILSPApplicationService? { - requestedRetryCount = 1 if (uid != 1000 || heartBeat == null || processName != "system") return null + systemServerRequested = true // Return the ApplicationService singleton if successfully registered return if (ApplicationService.registerHeartBeat(uid, pid, processName, heartBeat)) { @@ -92,8 +85,13 @@ class SystemServerService(private val maxRetry: Int, private val proxyServiceNam } return false } - // Route DEX and OBFUSCATION transactions to ApplicationService - else -> return ApplicationService.onTransact(code, data, reply, flags) + DEX_TRANSACTION_CODE, + OBFUSCATION_MAP_TRANSACTION_CODE -> { + return ApplicationService.onTransact(code, data, reply, flags) + } + else -> { + return super.onTransact(code, data, reply, flags) + } } } From 7b848e1902e06e9a4f2afd2dc778e5a2b155bee0 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 12:50:07 +0100 Subject: [PATCH 17/38] Add docs --- daemon/README.md | 63 +++++++++++++++++++ .../matrix/vector/daemon/ipc/ModuleService.kt | 1 - 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 daemon/README.md diff --git a/daemon/README.md b/daemon/README.md new file mode 100644 index 000000000..2542a9048 --- /dev/null +++ b/daemon/README.md @@ -0,0 +1,63 @@ +# Vector Daemon + +The Vector `daemon` is a highly privileged, standalone executable that runs as `root`. +It acts as the central coordinator and backend for the entire Vector framework. + +Unlike the injected framework code, the daemon does not hook methods directly. Instead, it manages state, provides IPC endpoints to hooked apps and modules, handles AOT compilation evasion, and interacts safely with Android system services. + +## Architecture Overview + +The daemon relies on a dual-IPC architecture and extensive use of Android Binder mechanisms to orchestrate the framework lifecycle without triggering SELinux denials or breaking system stability. + +1. **Bootstrapping & Bridge (`core/`)**: The daemon starts early in the boot process. It forces its primary Binder (`VectorService`) into `system_server` by hijacking transactions on the Android `activity` service. +2. **Privileged IPC Provider (`ipc/`)**: Android's sandbox prevents target processes from reading the framework APK, accessing SQLite databases, or resolving hidden ART symbols. The daemon exploits its root/system-level permissions to act as an asset server. It provides three critical components to hooked processes over Binder IPC: + * **Loader DEX**: Dispatched via `SharedMemory` to avoid disk-based detection and bypass SELinux `exec` restrictions. + * **Obfuscation Maps**: Pre-calculated maps that allow the injected framework to identify and hook internal ART structures regardless of the daemon's own obfuscation. + * **Dynamic Module Scopes**: Fast, in-memory lookups of which modules should be loaded into a specific UID/ProcessName. +3. **State Management (`data/`)**: To ensure IPC calls resolve in microseconds, the daemon caches SQLite database records (modules, scopes, preferences) in memory. This cache is lazily loaded only after Android's `PackageManager` becomes available. +4. **Native Environment (`env/` & JNI)**: Background threads (C++ and Kotlin Coroutines) handle low-level system subversion, including `dex2oat` compilation hijacking and logcat monitoring. + +## Directory Layout + +```text +src/main/ +├── kotlin/org/matrix/vector/daemon/ +│ ├── core/ # Entry point (Main), looper setup, and OS broadcast receivers +│ ├── ipc/ # AIDL implementations (Manager, Module, App, SystemServer endpoints) +│ ├── data/ # SQLite DB, in-memory ConcurrentHashMap cache, File & ZIP parsing +│ ├── system/ # System binder wrappers, UID observers, Notification UI +│ ├── env/ # Socket servers and monitors communicating with JNI (dex2oat, logcat) +│ └── utils/ # OEM-specific workarounds, FakeContext, JNI bindings +└── jni/ # Native C++ layer (dex2oat wrapper, logcat watcher, slicer obfuscation) +``` + +## Core Technical Mechanisms + +### 1. IPC Routing (The Two Doors) +* **Door 1 (`SystemServerService`)**: A native-to-native entry point used exclusively for the **System-Level Initialization** of `system_server`. By proxying the hardware `serial` service (via `IServiceCallback`), the daemon provides a rendezvous point accessible to the system before the Activity Manager is even initialized. It handles raw UID/PID/Heartbeat packets to authorize the base system framework hook. +* **Door 2 (`VectorService`)**: The **Application-Level Entrance** used by user-space apps. Since user apps are forbidden by SELinux from accessing hardware services like `serial`, they use the "Activity Bridge" to reach the daemon. This door utilizes an action-based protocol (`kActionGetBinder`) allowing the daemon to perform **Scope Filtering**—matching the calling process against the `ConfigCache` before granting access to the framework. + + +### 2. AOT Compilation Hijacking (`dex2oat`) +To prevent Android's ART from inlining hooked methods (which makes them unhookable), Vector hijacks the Ahead-of-Time (AOT) compiler. +* **Mechanism**: The daemon (`Dex2OatServer`) mounts a C++ wrapper binary (`bin/dex2oatXX`) over the system's actual `dex2oat` binaries in the `/apex` mount namespace. +* **FD Passing**: When the wrapper executes, to read the original compiler or the `liboat_hook.so`, it opens a UNIX domain socket to the daemon. The daemon (running as root) opens the files and passes the File Descriptors (FDs) back to the wrapper via `SCM_RIGHTS`. +* **Execution**: The wrapper uses `memfd_create` and `sendfile` to load the hook, bypassing execute restrictions, and uses `LD_PRELOAD` to inject the hook into the real `dex2oat` process while appending `--inline-max-code-units=0`. + +### 3. Dex Obfuscation & Zero-Copy Memory +The framework DEX is passed to target apps via Android's `SharedMemory` API. +To protect Xposed API from reflections, `ObfuscationManager.kt` passes this memory buffer to JNI (`obfuscation.cpp`), which uses `slicer` to mutate Dalvik string pools in-place using `MAP_SHARED`. This ensures zero-copy manipulation; the Java side immediately sees the obfuscated DEX without reallocating buffers. + +### 4. Lifecycle & State Tracking +The daemon must precisely know which apps are installed and which processes are running. +* **Broadcasts**: `VectorService` registers a hidden `IIntentReceiver` to listen for `ACTION_PACKAGE_ADDED`, `REMOVED`, and `ACTION_LOCKED_BOOT_COMPLETED`. +* **UID Observers**: `IUidObserver` tracks `onUidActive` and `onUidGone`. When a process becomes active, the daemon uses a forged `ContentProvider` call to proactively push the `IXposedService` binder into the target process, bypassing standard `bindService` limitations. + +## Development & Maintenance Guidelines + +When modifying the daemon, strictly adhere to the following principles: + +1. **Never Block IPC Threads**: AIDL `onTransact` methods are called synchronously by the Android framework and target apps. Blocking these threads (e.g., by executing raw SQL queries or heavy I/O directly) will cause Application Not Responding (ANR) crashes system-wide. Always read from the in-memory `ConfigCache`. +2. **Resource Determinism**: The daemon runs indefinitely. Leaking a single `Cursor`, `ParcelFileDescriptor`, or `SharedMemory` instance will eventually exhaust system limits and crash the OS. Always use Kotlin's `.use { }` blocks or explicit C++ RAII wrappers for native resources. +3. **Isolate OEM Quirks**: Android OS behavior varies wildly between manufacturers (e.g., Lenovo hiding cloned apps in user IDs 900-909, MIUI killing background dual-apps). Place all OEM-specific logic in `utils/Workarounds.kt` to prevent core logic pollution. +4. **Context Forgery (`FakeContext`)**: The daemon does not have a real Android `Context`. To interact with system APIs that require one (like building Notifications or querying packages), use `FakeContext`. Be aware that standard `Context` methods may crash if not explicitly mocked. diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt index cbd3fd091..8b55ceb33 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt @@ -21,7 +21,6 @@ import org.matrix.vector.daemon.system.PER_USER_RANGE import org.matrix.vector.daemon.system.activityManager private const val TAG = "VectorModuleService" -private const val AUTHORITY_SUFFIX = ".lsposed" private const val SEND_BINDER = "send_binder" class ModuleService(private val loadedModule: Module) : IXposedService.Stub() { From f915fcf2020c5ef83937856a5fd0f15bbd81d0df Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 16:46:27 +0100 Subject: [PATCH 18/38] fix compilation --- daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java index 220f9b88b..f81025fb9 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java @@ -34,6 +34,7 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteStatement; +import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.Handler; From fec0f73c2139d4f83cd97a9f0dc0a09608ddb175 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 17:14:05 +0100 Subject: [PATCH 19/38] fix format --- daemon/build.gradle.kts | 135 ++++++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 69 deletions(-) diff --git a/daemon/build.gradle.kts b/daemon/build.gradle.kts index bc5b9275a..08633588a 100644 --- a/daemon/build.gradle.kts +++ b/daemon/build.gradle.kts @@ -9,73 +9,71 @@ val versionCodeProvider: Provider by rootProject.extra val versionNameProvider: Provider by rootProject.extra plugins { - alias(libs.plugins.agp.app) - alias(libs.plugins.kotlin) - alias(libs.plugins.ktfmt) + alias(libs.plugins.agp.app) + alias(libs.plugins.kotlin) + alias(libs.plugins.ktfmt) } android { - defaultConfig { - buildConfigField( - "String", - "DEFAULT_MANAGER_PACKAGE_NAME", - """"$defaultManagerPackageName"""", - ) - buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""") - buildConfigField("String", "MANAGER_INJECTED_PKG_NAME", """"$injectedPackageName"""") - buildConfigField("int", "MANAGER_INJECTED_UID", """$injectedPackageUid""") - buildConfigField("String", "VERSION_NAME", """"${versionNameProvider.get()}"""") - buildConfigField("long", "VERSION_CODE", versionCodeProvider.get()) - } + defaultConfig { + buildConfigField( + "String", + "DEFAULT_MANAGER_PACKAGE_NAME", + """"$defaultManagerPackageName"""", + ) + buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""") + buildConfigField("String", "MANAGER_INJECTED_PKG_NAME", """"$injectedPackageName"""") + buildConfigField("int", "MANAGER_INJECTED_UID", """$injectedPackageUid""") + buildConfigField("String", "VERSION_NAME", """"${versionNameProvider.get()}"""") + buildConfigField("long", "VERSION_CODE", versionCodeProvider.get()) + } - buildTypes { - all { - externalNativeBuild { cmake { arguments += "-DANDROID_ALLOW_UNDEFINED_SYMBOLS=true" } } - } - release { - isMinifyEnabled = true - proguardFiles("proguard-rules.pro") - } + buildTypes { + all { externalNativeBuild { cmake { arguments += "-DANDROID_ALLOW_UNDEFINED_SYMBOLS=true" } } } + release { + isMinifyEnabled = true + proguardFiles("proguard-rules.pro") } + } - externalNativeBuild { cmake { path("src/main/jni/CMakeLists.txt") } } + externalNativeBuild { cmake { path("src/main/jni/CMakeLists.txt") } } - namespace = "org.matrix.vector.daemon" + namespace = "org.matrix.vector.daemon" } android.applicationVariants.all { - val variantCapped = name.replaceFirstChar { it.uppercase() } - val variantLowered = name.lowercase() + val variantCapped = name.replaceFirstChar { it.uppercase() } + val variantLowered = name.lowercase() - val outSrcDir = layout.buildDirectory.dir("generated/source/signInfo/${variantLowered}").get() - val signInfoTask = - tasks.register("generate${variantCapped}SignInfo") { - dependsOn(":app:validateSigning${variantCapped}") - val sign = - rootProject - .project(":app") - .extensions - .getByType(ApplicationExtension::class.java) - .buildTypes - .named(variantLowered) - .get() - .signingConfig - val outSrc = file("$outSrcDir/org/matrix/vector/daemon/utils/SignInfo.kt") - outputs.file(outSrc) - doLast { - outSrc.parentFile.mkdirs() - val certificateInfo = - KeystoreHelper.getCertificateInfo( - sign?.storeType, - sign?.storeFile, - sign?.storePassword, - sign?.keyPassword, - sign?.keyAlias, - ) + val outSrcDir = layout.buildDirectory.dir("generated/source/signInfo/${variantLowered}").get() + val signInfoTask = + tasks.register("generate${variantCapped}SignInfo") { + dependsOn(":app:validateSigning${variantCapped}") + val sign = + rootProject + .project(":app") + .extensions + .getByType(ApplicationExtension::class.java) + .buildTypes + .named(variantLowered) + .get() + .signingConfig + val outSrc = file("$outSrcDir/org/matrix/vector/daemon/utils/SignInfo.kt") + outputs.file(outSrc) + doLast { + outSrc.parentFile.mkdirs() + val certificateInfo = + KeystoreHelper.getCertificateInfo( + sign?.storeType, + sign?.storeFile, + sign?.storePassword, + sign?.keyPassword, + sign?.keyAlias, + ) - PrintStream(outSrc) - .print( - """ + PrintStream(outSrc) + .print( + """ |package org.matrix.vector.daemon.utils | |object SignInfo { @@ -84,23 +82,22 @@ android.applicationVariants.all { certificateInfo.certificate.encoded.joinToString(",") }) |}""" - .trimMargin() - ) - } + .trimMargin()) } - // registeoJavaGeneratingTask(signInfoTask, outSrcDir.asFile) + } + // registeoJavaGeneratingTask(signInfoTask, outSrcDir.asFile) - kotlin.sourceSets.getByName(variantLowered) { kotlin.srcDir(signInfoTask.map { outSrcDir }) } + kotlin.sourceSets.getByName(variantLowered) { kotlin.srcDir(signInfoTask.map { outSrcDir }) } } dependencies { - implementation(libs.agp.apksig) - implementation(libs.kotlinx.coroutines.android) - implementation(libs.kotlinx.coroutines.core) - implementation(projects.external.apache) - implementation(projects.hiddenapi.bridge) - implementation(projects.services.daemonService) - implementation(projects.services.managerService) - compileOnly(libs.androidx.annotation) - compileOnly(projects.hiddenapi.stubs) + implementation(libs.agp.apksig) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.kotlinx.coroutines.core) + implementation(projects.external.apache) + implementation(projects.hiddenapi.bridge) + implementation(projects.services.daemonService) + implementation(projects.services.managerService) + compileOnly(libs.androidx.annotation) + compileOnly(projects.hiddenapi.stubs) } From c97d519ff79112d3ea40ab2a75d38d5ff8c15510 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 17:14:21 +0100 Subject: [PATCH 20/38] Add cache permission fix --- .../vector/daemon/ipc/ManagerService.kt | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt index 045ca3e03..d719d7ba4 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt @@ -15,9 +15,11 @@ import android.os.Build import android.os.Bundle import android.os.IBinder import android.os.ParcelFileDescriptor +import android.os.SELinux import android.os.SystemProperties import android.util.Log import android.view.IWindowManager +import hidden.HiddenApiBridge import io.github.libxposed.service.IXposedService import java.io.File import java.io.FileOutputStream @@ -191,8 +193,45 @@ object ManagerService : ILSPManagerService.Stub() { fun postStartManager(pid: Int, uid: Int): Boolean = isEnabled && uid == BuildConfig.MANAGER_INJECTED_UID && pid == managerPid + /** Fixes permissions for the WebView cache. */ + private fun fixWebViewPermissions(file: File, targetUid: Int) { + if (!file.exists()) return + + // Set the SELinux label that allows apps to read/write shared xposed data + SELinux.setFileContext(file.absolutePath, "u:object_r:xposed_file:s0") + + // Change ownership to the target UID (e.g., 2000) + runCatching { android.system.Os.chown(file.absolutePath, targetUid, targetUid) } + .onFailure { Log.e(TAG, "Failed to chown ${file.path}", it) } + + // Recurse into directories + if (file.isDirectory) { + file.listFiles()?.forEach { fixWebViewPermissions(it, targetUid) } + } + } + + private fun ensureWebViewPermission() { + val targetUid = BuildConfig.MANAGER_INJECTED_UID + runCatching { + val pkgInfo = + packageManager?.getPackageInfoCompat(BuildConfig.MANAGER_INJECTED_PKG_NAME, 0, 0) + ?: return@runCatching + + val dataDir = + HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(pkgInfo.applicationInfo) + val cacheDir = File(dataDir, "cache") + + if (!cacheDir.exists()) cacheDir.mkdirs() + fixWebViewPermissions(cacheDir, targetUid) + } + .onFailure { Log.w(TAG, "WebView permission fix failed", it) } + } + fun obtainManagerBinder(heartbeat: IBinder, pid: Int, uid: Int): IBinder { ManagerGuard(heartbeat, pid, uid) + if (uid == BuildConfig.MANAGER_INJECTED_UID) { + ensureWebViewPermission() + } return this } From 52022e127655eef4811c6b8aa62918e44286fed0 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 18:28:19 +0100 Subject: [PATCH 21/38] Refactor CacheConfig --- daemon/README.md | 15 +- .../matrix/vector/daemon/data/ConfigCache.kt | 456 +++++------------- .../matrix/vector/daemon/data/DaemonState.kt | 20 + .../vector/daemon/data/ModuleDatabase.kt | 173 +++++++ .../vector/daemon/data/PreferenceStore.kt | 84 ++++ 5 files changed, 415 insertions(+), 333 deletions(-) create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/data/DaemonState.kt create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt diff --git a/daemon/README.md b/daemon/README.md index 2542a9048..0f263b005 100644 --- a/daemon/README.md +++ b/daemon/README.md @@ -13,8 +13,8 @@ The daemon relies on a dual-IPC architecture and extensive use of Android Binder 2. **Privileged IPC Provider (`ipc/`)**: Android's sandbox prevents target processes from reading the framework APK, accessing SQLite databases, or resolving hidden ART symbols. The daemon exploits its root/system-level permissions to act as an asset server. It provides three critical components to hooked processes over Binder IPC: * **Loader DEX**: Dispatched via `SharedMemory` to avoid disk-based detection and bypass SELinux `exec` restrictions. * **Obfuscation Maps**: Pre-calculated maps that allow the injected framework to identify and hook internal ART structures regardless of the daemon's own obfuscation. - * **Dynamic Module Scopes**: Fast, in-memory lookups of which modules should be loaded into a specific UID/ProcessName. -3. **State Management (`data/`)**: To ensure IPC calls resolve in microseconds, the daemon caches SQLite database records (modules, scopes, preferences) in memory. This cache is lazily loaded only after Android's `PackageManager` becomes available. + * **Dynamic Module Scopes**: Fast, lock-free lookups of which modules should be loaded into a specific UID/ProcessName. +3. **State Management (`data/`)**: To ensure IPC calls resolve in microseconds without race conditions, the daemon uses an **Immutable State Container** (`DaemonState`). Module topology and scopes are built into a frozen snapshot in the background, which is atomically swapped into memory. High-volume module preference updates are isolated in a separate `PreferenceStore` to prevent state pollution. 4. **Native Environment (`env/` & JNI)**: Background threads (C++ and Kotlin Coroutines) handle low-level system subversion, including `dex2oat` compilation hijacking and logcat monitoring. ## Directory Layout @@ -24,7 +24,7 @@ src/main/ ├── kotlin/org/matrix/vector/daemon/ │ ├── core/ # Entry point (Main), looper setup, and OS broadcast receivers │ ├── ipc/ # AIDL implementations (Manager, Module, App, SystemServer endpoints) -│ ├── data/ # SQLite DB, in-memory ConcurrentHashMap cache, File & ZIP parsing +│ ├── data/ # SQLite DB, Immutable State (DaemonState, ConfigCache), PreferenceStore, File & ZIP parsing │ ├── system/ # System binder wrappers, UID observers, Notification UI │ ├── env/ # Socket servers and monitors communicating with JNI (dex2oat, logcat) │ └── utils/ # OEM-specific workarounds, FakeContext, JNI bindings @@ -35,8 +35,7 @@ src/main/ ### 1. IPC Routing (The Two Doors) * **Door 1 (`SystemServerService`)**: A native-to-native entry point used exclusively for the **System-Level Initialization** of `system_server`. By proxying the hardware `serial` service (via `IServiceCallback`), the daemon provides a rendezvous point accessible to the system before the Activity Manager is even initialized. It handles raw UID/PID/Heartbeat packets to authorize the base system framework hook. -* **Door 2 (`VectorService`)**: The **Application-Level Entrance** used by user-space apps. Since user apps are forbidden by SELinux from accessing hardware services like `serial`, they use the "Activity Bridge" to reach the daemon. This door utilizes an action-based protocol (`kActionGetBinder`) allowing the daemon to perform **Scope Filtering**—matching the calling process against the `ConfigCache` before granting access to the framework. - +* **Door 2 (`VectorService`)**: The **Application-Level Entrance** used by user-space apps. Since user apps are forbidden by SELinux from accessing hardware services like `serial`, they use the "Activity Bridge" to reach the daemon. This door utilizes an action-based protocol allowing the daemon to perform **Scope Filtering**—matching the calling process against the current `DaemonState` before granting access to the framework. ### 2. AOT Compilation Hijacking (`dex2oat`) To prevent Android's ART from inlining hooked methods (which makes them unhookable), Vector hijacks the Ahead-of-Time (AOT) compiler. @@ -46,18 +45,18 @@ To prevent Android's ART from inlining hooked methods (which makes them unhookab ### 3. Dex Obfuscation & Zero-Copy Memory The framework DEX is passed to target apps via Android's `SharedMemory` API. -To protect Xposed API from reflections, `ObfuscationManager.kt` passes this memory buffer to JNI (`obfuscation.cpp`), which uses `slicer` to mutate Dalvik string pools in-place using `MAP_SHARED`. This ensures zero-copy manipulation; the Java side immediately sees the obfuscated DEX without reallocating buffers. +To protect the Xposed API from reflections, `ObfuscationManager.kt` passes this memory buffer to JNI (`obfuscation.cpp`), which uses `slicer` to mutate Dalvik string pools in-place using `MAP_SHARED`. This ensures zero-copy manipulation; the Java side immediately sees the obfuscated DEX without reallocating buffers. ### 4. Lifecycle & State Tracking The daemon must precisely know which apps are installed and which processes are running. * **Broadcasts**: `VectorService` registers a hidden `IIntentReceiver` to listen for `ACTION_PACKAGE_ADDED`, `REMOVED`, and `ACTION_LOCKED_BOOT_COMPLETED`. -* **UID Observers**: `IUidObserver` tracks `onUidActive` and `onUidGone`. When a process becomes active, the daemon uses a forged `ContentProvider` call to proactively push the `IXposedService` binder into the target process, bypassing standard `bindService` limitations. +* **UID Observers**: `IUidObserver` tracks `onUidActive` and `onUidGone`. When a process becomes active, the daemon uses a forged `ContentProvider` call (`send_binder`) to proactively push the `IXposedService` binder into the target process, bypassing standard `bindService` limitations. ## Development & Maintenance Guidelines When modifying the daemon, strictly adhere to the following principles: -1. **Never Block IPC Threads**: AIDL `onTransact` methods are called synchronously by the Android framework and target apps. Blocking these threads (e.g., by executing raw SQL queries or heavy I/O directly) will cause Application Not Responding (ANR) crashes system-wide. Always read from the in-memory `ConfigCache`. +1. **Never Block IPC Threads**: AIDL `onTransact` methods are called synchronously by the Android framework and target apps. Blocking these threads (e.g., by executing raw SQL queries or heavy I/O directly) will cause Application Not Responding (ANR) crashes system-wide. Always read from the lock-free, immutable `DaemonState` snapshot exposed by `ConfigCache.state`. 2. **Resource Determinism**: The daemon runs indefinitely. Leaking a single `Cursor`, `ParcelFileDescriptor`, or `SharedMemory` instance will eventually exhaust system limits and crash the OS. Always use Kotlin's `.use { }` blocks or explicit C++ RAII wrappers for native resources. 3. **Isolate OEM Quirks**: Android OS behavior varies wildly between manufacturers (e.g., Lenovo hiding cloned apps in user IDs 900-909, MIUI killing background dual-apps). Place all OEM-specific logic in `utils/Workarounds.kt` to prevent core logic pollution. 4. **Context Forgery (`FakeContext`)**: The daemon does not have a real Android `Context`. To interact with system APIs that require one (like building Notifications or querying packages), use `FakeContext`. Be aware that standard `Context` methods may crash if not explicitly mocked. diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt index 589bab04f..0157406b3 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt @@ -1,6 +1,5 @@ package org.matrix.vector.daemon.data -import android.content.ContentValues import android.content.pm.ApplicationInfo import android.content.pm.PackageParser import android.system.Os @@ -8,11 +7,9 @@ import android.util.Log import hidden.HiddenApiBridge import java.io.File import java.nio.file.Files -import java.nio.file.Path import java.nio.file.Paths import java.nio.file.attribute.PosixFilePermissions import java.util.UUID -import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -28,26 +25,16 @@ import org.matrix.vector.daemon.utils.getRealUsers private const val TAG = "VectorConfigCache" -data class ProcessScope(val processName: String, val uid: Int) - object ConfigCache { - @Volatile var api: String = "(???)" - @Volatile var enableStatusNotification = true - - @Volatile private var managerUid = -1 - @Volatile private var isCacheReady = false - @Volatile private var miscPath: Path? = null - val dbHelper = Database() + // --- IMMUTABLE STATE --- + @Volatile + var state = DaemonState() + private set - // Thread-safe maps for IPC readers - val cachedModules = ConcurrentHashMap() - val cachedScopes = ConcurrentHashMap>() + val dbHelper = Database() // Kept public for PreferenceStore and ModuleDatabase - // Coroutine Scope for background DB tasks private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - - // A conflated channel automatically drops older pending events if a new one arrives. private val cacheUpdateChannel = Channel(Channel.CONFLATED) init { @@ -56,49 +43,69 @@ object ConfigCache { performCacheUpdate() } } - initializeConfig() } + // --- STATE PROXIES (For backwards compatibility) --- + var api: String + get() = state.api + set(value) { + state = state.copy(api = value) + } + + var enableStatusNotification: Boolean + get() = state.enableStatusNotification + set(value) { + state = state.copy(enableStatusNotification = value) + } + + val cachedModules: Map + get() = state.modules + + val cachedScopes: Map> + get() = state.scopes + private fun initializeConfig() { - val config = getModulePrefs("lspd", 0, "config") + val config = PreferenceStore.getModulePrefs("lspd", 0, "config") ManagerService.isVerboseLog = config["enable_verbose_log"] as? Boolean ?: true - enableStatusNotification = config["enable_status_notification"] as? Boolean ?: true + val enableStatusNotif = config["enable_status_notification"] as? Boolean ?: true - // Clean up legacy setting if (config["enable_auto_add_shortcut"] != null) { - updateModulePref("lspd", 0, "config", "enable_auto_add_shortcut", null) + PreferenceStore.updateModulePref("lspd", 0, "config", "enable_auto_add_shortcut", null) } - // Initialize miscPath val pathStr = config["misc_path"] as? String - if (pathStr == null) { - val newPath = Paths.get("/data/misc", UUID.randomUUID().toString()) - updateModulePref("lspd", 0, "config", "misc_path", newPath.toString()) - miscPath = newPath - } else { - miscPath = Paths.get(pathStr) - } + val miscPath = + if (pathStr == null) { + val newPath = Paths.get("/data/misc", UUID.randomUUID().toString()) + PreferenceStore.updateModulePref("lspd", 0, "config", "misc_path", newPath.toString()) + newPath + } else { + Paths.get(pathStr) + } runCatching { val perms = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx--x--x")) - java.nio.file.Files.createDirectories(miscPath!!, perms) - FileSystem.setSelinuxContextRecursive(miscPath!!, "u:object_r:xposed_data:s0") + Files.createDirectories(miscPath, perms) + FileSystem.setSelinuxContextRecursive(miscPath, "u:object_r:xposed_data:s0") } .onFailure { Log.e(TAG, "Failed to create misc directory", it) } + + // Swap state with initialization data + state = state.copy(enableStatusNotification = enableStatusNotif, miscPath = miscPath) } private fun ensureCacheReady() { - // Lazy Execution (Wait for PackageManager) - if (!isCacheReady && packageManager?.asBinder()?.isBinderAlive == true) { + val currentState = state + if (!currentState.isCacheReady && packageManager?.asBinder()?.isBinderAlive == true) { synchronized(this) { - if (!isCacheReady) { + if (!state.isCacheReady) { Log.i(TAG, "System services are ready. Mapping modules and scopes.") updateManager(false) forceCacheUpdateSync() - isCacheReady = true + state = state.copy(isCacheReady = true) } } } @@ -106,45 +113,42 @@ object ConfigCache { fun updateManager(uninstalled: Boolean) { if (uninstalled) { - managerUid = -1 + state = state.copy(managerUid = -1) return } if (packageManager?.asBinder()?.isBinderAlive == true) { runCatching { val info = packageManager?.getPackageInfoCompat(BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME, 0, 0) - managerUid = info?.applicationInfo?.uid ?: -1 - if (managerUid == -1) Log.i(TAG, "Manager is not installed") + val uid = info?.applicationInfo?.uid ?: -1 + if (uid == -1) Log.i(TAG, "Manager is not installed") + state = state.copy(managerUid = uid) } - .onFailure { managerUid = -1 } + .onFailure { state = state.copy(managerUid = -1) } } } fun isManager(uid: Int): Boolean { ensureCacheReady() - return uid == managerUid || uid == BuildConfig.MANAGER_INJECTED_UID + return uid == state.managerUid || uid == BuildConfig.MANAGER_INJECTED_UID } - /** - * Triggers an asynchronous cache update. Multiple rapid calls are naturally coalesced by the - * Conflated Channel. - */ fun requestCacheUpdate() { cacheUpdateChannel.trySend(Unit) } - /** Blocks and forces an immediate cache update (Used during system_server boot). */ fun forceCacheUpdateSync() { performCacheUpdate() } + /** Builds a completely new Immutable State and atomically swaps it. */ private fun performCacheUpdate() { - if (packageManager == null) return // Wait for PM to be ready + if (packageManager == null) return Log.d(TAG, "Executing Cache Update...") val db = dbHelper.readableDatabase + val oldState = state - // Fetch enabled modules val newModules = mutableMapOf() val obsoleteModules = mutableSetOf() val obsoletePaths = mutableMapOf() @@ -163,9 +167,8 @@ object ConfigCache { var apkPath = cursor.getString(1) if (pkgName == "lspd") continue - val oldModule = cachedModules[pkgName] + val oldModule = oldState.modules[pkgName] - // Find the PackageInfo across all users to get UID and ApplicationInfo var pkgInfo: android.content.pm.PackageInfo? = null val users = userManager?.getRealUsers() ?: emptyList() for (user in users) { @@ -181,7 +184,6 @@ object ConfigCache { val appInfo = pkgInfo.applicationInfo - // Optimization: Check if the APK has changed, skip parsing if identical if (oldModule != null && appInfo?.sourceDir != null && apkPath != null && @@ -190,17 +192,11 @@ object ConfigCache { apkPath == oldModule.apkPath && File(appInfo.sourceDir).parent == File(apkPath).parent) { - if (oldModule.appId != -1) { - Log.d(TAG, "$pkgName did not change, skip caching it") - } else { - // System server edge case: update app info - oldModule.applicationInfo = appInfo - } + if (oldModule.appId == -1) oldModule.applicationInfo = appInfo newModules[pkgName] = oldModule continue } - // Update APK path if it shifted during an update val realApkPath = getModuleApkPath(appInfo!!) if (realApkPath == null) { Log.w(TAG, "Failed to find path of $pkgName") @@ -211,18 +207,18 @@ object ConfigCache { obsoletePaths[pkgName] = realApkPath } - // Load the actual DEX and construct the Module - val preLoadedApk = FileSystem.loadModule(apkPath, isDexObfuscateEnabled()) - + val preLoadedApk = + FileSystem.loadModule(apkPath, PreferenceStore.isDexObfuscateEnabled()) if (preLoadedApk != null) { - val module = Module() - module.packageName = pkgName - module.apkPath = apkPath - module.appId = appInfo.uid - module.applicationInfo = appInfo - module.service = oldModule?.service ?: InjectedModuleService(pkgName) - module.file = preLoadedApk - + val module = + Module().apply { + packageName = pkgName + this.apkPath = apkPath + appId = appInfo.uid + applicationInfo = appInfo + service = oldModule?.service ?: InjectedModuleService(pkgName) + file = preLoadedApk + } newModules[pkgName] = module } else { Log.w(TAG, "Failed to parse DEX/ZIP for $pkgName, skipping.") @@ -231,14 +227,12 @@ object ConfigCache { } } - // Clean up obsolete data to keep the database perfectly synced with Android if (packageManager?.asBinder()?.isBinderAlive == true) { - obsoleteModules.forEach { removeModule(it) } - obsoletePaths.forEach { (pkg, path) -> updateModuleApkPath(pkg, path, true) } + obsoleteModules.forEach { ModuleDatabase.removeModule(it) } + obsoletePaths.forEach { (pkg, path) -> ModuleDatabase.updateModuleApkPath(pkg, path, true) } } - // Fetch scopes and map heavy PM logic - val newScopes = ConcurrentHashMap>() + val newScopes = mutableMapOf>() db.query( "scope INNER JOIN modules ON scope.mid = modules.mid", arrayOf("app_pkg_name", "module_pkg_name", "user_id"), @@ -253,13 +247,9 @@ object ConfigCache { val modPkg = cursor.getString(1) val userId = cursor.getInt(2) - // system_server it fetches its own modules if (appPkg == "system") continue - // Ensure the module is actually valid and loaded val module = newModules[modPkg] ?: continue - - // Fetch associated processes val pkgInfo = packageManager?.getPackageInfoWithComponents(appPkg, MATCH_ALL_FLAGS, userId) if (pkgInfo?.applicationInfo == null) continue @@ -273,12 +263,11 @@ object ConfigCache { val processScope = ProcessScope(processName, appUid) newScopes.getOrPut(processScope) { mutableListOf() }.add(module) - // Always allow the module to inject itself across all users if (modPkg == appPkg) { val appId = appUid % PER_USER_RANGE userManager?.getRealUsers()?.forEach { user -> val moduleUid = user.id * PER_USER_RANGE + appId - if (moduleUid != appUid) { // Skip duplicate + if (moduleUid != appUid) { val moduleSelf = ProcessScope(processName, moduleUid) newScopes.getOrPut(moduleSelf) { mutableListOf() }.add(module) } @@ -288,204 +277,30 @@ object ConfigCache { } } - // Atomically swap the memory cache - cachedModules.clear() - cachedModules.putAll(newModules) - - cachedScopes.clear() - cachedScopes.putAll(newScopes) + // --- ATOMIC STATE SWAP --- + state = oldState.copy(modules = newModules, scopes = newScopes) - Log.d(TAG, "cached modules") - cachedModules.forEach { (pkg, mod) -> Log.d(TAG, "$pkg ${mod.apkPath}") } - Log.d(TAG, "cached scope") - cachedScopes.forEach { (ps, modules) -> - Log.d(TAG, "${ps.processName}/${ps.uid}") - modules.forEach { mod -> Log.d(TAG, "\t${mod.packageName}") } - } + Log.d(TAG, "Cache Update Complete. Map Swap successful.") } fun getModulesForProcess(processName: String, uid: Int): List { ensureCacheReady() - return cachedScopes[ProcessScope(processName, uid)] ?: emptyList() - } - - // --- Preferences & Settings --- - fun isDexObfuscateEnabled(): Boolean = - getModulePrefs("lspd", 0, "config")["enable_dex_obfuscate"] as? Boolean ?: true - - fun setDexObfuscate(enabled: Boolean) = - updateModulePref("lspd", 0, "config", "enable_dex_obfuscate", enabled) - - fun isLogWatchdogEnabled(): Boolean = - getModulePrefs("lspd", 0, "config")["enable_log_watchdog"] as? Boolean ?: true - - fun setLogWatchdog(enabled: Boolean) = - updateModulePref("lspd", 0, "config", "enable_log_watchdog", enabled) - - // --- Modules & Scope DB Operations --- - fun getEnabledModules(): List = cachedModules.keys.toList() - - fun enableModule(packageName: String): Boolean { - if (packageName == "lspd") return false - val values = ContentValues().apply { put("enabled", 1) } - val changed = - dbHelper.writableDatabase.update( - "modules", values, "module_pkg_name = ?", arrayOf(packageName)) > 0 - if (changed) requestCacheUpdate() - return changed - } - - fun disableModule(packageName: String): Boolean { - if (packageName == "lspd") return false - val values = ContentValues().apply { put("enabled", 0) } - val changed = - dbHelper.writableDatabase.update( - "modules", values, "module_pkg_name = ?", arrayOf(packageName)) > 0 - if (changed) requestCacheUpdate() - return changed - } - - fun getModuleScope(packageName: String): MutableList? { - if (packageName == "lspd") return null - val result = mutableListOf() - dbHelper.readableDatabase - .query( - "scope INNER JOIN modules ON scope.mid = modules.mid", - arrayOf("app_pkg_name", "user_id"), - "modules.module_pkg_name = ?", - arrayOf(packageName), - null, - null, - null) - .use { cursor -> - while (cursor.moveToNext()) { - result.add( - Application().apply { - this.packageName = cursor.getString(0) - this.userId = cursor.getInt(1) - }) - } - } - return result - } - - fun setModuleScope(packageName: String, scope: MutableList): Boolean { - enableModule(packageName) - val db = dbHelper.writableDatabase - db.beginTransaction() - try { - val mid = - db.compileStatement("SELECT mid FROM modules WHERE module_pkg_name = ?") - .apply { bindString(1, packageName) } - .simpleQueryForLong() - db.delete("scope", "mid = ?", arrayOf(mid.toString())) - - val values = ContentValues().apply { put("mid", mid) } - for (app in scope) { - if (app.packageName == "system" && app.userId != 0) continue - values.put("app_pkg_name", app.packageName) - values.put("user_id", app.userId) - db.insertWithOnConflict( - "scope", null, values, android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE) - } - db.setTransactionSuccessful() - } catch (e: Exception) { - Log.e(TAG, "Failed to set scope", e) - return false - } finally { - db.endTransaction() - } - requestCacheUpdate() - return true - } - - // --- Configs Table Operations --- - fun getModulePrefs(packageName: String, userId: Int, group: String): Map { - val result = mutableMapOf() - dbHelper.readableDatabase - .query( - "configs", - arrayOf("`key`", "data"), - "module_pkg_name = ? AND user_id = ? AND `group` = ?", - arrayOf(packageName, userId.toString(), group), - null, - null, - null) - .use { cursor -> - while (cursor.moveToNext()) { - val key = cursor.getString(0) - val blob = cursor.getBlob(1) - val obj = org.apache.commons.lang3.SerializationUtilsX.deserialize(blob) - if (obj != null) result[key] = obj - } - } - return result - } - - fun updateModulePref(moduleName: String, userId: Int, group: String, key: String, value: Any?) { - updateModulePrefs(moduleName, userId, group, mapOf(key to value)) - } - - fun updateModulePrefs(moduleName: String, userId: Int, group: String, diff: Map) { - val db = dbHelper.writableDatabase - db.beginTransaction() - try { - for ((key, value) in diff) { - if (value is java.io.Serializable) { - val values = - ContentValues().apply { - put("`group`", group) - put("`key`", key) - put("data", org.apache.commons.lang3.SerializationUtilsX.serialize(value)) - put("module_pkg_name", moduleName) - put("user_id", userId.toString()) - } - db.insertWithOnConflict( - "configs", null, values, android.database.sqlite.SQLiteDatabase.CONFLICT_REPLACE) - } else { - db.delete( - "configs", - "module_pkg_name=? AND user_id=? AND `group`=? AND `key`=?", - arrayOf(moduleName, userId.toString(), group, key)) - } - } - db.setTransactionSuccessful() - } finally { - db.endTransaction() - } - } - - fun deleteModulePrefs(moduleName: String, userId: Int, group: String) { - dbHelper.writableDatabase.delete( - "configs", - "module_pkg_name=? AND user_id=? AND `group`=?", - arrayOf(moduleName, userId.toString(), group)) + return state.scopes[ProcessScope(processName, uid)] ?: emptyList() } fun getModuleByUid(uid: Int): Module? = - cachedModules.values.firstOrNull { it.appId == uid % PER_USER_RANGE } - - fun isScopeRequestBlocked(pkg: String): Boolean = - (getModulePrefs("lspd", 0, "config")["scope_request_blocked"] as? Set<*>)?.contains(pkg) == - true - - fun getDenyListPackages(): List = - emptyList() // Needs Magisk DB parsing logic if Api == "Zygisk" - - fun getAutoInclude(pkg: String): Boolean = false // Query modules table for auto_include flag - - fun setAutoInclude(pkg: String, enabled: Boolean): Boolean = false + state.modules.values.firstOrNull { it.appId == uid % PER_USER_RANGE } fun getModulesForSystemServer(): List { val modules = mutableListOf() - - // system_server must have specific SELinux execmem capabilities to hook properly if (!android.os.SELinux.checkSELinuxAccess( "u:r:system_server:s0", "u:r:system_server:s0", "process", "execmem")) { Log.e(TAG, "Skipping system_server injection: sepolicy execmem denied") return modules } + val currentState = state + dbHelper.readableDatabase .query( "scope INNER JOIN modules ON scope.mid = modules.mid", @@ -500,15 +315,13 @@ object ConfigCache { val pkgName = cursor.getString(0) val apkPath = cursor.getString(1) - // Reuse memory cache if available - val cached = cachedModules[pkgName] + val cached = currentState.modules[pkgName] if (cached != null) { modules.add(cached) continue } val statPath = FileSystem.toGlobalNamespace("/data/user_de/0/$pkgName").absolutePath - val module = Module().apply { packageName = pkgName @@ -517,7 +330,6 @@ object ConfigCache { service = InjectedModuleService(pkgName) } - // Parse the APK locally to simulate ApplicationInfo without ActivityManager running runCatching { @Suppress("DEPRECATION") val pkg = PackageParser().parsePackage(File(apkPath), 0, false) @@ -532,10 +344,10 @@ object ConfigCache { } .onFailure { Log.w(TAG, "Failed to parse $apkPath", it) } - FileSystem.loadModule(apkPath, isDexObfuscateEnabled())?.let { + FileSystem.loadModule(apkPath, PreferenceStore.isDexObfuscateEnabled())?.let { module.file = it - cachedModules.putIfAbsent(pkgName, module) modules.add(module) + // We intentionally don't mutate state.modules here. Cache update will catch it. } } } @@ -544,17 +356,15 @@ object ConfigCache { fun getPrefsPath(packageName: String, uid: Int): String { ensureCacheReady() - - // Strictly enforce that miscPath exists. + val currentState = state val basePath = - miscPath - ?: throw IllegalStateException("Fatal: miscPath was not initialized from the database!") + currentState.miscPath ?: throw IllegalStateException("Fatal: miscPath not initialized!") val userId = uid / PER_USER_RANGE val userSuffix = if (userId == 0) "" else userId.toString() val path = basePath.resolve("prefs$userSuffix").resolve(packageName) - val module = cachedModules[packageName] + val module = currentState.modules[packageName] if (module != null && module.appId == uid % PER_USER_RANGE) { runCatching { val perms = @@ -567,57 +377,6 @@ object ConfigCache { return path.toString() } - fun removeModuleScope(packageName: String, scopePackageName: String, userId: Int): Boolean { - if (packageName == "lspd" || (scopePackageName == "system" && userId != 0)) return false - val db = dbHelper.writableDatabase - val mid = - db.compileStatement("SELECT mid FROM modules WHERE module_pkg_name = ?") - .apply { bindString(1, packageName) } - .simpleQueryForLong() - db.delete( - "scope", - "mid = ? AND app_pkg_name = ? AND user_id = ?", - arrayOf(mid.toString(), scopePackageName, userId.toString())) - requestCacheUpdate() - return true - } - - fun updateModuleApkPath(packageName: String, apkPath: String?, force: Boolean): Boolean { - if (apkPath == null || packageName == "lspd") return false - val values = - ContentValues().apply { - put("module_pkg_name", packageName) - put("apk_path", apkPath) - } - val db = dbHelper.writableDatabase - var count = - db.insertWithOnConflict( - "modules", null, values, android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE) - .toInt() - if (count < 0) { - val cached = cachedModules[packageName] - if (force || cached == null || cached.apkPath != apkPath) { - count = - db.updateWithOnConflict( - "modules", - values, - "module_pkg_name=?", - arrayOf(packageName), - android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE) - } else count = 0 - } - if (!force && count > 0) requestCacheUpdate() - return count > 0 - } - - fun removeModule(packageName: String): Boolean { - if (packageName == "lspd") return false - val res = - dbHelper.writableDatabase.delete("modules", "module_pkg_name = ?", arrayOf(packageName)) > 0 - if (res) requestCacheUpdate() - return res - } - fun getModuleApkPath(info: ApplicationInfo): String? { val apks = mutableListOf() info.sourceDir?.let { apks.add(it) } @@ -636,6 +395,53 @@ object ConfigCache { fun shouldSkipProcess(scope: ProcessScope): Boolean { ensureCacheReady() - return !cachedScopes.containsKey(scope) && !isManager(scope.uid) + return !state.scopes.containsKey(scope) && !isManager(scope.uid) } + + fun getEnabledModules(): List = state.modules.keys.toList() + + fun getDenyListPackages(): List = emptyList() + + fun getModulePrefs(pkg: String, userId: Int, group: String) = + PreferenceStore.getModulePrefs(pkg, userId, group) + + fun updateModulePref(pkg: String, userId: Int, group: String, key: String, value: Any?) = + PreferenceStore.updateModulePref(pkg, userId, group, key, value) + + fun updateModulePrefs(pkg: String, userId: Int, group: String, diff: Map) = + PreferenceStore.updateModulePrefs(pkg, userId, group, diff) + + fun deleteModulePrefs(pkg: String, userId: Int, group: String) = + PreferenceStore.deleteModulePrefs(pkg, userId, group) + + fun isDexObfuscateEnabled() = PreferenceStore.isDexObfuscateEnabled() + + fun setDexObfuscate(enabled: Boolean) = PreferenceStore.setDexObfuscate(enabled) + + fun isLogWatchdogEnabled() = PreferenceStore.isLogWatchdogEnabled() + + fun setLogWatchdog(enabled: Boolean) = PreferenceStore.setLogWatchdog(enabled) + + fun isScopeRequestBlocked(pkg: String) = PreferenceStore.isScopeRequestBlocked(pkg) + + fun enableModule(pkg: String) = ModuleDatabase.enableModule(pkg) + + fun disableModule(pkg: String) = ModuleDatabase.disableModule(pkg) + + fun getModuleScope(pkg: String) = ModuleDatabase.getModuleScope(pkg) + + fun setModuleScope(pkg: String, scope: MutableList) = + ModuleDatabase.setModuleScope(pkg, scope) + + fun removeModuleScope(pkg: String, scopePkg: String, userId: Int) = + ModuleDatabase.removeModuleScope(pkg, scopePkg, userId) + + fun updateModuleApkPath(pkg: String, apkPath: String?, force: Boolean) = + ModuleDatabase.updateModuleApkPath(pkg, apkPath, force) + + fun removeModule(pkg: String) = ModuleDatabase.removeModule(pkg) + + fun getAutoInclude(pkg: String) = ModuleDatabase.getAutoInclude(pkg) + + fun setAutoInclude(pkg: String, enabled: Boolean) = ModuleDatabase.setAutoInclude(pkg, enabled) } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/DaemonState.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/DaemonState.kt new file mode 100644 index 000000000..e4f23fea3 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/DaemonState.kt @@ -0,0 +1,20 @@ +package org.matrix.vector.daemon.data + +import java.nio.file.Path +import org.lsposed.lspd.models.Module + +data class ProcessScope(val processName: String, val uid: Int) + +/** + * An immutable snapshot of the Daemon's state. Any updates will generate a new copy of this class + * and atomically swap the reference. + */ +data class DaemonState( + val api: String = "(???)", + val enableStatusNotification: Boolean = true, + val managerUid: Int = -1, + val isCacheReady: Boolean = false, + val miscPath: Path? = null, + val modules: Map = emptyMap(), + val scopes: Map> = emptyMap() +) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt new file mode 100644 index 000000000..1069afcb3 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt @@ -0,0 +1,173 @@ +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 + +private const val TAG = "VectorModuleDb" + +object ModuleDatabase { + + fun enableModule(packageName: String): Boolean { + if (packageName == "lspd") return false + val values = ContentValues().apply { put("enabled", 1) } + val changed = + ConfigCache.dbHelper.writableDatabase.update( + "modules", values, "module_pkg_name = ?", arrayOf(packageName)) > 0 + if (changed) ConfigCache.requestCacheUpdate() + return changed + } + + fun disableModule(packageName: String): Boolean { + if (packageName == "lspd") return false + val values = ContentValues().apply { put("enabled", 0) } + val changed = + ConfigCache.dbHelper.writableDatabase.update( + "modules", values, "module_pkg_name = ?", arrayOf(packageName)) > 0 + if (changed) ConfigCache.requestCacheUpdate() + return changed + } + + fun getModuleScope(packageName: String): MutableList? { + if (packageName == "lspd") return null + val result = mutableListOf() + ConfigCache.dbHelper.readableDatabase + .query( + "scope INNER JOIN modules ON scope.mid = modules.mid", + arrayOf("app_pkg_name", "user_id"), + "modules.module_pkg_name = ?", + arrayOf(packageName), + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + result.add( + Application().apply { + this.packageName = cursor.getString(0) + this.userId = cursor.getInt(1) + }) + } + } + return result + } + + fun setModuleScope(packageName: String, scope: MutableList): Boolean { + enableModule(packageName) + val db = ConfigCache.dbHelper.writableDatabase + db.beginTransaction() + try { + val mid = + db.compileStatement("SELECT mid FROM modules WHERE module_pkg_name = ?") + .apply { bindString(1, packageName) } + .simpleQueryForLong() + db.delete("scope", "mid = ?", arrayOf(mid.toString())) + + val values = ContentValues().apply { put("mid", mid) } + for (app in scope) { + if (app.packageName == "system" && app.userId != 0) continue + values.put("app_pkg_name", app.packageName) + values.put("user_id", app.userId) + db.insertWithOnConflict("scope", null, values, SQLiteDatabase.CONFLICT_IGNORE) + } + db.setTransactionSuccessful() + } catch (e: Exception) { + Log.e(TAG, "Failed to set scope", e) + return false + } finally { + db.endTransaction() + } + ConfigCache.requestCacheUpdate() + return true + } + + fun removeModuleScope(packageName: String, scopePackageName: String, userId: Int): Boolean { + if (packageName == "lspd" || (scopePackageName == "system" && userId != 0)) return false + val db = ConfigCache.dbHelper.writableDatabase + val mid = + db.compileStatement("SELECT mid FROM modules WHERE module_pkg_name = ?") + .apply { bindString(1, packageName) } + .simpleQueryForLong() + db.delete( + "scope", + "mid = ? AND app_pkg_name = ? AND user_id = ?", + arrayOf(mid.toString(), scopePackageName, userId.toString())) + ConfigCache.requestCacheUpdate() + return true + } + + fun updateModuleApkPath(packageName: String, apkPath: String?, force: Boolean): Boolean { + if (apkPath == null || packageName == "lspd") return false + val values = + ContentValues().apply { + put("module_pkg_name", packageName) + put("apk_path", apkPath) + } + val db = ConfigCache.dbHelper.writableDatabase + var count = + db.insertWithOnConflict("modules", null, values, SQLiteDatabase.CONFLICT_IGNORE).toInt() + + if (count < 0) { + val cached = ConfigCache.state.modules[packageName] + if (force || cached == null || cached.apkPath != apkPath) { + count = + db.updateWithOnConflict( + "modules", + values, + "module_pkg_name=?", + arrayOf(packageName), + SQLiteDatabase.CONFLICT_IGNORE) + } else count = 0 + } + if (!force && count > 0) ConfigCache.requestCacheUpdate() + return count > 0 + } + + fun removeModule(packageName: String): Boolean { + if (packageName == "lspd") return false + val res = + ConfigCache.dbHelper.writableDatabase.delete( + "modules", "module_pkg_name = ?", arrayOf(packageName)) > 0 + if (res) ConfigCache.requestCacheUpdate() + return res + } + + fun getAutoInclude(packageName: String): Boolean { + if (packageName == "lspd") return false + + var isAutoInclude = false + ConfigCache.dbHelper.readableDatabase + .query( + "modules", + arrayOf("auto_include"), + "module_pkg_name = ?", + arrayOf(packageName), + null, + null, + null) + .use { cursor -> + if (cursor.moveToFirst()) { + isAutoInclude = cursor.getInt(0) == 1 + } + } + return isAutoInclude + } + + fun setAutoInclude(packageName: String, enabled: Boolean): Boolean { + if (packageName == "lspd") return false + + val values = ContentValues().apply { put("auto_include", if (enabled) 1 else 0) } + + val changed = + ConfigCache.dbHelper.writableDatabase.update( + "modules", values, "module_pkg_name = ?", arrayOf(packageName)) > 0 + + // If the auto_include flag changes, we should rebuild the scope cache + if (changed) { + ConfigCache.requestCacheUpdate() + } + + return changed + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt new file mode 100644 index 000000000..2e64df37f --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt @@ -0,0 +1,84 @@ +package org.matrix.vector.daemon.data + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase + +object PreferenceStore { + + fun getModulePrefs(packageName: String, userId: Int, group: String): Map { + val result = mutableMapOf() + ConfigCache.dbHelper.readableDatabase + .query( + "configs", + arrayOf("`key`", "data"), + "module_pkg_name = ? AND user_id = ? AND `group` = ?", + arrayOf(packageName, userId.toString(), group), + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + val key = cursor.getString(0) + val blob = cursor.getBlob(1) + val obj = org.apache.commons.lang3.SerializationUtilsX.deserialize(blob) + if (obj != null) result[key] = obj + } + } + return result + } + + fun updateModulePref(moduleName: String, userId: Int, group: String, key: String, value: Any?) { + updateModulePrefs(moduleName, userId, group, mapOf(key to value)) + } + + fun updateModulePrefs(moduleName: String, userId: Int, group: String, diff: Map) { + val db = ConfigCache.dbHelper.writableDatabase + db.beginTransaction() + try { + for ((key, value) in diff) { + if (value is java.io.Serializable) { + val values = + ContentValues().apply { + put("`group`", group) + put("`key`", key) + put("data", org.apache.commons.lang3.SerializationUtilsX.serialize(value)) + put("module_pkg_name", moduleName) + put("user_id", userId.toString()) + } + db.insertWithOnConflict("configs", null, values, SQLiteDatabase.CONFLICT_REPLACE) + } else { + db.delete( + "configs", + "module_pkg_name=? AND user_id=? AND `group`=? AND `key`=?", + arrayOf(moduleName, userId.toString(), group, key)) + } + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + fun deleteModulePrefs(moduleName: String, userId: Int, group: String) { + ConfigCache.dbHelper.writableDatabase.delete( + "configs", + "module_pkg_name=? AND user_id=? AND `group`=?", + arrayOf(moduleName, userId.toString(), group)) + } + + fun isDexObfuscateEnabled(): Boolean = + getModulePrefs("lspd", 0, "config")["enable_dex_obfuscate"] as? Boolean ?: true + + fun setDexObfuscate(enabled: Boolean) = + updateModulePref("lspd", 0, "config", "enable_dex_obfuscate", enabled) + + fun isLogWatchdogEnabled(): Boolean = + getModulePrefs("lspd", 0, "config")["enable_log_watchdog"] as? Boolean ?: true + + fun setLogWatchdog(enabled: Boolean) = + updateModulePref("lspd", 0, "config", "enable_log_watchdog", enabled) + + fun isScopeRequestBlocked(pkg: String): Boolean = + (getModulePrefs("lspd", 0, "config")["scope_request_blocked"] as? Set<*>)?.contains(pkg) == + true +} From db5386595b3b3b21c4aa2a80c596ee5a84acea7c Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 18:57:31 +0100 Subject: [PATCH 22/38] update readme --- daemon/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/daemon/README.md b/daemon/README.md index 0f263b005..84d8ccc5e 100644 --- a/daemon/README.md +++ b/daemon/README.md @@ -11,8 +11,8 @@ The daemon relies on a dual-IPC architecture and extensive use of Android Binder 1. **Bootstrapping & Bridge (`core/`)**: The daemon starts early in the boot process. It forces its primary Binder (`VectorService`) into `system_server` by hijacking transactions on the Android `activity` service. 2. **Privileged IPC Provider (`ipc/`)**: Android's sandbox prevents target processes from reading the framework APK, accessing SQLite databases, or resolving hidden ART symbols. The daemon exploits its root/system-level permissions to act as an asset server. It provides three critical components to hooked processes over Binder IPC: - * **Loader DEX**: Dispatched via `SharedMemory` to avoid disk-based detection and bypass SELinux `exec` restrictions. - * **Obfuscation Maps**: Pre-calculated maps that allow the injected framework to identify and hook internal ART structures regardless of the daemon's own obfuscation. + * **Framework Loader DEX**: Dispatched via `SharedMemory` to avoid disk-based detection and bypass SELinux `exec` restrictions. + * **Obfuscation Maps**: Dictionaries provided over IPC when API protection is enabled. Because the daemon dynamically obfuscates both the loader module and the framework loader to protect the Xposed API from unexpected invocation, these maps allow the injected code to correctly resolve the randomized class names at runtime. * **Dynamic Module Scopes**: Fast, lock-free lookups of which modules should be loaded into a specific UID/ProcessName. 3. **State Management (`data/`)**: To ensure IPC calls resolve in microseconds without race conditions, the daemon uses an **Immutable State Container** (`DaemonState`). Module topology and scopes are built into a frozen snapshot in the background, which is atomically swapped into memory. High-volume module preference updates are isolated in a separate `PreferenceStore` to prevent state pollution. 4. **Native Environment (`env/` & JNI)**: Background threads (C++ and Kotlin Coroutines) handle low-level system subversion, including `dex2oat` compilation hijacking and logcat monitoring. @@ -45,7 +45,9 @@ To prevent Android's ART from inlining hooked methods (which makes them unhookab ### 3. Dex Obfuscation & Zero-Copy Memory The framework DEX is passed to target apps via Android's `SharedMemory` API. -To protect the Xposed API from reflections, `ObfuscationManager.kt` passes this memory buffer to JNI (`obfuscation.cpp`), which uses `slicer` to mutate Dalvik string pools in-place using `MAP_SHARED`. This ensures zero-copy manipulation; the Java side immediately sees the obfuscated DEX without reallocating buffers. +The primary purpose of obfuscation maps is to protect the Xposed API from unexpected invocation. If this configuration is enabled, the daemon dynamically obfuscates *both* the loader module and the framework loader. + +To achieve this efficiently, `ObfuscationManager.kt` passes the memory buffer to JNI (`obfuscation.cpp`), which uses `slicer` to mutate Dalvik string pools in-place using `MAP_SHARED`. This ensures zero-copy manipulation; the Java side immediately sees the obfuscated DEX without reallocating buffers. ### 4. Lifecycle & State Tracking The daemon must precisely know which apps are installed and which processes are running. From d46d7b5442795aad66568c6b075e1f5806531108 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 19:19:29 +0100 Subject: [PATCH 23/38] catch package parsing error --- .../matrix/vector/daemon/data/ConfigCache.kt | 32 +++++++++++++------ .../vector/daemon/ipc/ManagerService.kt | 2 +- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt index 0157406b3..5add6ae2e 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt @@ -281,6 +281,14 @@ object ConfigCache { state = oldState.copy(modules = newModules, scopes = newScopes) Log.d(TAG, "Cache Update Complete. Map Swap successful.") + // Log.d(TAG, "cached modules:") + // newModules.forEach { (pkg, mod) -> Log.d(TAG, "$pkg ${mod.apkPath}") } + + // Log.d(TAG, "cached scopes:") + // newScopes.forEach { (ps, modules) -> + // Log.d(TAG, "${ps.processName}/${ps.uid}") + // modules.forEach { mod -> Log.d(TAG, "\t${mod.packageName}") } + // } } fun getModulesForProcess(processName: String, uid: Int): List { @@ -333,16 +341,22 @@ object ConfigCache { runCatching { @Suppress("DEPRECATION") val pkg = PackageParser().parsePackage(File(apkPath), 0, false) - module.applicationInfo = - pkg.applicationInfo.apply { - sourceDir = apkPath - dataDir = statPath - deviceProtectedDataDir = statPath - HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(this, statPath) - processName = pkgName - } + module.applicationInfo = pkg.applicationInfo } - .onFailure { Log.w(TAG, "Failed to parse $apkPath", it) } + .onFailure { + Log.w(TAG, "PackageParser failed for $apkPath, using fallback ApplicationInfo") + module.applicationInfo = ApplicationInfo().apply { packageName = pkgName } + } + + // Always apply the critical paths manually, even on fallback + module.applicationInfo?.apply { + sourceDir = apkPath + dataDir = statPath + deviceProtectedDataDir = statPath + HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(this, statPath) + processName = pkgName + uid = module.appId + } FileSystem.loadModule(apkPath, PreferenceStore.isDexObfuscateEnabled())?.let { module.file = it diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt index d719d7ba4..c6e0bcf6c 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt @@ -268,7 +268,7 @@ object ManagerService : ILSPManagerService.Stub() { override fun setVerboseLog(enabled: Boolean) { _isVerboseLog = enabled - if (enabled) LogcatMonitor.startVerbose() else LogcatMonitor.stopVerbose() + if (isVerboseLog()) LogcatMonitor.startVerbose() else LogcatMonitor.stopVerbose() ConfigCache.updateModulePref("lspd", 0, "config", "enable_verbose_log", enabled) } From 914a1d8e058947e3dc7c8a21ccc1fb160bcbd1f0 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 19:40:43 +0100 Subject: [PATCH 24/38] [skip ci] improve doc --- daemon/README.md | 9 ++++----- daemon/src/main/jni/obfuscation.cpp | 25 +++++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/daemon/README.md b/daemon/README.md index 84d8ccc5e..fbead783c 100644 --- a/daemon/README.md +++ b/daemon/README.md @@ -12,7 +12,7 @@ The daemon relies on a dual-IPC architecture and extensive use of Android Binder 1. **Bootstrapping & Bridge (`core/`)**: The daemon starts early in the boot process. It forces its primary Binder (`VectorService`) into `system_server` by hijacking transactions on the Android `activity` service. 2. **Privileged IPC Provider (`ipc/`)**: Android's sandbox prevents target processes from reading the framework APK, accessing SQLite databases, or resolving hidden ART symbols. The daemon exploits its root/system-level permissions to act as an asset server. It provides three critical components to hooked processes over Binder IPC: * **Framework Loader DEX**: Dispatched via `SharedMemory` to avoid disk-based detection and bypass SELinux `exec` restrictions. - * **Obfuscation Maps**: Dictionaries provided over IPC when API protection is enabled. Because the daemon dynamically obfuscates both the loader module and the framework loader to protect the Xposed API from unexpected invocation, these maps allow the injected code to correctly resolve the randomized class names at runtime. + * **Obfuscation Maps**: Dictionaries provided over IPC when API protection is enabled, allowing the injected code to correctly resolve the randomized class names at runtime. * **Dynamic Module Scopes**: Fast, lock-free lookups of which modules should be loaded into a specific UID/ProcessName. 3. **State Management (`data/`)**: To ensure IPC calls resolve in microseconds without race conditions, the daemon uses an **Immutable State Container** (`DaemonState`). Module topology and scopes are built into a frozen snapshot in the background, which is atomically swapped into memory. High-volume module preference updates are isolated in a separate `PreferenceStore` to prevent state pollution. 4. **Native Environment (`env/` & JNI)**: Background threads (C++ and Kotlin Coroutines) handle low-level system subversion, including `dex2oat` compilation hijacking and logcat monitoring. @@ -43,11 +43,10 @@ To prevent Android's ART from inlining hooked methods (which makes them unhookab * **FD Passing**: When the wrapper executes, to read the original compiler or the `liboat_hook.so`, it opens a UNIX domain socket to the daemon. The daemon (running as root) opens the files and passes the File Descriptors (FDs) back to the wrapper via `SCM_RIGHTS`. * **Execution**: The wrapper uses `memfd_create` and `sendfile` to load the hook, bypassing execute restrictions, and uses `LD_PRELOAD` to inject the hook into the real `dex2oat` process while appending `--inline-max-code-units=0`. -### 3. Dex Obfuscation & Zero-Copy Memory -The framework DEX is passed to target apps via Android's `SharedMemory` API. -The primary purpose of obfuscation maps is to protect the Xposed API from unexpected invocation. If this configuration is enabled, the daemon dynamically obfuscates *both* the loader module and the framework loader. +### 3. API Protection & DEX Obfuscation +To prevent unauthorized apps from detecting the framework or invoking the Xposed API, the daemon randomizes framework and loader class names on each boot. JNI maps the input `SharedMemory` via `MAP_SHARED` to gain direct, zero-copy access to the physical pages populated by Java. Using the [DexBuilder](https://github.com/JingMatrix/DexBuilder) library, the daemon mutates the DEX string pool in-place; this is highly efficient as the library's Intermediate Representation points directly to the mapped buffer, avoiding unnecessary heap allocations during the randomization process. -To achieve this efficiently, `ObfuscationManager.kt` passes the memory buffer to JNI (`obfuscation.cpp`), which uses `slicer` to mutate Dalvik string pools in-place using `MAP_SHARED`. This ensures zero-copy manipulation; the Java side immediately sees the obfuscated DEX without reallocating buffers. +Once mutation is complete, the finalized DEX is written into a new `SharedMemory` region and the original plaintext handle is closed. Because signatures are now randomized, the daemon provides **Obfuscation Maps** via Door 1 and Door 2. These dictionaries allow the injected code to correctly "re-link" and resolve the framework's internal classes at runtime despite their randomized names. ### 4. Lifecycle & State Tracking The daemon must precisely know which apps are installed and which processes are running. diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index 01d34b749..896b491c9 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -205,18 +205,19 @@ Java_org_matrix_vector_daemon_utils_ObfuscationManager_obfuscateDex(JNIEnv *env, LOGD("obfuscateDex: fd=%d, size=%zu", fd, size); // CRITICAL: We MUST use MAP_SHARED here, not MAP_PRIVATE. - // 1. Android's SharedMemory is backed by purely virtual IPC buffers (ashmem/memfd). - // If we use MAP_PRIVATE, the kernel attempts to create a Copy-On-Write snapshot. - // Because the Java side just populated this virtual buffer and immediately passed - // it to JNI, mapping it MAP_PRIVATE often results in mapping unpopulated zero-pages, - // which causes Slicer to read a corrupted/empty header and abort. - // 2. Using MAP_SHARED gives us direct pointers to the exact physical memory pages - // populated by Java. - // 3. ZERO-COPY ARCHITECTURE: Because Slicer's IR holds direct pointers to this mapped - // memory, mutating strings in-place (via const_cast) instantly updates the IR - // without allocating new memory. Since the Java caller discards the original - // SharedMemory buffer anyway, this in-place mutation is completely safe and highly - // efficient. + // 1. Android's SharedMemory is backed by ashmem or memfd. Mapping these as + // MAP_PRIVATE creates a Copy-On-Write (COW) layer. In many Android kernel + // configurations, this COW layer does not correctly fault-in the initial + // contents from the shared source, resulting in the JNI side seeing + // unpopulated zero-pages. This causes slicer to fail immediately. + // 2. Using MAP_SHARED ensures we have direct access to the same physical + // pages populated by the Java layer. + // 3. ZERO-COPY MUTATION: Slicer's Intermediate Representation (IR) points + // directly into this mapped memory for string data. By mutating the + // buffer in-place, we update the IR's state without any additional + // heap allocations. This is safe here because the Daemon owns the + // lifecycle of this temporary buffer and the Java caller will discard + // the un-obfuscated original anyway. void *mem = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (mem == MAP_FAILED) { LOGE("Failed to map input dex"); From 99c73e36093c8848b4748da642af1d1ff7f3fd4a Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 19:43:29 +0100 Subject: [PATCH 25/38] Remove Java code --- .../src/main/java/org/lsposed/lspd/Main.java | 10 - .../lspd/service/ActivityManagerService.java | 212 --- .../lsposed/lspd/service/BridgeService.java | 179 --- .../lspd/service/ConfigFileManager.java | 518 ------- .../lsposed/lspd/service/ConfigManager.java | 1274 ----------------- .../lsposed/lspd/service/Dex2OatService.java | 285 ---- .../lspd/service/LSPApplicationService.java | 179 --- .../service/LSPInjectedModuleService.java | 95 -- .../lspd/service/LSPManagerService.java | 569 -------- .../lspd/service/LSPModuleService.java | 274 ---- .../lspd/service/LSPNotificationManager.java | 332 ----- .../lspd/service/LSPSystemServerService.java | 151 -- .../lsposed/lspd/service/LSPosedService.java | 499 ------- .../lsposed/lspd/service/LogcatService.java | 222 --- .../lspd/service/ObfuscationManager.java | 13 - .../lsposed/lspd/service/PackageService.java | 431 ------ .../lsposed/lspd/service/PowerService.java | 63 - .../lsposed/lspd/service/ServiceManager.java | 314 ---- .../org/lsposed/lspd/service/UserService.java | 123 -- .../org/lsposed/lspd/util/FakeContext.java | 96 -- .../lsposed/lspd/util/InstallerVerifier.java | 31 - 21 files changed, 5870 deletions(-) delete mode 100644 daemon/src/main/java/org/lsposed/lspd/Main.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/ActivityManagerService.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/Dex2OatService.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/LSPSystemServerService.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/LogcatService.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/ObfuscationManager.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/PackageService.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/PowerService.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/service/UserService.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/util/FakeContext.java delete mode 100644 daemon/src/main/java/org/lsposed/lspd/util/InstallerVerifier.java diff --git a/daemon/src/main/java/org/lsposed/lspd/Main.java b/daemon/src/main/java/org/lsposed/lspd/Main.java deleted file mode 100644 index 3263091f1..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/Main.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.lsposed.lspd; - -import org.lsposed.lspd.service.ServiceManager; - -public class Main { - - public static void main(String[] args) { - ServiceManager.start(args); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ActivityManagerService.java b/daemon/src/main/java/org/lsposed/lspd/service/ActivityManagerService.java deleted file mode 100644 index 04fbe6a4a..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/ActivityManagerService.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.app.ContentProviderHolder; -import android.app.IActivityManager; -import android.app.IApplicationThread; -import android.app.IServiceConnection; -import android.app.IUidObserver; -import android.app.ProfilerInfo; -import android.content.Context; -import android.content.IContentProvider; -import android.content.IIntentReceiver; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.UserInfo; -import android.content.res.Configuration; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.util.Log; - -public class ActivityManagerService { - private static IActivityManager am = null; - private static IBinder binder = null; - private static IApplicationThread appThread = null; - private static IBinder token = null; - - private static final IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - Log.w(TAG, "am is dead"); - binder.unlinkToDeath(this, 0); - binder = null; - am = null; - appThread = null; - token = null; - } - }; - - public static IActivityManager getActivityManager() { - if (binder == null || am == null) { - binder = ServiceManager.getService(Context.ACTIVITY_SERVICE); - if (binder == null) return null; - try { - binder.linkToDeath(deathRecipient, 0); - am = IActivityManager.Stub.asInterface(binder); - // For oddo Android 9 we cannot set activity controller here... - // am.setActivityController(null, false); - } catch (RemoteException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - return am; - } - - public static int broadcastIntentWithFeature(String callingFeatureId, - Intent intent, String resolvedType, IIntentReceiver resultTo, int resultCode, - String resultData, Bundle map, String[] requiredPermissions, - int appOp, Bundle options, boolean serialized, boolean sticky, int userId) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null || appThread == null) return -1; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - try { - return am.broadcastIntentWithFeature(appThread, callingFeatureId, intent, resolvedType, resultTo, - resultCode, resultData, null, requiredPermissions, null, null, appOp, null, - serialized, sticky, userId); - } catch (NoSuchMethodError ignored) { - return am.broadcastIntentWithFeature(appThread, callingFeatureId, intent, resolvedType, resultTo, - resultCode, resultData, null, requiredPermissions, null, appOp, null, - serialized, sticky, userId); - } - } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - return am.broadcastIntentWithFeature(appThread, callingFeatureId, intent, resolvedType, resultTo, resultCode, resultData, map, requiredPermissions, appOp, options, serialized, sticky, userId); - } else { - return am.broadcastIntent(appThread, intent, resolvedType, resultTo, resultCode, resultData, map, requiredPermissions, appOp, options, serialized, sticky, userId); - } - } - - public static void forceStopPackage(String packageName, int userId) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return; - am.forceStopPackage(packageName, userId); - } - - public static boolean startUserInBackground(int userId) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return false; - return am.startUserInBackground(userId); - } - - public static Intent registerReceiver(String callerPackage, - String callingFeatureId, IIntentReceiver receiver, IntentFilter filter, - String requiredPermission, int userId, int flags) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null || appThread == null) return null; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) - return am.registerReceiverWithFeature(appThread, callerPackage, callingFeatureId, "null", receiver, filter, requiredPermission, userId, flags); - else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - return am.registerReceiverWithFeature(appThread, callerPackage, callingFeatureId, receiver, filter, requiredPermission, userId, flags); - } else { - return am.registerReceiver(appThread, callerPackage, receiver, filter, requiredPermission, userId, flags); - } - } - - public static void finishReceiver(IBinder intentReceiver, IBinder applicationThread, int resultCode, - String resultData, Bundle resultExtras, boolean resultAbort, - int flags) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null || appThread == null) return; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - am.finishReceiver(applicationThread, resultCode, resultData, resultExtras, resultAbort, flags); - } else { - am.finishReceiver(intentReceiver, resultCode, resultData, resultExtras, resultAbort, flags); - } - } - - public static int bindService(Intent service, - String resolvedType, IServiceConnection connection, int flags, - String callingPackage, int userId) throws RemoteException { - - IActivityManager am = getActivityManager(); - if (am == null || appThread == null) return -1; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - return am.bindService(appThread, token, service, resolvedType, connection, (long) flags, callingPackage, userId); - else - return am.bindService(appThread, token, service, resolvedType, connection, flags, callingPackage, userId); - } - - public static boolean unbindService(IServiceConnection connection) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return false; - return am.unbindService(connection); - } - - public static int startActivityAsUserWithFeature(String callingPackage, - String callingFeatureId, Intent intent, String resolvedType, - IBinder resultTo, String resultWho, int requestCode, int flags, - ProfilerInfo profilerInfo, Bundle options, int userId) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null || appThread == null) return -1; - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - return am.startActivityAsUserWithFeature(appThread, callingPackage, callingFeatureId, intent, resolvedType, resultTo, resultWho, requestCode, flags, profilerInfo, options, userId); - } else { - return am.startActivityAsUser(appThread, callingPackage, intent, resolvedType, resultTo, resultWho, requestCode, flags, profilerInfo, options, userId); - } - } - - public static void onSystemServerContext(IApplicationThread thread, IBinder token) { - ActivityManagerService.appThread = thread; - ActivityManagerService.token = token; - } - - public static boolean switchUser(int userid) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return false; - return am.switchUser(userid); - } - - public static UserInfo getCurrentUser() throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return null; - return am.getCurrentUser(); - } - - public static Configuration getConfiguration() throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return null; - return am.getConfiguration(); - } - - public static IContentProvider getContentProvider(String auth, int userId) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return null; - ContentProviderHolder holder; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - holder = am.getContentProviderExternal(auth, userId, token, null); - } else { - holder = am.getContentProviderExternal(auth, userId, token); - } - return holder != null ? holder.provider : null; - } - - public static void registerUidObserver(IUidObserver observer, int which, int cutpoint, String callingPackage) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return; - am.registerUidObserver(observer, which, cutpoint, callingPackage); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java b/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java deleted file mode 100644 index 564f6130f..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java +++ /dev/null @@ -1,179 +0,0 @@ -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.app.ActivityManager; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.Parcel; -import android.os.ServiceManager; -import android.system.ErrnoException; -import android.system.Os; -import android.util.Log; - -import org.matrix.vector.daemon.BuildConfig; - -import java.lang.reflect.Field; -import java.util.Map; - -public class BridgeService { - - static final int TRANSACTION_CODE = ('_' << 24) | ('V' << 16) | ('E' << 8) | 'C'; - private static final String SERVICE_NAME = "activity"; - - enum ACTION { - ACTION_UNKNOWN, - ACTION_SEND_BINDER, - ACTION_GET_BINDER, - } - - public interface Listener { - void onSystemServerRestarted(); - - void onResponseFromBridgeService(boolean response); - - void onSystemServerDied(); - } - - private static IBinder serviceBinder = null; - - private static Listener listener; - private static IBinder bridgeService; - private static final IBinder.DeathRecipient bridgeRecipient = new IBinder.DeathRecipient() { - - @Override - public void binderDied() { - Log.i(TAG, "service " + SERVICE_NAME + " is dead. "); - - try { - //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi - Field field = ServiceManager.class.getDeclaredField("sServiceManager"); - field.setAccessible(true); - field.set(null, null); - - //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi - field = ServiceManager.class.getDeclaredField("sCache"); - field.setAccessible(true); - Object sCache = field.get(null); - if (sCache instanceof Map) { - //noinspection rawtypes - ((Map) sCache).clear(); - } - Log.i(TAG, "clear ServiceManager"); - - //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi - field = ActivityManager.class.getDeclaredField("IActivityManagerSingleton"); - field.setAccessible(true); - Object singleton = field.get(null); - if (singleton != null) { - //noinspection PrivateApi DiscouragedPrivateApi - field = Class.forName("android.util.Singleton").getDeclaredField("mInstance"); - field.setAccessible(true); - synchronized (singleton) { - field.set(singleton, null); - } - } - Log.i(TAG, "clear ActivityManager"); - } catch (Throwable e) { - Log.w(TAG, "clear ServiceManager: " + Log.getStackTraceString(e)); - } - - bridgeService.unlinkToDeath(this, 0); - bridgeService = null; - listener.onSystemServerDied(); - new Handler(Looper.getMainLooper()).post(() -> sendToBridge(serviceBinder, true)); - } - }; - - // For service - // This MUST run in main thread - private static synchronized void sendToBridge(IBinder binder, boolean isRestart) { - assert Looper.myLooper() == Looper.getMainLooper(); - try { - Os.seteuid(0); - } catch (ErrnoException e) { - Log.e(TAG, "seteuid 0", e); - } - try { - do { - bridgeService = ServiceManager.getService(SERVICE_NAME); - if (bridgeService != null && bridgeService.pingBinder()) { - break; - } - - Log.i(TAG, "service " + SERVICE_NAME + " is not started, wait 1s."); - - try { - //noinspection BusyWait - Thread.sleep(1000); - } catch (Throwable e) { - Log.w(TAG, "sleep" + Log.getStackTraceString(e)); - } - } while (true); - - if (isRestart && listener != null) { - listener.onSystemServerRestarted(); - } - - try { - bridgeService.linkToDeath(bridgeRecipient, 0); - } catch (Throwable e) { - Log.w(TAG, "linkToDeath " + Log.getStackTraceString(e)); - var snapshot = bridgeService; - sendToBridge(binder, snapshot == null || !snapshot.isBinderAlive()); - return; - } - - boolean res = false; - // try at most three times - for (int i = 0; i < 3; i++) { - Parcel data = Parcel.obtain(); - Parcel reply = Parcel.obtain(); - try { - data.writeInt(ACTION.ACTION_SEND_BINDER.ordinal()); - Log.v(TAG, "binder " + binder.toString()); - data.writeStrongBinder(binder); - if (bridgeService == null) break; - res = bridgeService.transact(TRANSACTION_CODE, data, reply, 0); - reply.readException(); - } catch (Throwable e) { - Log.e(TAG, "send binder " + Log.getStackTraceString(e)); - var snapshot = bridgeService; - sendToBridge(binder, snapshot == null || !snapshot.isBinderAlive()); - return; - } finally { - data.recycle(); - reply.recycle(); - } - - if (res) break; - - Log.w(TAG, "no response from bridge, retry in 1s"); - - try { - Thread.sleep(1000); - } catch (InterruptedException ignored) { - } - } - - if (listener != null) { - listener.onResponseFromBridgeService(res); - } - } finally { - try { - if (!BuildConfig.DEBUG) { - Os.seteuid(1000); - } - } catch (ErrnoException e) { - Log.e(TAG, "seteuid 1000", e); - } - } - } - - public static void send(LSPosedService service, Listener listener) { - BridgeService.listener = listener; - BridgeService.serviceBinder = service.asBinder(); - sendToBridge(serviceBinder, false); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java deleted file mode 100644 index 77f15f2d0..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java +++ /dev/null @@ -1,518 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; -import static org.lsposed.lspd.service.ServiceManager.toGlobalNamespace; - -import android.content.res.AssetManager; -import android.content.res.Resources; -import android.os.Binder; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.os.RemoteException; -import android.os.SELinux; -import android.os.SharedMemory; -import android.system.ErrnoException; -import android.system.Os; -import android.system.OsConstants; -import android.util.Log; - -import androidx.annotation.Nullable; - -import org.matrix.vector.daemon.BuildConfig; -import org.lsposed.lspd.models.PreLoadedApk; -import org.lsposed.lspd.util.InstallerVerifier; -import org.lsposed.lspd.util.Utils; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.lang.reflect.Method; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.LinkOption; -import java.nio.file.OpenOption; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.StandardOpenOption; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.PosixFilePermissions; -import java.time.Instant; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.stream.Stream; -import java.util.zip.Deflater; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; - -import hidden.HiddenApiBridge; - -public class ConfigFileManager { - static final Path basePath = Paths.get("/data/adb/lspd"); - static final Path modulePath = basePath.resolve("modules"); - static final Path daemonApkPath = Paths.get(System.getProperty("java.class.path", null)); - static final Path managerApkPath = daemonApkPath.getParent().resolve("manager.apk"); - static final File magiskDbPath = new File("/data/adb/magisk.db"); - private static final Path lockPath = basePath.resolve("lock"); - private static final Path configDirPath = basePath.resolve("config"); - static final File dbPath = configDirPath.resolve("modules_config.db").toFile(); - private static final Path logDirPath = basePath.resolve("log"); - private static final Path oldLogDirPath = basePath.resolve("log.old"); - private static final DateTimeFormatter formatter = - DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(Utils.getZoneId()); - @SuppressWarnings("FieldCanBeLocal") - private static FileLocker locker = null; - private static Resources res = null; - private static ParcelFileDescriptor fd = null; - private static SharedMemory preloadDex = null; - - static { - try { - Files.createDirectories(basePath); - SELinux.setFileContext(basePath.toString(), "u:object_r:system_file:s0"); - Files.createDirectories(configDirPath); - createLogDirPath(); - } catch (IOException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - - public static void transfer(InputStream in, OutputStream out) throws IOException { - int size = 8192; - var buffer = new byte[size]; - int read; - while ((read = in.read(buffer, 0, size)) >= 0) { - out.write(buffer, 0, read); - } - } - - private static void createLogDirPath() throws IOException { - if (!Files.isDirectory(logDirPath, LinkOption.NOFOLLOW_LINKS)) { - Files.deleteIfExists(logDirPath); - } - Files.createDirectories(logDirPath); - } - - public static Resources getResources() { - loadRes(); - return res; - } - - private static void loadRes() { - if (res != null) return; - try { - var am = AssetManager.class.newInstance(); - //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi - Method addAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); - addAssetPath.setAccessible(true); - //noinspection ConstantConditions - if ((int) addAssetPath.invoke(am, daemonApkPath.toString()) > 0) { - //noinspection deprecation - res = new Resources(am, null, null); - } - } catch (Throwable e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - - static void reloadConfiguration() { - loadRes(); - try { - var conf = ActivityManagerService.getConfiguration(); - if (conf != null) - //noinspection deprecation - res.updateConfiguration(conf, res.getDisplayMetrics()); - } catch (Throwable e) { - Log.e(TAG, "reload configuration", e); - } - } - - static ParcelFileDescriptor getManagerApk() throws IOException { - if (fd != null) return fd.dup(); - InstallerVerifier.verifyInstallerSignature(managerApkPath.toString()); - - SELinux.setFileContext(managerApkPath.toString(), "u:object_r:system_file:s0"); - fd = ParcelFileDescriptor.open(managerApkPath.toFile(), ParcelFileDescriptor.MODE_READ_ONLY); - return fd.dup(); - } - - static void deleteFolderIfExists(Path target) throws IOException { - if (Files.notExists(target)) return; - Files.walkFileTree(target, new SimpleFileVisitor<>() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) - throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException e) - throws IOException { - if (e == null) { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } else { - throw e; - } - } - }); - } - - public static boolean chattr0(Path path) { - try { - var dir = Os.open(path.toString(), OsConstants.O_RDONLY, 0); - // Clear all special file attributes on the directory - HiddenApiBridge.Os_ioctlInt(dir, Process.is64Bit() ? 0x40086602 : 0x40046602, 0); - Os.close(dir); - return true; - } catch (ErrnoException e) { - // If the operation is not supported (ENOTSUP), it means the filesystem doesn't support attributes. - // We can assume the file is not immutable and proceed. - if (e.errno == OsConstants.ENOTSUP) { - return true; - } - Log.d(TAG, "chattr 0", e); - return false; - } catch (Throwable e) { - Log.d(TAG, "chattr 0", e); - return false; - } - } - - static void moveLogDir() { - try { - if (Files.exists(logDirPath)) { - if (chattr0(logDirPath)) { - deleteFolderIfExists(oldLogDirPath); - Files.move(logDirPath, oldLogDirPath); - } - } - Files.createDirectories(logDirPath); - } catch (IOException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - - private static String getNewLogFileName(String prefix) { - return prefix + "_" + formatter.format(Instant.now()) + ".log"; - } - - static File getNewVerboseLogPath() throws IOException { - createLogDirPath(); - return logDirPath.resolve(getNewLogFileName("verbose")).toFile(); - } - - static File getNewModulesLogPath() throws IOException { - createLogDirPath(); - return logDirPath.resolve(getNewLogFileName("modules")).toFile(); - } - - static File getPropsPath() throws IOException { - createLogDirPath(); - return logDirPath.resolve("props.txt").toFile(); - } - - static File getKmsgPath() throws IOException { - createLogDirPath(); - return logDirPath.resolve("kmsg.log").toFile(); - } - - static void getLogs(ParcelFileDescriptor zipFd) throws IllegalStateException { - try (zipFd; var os = new ZipOutputStream(new FileOutputStream(zipFd.getFileDescriptor()))) { - var comment = String.format(Locale.ROOT, "LSPosed %s %s (%d)", - BuildConfig.BUILD_TYPE, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE); - os.setComment(comment); - os.setLevel(Deflater.BEST_COMPRESSION); - zipAddDir(os, logDirPath); - zipAddDir(os, oldLogDirPath); - zipAddDir(os, Paths.get("/data/tombstones")); - zipAddDir(os, Paths.get("/data/anr")); - var data = Paths.get("/data/data"); - var app1 = data.resolve(BuildConfig.MANAGER_INJECTED_PKG_NAME + "/cache/crash"); - var app2 = data.resolve(BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME + "/cache/crash"); - zipAddDir(os, app1); - zipAddDir(os, app2); - zipAddProcOutput(os, "full.log", "logcat", "-b", "all", "-d"); - zipAddProcOutput(os, "dmesg.log", "dmesg"); - var magiskDataDir = Paths.get("/data/adb"); - try (var l = Files.list(magiskDataDir.resolve("modules"))) { - l.forEach(p -> { - zipAddFile(os, p, magiskDataDir); - zipAddFile(os, p.resolve("module.prop"), magiskDataDir); - zipAddFile(os, p.resolve("remove"), magiskDataDir); - zipAddFile(os, p.resolve("disable"), magiskDataDir); - zipAddFile(os, p.resolve("update"), magiskDataDir); - zipAddFile(os, p.resolve("sepolicy.rule"), magiskDataDir); - }); - } - var proc = Paths.get("/proc"); - for (var pid : new String[]{"self", String.valueOf(Binder.getCallingPid())}) { - var pidPath = proc.resolve(pid); - zipAddFile(os, pidPath.resolve("maps"), proc); - zipAddFile(os, pidPath.resolve("mountinfo"), proc); - zipAddFile(os, pidPath.resolve("status"), proc); - } - zipAddFile(os, dbPath.toPath(), configDirPath); - ConfigManager.getInstance().exportScopes(os); - } catch (Throwable e) { - Log.w(TAG, "get log", e); - throw new IllegalStateException(e); - } - } - - private static void zipAddProcOutput(ZipOutputStream os, String name, String... command) { - try (var is = new ProcessBuilder(command).start().getInputStream()) { - os.putNextEntry(new ZipEntry(name)); - transfer(is, os); - os.closeEntry(); - } catch (IOException e) { - Log.w(TAG, name, e); - } - } - - private static void zipAddFile(ZipOutputStream os, Path path, Path base) { - var name = base.relativize(path).toString(); - if (Files.isDirectory(path)) { - try { - os.putNextEntry(new ZipEntry(name + "/")); - os.closeEntry(); - } catch (IOException e) { - Log.w(TAG, name, e); - } - } else if (Files.exists(path)) { - try (var is = new FileInputStream(path.toFile())) { - os.putNextEntry(new ZipEntry(name)); - transfer(is, os); - os.closeEntry(); - } catch (IOException e) { - Log.w(TAG, name, e); - } - } - } - - private static void zipAddDir(ZipOutputStream os, Path path) throws IOException { - if (!Files.isDirectory(path)) return; - Files.walkFileTree(path, new SimpleFileVisitor<>() { - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - if (Files.isRegularFile(file)) { - var name = path.getParent().relativize(file).toString(); - try (var is = new FileInputStream(file.toFile())) { - os.putNextEntry(new ZipEntry(name)); - transfer(is, os); - os.closeEntry(); - } catch (IOException e) { - Log.w(TAG, name, e); - } - } - return FileVisitResult.CONTINUE; - } - }); - } - - private static SharedMemory readDex(InputStream in, boolean obfuscate) throws IOException, ErrnoException { - var memory = SharedMemory.create(null, in.available()); - var byteBuffer = memory.mapReadWrite(); - Channels.newChannel(in).read(byteBuffer); - SharedMemory.unmap(byteBuffer); - if (obfuscate) { - var newMemory = ObfuscationManager.obfuscateDex(memory); - if (memory != newMemory) { - memory.close(); - memory = newMemory; - } - } - memory.setProtect(OsConstants.PROT_READ); - return memory; - } - - private static void readDexes(ZipFile apkFile, List preLoadedDexes, - boolean obfuscate) { - int secondary = 2; - for (var dexFile = apkFile.getEntry("classes.dex"); dexFile != null; - dexFile = apkFile.getEntry("classes" + secondary + ".dex"), secondary++) { - try (var is = apkFile.getInputStream(dexFile)) { - preLoadedDexes.add(readDex(is, obfuscate)); - } catch (IOException | ErrnoException e) { - Log.w(TAG, "Can not load " + dexFile + " in " + apkFile, e); - } - } - } - - private static void readName(ZipFile apkFile, String initName, List names) { - var initEntry = apkFile.getEntry(initName); - if (initEntry == null) return; - try (var in = apkFile.getInputStream(initEntry)) { - var reader = new BufferedReader(new InputStreamReader(in)); - String name; - while ((name = reader.readLine()) != null) { - name = name.trim(); - if (name.isEmpty() || name.startsWith("#")) continue; - names.add(name); - } - } catch (IOException | OutOfMemoryError e) { - Log.e(TAG, "Can not open " + initEntry, e); - } - } - - @Nullable - static PreLoadedApk loadModule(String path, boolean obfuscate) { - if (path == null) return null; - var file = new PreLoadedApk(); - var preLoadedDexes = new ArrayList(); - var moduleClassNames = new ArrayList(1); - var moduleLibraryNames = new ArrayList(1); - try (var apkFile = new ZipFile(toGlobalNamespace(path))) { - readDexes(apkFile, preLoadedDexes, obfuscate); - readName(apkFile, "META-INF/xposed/java_init.list", moduleClassNames); - if (moduleClassNames.isEmpty()) { - file.legacy = true; - readName(apkFile, "assets/xposed_init", moduleClassNames); - readName(apkFile, "assets/native_init", moduleLibraryNames); - } else { - file.legacy = false; - readName(apkFile, "META-INF/xposed/native_init.list", moduleLibraryNames); - } - } catch (IOException e) { - Log.e(TAG, "Can not open " + path, e); - return null; - } - if (preLoadedDexes.isEmpty()) return null; - if (moduleClassNames.isEmpty()) return null; - - if (obfuscate) { - var signatures = ObfuscationManager.getSignatures(); - for (int i = 0; i < moduleClassNames.size(); i++) { - var s = moduleClassNames.get(i); - for (var entry : signatures.entrySet()) { - if (s.startsWith(entry.getKey())) { - moduleClassNames.add(i, s.replace(entry.getKey(), entry.getValue())); - } - } - } - } - - file.preLoadedDexes = preLoadedDexes; - file.moduleClassNames = moduleClassNames; - file.moduleLibraryNames = moduleLibraryNames; - return file; - } - - static boolean tryLock() { - var openOptions = new HashSet(); - openOptions.add(StandardOpenOption.CREATE); - openOptions.add(StandardOpenOption.WRITE); - var p = PosixFilePermissions.fromString("rw-------"); - var permissions = PosixFilePermissions.asFileAttribute(p); - - try { - var lockChannel = FileChannel.open(lockPath, openOptions, permissions); - locker = new FileLocker(lockChannel); - return locker.isValid(); - } catch (Throwable e) { - return false; - } - } - - synchronized static SharedMemory getPreloadDex(boolean obfuscate) { - if (preloadDex == null) { - try (var is = new FileInputStream("framework/lspd.dex")) { - preloadDex = readDex(is, obfuscate); - } catch (Throwable e) { - Log.e(TAG, "preload dex", e); - } - } - return preloadDex; - } - - static void ensureModuleFilePath(String path) throws RemoteException { - if (path == null || path.indexOf(File.separatorChar) >= 0 || ".".equals(path) || "..".equals(path)) { - throw new RemoteException("Invalid path: " + path); - } - } - - static Path resolveModuleDir(String packageName, String dir, int userId, int uid) throws IOException { - var path = modulePath.resolve(String.valueOf(userId)).resolve(packageName).resolve(dir).normalize(); - // Ensure the directory and any necessary parent directories exist. - path.toFile().mkdirs(); - - if (SELinux.getFileContext(path.toString()) != "u:object_r:xposed_data:s0") { - // SELinux label could be reset after a reboot. - try { - setSelinuxContextRecursive(path, "u:object_r:xposed_data:s0"); - Os.chown(path.toString(), uid, uid); - Os.chmod(path.toString(), 0755); - } catch (ErrnoException e) { - throw new IOException(e); - } - } - return path; - } - - private static void setSelinuxContextRecursive(Path path, String context) throws IOException { - try { - SELinux.setFileContext(path.toString(), context); - - if (Files.isDirectory(path)) { - try (Stream stream = Files.list(path)) { - for (Path entry : (Iterable) stream::iterator) { - setSelinuxContextRecursive(entry, context); - } - } - } - } catch (Exception e) { - throw new IOException("Failed to recursively set SELinux context for " + path, e); - } - } - - private static class FileLocker { - private final FileChannel lockChannel; - private final FileLock locker; - - FileLocker(FileChannel lockChannel) throws IOException { - this.lockChannel = lockChannel; - this.locker = lockChannel.tryLock(); - } - - boolean isValid() { - return this.locker != null && this.locker.isValid(); - } - - @Override - protected void finalize() throws Throwable { - this.locker.release(); - this.lockChannel.close(); - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java deleted file mode 100644 index f81025fb9..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java +++ /dev/null @@ -1,1274 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.PackageService.MATCH_ALL_FLAGS; -import static org.lsposed.lspd.service.PackageService.PER_USER_RANGE; -import static org.lsposed.lspd.service.ServiceManager.TAG; -import static org.lsposed.lspd.service.ServiceManager.existsInGlobalNamespace; -import static org.lsposed.lspd.service.ServiceManager.toGlobalNamespace; - -import android.annotation.SuppressLint; -import android.content.ContentValues; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageParser; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; -import android.database.sqlite.SQLiteStatement; -import android.os.Binder; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.os.RemoteException; -import android.os.SELinux; -import android.os.SharedMemory; -import android.os.SystemClock; -import android.system.ErrnoException; -import android.system.Os; -import android.util.Log; -import android.util.Pair; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.apache.commons.lang3.SerializationUtilsX; -import org.matrix.vector.daemon.BuildConfig; -import org.lsposed.lspd.models.Application; -import org.lsposed.lspd.models.Module; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.Serializable; -import java.nio.charset.StandardCharsets; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.PosixFilePermissions; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Consumer; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; - -import hidden.HiddenApiBridge; - -public class ConfigManager { - private static ConfigManager instance = null; - - private final SQLiteDatabase db = openDb(); - - private boolean verboseLog = true; - private boolean logWatchdog = true; - private boolean dexObfuscate = true; - private boolean enableStatusNotification = true; - private Path miscPath = null; - - private int managerUid = -1; - - private final Handler cacheHandler; - - private long lastModuleCacheTime = 0; - private long requestModuleCacheTime = 0; - - private long lastScopeCacheTime = 0; - private long requestScopeCacheTime = 0; - - private String api = "(???)"; - - static class ProcessScope { - final String processName; - final int uid; - - ProcessScope(@NonNull String processName, int uid) { - this.processName = processName; - this.uid = uid; - } - - @Override - public boolean equals(@Nullable Object o) { - if (o instanceof ProcessScope) { - ProcessScope p = (ProcessScope) o; - return p.processName.equals(processName) && p.uid == uid; - } - return false; - } - - @Override - public int hashCode() { - return Objects.hashCode(processName) ^ uid; - } - } - - private static final String CREATE_MODULES_TABLE = "CREATE TABLE IF NOT EXISTS modules (" + - "mid integer PRIMARY KEY AUTOINCREMENT," + - "module_pkg_name text NOT NULL UNIQUE," + - "apk_path text NOT NULL, " + - "enabled BOOLEAN DEFAULT 0 " + - "CHECK (enabled IN (0, 1))" + - ");"; - - private static final String CREATE_SCOPE_TABLE = "CREATE TABLE IF NOT EXISTS scope (" + - "mid integer," + - "app_pkg_name text NOT NULL," + - "user_id integer NOT NULL," + - "PRIMARY KEY (mid, app_pkg_name, user_id)," + - "CONSTRAINT scope_module_constraint" + - " FOREIGN KEY (mid)" + - " REFERENCES modules (mid)" + - " ON DELETE CASCADE" + - ");"; - - private static final String CREATE_CONFIG_TABLE = "CREATE TABLE IF NOT EXISTS configs (" + - "module_pkg_name text NOT NULL," + - "user_id integer NOT NULL," + - "`group` text NOT NULL," + - "`key` text NOT NULL," + - "data blob NOT NULL," + - "PRIMARY KEY (module_pkg_name, user_id, `group`, `key`)," + - "CONSTRAINT config_module_constraint" + - " FOREIGN KEY (module_pkg_name)" + - " REFERENCES modules (module_pkg_name)" + - " ON DELETE CASCADE" + - ");"; - - private final Map> cachedScope = new ConcurrentHashMap<>(); - - // packageName, Module - private final Map cachedModule = new ConcurrentHashMap<>(); - - // packageName, userId, group, key, value - private final Map, Map>> cachedConfig = new ConcurrentHashMap<>(); - - private Set scopeRequestBlocked = new HashSet<>(); - - private static SQLiteDatabase openDb() { - var params = new SQLiteDatabase.OpenParams.Builder() - .addOpenFlags(SQLiteDatabase.CREATE_IF_NECESSARY | SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) - .setErrorHandler(sqLiteDatabase -> Log.w(TAG, "database corrupted")); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - params.setSynchronousMode("NORMAL"); - } - return SQLiteDatabase.openDatabase(ConfigFileManager.dbPath.getAbsoluteFile(), params.build()); - } - - private void updateCaches(boolean sync) { - synchronized (cacheHandler) { - requestScopeCacheTime = requestModuleCacheTime = SystemClock.elapsedRealtime(); - } - if (sync) { - cacheModules(); - } else { - cacheHandler.post(this::cacheModules); - } - } - - // for system server, cache is not yet ready, we need to query database for it - public boolean shouldSkipSystemServer() { - if (!SELinux.checkSELinuxAccess("u:r:system_server:s0", "u:r:system_server:s0", "process", "execmem")) { - Log.e(TAG, "skip injecting into android because sepolicy was not loaded properly"); - return true; // skip - } - /* - try (Cursor cursor = db.query("scope INNER JOIN modules ON scope.mid = modules.mid", new String[]{"modules.mid"}, "app_pkg_name=? AND enabled=1", new String[]{"system"}, null, null, null)) { - return cursor == null || !cursor.moveToNext(); - }*/ - return false; - } - - @SuppressLint("BlockedPrivateApi") - public List getModulesForSystemServer() { - List modules = new LinkedList<>(); - try (Cursor cursor = db.query("scope INNER JOIN modules ON scope.mid = modules.mid", new String[]{"module_pkg_name", "apk_path"}, "app_pkg_name=? AND enabled=1", new String[]{"system"}, null, null, null)) { - int apkPathIdx = cursor.getColumnIndex("apk_path"); - int pkgNameIdx = cursor.getColumnIndex("module_pkg_name"); - while (cursor.moveToNext()) { - var module = new Module(); - module.apkPath = cursor.getString(apkPathIdx); - module.packageName = cursor.getString(pkgNameIdx); - var cached = cachedModule.get(module.packageName); - if (cached != null) { - modules.add(cached); - continue; - } - var statPath = toGlobalNamespace("/data/user_de/0/" + module.packageName).getAbsolutePath(); - try { - module.appId = Os.stat(statPath).st_uid; - } catch (ErrnoException e) { - Log.w(TAG, "cannot stat " + statPath, e); - module.appId = -1; - } - try { - var apkFile = new File(module.apkPath); - var pkg = new PackageParser().parsePackage(apkFile, 0, false); - module.applicationInfo = pkg.applicationInfo; - module.applicationInfo.sourceDir = module.apkPath; - module.applicationInfo.dataDir = statPath; - module.applicationInfo.deviceProtectedDataDir = statPath; - HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(module.applicationInfo, statPath); - module.applicationInfo.processName = module.packageName; - } catch (PackageParser.PackageParserException e) { - Log.w(TAG, "failed to parse " + module.apkPath, e); - } - module.service = new LSPInjectedModuleService(module.packageName); - modules.add(module); - } - } - - return modules.parallelStream().filter(m -> { - var file = ConfigFileManager.loadModule(m.apkPath, dexObfuscate); - if (file == null) { - Log.w(TAG, "Can not load " + m.apkPath + ", skip!"); - return false; - } - m.file = file; - cachedModule.putIfAbsent(m.packageName, m); - return true; - }).collect(Collectors.toList()); - } - - private synchronized void updateConfig() { - Map config = getModulePrefs("lspd", 0, "config"); - - Object bool = config.get("enable_verbose_log"); - verboseLog = bool == null || (boolean) bool; - - bool = config.get("enable_log_watchdog"); - logWatchdog = bool == null || (boolean) bool; - - bool = config.get("enable_dex_obfuscate"); - dexObfuscate = bool == null || (boolean) bool; - - bool = config.get("enable_auto_add_shortcut"); - if (bool != null) { - // TODO: remove - updateModulePrefs("lspd", 0, "config", "enable_auto_add_shortcut", null); - } - - bool = config.get("enable_status_notification"); - enableStatusNotification = bool == null || (boolean) bool; - - var set = (Set) config.get("scope_request_blocked"); - scopeRequestBlocked = set == null ? new HashSet<>() : set; - - // Don't migrate to ConfigFileManager, as XSharedPreferences will be restored soon - String string = (String) config.get("misc_path"); - if (string == null) { - miscPath = Paths.get("/data", "misc", UUID.randomUUID().toString()); - updateModulePrefs("lspd", 0, "config", "misc_path", miscPath.toString()); - } else { - miscPath = Paths.get(string); - } - try { - var perms = PosixFilePermissions.fromString("rwx--x--x"); - Files.createDirectories(miscPath, PosixFilePermissions.asFileAttribute(perms)); - walkFileTree(miscPath, f -> SELinux.setFileContext(f.toString(), "u:object_r:xposed_data:s0")); - } catch (IOException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - - updateManager(false); - - cacheHandler.post(this::getPreloadDex); - } - - public synchronized void updateManager(boolean uninstalled) { - if (uninstalled) { - managerUid = -1; - return; - } - if (!PackageService.isAlive()) return; - try { - PackageInfo info = PackageService.getPackageInfo(BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME, 0, 0); - if (info != null) { - managerUid = info.applicationInfo.uid; - } else { - managerUid = -1; - Log.i(TAG, "manager is not installed"); - } - } catch (RemoteException ignored) { - } - } - - static ConfigManager getInstance() { - long enterTime = SystemClock.elapsedRealtime(); - int tid = Process.myTid(); - int uid = Binder.getCallingUid(); - Log.d(TAG, "getInstance [TID:" + tid + " UID:" + uid + "]: entered"); - - if (instance == null) { - instance = new ConfigManager(); - Log.d(TAG, "new instance created for [TID:" + tid + " UID:" + uid + "]"); - } - boolean needCached; - synchronized (instance.cacheHandler) { - needCached = instance.lastModuleCacheTime == 0 || instance.lastScopeCacheTime == 0; - } - if (needCached) { - if (PackageService.isAlive() && UserService.isAlive()) { - Log.d(TAG, "pm & um are ready, updating cache"); - // must ensure cache is valid for later usage - instance.updateCaches(true); - // instance.updateManager(false); - } - } - return instance; - } - - private ConfigManager() { - HandlerThread cacheThread = new HandlerThread("cache"); - cacheThread.start(); - cacheHandler = new Handler(cacheThread.getLooper()); - - initDB(); - updateConfig(); - // must ensure cache is valid for later usage - updateCaches(true); - } - - - private T executeInTransaction(Supplier execution) { - try { - db.beginTransaction(); - var res = execution.get(); - db.setTransactionSuccessful(); - return res; - } finally { - db.endTransaction(); - } - } - - private void executeInTransaction(Runnable execution) { - executeInTransaction((Supplier) () -> { - execution.run(); - return null; - }); - } - - private void initDB() { - db.setForeignKeyConstraintsEnabled(true); - int oldVersion = db.getVersion(); - if (oldVersion >= 4) { - // Database is already up to date. - return; - } - - Log.i(TAG, "Initializing/Upgrading database from version " + oldVersion + " to 4"); - db.beginTransaction(); - try { - if (oldVersion == 0) { - db.execSQL(CREATE_MODULES_TABLE); - db.execSQL(CREATE_SCOPE_TABLE); - db.execSQL(CREATE_CONFIG_TABLE); - - var values = new ContentValues(); - values.put("module_pkg_name", "lspd"); - values.put("apk_path", ConfigFileManager.managerApkPath.toString()); - db.insertWithOnConflict("modules", null, values, SQLiteDatabase.CONFLICT_IGNORE); - oldVersion = 1; - } - if (oldVersion < 2) { - // Upgrade from 1 to 2: Recreate tables to enforce constraints and clean up. - db.compileStatement("DROP INDEX IF EXISTS configs_idx;").execute(); - db.compileStatement("DROP TABLE IF EXISTS config;").execute(); - db.compileStatement("ALTER TABLE scope RENAME TO old_scope;").execute(); - db.compileStatement("ALTER TABLE configs RENAME TO old_configs;").execute(); - - db.execSQL(CREATE_SCOPE_TABLE); - db.execSQL(CREATE_CONFIG_TABLE); - - try { - db.compileStatement("INSERT INTO scope SELECT * FROM old_scope;").execute(); - } catch (Throwable e) { - Log.w(TAG, "Failed to migrate scope data", e); - } - try { - db.compileStatement("INSERT INTO configs SELECT * FROM old_configs;").execute(); - } catch (Throwable e) { - Log.w(TAG, "Failed to migrate config data", e); - } - - db.compileStatement("DROP TABLE old_scope;").execute(); - db.compileStatement("DROP TABLE old_configs;").execute(); - db.compileStatement("CREATE INDEX IF NOT EXISTS configs_idx ON configs (module_pkg_name, user_id);").execute(); - } - if (oldVersion < 3) { - // Upgrade from 2 to 3: Rename 'android' scope to 'system'. - db.compileStatement("UPDATE scope SET app_pkg_name = 'system' WHERE app_pkg_name = 'android';").execute(); - } - if (oldVersion < 4) { - // Upgrade from 3 to 4: Add the 'auto_include' column to the modules table. - try { - db.compileStatement("ALTER TABLE modules ADD COLUMN auto_include BOOLEAN DEFAULT 0 CHECK (auto_include IN (0, 1));").execute(); - } catch (SQLiteException ex) { - // This might happen if the column already exists from a previous buggy run. - Log.w(TAG, "Could not add auto_include column, it may already exist.", ex); - } - } - db.setVersion(4); - db.setTransactionSuccessful(); - Log.i(TAG, "Database upgrade to version 4 successful."); - } catch (Throwable e) { - Log.e(TAG, "Failed to initialize or upgrade database, transaction rolled back.", e); - } finally { - db.endTransaction(); - } - } - - private List getAssociatedProcesses(Application app) throws RemoteException { - Pair, Integer> result = PackageService.fetchProcessesWithUid(app); - List processes = new ArrayList<>(); - if (app.packageName.equals("android")) { - // this is hardcoded for ResolverActivity - processes.add(new ProcessScope("system:ui", Process.SYSTEM_UID)); - } - for (String processName : result.first) { - var uid = result.second; - if (uid == Process.SYSTEM_UID && processName.equals("system")) { - // code run in system_server - continue; - } - processes.add(new ProcessScope(processName, uid)); - } - return processes; - } - - private @NonNull - Map> fetchModuleConfig(String name, int user_id) { - var config = new ConcurrentHashMap>(); - - try (Cursor cursor = db.query("configs", new String[]{"`group`", "`key`", "data"}, - "module_pkg_name = ? and user_id = ?", new String[]{name, String.valueOf(user_id)}, null, null, null)) { - if (cursor == null) { - Log.e(TAG, "db cache failed"); - return config; - } - int groupIdx = cursor.getColumnIndex("group"); - int keyIdx = cursor.getColumnIndex("key"); - int dataIdx = cursor.getColumnIndex("data"); - while (cursor.moveToNext()) { - var group = cursor.getString(groupIdx); - var key = cursor.getString(keyIdx); - var data = cursor.getBlob(dataIdx); - var object = SerializationUtilsX.deserialize(data); - if (object == null) continue; - config.computeIfAbsent(group, g -> new HashMap<>()).put(key, object); - } - } - return config; - } - - public void updateModulePrefs(String moduleName, int userId, String group, String key, Object value) { - Map values = new HashMap<>(); - values.put(key, value); - updateModulePrefs(moduleName, userId, group, values); - } - - public void updateModulePrefs(String moduleName, int userId, String group, Map values) { - var config = cachedConfig.computeIfAbsent(new Pair<>(moduleName, userId), module -> fetchModuleConfig(module.first, module.second)); - config.compute(group, (g, prefs) -> { - HashMap newPrefs = prefs == null ? new HashMap<>() : new HashMap<>(prefs); - executeInTransaction(() -> { - for (var entry : values.entrySet()) { - var key = entry.getKey(); - var value = entry.getValue(); - if (value instanceof Serializable) { - newPrefs.put(key, value); - var contents = new ContentValues(); - contents.put("`group`", group); - contents.put("`key`", key); - contents.put("data", SerializationUtilsX.serialize((Serializable) value)); - contents.put("module_pkg_name", moduleName); - contents.put("user_id", String.valueOf(userId)); - db.insertWithOnConflict("configs", null, contents, SQLiteDatabase.CONFLICT_REPLACE); - } else { - newPrefs.remove(key); - db.delete("configs", "module_pkg_name=? and user_id=? and `group`=? and `key`=?", new String[]{moduleName, String.valueOf(userId), group, key}); - } - } - var bundle = new Bundle(); - bundle.putSerializable("config", (Serializable) config); - if (bundle.size() > 1024 * 1024) { - throw new IllegalArgumentException("Preference too large"); - } - }); - return newPrefs; - }); - } - - public void deleteModulePrefs(String moduleName, int userId, String group) { - db.delete("configs", "module_pkg_name=? and user_id=? and `group`=?", new String[]{moduleName, String.valueOf(userId), group}); - var config = cachedConfig.getOrDefault(new Pair<>(moduleName, userId), null); - if (config != null) { - config.remove(group); - } - } - - public HashMap getModulePrefs(String moduleName, int userId, String group) { - var config = cachedConfig.computeIfAbsent(new Pair<>(moduleName, userId), module -> fetchModuleConfig(module.first, module.second)); - return config.getOrDefault(group, new HashMap<>()); - } - - private synchronized void clearCache() { - synchronized (cacheHandler) { - lastScopeCacheTime = 0; - lastModuleCacheTime = 0; - } - cachedModule.clear(); - cachedScope.clear(); - } - - private synchronized void cacheModules() { - // skip caching when pm is not yet available - if (!PackageService.isAlive() || !UserService.isAlive()) return; - synchronized (cacheHandler) { - if (lastModuleCacheTime >= requestModuleCacheTime) return; - else lastModuleCacheTime = SystemClock.elapsedRealtime(); - } - Set toClose = ConcurrentHashMap.newKeySet(); - try (Cursor cursor = db.query(true, "modules", new String[]{"module_pkg_name", "apk_path"}, - "enabled = 1", null, null, null, null, null)) { - if (cursor == null) { - Log.e(TAG, "db cache failed"); - return; - } - int pkgNameIdx = cursor.getColumnIndex("module_pkg_name"); - int apkPathIdx = cursor.getColumnIndex("apk_path"); - Set obsoleteModules = ConcurrentHashMap.newKeySet(); - // packageName, apkPath - Map obsoletePaths = new ConcurrentHashMap<>(); - cachedModule.values().removeIf(m -> { - if (m.apkPath == null || !existsInGlobalNamespace(m.apkPath)) { - toClose.addAll(m.file.preLoadedDexes); - return true; - } - return false; - }); - List modules = new ArrayList<>(); - while (cursor.moveToNext()) { - String packageName = cursor.getString(pkgNameIdx); - String apkPath = cursor.getString(apkPathIdx); - if (packageName.equals("lspd")) continue; - var module = new Module(); - module.packageName = packageName; - module.apkPath = apkPath; - modules.add(module); - } - - modules.stream().parallel().filter(m -> { - var oldModule = cachedModule.get(m.packageName); - PackageInfo pkgInfo = null; - try { - pkgInfo = PackageService.getPackageInfoFromAllUsers(m.packageName, MATCH_ALL_FLAGS).values().stream().findFirst().orElse(null); - } catch (Throwable e) { - Log.w(TAG, "Get package info of " + m.packageName, e); - } - if (pkgInfo == null || pkgInfo.applicationInfo == null) { - Log.w(TAG, "Failed to find package info of " + m.packageName); - obsoleteModules.add(m.packageName); - return false; - } - - if (oldModule != null && - pkgInfo.applicationInfo.sourceDir != null && - m.apkPath != null && oldModule.apkPath != null && - existsInGlobalNamespace(m.apkPath) && - Objects.equals(m.apkPath, oldModule.apkPath) && - Objects.equals(new File(pkgInfo.applicationInfo.sourceDir).getParent(), new File(m.apkPath).getParent())) { - if (oldModule.appId != -1) { - Log.d(TAG, m.packageName + " did not change, skip caching it"); - } else { - // cache from system server, update application info - oldModule.applicationInfo = pkgInfo.applicationInfo; - } - return false; - } - m.apkPath = getModuleApkPath(pkgInfo.applicationInfo); - if (m.apkPath == null) { - Log.w(TAG, "Failed to find path of " + m.packageName); - obsoleteModules.add(m.packageName); - return false; - } else { - obsoletePaths.put(m.packageName, m.apkPath); - } - m.appId = pkgInfo.applicationInfo.uid; - m.applicationInfo = pkgInfo.applicationInfo; - m.service = oldModule != null ? oldModule.service : new LSPInjectedModuleService(m.packageName); - return true; - }).forEach(m -> { - var file = ConfigFileManager.loadModule(m.apkPath, dexObfuscate); - if (file == null) { - Log.w(TAG, "failed to load module " + m.packageName); - obsoleteModules.add(m.packageName); - return; - } - m.file = file; - cachedModule.put(m.packageName, m); - }); - - if (PackageService.isAlive()) { - obsoleteModules.forEach(this::removeModuleWithoutCache); - obsoletePaths.forEach((packageName, path) -> updateModuleApkPath(packageName, path, true)); - } else { - Log.w(TAG, "pm is dead while caching. invalidating..."); - clearCache(); - return; - } - } - Log.d(TAG, "cached modules"); - for (var module : cachedModule.entrySet()) { - Log.d(TAG, module.getKey() + " " + module.getValue().apkPath); - } - cacheScopes(); - toClose.forEach(SharedMemory::close); - } - - private synchronized void cacheScopes() { - // skip caching when pm is not yet available - if (!PackageService.isAlive()) return; - synchronized (cacheHandler) { - if (lastScopeCacheTime >= requestScopeCacheTime) return; - else lastScopeCacheTime = SystemClock.elapsedRealtime(); - } - cachedScope.clear(); - try (Cursor cursor = db.query("scope INNER JOIN modules ON scope.mid = modules.mid", new String[]{"app_pkg_name", "module_pkg_name", "user_id"}, - "enabled = 1", null, null, null, null)) { - int appPkgNameIdx = cursor.getColumnIndex("app_pkg_name"); - int modulePkgNameIdx = cursor.getColumnIndex("module_pkg_name"); - int userIdIdx = cursor.getColumnIndex("user_id"); - - final var obsoletePackages = new HashSet(); - final var obsoleteModules = new HashSet(); - final var moduleAvailability = new HashMap, Boolean>(); - final var cachedProcessScope = new HashMap, List>(); - - final var denylist = new HashSet<>(getDenyListPackages()); - while (cursor.moveToNext()) { - Application app = new Application(); - app.packageName = cursor.getString(appPkgNameIdx); - app.userId = cursor.getInt(userIdIdx); - var modulePackageName = cursor.getString(modulePkgNameIdx); - - // check if module is present in this user - if (!moduleAvailability.computeIfAbsent(new Pair<>(modulePackageName, app.userId), n -> { - var available = false; - try { - available = PackageService.isPackageAvailable(n.first, n.second, true) && cachedModule.containsKey(modulePackageName); - } catch (Throwable e) { - Log.w(TAG, "check package availability ", e); - } - if (!available) { - var obsoleteModule = new Application(); - obsoleteModule.packageName = modulePackageName; - obsoleteModule.userId = app.userId; - obsoleteModules.add(obsoleteModule); - } - return available; - })) continue; - - // system server always loads database - if (app.packageName.equals("system")) continue; - - try { - List processesScope = cachedProcessScope.computeIfAbsent(new Pair<>(app.packageName, app.userId), (k) -> { - try { - if (denylist.contains(app.packageName)) - Log.w(TAG, app.packageName + " is on denylist. It may not take effect."); - return getAssociatedProcesses(app); - } catch (RemoteException e) { - return Collections.emptyList(); - } - }); - if (processesScope.isEmpty()) { - obsoletePackages.add(app); - continue; - } - var module = cachedModule.get(modulePackageName); - assert module != null; - for (ProcessScope processScope : processesScope) { - cachedScope.computeIfAbsent(processScope, - ignored -> new LinkedList<>()).add(module); - // Always allow the module to inject itself - if (modulePackageName.equals(app.packageName)) { - var appId = processScope.uid % PER_USER_RANGE; - for (var user : UserService.getUsers()) { - var moduleUid = user.id * PER_USER_RANGE + appId; - if (moduleUid == processScope.uid) continue; // skip duplicate - var moduleSelf = new ProcessScope(processScope.processName, moduleUid); - cachedScope.computeIfAbsent(moduleSelf, - ignored -> new LinkedList<>()).add(module); - } - } - } - } catch (RemoteException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - if (PackageService.isAlive()) { - for (Application obsoletePackage : obsoletePackages) { - Log.d(TAG, "removing obsolete package: " + obsoletePackage.packageName + "/" + obsoletePackage.userId); - removeAppWithoutCache(obsoletePackage); - } - for (Application obsoleteModule : obsoleteModules) { - Log.d(TAG, "removing obsolete module: " + obsoleteModule.packageName + "/" + obsoleteModule.userId); - removeModuleScopeWithoutCache(obsoleteModule); - removeBlockedScopeRequest(obsoleteModule.packageName); - } - } else { - Log.w(TAG, "pm is dead while caching. invalidating..."); - clearCache(); - return; - } - } - Log.d(TAG, "cached scope"); - cachedScope.forEach((ps, modules) -> { - Log.d(TAG, ps.processName + "/" + ps.uid); - modules.forEach(module -> Log.d(TAG, "\t" + module.packageName)); - }); - } - - // This is called when a new process created, use the cached result - public List getModulesForProcess(String processName, int uid) { - return isManager(uid) ? Collections.emptyList() : cachedScope.getOrDefault(new ProcessScope(processName, uid), Collections.emptyList()); - } - - // This is called when a new process created, use the cached result - public boolean shouldSkipProcess(ProcessScope scope) { - return !cachedScope.containsKey(scope) && !isManager(scope.uid); - } - - public boolean isUidHooked(int uid) { - return cachedScope.keySet().stream().reduce(false, (p, scope) -> p || scope.uid == uid, Boolean::logicalOr); - } - - @Nullable - public List getModuleScope(String packageName) { - if (packageName.equals("lspd")) return null; - try (Cursor cursor = db.query("scope INNER JOIN modules ON scope.mid = modules.mid", new String[]{"app_pkg_name", "user_id"}, - "modules.module_pkg_name = ?", new String[]{packageName}, null, null, null)) { - if (cursor == null) { - return null; - } - int userIdIdx = cursor.getColumnIndex("user_id"); - int appPkgNameIdx = cursor.getColumnIndex("app_pkg_name"); - ArrayList result = new ArrayList<>(); - while (cursor.moveToNext()) { - Application scope = new Application(); - scope.packageName = cursor.getString(appPkgNameIdx); - scope.userId = cursor.getInt(userIdIdx); - result.add(scope); - } - return result; - } - } - - @Nullable - public String getModuleApkPath(ApplicationInfo info) { - String[] apks; - if (info.splitSourceDirs != null) { - apks = Arrays.copyOf(info.splitSourceDirs, info.splitSourceDirs.length + 1); - apks[info.splitSourceDirs.length] = info.sourceDir; - } else apks = new String[]{info.sourceDir}; - var apkPath = Arrays.stream(apks).parallel().filter(apk -> { - if (apk == null) { - Log.w(TAG, info.packageName + " has null apk path???"); - return false; - } - try (var zip = new ZipFile(toGlobalNamespace(apk))) { - return zip.getEntry("META-INF/xposed/java_init.list") != null || zip.getEntry("assets/xposed_init") != null; - } catch (IOException e) { - return false; - } - }).findFirst(); - return apkPath.orElse(null); - } - - public boolean updateModuleApkPath(String packageName, String apkPath, boolean force) { - if (apkPath == null || packageName.equals("lspd")) return false; - if (db.inTransaction()) { - Log.w(TAG, "update module apk path should not be called inside transaction"); - return false; - } - - ContentValues values = new ContentValues(); - values.put("module_pkg_name", packageName); - values.put("apk_path", apkPath); - // insert or update in two step since insert or replace will change the autoincrement mid - int count = (int) db.insertWithOnConflict("modules", null, values, SQLiteDatabase.CONFLICT_IGNORE); - if (count < 0) { - var cached = cachedModule.getOrDefault(packageName, null); - if (force || cached == null || cached.apkPath == null || !cached.apkPath.equals(apkPath)) - count = db.updateWithOnConflict("modules", values, "module_pkg_name=?", new String[]{packageName}, SQLiteDatabase.CONFLICT_IGNORE); - else - count = 0; - } - // force update is because cache is already update to date - // skip caching again - if (!force && count > 0) { - // Called by oneway binder - updateCaches(true); - return true; - } - return count > 0; - } - - // Only be called before updating modules. No need to cache. - private int getModuleId(String packageName) { - if (packageName.equals("lspd")) return -1; - if (db.inTransaction()) { - Log.w(TAG, "get module id should not be called inside transaction"); - return -1; - } - try (Cursor cursor = db.query("modules", new String[]{"mid"}, "module_pkg_name=?", new String[]{packageName}, null, null, null)) { - if (cursor == null) return -1; - if (cursor.getCount() != 1) return -1; - cursor.moveToFirst(); - return cursor.getInt(cursor.getColumnIndexOrThrow("mid")); - } - } - - public boolean setModuleScope(String packageName, List scopes) throws RemoteException { - if (scopes == null) return false; - enableModule(packageName); - int mid = getModuleId(packageName); - if (mid == -1) return false; - executeInTransaction(() -> { - db.delete("scope", "mid = ?", new String[]{String.valueOf(mid)}); - for (Application app : scopes) { - if (app.packageName.equals("system") && app.userId != 0) continue; - ContentValues values = new ContentValues(); - values.put("mid", mid); - values.put("app_pkg_name", app.packageName); - values.put("user_id", app.userId); - db.insertWithOnConflict("scope", null, values, SQLiteDatabase.CONFLICT_IGNORE); - } - }); - // Called by manager, should be async - updateCaches(false); - return true; - } - - public boolean setModuleScope(String packageName, String scopePackageName, int userId) { - if (scopePackageName == null) return false; - int mid = getModuleId(packageName); - if (mid == -1) return false; - if (scopePackageName.equals("system") && userId != 0) return false; - executeInTransaction(() -> { - ContentValues values = new ContentValues(); - values.put("mid", mid); - values.put("app_pkg_name", scopePackageName); - values.put("user_id", userId); - db.insertWithOnConflict("scope", null, values, SQLiteDatabase.CONFLICT_IGNORE); - }); - // Called by xposed service, should be async - updateCaches(false); - return true; - } - - public boolean removeModuleScope(String packageName, String scopePackageName, int userId) { - if (scopePackageName == null) return false; - int mid = getModuleId(packageName); - if (mid == -1) return false; - if (scopePackageName.equals("system") && userId != 0) return false; - executeInTransaction(() -> { - db.delete("scope", "mid = ? AND app_pkg_name = ? AND user_id = ?", new String[]{String.valueOf(mid), scopePackageName, String.valueOf(userId)}); - }); - // Called by xposed service, should be async - updateCaches(false); - return true; - } - - - public String[] enabledModules() { - return listModules("enabled"); - } - - public boolean removeModule(String packageName) { - if (removeModuleWithoutCache(packageName)) { - // called by oneway binder - // Called only when the application is completely uninstalled - // If it's a module we need to return as soon as possible to broadcast to the manager - // for updating the module status - updateCaches(false); - return true; - } - return false; - } - - private boolean removeModuleWithoutCache(String packageName) { - if (packageName.equals("lspd")) return false; - boolean res = executeInTransaction(() -> db.delete("modules", "module_pkg_name = ?", new String[]{packageName}) > 0); - try { - for (var user : UserService.getUsers()) { - removeModulePrefs(user.id, packageName); - } - } catch (Throwable e) { - Log.w(TAG, "remove module prefs for " + packageName); - } - return res; - } - - private boolean removeModuleScopeWithoutCache(Application module) { - if (module.packageName.equals("lspd")) return false; - int mid = getModuleId(module.packageName); - if (mid == -1) return false; - boolean res = executeInTransaction(() -> db.delete("scope", "mid = ? and user_id = ?", new String[]{String.valueOf(mid), String.valueOf(module.userId)}) > 0); - try { - removeModulePrefs(module.userId, module.packageName); - } catch (IOException e) { - Log.w(TAG, "removeModulePrefs", e); - } - return res; - } - - private boolean removeAppWithoutCache(Application app) { - return executeInTransaction(() -> db.delete("scope", "app_pkg_name = ? AND user_id=?", - new String[]{app.packageName, String.valueOf(app.userId)}) > 0); - } - - public boolean disableModule(String packageName) { - if (packageName.equals("lspd")) return false; - boolean changed = executeInTransaction(() -> { - ContentValues values = new ContentValues(); - values.put("enabled", 0); - return db.update("modules", values, "module_pkg_name = ?", new String[]{packageName}) > 0; - }); - if (changed) { - // called by manager, should be async - updateCaches(false); - return true; - } else { - return false; - } - } - - public boolean enableModule(String packageName) throws RemoteException { - if (packageName.equals("lspd")) return false; - PackageInfo pkgInfo = PackageService.getPackageInfoFromAllUsers(packageName, PackageService.MATCH_ALL_FLAGS).values().stream().findFirst().orElse(null); - if (pkgInfo == null || pkgInfo.applicationInfo == null) return false; - var modulePath = getModuleApkPath(pkgInfo.applicationInfo); - if (modulePath == null) return false; - boolean changed = updateModuleApkPath(packageName, modulePath, false); - changed = executeInTransaction(() -> { - ContentValues values = new ContentValues(); - values.put("enabled", 1); - return db.update("modules", values, "module_pkg_name = ?", new String[]{packageName}) > 0; - }) || changed; - if (changed) { - // Called by manager, should be async - updateCaches(false); - return true; - } else { - return false; - } - } - - public void updateCache() { - // Called by oneway binder - updateCaches(true); - } - - public void updateAppCache() { - // Called by oneway binder - cacheScopes(); - } - - public void setVerboseLog(boolean on) { - if (BuildConfig.DEBUG) return; - var logcatService = ServiceManager.getLogcatService(); - if (on) { - logcatService.startVerbose(); - } else { - logcatService.stopVerbose(); - } - updateModulePrefs("lspd", 0, "config", "enable_verbose_log", on); - verboseLog = on; - } - - public boolean verboseLog() { - return BuildConfig.DEBUG || verboseLog; - } - - public void setLogWatchdog(boolean on) { - var logcatService = ServiceManager.getLogcatService(); - if (on) { - logcatService.enableWatchdog(); - } else { - logcatService.disableWatchdog(); - } - updateModulePrefs("lspd", 0, "config", "enable_log_watchdog", on); - logWatchdog = on; - } - - public boolean isLogWatchdogEnabled() { - return logWatchdog; - } - - public void setDexObfuscate(boolean on) { - updateModulePrefs("lspd", 0, "config", "enable_dex_obfuscate", on); - } - - public boolean scopeRequestBlocked(String packageName) { - return scopeRequestBlocked.contains(packageName); - } - - public void blockScopeRequest(String packageName) { - var set = new HashSet<>(scopeRequestBlocked); - set.add(packageName); - updateModulePrefs("lspd", 0, "config", "scope_request_blocked", set); - scopeRequestBlocked = set; - } - - public void removeBlockedScopeRequest(String packageName) { - var set = new HashSet<>(scopeRequestBlocked); - set.remove(packageName); - updateModulePrefs("lspd", 0, "config", "scope_request_blocked", set); - scopeRequestBlocked = set; - } - - // this is for manager and should not use the cache result - boolean dexObfuscate() { - var bool = getModulePrefs("lspd", 0, "config").get("enable_dex_obfuscate"); - return bool == null || (boolean) bool; - } - - public boolean enableStatusNotification() { - Log.d(TAG, "show status notification = " + enableStatusNotification); - return enableStatusNotification; - } - - public void setEnableStatusNotification(boolean enable) { - updateModulePrefs("lspd", 0, "config", "enable_status_notification", enable); - enableStatusNotification = enable; - } - - public ParcelFileDescriptor getManagerApk() { - try { - return ConfigFileManager.getManagerApk(); - } catch (Throwable e) { - Log.e(TAG, "failed to open manager apk", e); - return null; - } - } - - public ParcelFileDescriptor getModulesLog() { - try { - var modulesLog = ServiceManager.getLogcatService().getModulesLog(); - if (modulesLog == null) return null; - return ParcelFileDescriptor.open(modulesLog, ParcelFileDescriptor.MODE_READ_ONLY); - } catch (IOException e) { - Log.e(TAG, Log.getStackTraceString(e)); - return null; - } - } - - public ParcelFileDescriptor getVerboseLog() { - try { - var verboseLog = ServiceManager.getLogcatService().getVerboseLog(); - if (verboseLog == null) return null; - return ParcelFileDescriptor.open(verboseLog, ParcelFileDescriptor.MODE_READ_ONLY); - } catch (FileNotFoundException e) { - Log.e(TAG, Log.getStackTraceString(e)); - return null; - } - } - - public boolean clearLogs(boolean verbose) { - ServiceManager.getLogcatService().refresh(verbose); - return true; - } - - public boolean isManager(int uid) { - return uid == managerUid; - } - - public boolean isManagerInstalled() { - return managerUid != -1; - } - - public String getPrefsPath(String packageName, int uid) { - int userId = uid / PER_USER_RANGE; - var path = miscPath.resolve("prefs" + (userId == 0 ? "" : String.valueOf(userId))).resolve(packageName); - var module = cachedModule.getOrDefault(packageName, null); - if (module != null && module.appId == uid % PER_USER_RANGE) { - try { - var perms = PosixFilePermissions.fromString("rwx--x--x"); - Files.createDirectories(path, PosixFilePermissions.asFileAttribute(perms)); - walkFileTree(path, p -> { - try { - Os.chown(p.toString(), uid, uid); - } catch (ErrnoException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - }); - } catch (IOException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - return path.toString(); - } - - // this is slow, avoid using it - public Module getModule(int uid) { - for (var module : cachedModule.values()) { - if (module.appId == uid % PER_USER_RANGE) return module; - } - return null; - } - - private void walkFileTree(Path rootDir, Consumer action) throws IOException { - if (Files.notExists(rootDir)) return; - Files.walkFileTree(rootDir, new SimpleFileVisitor<>() { - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { - action.accept(dir); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - action.accept(file); - return FileVisitResult.CONTINUE; - } - }); - } - - private void removeModulePrefs(int uid, String packageName) throws IOException { - if (packageName == null) return; - var path = Paths.get(getPrefsPath(packageName, uid)); - ConfigFileManager.deleteFolderIfExists(path); - } - - public List getDenyListPackages() { - List result = new ArrayList<>(); - if (!getApi().equals("Zygisk")) return result; - if (!ConfigFileManager.magiskDbPath.exists()) return result; - try (final SQLiteDatabase magiskDb = - SQLiteDatabase.openDatabase(ConfigFileManager.magiskDbPath, new SQLiteDatabase.OpenParams.Builder().addOpenFlags(SQLiteDatabase.OPEN_READONLY).build())) { - try (Cursor cursor = magiskDb.query("settings", new String[]{"value"}, "`key`=?", new String[]{"denylist"}, null, null, null)) { - if (!cursor.moveToNext()) return result; - int valueIndex = cursor.getColumnIndex("value"); - if (valueIndex >= 0 && cursor.getInt(valueIndex) == 0) return result; - } - try (Cursor cursor = magiskDb.query(true, "denylist", new String[]{"package_name"}, null, null, null, null, null, null, null)) { - if (cursor == null) return result; - int packageNameIdx = cursor.getColumnIndex("package_name"); - while (cursor.moveToNext()) { - result.add(cursor.getString(packageNameIdx)); - } - return result; - } - } catch (Throwable e) { - Log.e(TAG, "get denylist", e); - } - return result; - } - - public void setApi(String api) { - this.api = api; - } - - public String getApi() { - return api; - } - - public void exportScopes(ZipOutputStream os) throws IOException { - os.putNextEntry(new ZipEntry("scopes.txt")); - cachedScope.forEach((scope, modules) -> { - try { - os.write((scope.processName + "/" + scope.uid + "\n").getBytes(StandardCharsets.UTF_8)); - for (var module : modules) { - os.write(("\t" + module.packageName + "\n").getBytes(StandardCharsets.UTF_8)); - for (var cn : module.file.moduleClassNames) { - os.write(("\t\t" + cn + "\n").getBytes(StandardCharsets.UTF_8)); - } - for (var ln : module.file.moduleLibraryNames) { - os.write(("\t\t" + ln + "\n").getBytes(StandardCharsets.UTF_8)); - } - } - } catch (IOException e) { - Log.w(TAG, scope.processName, e); - } - }); - os.closeEntry(); - } - - SharedMemory getPreloadDex() { - return ConfigFileManager.getPreloadDex(dexObfuscate); - } - - public boolean getAutoInclude(String packageName) { - try (Cursor cursor = db.query("modules", new String[]{"auto_include"}, - "module_pkg_name = ? and auto_include = 1", new String[]{packageName}, null, null, null, null)) { - return cursor == null || cursor.moveToNext(); - } - } - - public boolean setAutoInclude(String packageName, boolean enable) { - boolean changed = executeInTransaction(() -> { - ContentValues values = new ContentValues(); - values.put("auto_include", enable ? 1 : 0); - return db.update("modules", values, "module_pkg_name = ?", new String[]{packageName}) > 0; - }); - return true; - } - - public String[] getAutoIncludeModules() { - return listModules("auto_include"); - } - - private String[] listModules(String column) { - try (Cursor cursor = db.query("modules", new String[]{"module_pkg_name"}, column + " = 1", null, null, null, null)) { - if (cursor == null) { - Log.e(TAG, "query " + column + " modules failed"); - return null; - } - int modulePkgNameIdx = cursor.getColumnIndex("module_pkg_name"); - HashSet result = new HashSet<>(); - while (cursor.moveToNext()) { - var pkgName = cursor.getString(modulePkgNameIdx); - if (pkgName.equals("lspd")) continue; - result.add(pkgName); - } - return result.toArray(new String[0]); - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/Dex2OatService.java b/daemon/src/main/java/org/lsposed/lspd/service/Dex2OatService.java deleted file mode 100644 index f4da23ebb..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/Dex2OatService.java +++ /dev/null @@ -1,285 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2022 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.ILSPManagerService.DEX2OAT_CRASHED; -import static org.lsposed.lspd.ILSPManagerService.DEX2OAT_MOUNT_FAILED; -import static org.lsposed.lspd.ILSPManagerService.DEX2OAT_OK; -import static org.lsposed.lspd.ILSPManagerService.DEX2OAT_SELINUX_PERMISSIVE; -import static org.lsposed.lspd.ILSPManagerService.DEX2OAT_SEPOLICY_INCORRECT; - -import android.net.LocalServerSocket; -import android.os.Build; -import android.os.FileObserver; -import android.os.Process; -import android.os.SELinux; -import android.system.ErrnoException; -import android.system.Os; -import android.system.OsConstants; -import android.util.Log; - -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; - -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.ArrayList; - -@RequiresApi(Build.VERSION_CODES.Q) -public class Dex2OatService implements Runnable { - private static final String TAG = "LSPosedDex2Oat"; - private static final String WRAPPER32 = "bin/dex2oat32"; - private static final String WRAPPER64 = "bin/dex2oat64"; - private static final String HOOKER32 = "bin/liboat_hook32.so"; - private static final String HOOKER64 = "bin/liboat_hook64.so"; - - private final String[] dex2oatArray = new String[6]; - private final FileDescriptor[] fdArray = new FileDescriptor[6]; - private final FileObserver selinuxObserver; - private int compatibility = DEX2OAT_OK; - - private void openDex2oat(int id, String path) { - try { - var fd = Os.open(path, OsConstants.O_RDONLY, 0); - dex2oatArray[id] = path; - fdArray[id] = fd; - } catch (ErrnoException ignored) { - } - } - - /** - * Checks the ELF header of the target file. - * If 32-bit -> Assigns to Index 0 (Release) or 1 (Debug). - * If 64-bit -> Assigns to Index 2 (Release) or 3 (Debug). - */ - private void checkAndAddDex2Oat(String path) { - if (path == null) - return; - File file = new File(path); - if (!file.exists()) - return; - - try (FileInputStream fis = new FileInputStream(file)) { - byte[] header = new byte[5]; - if (fis.read(header) != 5) - return; - - // 1. Verify ELF Magic: 0x7F 'E' 'L' 'F' - if (header[0] != 0x7F || header[1] != 'E' || header[2] != 'L' || header[3] != 'F') { - return; - } - - // 2. Check Architecture (header[4]): 1 = 32-bit, 2 = 64-bit - boolean is32Bit = (header[4] == 1); - boolean is64Bit = (header[4] == 2); - boolean isDebug = path.contains("dex2oatd"); - - int index = -1; - - if (is32Bit) { - index = isDebug ? 1 : 0; // Index 0/1 maps to r32/d32 in C++ - } else if (is64Bit) { - index = isDebug ? 3 : 2; // Index 2/3 maps to r64/d64 in C++ - } - - // 3. Assign to the detected slot - if (index != -1 && dex2oatArray[index] == null) { - dex2oatArray[index] = path; - try { - // Open the FD for the wrapper to use later - fdArray[index] = Os.open(path, OsConstants.O_RDONLY, 0); - Log.i(TAG, "Detected " + path + " as " + (is64Bit ? "64-bit" : "32-bit") + " -> Assigned Index " - + index); - } catch (ErrnoException e) { - Log.e(TAG, "Failed to open FD for " + path, e); - dex2oatArray[index] = null; - } - } - } catch (IOException e) { - // File not readable, skip - } - } - - public Dex2OatService() { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { - // Android 10: Check the standard path. - // Logic will detect if it is 32-bit and put it in Index 0. - checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oat"); - checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oatd"); - - // Check for explicit 64-bit paths (just in case) - checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oat64"); - checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oatd64"); - } else { - checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oat32"); - checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oatd32"); - checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oat64"); - checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oatd64"); - } - - openDex2oat(4, "/data/adb/modules/zygisk_vector/bin/liboat_hook32.so"); - openDex2oat(5, "/data/adb/modules/zygisk_vector/bin/liboat_hook64.so"); - - var enforce = Paths.get("/sys/fs/selinux/enforce"); - var policy = Paths.get("/sys/fs/selinux/policy"); - var list = new ArrayList(); - list.add(enforce.toFile()); - list.add(policy.toFile()); - selinuxObserver = new FileObserver(list, FileObserver.CLOSE_WRITE) { - @Override - public synchronized void onEvent(int i, @Nullable String s) { - Log.d(TAG, "SELinux status changed"); - if (compatibility == DEX2OAT_CRASHED) { - stopWatching(); - return; - } - - boolean enforcing = false; - try (var is = Files.newInputStream(enforce)) { - enforcing = is.read() == '1'; - } catch (IOException ignored) { - } - - if (!enforcing) { - if (compatibility == DEX2OAT_OK) doMount(false); - compatibility = DEX2OAT_SELINUX_PERMISSIVE; - } else if (SELinux.checkSELinuxAccess("u:r:untrusted_app:s0", - "u:object_r:dex2oat_exec:s0", "file", "execute") - || SELinux.checkSELinuxAccess("u:r:untrusted_app:s0", - "u:object_r:dex2oat_exec:s0", "file", "execute_no_trans")) { - if (compatibility == DEX2OAT_OK) doMount(false); - compatibility = DEX2OAT_SEPOLICY_INCORRECT; - } else if (compatibility != DEX2OAT_OK) { - doMount(true); - if (notMounted()) { - doMount(false); - compatibility = DEX2OAT_MOUNT_FAILED; - stopWatching(); - } else { - compatibility = DEX2OAT_OK; - } - } - } - - @Override - public void stopWatching() { - super.stopWatching(); - Log.w(TAG, "SELinux observer stopped"); - } - }; - } - - private boolean notMounted() { - for (int i = 0; i < dex2oatArray.length && i < 4; i++) { - var bin = dex2oatArray[i]; - if (bin == null) continue; - try { - var apex = Os.stat("/proc/1/root" + bin); - var wrapper = Os.stat(i < 2 ? WRAPPER32 : WRAPPER64); - if (apex.st_dev != wrapper.st_dev || apex.st_ino != wrapper.st_ino) { - Log.w(TAG, "Check mount failed for " + bin); - return true; - } - } catch (ErrnoException e) { - Log.e(TAG, "Check mount failed for " + bin, e); - return true; - } - } - Log.d(TAG, "Check mount succeeded"); - return false; - } - - private void doMount(boolean enabled) { - doMountNative(enabled, dex2oatArray[0], dex2oatArray[1], dex2oatArray[2], dex2oatArray[3]); - } - - public void start() { - if (notMounted()) { // Already mounted when restart daemon - doMount(true); - if (notMounted()) { - doMount(false); - compatibility = DEX2OAT_MOUNT_FAILED; - return; - } - } - - var thread = new Thread(this); - thread.setName("dex2oat"); - thread.start(); - selinuxObserver.startWatching(); - selinuxObserver.onEvent(0, null); - } - - @Override - public void run() { - Log.i(TAG, "Dex2oat wrapper daemon start"); - var sockPath = getSockPath(); - Log.d(TAG, "wrapper path: " + sockPath); - var xposed_file = "u:object_r:xposed_file:s0"; - var dex2oat_exec = "u:object_r:dex2oat_exec:s0"; - if (SELinux.checkSELinuxAccess("u:r:dex2oat:s0", dex2oat_exec, - "file", "execute_no_trans")) { - SELinux.setFileContext(WRAPPER32, dex2oat_exec); - SELinux.setFileContext(WRAPPER64, dex2oat_exec); - setSockCreateContext("u:r:dex2oat:s0"); - } else { - SELinux.setFileContext(WRAPPER32, xposed_file); - SELinux.setFileContext(WRAPPER64, xposed_file); - setSockCreateContext("u:r:installd:s0"); - } - SELinux.setFileContext(HOOKER32, xposed_file); - SELinux.setFileContext(HOOKER64, xposed_file); - try (var server = new LocalServerSocket(sockPath)) { - setSockCreateContext(null); - while (true) { - try (var client = server.accept(); - var is = client.getInputStream(); - var os = client.getOutputStream()) { - var id = is.read(); - var fd = new FileDescriptor[]{fdArray[id]}; - client.setFileDescriptorsForSend(fd); - os.write(1); - Log.d(TAG, "Sent fd of " + dex2oatArray[id]); - } - } - } catch (IOException e) { - Log.e(TAG, "Dex2oat wrapper daemon crashed", e); - if (compatibility == DEX2OAT_OK) { - doMount(false); - compatibility = DEX2OAT_CRASHED; - } - } - } - - public int getCompatibility() { - return compatibility; - } - - private native void doMountNative(boolean enabled, - String r32, String d32, String r64, String d64); - - private static native boolean setSockCreateContext(String context); - - private native String getSockPath(); -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java deleted file mode 100644 index f5cd46555..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.os.IBinder; -import android.os.Parcel; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.os.RemoteException; -import android.util.Log; -import android.util.Pair; - -import androidx.annotation.NonNull; - -import org.lsposed.lspd.models.Module; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -public class LSPApplicationService extends ILSPApplicationService.Stub { - final static int DEX_TRANSACTION_CODE = ('_' << 24) | ('D' << 16) | ('E' << 8) | 'X'; - final static int OBFUSCATION_MAP_TRANSACTION_CODE = ('_' << 24) | ('O' << 16) | ('B' << 8) | 'F'; - // key: - private final static Map, ProcessInfo> processes = new ConcurrentHashMap<>(); - - static class ProcessInfo implements DeathRecipient { - final int uid; - final int pid; - final String processName; - final IBinder heartBeat; - - ProcessInfo(int uid, int pid, String processName, IBinder heartBeat) throws RemoteException { - this.uid = uid; - this.pid = pid; - this.processName = processName; - this.heartBeat = heartBeat; - heartBeat.linkToDeath(this, 0); - Log.d(TAG, "register " + this); - processes.put(new Pair<>(uid, pid), this); - } - - @Override - public void binderDied() { - Log.d(TAG, this + " is dead"); - heartBeat.unlinkToDeath(this, 0); - processes.remove(new Pair<>(uid, pid), this); - } - - @NonNull - @Override - public String toString() { - return "ProcessInfo{" + - "uid=" + uid + - ", pid=" + pid + - ", processName='" + processName + '\'' + - ", heartBeat=" + heartBeat + - '}'; - } - } - - @Override - public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { - Log.d(TAG, "LSPApplicationService.onTransact: code=" + code); - switch (code) { - case DEX_TRANSACTION_CODE: { - var shm = ConfigManager.getInstance().getPreloadDex(); - if (shm == null) return false; - reply.writeNoException(); - // assume that write only a fd - shm.writeToParcel(reply, 0); - reply.writeLong(shm.getSize()); - return true; - } - case OBFUSCATION_MAP_TRANSACTION_CODE: { - var obfuscation = ConfigManager.getInstance().dexObfuscate(); - var signatures = ObfuscationManager.getSignatures(); - reply.writeNoException(); - reply.writeInt(signatures.size() * 2); - for (Map.Entry entry : signatures.entrySet()) { - reply.writeString(entry.getKey()); - // return val = key if obfuscation disabled - reply.writeString(obfuscation ? entry.getValue() : entry.getKey()); - } - return true; - } - } - return super.onTransact(code, data, reply, flags); - } - - public boolean registerHeartBeat(int uid, int pid, String processName, IBinder heartBeat) { - try { - new ProcessInfo(uid, pid, processName, heartBeat); - return true; - } catch (RemoteException e) { - return false; - } - } - - private List getAllModulesList() throws RemoteException { - var processInfo = ensureRegistered(); - if (processInfo.uid == Process.SYSTEM_UID && processInfo.processName.equals("system")) { - return ConfigManager.getInstance().getModulesForSystemServer(); - } - if (ServiceManager.getManagerService().isRunningManager(processInfo.pid, processInfo.uid)) - return Collections.emptyList(); - return ConfigManager.getInstance().getModulesForProcess(processInfo.processName, processInfo.uid); - } - - @Override - public boolean isLogMuted() throws RemoteException { - return !ServiceManager.getManagerService().isVerboseLog(); - } - - @Override - public List getLegacyModulesList() throws RemoteException { - return getAllModulesList().stream().filter(m -> m.file.legacy).collect(Collectors.toList()); - } - - @Override - public List getModulesList() throws RemoteException { - return getAllModulesList().stream().filter(m -> !m.file.legacy).collect(Collectors.toList()); - } - - @Override - public String getPrefsPath(String packageName) throws RemoteException { - ensureRegistered(); - return ConfigManager.getInstance().getPrefsPath(packageName, getCallingUid()); - } - - @Override - public ParcelFileDescriptor requestInjectedManagerBinder(List binder) throws RemoteException { - var processInfo = ensureRegistered(); - if (ServiceManager.getManagerService().postStartManager(processInfo.pid, processInfo.uid) || - ConfigManager.getInstance().isManager(processInfo.uid)) { - binder.add(ServiceManager.getManagerService().obtainManagerBinder(processInfo.heartBeat, processInfo.pid, processInfo.uid)); - } - return ConfigManager.getInstance().getManagerApk(); - } - - public boolean hasRegister(int uid, int pid) { - return processes.containsKey(new Pair<>(uid, pid)); - } - - @NonNull - private ProcessInfo ensureRegistered() throws RemoteException { - var uid = getCallingUid(); - var pid = getCallingPid(); - var key = new Pair<>(uid, pid); - ProcessInfo processInfo = processes.getOrDefault(key, null); - if (processInfo == null || uid != processInfo.uid || pid != processInfo.pid) { - processes.remove(key, processInfo); - Log.w(TAG, "non-authorized: info=" + processInfo + " uid=" + uid + " pid=" + pid); - throw new RemoteException("Not registered"); - } - return processInfo; - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java deleted file mode 100644 index fa402923c..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.LSPModuleService.FILES_DIR; -import static org.lsposed.lspd.service.PackageService.PER_USER_RANGE; - -import android.os.Binder; -import android.os.Bundle; -import android.os.ParcelFileDescriptor; -import android.os.RemoteException; -import android.util.Log; - -import org.lsposed.lspd.models.Module; - -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import io.github.libxposed.service.IXposedService; - -public class LSPInjectedModuleService extends ILSPInjectedModuleService.Stub { - - private static final String TAG = "LSPosedInjectedModuleService"; - - private final String mPackageName; - - Map> callbacks = new ConcurrentHashMap<>(); - - LSPInjectedModuleService(String packageName) { - mPackageName = packageName; - } - - @Override - public long getFrameworkProperties() { - var prop = IXposedService.PROP_CAP_SYSTEM | IXposedService.PROP_CAP_REMOTE; - if (ConfigManager.getInstance().dexObfuscate()) { - prop = prop | IXposedService.PROP_RT_API_PROTECTION; - } - return prop; - } - - @Override - public Bundle requestRemotePreferences(String group, IRemotePreferenceCallback callback) { - var bundle = new Bundle(); - var userId = Binder.getCallingUid() / PER_USER_RANGE; - bundle.putSerializable("map", ConfigManager.getInstance().getModulePrefs(mPackageName, userId, group)); - if (callback != null) { - var groupCallbacks = callbacks.computeIfAbsent(group, k -> ConcurrentHashMap.newKeySet()); - groupCallbacks.add(callback); - try { - callback.asBinder().linkToDeath(() -> groupCallbacks.remove(callback), 0); - } catch (RemoteException e) { - Log.w(TAG, "requestRemotePreferences: ", e); - } - } - return bundle; - } - - @Override - public ParcelFileDescriptor openRemoteFile(String path) throws RemoteException { - ConfigFileManager.ensureModuleFilePath(path); - var userId = Binder.getCallingUid() / PER_USER_RANGE; - try { - var dir = ConfigFileManager.resolveModuleDir(mPackageName, FILES_DIR, userId, -1); - return ParcelFileDescriptor.open(dir.resolve(path).toFile(), ParcelFileDescriptor.MODE_READ_ONLY); - } catch (Throwable e) { - throw new RemoteException(e.getMessage()); - } - } - - @Override - public String[] getRemoteFileList() throws RemoteException { - var userId = Binder.getCallingUid() / PER_USER_RANGE; - try { - var dir = ConfigFileManager.resolveModuleDir(mPackageName, FILES_DIR, userId, -1); - var files = dir.toFile().list(); - return files == null ? new String[0] : files; - - } catch (Throwable e) { - throw new RemoteException(e.getMessage()); - } - } - - void onUpdateRemotePreferences(String group, Bundle diff) { - var groupCallbacks = callbacks.get(group); - if (groupCallbacks != null) { - for (var callback : groupCallbacks) { - try { - callback.onUpdate(diff); - } catch (RemoteException e) { - groupCallbacks.remove(callback); - } - } - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java deleted file mode 100644 index 1f76154b9..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java +++ /dev/null @@ -1,569 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static android.content.Context.BIND_AUTO_CREATE; -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.annotation.SuppressLint; -import android.app.IServiceConnection; -import android.content.AttributionSource; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.VersionedPackage; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.ParcelFileDescriptor; -import android.os.RemoteException; -import android.os.SELinux; -import android.os.SystemProperties; -import android.system.ErrnoException; -import android.system.Os; -import android.util.Log; -import android.view.IWindowManager; - -import androidx.annotation.NonNull; - -import org.matrix.vector.daemon.BuildConfig; -import org.lsposed.lspd.ILSPManagerService; -import org.lsposed.lspd.models.Application; -import org.lsposed.lspd.models.UserInfo; -import org.lsposed.lspd.util.Utils; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import hidden.HiddenApiBridge; -import io.github.libxposed.service.IXposedService; -import rikka.parcelablelist.ParcelableListSlice; - -public class LSPManagerService extends ILSPManagerService.Stub { - - private static Intent managerIntent = null; - private boolean enabled = true; - - public class ManagerGuard implements IBinder.DeathRecipient { - private final @NonNull - IBinder binder; - private final int pid; - private final int uid; - private final IServiceConnection connection = new IServiceConnection.Stub() { - @Override - public void connected(ComponentName name, IBinder service, boolean dead) { - } - }; - - public ManagerGuard(@NonNull IBinder binder, int pid, int uid) { - guard = this; - this.pid = pid; - this.uid = uid; - this.binder = binder; - try { - this.binder.linkToDeath(this, 0); - if (Utils.isMIUI) { - var intent = new Intent(); - intent.setComponent(ComponentName.unflattenFromString("com.miui.securitycore/com.miui.xspace.service.XSpaceService")); - ActivityManagerService.bindService(intent, intent.getType(), connection, BIND_AUTO_CREATE, "android", 0); - } - } catch (Throwable e) { - Log.e(TAG, "manager guard", e); - guard = null; - } - } - - @Override - public void binderDied() { - try { - binder.unlinkToDeath(this, 0); - ActivityManagerService.unbindService(connection); - } catch (Throwable e) { - Log.e(TAG, "manager guard", e); - } - guard = null; - } - - boolean isAlive() { - return binder.isBinderAlive(); - } - } - - public ManagerGuard guard = null; - - // guard to determine the manager or the injected app - // that is to say, to make the parasitic success, - // we should make sure no extra launch after parasitic - // launch is queued and before the process is started - private boolean pendingManager = false; - private int managerPid = -1; - - LSPManagerService() { - } - - private static Intent getManagerIntent() { - if (managerIntent != null) return managerIntent; - try { - var intent = PackageService.getLaunchIntentForPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME); - if (intent == null) { - var pkgInfo = PackageService.getPackageInfo(BuildConfig.MANAGER_INJECTED_PKG_NAME, PackageManager.GET_ACTIVITIES, 0); - if (pkgInfo != null && pkgInfo.activities != null && pkgInfo.activities.length > 0) { - for (var activityInfo : pkgInfo.activities) { - if (activityInfo.processName.equals(activityInfo.packageName)) { - intent = new Intent(); - intent.setComponent(new ComponentName(activityInfo.packageName, activityInfo.name)); - intent.setAction(Intent.ACTION_MAIN); - break; - } - } - } - } - if (intent != null) { - if (intent.getCategories() != null) intent.getCategories().clear(); - intent.addCategory("org.lsposed.manager.LAUNCH_MANAGER"); - intent.setPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME); - managerIntent = new Intent(intent); - } - } catch (RemoteException e) { - Log.e(TAG, "get Intent", e); - } - return managerIntent; - } - - static void openManager(Uri withData) { - var intent = getManagerIntent(); - if (intent == null) return; - intent = new Intent(intent); - intent.setData(withData); - try { - ActivityManagerService.startActivityAsUserWithFeature("android", null, intent, intent.getType(), null, null, 0, 0, null, null, 0); - } catch (RemoteException e) { - Log.e(TAG, "failed to open manager"); - } - } - - @SuppressLint("WrongConstant") - public static void broadcastIntent(Intent inIntent) { - var intent = new Intent("org.lsposed.manager.NOTIFICATION"); - intent.putExtra(Intent.EXTRA_INTENT, inIntent); - intent.addFlags(0x01000000); //Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND - intent.addFlags(0x00400000); //Intent.FLAG_RECEIVER_FROM_SHELL - intent.setPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME); - try { - ActivityManagerService.broadcastIntentWithFeature(null, intent, - null, null, 0, null, null, - null, -1, null, true, false, - 0); - intent.setPackage(BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME); - ActivityManagerService.broadcastIntentWithFeature(null, intent, - null, null, 0, null, null, - null, -1, null, true, false, - 0); - } catch (RemoteException t) { - Log.e(TAG, "Broadcast to manager failed: ", t); - } - } - - private void ensureWebViewPermission(File f) { - if (!f.exists()) return; - SELinux.setFileContext(f.getAbsolutePath(), "u:object_r:xposed_file:s0"); - try { - Os.chown(f.getAbsolutePath(), BuildConfig.MANAGER_INJECTED_UID, BuildConfig.MANAGER_INJECTED_UID); - } catch (ErrnoException e) { - Log.e(TAG, "chown of webview", e); - } - if (f.isDirectory()) { - for (var g : f.listFiles()) { - ensureWebViewPermission(g); - } - } - } - - private void ensureWebViewPermission() { - try { - var pkgInfo = PackageService.getPackageInfo(BuildConfig.MANAGER_INJECTED_PKG_NAME, 0, 0); - if (pkgInfo != null) { - var cacheDir = new File(HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(pkgInfo.applicationInfo) + "/cache"); - // The cache directory does not exist after `pm clear` - cacheDir.mkdirs(); - ensureWebViewPermission(cacheDir); - } - } catch (Throwable e) { - Log.w(TAG, "cannot ensure webview dir", e); - } - } - - synchronized boolean preStartManager() { - pendingManager = true; - managerPid = -1; - return true; - } - - // return true to inject manager - synchronized boolean shouldStartManager(int pid, int uid, String processName) { - if (!enabled || uid != BuildConfig.MANAGER_INJECTED_UID || !BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME.equals(processName) || !pendingManager) - return false; - pendingManager = false; - managerPid = pid; - Log.d(TAG, "starting injected manager: pid = " + pid + " uid = " + uid + " processName = " + processName); - return true; - } - - synchronized boolean setEnabled(boolean newValue) { - enabled = newValue; - Log.i(TAG, "manager enabled = " + enabled); - return enabled; - } - - // return true to send manager binder - boolean postStartManager(int pid, int uid) { - return enabled && uid == BuildConfig.MANAGER_INJECTED_UID && pid == managerPid; - } - - public @NonNull - IBinder obtainManagerBinder(@NonNull IBinder heartbeat, int pid, int uid) { - new ManagerGuard(heartbeat, pid, uid); - if (uid == BuildConfig.MANAGER_INJECTED_UID) - ensureWebViewPermission(); - return this; - } - - public boolean isRunningManager(int pid, int uid) { - return false; - } - - void onSystemServerDied() { - guard = null; - } - - @Override - public IBinder asBinder() { - return this; - } - - @Override - public int getXposedApiVersion() { - return IXposedService.LIB_API; - } - - @Override - public long getXposedVersionCode() { - return BuildConfig.VERSION_CODE; - } - - @Override - public String getXposedVersionName() { - return BuildConfig.VERSION_NAME; - } - - @Override - public String getApi() { - return ConfigManager.getInstance().getApi(); - } - - @Override - public ParcelableListSlice getInstalledPackagesFromAllUsers(int flags, boolean filterNoProcess) throws RemoteException { - return PackageService.getInstalledPackagesFromAllUsers(flags, filterNoProcess); - } - - @Override - public String[] enabledModules() { - return ConfigManager.getInstance().enabledModules(); - } - - @Override - public boolean enableModule(String packageName) throws RemoteException { - return ConfigManager.getInstance().enableModule(packageName); - } - - @Override - public boolean setModuleScope(String packageName, List scope) throws RemoteException { - return ConfigManager.getInstance().setModuleScope(packageName, scope); - } - - @Override - public List getModuleScope(String packageName) { - return ConfigManager.getInstance().getModuleScope(packageName); - } - - @Override - public boolean disableModule(String packageName) { - return ConfigManager.getInstance().disableModule(packageName); - } - - @Override - public boolean isVerboseLog() { - return ConfigManager.getInstance().verboseLog(); - } - - @Override - public void setVerboseLog(boolean enabled) { - ConfigManager.getInstance().setVerboseLog(enabled); - } - - @Override - public ParcelFileDescriptor getVerboseLog() { - return ConfigManager.getInstance().getVerboseLog(); - } - - @Override - public ParcelFileDescriptor getModulesLog() { - ServiceManager.getLogcatService().checkLogFile(); - return ConfigManager.getInstance().getModulesLog(); - } - - @Override - public boolean clearLogs(boolean verbose) { - return ConfigManager.getInstance().clearLogs(verbose); - } - - @Override - public PackageInfo getPackageInfo(String packageName, int flags, int uid) throws RemoteException { - return PackageService.getPackageInfo(packageName, flags, uid); - } - - @Override - public void forceStopPackage(String packageName, int userId) throws RemoteException { - ActivityManagerService.forceStopPackage(packageName, userId); - } - - @Override - public void reboot() throws RemoteException { - PowerService.reboot(false, null, false); - } - - @Override - public boolean uninstallPackage(String packageName, int userId) throws RemoteException { - try { - if (ActivityManagerService.startUserInBackground(userId)) { - var pkg = new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST); - return PackageService.uninstallPackage(pkg, userId); - } else { - return false; - } - } catch (InterruptedException | ReflectiveOperationException e) { - Log.e(TAG, e.getMessage(), e); - return false; - } - } - - @Override - public boolean isSepolicyLoaded() { - return SELinux.checkSELinuxAccess("u:r:dex2oat:s0", "u:object_r:dex2oat_exec:s0", - "file", "execute_no_trans"); - } - - @Override - public List getUsers() throws RemoteException { - var users = new LinkedList(); - for (var user : UserService.getUsers()) { - var info = new UserInfo(); - info.id = user.id; - info.name = user.name; - users.add(info); - } - return users; - } - - @Override - public int installExistingPackageAsUser(String packageName, int userId) { - try { - if (ActivityManagerService.startUserInBackground(userId)) - return PackageService.installExistingPackageAsUser(packageName, userId); - else return PackageService.INSTALL_FAILED_INTERNAL_ERROR; - } catch (Throwable e) { - Log.w(TAG, "install existing package as user: ", e); - return PackageService.INSTALL_FAILED_INTERNAL_ERROR; - } - } - - @Override - public boolean systemServerRequested() { - return ServiceManager.systemServerRequested(); - } - - @Override - public int startActivityAsUserWithFeature(Intent intent, int userId) throws RemoteException { - if (!intent.getBooleanExtra("lsp_no_switch_to_user", false)) { - intent.removeExtra("lsp_no_switch_to_user"); - var currentUser = ActivityManagerService.getCurrentUser(); - if (currentUser == null) return -1; - var parent = UserService.getProfileParent(userId); - if (parent < 0) return -1; - if (currentUser.id != parent) { - if (!ActivityManagerService.switchUser(parent)) return -1; - var window = android.os.ServiceManager.getService(Context.WINDOW_SERVICE); - if (window != null) { - var wm = IWindowManager.Stub.asInterface(window); - wm.lockNow(null); - } - } - } - return ActivityManagerService.startActivityAsUserWithFeature("android", null, intent, intent.getType(), null, null, 0, 0, null, null, userId); - } - - @Override - public ParcelableListSlice queryIntentActivitiesAsUser(Intent intent, int flags, int userId) throws RemoteException { - return PackageService.queryIntentActivities(intent, intent.getType(), flags, userId); - } - - @Override - public boolean dex2oatFlagsLoaded() { - return SystemProperties.get("dalvik.vm.dex2oat-flags").contains("--inline-max-code-units=0"); - } - - @Override - public void setHiddenIcon(boolean hide) { - Bundle args = new Bundle(); - args.putString("value", hide ? "0" : "1"); - args.putString("_user", "0"); - try { - var contentProvider = ActivityManagerService.getContentProvider("settings", 0); - if (contentProvider != null) { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - contentProvider.call(new AttributionSource.Builder(1000).setPackageName("android").build(), - "settings", "PUT_global", "show_hidden_icon_apps_enabled", args); - } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - contentProvider.call("android", null, "settings", "PUT_global", "show_hidden_icon_apps_enabled", args); - } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { - contentProvider.call("android", "settings", "PUT_global", "show_hidden_icon_apps_enabled", args); - } - } catch (NoSuchMethodError e) { - Log.w(TAG, "setHiddenIcon: ", e); - } - } - } catch (Throwable e) { - Log.w(TAG, "setHiddenIcon: ", e); - } - } - - @Override - public void getLogs(ParcelFileDescriptor zipFd) { - ConfigFileManager.getLogs(zipFd); - } - - @Override - public void restartFor(Intent intent) throws RemoteException { - } - - @Override - public List getDenyListPackages() { - return ConfigManager.getInstance().getDenyListPackages(); - } - - @Override - public void flashZip(String zipPath, ParcelFileDescriptor outputStream) { - var processBuilder = new ProcessBuilder("magisk", "--install-module", zipPath); - var fd = new File("/proc/self/fd/" + outputStream.getFd()); - processBuilder.redirectOutput(ProcessBuilder.Redirect.appendTo(fd)); - try (outputStream; var fdw = new FileOutputStream(fd, true)) { - var proc = processBuilder.start(); - if (proc.waitFor(10, TimeUnit.SECONDS)) { - var exit = proc.exitValue(); - if (exit == 0) { - fdw.write("- Reboot after 5s\n".getBytes()); - Thread.sleep(5000); - reboot(); - } else { - var s = "! Flash failed, exit with " + exit + "\n"; - fdw.write(s.getBytes()); - } - } else { - proc.destroy(); - fdw.write("! Timeout, abort\n".getBytes()); - } - } catch (IOException | InterruptedException | RemoteException e) { - Log.e(TAG, "flashZip: ", e); - } - } - - @Override - public void clearApplicationProfileData(String packageName) throws RemoteException { - PackageService.clearApplicationProfileData(packageName); - } - - @Override - public boolean enableStatusNotification() { - return ConfigManager.getInstance().enableStatusNotification(); - } - - @Override - public void setEnableStatusNotification(boolean enable) { - ConfigManager.getInstance().setEnableStatusNotification(enable); - if (enable) { - LSPNotificationManager.notifyStatusNotification(); - } else { - LSPNotificationManager.cancelStatusNotification(); - } - } - - @Override - public boolean performDexOptMode(String packageName) throws RemoteException { - return PackageService.performDexOptMode(packageName); - } - - @Override - public boolean getDexObfuscate() { - return ConfigManager.getInstance().dexObfuscate(); - } - - @Override - public void setDexObfuscate(boolean enabled) { - ConfigManager.getInstance().setDexObfuscate(enabled); - } - - @Override - public int getDex2OatWrapperCompatibility() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return ServiceManager.getDex2OatService().getCompatibility(); - } else { - return 0; - } - } - - @Override - public void setLogWatchdog(boolean enabled) { - ConfigManager.getInstance().setLogWatchdog(enabled); - } - - @Override - public boolean isLogWatchdogEnabled() { - return ConfigManager.getInstance().isLogWatchdogEnabled(); - } - - @Override - public boolean setAutoInclude(String packageName, boolean enabled) { - return ConfigManager.getInstance().setAutoInclude(packageName, enabled); - } - - @Override - public boolean getAutoInclude(String packageName) { - return ConfigManager.getInstance().getAutoInclude(packageName); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java deleted file mode 100644 index f5980139d..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.PackageService.PER_USER_RANGE; - -import android.content.AttributionSource; -import android.os.Binder; -import android.os.Build; -import android.os.Bundle; -import android.os.ParcelFileDescriptor; -import android.os.RemoteException; -import android.util.ArrayMap; -import android.util.Log; - -import androidx.annotation.NonNull; - -import org.matrix.vector.daemon.BuildConfig; -import org.lsposed.lspd.models.Module; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.concurrent.ConcurrentHashMap; - -import io.github.libxposed.service.IXposedScopeCallback; -import io.github.libxposed.service.IXposedService; - -public class LSPModuleService extends IXposedService.Stub { - - private final static String TAG = "LSPosedModuleService"; - - private final static Set uidSet = ConcurrentHashMap.newKeySet(); - private final static Map serviceMap = Collections.synchronizedMap(new WeakHashMap<>()); - - public final static String FILES_DIR = "files"; - - private final @NonNull - Module loadedModule; - - static void uidClear() { - uidSet.clear(); - } - - static void uidStarts(int uid) { - if (!uidSet.contains(uid)) { - uidSet.add(uid); - var module = ConfigManager.getInstance().getModule(uid); - if (module != null && module.file != null && !module.file.legacy) { - var service = serviceMap.computeIfAbsent(module, LSPModuleService::new); - service.sendBinder(uid); - } - } - } - - static void uidGone(int uid) { - uidSet.remove(uid); - } - - private void sendBinder(int uid) { - var name = loadedModule.packageName; - try { - int userId = uid / PackageService.PER_USER_RANGE; - var authority = name + AUTHORITY_SUFFIX; - var provider = ActivityManagerService.getContentProvider(authority, userId); - if (provider == null) { - Log.d(TAG, "no service provider for " + name); - return; - } - var extra = new Bundle(); - extra.putBinder("binder", asBinder()); - Bundle reply = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - reply = provider.call(new AttributionSource.Builder(1000).setPackageName("android").build(), authority, SEND_BINDER, null, extra); - } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - reply = provider.call("android", null, authority, SEND_BINDER, null, extra); - } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { - reply = provider.call("android", authority, SEND_BINDER, null, extra); - } else { - reply = provider.call("android", SEND_BINDER, null, extra); - } - if (reply != null) { - Log.d(TAG, "sent module binder to " + name); - } else { - Log.w(TAG, "failed to send module binder to " + name); - } - } catch (Throwable e) { - Log.w(TAG, "failed to send module binder for uid " + uid, e); - } - } - - LSPModuleService(@NonNull Module module) { - loadedModule = module; - } - - private int ensureModule() throws RemoteException { - var appId = Binder.getCallingUid() % PER_USER_RANGE; - if (loadedModule.appId != appId) { - throw new RemoteException("Module " + loadedModule.packageName + " is not for uid " + Binder.getCallingUid()); - } - return Binder.getCallingUid() / PER_USER_RANGE; - } - - @Override - public int getApiVersion() throws RemoteException { - ensureModule(); - return IXposedService.LIB_API; - } - - @Override - public String getFrameworkName() throws RemoteException { - ensureModule(); - return BuildConfig.FRAMEWORK_NAME; - } - - @Override - public String getFrameworkVersion() throws RemoteException { - ensureModule(); - return BuildConfig.VERSION_NAME; - } - - @Override - public long getFrameworkVersionCode() throws RemoteException { - ensureModule(); - return BuildConfig.VERSION_CODE; - } - - @Override - public long getFrameworkProperties() throws RemoteException { - ensureModule(); - var prop = IXposedService.PROP_CAP_SYSTEM | IXposedService.PROP_CAP_REMOTE; - if (ConfigManager.getInstance().dexObfuscate()) { - prop = prop | IXposedService.PROP_RT_API_PROTECTION; - } - return prop; - } - - @Override - public List getScope() throws RemoteException { - ensureModule(); - ArrayList res = new ArrayList<>(); - var scope = ConfigManager.getInstance().getModuleScope(loadedModule.packageName); - if (scope == null) return res; - for (var s : scope) { - res.add(s.packageName); - } - return res; - } - - @Override - public void requestScope(List packages, IXposedScopeCallback callback) throws RemoteException { - var userId = ensureModule(); - if (!ConfigManager.getInstance().scopeRequestBlocked(loadedModule.packageName)) { - for (String packageName : packages) { - LSPNotificationManager.requestModuleScope(loadedModule.packageName, userId, packageName, callback); - } - } else { - callback.onScopeRequestFailed("Scope request blocked by user configuration"); - } - } - - @Override - public void removeScope(List packages) throws RemoteException { - var userId = ensureModule(); - for (String packageName : packages) { - try { - if (!ConfigManager.getInstance().removeModuleScope(loadedModule.packageName, packageName, userId)) { - Log.w(TAG, "Failed to remove scope: " + packageName + " (Invalid request)"); - } - } catch (Throwable e) { - Log.e(TAG, "Error removing scope for " + packageName, e); - } - } - } - - @Override - public Bundle requestRemotePreferences(String group) throws RemoteException { - var userId = ensureModule(); - var bundle = new Bundle(); - bundle.putSerializable("map", ConfigManager.getInstance().getModulePrefs(loadedModule.packageName, userId, group)); - return bundle; - } - - @Override - public void updateRemotePreferences(String group, Bundle diff) throws RemoteException { - var userId = ensureModule(); - Map values = new ArrayMap<>(); - if (diff.containsKey("delete")) { - var deletes = (Set) diff.getSerializable("delete"); - for (var key : deletes) { - values.put((String) key, null); - } - } - if (diff.containsKey("put")) { - try { - var puts = (Map) diff.getSerializable("put"); - for (var entry : puts.entrySet()) { - values.put((String) entry.getKey(), entry.getValue()); - } - } catch (Throwable e) { - Log.e(TAG, "updateRemotePreferences: ", e); - } - } - try { - ConfigManager.getInstance().updateModulePrefs(loadedModule.packageName, userId, group, values); - ((LSPInjectedModuleService) loadedModule.service).onUpdateRemotePreferences(group, diff); - } catch (Throwable e) { - throw new RemoteException(e.getMessage()); - } - } - - @Override - public void deleteRemotePreferences(String group) throws RemoteException { - var userId = ensureModule(); - ConfigManager.getInstance().deleteModulePrefs(loadedModule.packageName, userId, group); - } - - @Override - public String[] listRemoteFiles() throws RemoteException { - var userId = ensureModule(); - try { - var dir = ConfigFileManager.resolveModuleDir(loadedModule.packageName, FILES_DIR, userId, Binder.getCallingUid()); - var files = dir.toFile().list(); - return files == null ? new String[0] : files; - } catch (IOException e) { - throw new RemoteException(e.getMessage()); - } - } - - @Override - public ParcelFileDescriptor openRemoteFile(String path) throws RemoteException { - var userId = ensureModule(); - ConfigFileManager.ensureModuleFilePath(path); - try { - var dir = ConfigFileManager.resolveModuleDir(loadedModule.packageName, FILES_DIR, userId, Binder.getCallingUid()); - return ParcelFileDescriptor.open(dir.resolve(path).toFile(), ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE); - } catch (IOException e) { - throw new RemoteException(e.getMessage()); - } - } - - @Override - public boolean deleteRemoteFile(String path) throws RemoteException { - var userId = ensureModule(); - ConfigFileManager.ensureModuleFilePath(path); - try { - var dir = ConfigFileManager.resolveModuleDir(loadedModule.packageName, FILES_DIR, userId, Binder.getCallingUid()); - return dir.resolve(path).toFile().delete(); - } catch (IOException e) { - throw new RemoteException(e.getMessage()); - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java deleted file mode 100644 index c422ffe70..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java +++ /dev/null @@ -1,332 +0,0 @@ -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.app.INotificationManager; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ParceledListSlice; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.AdaptiveIconDrawable; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.graphics.drawable.LayerDrawable; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.util.Log; - -import org.matrix.vector.daemon.BuildConfig; -import org.matrix.vector.daemon.R; -import org.lsposed.lspd.util.FakeContext; - -import java.util.ArrayList; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -import io.github.libxposed.service.IXposedScopeCallback; - -public class LSPNotificationManager { - static final String UPDATED_CHANNEL_ID = "lsposed_module_updated"; - static final String SCOPE_CHANNEL_ID = "lsposed_module_scope"; - private static final String STATUS_CHANNEL_ID = "lsposed_status"; - private static final int STATUS_NOTIFICATION_ID = 2000; - private static final String opPkg = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? - "android" : "com.android.settings"; - - private static final Map notificationIds = new ConcurrentHashMap<>(); - private static int previousNotificationId = STATUS_NOTIFICATION_ID; - - static final String openManagerAction = UUID.randomUUID().toString(); - static final String moduleScope = UUID.randomUUID().toString(); - - private static INotificationManager notificationManager = null; - private static IBinder binder = null; - - private static final IBinder.DeathRecipient recipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - Log.w(TAG, "notificationManager is dead"); - binder.unlinkToDeath(this, 0); - binder = null; - notificationManager = null; - } - }; - - private static INotificationManager getNotificationManager() throws RemoteException { - if (binder == null || notificationManager == null) { - binder = android.os.ServiceManager.getService(Context.NOTIFICATION_SERVICE); - binder.linkToDeath(recipient, 0); - notificationManager = INotificationManager.Stub.asInterface(binder); - } - return notificationManager; - } - - private static Bitmap getBitmap(int id) { - var r = ConfigFileManager.getResources(); - var res = r.getDrawable(id, r.newTheme()); - if (res instanceof BitmapDrawable) { - return ((BitmapDrawable) res).getBitmap(); - } else { - if (res instanceof AdaptiveIconDrawable) { - var layers = new Drawable[]{((AdaptiveIconDrawable) res).getBackground(), - ((AdaptiveIconDrawable) res).getForeground()}; - res = new LayerDrawable(layers); - } - var bitmap = Bitmap.createBitmap(res.getIntrinsicWidth(), - res.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - var canvas = new Canvas(bitmap); - res.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - res.draw(canvas); - return bitmap; - } - } - - private static Icon getNotificationIcon() { - return Icon.createWithBitmap(getBitmap(R.drawable.ic_notification)); - } - - private static boolean hasNotificationChannelForSystem( - INotificationManager nm, String channelId) throws RemoteException { - NotificationChannel channel; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - channel = nm.getNotificationChannelForPackage("android", 1000, channelId, null, false); - } else { - channel = nm.getNotificationChannelForPackage("android", 1000, channelId, false); - } - if (channel != null) { - Log.d(TAG, "hasNotificationChannelForSystem: " + channel); - } - return channel != null; - } - - private static void createNotificationChannel(INotificationManager nm) throws RemoteException { - var context = new FakeContext(); - var list = new ArrayList(); - - var updated = new NotificationChannel(UPDATED_CHANNEL_ID, - context.getString(R.string.module_updated_channel_name), - NotificationManager.IMPORTANCE_HIGH); - updated.setShowBadge(false); - if (hasNotificationChannelForSystem(nm, UPDATED_CHANNEL_ID)) { - Log.d(TAG, "update notification channel: " + UPDATED_CHANNEL_ID); - nm.updateNotificationChannelForPackage("android", 1000, updated); - } else { - list.add(updated); - } - - var status = new NotificationChannel(STATUS_CHANNEL_ID, - context.getString(R.string.status_channel_name), - NotificationManager.IMPORTANCE_MIN); - status.setShowBadge(false); - if (hasNotificationChannelForSystem(nm, STATUS_CHANNEL_ID)) { - Log.d(TAG, "update notification channel: " + STATUS_CHANNEL_ID); - nm.updateNotificationChannelForPackage("android", 1000, status); - } else { - list.add(status); - } - - var scope = new NotificationChannel(SCOPE_CHANNEL_ID, - context.getString(R.string.scope_channel_name), - NotificationManager.IMPORTANCE_HIGH); - scope.setShowBadge(false); - if (hasNotificationChannelForSystem(nm, SCOPE_CHANNEL_ID)) { - Log.d(TAG, "update notification channel: " + SCOPE_CHANNEL_ID); - nm.updateNotificationChannelForPackage("android", 1000, scope); - } else { - list.add(scope); - } - - Log.d(TAG, "create notification channels for android: " + list); - nm.createNotificationChannelsForPackage("android", 1000, new ParceledListSlice<>(list)); - } - - static void notifyStatusNotification() { - var intent = new Intent(openManagerAction); - intent.setPackage("android"); - var context = new FakeContext(); - int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; - var notification = new Notification.Builder(context, STATUS_CHANNEL_ID) - .setContentTitle(context.getString(R.string.vector_running_notification_title)) - .setContentText(context.getString(R.string.vector_running_notification_content)) - .setSmallIcon(getNotificationIcon()) - .setContentIntent(PendingIntent.getBroadcast(context, 1, intent, flags)) - .setVisibility(Notification.VISIBILITY_SECRET) - .setColor(0xFFF48FB1) - .setOngoing(true) - .setAutoCancel(false) - .build(); - notification.extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME); - try { - var nm = getNotificationManager(); - createNotificationChannel(nm); - nm.enqueueNotificationWithTag("android", opPkg, null, - STATUS_NOTIFICATION_ID, notification, 0); - } catch (RemoteException e) { - Log.e(TAG, "notifyStatusNotification: ", e); - } - } - - static void cancelStatusNotification() { - try { - var nm = getNotificationManager(); - createNotificationChannel(nm); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - nm.cancelNotificationWithTag("android", "android", null, STATUS_NOTIFICATION_ID, 0); - } else { - nm.cancelNotificationWithTag("android", null, STATUS_NOTIFICATION_ID, 0); - } - } catch (RemoteException e) { - Log.e(TAG, "cancelStatusNotification: ", e); - } - } - - private static PendingIntent getModuleIntent(String modulePackageName, int moduleUserId) { - var intent = new Intent(openManagerAction); - intent.setPackage("android"); - intent.setData(new Uri.Builder().scheme("module").encodedAuthority(modulePackageName + ":" + moduleUserId).build()); - int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; - return PendingIntent.getBroadcast(new FakeContext(), 3, intent, flags); - } - - private static PendingIntent getModuleScopeIntent(String modulePackageName, int moduleUserId, String scopePackageName, String action, IXposedScopeCallback callback) { - var intent = new Intent(moduleScope); - intent.setPackage("android"); - intent.setData(new Uri.Builder().scheme("module").encodedAuthority(modulePackageName + ":" + moduleUserId).encodedPath(scopePackageName).appendQueryParameter("action", action).build()); - var extras = new Bundle(); - extras.putBinder("callback", callback.asBinder()); - intent.putExtras(extras); - int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; - return PendingIntent.getBroadcast(new FakeContext(), 4, intent, flags); - } - - private static String getNotificationIdKey(String channel, String modulePackageName, int moduleUserId) { - return channel + "/" + modulePackageName + ":" + moduleUserId; - } - - private static int pushAndGetNotificationId(String channel, String modulePackageName, int moduleUserId) { - var idKey = getNotificationIdKey(channel, modulePackageName, moduleUserId); - // previousNotificationId start with 2001 - // https://android.googlesource.com/platform/frameworks/base/+/master/proto/src/system_messages.proto - // https://android.googlesource.com/platform/system/core/+/master/libcutils/include/private/android_filesystem_config.h - // (AID_APP_END - AID_APP_START) x10 = 100000 < NOTE_NETWORK_AVAILABLE - return notificationIds.computeIfAbsent(idKey, key -> previousNotificationId++); - } - - static void notifyModuleUpdated(String modulePackageName, - int moduleUserId, - boolean enabled, - boolean systemModule) { - var context = new FakeContext(); - var userName = UserService.getUserName(moduleUserId); - String title = context.getString(enabled ? systemModule ? - R.string.xposed_module_updated_notification_title_system : - R.string.xposed_module_updated_notification_title : - R.string.module_is_not_activated_yet); - String content = context.getString(enabled ? systemModule ? - R.string.xposed_module_updated_notification_content_system : - R.string.xposed_module_updated_notification_content : - (moduleUserId == 0 ? - R.string.module_is_not_activated_yet_main_user_detailed : - R.string.module_is_not_activated_yet_multi_user_detailed), modulePackageName, userName); - - var style = new Notification.BigTextStyle(); - style.bigText(content); - - var notification = new Notification.Builder(context, UPDATED_CHANNEL_ID) - .setContentTitle(title) - .setContentText(content) - .setSmallIcon(getNotificationIcon()) - .setContentIntent(getModuleIntent(modulePackageName, moduleUserId)) - .setVisibility(Notification.VISIBILITY_SECRET) - .setColor(0xFFF48FB1) - .setAutoCancel(true) - .setStyle(style) - .build(); - notification.extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME); - try { - var nm = getNotificationManager(); - nm.enqueueNotificationWithTag("android", opPkg, modulePackageName, - pushAndGetNotificationId(UPDATED_CHANNEL_ID, modulePackageName, moduleUserId), - notification, 0); - } catch (RemoteException e) { - Log.e(TAG, "notify module updated", e); - } - } - - static void requestModuleScope(String modulePackageName, int moduleUserId, String scopePackageName, IXposedScopeCallback callback) { - var context = new FakeContext(); - var userName = UserService.getUserName(moduleUserId); - String title = context.getString(R.string.xposed_module_request_scope_title); - String content = context.getString(R.string.xposed_module_request_scope_content, modulePackageName, userName, scopePackageName); - - var style = new Notification.BigTextStyle(); - style.bigText(content); - - var notification = new Notification.Builder(context, SCOPE_CHANNEL_ID) - .setContentTitle(title) - .setContentText(content) - .setSmallIcon(getNotificationIcon()) - .setVisibility(Notification.VISIBILITY_SECRET) - .setColor(0xFFF48FB1) - .setAutoCancel(true) - .setTimeoutAfter(1000 * 60 * 60) - .setStyle(style) - .setDeleteIntent(getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "delete", callback)) - .setActions(new Notification.Action.Builder( - Icon.createWithResource(context, R.drawable.ic_baseline_check_24), - context.getString(R.string.scope_approve), - getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "approve", callback)) - .build(), - new Notification.Action.Builder( - Icon.createWithResource(context, R.drawable.ic_baseline_close_24), - context.getString(R.string.scope_deny), - getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "deny", callback)) - .build(), - new Notification.Action.Builder( - Icon.createWithResource(context, R.drawable.ic_baseline_block_24), - context.getString(R.string.never_ask_again), - getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "block", callback)) - .build() - ).build(); - notification.extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME); - try { - var nm = getNotificationManager(); - nm.enqueueNotificationWithTag("android", opPkg, modulePackageName, - pushAndGetNotificationId(SCOPE_CHANNEL_ID, modulePackageName, moduleUserId), - notification, 0); - } catch (RemoteException e) { - try { - callback.onScopeRequestFailed(e.getMessage()); - } catch (RemoteException ignored) { - } - Log.e(TAG, "request module scope", e); - } - } - - static void cancelNotification(String channel, String modulePackageName, int moduleUserId) { - try { - var idKey = getNotificationIdKey(channel, modulePackageName, moduleUserId); - var idValue = notificationIds.get(idKey); - if (idValue == null) return; - var nm = getNotificationManager(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - nm.cancelNotificationWithTag("android", "android", modulePackageName, idValue, 0); - } else { - nm.cancelNotificationWithTag("android", modulePackageName, idValue, 0); - } - notificationIds.remove(idKey); - } catch (RemoteException e) { - Log.e(TAG, "cancel notification", e); - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPSystemServerService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPSystemServerService.java deleted file mode 100644 index facf75417..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPSystemServerService.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; -import static org.lsposed.lspd.service.ServiceManager.getSystemServiceManager; - -import android.os.Build; -import android.os.IBinder; -import android.os.IServiceCallback; -import android.os.Parcel; -import android.os.RemoteException; -import android.os.SystemProperties; -import android.util.Log; - -public class LSPSystemServerService extends ILSPSystemServerService.Stub implements IBinder.DeathRecipient { - - private final String proxyServiceName; - private IBinder originService = null; - private int requested; - - public boolean systemServerRequested() { - return requested > 0; - } - - public void putBinderForSystemServer() { - android.os.ServiceManager.addService(proxyServiceName, this); - binderDied(); - } - - public LSPSystemServerService(int maxRetry, String serviceName) { - Log.d(TAG, "LSPSystemServerService::LSPSystemServerService with proxy " + serviceName); - proxyServiceName = serviceName; - requested = -maxRetry; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // Registers a callback when system is registering an authentic "serial" service - // And we are proxying all requests to that system service - var serviceCallback = new IServiceCallback.Stub() { - @Override - public void onRegistration(String name, IBinder binder) { - Log.d(TAG, "LSPSystemServerService::LSPSystemServerService onRegistration: " + name + " " + binder); - if (name.equals(proxyServiceName) && binder != null && binder != LSPSystemServerService.this) { - Log.d(TAG, "Register " + name + " " + binder); - originService = binder; - LSPSystemServerService.this.linkToDeath(); - } - } - - @Override - public IBinder asBinder() { - return this; - } - }; - try { - getSystemServiceManager().registerForNotifications(proxyServiceName, serviceCallback); - } catch (Throwable e) { - Log.e(TAG, "unregister: ", e); - } - } - } - - @Override - public ILSPApplicationService requestApplicationService(int uid, int pid, String processName, IBinder heartBeat) { - Log.d(TAG, "ILSPApplicationService.requestApplicationService: " + uid + " " + pid + " " + processName + " " + heartBeat); - requested = 1; - if (ConfigManager.getInstance().shouldSkipSystemServer() || uid != 1000 || heartBeat == null || !"system".equals(processName)) - return null; - else - return ServiceManager.requestApplicationService(uid, pid, processName, heartBeat); - } - - @Override - public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { - Log.d(TAG, "LSPSystemServerService.onTransact: code=" + code); - if (originService != null) { - return originService.transact(code, data, reply, flags); - } - - switch (code) { - case BridgeService.TRANSACTION_CODE -> { - int uid = data.readInt(); - int pid = data.readInt(); - String processName = data.readString(); - IBinder heartBeat = data.readStrongBinder(); - var service = requestApplicationService(uid, pid, processName, heartBeat); - if (service != null) { - Log.d(TAG, "LSPSystemServerService.onTransact requestApplicationService granted: " + service); - reply.writeNoException(); - reply.writeStrongBinder(service.asBinder()); - return true; - } else { - Log.d(TAG, "LSPSystemServerService.onTransact requestApplicationService rejected"); - return false; - } - } - case LSPApplicationService.OBFUSCATION_MAP_TRANSACTION_CODE, LSPApplicationService.DEX_TRANSACTION_CODE -> { - // Proxy LSP dex transaction to Application Binder - return ServiceManager.getApplicationService().onTransact(code, data, reply, flags); - } - default -> { - return super.onTransact(code, data, reply, flags); - } - } - } - - public void linkToDeath() { - try { - originService.linkToDeath(this, 0); - } catch (Throwable e) { - Log.e(TAG, "system server service: link to death", e); - } - } - - @Override - public void binderDied() { - if (originService != null) { - originService.unlinkToDeath(this, 0); - originService = null; - } - } - - public void maybeRetryInject() { - if (requested < 0) { - Log.w(TAG, "System server injection fails, trying a restart"); - ++requested; - if (Build.SUPPORTED_64_BIT_ABIS.length > 0 && Build.SUPPORTED_32_BIT_ABIS.length > 0) { - // Only devices with both 32-bit and 64-bit support have zygote_secondary - SystemProperties.set("ctl.restart", "zygote_secondary"); - } else { - SystemProperties.set("ctl.restart", "zygote"); - } - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java deleted file mode 100644 index 6c0aec58d..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java +++ /dev/null @@ -1,499 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static android.content.Intent.EXTRA_UID; -import static org.lsposed.lspd.service.LSPNotificationManager.SCOPE_CHANNEL_ID; -import static org.lsposed.lspd.service.LSPNotificationManager.UPDATED_CHANNEL_ID; -import static org.lsposed.lspd.service.PackageService.PER_USER_RANGE; -import static org.lsposed.lspd.service.ServiceManager.TAG; -import static org.lsposed.lspd.service.ServiceManager.getExecutorService; - -import android.app.IApplicationThread; -import android.app.IUidObserver; -import android.content.Context; -import android.content.IIntentReceiver; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Binder; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.provider.Telephony; -import android.telephony.TelephonyManager; -import android.util.Log; - -import org.matrix.vector.daemon.BuildConfig; -import org.lsposed.lspd.models.Application; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.zip.ZipFile; - -import hidden.HiddenApiBridge; -import io.github.libxposed.service.IXposedScopeCallback; - -public class LSPosedService extends ILSPosedService.Stub { - private static final int AID_NOBODY = 9999; - private static final int USER_NULL = -10000; - private static final String ACTION_USER_ADDED = "android.intent.action.USER_ADDED"; - public static final String ACTION_USER_REMOVED = "android.intent.action.USER_REMOVED"; - private static final String EXTRA_USER_HANDLE = "android.intent.extra.user_handle"; - private static final String EXTRA_REMOVED_FOR_ALL_USERS = "android.intent.extra.REMOVED_FOR_ALL_USERS"; - private static boolean bootCompleted = false; - private IBinder appThread = null; - - private static boolean isModernModules(ApplicationInfo info) { - String[] apks; - if (info.splitSourceDirs != null) { - apks = Arrays.copyOf(info.splitSourceDirs, info.splitSourceDirs.length + 1); - apks[info.splitSourceDirs.length] = info.sourceDir; - } else apks = new String[]{info.sourceDir}; - for (var apk : apks) { - try (var zip = new ZipFile(apk)) { - if (zip.getEntry("META-INF/xposed/java_init.list") != null) { - return true; - } - } catch (IOException ignored) { - } - } - return false; - } - - @Override - public ILSPApplicationService requestApplicationService(int uid, int pid, String processName, IBinder heartBeat) { - if (Binder.getCallingUid() != 1000) { - Log.w(TAG, "Someone else got my binder!?"); - return null; - } - if (ServiceManager.getApplicationService().hasRegister(uid, pid)) { - Log.d(TAG, "Skipped duplicated request for uid " + uid + " pid " + pid); - return null; - } - if (!ServiceManager.getManagerService().shouldStartManager(pid, uid, processName) && ConfigManager.getInstance().shouldSkipProcess(new ConfigManager.ProcessScope(processName, uid))) { - Log.d(TAG, "Skipped " + processName + "/" + uid); - return null; - } - Log.d(TAG, "returned service"); - return ServiceManager.requestApplicationService(uid, pid, processName, heartBeat); - } - - /** - * This part is quite complex. - * For modules, we never care about its user id, we only care about its apk path. - * So we will only process module's removal when it's removed from all users. - * And FULLY_REMOVE is exactly the one. - *

- * For applications, we care about its user id. - * So we will process application's removal when it's removed from every single user. - * However, PACKAGE_REMOVED will be triggered by `pm hide`, so we use UID_REMOVED instead. - */ - - private void dispatchPackageChanged(Intent intent) { - if (intent == null) return; - int uid = intent.getIntExtra(EXTRA_UID, AID_NOBODY); - if (uid == AID_NOBODY || uid <= 0) return; - int userId = intent.getIntExtra("android.intent.extra.user_handle", USER_NULL); - var intentAction = intent.getAction(); - if (intentAction == null) return; - var allUsers = intent.getBooleanExtra(EXTRA_REMOVED_FOR_ALL_USERS, false); - if (userId == USER_NULL) userId = uid % PER_USER_RANGE; - Uri uri = intent.getData(); - var module = ConfigManager.getInstance().getModule(uid); - String moduleName = (uri != null) ? uri.getSchemeSpecificPart() : (module != null) ? module.packageName : null; - - ApplicationInfo applicationInfo = null; - if (moduleName != null) { - try { - applicationInfo = PackageService.getApplicationInfo(moduleName, PackageManager.GET_META_DATA | PackageService.MATCH_ALL_FLAGS, 0); - } catch (Throwable ignored) { - } - } - - boolean isXposedModule = applicationInfo != null && ((applicationInfo.metaData != null && applicationInfo.metaData.containsKey("xposedminversion")) || isModernModules(applicationInfo)); - - switch (intentAction) { - case Intent.ACTION_PACKAGE_FULLY_REMOVED -> { - // for module, remove module - // because we only care about when the apk is gone - if (moduleName != null) { - if (allUsers && ConfigManager.getInstance().removeModule(moduleName)) { - isXposedModule = true; - broadcastAndShowNotification(moduleName, userId, intent, true); - } - LSPNotificationManager.cancelNotification(UPDATED_CHANNEL_ID, moduleName, userId); - } - } - case Intent.ACTION_PACKAGE_REMOVED -> { - if (moduleName != null) { - LSPNotificationManager.cancelNotification(UPDATED_CHANNEL_ID, moduleName, userId); - } - break; - } - case Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_CHANGED -> { - var configManager = ConfigManager.getInstance(); - // make sure that the change is for the complete package, not only a - // component - String[] components = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST); - if (components != null && !Arrays.stream(components).reduce(false, (p, c) -> p || c.equals(moduleName), Boolean::logicalOr)) { - return; - } - if (isXposedModule) { - // When installing a new Xposed module, we update the apk path to mark it as a - // module to send a broadcast when modules that have not been activated are - // uninstalled. - // If cache not updated, assume it's not xposed module - isXposedModule = configManager.updateModuleApkPath(moduleName, ConfigManager.getInstance().getModuleApkPath(applicationInfo), false); - } else { - if (configManager.isUidHooked(uid)) { - // it will automatically remove obsolete app from database - configManager.updateAppCache(); - } - if (intentAction.equals(Intent.ACTION_PACKAGE_ADDED) && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { - for (String xposedModule : configManager.getAutoIncludeModules()) { - // For Xposed modules with auto_include set, we always add new applications - // to its scope - var list = configManager.getModuleScope(xposedModule); - if (list != null) { - Application scope = new Application(); - scope.packageName = moduleName; - scope.userId = userId; - list.add(scope); - try { - if (!configManager.setModuleScope(xposedModule, list)) { - Log.e(TAG, "failed to set scope for " + xposedModule); - } - } catch(RemoteException re) { - Log.e(TAG, "failed to set scope for " + xposedModule, re); - } - } - } - } - } - broadcastAndShowNotification(moduleName, userId, intent, isXposedModule); - } - case Intent.ACTION_UID_REMOVED -> { - // when a package is removed (rather than hide) for a single user - // (apk may still be there because of multi-user) - broadcastAndShowNotification(moduleName, userId, intent, isXposedModule); - if (isXposedModule) { - // it will auto remove obsolete app and scope from database - ConfigManager.getInstance().updateCache(); - } else if (ConfigManager.getInstance().isUidHooked(uid)) { - // it will auto remove obsolete scope from database - ConfigManager.getInstance().updateAppCache(); - } - } - } - boolean removed = Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(intentAction) || Intent.ACTION_UID_REMOVED.equals(intentAction); - - Log.d(TAG, "Package changed: uid=" + uid + " userId=" + userId + " action=" + intentAction + " isXposedModule=" + isXposedModule + " isAllUsers=" + allUsers); - - if (BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME.equals(moduleName) && userId == 0) { - Log.d(TAG, "Manager updated"); - ConfigManager.getInstance().updateManager(removed); - } - } - - private void broadcastAndShowNotification(String packageName, int userId, Intent intent, boolean isXposedModule) { - Log.d(TAG, "package " + packageName + " changed, dispatching to manager"); - var action = intent.getAction(); - var allUsers = intent.getBooleanExtra(EXTRA_REMOVED_FOR_ALL_USERS, false); - intent.putExtra("android.intent.extra.PACKAGES", packageName); - intent.putExtra(Intent.EXTRA_USER, userId); - intent.putExtra("isXposedModule", isXposedModule); - LSPManagerService.broadcastIntent(intent); - if (isXposedModule) { - var enabledModules = ConfigManager.getInstance().enabledModules(); - var scope = ConfigManager.getInstance().getModuleScope(packageName); - boolean systemModule = scope != null && scope.parallelStream().anyMatch(app -> app.packageName.equals("system")); - boolean enabled = Arrays.asList(enabledModules).contains(packageName); - if (!(Intent.ACTION_UID_REMOVED.equals(action) || Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action) || allUsers)) - LSPNotificationManager.notifyModuleUpdated(packageName, userId, enabled, systemModule); - } - } - - private void dispatchUserChanged(Intent intent) { - if (intent == null) return; - int uid = intent.getIntExtra(EXTRA_USER_HANDLE, AID_NOBODY); - if (uid == AID_NOBODY || uid <= 0) return; - LSPManagerService.broadcastIntent(intent); - } - - private void dispatchBootCompleted(Intent intent) { - bootCompleted = true; - var configManager = ConfigManager.getInstance(); - if (configManager.enableStatusNotification()) { - LSPNotificationManager.notifyStatusNotification(); - } else { - LSPNotificationManager.cancelStatusNotification(); - } - } - - private void dispatchConfigurationChanged(Intent intent) { - if (!bootCompleted) return; - ConfigFileManager.reloadConfiguration(); - var configManager = ConfigManager.getInstance(); - if (configManager.enableStatusNotification()) { - LSPNotificationManager.notifyStatusNotification(); - } else { - LSPNotificationManager.cancelStatusNotification(); - } - } - - private void dispatchSecretCodeReceive(Intent i) { - LSPManagerService.openManager(null); - } - - private void dispatchOpenManager(Intent intent) { - LSPManagerService.openManager(intent.getData()); - } - - private void dispatchModuleScope(Intent intent) { - Log.d(TAG, "dispatchModuleScope: " + intent); - var data = intent.getData(); - var extras = intent.getExtras(); - if (extras == null || data == null) return; - var callback = extras.getBinder("callback"); - if (callback == null || !callback.isBinderAlive()) return; - var authority = data.getEncodedAuthority(); - if (authority == null) return; - var s = authority.split(":", 2); - if (s.length != 2) return; - var packageName = s[0]; - int userId; - try { - userId = Integer.parseInt(s[1]); - } catch (NumberFormatException e) { - return; - } - var scopePackageName = data.getPath(); - if (scopePackageName == null) return; - scopePackageName = scopePackageName.substring(1); - var action = data.getQueryParameter("action"); - if (action == null) return; - - var iCallback = IXposedScopeCallback.Stub.asInterface(callback); - try { - var applicationInfo = PackageService.getApplicationInfo(scopePackageName, 0, userId); - if (applicationInfo == null) { - iCallback.onScopeRequestFailed("Package not found"); - return; - } - - switch (action) { - case "approve" -> { - ConfigManager.getInstance().setModuleScope(packageName, scopePackageName, userId); - iCallback.onScopeRequestApproved(Collections.singletonList(scopePackageName)); - } - case "deny" -> iCallback.onScopeRequestFailed("Request denied by user"); - case "delete" -> iCallback.onScopeRequestFailed("Request timeout"); - case "block" -> { - ConfigManager.getInstance().blockScopeRequest(packageName); - iCallback.onScopeRequestFailed("Request blocked by configuration"); - } - } - Log.i(TAG, action + " scope " + scopePackageName + " for " + packageName + " in user " + userId); - } catch (RemoteException e) { - try { - iCallback.onScopeRequestFailed(e.getMessage()); - } catch (RemoteException ignored) { - // callback died - } - } - LSPNotificationManager.cancelNotification(SCOPE_CHANNEL_ID, packageName, userId); - } - - private void registerReceiver(List filters, String requiredPermission, int userId, Consumer task, int flag) { - var receiver = new IIntentReceiver.Stub() { - @Override - public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) { - getExecutorService().submit(() -> { - try { - task.accept(intent); - } catch (Throwable t) { - Log.e(TAG, "performReceive: ", t); - } - }); - if (!ordered && !Objects.equals(intent.getAction(), Intent.ACTION_LOCKED_BOOT_COMPLETED)) - return; - try { - ActivityManagerService.finishReceiver(this, appThread, resultCode, data, extras, false, intent.getFlags()); - } catch (RemoteException e) { - Log.e(TAG, "finish receiver", e); - } - } - }; - try { - for (var filter : filters) { - ActivityManagerService.registerReceiver("android", null, receiver, filter, requiredPermission, userId, flag); - } - } catch (RemoteException e) { - Log.e(TAG, "register receiver", e); - } - } - - private void registerReceiver(List filters, int userId, Consumer task) { - //noinspection InlinedApi - registerReceiver(filters, "android.permission.BRICK", userId, task, Context.RECEIVER_NOT_EXPORTED); - } - - private void registerPackageReceiver() { - var packageFilter = new IntentFilter(); - packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); - packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); - packageFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); - packageFilter.addDataScheme("package"); - - var uidFilter = new IntentFilter(Intent.ACTION_UID_REMOVED); - - registerReceiver(List.of(packageFilter, uidFilter), -1, this::dispatchPackageChanged); - Log.d(TAG, "registered package receiver"); - } - - private void registerConfigurationReceiver() { - var intentFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); - - registerReceiver(List.of(intentFilter), 0, this::dispatchConfigurationChanged); - Log.d(TAG, "registered configuration receiver"); - } - - private void registerSecretCodeReceiver() { - IntentFilter intentFilter = new IntentFilter(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - intentFilter.addAction(TelephonyManager.ACTION_SECRET_CODE); - } else { - // noinspection InlinedApi - intentFilter.addAction(Telephony.Sms.Intents.SECRET_CODE_ACTION); - } - intentFilter.addDataAuthority("5776733", null); - intentFilter.addDataScheme("android_secret_code"); - - //noinspection InlinedApi - registerReceiver(List.of(intentFilter), "android.permission.CONTROL_INCALL_EXPERIENCE", - 0, this::dispatchSecretCodeReceive, Context.RECEIVER_EXPORTED); - Log.d(TAG, "registered secret code receiver"); - } - - private void registerBootCompleteReceiver() { - var intentFilter = new IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED); - intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); - registerReceiver(List.of(intentFilter), 0, this::dispatchBootCompleted); - Log.d(TAG, "registered boot receiver"); - } - - private void registerUserChangeReceiver() { - var userFilter = new IntentFilter(); - userFilter.addAction(ACTION_USER_ADDED); - userFilter.addAction(ACTION_USER_REMOVED); - - registerReceiver(List.of(userFilter), -1, this::dispatchUserChanged); - Log.d(TAG, "registered user info change receiver"); - } - - private void registerOpenManagerReceiver() { - var intentFilter = new IntentFilter(LSPNotificationManager.openManagerAction); - var moduleFilter = new IntentFilter(intentFilter); - moduleFilter.addDataScheme("module"); - - registerReceiver(List.of(intentFilter, moduleFilter), 0, this::dispatchOpenManager); - Log.d(TAG, "registered open manager receiver"); - } - - private void registerModuleScopeReceiver() { - var intentFilter = new IntentFilter(LSPNotificationManager.moduleScope); - intentFilter.addDataScheme("module"); - - registerReceiver(List.of(intentFilter), 0, this::dispatchModuleScope); - Log.d(TAG, "registered module scope receiver"); - } - - private void registerUidObserver() { - try { - var which = HiddenApiBridge.ActivityManager_UID_OBSERVER_ACTIVE() - | HiddenApiBridge.ActivityManager_UID_OBSERVER_GONE() - | HiddenApiBridge.ActivityManager_UID_OBSERVER_IDLE() - | HiddenApiBridge.ActivityManager_UID_OBSERVER_CACHED(); - LSPModuleService.uidClear(); - ActivityManagerService.registerUidObserver(new IUidObserver.Stub() { - @Override - public void onUidActive(int uid) { - LSPModuleService.uidStarts(uid); - } - - @Override - public void onUidCachedChanged(int uid, boolean cached) { - if (!cached) LSPModuleService.uidStarts(uid); - } - - @Override - public void onUidIdle(int uid, boolean disabled) { - LSPModuleService.uidStarts(uid); - } - - @Override - public void onUidGone(int uid, boolean disabled) { - LSPModuleService.uidGone(uid); - } - }, which, HiddenApiBridge.ActivityManager_PROCESS_STATE_UNKNOWN(), null); - } catch (RemoteException e) { - Log.e(TAG, "registerUidObserver", e); - } - } - - @Override - public void dispatchSystemServerContext(IBinder appThread, IBinder activityToken, String api) { - Log.d(TAG, "received system context"); - this.appThread = appThread; - ConfigManager.getInstance().setApi(api); - ActivityManagerService.onSystemServerContext(IApplicationThread.Stub.asInterface(appThread), activityToken); - registerBootCompleteReceiver(); - registerPackageReceiver(); - registerConfigurationReceiver(); - registerSecretCodeReceiver(); - registerUserChangeReceiver(); - registerOpenManagerReceiver(); - registerModuleScopeReceiver(); - registerUidObserver(); - - if (ServiceManager.isLateInject) { - Log.i(TAG, "System already booted during late injection. Manually triggering boot completed."); - dispatchBootCompleted(null); - } - } - - @Override - public boolean preStartManager() { - return ServiceManager.getManagerService().preStartManager(); - } - - @Override - public boolean setManagerEnabled(boolean enabled) throws RemoteException { - return ServiceManager.getManagerService().setEnabled(enabled); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LogcatService.java b/daemon/src/main/java/org/lsposed/lspd/service/LogcatService.java deleted file mode 100644 index 74ba4fae9..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LogcatService.java +++ /dev/null @@ -1,222 +0,0 @@ -package org.lsposed.lspd.service; - -import android.annotation.SuppressLint; -import android.os.Build; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.os.SELinux; -import android.os.SystemProperties; -import android.system.Os; -import android.util.Log; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.LinkOption; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.LinkedHashMap; - -public class LogcatService implements Runnable { - private static final String TAG = "LSPosedLogcat"; - private static final int mode = ParcelFileDescriptor.MODE_WRITE_ONLY | - ParcelFileDescriptor.MODE_CREATE | - ParcelFileDescriptor.MODE_TRUNCATE | - ParcelFileDescriptor.MODE_APPEND; - private int modulesFd = -1; - private int verboseFd = -1; - private Thread thread = null; - - static class LogLRU extends LinkedHashMap { - private static final int MAX_ENTRIES = 10; - - public LogLRU() { - super(MAX_ENTRIES, 1f, false); - } - - @Override - synchronized protected boolean removeEldestEntry(Entry eldest) { - if (size() > MAX_ENTRIES && eldest.getKey().delete()) { - Log.d(TAG, "Deleted old log " + eldest.getKey().getAbsolutePath()); - return true; - } - return false; - } - } - - @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") - private final LinkedHashMap moduleLogs = new LogLRU(); - @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") - private final LinkedHashMap verboseLogs = new LogLRU(); - - @SuppressLint("UnsafeDynamicallyLoadedCode") - public LogcatService() { - String classPath = System.getProperty("java.class.path"); - var abi = Process.is64Bit() ? Build.SUPPORTED_64_BIT_ABIS[0] : Build.SUPPORTED_32_BIT_ABIS[0]; - System.load(classPath + "!/lib/" + abi + "/" + System.mapLibraryName("daemon")); - ConfigFileManager.moveLogDir(); - - // Meizu devices set this prop and prevent debug logs from being recorded - if (SystemProperties.getInt("persist.sys.log_reject_level", 0) > 0) { - SystemProperties.set("persist.sys.log_reject_level", "0"); - } - - getprop(); - dmesg(); - } - - private static void getprop() { - // multithreaded process can not change their context type, - // start a new process to set restricted context to filter privacy props - var cmd = "echo -n u:r:untrusted_app:s0 > /proc/thread-self/attr/current; getprop"; - try { - SELinux.setFSCreateContext("u:object_r:app_data_file:s0"); - new ProcessBuilder("sh", "-c", cmd) - .redirectOutput(ConfigFileManager.getPropsPath()) - .start(); - } catch (IOException e) { - Log.e(TAG, "getprop: ", e); - } finally { - SELinux.setFSCreateContext(null); - } - } - - private static void dmesg() { - try { - new ProcessBuilder("dmesg") - .redirectOutput(ConfigFileManager.getKmsgPath()) - .start(); - } catch (IOException e) { - Log.e(TAG, "dmesg: ", e); - } - } - - private native void runLogcat(); - - @Override - public void run() { - Log.i(TAG, "start running"); - runLogcat(); - Log.i(TAG, "stopped"); - } - - @SuppressWarnings("unused") - private int refreshFd(boolean isVerboseLog) { - try { - File log; - if (isVerboseLog) { - checkFd(verboseFd); - log = ConfigFileManager.getNewVerboseLogPath(); - } else { - checkFd(modulesFd); - log = ConfigFileManager.getNewModulesLogPath(); - } - Log.i(TAG, "New log file: " + log); - ConfigFileManager.chattr0(log.toPath().getParent()); - int fd = ParcelFileDescriptor.open(log, mode).detachFd(); - if (isVerboseLog) { - synchronized (verboseLogs) { - verboseLogs.put(log, new Object()); - } - verboseFd = fd; - } else { - synchronized (moduleLogs) { - moduleLogs.put(log, new Object()); - } - modulesFd = fd; - } - return fd; - } catch (IOException e) { - if (isVerboseLog) verboseFd = -1; - else modulesFd = -1; - Log.w(TAG, "refreshFd", e); - return -1; - } - } - - private static void checkFd(int fd) { - if (fd == -1) return; - try { - var jfd = new FileDescriptor(); - //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi - jfd.getClass().getDeclaredMethod("setInt$", int.class).invoke(jfd, fd); - var stat = Os.fstat(jfd); - if (stat.st_nlink == 0) { - var file = Files.readSymbolicLink(fdToPath(fd)); - var parent = file.getParent(); - if (!Files.isDirectory(parent, LinkOption.NOFOLLOW_LINKS)) { - if (ConfigFileManager.chattr0(parent)) - Files.deleteIfExists(parent); - } - var name = file.getFileName().toString(); - var originName = name.substring(0, name.lastIndexOf(' ')); - Files.copy(file, parent.resolve(originName)); - } - } catch (Throwable e) { - Log.w(TAG, "checkFd " + fd, e); - } - } - - public boolean isRunning() { - return thread != null && thread.isAlive(); - } - - public void start() { - if (isRunning()) return; - thread = new Thread(this); - thread.setName("logcat"); - thread.setUncaughtExceptionHandler((t, e) -> { - Log.e(TAG, "Crash unexpectedly: ", e); - thread = null; - start(); - }); - thread.start(); - } - - public void startVerbose() { - Log.i(TAG, "!!start_verbose!!"); - } - - public void stopVerbose() { - Log.i(TAG, "!!stop_verbose!!"); - } - - public void enableWatchdog() { - Log.i(TAG, "!!start_watchdog!!"); - } - - public void disableWatchdog() { - Log.i(TAG, "!!stop_watchdog!!"); - } - - public void refresh(boolean isVerboseLog) { - if (isVerboseLog) { - Log.i(TAG, "!!refresh_verbose!!"); - } else { - Log.i(TAG, "!!refresh_modules!!"); - } - } - - private static Path fdToPath(int fd) { - if (fd == -1) return null; - else return Paths.get("/proc/self/fd", String.valueOf(fd)); - } - - public File getVerboseLog() { - var path = fdToPath(verboseFd); - return path == null ? null : path.toFile(); - } - - public File getModulesLog() { - var path = fdToPath(modulesFd); - return path == null ? null : path.toFile(); - } - - public void checkLogFile() { - if (modulesFd == -1) - refresh(false); - if (verboseFd == -1) - refresh(true); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ObfuscationManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ObfuscationManager.java deleted file mode 100644 index 4446f2366..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/ObfuscationManager.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.lsposed.lspd.service; - -import android.os.SharedMemory; - -import java.util.HashMap; - -public class ObfuscationManager { - // For module dexes - static native SharedMemory obfuscateDex(SharedMemory memory); - - // generates signature - static native HashMap getSignatures(); -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/PackageService.java b/daemon/src/main/java/org/lsposed/lspd/service/PackageService.java deleted file mode 100644 index 8c7a07a1d..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/PackageService.java +++ /dev/null @@ -1,431 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static android.content.pm.ServiceInfo.FLAG_ISOLATED_PROCESS; -import static org.lsposed.lspd.service.ServiceManager.TAG; -import static org.lsposed.lspd.service.ServiceManager.existsInGlobalNamespace; - -import android.content.IIntentReceiver; -import android.content.IIntentSender; -import android.content.Intent; -import android.content.IntentSender; -import android.content.pm.ApplicationInfo; -import android.content.pm.ComponentInfo; -import android.content.pm.IPackageManager; -import android.content.pm.PackageInfo; -import android.content.pm.PackageInstaller; -import android.content.pm.PackageManager; -import android.content.pm.ParceledListSlice; -import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; -import android.content.pm.VersionedPackage; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemProperties; -import android.util.Log; -import android.util.Pair; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.lsposed.lspd.models.Application; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.stream.Collectors; - -import rikka.parcelablelist.ParcelableListSlice; - -public class PackageService { - - static final int INSTALL_FAILED_INTERNAL_ERROR = -110; - static final int INSTALL_REASON_UNKNOWN = 0; - static final int MATCH_ANY_USER = 0x00400000; // PackageManager.MATCH_ANY_USER - - static final int MATCH_ALL_FLAGS = PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_UNINSTALLED_PACKAGES | MATCH_ANY_USER; - public static final int PER_USER_RANGE = 100000; - - private static IPackageManager pm = null; - private static IBinder binder = null; - private static final Method getInstalledPackagesMethod; - - static { - Method method = null; - try { - boolean isLongFlags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU; - Class flagsType = isLongFlags ? long.class : int.class; - - for (Method m : IPackageManager.class.getDeclaredMethods()) { - if (m.getName().equals("getInstalledPackages") && - m.getParameterTypes().length == 2 && - m.getParameterTypes()[0] == flagsType) { - m.setAccessible(true); - method = m; - break; - } - } - } catch (Exception e) { - Log.e("PackageManagerUtils", "Failed to find getInstalledPackages method", e); - } - getInstalledPackagesMethod = method; - } - - private static List getInstalledPackagesReflect(IPackageManager pm, Object flags, int userId) { - if (getInstalledPackagesMethod == null || pm == null) - return Collections.emptyList(); - try { - Object result = getInstalledPackagesMethod.invoke(pm, flags, userId); - if (result instanceof ParceledListSlice) { - // noinspection unchecked - return ((ParceledListSlice) result).getList(); - } - } catch (Exception e) { - Log.w("PackageManagerUtils", "Reflection call failed", e); - } - return Collections.emptyList(); - } - - static boolean isAlive() { - var pm = getPackageManager(); - return pm != null && pm.asBinder().isBinderAlive(); - } - - private static final IBinder.DeathRecipient recipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - Log.w(TAG, "pm is dead"); - binder.unlinkToDeath(this, 0); - binder = null; - pm = null; - } - }; - - private static IPackageManager getPackageManager() { - if (binder == null || pm == null) { - binder = ServiceManager.getService("package"); - if (binder == null) return null; - try { - binder.linkToDeath(recipient, 0); - } catch (RemoteException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - pm = IPackageManager.Stub.asInterface(binder); - } - return pm; - } - - @Nullable - public static PackageInfo getPackageInfo(String packageName, int flags, int userId) throws RemoteException { - IPackageManager pm = getPackageManager(); - if (pm == null) return null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - return pm.getPackageInfo(packageName, (long) flags, userId); - } - return pm.getPackageInfo(packageName, flags, userId); - } - - public static @NonNull - Map getPackageInfoFromAllUsers(String packageName, int flags) throws RemoteException { - IPackageManager pm = getPackageManager(); - Map res = new HashMap<>(); - if (pm == null) return res; - for (var user : UserService.getUsers()) { - var info = getPackageInfo(packageName, flags, user.id); - if (info != null && info.applicationInfo != null) res.put(user.id, info); - } - return res; - } - - @Nullable - public static ApplicationInfo getApplicationInfo(String packageName, int flags, int userId) throws RemoteException { - IPackageManager pm = getPackageManager(); - if (pm == null) return null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - return pm.getApplicationInfo(packageName, (long) flags, userId); - } - return pm.getApplicationInfo(packageName, flags, userId); - } - - // Only for manager - public static ParcelableListSlice getInstalledPackagesFromAllUsers(int flags, boolean filterNoProcess) - throws RemoteException { - List res = new ArrayList<>(); - IPackageManager pm = getPackageManager(); - if (pm == null) - return ParcelableListSlice.emptyList(); - // Prepare flags once outside the loop - Object flagsObj = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) ? (long) flags : flags; - for (var user : UserService.getUsers()) { - // Use the reflective helper instead of direct AIDL calls - List infos = getInstalledPackagesReflect(pm, flagsObj, user.id); - res.addAll(infos.parallelStream() - .filter(info -> info.applicationInfo != null - && info.applicationInfo.uid / PER_USER_RANGE == user.id) - .filter(info -> { - try { - return isPackageAvailable(info.packageName, user.id, true); - } catch (RemoteException e) { - return false; - } - }) - .collect(Collectors.toList())); - } - if (filterNoProcess) { - return new ParcelableListSlice<>(res.parallelStream().filter(packageInfo -> { - try { - PackageInfo pkgInfo = getPackageInfoWithComponents(packageInfo.packageName, MATCH_ALL_FLAGS, packageInfo.applicationInfo.uid / PER_USER_RANGE); - return !fetchProcesses(pkgInfo).isEmpty(); - } catch (RemoteException e) { - Log.w(TAG, "filter failed", e); - return true; - } - }).collect(Collectors.toList())); - } - return new ParcelableListSlice<>(res); - } - - private static Set fetchProcesses(PackageInfo pkgInfo) { - HashSet processNames = new HashSet<>(); - if (pkgInfo == null) return processNames; - for (ComponentInfo[] componentInfos : new ComponentInfo[][]{pkgInfo.activities, pkgInfo.receivers, pkgInfo.providers}) { - if (componentInfos == null) continue; - for (ComponentInfo componentInfo : componentInfos) { - processNames.add(componentInfo.processName); - } - } - if (pkgInfo.services == null) return processNames; - for (ServiceInfo service : pkgInfo.services) { - if ((service.flags & FLAG_ISOLATED_PROCESS) == 0) { - processNames.add(service.processName); - } - } - return processNames; - } - - public static Pair, Integer> fetchProcessesWithUid(Application app) throws RemoteException { - IPackageManager pm = getPackageManager(); - if (pm == null) return new Pair<>(Collections.emptySet(), -1); - PackageInfo pkgInfo = getPackageInfoWithComponents(app.packageName, MATCH_ALL_FLAGS, app.userId); - if (pkgInfo == null || pkgInfo.applicationInfo == null) - return new Pair<>(Collections.emptySet(), -1); - return new Pair<>(fetchProcesses(pkgInfo), pkgInfo.applicationInfo.uid); - } - - public static boolean isPackageAvailable(String packageName, int userId, boolean ignoreHidden) throws RemoteException { - return pm.isPackageAvailable(packageName, userId) || (ignoreHidden && pm.getApplicationHiddenSettingAsUser(packageName, userId)); - } - - @SuppressWarnings({"ConstantConditions", "SameParameterValue"}) - @Nullable - private static PackageInfo getPackageInfoWithComponents(String packageName, int flags, int userId) throws RemoteException { - IPackageManager pm = getPackageManager(); - if (pm == null) return null; - PackageInfo pkgInfo; - try { - pkgInfo = getPackageInfo(packageName, flags | PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS, userId); - } catch (Exception e) { - pkgInfo = getPackageInfo(packageName, flags, userId); - if (pkgInfo == null) return null; - try { - pkgInfo.activities = getPackageInfo(packageName, flags | PackageManager.GET_ACTIVITIES, userId).activities; - } catch (Exception ignored) { - - } - try { - pkgInfo.services = getPackageInfo(packageName, flags | PackageManager.GET_SERVICES, userId).services; - } catch (Exception ignored) { - - } - try { - pkgInfo.receivers = getPackageInfo(packageName, flags | PackageManager.GET_RECEIVERS, userId).receivers; - } catch (Exception ignored) { - - } - try { - pkgInfo.providers = getPackageInfo(packageName, flags | PackageManager.GET_PROVIDERS, userId).providers; - } catch (Exception ignored) { - - } - } - if (pkgInfo == null || pkgInfo.applicationInfo == null || (!pkgInfo.packageName.equals("android") && (pkgInfo.applicationInfo.sourceDir == null || !existsInGlobalNamespace(pkgInfo.applicationInfo.sourceDir) || !isPackageAvailable(packageName, userId, true)))) - return null; - return pkgInfo; - } - - static abstract class IntentSenderAdaptor extends IIntentSender.Stub { - public abstract void send(Intent intent); - - @Override - public int send(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { - send(intent); - return 0; - } - - @Override - public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { - send(intent); - } - - public IntentSender getIntentSender() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { - @SuppressWarnings("JavaReflectionMemberAccess") - Constructor intentSenderConstructor = IntentSender.class.getConstructor(IIntentSender.class); - intentSenderConstructor.setAccessible(true); - return intentSenderConstructor.newInstance(this); - } - } - - public static boolean uninstallPackage(VersionedPackage versionedPackage, int userId) throws RemoteException, InterruptedException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { - CountDownLatch latch = new CountDownLatch(1); - final boolean[] result = {false}; - var flag = userId == -1 ? 0x00000002 : 0; //PackageManager.DELETE_ALL_USERS = 0x00000002; UserHandle ALL = new UserHandle(-1); - pm.getPackageInstaller().uninstall(versionedPackage, "android", flag, new IntentSenderAdaptor() { - @Override - public void send(Intent intent) { - int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); - result[0] = status == PackageInstaller.STATUS_SUCCESS; - Log.d(TAG, intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)); - latch.countDown(); - } - }.getIntentSender(), userId == -1 ? 0 : userId); - latch.await(); - return result[0]; - } - - public static int installExistingPackageAsUser(String packageName, int userId) throws RemoteException { - IPackageManager pm = getPackageManager(); - Log.d(TAG, "about to install existing package " + packageName + "/" + userId); - if (pm == null) return INSTALL_FAILED_INTERNAL_ERROR; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return pm.installExistingPackageAsUser(packageName, userId, 0, INSTALL_REASON_UNKNOWN, null); - } else { - return pm.installExistingPackageAsUser(packageName, userId, 0, INSTALL_REASON_UNKNOWN); - } - } - - @Nullable - public static ParcelableListSlice queryIntentActivities(Intent intent, String resolvedType, int flags, int userId) { - try { - IPackageManager pm = getPackageManager(); - if (pm == null) return null; - ParceledListSlice infos; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - infos = pm.queryIntentActivities(intent, resolvedType, (long) flags, userId); - } else { - infos = pm.queryIntentActivities(intent, resolvedType, flags, userId); - } - return new ParcelableListSlice<>(infos.getList()); - } catch (Exception e) { - Log.e(TAG, "queryIntentActivities", e); - return new ParcelableListSlice<>(new ArrayList<>()); - } - } - - @Nullable - public static Intent getLaunchIntentForPackage(String packageName) throws RemoteException { - Intent intentToResolve = new Intent(Intent.ACTION_MAIN); - intentToResolve.addCategory(Intent.CATEGORY_INFO); - intentToResolve.setPackage(packageName); - var ris = queryIntentActivities(intentToResolve, intentToResolve.getType(), 0, 0); - - // Otherwise, try to find a main launcher activity. - if (ris == null || ris.getList().size() == 0) { - // reuse the intent instance - intentToResolve.removeCategory(Intent.CATEGORY_INFO); - intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER); - intentToResolve.setPackage(packageName); - ris = queryIntentActivities(intentToResolve, intentToResolve.getType(), 0, 0); - } - if (ris == null || ris.getList().size() == 0) { - return null; - } - Intent intent = new Intent(intentToResolve); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setClassName(ris.getList().get(0).activityInfo.packageName, - ris.getList().get(0).activityInfo.name); - return intent; - } - - public static void clearApplicationProfileData(String packageName) throws RemoteException { - IPackageManager pm = getPackageManager(); - if (pm == null) return; - pm.clearApplicationProfileData(packageName); - } - - public static boolean performDexOptMode(String packageName) throws RemoteException { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - Process process = null; - try { - // The 'speed-profile' filter is a balanced choice for performance. - String command = "cmd package compile -m speed-profile -f " + packageName; - process = Runtime.getRuntime().exec(command); - - // Capture and log the output for debugging. - StringBuilder output = new StringBuilder(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - output.append(line).append("\n"); - } - } - - int exitCode = process.waitFor(); - Log.i(TAG, "Dexopt command finished for " + packageName + " with exit code: " + exitCode); - - // A successful command returns exit code 0 and typically "Success" in its output. - return exitCode == 0 && output.toString().contains("Success"); - - } catch (Exception e) { - Log.e(TAG, "Failed to execute dexopt shell command for " + packageName, e); - if (e instanceof InterruptedException) { - // Preserve the interrupted status. - Thread.currentThread().interrupt(); - } - return false; - } finally { - if (process != null) { - process.destroy(); - } - } - } else { - // Fallback to the original reflection method for older Android versions. - IPackageManager pm = getPackageManager(); - if (pm == null) return false; - return pm.performDexOptMode(packageName, - SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false), - SystemProperties.get("pm.dexopt.install", "speed-profile"), true, true, null); - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/PowerService.java b/daemon/src/main/java/org/lsposed/lspd/service/PowerService.java deleted file mode 100644 index 7ce1ac5b6..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/PowerService.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * - */ - -package org.lsposed.lspd.service; - -import static android.content.Context.POWER_SERVICE; -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.os.IBinder; -import android.os.IPowerManager; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.util.Log; - -public class PowerService { - private static IPowerManager pm = null; - private static IBinder binder = null; - private static final IBinder.DeathRecipient recipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - Log.w(TAG, "PowerManager is dead"); - binder.unlinkToDeath(this, 0); - binder = null; - pm = null; - } - }; - - private static IPowerManager getPowerManager() { - if (binder == null || pm == null) { - binder = ServiceManager.getService(POWER_SERVICE); - if (binder == null) return null; - try { - binder.linkToDeath(recipient, 0); - } catch (RemoteException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - pm = IPowerManager.Stub.asInterface(binder); - } - return pm; - } - - public static void reboot(boolean confirm, String reason, boolean wait) throws RemoteException { - IPowerManager pm = getPowerManager(); - if (pm == null) return; - pm.reboot(confirm, reason, wait); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java deleted file mode 100644 index c2c066b09..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import android.app.ActivityThread; -import android.app.Notification; -import android.content.Context; -import android.ddm.DdmHandleAppName; -import android.os.Binder; -import android.os.Build; -import android.os.IBinder; -import android.os.IServiceManager; -import android.os.Looper; -import android.os.Parcel; -import android.os.Process; -import android.os.RemoteException; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; - -import com.android.internal.os.BinderInternal; - -import org.matrix.vector.daemon.BuildConfig; -import org.lsposed.lspd.util.FakeContext; - -import java.io.File; -import java.lang.AbstractMethodError; -import java.lang.Class; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import hidden.HiddenApiBridge; - -public class ServiceManager { - public static final String TAG = "LSPosedService"; - private static final File globalNamespace = new File("/proc/1/root"); - @SuppressWarnings("FieldCanBeLocal") - private static LSPosedService mainService = null; - private static LSPApplicationService applicationService = null; - private static LSPManagerService managerService = null; - private static LSPSystemServerService systemServerService = null; - private static LogcatService logcatService = null; - private static Dex2OatService dex2OatService = null; - - public static boolean isLateInject = false; - public static String proxyServiceName = "serial"; - - private static final ExecutorService executorService = Executors.newSingleThreadExecutor(); - - @RequiresApi(Build.VERSION_CODES.Q) - public static Dex2OatService getDex2OatService() { - return dex2OatService; - } - - public static ExecutorService getExecutorService() { - return executorService; - } - - private static void waitSystemService(String name) { - while (android.os.ServiceManager.getService(name) == null) { - try { - Log.i(TAG, "service " + name + " is not started, wait 1s."); - //noinspection BusyWait - Thread.sleep(1000); - } catch (InterruptedException e) { - Log.i(TAG, Log.getStackTraceString(e)); - } - } - } - - public static IServiceManager getSystemServiceManager() { - return IServiceManager.Stub.asInterface(HiddenApiBridge.Binder_allowBlocking(BinderInternal.getContextObject())); - } - - // call by ourselves - public static void start(String[] args) { - if (!ConfigFileManager.tryLock()) System.exit(0); - - int systemServerMaxRetry = 1; - for (String arg : args) { - if (arg.startsWith("--system-server-max-retry=")) { - try { - systemServerMaxRetry = Integer.parseInt(arg.substring(arg.lastIndexOf('=') + 1)); - } catch (Throwable ignored) { - } - } else if (arg.equals("--late-inject")) { - isLateInject = true; - proxyServiceName = "serial_vector"; - } - } - - Log.i(TAG, "Vector daemon started: lateInject: " + isLateInject); - Log.i(TAG, String.format("version %s (%d)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)); - - Thread.setDefaultUncaughtExceptionHandler((t, e) -> { - Log.e(TAG, "Uncaught exception", e); - System.exit(1); - }); - - logcatService = new LogcatService(); - logcatService.start(); - - // get config before package service is started - // otherwise getInstance will trigger module/scope cache - var configManager = ConfigManager.getInstance(); - // --- DO NOT call ConfigManager.getInstance later!!! --- - - // Unblock log watchdog before starting anything else - if (configManager.isLogWatchdogEnabled()) - logcatService.enableWatchdog(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) - permissionManagerWorkaround(); - - Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND); - Looper.prepareMainLooper(); - - - mainService = new LSPosedService(); - applicationService = new LSPApplicationService(); - managerService = new LSPManagerService(); - systemServerService = new LSPSystemServerService(systemServerMaxRetry, proxyServiceName); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - dex2OatService = new Dex2OatService(); - dex2OatService.start(); - } - - systemServerService.putBinderForSystemServer(); - - ActivityThread.systemMain(); - - DdmHandleAppName.setAppName("org.lsposed.daemon", 0); - - waitSystemService("package"); - waitSystemService("activity"); - waitSystemService(Context.USER_SERVICE); - waitSystemService(Context.APP_OPS_SERVICE); - - ConfigFileManager.reloadConfiguration(); - - notificationWorkaround(); - - BridgeService.send(mainService, new BridgeService.Listener() { - @Override - public void onSystemServerRestarted() { - Log.w(TAG, "system restarted..."); - } - - @Override - public void onResponseFromBridgeService(boolean response) { - if (response) { - Log.i(TAG, "sent service to bridge"); - } else { - Log.w(TAG, "no response from bridge"); - } - systemServerService.maybeRetryInject(); - } - - @Override - public void onSystemServerDied() { - Log.w(TAG, "system server died"); - systemServerService.putBinderForSystemServer(); - managerService.onSystemServerDied(); - } - }); - - // Force logging on boot, now let's see if we need to stop logging - if (!configManager.verboseLog()) { - logcatService.stopVerbose(); - } - - Looper.loop(); - throw new RuntimeException("Main thread loop unexpectedly exited"); - } - - public static LSPApplicationService getApplicationService() { - return applicationService; - } - - public static LSPApplicationService requestApplicationService(int uid, int pid, String processName, IBinder heartBeat) { - if (applicationService.registerHeartBeat(uid, pid, processName, heartBeat)) - return applicationService; - else return null; - } - - public static LSPManagerService getManagerService() { - return managerService; - } - - public static LogcatService getLogcatService() { - return logcatService; - } - - public static boolean systemServerRequested() { - return systemServerService.systemServerRequested(); - } - - public static File toGlobalNamespace(File file) { - return new File(globalNamespace, file.getAbsolutePath()); - } - - public static File toGlobalNamespace(String path) { - if (path == null) return null; - if (path.startsWith("/")) return new File(globalNamespace, path); - else return toGlobalNamespace(new File(path)); - } - - public static boolean existsInGlobalNamespace(File file) { - return toGlobalNamespace(file).exists(); - } - - public static boolean existsInGlobalNamespace(String path) { - return toGlobalNamespace(path).exists(); - } - - private static void permissionManagerWorkaround() { - try { - Field sCacheField = android.os.ServiceManager.class.getDeclaredField("sCache"); - sCacheField.setAccessible(true); - var sCache = (Map) sCacheField.get(null); - sCache.put("permissionmgr", new BinderProxy("permissionmgr")); - sCache.put("legacy_permission", new BinderProxy("legacy_permission")); - sCache.put("appops", new BinderProxy("appops")); - } catch (Throwable e) { - Log.e(TAG, "failed to init permission manager", e); - } - } - - private static void notificationWorkaround() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { - try { - Class feature = Class.forName("android.app.FeatureFlagsImpl"); - Field systemui_is_cached = feature.getDeclaredField("systemui_is_cached"); - systemui_is_cached.setAccessible(true); - systemui_is_cached.set(null, true); - Log.d(TAG, "set flag systemui_is_cached to true"); - } catch (Throwable e) { - Log.e(TAG, "failed to change feature flags", e); - } - } - - try { - new Notification.Builder(new FakeContext(), "notification_workaround").build(); - } catch (AbstractMethodError e) { - FakeContext.nullProvider = ! FakeContext.nullProvider; - } catch (Throwable e) { - Log.e(TAG, "failed to build notifications", e); - } - - } - - private static class BinderProxy extends Binder { - private static final Method rawGetService; - - static { - try { - rawGetService = android.os.ServiceManager.class.getDeclaredMethod("rawGetService", String.class); - rawGetService.setAccessible(true); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - private IBinder mReal = null; - private final String mName; - - BinderProxy(String name) { - mName = name; - } - - @Override - protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException { - synchronized (this) { - if (mReal == null) { - try { - mReal = (IBinder) rawGetService.invoke(null, mName); - } catch (IllegalAccessException | InvocationTargetException ignored){ - - } - } - if (mReal != null) { - return mReal.transact(code, data, reply, flags); - } - } - // getSplitPermissions - if (reply != null && mName.equals("permissionmgr")) - reply.writeTypedList(List.of()); - return true; - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/UserService.java b/daemon/src/main/java/org/lsposed/lspd/service/UserService.java deleted file mode 100644 index f6eee5597..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/UserService.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.content.Context; -import android.content.pm.UserInfo; -import android.os.Build; -import android.os.IBinder; -import android.os.IUserManager; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.util.Log; - -import org.lsposed.lspd.util.Utils; - -import java.util.LinkedList; -import java.util.List; - -public class UserService { - private static IUserManager um = null; - private static IBinder binder = null; - private static final IBinder.DeathRecipient recipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - Log.w(TAG, "um is dead"); - binder.unlinkToDeath(this, 0); - binder = null; - um = null; - } - }; - - static boolean isAlive() { - var um = getUserManager(); - return um != null && um.asBinder().isBinderAlive(); - } - - public static IUserManager getUserManager() { - if (binder == null || um == null) { - binder = ServiceManager.getService(Context.USER_SERVICE); - if (binder == null) return null; - try { - binder.linkToDeath(recipient, 0); - } catch (RemoteException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - um = IUserManager.Stub.asInterface(binder); - } - return um; - } - - public static List getUsers() throws RemoteException { - IUserManager um = getUserManager(); - List users = new LinkedList<>(); - if (um == null) return users; - try { - users = um.getUsers(true); - } catch (NoSuchMethodError e) { - users = um.getUsers(true, true, true); - } - if (Utils.isLENOVO) { // lenovo hides user [900, 910) for app cloning - var gotUsers = new boolean[10]; - for (var user : users) { - var residual = user.id - 900; - if (residual >= 0 && residual < 10) gotUsers[residual] = true; - } - for (int i = 900; i <= 909; i++) { - var user = um.getUserInfo(i); - if (user != null && !gotUsers[i - 900]) { - users.add(user); - } - } - } - return users; - } - - public static UserInfo getUserInfo(int userId) throws RemoteException { - IUserManager um = getUserManager(); - if (um == null) return null; - return um.getUserInfo(userId); - } - - public static String getUserName(int userId) { - try { - var userInfo = getUserInfo(userId); - if (userInfo != null) return userInfo.name; - } catch (RemoteException ignored) { - } - return String.valueOf(userId); - } - - public static int getProfileParent(int userId) throws RemoteException { - IUserManager um = getUserManager(); - if (um == null) return -1; - var userInfo = um.getProfileParent(userId); - if (userInfo == null) return userId; - else return userInfo.id; - } - - public static boolean isUserUnlocked(int userId) throws RemoteException { - IUserManager um = getUserManager(); - if (um == null) return false; - return um.isUserUnlocked(userId); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/util/FakeContext.java b/daemon/src/main/java/org/lsposed/lspd/util/FakeContext.java deleted file mode 100644 index ce5d5a3ad..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/util/FakeContext.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.lsposed.lspd.util; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.os.UserHandle; -import android.util.Log; - -import androidx.annotation.Nullable; - -import org.lsposed.lspd.service.ConfigFileManager; -import org.lsposed.lspd.service.PackageService; - -import hidden.HiddenApiBridge; - -public class FakeContext extends ContextWrapper { - static ApplicationInfo systemApplicationInfo = null; - static Resources.Theme theme = null; - - public static Boolean nullProvider = false; - - private String packageName = "android"; - public FakeContext() { - super(null); - } - - public FakeContext(String packageName) { - super(null); - this.packageName = packageName; - } - - @Override - public String getPackageName() { - return packageName; - } - - @Override - public Resources getResources() { - return ConfigFileManager.getResources(); - } - - @Override - public String getOpPackageName() { - return "android"; - } - - @Override - public ApplicationInfo getApplicationInfo() { - try { - if (systemApplicationInfo == null) - systemApplicationInfo = PackageService.getApplicationInfo("android", 0, 0); - } catch (Throwable e) { - Log.e(TAG, "getApplicationInfo", e); - } - return systemApplicationInfo; - } - - @Override - public ContentResolver getContentResolver() { - if (nullProvider) { - return null; - } else { - return new ContentResolver(this) {}; - } - } - - public int getUserId() { - return 0; - } - - public UserHandle getUser() { - return HiddenApiBridge.UserHandle(0); - } - - @Override - public Resources.Theme getTheme() { - if (theme == null) theme = getResources().newTheme(); - return theme; - } - - @Nullable - @Override - public String getAttributionTag() { - return null; - } - - @Override - public Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException { - throw new PackageManager.NameNotFoundException(packageName); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/util/InstallerVerifier.java b/daemon/src/main/java/org/lsposed/lspd/util/InstallerVerifier.java deleted file mode 100644 index d682a5c95..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/util/InstallerVerifier.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.lsposed.lspd.util; - -import static org.matrix.vector.daemon.utils.SignInfo.CERTIFICATE; - -import com.android.apksig.ApkVerifier; - -import java.io.File; -import java.io.IOException; -import java.util.Arrays; - -public class InstallerVerifier { - - public static void verifyInstallerSignature(String path) throws IOException { - ApkVerifier verifier = new ApkVerifier.Builder(new File(path)) - .setMinCheckedPlatformVersion(27) - .build(); - try { - ApkVerifier.Result result = verifier.verify(); - if (!result.isVerified()) { - throw new IOException("apk signature not verified"); - } - var mainCert = result.getSignerCertificates().get(0); - if (!Arrays.equals(mainCert.getEncoded(), CERTIFICATE)) { - var dname = mainCert.getSubjectX500Principal().getName(); - throw new IOException("apk signature mismatch: " + dname); - } - } catch (Exception t) { - throw new IOException(t); - } - } -} From e45ffa98e1400fbd1e613110d8e797c0abb9d0a1 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 22:44:05 +0100 Subject: [PATCH 26/38] minor improvements --- daemon/README.md | 42 +++++++++---------- .../vector/daemon/ipc/ApplicationService.kt | 2 +- .../matrix/vector/daemon/ipc/ModuleService.kt | 3 +- .../daemon/system/NotificationManager.kt | 4 +- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/daemon/README.md b/daemon/README.md index fbead783c..52a5bf34d 100644 --- a/daemon/README.md +++ b/daemon/README.md @@ -9,13 +9,13 @@ Unlike the injected framework code, the daemon does not hook methods directly. I The daemon relies on a dual-IPC architecture and extensive use of Android Binder mechanisms to orchestrate the framework lifecycle without triggering SELinux denials or breaking system stability. -1. **Bootstrapping & Bridge (`core/`)**: The daemon starts early in the boot process. It forces its primary Binder (`VectorService`) into `system_server` by hijacking transactions on the Android `activity` service. -2. **Privileged IPC Provider (`ipc/`)**: Android's sandbox prevents target processes from reading the framework APK, accessing SQLite databases, or resolving hidden ART symbols. The daemon exploits its root/system-level permissions to act as an asset server. It provides three critical components to hooked processes over Binder IPC: - * **Framework Loader DEX**: Dispatched via `SharedMemory` to avoid disk-based detection and bypass SELinux `exec` restrictions. - * **Obfuscation Maps**: Dictionaries provided over IPC when API protection is enabled, allowing the injected code to correctly resolve the randomized class names at runtime. - * **Dynamic Module Scopes**: Fast, lock-free lookups of which modules should be loaded into a specific UID/ProcessName. -3. **State Management (`data/`)**: To ensure IPC calls resolve in microseconds without race conditions, the daemon uses an **Immutable State Container** (`DaemonState`). Module topology and scopes are built into a frozen snapshot in the background, which is atomically swapped into memory. High-volume module preference updates are isolated in a separate `PreferenceStore` to prevent state pollution. -4. **Native Environment (`env/` & JNI)**: Background threads (C++ and Kotlin Coroutines) handle low-level system subversion, including `dex2oat` compilation hijacking and logcat monitoring. +1. _Bootstrapping & Bridge (`core/`)_: The daemon starts early in the boot process. It forces its primary Binder (`VectorService`) into `system_server` by hijacking transactions on the Android `activity` service. +2. _Privileged IPC Provider (`ipc/`)_: Android's sandbox prevents target processes from reading the framework APK, accessing SQLite databases, or resolving hidden ART symbols. The daemon exploits its root/system-level permissions to act as an asset server. It provides three critical components to hooked processes over Binder IPC: + * _Framework Loader DEX_: Dispatched via `SharedMemory` to avoid disk-based detection and bypass SELinux `exec` restrictions. + * _Obfuscation Maps_: Dictionaries provided over IPC when API protection is enabled, allowing the injected code to correctly resolve the randomized class names at runtime. + * _Dynamic Module Scopes_: Fast, lock-free lookups of which modules should be loaded into a specific UID/ProcessName. +3. _State Management (`data/`)_: To ensure IPC calls resolve in microseconds without race conditions, the daemon uses an _Immutable State Container_ (`DaemonState`). Module topology and scopes are built into a frozen snapshot in the background, which is atomically swapped into memory. High-volume module preference updates are isolated in a separate `PreferenceStore` to prevent state pollution. +4. _Native Environment (`env/` & JNI)_: Background threads (C++ and Kotlin Coroutines) handle low-level system subversion, including `dex2oat` compilation hijacking and logcat monitoring. ## Directory Layout @@ -34,30 +34,30 @@ src/main/ ## Core Technical Mechanisms ### 1. IPC Routing (The Two Doors) -* **Door 1 (`SystemServerService`)**: A native-to-native entry point used exclusively for the **System-Level Initialization** of `system_server`. By proxying the hardware `serial` service (via `IServiceCallback`), the daemon provides a rendezvous point accessible to the system before the Activity Manager is even initialized. It handles raw UID/PID/Heartbeat packets to authorize the base system framework hook. -* **Door 2 (`VectorService`)**: The **Application-Level Entrance** used by user-space apps. Since user apps are forbidden by SELinux from accessing hardware services like `serial`, they use the "Activity Bridge" to reach the daemon. This door utilizes an action-based protocol allowing the daemon to perform **Scope Filtering**—matching the calling process against the current `DaemonState` before granting access to the framework. +* _Door 1 (`SystemServerService`)_: A native-to-native entry point used exclusively for the _System-Level Initialization_ of `system_server`. By proxying the hardware `serial` service (via `IServiceCallback`), the daemon provides a rendezvous point accessible to the system before the Activity Manager is even initialized. It handles raw UID/PID/Heartbeat packets to authorize the base system framework hook. +* _Door 2 (`VectorService`)_: The _Application-Level Entrance_ used by user-space apps. Since user apps are forbidden by SELinux from accessing hardware services like `serial`, they use the "Activity Bridge" to reach the daemon. This door utilizes an action-based protocol allowing the daemon to perform _Scope Filtering_—matching the calling process against the current `DaemonState` before granting access to the framework. ### 2. AOT Compilation Hijacking (`dex2oat`) To prevent Android's ART from inlining hooked methods (which makes them unhookable), Vector hijacks the Ahead-of-Time (AOT) compiler. -* **Mechanism**: The daemon (`Dex2OatServer`) mounts a C++ wrapper binary (`bin/dex2oatXX`) over the system's actual `dex2oat` binaries in the `/apex` mount namespace. -* **FD Passing**: When the wrapper executes, to read the original compiler or the `liboat_hook.so`, it opens a UNIX domain socket to the daemon. The daemon (running as root) opens the files and passes the File Descriptors (FDs) back to the wrapper via `SCM_RIGHTS`. -* **Execution**: The wrapper uses `memfd_create` and `sendfile` to load the hook, bypassing execute restrictions, and uses `LD_PRELOAD` to inject the hook into the real `dex2oat` process while appending `--inline-max-code-units=0`. +* _Mechanism_: The daemon (`Dex2OatServer`) mounts a C++ wrapper binary (`bin/dex2oatXX`) over the system's actual `dex2oat` binaries in the `/apex` mount namespace. +* _FD Passing_: When the wrapper executes, to read the original compiler or the `liboat_hook.so`, it opens a UNIX domain socket to the daemon. The daemon (running as root) opens the files and passes the File Descriptors (FDs) back to the wrapper via `SCM_RIGHTS`. +* _Execution_: The wrapper uses `memfd_create` and `sendfile` to load the hook, bypassing execute restrictions, and uses `LD_PRELOAD` to inject the hook into the real `dex2oat` process while appending `--inline-max-code-units=0`. ### 3. API Protection & DEX Obfuscation To prevent unauthorized apps from detecting the framework or invoking the Xposed API, the daemon randomizes framework and loader class names on each boot. JNI maps the input `SharedMemory` via `MAP_SHARED` to gain direct, zero-copy access to the physical pages populated by Java. Using the [DexBuilder](https://github.com/JingMatrix/DexBuilder) library, the daemon mutates the DEX string pool in-place; this is highly efficient as the library's Intermediate Representation points directly to the mapped buffer, avoiding unnecessary heap allocations during the randomization process. -Once mutation is complete, the finalized DEX is written into a new `SharedMemory` region and the original plaintext handle is closed. Because signatures are now randomized, the daemon provides **Obfuscation Maps** via Door 1 and Door 2. These dictionaries allow the injected code to correctly "re-link" and resolve the framework's internal classes at runtime despite their randomized names. +Once mutation is complete, the finalized DEX is written into a new `SharedMemory` region and the original plaintext handle is closed. Because signatures are now randomized, the daemon provides _Obfuscation Maps_ via Door 1 and Door 2. These dictionaries allow the injected code to correctly "re-link" and resolve the framework's internal classes at runtime despite their randomized names. -### 4. Lifecycle & State Tracking -The daemon must precisely know which apps are installed and which processes are running. -* **Broadcasts**: `VectorService` registers a hidden `IIntentReceiver` to listen for `ACTION_PACKAGE_ADDED`, `REMOVED`, and `ACTION_LOCKED_BOOT_COMPLETED`. -* **UID Observers**: `IUidObserver` tracks `onUidActive` and `onUidGone`. When a process becomes active, the daemon uses a forged `ContentProvider` call (`send_binder`) to proactively push the `IXposedService` binder into the target process, bypassing standard `bindService` limitations. +### 4. Lifecycle & Process Injection +Vector uses a proactive _Push Model_ to distribute the `IXposedService` binder. Upon detecting a process start via `IUidObserver`, the daemon utilizes `getContentProviderExternal` to obtain a direct line to the module's internal provider. It then executes a synchronous `IContentProvider.call()`, passing the control binder within a `Bundle`. This ensures the framework reference is injected into the target process’s memory before its `Application.onCreate()` executes, bypassing the detection and latency associated with standard `bindService` calls. + +_Remote Preferences & Files_ are supported by a combination of the injected Binder and custom SELinux types. The daemon stores preferences and shared files in directories labeled `xposed_data`. Because the policy allows global access to this type, the injected binder simply provides the path or File Descriptor, and the target app can perform direct I/O, bypassing standard per-app sandbox restrictions. ## Development & Maintenance Guidelines When modifying the daemon, strictly adhere to the following principles: -1. **Never Block IPC Threads**: AIDL `onTransact` methods are called synchronously by the Android framework and target apps. Blocking these threads (e.g., by executing raw SQL queries or heavy I/O directly) will cause Application Not Responding (ANR) crashes system-wide. Always read from the lock-free, immutable `DaemonState` snapshot exposed by `ConfigCache.state`. -2. **Resource Determinism**: The daemon runs indefinitely. Leaking a single `Cursor`, `ParcelFileDescriptor`, or `SharedMemory` instance will eventually exhaust system limits and crash the OS. Always use Kotlin's `.use { }` blocks or explicit C++ RAII wrappers for native resources. -3. **Isolate OEM Quirks**: Android OS behavior varies wildly between manufacturers (e.g., Lenovo hiding cloned apps in user IDs 900-909, MIUI killing background dual-apps). Place all OEM-specific logic in `utils/Workarounds.kt` to prevent core logic pollution. -4. **Context Forgery (`FakeContext`)**: The daemon does not have a real Android `Context`. To interact with system APIs that require one (like building Notifications or querying packages), use `FakeContext`. Be aware that standard `Context` methods may crash if not explicitly mocked. +1. _Never Block IPC Threads_: AIDL `onTransact` methods are called synchronously by the Android framework and target apps. Blocking these threads (e.g., by executing raw SQL queries or heavy I/O directly) will cause Application Not Responding (ANR) crashes system-wide. Always read from the lock-free, immutable `DaemonState` snapshot exposed by `ConfigCache.state`. +2. _Resource Determinism_: The daemon runs indefinitely. Leaking a single `Cursor`, `ParcelFileDescriptor`, or `SharedMemory` instance will eventually exhaust system limits and crash the OS. Always use Kotlin's `.use { }` blocks or explicit C++ RAII wrappers for native resources. +3. _Isolate OEM Quirks_: Android OS behavior varies wildly between manufacturers (e.g., Lenovo hiding cloned apps in user IDs 900-909, MIUI killing background dual-apps). Place all OEM-specific logic in `utils/Workarounds.kt` to prevent core logic pollution. +4. _Context Forgery (`FakeContext`)_: The daemon does not have a real Android `Context`. To interact with system APIs that require one (like building Notifications or querying packages), use `FakeContext`. Be aware that standard `Context` methods may crash if not explicitly mocked. diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt index ec9b4cf78..c83c064d9 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt @@ -90,7 +90,7 @@ object ApplicationService : ILSPApplicationService.Stub() { override fun getModulesList(): List { val info = ensureRegistered() if (info.key.uid == Process.SYSTEM_UID && info.processName == "system") { - return ConfigCache.getModulesForSystemServer() // Needs implementation in ConfigCache + return ConfigCache.getModulesForSystemServer() } if (ManagerService.isRunningManager(getCallingPid(), info.key.uid)) { return emptyList() diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt index 8b55ceb33..c8434ff6a 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt @@ -21,7 +21,6 @@ import org.matrix.vector.daemon.system.PER_USER_RANGE import org.matrix.vector.daemon.system.activityManager private const val TAG = "VectorModuleService" -private const val SEND_BINDER = "send_binder" class ModuleService(private val loadedModule: Module) : IXposedService.Stub() { @@ -35,7 +34,7 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() { fun uidStarts(uid: Int) { if (uidSet.add(uid)) { - val module = ConfigCache.getModuleByUid(uid) // Needs impl in ConfigCache + val module = ConfigCache.getModuleByUid(uid) if (module?.file?.legacy == false) { val service = serviceMap.getOrPut(module) { ModuleService(module) } service.sendBinder(uid) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt index 36d5ebcf7..3bf25000a 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt @@ -24,8 +24,8 @@ import org.matrix.vector.daemon.data.FileSystem import org.matrix.vector.daemon.utils.FakeContext private const val TAG = "VectorNotifManager" -private const val STATUS_CHANNEL_ID = "lsposed_status" -private const val UPDATED_CHANNEL_ID = "lsposed_module_updated" +private const val STATUS_CHANNEL_ID = "vector_status" +private const val UPDATED_CHANNEL_ID = "vector_module_updated" private const val STATUS_NOTIF_ID = 2000 object NotificationManager { From ce4af047c68fd9adcc581fdf946facf458f40fe0 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Fri, 27 Mar 2026 09:17:49 +0100 Subject: [PATCH 27/38] Support Android 17 beta3 --- .../vector/daemon/system/SystemExtensions.kt | 49 +++++++++++++++---- .../android/content/pm/IPackageManager.java | 7 --- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt index 47403be73..9a5d687a7 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt @@ -8,11 +8,13 @@ import android.content.IntentFilter import android.content.pm.IPackageManager import android.content.pm.PackageInfo import android.content.pm.PackageManager +import android.content.pm.ParceledListSlice import android.content.pm.ResolveInfo import android.content.pm.ServiceInfo import android.os.Build import android.os.IUserManager import android.util.Log +import java.lang.reflect.Method import org.matrix.vector.daemon.utils.getRealUsers private const val TAG = "VectorSystem" @@ -131,6 +133,37 @@ fun IPackageManager.clearApplicationProfileDataCompat(packageName: String) { runCatching { clearApplicationProfileData(packageName) } } +/** Cached method reference to avoid repeated reflection lookups in loops. */ +private val getInstalledPackagesMethod: Method? by lazy { + val isLongFlags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + android.content.pm.IPackageManager::class + .java + .declaredMethods + .find { + it.name == "getInstalledPackages" && + it.parameterTypes.size == 2 && + it.parameterTypes[0] == + (if (isLongFlags) Long::class.javaPrimitiveType else Int::class.javaPrimitiveType) + } + ?.apply { isAccessible = true } +} + +/** + * Reflectively calls getInstalledPackages and casts to ParceledListSlice. This works on Android 17+ + * because PackageInfoList extends ParceledListSlice. + */ +private fun IPackageManager.getInstalledPackagesReflect( + flags: Any, + userId: Int +): List { + val method = getInstalledPackagesMethod ?: return emptyList() + return runCatching { + val result = method.invoke(this, flags, userId) + @Suppress("UNCHECKED_CAST") (result as? ParceledListSlice)?.list + } + .getOrNull() ?: emptyList() +} + fun IPackageManager.getInstalledPackagesForAllUsers( flags: Int, filterNoProcess: Boolean @@ -139,16 +172,12 @@ fun IPackageManager.getInstalledPackagesForAllUsers( val users = userManager?.getRealUsers() ?: emptyList() for (user in users) { - val infos = - runCatching { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getInstalledPackages(flags.toLong(), user.id) - } else { - getInstalledPackages(flags, user.id) - } - } - .getOrNull() - ?.list ?: continue + // We pass flags as Any so the reflective invoke handles Long or Int correctly + val flagParam: Any = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) flags.toLong() else flags + + val infos = getInstalledPackagesReflect(flagParam, user.id) + if (infos.isEmpty()) continue result.addAll( infos.filter { diff --git a/hiddenapi/stubs/src/main/java/android/content/pm/IPackageManager.java b/hiddenapi/stubs/src/main/java/android/content/pm/IPackageManager.java index 279e89754..15578dc7c 100644 --- a/hiddenapi/stubs/src/main/java/android/content/pm/IPackageManager.java +++ b/hiddenapi/stubs/src/main/java/android/content/pm/IPackageManager.java @@ -38,13 +38,6 @@ PackageInfo getPackageInfo(String packageName, long flags, int userId) String[] getPackagesForUid(int uid) throws RemoteException; - ParceledListSlice getInstalledPackages(int flags, int userId) - throws RemoteException; - - @RequiresApi(33) - ParceledListSlice getInstalledPackages(long flags, int userId) - throws RemoteException; - ParceledListSlice getInstalledApplications(int flags, int userId) throws RemoteException; From c8b74c705b6c6b09c8e9b28ad1be9cae3d9bedab Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Fri, 27 Mar 2026 11:39:11 +0100 Subject: [PATCH 28/38] improve logging --- .../matrix/vector/daemon/utils/Workarounds.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt index eef5c51a1..f062b771b 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt @@ -13,20 +13,22 @@ import org.matrix.vector.daemon.system.packageManager private const val TAG = "VectorWorkarounds" private val isLenovo = Build.MANUFACTURER.equals("lenovo", ignoreCase = true) -/** Retrieves all users, applying Lenovo's app cloning workaround (hides users 900-909). */ fun IUserManager.getRealUsers(): List { val users = runCatching { getUsers(true, true, true) } - .getOrElse { - getUsers(true) // Fallback for older Android versions - } - ?.toMutableList() ?: mutableListOf() + .recoverCatching { t -> if (t is NoSuchMethodError) getUsers(true) else throw t } + .onFailure { Log.e(TAG, "All user retrieval attempts failed", it) } + .getOrDefault(emptyList()) + .toMutableList() if (isLenovo) { val existingIds = users.map { it.id }.toSet() for (i in 900..909) { if (i !in existingIds) { - runCatching { getUserInfo(i) }.getOrNull()?.let { users.add(it) } + runCatching { getUserInfo(i) } + .onFailure { Log.e(TAG, "Failed to apply Lenovo's app cloning workaround", it) } + .getOrNull() + ?.let { users.add(it) } } } } @@ -70,6 +72,7 @@ fun performDexOptMode(packageName: String): Boolean { val exitCode = process.waitFor() exitCode == 0 && output.contains("Success") } + .onFailure { Log.e(TAG, "Failed to exectute dexopt via cmd", it) } .getOrDefault(false) } else { return runCatching { @@ -81,6 +84,7 @@ fun performDexOptMode(packageName: String): Boolean { true, null) == true } + .onFailure { Log.e(TAG, "Failed to invoke IPackageManager.performDexOptMode", it) } .getOrDefault(false) } } From 7d3459be42c911685e22823b051df8caf713aec9 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Fri, 27 Mar 2026 16:07:46 +0100 Subject: [PATCH 29/38] Fix proguard rules --- daemon/proguard-rules.pro | 11 ++--------- .../org/matrix/vector/daemon/env/LogcatMonitor.kt | 2 +- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/daemon/proguard-rules.pro b/daemon/proguard-rules.pro index 74c73f81e..2926ac59f 100644 --- a/daemon/proguard-rules.pro +++ b/daemon/proguard-rules.pro @@ -1,17 +1,10 @@ -keepclasseswithmembers,includedescriptorclasses class * { native ; } --keepclasseswithmembers class org.lsposed.lspd.Main { +-keepclasseswithmembers class org.matrix.vector.daemon.core.VectorDaemon { public static void main(java.lang.String[]); } --keepclasseswithmembers class org.lsposed.lspd.service.Dex2OatService { - private java.lang.String devTmpDir; - private java.lang.String magiskPath; - private java.lang.String fakeBin32; - private java.lang.String fakeBin64; - private java.lang.String[] dex2oatBinaries; -} --keepclasseswithmembers class org.lsposed.lspd.service.LogcatService { +-keepclasseswithmembers class org.matrix.vector.daemon.env.LogcatMonitor { private int refreshFd(boolean); } -keepclassmembers class ** implements android.content.ContextWrapper { diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt index 6900a41d7..060c62202 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt @@ -155,7 +155,7 @@ object LogcatMonitor { } @Suppress("unused") // Called via JNI - fun refreshFd(isVerboseLog: Boolean): Int { + private fun refreshFd(isVerboseLog: Boolean): Int { return runCatching { val logFile = if (isVerboseLog) { From 0da27d941d64bf177f6c87b6265276b88b64fd72 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Fri, 27 Mar 2026 16:35:35 +0100 Subject: [PATCH 30/38] Fix enable modules --- .../vector/daemon/data/ModuleDatabase.kt | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt index 1069afcb3..55bc735a4 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt @@ -11,10 +11,28 @@ object ModuleDatabase { fun enableModule(packageName: String): Boolean { if (packageName == "lspd") return false - val values = ContentValues().apply { put("enabled", 1) } - val changed = - ConfigCache.dbHelper.writableDatabase.update( - "modules", values, "module_pkg_name = ?", arrayOf(packageName)) > 0 + val db = ConfigCache.dbHelper.writableDatabase + var changed = false + + // First, check if it exists. If not, we need to "discover" it. + val exists = + db.compileStatement("SELECT COUNT(*) FROM modules WHERE module_pkg_name = ?") + .apply { bindString(1, packageName) } + .simpleQueryForLong() > 0 + if (!exists) { + val values = + ContentValues().apply { + put("module_pkg_name", packageName) + put("apk_path", "") // defer to cache updating + put("enabled", 1) + } + db.insert("modules", null, values) + changed = true + } else { + val values = ContentValues().apply { put("enabled", 1) } + changed = db.update("modules", values, "module_pkg_name = ?", arrayOf(packageName)) > 0 + } + if (changed) ConfigCache.requestCacheUpdate() return changed } From 92453a177223f83f3d9007ad090b8446dfa5d478 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sat, 28 Mar 2026 02:32:31 +0100 Subject: [PATCH 31/38] Add cli feature Implement cli via socket --- .../org/lsposed/manager/ConfigManager.java | 9 - .../manager/ui/fragment/HomeFragment.java | 4 +- .../manager/ui/fragment/SettingsFragment.java | 2 +- .../org/lsposed/manager/util/UpdateUtil.java | 6 +- daemon/build.gradle.kts | 8 + daemon/proguard-rules.pro | 5 +- .../kotlin/org/matrix/vector/daemon/Cli.kt | 364 ++++++++++++++++++ .../vector/daemon/{core => }/VectorDaemon.kt | 13 +- .../vector/daemon/core/VectorService.kt | 25 +- .../matrix/vector/daemon/data/ConfigCache.kt | 189 ++++----- .../matrix/vector/daemon/data/DaemonState.kt | 4 - .../matrix/vector/daemon/data/FileSystem.kt | 29 +- .../vector/daemon/data/ModuleDatabase.kt | 45 --- .../vector/daemon/data/PreferenceStore.kt | 17 +- .../vector/daemon/env/CliSocketServer.kt | 109 ++++++ .../vector/daemon/ipc/ApplicationService.kt | 5 +- .../matrix/vector/daemon/ipc/CliHandler.kt | 211 ++++++++++ .../daemon/ipc/InjectedModuleService.kt | 6 +- .../vector/daemon/ipc/ManagerService.kt | 64 +-- .../matrix/vector/daemon/ipc/ModuleService.kt | 15 +- .../vector/daemon/ipc/SystemServerService.kt | 6 +- .../matrix/vector/daemon/utils/Workarounds.kt | 39 +- gradle/libs.versions.toml | 1 + .../lsposed/lspd/service/ILSPosedService.aidl | 2 +- .../org/lsposed/lspd/ILSPManagerService.aidl | 2 - zygisk/module/cli | 27 ++ zygisk/module/customize.sh | 3 +- zygisk/module/daemon | 2 +- .../matrix/vector/service/BridgeService.kt | 6 +- 29 files changed, 955 insertions(+), 263 deletions(-) create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/Cli.kt rename daemon/src/main/kotlin/org/matrix/vector/daemon/{core => }/VectorDaemon.kt (88%) create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/env/CliSocketServer.kt create mode 100644 daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/CliHandler.kt create mode 100644 zygisk/module/cli diff --git a/app/src/main/java/org/lsposed/manager/ConfigManager.java b/app/src/main/java/org/lsposed/manager/ConfigManager.java index 9be97ba5e..cbef009f8 100644 --- a/app/src/main/java/org/lsposed/manager/ConfigManager.java +++ b/app/src/main/java/org/lsposed/manager/ConfigManager.java @@ -336,15 +336,6 @@ public static boolean setHiddenIcon(boolean hide) { } } - public static String getApi() { - try { - return LSPManagerServiceHolder.getService().getApi(); - } catch (RemoteException e) { - Log.e(App.TAG, Log.getStackTraceString(e)); - return e.toString(); - } - } - public static List getDenyListPackages() { List list = new ArrayList<>(); try { diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/HomeFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/HomeFragment.java index ac9b9d450..438150e81 100644 --- a/app/src/main/java/org/lsposed/manager/ui/fragment/HomeFragment.java +++ b/app/src/main/java/org/lsposed/manager/ui/fragment/HomeFragment.java @@ -150,8 +150,8 @@ private void updateStates(Activity activity, boolean binderAlive, boolean needUp binding.statusTitle.setText(R.string.activated); binding.statusIcon.setImageResource(R.drawable.ic_round_check_circle_24); } - binding.statusSummary.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d) - %s", - ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode(), ConfigManager.getApi())); + binding.statusSummary.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d)", + ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode())); binding.developerWarningCard.setVisibility(isDeveloper() ? View.VISIBLE : View.GONE); } else { boolean isMagiskInstalled = ConfigManager.isMagiskInstalled(); diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/SettingsFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/SettingsFragment.java index ef8c5f728..c077074b3 100644 --- a/app/src/main/java/org/lsposed/manager/ui/fragment/SettingsFragment.java +++ b/app/src/main/java/org/lsposed/manager/ui/fragment/SettingsFragment.java @@ -80,7 +80,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c getChildFragmentManager().beginTransaction().add(R.id.setting_container, new PreferenceFragment()).commitNow(); } if (ConfigManager.isBinderAlive()) { - binding.toolbar.setSubtitle(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d) - %s", ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode(), ConfigManager.getApi())); + binding.toolbar.setSubtitle(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d)", ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode())); } else { binding.toolbar.setSubtitle(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d) - %s", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, getString(R.string.not_installed))); } diff --git a/app/src/main/java/org/lsposed/manager/util/UpdateUtil.java b/app/src/main/java/org/lsposed/manager/util/UpdateUtil.java index 8e7285419..525149815 100644 --- a/app/src/main/java/org/lsposed/manager/util/UpdateUtil.java +++ b/app/src/main/java/org/lsposed/manager/util/UpdateUtil.java @@ -55,13 +55,12 @@ public void onResponse(@NonNull Call call, @NonNull Response response) { if (!response.isSuccessful()) return; var body = response.body(); if (body == null) return; - String api = ConfigManager.isBinderAlive() ? ConfigManager.getApi() : "riru"; try { var info = JsonParser.parseReader(body.charStream()).getAsJsonObject(); var notes = info.get("body").getAsString(); var assetsArray = info.getAsJsonArray("assets"); for (var assets : assetsArray) { - checkAssets(assets.getAsJsonObject(), notes, api.toLowerCase(Locale.ROOT)); + checkAssets(assets.getAsJsonObject(), notes); } } catch (Throwable t) { Log.e(App.TAG, t.getMessage(), t); @@ -79,11 +78,10 @@ public void onFailure(@NonNull Call call, @NonNull IOException e) { App.getOkHttpClient().newCall(request).enqueue(callback); } - private static void checkAssets(JsonObject assets, String releaseNotes, String api) { + private static void checkAssets(JsonObject assets, String releaseNotes) { var pref = App.getPreferences(); var name = assets.get("name").getAsString(); var splitName = name.split("-"); - if (!splitName[3].equals(api)) return; pref.edit() .putInt("latest_version", Integer.parseInt(splitName[2])) .putLong("latest_check", Instant.now().getEpochSecond()) diff --git a/daemon/build.gradle.kts b/daemon/build.gradle.kts index 08633588a..40b7e7b61 100644 --- a/daemon/build.gradle.kts +++ b/daemon/build.gradle.kts @@ -1,6 +1,7 @@ import com.android.build.api.dsl.ApplicationExtension import com.android.ide.common.signing.KeystoreHelper import java.io.PrintStream +import java.util.UUID val defaultManagerPackageName: String by rootProject.extra val injectedPackageName: String by rootProject.extra @@ -26,6 +27,11 @@ android { buildConfigField("int", "MANAGER_INJECTED_UID", """$injectedPackageUid""") buildConfigField("String", "VERSION_NAME", """"${versionNameProvider.get()}"""") buildConfigField("long", "VERSION_CODE", versionCodeProvider.get()) + + val cliToken = UUID.randomUUID() + // Inject the MSB and LSB as Long constants + buildConfigField("Long", "CLI_TOKEN_MSB", "${cliToken.mostSignificantBits}L") + buildConfigField("Long", "CLI_TOKEN_LSB", "${cliToken.leastSignificantBits}L") } buildTypes { @@ -92,6 +98,8 @@ android.applicationVariants.all { dependencies { implementation(libs.agp.apksig) + implementation(libs.gson) + implementation(libs.picocli) implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.coroutines.core) implementation(projects.external.apache) diff --git a/daemon/proguard-rules.pro b/daemon/proguard-rules.pro index 2926ac59f..4f1bd0b1a 100644 --- a/daemon/proguard-rules.pro +++ b/daemon/proguard-rules.pro @@ -1,7 +1,10 @@ -keepclasseswithmembers,includedescriptorclasses class * { native ; } --keepclasseswithmembers class org.matrix.vector.daemon.core.VectorDaemon { +-keepclasseswithmembers class org.matrix.vector.daemon.VectorDaemon { + public static void main(java.lang.String[]); +} +-keepclasseswithmembers class org.matrix.vector.daemon.Cli { public static void main(java.lang.String[]); } -keepclasseswithmembers class org.matrix.vector.daemon.env.LogcatMonitor { diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/Cli.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/Cli.kt new file mode 100644 index 000000000..b305fbc11 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/Cli.kt @@ -0,0 +1,364 @@ +package org.matrix.vector.daemon + +import android.net.LocalSocket +import android.net.LocalSocketAddress +import android.os.Process +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.ToNumberPolicy +import java.io.DataInputStream +import java.io.DataOutputStream +import java.io.FileDescriptor +import java.io.FileInputStream +import java.util.concurrent.Callable +import kotlin.system.exitProcess +import org.matrix.vector.daemon.data.FileSystem +import picocli.CommandLine +import picocli.CommandLine.* + +// --- IPC Data Models --- +data class CliRequest( + val command: String, + val action: String = "", + val targets: List = emptyList(), + val options: Map = emptyMap() +) + +data class CliResponse( + val success: Boolean, + val data: Any? = null, + val error: String? = null, + val isFdAttached: Boolean = false +) + +// --- IPC Client Logic --- +object VectorIPC { + val gson: Gson = + GsonBuilder() + .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) // Handles Any/Object fields + .setNumberToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) // Handles Map values + .setPrettyPrinting() + .create() + + fun transmit(request: CliRequest): CliResponse { + val socket = LocalSocket() + return try { + val cliSocket = FileSystem.socketPath.toString() + val socketFile = java.io.File(cliSocket) + + if (!socketFile.exists()) { + System.err.println("Error: Socket file not found at $cliSocket") + System.err.println("Current UID: ${android.os.Process.myUid()}") + } + socket.connect(LocalSocketAddress(cliSocket, LocalSocketAddress.Namespace.FILESYSTEM)) + + val output = DataOutputStream(socket.outputStream) + val input = DataInputStream(socket.inputStream) + + // Send Security Token + output.writeLong(BuildConfig.CLI_TOKEN_MSB) + output.writeLong(BuildConfig.CLI_TOKEN_LSB) + + // Send Request + output.writeUTF(gson.toJson(request)) + + // Read Response + val responseJson = input.readUTF() + val response = gson.fromJson(responseJson, CliResponse::class.java) + + // Handle Log Streaming + if (response.isFdAttached) { + val hasFd = input.readByte() + if (hasFd.toInt() == 1) { + val fds = socket.getAncillaryFileDescriptors() + if (!fds.isNullOrEmpty()) { + streamLog(fds[0], request.options["follow"] as? Boolean ?: false) + } + } + } + response + } catch (e: Exception) { + CliResponse(success = false, error = "Socket Failure: ${e.message}") + } finally { + socket.close() + } + } + + private fun streamLog(fd: java.io.FileDescriptor, follow: Boolean) { + // Wrap the raw FileDescriptor in a FileInputStream. + // 'use' ensures that fis.close() (and thus the FD) is called + // when the block finishes or if an exception is thrown. + FileInputStream(fd).use { fis -> + val reader = fis.bufferedReader() + + try { + while (true) { + val line = reader.readLine() + if (line != null) { + println(line) + } else { + if (!follow) break // EOF reached, exit + + // In follow mode, wait for new data to be written to the log + Thread.sleep(100) + } + + // Check if thread was interrupted (e.g. by a shutdown hook) + if (Thread.interrupted()) break + } + } catch (e: Exception) { + if (e !is InterruptedException) { + System.err.println("Log streaming error: ${e.message}") + } + } + } // FD is closed here automatically + } +} + +// --- UI Formatter --- +object OutputFormatter { + /** + * Auto-formats the Daemon's output. Prints ASCII tables for lists, Key-Value for maps, or raw + * JSON. + */ + @Suppress("UNCHECKED_CAST") + fun print(response: CliResponse, isJson: Boolean): Int { + if (isJson) { + println(VectorIPC.gson.toJson(response)) + return if (response.success) 0 else 1 + } + + if (!response.success) { + System.err.println("Error: ${response.error}") + return 1 + } + + val data = response.data ?: return 0 + + when (data) { + is List<*> -> { + if (data.isEmpty()) { + println("No records found.") + return 0 + } + // Check if it's a list of objects/maps to draw a table + val first = data[0] + if (first is Map<*, *>) { + printTable(data as List>) + } else { + data.forEach { println(" - $it") } + } + } + is Map<*, *> -> { + data.forEach { (k, v) -> println("$k: $v") } + } + else -> println(data.toString()) + } + return 0 + } + + private fun printTable(rows: List>) { + val headers = rows.first().keys.toList() + val columnWidths = headers.associateWith { it.length }.toMutableMap() + + // Calculate maximum width for each column + for (row in rows) { + for (header in headers) { + val length = row[header]?.toString()?.length ?: 0 + if (length > columnWidths[header]!!) { + columnWidths[header] = length + } + } + } + + // Print Headers + val headerRow = headers.joinToString(" ") { it.padEnd(columnWidths[it]!!) } + println(headerRow.uppercase()) + println("-".repeat(headerRow.length)) + + // Print Data + for (row in rows) { + println( + headers.joinToString(" ") { header -> + (row[header]?.toString() ?: "").padEnd(columnWidths[header]!!) + }) + } + } +} + +// --- CLI Commands (picocli) --- +@Command( + name = "vector-cli", + mixinStandardHelpOptions = true, + version = ["Vector CLI ${BuildConfig.VERSION_NAME}"], + description = ["A fast, scriptable CLI for configuring the Vector Framework daemon."], + subcommands = + [ + StatusCommand::class, + ModulesCommand::class, + ScopeCommand::class, + ConfigCommand::class, + LogCommand::class]) +class Cli : Callable { + + @Option( + names = ["--json"], + description = ["Output structured JSON for scripting"], + scope = ScopeType.INHERIT) + var json: Boolean = false + + override fun call(): Int { + CommandLine(this).usage(System.out) + return 0 + } + + companion object { + @JvmStatic + fun main(args: Array) { + val uid = Process.myUid() + if (uid != 0) { + System.err.println("Permission denied: Vector CLI must run as root.") + exitProcess(1) + } + val mainThread = Thread.currentThread() + Runtime.getRuntime() + .addShutdownHook( + Thread { + mainThread.interrupt() // Signal the loop to stop and close the stream + }) + val exitCode = CommandLine(Cli()).execute(*args) + exitProcess(exitCode) + } + } +} + +@Command(name = "status", description = ["Show framework and system health status"]) +class StatusCommand : Callable { + @ParentCommand lateinit var parent: Cli + + override fun call(): Int { + val req = CliRequest(command = "status") + val res = VectorIPC.transmit(req) + return OutputFormatter.print(res, parent.json) + } +} + +@Command(name = "modules", description = ["Manage Xposed modules"]) +class ModulesCommand { + @ParentCommand lateinit var parent: Cli + + @Command(name = "ls", description = ["List installed modules"]) + fun ls( + @Option(names = ["-e", "--enabled"], description = ["Show only enabled modules"]) + enabled: Boolean, + @Option(names = ["-d", "--disabled"], description = ["Show only disabled modules"]) + disabled: Boolean + ): Int { + val req = + CliRequest( + command = "modules", + action = "ls", + options = mapOf("enabled" to enabled, "disabled" to disabled)) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } + + @Command(name = "enable", description = ["Enable one or more modules (batch processing)"]) + fun enable(@Parameters(paramLabel = "PKG", arity = "1..*") pkgs: List): Int { + val req = CliRequest(command = "modules", action = "enable", targets = pkgs) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } + + @Command(name = "disable", description = ["Disable one or more modules (batch processing)"]) + fun disable(@Parameters(paramLabel = "PKG", arity = "1..*") pkgs: List): Int { + val req = CliRequest(command = "modules", action = "disable", targets = pkgs) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } +} + +@Command(name = "scope", description = ["Manage granular application injection scopes"]) +class ScopeCommand { + @ParentCommand lateinit var parent: Cli + + @Command(name = "ls", description = ["List apps in a module's scope"]) + fun ls(@Parameters(index = "0", paramLabel = "MODULE_PKG") modulePkg: String): Int { + val req = CliRequest(command = "scope", action = "ls", targets = listOf(modulePkg)) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } + + @Command(name = "add", description = ["Append apps to scope (format: pkg/user_id)"]) + fun add( + @Parameters(index = "0", paramLabel = "MODULE_PKG") modulePkg: String, + @Parameters(index = "1..*") apps: List + ): Int { + val req = CliRequest(command = "scope", action = "add", targets = listOf(modulePkg) + apps) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } + + @Command(name = "set", description = ["Overwrite entire scope (format: pkg/user_id)"]) + fun set( + @Parameters(index = "0", paramLabel = "MODULE_PKG") modulePkg: String, + @Parameters(index = "1..*") apps: List + ): Int { + val req = CliRequest(command = "scope", action = "set", targets = listOf(modulePkg) + apps) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } +} + +@Command(name = "config", description = ["Manage daemon preferences natively"]) +class ConfigCommand { + @ParentCommand lateinit var parent: Cli + + @Command(name = "get", description = ["Get a config value"]) + fun get(@Parameters(paramLabel = "KEY") key: String): Int { + val req = CliRequest(command = "config", action = "get", targets = listOf(key)) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } + + @Command(name = "set", description = ["Set a config value"]) + fun set( + @Parameters(index = "0", paramLabel = "KEY") key: String, + @Parameters(index = "1", paramLabel = "VALUE") value: String + ): Int { + val req = CliRequest(command = "config", action = "set", targets = listOf(key, value)) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } +} + +@Command(name = "log", description = ["Stream or clear framework logs"]) +class LogCommand { + @ParentCommand lateinit var parent: Cli + + @Command(name = "cat", description = ["Dump logs and exit"]) + fun cat( + @Option(names = ["-v", "--verbose"], description = ["Read verbose daemon log"]) + verbose: Boolean + ): Int { + val req = + CliRequest( + command = "log", + action = "stream", + options = mapOf("verbose" to verbose, "follow" to false)) + VectorIPC.transmit(req) + return 0 + } + + @Command(name = "tail", description = ["Follow logs in real-time"]) + fun tail( + @Option(names = ["-v", "--verbose"], description = ["Follow verbose daemon log"]) + verbose: Boolean + ): Int { + val req = + CliRequest( + command = "log", + action = "stream", + options = mapOf("verbose" to verbose, "follow" to true)) + VectorIPC.transmit(req) + return 0 + } + + @Command(name = "clear", description = ["Clear log buffers"]) + fun clear(@Option(names = ["-v", "--verbose"]) verbose: Boolean): Int { + val req = CliRequest(command = "log", action = "clear", options = mapOf("verbose" to verbose)) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt similarity index 88% rename from daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt rename to daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt index 721523a0a..83a3b5cb2 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorDaemon.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt @@ -1,4 +1,4 @@ -package org.matrix.vector.daemon.core +package org.matrix.vector.daemon import android.app.ActivityThread import android.content.Context @@ -13,9 +13,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.matrix.vector.daemon.BuildConfig -import org.matrix.vector.daemon.data.ConfigCache +import org.matrix.vector.daemon.core.SystemServerBridge +import org.matrix.vector.daemon.core.VectorService import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.data.PreferenceStore +import org.matrix.vector.daemon.env.CliSocketServer import org.matrix.vector.daemon.env.Dex2OatServer import org.matrix.vector.daemon.env.LogcatMonitor import org.matrix.vector.daemon.ipc.ManagerService @@ -55,12 +57,13 @@ object VectorDaemon { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { Dex2OatServer.start() } + CliSocketServer.start() // Accessing the object triggers the `init` block, reading SQLite instantly. - if (ConfigCache.isLogWatchdogEnabled()) LogcatMonitor.enableWatchdog() + if (PreferenceStore.isLogWatchdogEnabled()) LogcatMonitor.enableWatchdog() // Preload Framework DEX in the background CoroutineScope(Dispatchers.IO).launch { - FileSystem.getPreloadDex(ConfigCache.isDexObfuscateEnabled()) + FileSystem.getPreloadDex(PreferenceStore.isDexObfuscateEnabled()) } // Setup Main Looper & System Services diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt index 3cf43a6d8..e75bbcdd6 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt @@ -19,7 +19,10 @@ import kotlinx.coroutines.launch import org.lsposed.lspd.service.ILSPApplicationService import org.lsposed.lspd.service.ILSPosedService import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.VectorDaemon 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.ManagerService @@ -36,13 +39,9 @@ object VectorService : ILSPosedService.Stub() { override fun dispatchSystemServerContext( appThread: IBinder?, activityToken: IBinder?, - api: String ) { - Log.d(TAG, "Received System Server Context (API: $api)") - appThread?.let { SystemContext.appThread = IApplicationThread.Stub.asInterface(it) } SystemContext.token = activityToken - ConfigCache.api = api // Initialize OS Observers using Coroutines for the dispatch blocks registerReceivers() @@ -201,8 +200,8 @@ object VectorService : ILSPosedService.Stub() { private fun dispatchBootCompleted() { bootCompleted = true - if (ConfigCache.enableStatusNotification) { - NotificationManager.notifyStatusNotification() + if (PreferenceStore.isStatusNotificationEnabled()) { + NotificationManager.cancelStatusNotification() } } @@ -229,7 +228,7 @@ object VectorService : ILSPosedService.Stub() { Intent.ACTION_PACKAGE_FULLY_REMOVED -> { if (moduleName != null && intent.getBooleanExtra("android.intent.extra.REMOVED_FOR_ALL_USERS", false)) { - if (ConfigCache.removeModule(moduleName)) isXposedModule = true + if (ModuleDatabase.removeModule(moduleName)) isXposedModule = true } } Intent.ACTION_PACKAGE_ADDED, @@ -239,16 +238,16 @@ object VectorService : ILSPosedService.Stub() { packageManager?.getPackageInfoCompat(moduleName, MATCH_ALL_FLAGS, 0)?.applicationInfo if (appInfo != null) { isXposedModule = - ConfigCache.updateModuleApkPath( + ModuleDatabase.updateModuleApkPath( moduleName, ConfigCache.getModuleApkPath(appInfo), false) } - } else if (ConfigCache.cachedScopes.keys.any { it.uid == uid }) { + } else if (ConfigCache.state.scopes.keys.any { it.uid == uid }) { ConfigCache.requestCacheUpdate() } } Intent.ACTION_UID_REMOVED -> { if (isXposedModule) ConfigCache.requestCacheUpdate() - else if (ConfigCache.cachedScopes.keys.any { it.uid == uid }) + else if (ConfigCache.state.scopes.keys.any { it.uid == uid }) ConfigCache.requestCacheUpdate() } } @@ -309,7 +308,7 @@ object VectorService : ILSPosedService.Stub() { this.packageName = scopePackageName this.userId = userId }) - ConfigCache.setModuleScope(packageName, scopes) + ModuleDatabase.setModuleScope(packageName, scopes) } iCallback.onScopeRequestApproved(listOf(scopePackageName)) } @@ -317,9 +316,9 @@ object VectorService : ILSPosedService.Stub() { "delete" -> iCallback.onScopeRequestFailed("Request timeout") "block" -> { val blocked = - ConfigCache.getModulePrefs("lspd", 0, "config")["scope_request_blocked"] + PreferenceStore.getModulePrefs("lspd", 0, "config")["scope_request_blocked"] as? Set ?: emptySet() - ConfigCache.updateModulePref( + PreferenceStore.updateModulePref( "lspd", 0, "config", "scope_request_blocked", blocked + packageName) iCallback.onScopeRequestFailed("Request blocked by configuration") } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt index 5add6ae2e..f1a89c9dc 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt @@ -7,6 +7,7 @@ import android.util.Log import hidden.HiddenApiBridge import java.io.File import java.nio.file.Files +import java.nio.file.Path import java.nio.file.Paths import java.nio.file.attribute.PosixFilePermissions import java.util.UUID @@ -19,21 +20,23 @@ import org.lsposed.lspd.models.Application import org.lsposed.lspd.models.Module import org.matrix.vector.daemon.BuildConfig import org.matrix.vector.daemon.ipc.InjectedModuleService -import org.matrix.vector.daemon.ipc.ManagerService import org.matrix.vector.daemon.system.* import org.matrix.vector.daemon.utils.getRealUsers private const val TAG = "VectorConfigCache" object ConfigCache { + // Module preference operations are delegated to PreferenceStore + // Writable operations of modules are delegated to ModuleDatabase - // --- IMMUTABLE STATE --- @Volatile var state = DaemonState() private set val dbHelper = Database() // Kept public for PreferenceStore and ModuleDatabase + private var miscPath: Path? = null + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val cacheUpdateChannel = Channel(Channel.CONFLATED) @@ -46,37 +49,15 @@ object ConfigCache { initializeConfig() } - // --- STATE PROXIES (For backwards compatibility) --- - var api: String - get() = state.api - set(value) { - state = state.copy(api = value) - } - - var enableStatusNotification: Boolean - get() = state.enableStatusNotification - set(value) { - state = state.copy(enableStatusNotification = value) - } - - val cachedModules: Map - get() = state.modules - - val cachedScopes: Map> - get() = state.scopes - private fun initializeConfig() { val config = PreferenceStore.getModulePrefs("lspd", 0, "config") - ManagerService.isVerboseLog = config["enable_verbose_log"] as? Boolean ?: true - val enableStatusNotif = config["enable_status_notification"] as? Boolean ?: true - - if (config["enable_auto_add_shortcut"] != null) { - PreferenceStore.updateModulePref("lspd", 0, "config", "enable_auto_add_shortcut", null) - } + // if (config["enable_auto_add_shortcut"] != null) { + // PreferenceStore.updateModulePref("lspd", 0, "config", "enable_auto_add_shortcut", null) + // } val pathStr = config["misc_path"] as? String - val miscPath = + miscPath = if (pathStr == null) { val newPath = Paths.get("/data/misc", UUID.randomUUID().toString()) PreferenceStore.updateModulePref("lspd", 0, "config", "misc_path", newPath.toString()) @@ -88,13 +69,10 @@ object ConfigCache { runCatching { val perms = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx--x--x")) - Files.createDirectories(miscPath, perms) - FileSystem.setSelinuxContextRecursive(miscPath, "u:object_r:xposed_data:s0") + Files.createDirectories(miscPath!!, perms) + FileSystem.setSelinuxContextRecursive(miscPath!!, "u:object_r:xposed_data:s0") } .onFailure { Log.e(TAG, "Failed to create misc directory", it) } - - // Swap state with initialization data - state = state.copy(enableStatusNotification = enableStatusNotif, miscPath = miscPath) } private fun ensureCacheReady() { @@ -104,7 +82,7 @@ object ConfigCache { if (!state.isCacheReady) { Log.i(TAG, "System services are ready. Mapping modules and scopes.") updateManager(false) - forceCacheUpdateSync() + performCacheUpdate() state = state.copy(isCacheReady = true) } } @@ -137,10 +115,6 @@ object ConfigCache { cacheUpdateChannel.trySend(Unit) } - fun forceCacheUpdateSync() { - performCacheUpdate() - } - /** Builds a completely new Immutable State and atomically swaps it. */ private fun performCacheUpdate() { if (packageManager == null) return @@ -291,6 +265,51 @@ object ConfigCache { // } } + fun getModuleScope(packageName: String): MutableList? { + if (packageName == "lspd") return null + val result = mutableListOf() + ConfigCache.dbHelper.readableDatabase + .query( + "scope INNER JOIN modules ON scope.mid = modules.mid", + arrayOf("app_pkg_name", "user_id"), + "modules.module_pkg_name = ?", + arrayOf(packageName), + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + result.add( + Application().apply { + this.packageName = cursor.getString(0) + this.userId = cursor.getInt(1) + }) + } + } + return result + } + + fun getAutoInclude(packageName: String): Boolean { + if (packageName == "lspd") return false + + var isAutoInclude = false + ConfigCache.dbHelper.readableDatabase + .query( + "modules", + arrayOf("auto_include"), + "module_pkg_name = ?", + arrayOf(packageName), + null, + null, + null) + .use { cursor -> + if (cursor.moveToFirst()) { + isAutoInclude = cursor.getInt(0) == 1 + } + } + return isAutoInclude + } + fun getModulesForProcess(processName: String, uid: Int): List { ensureCacheReady() return state.scopes[ProcessScope(processName, uid)] ?: emptyList() @@ -368,29 +387,6 @@ object ConfigCache { return modules } - fun getPrefsPath(packageName: String, uid: Int): String { - ensureCacheReady() - val currentState = state - val basePath = - currentState.miscPath ?: throw IllegalStateException("Fatal: miscPath not initialized!") - - val userId = uid / PER_USER_RANGE - val userSuffix = if (userId == 0) "" else userId.toString() - val path = basePath.resolve("prefs$userSuffix").resolve(packageName) - - val module = currentState.modules[packageName] - if (module != null && module.appId == uid % PER_USER_RANGE) { - runCatching { - val perms = - PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx--x--x")) - Files.createDirectories(path, perms) - Files.walk(path).forEach { p -> Os.chown(p.toString(), uid, uid) } - } - .onFailure { Log.e(TAG, "Failed to prepare prefs path", it) } - } - return path.toString() - } - fun getModuleApkPath(info: ApplicationInfo): String? { val apks = mutableListOf() info.sourceDir?.let { apks.add(it) } @@ -407,55 +403,40 @@ object ConfigCache { } } + fun getInstalledModules(): List { + val allPackages = + packageManager?.getInstalledPackagesForAllUsers(MATCH_ALL_FLAGS, false) ?: emptyList() + return allPackages + .mapNotNull { it.applicationInfo } + .filter { info -> getModuleApkPath(info) != null } + } + fun shouldSkipProcess(scope: ProcessScope): Boolean { ensureCacheReady() return !state.scopes.containsKey(scope) && !isManager(scope.uid) } - fun getEnabledModules(): List = state.modules.keys.toList() - - fun getDenyListPackages(): List = emptyList() - - fun getModulePrefs(pkg: String, userId: Int, group: String) = - PreferenceStore.getModulePrefs(pkg, userId, group) - - fun updateModulePref(pkg: String, userId: Int, group: String, key: String, value: Any?) = - PreferenceStore.updateModulePref(pkg, userId, group, key, value) - - fun updateModulePrefs(pkg: String, userId: Int, group: String, diff: Map) = - PreferenceStore.updateModulePrefs(pkg, userId, group, diff) - - fun deleteModulePrefs(pkg: String, userId: Int, group: String) = - PreferenceStore.deleteModulePrefs(pkg, userId, group) - - fun isDexObfuscateEnabled() = PreferenceStore.isDexObfuscateEnabled() - - fun setDexObfuscate(enabled: Boolean) = PreferenceStore.setDexObfuscate(enabled) - - fun isLogWatchdogEnabled() = PreferenceStore.isLogWatchdogEnabled() - - fun setLogWatchdog(enabled: Boolean) = PreferenceStore.setLogWatchdog(enabled) - - fun isScopeRequestBlocked(pkg: String) = PreferenceStore.isScopeRequestBlocked(pkg) - - fun enableModule(pkg: String) = ModuleDatabase.enableModule(pkg) - - fun disableModule(pkg: String) = ModuleDatabase.disableModule(pkg) - - fun getModuleScope(pkg: String) = ModuleDatabase.getModuleScope(pkg) - - fun setModuleScope(pkg: String, scope: MutableList) = - ModuleDatabase.setModuleScope(pkg, scope) - - fun removeModuleScope(pkg: String, scopePkg: String, userId: Int) = - ModuleDatabase.removeModuleScope(pkg, scopePkg, userId) - - fun updateModuleApkPath(pkg: String, apkPath: String?, force: Boolean) = - ModuleDatabase.updateModuleApkPath(pkg, apkPath, force) + fun getPrefsPath(packageName: String, uid: Int): String { + ensureCacheReady() + val currentState = state + val basePath = miscPath ?: throw IllegalStateException("Fatal: miscPath not initialized!") - fun removeModule(pkg: String) = ModuleDatabase.removeModule(pkg) + val userId = uid / PER_USER_RANGE + val userSuffix = if (userId == 0) "" else userId.toString() + val path = basePath.resolve("prefs$userSuffix").resolve(packageName) - fun getAutoInclude(pkg: String) = ModuleDatabase.getAutoInclude(pkg) + val module = currentState.modules[packageName] + if (module != null && module.appId == uid % PER_USER_RANGE) { + runCatching { + val perms = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx--x--x")) + Files.createDirectories(path, perms) + Files.walk(path).forEach { p -> Os.chown(p.toString(), uid, uid) } + } + .onFailure { Log.e(TAG, "Failed to prepare prefs path", it) } + } + return path.toString() + } - fun setAutoInclude(pkg: String, enabled: Boolean) = ModuleDatabase.setAutoInclude(pkg, enabled) + fun getDenyListPackages(): List = emptyList() // TODO: implement it } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/DaemonState.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/DaemonState.kt index e4f23fea3..7e01f7521 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/DaemonState.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/DaemonState.kt @@ -1,6 +1,5 @@ package org.matrix.vector.daemon.data -import java.nio.file.Path import org.lsposed.lspd.models.Module data class ProcessScope(val processName: String, val uid: Int) @@ -10,11 +9,8 @@ data class ProcessScope(val processName: String, val uid: Int) * and atomically swap the reference. */ data class DaemonState( - val api: String = "(???)", - val enableStatusNotification: Boolean = true, val managerUid: Int = -1, val isCacheReady: Boolean = false, - val miscPath: Path? = null, val modules: Map = emptyMap(), val scopes: Map> = emptyMap() ) 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 6b3c7a9ad..8ac061ce7 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 @@ -43,6 +43,7 @@ object FileSystem { val logDirPath: Path = basePath.resolve("log") val oldLogDirPath: Path = basePath.resolve("log.old") val modulePath: Path = basePath.resolve("modules") + val socketPath: Path = basePath.resolve(".cli_sock") val daemonApkPath: Path = Paths.get(System.getProperty("java.class.path", "")) val managerApkPath: Path = daemonApkPath.parent.resolve("manager.apk") val configDirPath: Path = basePath.resolve("config") @@ -59,12 +60,34 @@ object FileSystem { init { runCatching { Files.createDirectories(basePath) + Os.chmod(basePath.toString(), "700".toInt(8)) SELinux.setFileContext(basePath.toString(), "u:object_r:system_file:s0") Files.createDirectories(configDirPath) } .onFailure { Log.e(TAG, "Failed to initialize directories", it) } } + fun setupCli(): String { + val cliSource = daemonApkPath.parent.resolve("cli").toFile() + val cliDest = basePath.resolve("cli").toFile() + if (cliSource.exists()) { + runCatching { + cliSource.copyTo(cliDest, overwrite = true) + Os.chmod(cliDest.absolutePath, "700".toInt(8)) + } + .onFailure { Log.e(TAG, "Failed to deploy CLI script", it) } + } + + val cliSocket: String = socketPath.toString() + val socketFile = File(cliSocket) + if (socketFile.exists()) { + Log.d(TAG, "Existing $cliSocket deleted") + socketFile.delete() + } + + return cliSocket + } + /** Tries to lock the daemon lockfile. Returns false if another daemon is running. */ fun tryLock(): Boolean { return runCatching { @@ -151,7 +174,7 @@ object FileSystem { runCatching { ZipFile(file).use { zip -> - // 1. Read all classes*.dex files + // Read all classes*.dex files var secondary = 1 while (true) { val entryName = if (secondary == 1) "classes.dex" else "classes$secondary.dex" @@ -160,7 +183,7 @@ object FileSystem { secondary++ } - // 2. Read initialization lists + // Read initialization lists fun readList(name: String, dest: MutableList) { zip.getEntry(name)?.let { entry -> zip.getInputStream(entry).bufferedReader().useLines { lines -> @@ -358,7 +381,7 @@ object FileSystem { addFile("modules_config.db", dbPath) runCatching { os.putNextEntry(ZipEntry("scopes.txt")) - ConfigCache.cachedScopes.forEach { (scope, modules) -> + ConfigCache.state.scopes.forEach { (scope, modules) -> os.write("${scope.processName}/${scope.uid}\n".toByteArray()) modules.forEach { mod -> os.write("\t${mod.packageName}\n".toByteArray()) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt index 55bc735a4..cfeca4165 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt @@ -47,30 +47,6 @@ object ModuleDatabase { return changed } - fun getModuleScope(packageName: String): MutableList? { - if (packageName == "lspd") return null - val result = mutableListOf() - ConfigCache.dbHelper.readableDatabase - .query( - "scope INNER JOIN modules ON scope.mid = modules.mid", - arrayOf("app_pkg_name", "user_id"), - "modules.module_pkg_name = ?", - arrayOf(packageName), - null, - null, - null) - .use { cursor -> - while (cursor.moveToNext()) { - result.add( - Application().apply { - this.packageName = cursor.getString(0) - this.userId = cursor.getInt(1) - }) - } - } - return result - } - fun setModuleScope(packageName: String, scope: MutableList): Boolean { enableModule(packageName) val db = ConfigCache.dbHelper.writableDatabase @@ -151,27 +127,6 @@ object ModuleDatabase { return res } - fun getAutoInclude(packageName: String): Boolean { - if (packageName == "lspd") return false - - var isAutoInclude = false - ConfigCache.dbHelper.readableDatabase - .query( - "modules", - arrayOf("auto_include"), - "module_pkg_name = ?", - arrayOf(packageName), - null, - null, - null) - .use { cursor -> - if (cursor.moveToFirst()) { - isAutoInclude = cursor.getInt(0) == 1 - } - } - return isAutoInclude - } - fun setAutoInclude(packageName: String, enabled: Boolean): Boolean { if (packageName == "lspd") return false diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt index 2e64df37f..dfd11579a 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt @@ -2,6 +2,7 @@ package org.matrix.vector.daemon.data import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import org.apache.commons.lang3.SerializationUtilsX object PreferenceStore { @@ -20,7 +21,7 @@ object PreferenceStore { while (cursor.moveToNext()) { val key = cursor.getString(0) val blob = cursor.getBlob(1) - val obj = org.apache.commons.lang3.SerializationUtilsX.deserialize(blob) + val obj = SerializationUtilsX.deserialize(blob) if (obj != null) result[key] = obj } } @@ -41,7 +42,7 @@ object PreferenceStore { ContentValues().apply { put("`group`", group) put("`key`", key) - put("data", org.apache.commons.lang3.SerializationUtilsX.serialize(value)) + put("data", SerializationUtilsX.serialize(value)) put("module_pkg_name", moduleName) put("user_id", userId.toString()) } @@ -78,6 +79,18 @@ object PreferenceStore { fun setLogWatchdog(enabled: Boolean) = updateModulePref("lspd", 0, "config", "enable_log_watchdog", enabled) + fun isStatusNotificationEnabled(): Boolean = + getModulePrefs("lspd", 0, "config")["enable_status_notification"] as? Boolean ?: true + + fun setStatusNotification(enabled: Boolean) = + updateModulePref("lspd", 0, "config", "enable_status_notification", enabled) + + fun isVerboseLogEnabled(): Boolean = + getModulePrefs("lspd", 0, "config")["enable_verbose_log"] as? Boolean ?: true + + fun setVerboseLog(enabled: Boolean) = + updateModulePref("lspd", 0, "config", "enable_verbose_log", enabled) + fun isScopeRequestBlocked(pkg: String): Boolean = (getModulePrefs("lspd", 0, "config")["scope_request_blocked"] as? Set<*>)?.contains(pkg) == true diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/CliSocketServer.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/CliSocketServer.kt new file mode 100644 index 000000000..2b548426a --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/CliSocketServer.kt @@ -0,0 +1,109 @@ +package org.matrix.vector.daemon.env + +import android.net.LocalServerSocket +import android.net.LocalSocket +import android.net.LocalSocketAddress +import android.util.Log +import java.io.DataInputStream +import java.io.DataOutputStream +import java.io.FileInputStream +import java.io.IOException +import org.matrix.vector.daemon.* +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.ipc.CliHandler + +private const val TAG = "VectorCliSocket" + +object CliSocketServer { + + private var isRunning = false + + fun start() { + if (isRunning) return + isRunning = true + + // Use a dedicated Thread for the blocking accept() loop + val serverThread = Thread { + try { + val cliSocket: String = FileSystem.setupCli() + + val vectorSocket = LocalSocket() + val address = LocalSocketAddress(cliSocket, LocalSocketAddress.Namespace.FILESYSTEM) + vectorSocket.bind(address) + val server = LocalServerSocket(vectorSocket.fileDescriptor) + + Log.d(TAG, "Cli socket server created at ${cliSocket}") + while (!Thread.currentThread().isInterrupted) { + try { + // This blocks until a command is run + val clientSocket = server.accept() + + // We handle each client in a nested try-catch. + // If a specific command crashes, we just close that socket and continue. + handleClient(clientSocket) + } catch (e: IOException) { + Log.w(TAG, "Error accepting client connection", e) + } + } + } catch (e: Exception) { + Log.e(TAG, "Fatal CLI Server error. CLI commands will be unavailable.", e) + } finally { + isRunning = false + } + } + + serverThread.name = "VectorCliListener" + serverThread.priority = Thread.MIN_PRIORITY // Run as background task + serverThread.start() + } + + private fun handleClient(socket: LocalSocket) { + try { + val input = DataInputStream(socket.inputStream) + val output = DataOutputStream(socket.outputStream) + + // Read & Verify Security Token (UUID MSB/LSB) + val msb = input.readLong() + val lsb = input.readLong() + if (msb != BuildConfig.CLI_TOKEN_MSB || lsb != BuildConfig.CLI_TOKEN_LSB) { + socket.close() + return + } + + val requestJson = input.readUTF() + val request = VectorIPC.gson.fromJson(requestJson, CliRequest::class.java) + + // Intercept Log Streaming specifically before CliHandler + if (request.command == "log" && request.action == "stream") { + val verbose = request.options["verbose"] as? Boolean ?: false + val logFile = if (verbose) LogcatMonitor.getVerboseLog() else LogcatMonitor.getModulesLog() + + if (logFile != null && logFile.exists()) { + val response = CliResponse(success = true, isFdAttached = true) + output.writeUTF(VectorIPC.gson.toJson(response)) + + // Open file and get raw FileDescriptor + val fis = FileInputStream(logFile) + val fd = fis.fd + + // Attach FD to the next write operation + socket.setFileDescriptorsForSend(arrayOf(fd)) + output.write(1) // Trigger byte to "carry" the ancillary FD data + + // fis is closed when the socket/method finishes + return + } else { + output.writeUTF( + VectorIPC.gson.toJson(CliResponse(success = false, error = "Log file not found."))) + return + } + } + + // Standard commands go to CliHandler as usual + val response = CliHandler.execute(request) + output.writeUTF(VectorIPC.gson.toJson(response)) + } finally { + socket.close() + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt index c83c064d9..e88325542 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt @@ -11,6 +11,7 @@ import org.lsposed.lspd.models.Module import org.lsposed.lspd.service.ILSPApplicationService import org.matrix.vector.daemon.data.ConfigCache import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.data.PreferenceStore import org.matrix.vector.daemon.utils.InstallerVerifier import org.matrix.vector.daemon.utils.ObfuscationManager @@ -46,14 +47,14 @@ object ApplicationService : ILSPApplicationService.Stub() { override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { when (code) { DEX_TRANSACTION_CODE -> { - val shm = FileSystem.getPreloadDex(ConfigCache.isDexObfuscateEnabled()) ?: return false + val shm = FileSystem.getPreloadDex(PreferenceStore.isDexObfuscateEnabled()) ?: return false reply?.writeNoException() reply?.let { shm.writeToParcel(it, 0) } reply?.writeLong(shm.size.toLong()) return true } OBFUSCATION_MAP_TRANSACTION_CODE -> { - val obfuscation = ConfigCache.isDexObfuscateEnabled() + val obfuscation = PreferenceStore.isDexObfuscateEnabled() val signatures = ObfuscationManager.getSignatures() reply?.writeNoException() reply?.writeInt(signatures.size * 2) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/CliHandler.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/CliHandler.kt new file mode 100644 index 000000000..380c7a3c5 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/CliHandler.kt @@ -0,0 +1,211 @@ +package org.matrix.vector.daemon.ipc + +import org.lsposed.lspd.models.Application +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.CliRequest +import org.matrix.vector.daemon.CliResponse +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.system.* + +object CliHandler { + + /** + * Executes the requested CLI command within the daemon's memory space. Returns a structured + * CliResponse. + */ + fun execute(request: CliRequest): CliResponse { + return try { + val responseData = + when (request.command) { + "status" -> handleStatus() + "modules" -> handleModules(request) + "scope" -> handleScope(request) + "config" -> handleConfig(request) + "log" -> handleLog(request) + else -> throw IllegalArgumentException("Unknown command: ${request.command}") + } + CliResponse(success = true, data = responseData) + } catch (e: Exception) { + CliResponse(success = false, error = e.message ?: "Unknown error occurred") + } + } + + private fun handleStatus(): Map { + return mapOf( + "Framework Version" to BuildConfig.VERSION_NAME, + "Version Code" to BuildConfig.VERSION_CODE, + "Enabled Modules" to ConfigCache.state.modules.size, + "Daemon Status" to "Running", + "Status Notification" to PreferenceStore.isStatusNotificationEnabled()) + } + + private fun isPackageInstalled(pkg: String, userId: Int = 0): Boolean { + return runCatching { packageManager?.getPackageInfo(pkg, 0, userId) != null } + .getOrDefault(false) + } + + private fun handleModules(request: CliRequest): Any { + return when (request.action) { + "ls" -> { + val enabledOnly = request.options["enabled"] as? Boolean ?: false + val disabledOnly = request.options["disabled"] as? Boolean ?: false + + // Get the current immutable snapshot of enabled modules + val enabledModuleKeys = ConfigCache.state.modules.keys + // Get all installed modules from the system + val installed = ConfigCache.getInstalledModules() + + // Map to the CLI view model + installed + .mapNotNull { info -> + val pkg = info.packageName + val isEnabled = enabledModuleKeys.contains(pkg) + + // Filter based on CLI flags + if (enabledOnly && !isEnabled) return@mapNotNull null + if (disabledOnly && isEnabled) return@mapNotNull null + + mapOf( + "PACKAGE" to pkg, + "UID" to info.uid, + "STATUS" to (if (isEnabled) "enabled" else "disabled")) + } + .sortedBy { it["PACKAGE"] as String } + } + "enable" -> { + if (request.targets.isEmpty()) + throw IllegalArgumentException("No packages provided to enable.") + val success = mutableListOf() + val failed = mutableListOf() + request.targets.forEach { pkg -> + if (ModuleDatabase.enableModule(pkg)) success.add(pkg) else failed.add(pkg) + } + mapOf("Enabled" to success, "Failed" to failed) + } + "disable" -> { + if (request.targets.isEmpty()) + throw IllegalArgumentException("No packages provided to disable.") + val success = mutableListOf() + val failed = mutableListOf() + request.targets.forEach { pkg -> + if (ModuleDatabase.disableModule(pkg)) success.add(pkg) else failed.add(pkg) + } + mapOf("Disabled" to success, "Failed" to failed) + } + else -> throw IllegalArgumentException("Unknown module action: ${request.action}") + } + } + + private fun handleScope(request: CliRequest): Any { + if (request.targets.isEmpty()) throw IllegalArgumentException("Module package name required.") + val modulePkg = request.targets[0] + val apps = request.targets.drop(1) + + return when (request.action) { + "ls" -> { + val scope = + ConfigCache.getModuleScope(modulePkg) + ?: throw IllegalArgumentException("Module not found: $modulePkg") + scope.map { mapOf("APP_PACKAGE" to it.packageName, "USER_ID" to it.userId) } + } + "add" -> { + if (apps.isEmpty()) throw IllegalArgumentException("No target apps provided.") + val scope = ConfigCache.getModuleScope(modulePkg) ?: mutableListOf() + + apps.forEach { appStr -> + val parts = appStr.split("/") + val pkg = parts[0] + val user = parts.getOrNull(1)?.toIntOrNull() ?: 0 + if (scope.none { it.packageName == pkg && it.userId == user }) { + scope.add( + Application().apply { + packageName = pkg + userId = user + }) + } + } + ModuleDatabase.setModuleScope(modulePkg, scope) + "Successfully appended ${apps.size} apps to $modulePkg scope." + } + "set" -> { + if (apps.isEmpty()) + throw IllegalArgumentException("No target apps provided for scope overwrite.") + val scope = mutableListOf() + apps.forEach { appStr -> + val parts = appStr.split("/") + val pkg = parts[0] + val user = parts.getOrNull(1)?.toIntOrNull() ?: 0 + scope.add( + Application().apply { + packageName = pkg + userId = user + }) + } + ModuleDatabase.setModuleScope(modulePkg, scope) + "Successfully overwrote scope for $modulePkg (${apps.size} apps)." + } + "rm" -> { + if (apps.isEmpty()) throw IllegalArgumentException("No target apps provided to remove.") + var removedCount = 0 + apps.forEach { appStr -> + val parts = appStr.split("/") + val pkg = parts[0] + val user = parts.getOrNull(1)?.toIntOrNull() ?: 0 + if (ModuleDatabase.removeModuleScope(modulePkg, pkg, user)) removedCount++ + } + "Successfully removed $removedCount apps from $modulePkg scope." + } + else -> throw IllegalArgumentException("Unknown scope action: ${request.action}") + } + } + + private fun handleConfig(request: CliRequest): Any { + val keys = request.targets + return when (request.action) { + "get" -> { + if (keys.isEmpty()) throw IllegalArgumentException("Config key required.") + val key = keys[0] + val value = + when (key) { + "dex-obfuscate" -> ManagerService.dexObfuscate + "status-notification" -> ManagerService.enableStatusNotification() + "log-watchdog" -> ManagerService.isLogWatchdogEnabled + "verbose-log" -> ManagerService.isVerboseLog + else -> throw IllegalArgumentException("Unknown config key: $key") + } + mapOf("KEY" to key, "VALUE" to value) + } + "set" -> { + if (keys.size < 2) throw IllegalArgumentException("Key and value required.") + val key = keys[0] + val value = + keys[1].toBooleanStrictOrNull() + ?: throw IllegalArgumentException("Value must be 'true' or 'false'.") + + when (key) { + "dex-obfuscate" -> ManagerService.dexObfuscate = value + "status-notification" -> ManagerService.setEnableStatusNotification(value) + "log-watchdog" -> ManagerService.setLogWatchdog(value) + "verbose-log" -> ManagerService.setVerboseLog(value) + else -> throw IllegalArgumentException("Unknown config key: $key") + } + "Successfully set $key to $value." + } + else -> throw IllegalArgumentException("Unknown config action: ${request.action}") + } + } + + private fun handleLog(request: CliRequest): Any { + return when (request.action) { + "clear" -> { + val verbose = request.options["verbose"] as? Boolean ?: false + ManagerService.clearLogs(verbose) + "Logs cleared successfully." + } + // "stream" is handled in SystemServerService.kt to attach the FileDescriptor + else -> throw IllegalArgumentException("Unknown log action: ${request.action}") + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt index 95475904b..392dca2a7 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt @@ -10,8 +10,8 @@ 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.PreferenceStore import org.matrix.vector.daemon.system.PER_USER_RANGE private const val TAG = "VectorInjectedModuleService" @@ -23,7 +23,7 @@ class InjectedModuleService(private val packageName: String) : ILSPInjectedModul override fun getFrameworkProperties(): Long { var prop = IXposedService.PROP_CAP_SYSTEM or IXposedService.PROP_CAP_REMOTE - if (ConfigCache.isDexObfuscateEnabled()) { + if (PreferenceStore.isDexObfuscateEnabled()) { prop = prop or IXposedService.PROP_RT_API_PROTECTION } return prop @@ -36,7 +36,7 @@ class InjectedModuleService(private val packageName: String) : ILSPInjectedModul val bundle = Bundle() val userId = Binder.getCallingUid() / PER_USER_RANGE bundle.putSerializable( - "map", ConfigCache.getModulePrefs(packageName, userId, group) as Serializable) + "map", PreferenceStore.getModulePrefs(packageName, userId, group) as Serializable) if (callback != null) { val groupCallbacks = callbacks.getOrPut(group) { ConcurrentHashMap.newKeySet() } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt index c6e0bcf6c..d501f56e8 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt @@ -31,9 +31,12 @@ import org.lsposed.lspd.models.UserInfo import org.matrix.vector.daemon.BuildConfig 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.env.Dex2OatServer import org.matrix.vector.daemon.env.LogcatMonitor import org.matrix.vector.daemon.system.* +import org.matrix.vector.daemon.utils.applyXspaceWorkaround import org.matrix.vector.daemon.utils.getRealUsers import rikka.parcelablelist.ParcelableListSlice @@ -41,7 +44,6 @@ private const val TAG = "VectorManagerService" object ManagerService : ILSPManagerService.Stub() { - @Volatile var _isVerboseLog = false @Volatile private var managerPid = -1 @Volatile private var pendingManager = false @Volatile private var isEnabled = true @@ -62,36 +64,7 @@ object ManagerService : ILSPManagerService.Stub() { ManagerService.guard = this runCatching { binder.linkToDeath(this, 0) - // MIUI XSpace Workaround - if (Build.MANUFACTURER.equals("xiaomi", ignoreCase = true)) { - val intent = - Intent().apply { - component = - ComponentName.unflattenFromString( - "com.miui.securitycore/com.miui.xspace.service.XSpaceService") - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - activityManager?.bindService( - SystemContext.appThread, - SystemContext.token, - intent, - intent.type, - connection, - Context.BIND_AUTO_CREATE.toLong(), - "android", - 0) - } else { - activityManager?.bindService( - SystemContext.appThread, - SystemContext.token, - intent, - intent.type, - connection, - Context.BIND_AUTO_CREATE, - "android", - 0) - } - } + applyXspaceWorkaround(connection) } .onFailure { Log.e(TAG, "ManagerGuard initialization failed", it) @@ -243,8 +216,6 @@ object ManagerService : ILSPManagerService.Stub() { override fun getXposedVersionName() = BuildConfig.VERSION_NAME - override fun getApi() = ConfigCache.api - override fun getInstalledPackagesFromAllUsers( flags: Int, filterNoProcess: Boolean @@ -253,23 +224,22 @@ object ManagerService : ILSPManagerService.Stub() { packageManager?.getInstalledPackagesForAllUsers(flags, filterNoProcess) ?: emptyList()) } - override fun enabledModules() = ConfigCache.getEnabledModules().toTypedArray() + override fun enabledModules() = ConfigCache.state.modules.keys.toTypedArray() - override fun enableModule(packageName: String) = ConfigCache.enableModule(packageName) + override fun enableModule(packageName: String) = ModuleDatabase.enableModule(packageName) - override fun disableModule(packageName: String) = ConfigCache.disableModule(packageName) + override fun disableModule(packageName: String) = ModuleDatabase.disableModule(packageName) override fun setModuleScope(packageName: String, scope: MutableList) = - ConfigCache.setModuleScope(packageName, scope) + ModuleDatabase.setModuleScope(packageName, scope) override fun getModuleScope(packageName: String) = ConfigCache.getModuleScope(packageName) - override fun isVerboseLog() = _isVerboseLog || BuildConfig.DEBUG + override fun isVerboseLog() = PreferenceStore.isVerboseLogEnabled() || BuildConfig.DEBUG override fun setVerboseLog(enabled: Boolean) { - _isVerboseLog = enabled if (isVerboseLog()) LogcatMonitor.startVerbose() else LogcatMonitor.stopVerbose() - ConfigCache.updateModulePref("lspd", 0, "config", "enable_verbose_log", enabled) + PreferenceStore.setVerboseLog(enabled) } override fun getVerboseLog() = @@ -483,29 +453,29 @@ object ManagerService : ILSPManagerService.Stub() { packageManager?.clearApplicationProfileData(packageName) } - override fun enableStatusNotification() = ConfigCache.enableStatusNotification + override fun enableStatusNotification() = PreferenceStore.isStatusNotificationEnabled() override fun setEnableStatusNotification(enable: Boolean) { - ConfigCache.enableStatusNotification = enable + PreferenceStore.setStatusNotification(enable) // NotificationManager.notifyStatusNotification() handled via observers later } override fun performDexOptMode(packageName: String) = org.matrix.vector.daemon.utils.performDexOptMode(packageName) - override fun getDexObfuscate() = ConfigCache.isDexObfuscateEnabled() + override fun getDexObfuscate() = PreferenceStore.isDexObfuscateEnabled() - override fun setDexObfuscate(enabled: Boolean) = ConfigCache.setDexObfuscate(enabled) + override fun setDexObfuscate(enabled: Boolean) = PreferenceStore.setDexObfuscate(enabled) override fun getDex2OatWrapperCompatibility() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) Dex2OatServer.compatibility else 0 - override fun setLogWatchdog(enabled: Boolean) = ConfigCache.setLogWatchdog(enabled) + override fun setLogWatchdog(enabled: Boolean) = PreferenceStore.setLogWatchdog(enabled) - override fun isLogWatchdogEnabled() = ConfigCache.isLogWatchdogEnabled() + override fun isLogWatchdogEnabled() = PreferenceStore.isLogWatchdogEnabled() override fun setAutoInclude(packageName: String, enabled: Boolean) = - ConfigCache.setAutoInclude(packageName, enabled) + ModuleDatabase.setAutoInclude(packageName, enabled) override fun getAutoInclude(packageName: String) = ConfigCache.getAutoInclude(packageName) } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt index c8434ff6a..a2b78314b 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt @@ -16,6 +16,8 @@ import org.lsposed.lspd.models.Module import org.matrix.vector.daemon.BuildConfig 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 import org.matrix.vector.daemon.system.activityManager @@ -98,7 +100,8 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() { override fun getFrameworkProperties(): Long { ensureModule() var prop = IXposedService.PROP_CAP_SYSTEM or IXposedService.PROP_CAP_REMOTE - if (ConfigCache.isDexObfuscateEnabled()) prop = prop or IXposedService.PROP_RT_API_PROTECTION + if (PreferenceStore.isDexObfuscateEnabled()) + prop = prop or IXposedService.PROP_RT_API_PROTECTION return prop } @@ -110,7 +113,7 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() { override fun requestScope(packages: List, callback: IXposedScopeCallback) { val userId = ensureModule() - if (!ConfigCache.isScopeRequestBlocked(loadedModule.packageName)) { + if (!PreferenceStore.isScopeRequestBlocked(loadedModule.packageName)) { packages.forEach { pkg -> NotificationManager.requestModuleScope(loadedModule.packageName, userId, pkg, callback) } @@ -122,7 +125,7 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() { override fun removeScope(packages: List) { val userId = ensureModule() packages.forEach { pkg -> - runCatching { ConfigCache.removeModuleScope(loadedModule.packageName, pkg, userId) } + runCatching { ModuleDatabase.removeModuleScope(loadedModule.packageName, pkg, userId) } .onFailure { Log.e(TAG, "Error removing scope for $pkg", it) } } } @@ -132,7 +135,7 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() { return Bundle().apply { putSerializable( "map", - ConfigCache.getModulePrefs(loadedModule.packageName, userId, group) as Serializable) + PreferenceStore.getModulePrefs(loadedModule.packageName, userId, group) as Serializable) } } @@ -149,14 +152,14 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() { } runCatching { - ConfigCache.updateModulePrefs(loadedModule.packageName, userId, group, values) + PreferenceStore.updateModulePrefs(loadedModule.packageName, userId, group, values) (loadedModule.service as? InjectedModuleService)?.onUpdateRemotePreferences(group, diff) } .getOrElse { throw RemoteException(it.message) } } override fun deleteRemotePreferences(group: String) { - ConfigCache.deleteModulePrefs(loadedModule.packageName, ensureModule(), group) + PreferenceStore.deleteModulePrefs(loadedModule.packageName, ensureModule(), group) } override fun listRemoteFiles(): Array { 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 27a80e131..74c94a353 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 @@ -4,10 +4,12 @@ import android.os.Build import android.os.IBinder import android.os.IServiceCallback import android.os.Parcel +import android.os.ServiceManager import android.os.SystemProperties import android.util.Log import org.lsposed.lspd.service.ILSPApplicationService import org.lsposed.lspd.service.ILSPSystemServerService +import org.matrix.vector.daemon.* import org.matrix.vector.daemon.system.getSystemServiceManager private const val TAG = "VectorSystemServer" @@ -46,7 +48,7 @@ class SystemServerService(private val maxRetry: Int, private val proxyServiceNam } fun putBinderForSystemServer() { - android.os.ServiceManager.addService(proxyServiceName, this) + ServiceManager.addService(proxyServiceName, this) binderDied() } @@ -67,6 +69,8 @@ class SystemServerService(private val maxRetry: Int, private val proxyServiceNam override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { originService?.let { + // This should however never happen, as service registration enforces later replacements + Log.i(TAG, "Original service $proxyServiceName alive, transmitting requests") return it.transact(code, data, reply, flags) } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt index f062b771b..d245b133e 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt @@ -1,6 +1,10 @@ package org.matrix.vector.daemon.utils +import android.app.IServiceConnection import android.app.Notification +import android.content.ComponentName +import android.content.Context +import android.content.Intent import android.content.pm.UserInfo import android.os.Build import android.os.IUserManager @@ -8,10 +12,11 @@ import android.util.Log import java.io.BufferedReader import java.io.InputStreamReader import java.lang.ClassNotFoundException -import org.matrix.vector.daemon.system.packageManager +import org.matrix.vector.daemon.system.* private const val TAG = "VectorWorkarounds" private val isLenovo = Build.MANUFACTURER.equals("lenovo", ignoreCase = true) +private val isXiaomi = Build.MANUFACTURER.equals("xiaomi", ignoreCase = true) fun IUserManager.getRealUsers(): List { val users = @@ -88,3 +93,35 @@ fun performDexOptMode(packageName: String): Boolean { .getOrDefault(false) } } + +fun applyXspaceWorkaround(connection: IServiceConnection) { + if (isXiaomi) { + val intent = + Intent().apply { + component = + ComponentName.unflattenFromString( + "com.miui.securitycore/com.miui.xspace.service.XSpaceService") + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + activityManager?.bindService( + SystemContext.appThread, + SystemContext.token, + intent, + intent.type, + connection, + Context.BIND_AUTO_CREATE.toLong(), + "android", + 0) + } else { + activityManager?.bindService( + SystemContext.appThread, + SystemContext.token, + intent, + intent.type, + connection, + Context.BIND_AUTO_CREATE, + "android", + 0) + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 43ee6a6e4..530c2248d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,3 +57,4 @@ hiddenapibypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", vers kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +picocli = { module = "info.picocli:picocli", version = "4.7.7" } diff --git a/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPosedService.aidl b/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPosedService.aidl index 4dff667c3..ba8c98bb1 100644 --- a/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPosedService.aidl +++ b/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPosedService.aidl @@ -5,7 +5,7 @@ import org.lsposed.lspd.service.ILSPApplicationService; interface ILSPosedService { ILSPApplicationService requestApplicationService(int uid, int pid, String processName, IBinder heartBeat); - oneway void dispatchSystemServerContext(in IBinder activityThread, in IBinder activityToken, String api); + oneway void dispatchSystemServerContext(in IBinder activityThread, in IBinder activityToken); boolean preStartManager(); diff --git a/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl b/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl index 11e2385e9..7734c22e0 100644 --- a/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl +++ b/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl @@ -12,8 +12,6 @@ interface ILSPManagerService { const int DEX2OAT_SELINUX_PERMISSIVE = 3; const int DEX2OAT_SEPOLICY_INCORRECT = 4; - String getApi() = 1; - ParcelableListSlice getInstalledPackagesFromAllUsers(int flags, boolean filterNoProcess) = 2; String[] enabledModules() = 3; diff --git a/zygisk/module/cli b/zygisk/module/cli new file mode 100644 index 000000000..f4ade4f10 --- /dev/null +++ b/zygisk/module/cli @@ -0,0 +1,27 @@ +#!/system/bin/sh + +tmpDaemonApk="/data/local/tmp/daemon.apk" + +# Safely check for debug APK and set classpath +if [ -r "$tmpDaemonApk" ]; then + java_options="-Djava.class.path=$tmpDaemonApk" +else + dex_path="" + for DEXDIR in /data/adb/modules $(magisk --path 2>/dev/null)/.magisk/modules; do + if [ -d "$DEXDIR/zygisk_vector" ]; then + dex_path="$DEXDIR/zygisk_vector" + break + fi + done + + if [ -z "$dex_path" ]; then + echo "No vector module path found" + exit 1 + fi + + dex_path="$dex_path/daemon.apk" + java_options="-Djava.class.path=$dex_path" +fi + +# Launch the cli +exec /system/bin/app_process $java_options /system/bin --nice-name=VectorCli org.matrix.vector.daemon.Cli "$@" diff --git a/zygisk/module/customize.sh b/zygisk/module/customize.sh index 7a9451dc8..44707fee7 100644 --- a/zygisk/module/customize.sh +++ b/zygisk/module/customize.sh @@ -82,7 +82,7 @@ esac ui_print "- Device platform: $ARCH ($ABI32 / $ABI64)" ui_print "- Extracting root module files" -for file in module.prop action.sh service.sh uninstall.sh sepolicy.rule framework/lspd.dex daemon.apk daemon manager.apk; do +for file in module.prop action.sh service.sh uninstall.sh sepolicy.rule framework/lspd.dex cli daemon.apk daemon manager.apk; do extract "$ZIPFILE" "$file" "$MODPATH" done @@ -132,6 +132,7 @@ set_perm_recursive "$MODPATH" 0 0 0755 0644 [ -d "$MODPATH/bin" ] && set_perm_recursive "$MODPATH/bin" 0 2000 0755 0755 u:object_r:xposed_file:s0 set_perm "$MODPATH/daemon" 0 0 0744 +set_perm "$MODPATH/cli" 0 0 0744 if [ "$(grep_prop ro.maple.enable)" = "1" ]; then ui_print "- Add ro.maple.enable=0" diff --git a/zygisk/module/daemon b/zygisk/module/daemon index cad6e9d46..faca6adef 100644 --- a/zygisk/module/daemon +++ b/zygisk/module/daemon @@ -43,4 +43,4 @@ fi [ "$debug" = "true" ] && log -p d -t "Vector" "Starting daemon $*" # Launch the daemon -exec /system/bin/app_process $java_options /system/bin --nice-name=lspd org.matrix.vector.daemon.core.VectorDaemon "$@" >/dev/null 2>&1 +exec /system/bin/app_process $java_options /system/bin --nice-name=lspd org.matrix.vector.daemon.VectorDaemon "$@" >/dev/null 2>&1 diff --git a/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt index 5aeb86a20..eca96d4c7 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt @@ -78,11 +78,7 @@ object BridgeService { val at = activityThread.applicationThread as android.app.IApplicationThread val atBinder = at.asBinder() val systemCtx = activityThread.systemContext - service?.dispatchSystemServerContext( - atBinder, - Context_getActivityToken(systemCtx), - "Zygisk", - ) + service?.dispatchSystemServerContext(atBinder, Context_getActivityToken(systemCtx)) } .onFailure { Log.e(TAG, "Failed to dispatch system context", it) } From ced370e8681e27c7812746e7bdd1f21616962086 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sat, 28 Mar 2026 21:53:52 +0100 Subject: [PATCH 32/38] Fix notification --- .../kotlin/org/matrix/vector/daemon/core/VectorService.kt | 2 +- .../kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt index e75bbcdd6..3a82e9df4 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/core/VectorService.kt @@ -201,7 +201,7 @@ object VectorService : ILSPosedService.Stub() { private fun dispatchBootCompleted() { bootCompleted = true if (PreferenceStore.isStatusNotificationEnabled()) { - NotificationManager.cancelStatusNotification() + NotificationManager.notifyStatusNotification() } } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt index d501f56e8..fbf9f56c6 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt @@ -456,8 +456,14 @@ object ManagerService : ILSPManagerService.Stub() { override fun enableStatusNotification() = PreferenceStore.isStatusNotificationEnabled() override fun setEnableStatusNotification(enable: Boolean) { + val isEnabled = enableStatusNotification() PreferenceStore.setStatusNotification(enable) - // NotificationManager.notifyStatusNotification() handled via observers later + if (isEnabled && !enable) { + NotificationManager.cancelStatusNotification() + } + if (!isEnabled && enable) { + NotificationManager.notifyStatusNotification() + } } override fun performDexOptMode(packageName: String) = From d653e69ac84c85f3a83ac61b82bbb049ec8648ec Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sat, 28 Mar 2026 23:42:04 +0100 Subject: [PATCH 33/38] Fix package filtering --- .../matrix/vector/daemon/data/ConfigCache.kt | 2 +- .../vector/daemon/ipc/ManagerService.kt | 2 +- .../vector/daemon/system/SystemExtensions.kt | 146 ++++++++++++------ 3 files changed, 104 insertions(+), 46 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt index f1a89c9dc..cfca60cc8 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt @@ -405,7 +405,7 @@ object ConfigCache { fun getInstalledModules(): List { val allPackages = - packageManager?.getInstalledPackagesForAllUsers(MATCH_ALL_FLAGS, false) ?: emptyList() + packageManager?.getInstalledPackagesFromAllUsers(MATCH_ALL_FLAGS, false) ?: emptyList() return allPackages .mapNotNull { it.applicationInfo } .filter { info -> getModuleApkPath(info) != null } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt index fbf9f56c6..a90df46ea 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt @@ -221,7 +221,7 @@ object ManagerService : ILSPManagerService.Stub() { filterNoProcess: Boolean ): ParcelableListSlice { return ParcelableListSlice( - packageManager?.getInstalledPackagesForAllUsers(flags, filterNoProcess) ?: emptyList()) + packageManager?.getInstalledPackagesFromAllUsers(flags, filterNoProcess) ?: emptyList()) } override fun enabledModules() = ConfigCache.state.modules.keys.toTypedArray() diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt index 9a5d687a7..971025c38 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt @@ -14,7 +14,9 @@ import android.content.pm.ServiceInfo import android.os.Build import android.os.IUserManager import android.util.Log +import java.io.File import java.lang.reflect.Method +import java.util.stream.Collectors import org.matrix.vector.daemon.utils.getRealUsers private const val TAG = "VectorSystem" @@ -27,6 +29,23 @@ const val MATCH_ALL_FLAGS = PackageManager.MATCH_UNINSTALLED_PACKAGES or MATCH_ANY_USER +/** + * Internal helper that throws exceptions instead of swallowing them. This is crucial for detecting + * TransactionTooLargeException (Binder limits). + */ +@Throws(Exception::class) +private fun IPackageManager.getPackageInfoCompatThrows( + packageName: String, + flags: Int, + userId: Int +): PackageInfo? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getPackageInfo(packageName, flags.toLong(), userId) + } else { + getPackageInfo(packageName, flags, userId) + } +} + /** Safely fetches PackageInfo, handling API level differences. */ fun IPackageManager.getPackageInfoCompat( packageName: String, @@ -34,20 +53,29 @@ fun IPackageManager.getPackageInfoCompat( userId: Int ): PackageInfo? { return try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getPackageInfo(packageName, flags.toLong(), userId) - } else { - getPackageInfo(packageName, flags, userId) - } + getPackageInfoCompatThrows(packageName, flags, userId) } catch (e: Exception) { null } } /** - * Fetches PackageInfo alongside its components (Activities, Services, Receivers, Providers). - * Includes a fallback mechanism to prevent TransactionTooLargeException on massive apps. + * Checks if the package is truly available for the given user. Apps can be "installed" but + * disabled/hidden by profile owners. */ +fun IPackageManager.isPackageAvailable( + packageName: String, + userId: Int, + ignoreHidden: Boolean +): Boolean { + return runCatching { + isPackageAvailable(packageName, userId) || + (ignoreHidden && getApplicationHiddenSettingAsUser(packageName, userId)) + } + .getOrDefault(false) +} + +/** Fetches PackageInfo alongside its components (Activities, Services, Receivers, Providers). */ fun IPackageManager.getPackageInfoWithComponents( packageName: String, flags: Int, @@ -60,33 +88,55 @@ fun IPackageManager.getPackageInfoWithComponents( PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS - // Fast path: Try fetching everything at once - getPackageInfoCompat(packageName, fullFlags, userId)?.let { - return it - } + var pkgInfo: PackageInfo? = null - // Fallback path: Fetch sequentially to avoid Binder Transaction limits - val baseInfo = getPackageInfoCompat(packageName, flags, userId) ?: return null + try { + // If the binder buffer overflows, it will throw an exception here. + pkgInfo = getPackageInfoCompatThrows(packageName, fullFlags, userId) + } catch (e: Exception) { + // Fallback path: Fetch sequentially if the initial query threw an Exception + pkgInfo = + try { + getPackageInfoCompatThrows(packageName, flags, userId) + } catch (ignored: Exception) { + null + } - runCatching { - baseInfo.activities = - getPackageInfoCompat(packageName, flags or PackageManager.GET_ACTIVITIES, userId) - ?.activities - } - runCatching { - baseInfo.services = - getPackageInfoCompat(packageName, flags or PackageManager.GET_SERVICES, userId)?.services - } - runCatching { - baseInfo.receivers = - getPackageInfoCompat(packageName, flags or PackageManager.GET_RECEIVERS, userId)?.receivers + if (pkgInfo != null) { + runCatching { + pkgInfo.activities = + getPackageInfoCompatThrows(packageName, flags or PackageManager.GET_ACTIVITIES, userId) + ?.activities + } + runCatching { + pkgInfo.services = + getPackageInfoCompatThrows(packageName, flags or PackageManager.GET_SERVICES, userId) + ?.services + } + runCatching { + pkgInfo.receivers = + getPackageInfoCompatThrows(packageName, flags or PackageManager.GET_RECEIVERS, userId) + ?.receivers + } + runCatching { + pkgInfo.providers = + getPackageInfoCompatThrows(packageName, flags or PackageManager.GET_PROVIDERS, userId) + ?.providers + } + } } - runCatching { - baseInfo.providers = - getPackageInfoCompat(packageName, flags or PackageManager.GET_PROVIDERS, userId)?.providers + + if (pkgInfo?.applicationInfo == null) return null + if (pkgInfo.packageName != "android") { + val sourceDir = pkgInfo.applicationInfo?.sourceDir + if (sourceDir == null || + !File(sourceDir).exists() || + !isPackageAvailable(packageName, userId, true)) { + return null + } } - return baseInfo + return pkgInfo } /** Extracts all unique process names associated with a package's components. */ @@ -148,10 +198,7 @@ private val getInstalledPackagesMethod: Method? by lazy { ?.apply { isAccessible = true } } -/** - * Reflectively calls getInstalledPackages and casts to ParceledListSlice. This works on Android 17+ - * because PackageInfoList extends ParceledListSlice. - */ +/** Reflectively calls getInstalledPackages and casts to ParceledListSlice. */ private fun IPackageManager.getInstalledPackagesReflect( flags: Any, userId: Int @@ -164,11 +211,12 @@ private fun IPackageManager.getInstalledPackagesReflect( .getOrNull() ?: emptyList() } -fun IPackageManager.getInstalledPackagesForAllUsers( +fun IPackageManager.getInstalledPackagesFromAllUsers( flags: Int, filterNoProcess: Boolean ): List { val result = mutableListOf() + // Assuming userManager is available in this scope as in original code val users = userManager?.getRealUsers() ?: emptyList() for (user in users) { @@ -179,20 +227,30 @@ fun IPackageManager.getInstalledPackagesForAllUsers( val infos = getInstalledPackagesReflect(flagParam, user.id) if (infos.isEmpty()) continue - result.addAll( - infos.filter { - it.applicationInfo != null && it.applicationInfo!!.uid / PER_USER_RANGE == user.id - }) + val validUserApps = + infos + .parallelStream() + .filter { + it.applicationInfo != null && (it.applicationInfo!!.uid / PER_USER_RANGE) == user.id + } + .filter { isPackageAvailable(it.packageName, user.id, true) } + .collect(Collectors.toList()) + + result.addAll(validUserApps) } if (filterNoProcess) { - return result.filter { - getPackageInfoWithComponents( - it.packageName, MATCH_ALL_FLAGS, it.applicationInfo!!.uid / PER_USER_RANGE) - ?.fetchProcesses() - ?.isNotEmpty() == true - } + return result + .parallelStream() + .filter { + getPackageInfoWithComponents( + it.packageName, MATCH_ALL_FLAGS, it.applicationInfo!!.uid / PER_USER_RANGE) + ?.fetchProcesses() + ?.isNotEmpty() == true + } + .collect(Collectors.toList()) } + return result } From 5fa67078aed864f65000dc8174616b7df594a188 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 29 Mar 2026 00:12:09 +0100 Subject: [PATCH 34/38] Fix modules loading --- .../vector/daemon/ipc/ApplicationService.kt | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt index e88325542..1016da143 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt @@ -88,7 +88,7 @@ object ApplicationService : ILSPApplicationService.Stub() { return info } - override fun getModulesList(): List { + private fun getAllModules(): List { val info = ensureRegistered() if (info.key.uid == Process.SYSTEM_UID && info.processName == "system") { return ConfigCache.getModulesForSystemServer() @@ -96,17 +96,12 @@ object ApplicationService : ILSPApplicationService.Stub() { if (ManagerService.isRunningManager(getCallingPid(), info.key.uid)) { return emptyList() } - return ConfigCache.getModulesForProcess(info.processName, info.key.uid).filter { - !it.file.legacy - } + return ConfigCache.getModulesForProcess(info.processName, info.key.uid) } - override fun getLegacyModulesList(): List { - val info = ensureRegistered() - return ConfigCache.getModulesForProcess(info.processName, info.key.uid).filter { - it.file.legacy - } - } + override fun getModulesList() = getAllModules().filter { !it.file.legacy } + + override fun getLegacyModulesList() = getAllModules().filter { it.file.legacy } override fun isLogMuted(): Boolean = !ManagerService.isVerboseLog From 522f86fa8f81afd410f4e82db66c6d9f2439a5dd Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 29 Mar 2026 01:05:02 +0100 Subject: [PATCH 35/38] Force socket listening --- .../vector/daemon/env/CliSocketServer.kt | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/CliSocketServer.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/CliSocketServer.kt index 2b548426a..3ea1a507a 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/CliSocketServer.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/CliSocketServer.kt @@ -3,16 +3,18 @@ package org.matrix.vector.daemon.env import android.net.LocalServerSocket import android.net.LocalSocket import android.net.LocalSocketAddress +import android.system.Os import android.util.Log import java.io.DataInputStream import java.io.DataOutputStream +import java.io.File import java.io.FileInputStream import java.io.IOException import org.matrix.vector.daemon.* import org.matrix.vector.daemon.data.FileSystem import org.matrix.vector.daemon.ipc.CliHandler -private const val TAG = "VectorCliSocket" +private const val TAG = "VectorCliSever" object CliSocketServer { @@ -22,38 +24,56 @@ object CliSocketServer { if (isRunning) return isRunning = true - // Use a dedicated Thread for the blocking accept() loop val serverThread = Thread { + // Keep these references outside the loop to prevent GC from closing them + var rootSocket: LocalSocket? = null + var server: LocalServerSocket? = null + var socketFile: File? = null + try { - val cliSocket: String = FileSystem.setupCli() + val cliSocketPath: String = FileSystem.setupCli() + socketFile = File(cliSocketPath) + + // Create a standard LocalSocket + rootSocket = LocalSocket() + // Bind it to the filesystem path + val address = LocalSocketAddress(cliSocketPath, LocalSocketAddress.Namespace.FILESYSTEM) + rootSocket.bind(address) + + // LocalServerSocket(FileDescriptor) requires the FD to already be listening. + Os.listen(rootSocket.fileDescriptor, 50) + // Wrap the underlying FileDescriptor into a ServerSocket + server = LocalServerSocket(rootSocket.fileDescriptor) - val vectorSocket = LocalSocket() - val address = LocalSocketAddress(cliSocket, LocalSocketAddress.Namespace.FILESYSTEM) - vectorSocket.bind(address) - val server = LocalServerSocket(vectorSocket.fileDescriptor) + Log.d(TAG, "CLI server started at $cliSocketPath") - Log.d(TAG, "Cli socket server created at ${cliSocket}") while (!Thread.currentThread().isInterrupted) { try { - // This blocks until a command is run val clientSocket = server.accept() - - // We handle each client in a nested try-catch. - // If a specific command crashes, we just close that socket and continue. handleClient(clientSocket) } catch (e: IOException) { - Log.w(TAG, "Error accepting client connection", e) + if (Thread.currentThread().isInterrupted) break + Log.w(TAG, "Error accepting client", e) } } } catch (e: Exception) { - Log.e(TAG, "Fatal CLI Server error. CLI commands will be unavailable.", e) + Log.e(TAG, "Fatal CLI Server error", e) } finally { + try { + server?.close() + rootSocket?.close() + } catch (ignored: Exception) {} + + if (socketFile?.exists() == true) { + socketFile.delete() + } isRunning = false + Log.d(TAG, "CLI server stopped") } } serverThread.name = "VectorCliListener" - serverThread.priority = Thread.MIN_PRIORITY // Run as background task + serverThread.priority = Thread.MIN_PRIORITY serverThread.start() } From 38e6c6dd390682dc46bb8706aeb16a19df5c38e9 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 29 Mar 2026 01:20:53 +0100 Subject: [PATCH 36/38] Force IContentProvider.call signature --- .../org/matrix/vector/daemon/ipc/ModuleService.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt index a2b78314b..aca2cacb8 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt @@ -1,5 +1,6 @@ package org.matrix.vector.daemon.ipc +import android.content.AttributionSource import android.os.Binder import android.os.Build import android.os.Bundle @@ -68,7 +69,16 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() { val extra = Bundle().apply { putBinder("binder", asBinder()) } val reply: Bundle? = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + provider.call( + AttributionSource.Builder(1000).setPackageName("android").build(), + authority, + SEND_BINDER, + null, + extra) + } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + provider.call("android", null, authority, SEND_BINDER, null, extra) + } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { provider.call("android", authority, SEND_BINDER, null, extra) } else { provider.call("android", SEND_BINDER, null, extra) From ec23fd8f6f14342272d48cfac88b1db4e8a69e26 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 29 Mar 2026 21:27:27 +0200 Subject: [PATCH 37/38] Fix comments --- daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt | 1 - 1 file changed, 1 deletion(-) 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 83a3b5cb2..96eedb846 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt @@ -59,7 +59,6 @@ object VectorDaemon { } CliSocketServer.start() - // Accessing the object triggers the `init` block, reading SQLite instantly. if (PreferenceStore.isLogWatchdogEnabled()) LogcatMonitor.enableWatchdog() // Preload Framework DEX in the background CoroutineScope(Dispatchers.IO).launch { From a099efe752e67f2c0c9707679cba49f90113eba0 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 30 Mar 2026 00:22:13 +0200 Subject: [PATCH 38/38] Force using readonly database for getModulePrefs --- .../vector/daemon/data/PreferenceStore.kt | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt index dfd11579a..70760d00d 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt @@ -8,23 +8,30 @@ object PreferenceStore { fun getModulePrefs(packageName: String, userId: Int, group: String): Map { val result = mutableMapOf() - ConfigCache.dbHelper.readableDatabase - .query( - "configs", - arrayOf("`key`", "data"), - "module_pkg_name = ? AND user_id = ? AND `group` = ?", - arrayOf(packageName, userId.toString(), group), - null, - null, - null) - .use { cursor -> - while (cursor.moveToNext()) { - val key = cursor.getString(0) - val blob = cursor.getBlob(1) - val obj = SerializationUtilsX.deserialize(blob) - if (obj != null) result[key] = obj + val db = + runCatching { + SQLiteDatabase.openDatabase( + FileSystem.dbPath.absolutePath, null, SQLiteDatabase.OPEN_READONLY) + } + .getOrNull() ?: return result + db.use { + it.query( + "configs", + arrayOf("`key`", "data"), + "module_pkg_name = ? AND user_id = ? AND `group` = ?", + arrayOf(packageName, userId.toString(), group), + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + val key = cursor.getString(0) + val blob = cursor.getBlob(1) + val obj = SerializationUtilsX.deserialize(blob) + if (obj != null) result[key] = obj + } } - } + } return result }