diff --git a/app/src/main/aidl/com/topjohnwu/magisk/core/utils/IRootUtils.aidl b/app/src/main/aidl/com/topjohnwu/magisk/core/utils/IRootUtils.aidl new file mode 100644 index 000000000..3f32dd5d3 --- /dev/null +++ b/app/src/main/aidl/com/topjohnwu/magisk/core/utils/IRootUtils.aidl @@ -0,0 +1,8 @@ +// IRootUtils.aidl +package com.topjohnwu.magisk.core.utils; + +// Declare any non-default types here with import statements + +interface IRootUtils { + android.app.ActivityManager.RunningAppProcessInfo getAppProcess(int pid); +} diff --git a/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/BaseDao.kt b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/BaseDao.kt deleted file mode 100644 index 462f81e64..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/BaseDao.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.topjohnwu.magisk.core.magiskdb - -import androidx.annotation.StringDef - -abstract class BaseDao { - - object Table { - const val POLICY = "policies" - const val LOG = "logs" - const val SETTINGS = "settings" - const val STRINGS = "strings" - } - - @StringDef(Table.POLICY, Table.LOG, Table.SETTINGS, Table.STRINGS) - @Retention(AnnotationRetention.SOURCE) - annotation class TableStrict - - @TableStrict - abstract val table: String - - inline fun buildQuery(builder: Builder.() -> Unit = {}) = - Builder::class.java.newInstance() - .apply { table = this@BaseDao.table } - .apply(builder) - .toString() - .let { Query(it) } - -} diff --git a/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/MagiskDB.kt b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/MagiskDB.kt new file mode 100644 index 000000000..6ddafc1d4 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/MagiskDB.kt @@ -0,0 +1,47 @@ +package com.topjohnwu.magisk.core.magiskdb + +import com.topjohnwu.magisk.ktx.await +import com.topjohnwu.superuser.Shell +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +open class MagiskDB { + + suspend fun exec( + query: String, + mapper: suspend (Map) -> R + ): List { + return withContext(Dispatchers.IO) { + val out = Shell.cmd("magisk --sqlite '$query'").await().out + out.map { line -> + line.split("\\|".toRegex()) + .map { it.split("=", limit = 2) } + .filter { it.size == 2 } + .associate { it[0] to it[1] } + .let { mapper(it) } + } + } + } + + suspend inline fun exec(query: String) { + exec(query) {} + } + + fun Map.toQuery(): String { + val keys = this.keys.joinToString(",") + val values = this.values.joinToString(",") { + when (it) { + is Boolean -> if (it) "1" else "0" + is Number -> it.toString() + else -> "\"$it\"" + } + } + return "($keys) VALUES($values)" + } + + object Table { + const val POLICY = "policies" + const val SETTINGS = "settings" + const val STRINGS = "strings" + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/PolicyDao.kt b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/PolicyDao.kt index a7d56567a..42bd611c9 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/PolicyDao.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/PolicyDao.kt @@ -2,61 +2,48 @@ package com.topjohnwu.magisk.core.magiskdb import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.model.su.SuPolicy -import com.topjohnwu.magisk.core.model.su.toPolicy +import com.topjohnwu.magisk.core.model.su.createPolicy import com.topjohnwu.magisk.di.AppContext -import com.topjohnwu.magisk.ktx.now -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.TimeUnit +class PolicyDao : MagiskDB() { -class PolicyDao : BaseDao() { - - override val table: String = Table.POLICY - - suspend fun deleteOutdated() = buildQuery { - condition { - greaterThan("until", "0") - and { - lessThan("until", TimeUnit.MILLISECONDS.toSeconds(now).toString()) - } - or { - lessThan("until", "0") - } - } - }.commit() - - suspend fun delete(uid: Int) = buildQuery { - condition { - equals("uid", uid) - } - }.commit() - - suspend fun fetch(uid: Int) = buildQuery { - condition { - equals("uid/100000", Const.USER_ID) - } - }.query { - it.toPolicyOrNull()?.let(mapper) + suspend fun deleteOutdated() { + val nowSeconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + val query = "DELETE FROM ${Table.POLICY} WHERE " + + "(until > 0 AND until < $nowSeconds) OR until < 0" + exec(query) } - private fun Map.toPolicyOrNull(): SuPolicy? { - return runCatching { toPolicy(AppContext.packageManager) }.getOrElse { - Timber.w(it) - val uid = getOrElse("uid") { return null } - GlobalScope.launch { delete(uid.toInt()) } - null + suspend fun delete(uid: Int) { + val query = "DELETE FROM ${Table.POLICY} WHERE uid == $uid" + exec(query) + } + + suspend fun fetch(uid: Int): SuPolicy? { + val query = "SELECT * FROM ${Table.POLICY} WHERE uid == $uid LIMIT = 1" + return exec(query) { it.toPolicyOrNull() }.firstOrNull() + } + + suspend fun update(policy: SuPolicy) { + val query = "REPLACE INTO ${Table.POLICY} ${policy.toMap().toQuery()}" + exec(query) + } + + suspend fun fetchAll(): List { + val query = "SELECT * FROM ${Table.POLICY} WHERE uid/100000 == ${Const.USER_ID}" + return exec(query) { it.toPolicyOrNull() }.filterNotNull() + } + + private suspend fun Map.toPolicyOrNull(): SuPolicy? { + try { + return AppContext.packageManager.createPolicy(this) + } catch (e: Exception) { + Timber.w(e) + val uid = get("uid") ?: return null + delete(uid.toInt()) + return null } } diff --git a/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/Query.kt b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/Query.kt deleted file mode 100644 index 6967a6e15..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/Query.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.topjohnwu.magisk.core.magiskdb - -import androidx.annotation.StringDef -import com.topjohnwu.magisk.ktx.await -import com.topjohnwu.superuser.Shell -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.withContext - -class Query(private val _query: String) { - val query get() = "magisk --sqlite '$_query'" - - interface Builder { - val requestType: String - var table: String - } - - suspend inline fun query(crossinline mapper: (Map) -> R?): List = - withContext(Dispatchers.Default) { - Shell.cmd(query).await().out.map { line -> - async { - line.split("\\|".toRegex()) - .map { it.split("=", limit = 2) } - .filter { it.size == 2 } - .map { it[0] to it[1] } - .toMap() - .let(mapper) - } - }.awaitAll().filterNotNull() - } - - suspend inline fun query() = query { it } - - suspend inline fun commit() = Shell.cmd(query).to(null).await() -} - -class Delete : Query.Builder { - override val requestType: String = "DELETE FROM" - override var table = "" - - private var condition = "" - - fun condition(builder: Condition.() -> Unit) { - condition = Condition().apply(builder).toString() - } - - override fun toString(): String { - return listOf(requestType, table, condition).joinToString(" ") - } -} - -class Select : Query.Builder { - override val requestType: String get() = "SELECT $fields FROM" - override lateinit var table: String - - private var fields = "*" - private var condition = "" - private var orderField = "" - - fun fields(vararg newFields: String) { - if (newFields.isEmpty()) { - fields = "*" - return - } - fields = newFields.joinToString(", ") - } - - fun condition(builder: Condition.() -> Unit) { - condition = Condition().apply(builder).toString() - } - - fun orderBy(field: String, @OrderStrict order: String) { - orderField = "ORDER BY $field $order" - } - - override fun toString(): String { - return listOf(requestType, table, condition, orderField).joinToString(" ") - } -} - -class Replace : Insert() { - override val requestType: String = "REPLACE INTO" -} - -open class Insert : Query.Builder { - override val requestType: String = "INSERT INTO" - override lateinit var table: String - - private val keys get() = _values.keys.joinToString(",") - private val values get() = _values.values.joinToString(",") { - when (it) { - is Boolean -> if (it) "1" else "0" - is Number -> it.toString() - else -> "\"$it\"" - } - } - private var _values: Map = mapOf() - - fun values(vararg pairs: Pair) { - _values = pairs.toMap() - } - - fun values(values: Map) { - _values = values - } - - override fun toString(): String { - return listOf(requestType, table, "($keys) VALUES($values)").joinToString(" ") - } -} - -class Condition { - - private val conditionWord = "WHERE %s" - private var condition: String = "" - - fun equals(field: String, value: Any) { - condition = when (value) { - is String -> "$field=\"$value\"" - else -> "$field=$value" - } - } - - fun greaterThan(field: String, value: String) { - condition = "$field > $value" - } - - fun lessThan(field: String, value: String) { - condition = "$field < $value" - } - - fun greaterOrEqualTo(field: String, value: String) { - condition = "$field >= $value" - } - - fun lessOrEqualTo(field: String, value: String) { - condition = "$field <= $value" - } - - fun and(builder: Condition.() -> Unit) { - condition = "($condition AND ${Condition().apply(builder).condition})" - } - - fun or(builder: Condition.() -> Unit) { - condition = "($condition OR ${Condition().apply(builder).condition})" - } - - override fun toString(): String { - return conditionWord.format(condition) - } -} - -object Order { - const val ASC = "ASC" - const val DESC = "DESC" -} - -@StringDef(Order.ASC, Order.DESC) -@Retention(AnnotationRetention.SOURCE) -annotation class OrderStrict diff --git a/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/SettingsDao.kt b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/SettingsDao.kt index db1d43cf1..3e121ca25 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/SettingsDao.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/SettingsDao.kt @@ -1,22 +1,20 @@ package com.topjohnwu.magisk.core.magiskdb -class SettingsDao : BaseDao() { +class SettingsDao : MagiskDB() { - override val table = Table.SETTINGS + suspend fun delete(key: String) { + val query = "DELETE FROM ${Table.SETTINGS} WHERE key == \"$key\"" + exec(query) + } - suspend fun delete(key: String) = buildQuery { - condition { equals("key", key) } - }.commit() - - suspend fun put(key: String, value: Int) = buildQuery { - values("key" to key, "value" to value) - }.commit() - - suspend fun fetch(key: String, default: Int = -1) = buildQuery { - fields("value") - condition { equals("key", key) } - }.query { - it["value"] - }.firstOrNull() ?: default + suspend fun put(key: String, value: String) { + val kv = mapOf("key" to key, "value" to value) + val query = "REPLACE INTO ${Table.STRINGS} ${kv.toQuery()}" + exec(query) + } + suspend fun fetch(key: String, default: String = ""): String { + val query = "SELECT value FROM ${Table.STRINGS} WHERE key == \"$key\" LIMIT = 1" + return exec(query) { it["value"] }.firstOrNull() ?: default + } } diff --git a/app/src/main/java/com/topjohnwu/magisk/core/model/su/SuLog.kt b/app/src/main/java/com/topjohnwu/magisk/core/model/su/SuLog.kt index 82e4d2534..0d53abfb3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/model/su/SuLog.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/model/su/SuLog.kt @@ -1,11 +1,13 @@ package com.topjohnwu.magisk.core.model.su +import android.content.pm.PackageInfo +import android.content.pm.PackageManager import androidx.room.Entity import androidx.room.PrimaryKey -import com.topjohnwu.magisk.ktx.now +import com.topjohnwu.magisk.ktx.getLabel @Entity(tableName = "logs") -data class SuLog( +class SuLog( val fromUid: Int, val toUid: Int, val fromPid: Int, @@ -13,7 +15,44 @@ data class SuLog( val appName: String, val command: String, val action: Boolean, - val time: Long = now + val time: Long = System.currentTimeMillis() ) { @PrimaryKey(autoGenerate = true) var id: Int = 0 } + +fun PackageManager.createSuLog( + info: PackageInfo, + toUid: Int, + fromPid: Int, + command: String, + policy: Int +): SuLog { + val appInfo = info.applicationInfo + return SuLog( + fromUid = appInfo.uid, + toUid = toUid, + fromPid = fromPid, + packageName = getNameForUid(appInfo.uid)!!, + appName = appInfo.getLabel(this), + command = command, + action = policy == SuPolicy.ALLOW + ) +} + +fun createSuLog( + fromUid: Int, + toUid: Int, + fromPid: Int, + command: String, + policy: Int +): SuLog { + return SuLog( + fromUid = fromUid, + toUid = toUid, + fromPid = fromPid, + packageName = "[UID] $fromUid", + appName = "[UID] $fromUid", + command = command, + action = policy == SuPolicy.ALLOW + ) +} diff --git a/app/src/main/java/com/topjohnwu/magisk/core/model/su/SuPolicy.kt b/app/src/main/java/com/topjohnwu/magisk/core/model/su/SuPolicy.kt index bd59df54f..55b137aa2 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/model/su/SuPolicy.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/model/su/SuPolicy.kt @@ -1,22 +1,20 @@ -@file:SuppressLint("InlinedApi") - package com.topjohnwu.magisk.core.model.su -import android.annotation.SuppressLint +import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.graphics.drawable.Drawable -import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.INTERACTIVE import com.topjohnwu.magisk.ktx.getLabel +import com.topjohnwu.magisk.ktx.getPackageInfo -data class SuPolicy( +class SuPolicy( val uid: Int, val packageName: String, val appName: String, val icon: Drawable, var policy: Int = INTERACTIVE, var until: Long = -1L, - val logging: Boolean = true, - val notification: Boolean = true + var logging: Boolean = true, + var notification: Boolean = true ) { companion object { @@ -25,10 +23,6 @@ data class SuPolicy( const val ALLOW = 2 } - fun toLog(toUid: Int, fromPid: Int, command: String) = SuLog( - uid, toUid, fromPid, packageName, appName, - command, policy == ALLOW) - fun toMap() = mapOf( "uid" to uid, "package_name" to packageName, @@ -39,47 +33,43 @@ data class SuPolicy( ) } -@Throws(PackageManager.NameNotFoundException::class) -fun Map.toPolicy(pm: PackageManager): SuPolicy { - val uid = get("uid")?.toIntOrNull() ?: -1 - val packageName = get("package_name").orEmpty() - val info = pm.getApplicationInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES) - - if (info.uid != uid) - throw PackageManager.NameNotFoundException() - +fun PackageManager.createPolicy(info: PackageInfo): SuPolicy { + val appInfo = info.applicationInfo + val prefix = if (info.sharedUserId == null) "" else "[SharedUID] " return SuPolicy( - uid = uid, - packageName = packageName, - appName = info.getLabel(pm), - icon = info.loadIcon(pm), - policy = get("policy")?.toIntOrNull() ?: INTERACTIVE, - until = get("until")?.toLongOrNull() ?: -1L, - logging = get("logging")?.toIntOrNull() != 0, - notification = get("notification")?.toIntOrNull() != 0 + uid = appInfo.uid, + packageName = getNameForUid(appInfo.uid)!!, + appName = "$prefix${appInfo.getLabel(this)}", + icon = appInfo.loadIcon(this), ) } @Throws(PackageManager.NameNotFoundException::class) -fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): SuPolicy { - val pkg = pm.getPackagesForUid(this)?.firstOrNull() - ?: throw PackageManager.NameNotFoundException() - val info = pm.getApplicationInfo(pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES) - return SuPolicy( - uid = info.uid, - packageName = pkg, - appName = info.getLabel(pm), - icon = info.loadIcon(pm), - policy = policy - ) +fun PackageManager.createPolicy(uid: Int): SuPolicy { + val info = getPackageInfo(uid, -1) + return if (info == null) { + // We can assert getNameForUid does not return null because + // getPackageInfo will already throw if UID does not exist + val name = getNameForUid(uid)!! + SuPolicy( + uid = uid, + packageName = name, + appName = "[SharedUID] $name", + icon = defaultActivityIcon, + ) + } else { + createPolicy(info) + } } -fun Int.toUidPolicy(pm: PackageManager, policy: Int): SuPolicy { - return SuPolicy( - uid = this, - packageName = "[UID] $this", - appName = "[UID] $this", - icon = pm.defaultActivityIcon, - policy = policy - ) +@Throws(PackageManager.NameNotFoundException::class) +fun PackageManager.createPolicy(map: Map): SuPolicy { + val uid = map["uid"]?.toIntOrNull() ?: throw IllegalArgumentException() + val policy = createPolicy(uid) + + map["policy"]?.toInt()?.let { policy.policy = it } + map["until"]?.toLong()?.let { policy.until = it } + map["logging"]?.toInt()?.let { policy.logging = it != 0 } + map["notification"]?.toInt()?.let { policy.notification = it != 0 } + return policy } diff --git a/app/src/main/java/com/topjohnwu/magisk/core/su/SuCallbackHandler.kt b/app/src/main/java/com/topjohnwu/magisk/core/su/SuCallbackHandler.kt index c988b9249..d33f748c9 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/su/SuCallbackHandler.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/su/SuCallbackHandler.kt @@ -7,12 +7,12 @@ import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.R import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.model.su.SuPolicy -import com.topjohnwu.magisk.core.model.su.toPolicy -import com.topjohnwu.magisk.core.model.su.toUidPolicy +import com.topjohnwu.magisk.core.model.su.createSuLog import com.topjohnwu.magisk.di.ServiceLocator +import com.topjohnwu.magisk.ktx.getLabel +import com.topjohnwu.magisk.ktx.getPackageInfo import com.topjohnwu.magisk.utils.Utils -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import timber.log.Timber object SuCallbackHandler { @@ -53,59 +53,47 @@ object SuCallbackHandler { private fun handleLogging(context: Context, data: Bundle) { val fromUid = data.getIntComp("from.uid", -1) val notify = data.getBoolean("notify", true) - val allow = data.getIntComp("policy", SuPolicy.ALLOW) + val policy = data.getIntComp("policy", SuPolicy.ALLOW) + val toUid = data.getIntComp("to.uid", -1) + val pid = data.getIntComp("pid", -1) + val command = data.getString("command", "") val pm = context.packageManager - val policy = runCatching { - fromUid.toPolicy(pm, allow) - }.getOrElse { - GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) } - fromUid.toUidPolicy(pm, allow) - } + val log = runCatching { + pm.getPackageInfo(fromUid, pid)?.let { + pm.createSuLog(it, toUid, pid, command, policy) + } + }.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy) if (notify) - notify(context, policy) + notify(context, log.action, log.appName) - val toUid = data.getIntComp("to.uid", -1) - val pid = data.getIntComp("pid", -1) - - val command = data.getString("command", "") - val log = policy.toLog( - toUid = toUid, - fromPid = pid, - command = command - ) - - GlobalScope.launch { - ServiceLocator.logRepo.insert(log) - } + runBlocking { ServiceLocator.logRepo.insert(log) } } private fun handleNotify(context: Context, data: Bundle) { - val fromUid = data.getIntComp("from.uid", -1) - val allow = data.getIntComp("policy", SuPolicy.ALLOW) + val uid = data.getIntComp("from.uid", -1) + val pid = data.getIntComp("pid", -1) + val policy = data.getIntComp("policy", SuPolicy.ALLOW) val pm = context.packageManager + val appName = runCatching { + pm.getPackageInfo(uid, pid)?.applicationInfo?.getLabel(pm) + }.getOrNull() ?: "[UID] $uid" - val policy = runCatching { - fromUid.toPolicy(pm, allow) - }.getOrElse { - GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) } - fromUid.toUidPolicy(pm, allow) - } - notify(context, policy) + notify(context, policy == SuPolicy.ALLOW, appName) } - private fun notify(context: Context, policy: SuPolicy) { + private fun notify(context: Context, granted: Boolean, appName: String) { if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) { - val resId = if (policy.policy == SuPolicy.ALLOW) + val resId = if (granted) R.string.su_allow_toast else R.string.su_deny_toast - Utils.toast(context.getString(resId, policy.appName), Toast.LENGTH_SHORT) + Utils.toast(context.getString(resId, appName), Toast.LENGTH_SHORT) } } } diff --git a/app/src/main/java/com/topjohnwu/magisk/core/su/SuRequestHandler.kt b/app/src/main/java/com/topjohnwu/magisk/core/su/SuRequestHandler.kt index 6fbf15d8d..f41c0c9d2 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/su/SuRequestHandler.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/su/SuRequestHandler.kt @@ -6,7 +6,8 @@ import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.magiskdb.PolicyDao import com.topjohnwu.magisk.core.model.su.SuPolicy -import com.topjohnwu.magisk.core.model.su.toPolicy +import com.topjohnwu.magisk.core.model.su.createPolicy +import com.topjohnwu.magisk.ktx.getPackageInfo import com.topjohnwu.superuser.Shell import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -60,10 +61,12 @@ class SuRequestHandler( private suspend fun init(intent: Intent) = withContext(Dispatchers.IO) { try { - val name = intent.getStringExtra("fifo") ?: throw SuRequestError() + val fifo = intent.getStringExtra("fifo") ?: throw SuRequestError() val uid = intent.getIntExtra("uid", -1).also { if (it < 0) throw SuRequestError() } - output = DataOutputStream(FileOutputStream(name).buffered()) - policy = uid.toPolicy(pm) + val pid = intent.getIntExtra("pid", -1) + val info = pm.getPackageInfo(uid, pid) ?: throw SuRequestError() + output = DataOutputStream(FileOutputStream(fifo).buffered()) + policy = pm.createPolicy(info) true } catch (e: Exception) { when (e) { diff --git a/app/src/main/java/com/topjohnwu/magisk/core/utils/RootUtils.kt b/app/src/main/java/com/topjohnwu/magisk/core/utils/RootUtils.kt index 3773d2f90..1357e2db2 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/utils/RootUtils.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/utils/RootUtils.kt @@ -1,30 +1,64 @@ package com.topjohnwu.magisk.core.utils +import android.app.ActivityManager import android.content.ComponentName import android.content.Intent import android.content.ServiceConnection -import android.os.Binder import android.os.IBinder +import androidx.core.content.getSystemService import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.ipc.RootService import timber.log.Timber +import java.io.File import java.util.concurrent.locks.AbstractQueuedSynchronizer class RootUtils(stub: Any?) : RootService() { private val className: String = stub?.javaClass?.name ?: javaClass.name + private lateinit var am: ActivityManager constructor() : this(null) { Timber.plant(Timber.DebugTree()) } + override fun onCreate() { + am = getSystemService()!! + } + override fun getComponentName(): ComponentName { return ComponentName(packageName, className) } override fun onBind(intent: Intent): IBinder { - return Binder() + return object : IRootUtils.Stub() { + override fun getAppProcess(pid: Int) = safe(null) { getAppProcessImpl(pid) } + } + } + + private inline fun safe(default: T, block: () -> T): T { + return try { + block() + } catch (e: Throwable) { + Timber.e(e) + default + } + } + + private fun getAppProcessImpl(_pid: Int): ActivityManager.RunningAppProcessInfo? { + val procList = am.runningAppProcesses + var pid = _pid + while (pid > 1) { + val proc = procList.find { it.pid == pid } + if (proc != null) + return proc + // Find PPID + File("/proc/$pid/status").useLines { + val s = it.find { line -> line.startsWith("PPid:") } + pid = s?.substring(5)?.trim()?.toInt() ?: -1 + } + } + return null } object Connection : AbstractQueuedSynchronizer(), ServiceConnection { diff --git a/app/src/main/java/com/topjohnwu/magisk/ktx/XAndroid.kt b/app/src/main/java/com/topjohnwu/magisk/ktx/XAndroid.kt index e4c692287..cb20cc2a7 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ktx/XAndroid.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ktx/XAndroid.kt @@ -7,10 +7,10 @@ import android.content.Context import android.content.ContextWrapper import android.content.Intent import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo import android.content.pm.PackageManager -import android.content.pm.PackageManager.* +import android.content.pm.PackageManager.PERMISSION_GRANTED import android.content.res.Configuration -import android.content.res.Resources import android.database.Cursor import android.graphics.Bitmap import android.graphics.Canvas @@ -19,40 +19,26 @@ import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.LayerDrawable import android.net.Uri import android.os.Build.VERSION.SDK_INT -import android.text.PrecomputedText import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver import android.view.inputmethod.InputMethodManager -import android.widget.TextView -import androidx.annotation.ColorRes -import androidx.annotation.DrawableRes import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.content.getSystemService -import androidx.core.net.toUri -import androidx.core.text.PrecomputedTextCompat -import androidx.core.view.isGone -import androidx.core.widget.TextViewCompat -import androidx.databinding.BindingAdapter import androidx.fragment.app.Fragment import androidx.interpolator.view.animation.FastOutSlowInInterpolator -import androidx.lifecycle.lifecycleScope import androidx.transition.AutoTransition import androidx.transition.TransitionManager import com.topjohnwu.magisk.R import com.topjohnwu.magisk.core.Const -import com.topjohnwu.magisk.core.base.BaseActivity +import com.topjohnwu.magisk.core.utils.RootUtils import com.topjohnwu.magisk.core.utils.currentLocale -import com.topjohnwu.magisk.di.AppContext import com.topjohnwu.magisk.utils.DynamicClassLoader -import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.superuser.Shell -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import java.io.File +import kotlin.Array +import kotlin.String import java.lang.reflect.Array as JArray fun Context.rawResource(id: Int) = resources.openRawResource(id) @@ -79,8 +65,6 @@ val Context.deviceProtectedContext: Context get() = createDeviceProtectedStorageContext() } else { this } -fun Intent.startActivity(context: Context) = context.startActivity(this) - fun Intent.startActivityWithRoot() { val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString()) val cmd = toCommand(args).joinToString(" ") @@ -185,8 +169,6 @@ fun Intent.toCommand(args: MutableList = mutableListOf()): MutableList Cursor.toList(transformer: (Cursor) -> Result): List { @@ -209,34 +191,6 @@ fun ApplicationInfo.getLabel(pm: PackageManager): String { return loadLabel(pm).toString() } -fun Intent.exists(packageManager: PackageManager) = resolveActivity(packageManager) != null - -fun Context.colorCompat(@ColorRes id: Int) = try { - ContextCompat.getColor(this, id) -} catch (e: Resources.NotFoundException) { - null -} - -fun Context.colorStateListCompat(@ColorRes id: Int) = try { - ContextCompat.getColorStateList(this, id) -} catch (e: Resources.NotFoundException) { - null -} - -fun Context.drawableCompat(@DrawableRes id: Int) = AppCompatResources.getDrawable(this, id) -/** - * Pass [start] and [end] dimensions, function will return left and right - * with respect to RTL layout direction - */ -fun Context.startEndToLeftRight(start: Int, end: Int): Pair { - if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL) { - return end to start - } - return start to end -} - -fun Context.openUrl(url: String) = Utils.openLink(this, url.toUri()) - inline fun T.createClassLoader(apk: File) = DynamicClassLoader(apk, T::class.java.classLoader) @@ -308,3 +262,20 @@ fun getProperty(key: String, def: String): String { } return def } + +@SuppressLint("InlinedApi") +@Throws(PackageManager.NameNotFoundException::class) +fun PackageManager.getPackageInfo(uid: Int, pid: Int): PackageInfo? { + val flag = PackageManager.MATCH_UNINSTALLED_PACKAGES + val pkgs = getPackagesForUid(uid) ?: throw PackageManager.NameNotFoundException() + return if (pkgs.size > 1) { + if (pid <= 0) + return null + // Try to find package name from PID + val proc = RootUtils.obj?.getAppProcess(pid) ?: return null + val pkg = proc.pkgList[0] + getPackageInfo(pkg, flag) + } else { + getPackageInfo(pkgs[0], flag) + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/superuser/PolicyRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/PolicyRvItem.kt index 0dc1fcef2..27b675e1c 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/superuser/PolicyRvItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/PolicyRvItem.kt @@ -20,34 +20,28 @@ class PolicyRvItem( var isExpanded = false set(value) = set(value, field, { field = it }, BR.expanded) - // This property hosts the policy state - var policyState = item.policy == SuPolicy.ALLOW - set(value) = set(value, field, { field = it }, BR.enabled) - // This property binds with the UI state @get:Bindable var isEnabled - get() = policyState - set(value) = set(value, policyState, { viewModel.togglePolicy(this, it) }, BR.enabled) - - @get:Bindable - var shouldNotify = item.notification - set(value) = set(value, field, { field = it }, BR.shouldNotify) { - viewModel.updatePolicy(updatedPolicy, isLogging = false) + get() = item.policy == SuPolicy.ALLOW + set(value) { + if (value != isEnabled) + viewModel.togglePolicy(this, value) } @get:Bindable - var shouldLog = item.logging - set(value) = set(value, field, { field = it }, BR.shouldLog) { - viewModel.updatePolicy(updatedPolicy, isLogging = true) + var shouldNotify + get() = item.notification + set(value) = set(value, shouldNotify, { item.notification = it }, BR.shouldNotify) { + viewModel.updatePolicy(item, isLogging = false) } - private val updatedPolicy - get() = item.copy( - policy = if (policyState) SuPolicy.ALLOW else SuPolicy.DENY, - notification = shouldNotify, - logging = shouldLog - ) + @get:Bindable + var shouldLog + get() = item.logging + set(value) = set(value, shouldLog, { item.logging = it }, BR.shouldLog) { + viewModel.updatePolicy(item, isLogging = true) + } fun toggleExpand() { isExpanded = !isExpanded diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt index 50dcff0ee..a2551d062 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt @@ -47,12 +47,12 @@ class SuperuserViewModel( return@launch } state = State.LOADING - val (policies, diff) = withContext(Dispatchers.Default) { + val (policies, diff) = withContext(Dispatchers.IO) { db.deleteOutdated() - val policies = db.fetchAll { + val policies = db.fetchAll().map { PolicyRvItem(it, it.icon, this@SuperuserViewModel) }.sortedWith(compareBy( - { it.item.appName.toLowerCase(currentLocale) }, + { it.item.appName.lowercase(currentLocale) }, { it.item.packageName } )) policies to itemsPolicies.calculateDiff(policies) @@ -107,14 +107,13 @@ class SuperuserViewModel( fun togglePolicy(item: PolicyRvItem, enable: Boolean) { fun updateState() { - item.policyState = enable - val policy = if (enable) SuPolicy.ALLOW else SuPolicy.DENY - val app = item.item.copy(policy = policy) + item.item.policy = policy + item.notifyPropertyChanged(BR.enabled) viewModelScope.launch { - db.update(app) - val res = if (app.policy == SuPolicy.ALLOW) R.string.su_snack_grant + db.update(item.item) + val res = if (item.item.policy == SuPolicy.ALLOW) R.string.su_snack_grant else R.string.su_snack_deny SnackbarEvent(res.asText(item.item.appName)).publish() } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt index 016c96609..2b6b903a8 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt @@ -7,6 +7,7 @@ import android.os.Build import android.os.Bundle import android.view.Window import android.view.WindowManager +import androidx.lifecycle.lifecycleScope import com.topjohnwu.magisk.R import com.topjohnwu.magisk.arch.UIActivity import com.topjohnwu.magisk.core.su.SuCallbackHandler @@ -14,6 +15,9 @@ import com.topjohnwu.magisk.core.su.SuCallbackHandler.REQUEST import com.topjohnwu.magisk.databinding.ActivityRequestBinding import com.topjohnwu.magisk.di.viewModel import com.topjohnwu.magisk.ui.theme.Theme +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext open class SuRequestActivity : UIActivity() { @@ -40,8 +44,12 @@ open class SuRequestActivity : UIActivity() { if (action == REQUEST) { viewModel.handleRequest(intent) } else { - SuCallbackHandler.run(this, action, intent.extras) - finish() + lifecycleScope.launch { + withContext(Dispatchers.IO) { + SuCallbackHandler.run(this@SuRequestActivity, action, intent.extras) + } + finish() + } } } else { finish() diff --git a/native/jni/su/connect.cpp b/native/jni/su/connect.cpp index 53d40d564..5aec48016 100644 --- a/native/jni/su/connect.cpp +++ b/native/jni/su/connect.cpp @@ -191,6 +191,7 @@ void app_notify(const su_context &ctx) { vector extras; extras.reserve(2); extras.emplace_back("from.uid", ctx.info->uid); + extras.emplace_back("pid", ctx.pid); extras.emplace_back("policy", ctx.info->access.policy); exec_cmd("notify", extras, ctx.info); @@ -198,21 +199,22 @@ void app_notify(const su_context &ctx) { } } -int app_request(const shared_ptr &info) { +int app_request(const su_context &ctx) { // Create FIFO char fifo[64]; strcpy(fifo, "/dev/socket/"); gen_rand_str(fifo + 12, 32, true); mkfifo(fifo, 0600); - chown(fifo, info->mgr_st.st_uid, info->mgr_st.st_gid); + chown(fifo, ctx.info->mgr_st.st_uid, ctx.info->mgr_st.st_gid); setfilecon(fifo, "u:object_r:" SEPOL_FILE_TYPE ":s0"); // Send request vector extras; extras.reserve(2); extras.emplace_back("fifo", fifo); - extras.emplace_back("uid", info->eval_uid); - exec_cmd("request", extras, info, false); + extras.emplace_back("uid", ctx.info->eval_uid); + extras.emplace_back("pid", ctx.pid); + exec_cmd("request", extras, ctx.info, false); // Wait for data input for at most 70 seconds int fd = xopen(fifo, O_RDONLY | O_CLOEXEC | O_NONBLOCK); diff --git a/native/jni/su/su.hpp b/native/jni/su/su.hpp index bdf710aef..25d830da3 100644 --- a/native/jni/su/su.hpp +++ b/native/jni/su/su.hpp @@ -60,4 +60,4 @@ struct su_context { void app_log(const su_context &ctx); void app_notify(const su_context &ctx); -int app_request(const std::shared_ptr &info); +int app_request(const su_context &ctx); diff --git a/native/jni/su/su_daemon.cpp b/native/jni/su/su_daemon.cpp index 8fe883b2c..9cf090826 100644 --- a/native/jni/su/su_daemon.cpp +++ b/native/jni/su/su_daemon.cpp @@ -193,20 +193,7 @@ static shared_ptr get_su_info(unsigned uid) { info->access = NO_SU_ACCESS; return info; } - } else { - return info; } - - // If still not determined, ask manager - int fd = app_request(info); - if (fd < 0) { - info->access.policy = DENY; - } else { - int ret = read_int_be(fd); - info->access.policy = ret < 0 ? DENY : static_cast(ret); - close(fd); - } - return info; } @@ -237,6 +224,18 @@ void su_daemon_handler(int client, const sock_cred *cred) { read_string(client, ctx.req.shell); read_string(client, ctx.req.command); + // If still not determined, ask manager + if (ctx.info->access.policy == QUERY) { + int fd = app_request(ctx); + if (fd < 0) { + ctx.info->access.policy = DENY; + } else { + int ret = read_int_be(fd); + ctx.info->access.policy = ret < 0 ? DENY : static_cast(ret); + close(fd); + } + } + if (ctx.info->access.log) app_log(ctx); else if (ctx.info->access.notify)