diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index 42492fa11c..96c7c28588 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -22,6 +22,13 @@ namespace { std::once_flag init_flag; +// What is rewritten, and the whole of what is rewritten: the string table of a dex. A module that +// names one of these prefixes anywhere a dex writer cannot reach keeps the original name and stops +// matching the one its classes now carry. A native library is the case that occurs: the JNI +// signature strings compiled into it still read Lde/robv/android/xposed/..., so RegisterNatives +// finds no method of that shape and the module loads without its native entry points. Nothing here +// can fix that -- the library is the module's own build output -- which is why +// `enable_dex_obfuscate` is a setting a user can turn off rather than a build type. std::map signatures = { {"Lde/robv/android/xposed/", ""}, {"Landroid/app/AndroidApp", ""}, {"Landroid/content/res/XRes", ""}, {"Landroid/content/res/XModule", ""}, diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/Cli.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/Cli.kt index 8786ab82e3..d7e841cb7a 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/Cli.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/Cli.kt @@ -314,7 +314,16 @@ class ScopeCommand { } } -@Command(name = "config", description = ["Manage daemon preferences natively"]) +@Command( + name = "config", + description = + [ + "Manage daemon preferences natively", + "Keys: status-notification, verbose-log, dex-obfuscation.", + "dex-obfuscation renames the legacy Xposed API classes in the framework dex", + "and in every module dex; a module whose native code names those classes", + "cannot bind its methods while it is on. Takes effect on the next boot.", + ]) class ConfigCommand { @ParentCommand lateinit var parent: Cli 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 b79aa11817..454a382fe8 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 @@ -63,6 +63,21 @@ object ConfigCache { } } applySqliteHelperWorkaround() + + // After the workaround, which has to be in place before the configuration database is opened, + // and this read may well be what opens it. Caught, because a daemon that cannot read its own + // preference must still start: the fallback in `DaemonState` is the build-type default this + // setting used to be. + // + // Read once and held, not consulted per use. `FileSystem.getPreloadDex` caches the framework + // dex the first time it is asked for, so the answer that produced it is the only one the rest + // of this process may act on; a module loaded with the other answer would look for class names + // the framework does not have. Writing the preference therefore takes effect on the next boot. + runCatching { PreferenceStore.isDexObfuscateEnabled() } + .onSuccess { synchronized(this) { state = state.copy(isDexObfuscateEnabled = it) } } + .onFailure { + Log.w(TAG, "Cannot read the dex obfuscation preference; using the default", it) + } } private fun ensureCacheReady() { 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 fe4a4e5e5b..01623254b2 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 @@ -11,7 +11,15 @@ data class ProcessScope(val processName: String, val uid: Int) * and atomically swap the reference. */ data class DaemonState( - // State non configurable for users + /** + * Whether the legacy API classes are renamed, seeded from + * [PreferenceStore.isDexObfuscateEnabled] when [ConfigCache] is first touched. + * + * The default here is only what a daemon that cannot read its own configuration falls back to. + * It used to be the entire setting, which is why it was marked as not configurable: the + * framework dex and every module dex were obfuscated in a release build and not in a debug + * one, with nothing able to say otherwise. + */ val isDexObfuscateEnabled: Boolean = !BuildConfig.DEBUG, // States initialized after system services are ready val isCacheReady: Boolean = 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 bf1f4905f5..7fd1e48613 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 @@ -3,6 +3,7 @@ package org.matrix.vector.daemon.data import android.content.ContentValues import android.database.sqlite.SQLiteDatabase import org.apache.commons.lang3.SerializationUtilsX +import org.matrix.vector.daemon.BuildConfig private const val TAG = "VectorPreferenceStore" @@ -97,6 +98,21 @@ object PreferenceStore { fun setVerboseLog(enabled: Boolean) = updateModulePref("lspd", 0, "config", "enable_verbose_log", enabled) + /** + * Whether the legacy Xposed API classes are renamed in the framework dex and in every module + * dex, under the key LSPosed wrote and this daemon has only ever read a build-type default for. + * + * A release build obfuscates and a debug build does not, which is the default below and was the + * whole of it: the row existed in the database, nothing read it, and the manager and the CLI + * offered no way to write it. The difference is not cosmetic — it decides whether a module whose + * native code names those classes can bind its methods at all — so it is a setting. + */ + fun isDexObfuscateEnabled(): Boolean = + getModulePrefs("lspd", 0, "config")["enable_dex_obfuscate"] as? Boolean ?: !BuildConfig.DEBUG + + fun setDexObfuscate(enabled: Boolean) = + updateModulePref("lspd", 0, "config", "enable_dex_obfuscate", 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/ipc/CliHandler.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/CliHandler.kt index ffedb0e9f3..796acd6fad 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/CliHandler.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/CliHandler.kt @@ -186,6 +186,11 @@ object CliHandler { when (key) { "status-notification" -> ManagerService.isStatusNotificationEnabled() "verbose-log" -> ManagerService.isVerboseLogEnabled() + // What the stored preference says, which after a write is not yet what this daemon + // is doing. `ConfigCache.state.isDexObfuscateEnabled` is what it is doing, and the + // two disagree until the next boot; reporting the setting is what lets a reader see + // that their write landed. + "dex-obfuscation" -> PreferenceStore.isDexObfuscateEnabled() else -> throw IllegalArgumentException("Unknown config key: $key") } mapOf("KEY" to key, "VALUE" to value) @@ -200,9 +205,15 @@ object CliHandler { when (key) { "status-notification" -> ManagerService.setStatusNotificationEnabled(value) "verbose-log" -> ManagerService.setVerboseLogEnabled(value) + "dex-obfuscation" -> PreferenceStore.setDexObfuscate(value) else -> throw IllegalArgumentException("Unknown config key: $key") } - "Successfully set $key to $value." + if (key == "dex-obfuscation" && value != ConfigCache.state.isDexObfuscateEnabled) { + "Set $key to $value. The framework dex this daemon handed out names its classes the " + + "other way, so reboot to apply." + } else { + "Successfully set $key to $value." + } } else -> throw IllegalArgumentException("Unknown config action: ${request.action}") } diff --git a/zygisk/src/main/cpp/ipc_bridge.cpp b/zygisk/src/main/cpp/ipc_bridge.cpp index 2cd6bf8089..5d48faf0de 100644 --- a/zygisk/src/main/cpp/ipc_bridge.cpp +++ b/zygisk/src/main/cpp/ipc_bridge.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -469,7 +470,14 @@ std::map IPCBridge::FetchObfuscationMap(JNIEnv *env, j result_map[key_str.get()] = val_str.get(); } - LOGV("Fetched obfuscation map with {} entries.", result_map.size()); + // Whether the names actually differ, and not merely how many arrived. The daemon answers with + // every prefix either way and maps each to itself when obfuscation is off, so a count alone + // reads the same in both configurations -- which is the one thing a reader looking at a module + // that will not bind its native methods needs to tell apart. + const bool renamed = std::any_of(result_map.begin(), result_map.end(), + [](const auto &entry) { return entry.first != entry.second; }); + LOGV("Fetched obfuscation map with {} entries, dex obfuscation {}.", result_map.size(), + renamed ? "on" : "off"); return result_map; }