Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -396,6 +396,7 @@ object ConfigCache {

val cached = currentState.modules[pkgName]
if (cached != null) {
stageNativeLibrariesFor(cached)
modules.add(cached)
return@forEach
}
Expand DownExpand Up@@ -431,14 +432,35 @@ object ConfigCache {

FileSystem.loadModule(apkPath, state.isDexObfuscateEnabled).apkOrNull?.let {
module.file = it
stageNativeLibrariesFor(module)
modules.add(module)
// We intentionally don't mutate state.modules here. Cache update will catch it.
}
}
}
FileSystem.pruneStagedNativeLibraries(
state.miscPath, modules.mapTo(mutableSetOf()) { it.packageName })
return modules
}

/**
* Hands a module bound for system_server the copy of its native libraries it can actually map.
*
* Only that scope needs one. Every other process may execute straight out of /data/app, so the
* in-APK entries the loader already builds serve them, and staging for them would buy nothing but
* disk. A module that ships no library, or whose staging failed, keeps a null here and loads
* exactly as it did before.
*/
private fun stageNativeLibrariesFor(module: Module) {
val file = module.file ?: return
// system_server asks for its modules early enough that the cache may not have been built yet,
// and this is the same reason getPrefsPath does not trust the field either.
setupMiscPath()
val misc = state.miscPath ?: return
file.nativeLibraryDir =
FileSystem.stageNativeLibraries(misc, module.packageName, module.apkPath)
}

fun getModuleApkPath(info: ApplicationInfo): String? {
val apks = mutableListOf<String>()
info.sourceDir?.let { apks.add(it) }
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ package org.matrix.vector.daemon.data
import android.content.res.AssetManager
import android.content.res.Resources
import android.os.Binder
import android.os.Build
import android.os.ParcelFileDescriptor
import android.os.Process
import android.os.RemoteException
Expand DownExpand Up@@ -399,6 +400,102 @@ object FileSystem {
return path
}

/**
* Copies a module's native libraries out of its APK into a directory this framework owns, and
* answers with that directory.
*
* A module loaded into system_server cannot dlopen a library straight out of its own APK.
* Everything under /data/app is apk_data_file, and while system_server may read and map such a
* file it may not execute it; AOSP says why in so many words - "Executable files in /data are a
* persistence vector" - and forbids granting it. Every app domain does hold that permission,
* which is the whole reason the same module loads the same library without trouble in an ordinary
* process and fails only in system_server.
*
* The way past it is not a new rule but the one this module already ships. xposed_data is a type
* we declare ourselves, outside the data_file_type attribute that neverallow is written against,
* and `allow * xposed_data {file dir} *` already reaches every domain - system_server included.
* A copy placed under it is one system_server may map executable. Extraction has a second
* benefit: the library ends up at offset zero of an ordinary file, so it no longer has to be
* stored uncompressed and page-aligned inside the APK to be mappable at all.
*
* Note that this deliberately does not consult moduleLibraryNames. That list only names the
* libraries whose native_init we are asked to call, and a module is free to load its own
* libraries without declaring any - the module that prompted all this does exactly that.
*
* Returns null when the module ships nothing for this ABI or the copy failed, in which case the
* module still loads and only its native part fails, exactly as it does today.
*/
fun stageNativeLibraries(root: Path, packageName: String, apkPath: String): String? =
runCatching {
val apk = File(apkPath)
val dir = root.resolve("lib").resolve(packageName)

// Re-extract only when the APK behind the copy changed. Getting this wrong in the
// lenient direction would leave system_server running a module's superseded native
// code, so the framework's own version is part of the stamp as well.
val stamp = "${apk.length()}:${apk.lastModified()}:${BuildConfig.VERSION_CODE}"
val stampFile = dir.resolve(".stamp").toFile()
if (stampFile.isFile && stampFile.readText() == stamp) return@runCatching dir.toString()

val abis =
if (Process.is64Bit()) Build.SUPPORTED_64_BIT_ABIS else Build.SUPPORTED_32_BIT_ABIS

ZipFile(apk).use { zip ->
val libraries =
zip.entries().asSequence().filter { !it.isDirectory && it.name.endsWith(".so") }
.toList()
// A module built for several ABIs keeps them in sibling directories, and only the one
// this process could load is worth copying.
val abi =
abis.firstOrNull { abi -> libraries.any { it.name.startsWith("lib/$abi/") } }
?: return@runCatching null

dir.toFile().deleteRecursively()
Files.createDirectories(dir)

libraries
.filter { it.name.startsWith("lib/$abi/") }
.forEach { entry ->
val target = dir.resolve(entry.name.substringAfterLast('/'))
zip.getInputStream(entry).use { input ->
Files.newOutputStream(target).use { input.copyTo(it) }
}
Os.chmod(target.toString(), "644".toInt(8))
}

stampFile.writeText(stamp)
// The daemon runs with a zero umask, so every mode here is set rather than inherited.
Os.chmod(stampFile.absolutePath, "644".toInt(8))
// The misc root is searchable but not listable; the staged tree keeps that shape, and
// the label is what actually decides whether system_server may map these files.
Os.chmod(dir.parent.toString(), "711".toInt(8))
Os.chmod(dir.toString(), "711".toInt(8))
setSelinuxContextRecursive(dir, "u:object_r:xposed_data:s0")
SELinux.setFileContext(dir.parent.toString(), "u:object_r:xposed_data:s0")
dir.toString()
}
}
.onFailure { Log.e(TAG, "Failed to stage the native libraries of $packageName", it) }
.getOrNull()

/**
* Drops staged libraries belonging to modules that are no longer bound for system_server, so an
* uninstalled or rescoped module does not leave a copy of its native code behind for good.
*/
fun pruneStagedNativeLibraries(root: Path?, keep: Set<String>) {
if (root == null) return
runCatching {
val libRoot = root.resolve("lib")
if (!libRoot.isDirectory()) return
Files.list(libRoot).use { stream ->
stream
.filter { it.fileName.toString() !in keep }
.forEach { it.toFile().deleteRecursively() }
}
}
.onFailure { Log.e(TAG, "Failed to prune staged native libraries", it) }
}

fun toGlobalNamespace(path: String): File {
return if (path.startsWith("/")) File("/proc/1/root", path) else File("/proc/1/root/$path")
}
Expand Down
7 changes: 7 additions & 0 deletions legacy/src/main/java/de/robv/android/xposed/XposedInit.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -291,6 +291,13 @@ private static boolean loadModule(String name, String apk, PreLoadedApk file) {
Log.v(TAG, "Loading legacy module " + name + " from " + apk);

var sb = new StringBuilder();
// In system_server the in-APK entries below can only ever be refused: /data/app is
// apk_data_file, which that domain may read and map but never execute. The daemon stages a
// copy under a label we own for exactly this reason, and it has to come first, because
// findLibrary answers with the first candidate it can open.
if (startsSystemServer && file.nativeLibraryDir != null) {
sb.append(file.nativeLibraryDir).append(File.pathSeparator);
}
var abis = Process.is64Bit() ? Build.SUPPORTED_64_BIT_ABIS : Build.SUPPORTED_32_BIT_ABIS;
for (String abi : abis) {
sb.append(apk).append("!/lib/").append(abi).append(File.pathSeparator);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,4 +8,8 @@ parcelable PreLoadedApk {
// module.prop 'exceptionMode', normalised by the daemon. false, the value an absent key
// parses to, is PROTECTIVE - what ExceptionMode.DEFAULT is specified to fall back to.
boolean exceptionPassthrough;
// Where the daemon staged this module's native libraries, for the one process that cannot map
// them out of the APK. Null when the module ships none for this ABI, when staging failed, or
// when the module was never destined for system_server in the first place.
@nullable String nativeLibraryDir;
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,6 +30,15 @@ object VectorModuleManager {

// Construct the native library search path
val librarySearchPath = buildString {
// In system_server the in-APK entries below can only ever be refused: /data/app is
// apk_data_file, which that domain may read and map but never execute. The daemon
// stages a copy under a label we own for exactly this reason, and it has to come
// first, because findLibrary answers with the first candidate it can open.
if (isSystemServer) {
module.file.nativeLibraryDir?.let {
append(it).append(File.pathSeparator)
}
}
val abis =
if (Process.is64Bit()) Build.SUPPORTED_64_BIT_ABIS
else Build.SUPPORTED_32_BIT_ABIS
Expand DownExpand Up@@ -68,6 +77,14 @@ object VectorModuleManager {
else ExceptionMode.PROTECTIVE,
)

// Register any native JNI entrypoints declared by the module. This has to happen before
// the entry classes run: a module is free to load its libraries from its constructor or
// from onModuleLoaded, and an entrypoint recorded afterwards is one the dlopen hook has
// already missed. The legacy loader has always done it in this order.
module.file.moduleLibraryNames.forEach { libraryName ->
NativeAPI.recordNativeEntrypoint(libraryName)
}

// Instantiate the module entry classes
for (className in module.file.moduleClassNames) {
runCatching {
Expand DownExpand Up@@ -101,11 +118,6 @@ object VectorModuleManager {
.onFailure { e -> Log.e(TAG, "Failed to instantiate class $className", e) }
}

// Register any native JNI entrypoints declared by the module
module.file.moduleLibraryNames.forEach { libraryName ->
NativeAPI.recordNativeEntrypoint(libraryName)
}

Log.d(TAG, "Loaded module ${module.packageName} successfully.")
return true
} catch (e: Throwable) {
Expand Down
Loading