Add preliminary shared UID app support

This commit is contained in:
topjohnwu 2022-03-25 13:08:13 -07:00
parent f2c15c7701
commit 9f1740cc4f
19 changed files with 337 additions and 461 deletions

View File

@ -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);
}

View File

@ -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 <reified Builder : Query.Builder> buildQuery(builder: Builder.() -> Unit = {}) =
Builder::class.java.newInstance()
.apply { table = this@BaseDao.table }
.apply(builder)
.toString()
.let { Query(it) }
}

View File

@ -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 <R> exec(
query: String,
mapper: suspend (Map<String, String>) -> R
): List<R> {
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<String, Any>.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"
}
}

View File

@ -2,61 +2,48 @@ package com.topjohnwu.magisk.core.magiskdb
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.model.su.SuPolicy 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.di.AppContext
import com.topjohnwu.magisk.ktx.now
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class PolicyDao : MagiskDB() {
class PolicyDao : BaseDao() { suspend fun deleteOutdated() {
val nowSeconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
override val table: String = Table.POLICY val query = "DELETE FROM ${Table.POLICY} WHERE " +
"(until > 0 AND until < $nowSeconds) OR until < 0"
suspend fun deleteOutdated() = buildQuery<Delete> { exec(query)
condition {
greaterThan("until", "0")
and {
lessThan("until", TimeUnit.MILLISECONDS.toSeconds(now).toString())
}
or {
lessThan("until", "0")
}
}
}.commit()
suspend fun delete(uid: Int) = buildQuery<Delete> {
condition {
equals("uid", uid)
}
}.commit()
suspend fun fetch(uid: Int) = buildQuery<Select> {
condition {
equals("uid", uid)
}
}.query().first().toPolicyOrNull()
suspend fun update(policy: SuPolicy) = buildQuery<Replace> {
values(policy.toMap())
}.commit()
suspend fun <R: Any> fetchAll(mapper: (SuPolicy) -> R) = buildQuery<Select> {
condition {
equals("uid/100000", Const.USER_ID)
}
}.query {
it.toPolicyOrNull()?.let(mapper)
} }
private fun Map<String, String>.toPolicyOrNull(): SuPolicy? { suspend fun delete(uid: Int) {
return runCatching { toPolicy(AppContext.packageManager) }.getOrElse { val query = "DELETE FROM ${Table.POLICY} WHERE uid == $uid"
Timber.w(it) exec(query)
val uid = getOrElse("uid") { return null } }
GlobalScope.launch { delete(uid.toInt()) }
null 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<SuPolicy> {
val query = "SELECT * FROM ${Table.POLICY} WHERE uid/100000 == ${Const.USER_ID}"
return exec(query) { it.toPolicyOrNull() }.filterNotNull()
}
private suspend fun Map<String, String>.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
} }
} }

View File

@ -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 <R : Any> query(crossinline mapper: (Map<String, String>) -> R?): List<R> =
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<String, Any> = mapOf()
fun values(vararg pairs: Pair<String, Any>) {
_values = pairs.toMap()
}
fun values(values: Map<String, Any>) {
_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

View File

@ -1,22 +1,20 @@
package com.topjohnwu.magisk.core.magiskdb 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<Delete> { suspend fun put(key: String, value: Int) {
condition { equals("key", key) } val kv = mapOf("key" to key, "value" to value)
}.commit() val query = "REPLACE INTO ${Table.SETTINGS} ${kv.toQuery()}"
exec(query)
suspend fun put(key: String, value: Int) = buildQuery<Replace> { }
values("key" to key, "value" to value)
}.commit()
suspend fun fetch(key: String, default: Int = -1) = buildQuery<Select> {
fields("value")
condition { equals("key", key) }
}.query {
it["value"]?.toIntOrNull()
}.firstOrNull() ?: default
suspend fun fetch(key: String, default: Int = -1): Int {
val query = "SELECT value FROM ${Table.SETTINGS} WHERE key == \"$key\" LIMIT = 1"
return exec(query) { it["value"]?.toInt() }.firstOrNull() ?: default
}
} }

View File

@ -1,22 +1,20 @@
package com.topjohnwu.magisk.core.magiskdb package com.topjohnwu.magisk.core.magiskdb
class StringDao : BaseDao() { class StringDao : MagiskDB() {
override val table = Table.STRINGS suspend fun delete(key: String) {
val query = "DELETE FROM ${Table.STRINGS} WHERE key == \"$key\""
exec(query)
}
suspend fun delete(key: String) = buildQuery<Delete> { suspend fun put(key: String, value: String) {
condition { equals("key", key) } val kv = mapOf("key" to key, "value" to value)
}.commit() val query = "REPLACE INTO ${Table.STRINGS} ${kv.toQuery()}"
exec(query)
suspend fun put(key: String, value: String) = buildQuery<Replace> { }
values("key" to key, "value" to value)
}.commit()
suspend fun fetch(key: String, default: String = "") = buildQuery<Select> {
fields("value")
condition { equals("key", key) }
}.query {
it["value"]
}.firstOrNull() ?: default
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
}
} }

View File

@ -1,11 +1,13 @@
package com.topjohnwu.magisk.core.model.su package com.topjohnwu.magisk.core.model.su
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.topjohnwu.magisk.ktx.now import com.topjohnwu.magisk.ktx.getLabel
@Entity(tableName = "logs") @Entity(tableName = "logs")
data class SuLog( class SuLog(
val fromUid: Int, val fromUid: Int,
val toUid: Int, val toUid: Int,
val fromPid: Int, val fromPid: Int,
@ -13,7 +15,44 @@ data class SuLog(
val appName: String, val appName: String,
val command: String, val command: String,
val action: Boolean, val action: Boolean,
val time: Long = now val time: Long = System.currentTimeMillis()
) { ) {
@PrimaryKey(autoGenerate = true) var id: Int = 0 @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
)
}

View File

@ -1,22 +1,20 @@
@file:SuppressLint("InlinedApi")
package com.topjohnwu.magisk.core.model.su package com.topjohnwu.magisk.core.model.su
import android.annotation.SuppressLint import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.drawable.Drawable 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.getLabel
import com.topjohnwu.magisk.ktx.getPackageInfo
data class SuPolicy( class SuPolicy(
val uid: Int, val uid: Int,
val packageName: String, val packageName: String,
val appName: String, val appName: String,
val icon: Drawable, val icon: Drawable,
var policy: Int = INTERACTIVE, var policy: Int = INTERACTIVE,
var until: Long = -1L, var until: Long = -1L,
val logging: Boolean = true, var logging: Boolean = true,
val notification: Boolean = true var notification: Boolean = true
) { ) {
companion object { companion object {
@ -25,10 +23,6 @@ data class SuPolicy(
const val ALLOW = 2 const val ALLOW = 2
} }
fun toLog(toUid: Int, fromPid: Int, command: String) = SuLog(
uid, toUid, fromPid, packageName, appName,
command, policy == ALLOW)
fun toMap() = mapOf( fun toMap() = mapOf(
"uid" to uid, "uid" to uid,
"package_name" to packageName, "package_name" to packageName,
@ -39,47 +33,43 @@ data class SuPolicy(
) )
} }
@Throws(PackageManager.NameNotFoundException::class) fun PackageManager.createPolicy(info: PackageInfo): SuPolicy {
fun Map<String, String>.toPolicy(pm: PackageManager): SuPolicy { val appInfo = info.applicationInfo
val uid = get("uid")?.toIntOrNull() ?: -1 val prefix = if (info.sharedUserId == null) "" else "[SharedUID] "
val packageName = get("package_name").orEmpty()
val info = pm.getApplicationInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES)
if (info.uid != uid)
throw PackageManager.NameNotFoundException()
return SuPolicy( return SuPolicy(
uid = uid, uid = appInfo.uid,
packageName = packageName, packageName = getNameForUid(appInfo.uid)!!,
appName = info.getLabel(pm), appName = "$prefix${appInfo.getLabel(this)}",
icon = info.loadIcon(pm), icon = appInfo.loadIcon(this),
policy = get("policy")?.toIntOrNull() ?: INTERACTIVE,
until = get("until")?.toLongOrNull() ?: -1L,
logging = get("logging")?.toIntOrNull() != 0,
notification = get("notification")?.toIntOrNull() != 0
) )
} }
@Throws(PackageManager.NameNotFoundException::class) @Throws(PackageManager.NameNotFoundException::class)
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): SuPolicy { fun PackageManager.createPolicy(uid: Int): SuPolicy {
val pkg = pm.getPackagesForUid(this)?.firstOrNull() val info = getPackageInfo(uid, -1)
?: throw PackageManager.NameNotFoundException() return if (info == null) {
val info = pm.getApplicationInfo(pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES) // We can assert getNameForUid does not return null because
return SuPolicy( // getPackageInfo will already throw if UID does not exist
uid = info.uid, val name = getNameForUid(uid)!!
packageName = pkg, SuPolicy(
appName = info.getLabel(pm), uid = uid,
icon = info.loadIcon(pm), packageName = name,
policy = policy appName = "[SharedUID] $name",
) icon = defaultActivityIcon,
)
} else {
createPolicy(info)
}
} }
fun Int.toUidPolicy(pm: PackageManager, policy: Int): SuPolicy { @Throws(PackageManager.NameNotFoundException::class)
return SuPolicy( fun PackageManager.createPolicy(map: Map<String, String>): SuPolicy {
uid = this, val uid = map["uid"]?.toIntOrNull() ?: throw IllegalArgumentException()
packageName = "[UID] $this", val policy = createPolicy(uid)
appName = "[UID] $this",
icon = pm.defaultActivityIcon, map["policy"]?.toInt()?.let { policy.policy = it }
policy = policy 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
} }

View File

@ -7,12 +7,12 @@ import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.model.su.SuPolicy import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.core.model.su.toPolicy import com.topjohnwu.magisk.core.model.su.createSuLog
import com.topjohnwu.magisk.core.model.su.toUidPolicy
import com.topjohnwu.magisk.di.ServiceLocator 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 com.topjohnwu.magisk.utils.Utils
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
object SuCallbackHandler { object SuCallbackHandler {
@ -53,59 +53,47 @@ object SuCallbackHandler {
private fun handleLogging(context: Context, data: Bundle) { private fun handleLogging(context: Context, data: Bundle) {
val fromUid = data.getIntComp("from.uid", -1) val fromUid = data.getIntComp("from.uid", -1)
val notify = data.getBoolean("notify", true) 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 pm = context.packageManager
val policy = runCatching { val log = runCatching {
fromUid.toPolicy(pm, allow) pm.getPackageInfo(fromUid, pid)?.let {
}.getOrElse { pm.createSuLog(it, toUid, pid, command, policy)
GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) } }
fromUid.toUidPolicy(pm, allow) }.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy)
}
if (notify) if (notify)
notify(context, policy) notify(context, log.action, log.appName)
val toUid = data.getIntComp("to.uid", -1) runBlocking { ServiceLocator.logRepo.insert(log) }
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)
}
} }
private fun handleNotify(context: Context, data: Bundle) { private fun handleNotify(context: Context, data: Bundle) {
val fromUid = data.getIntComp("from.uid", -1) val uid = data.getIntComp("from.uid", -1)
val allow = data.getIntComp("policy", SuPolicy.ALLOW) val pid = data.getIntComp("pid", -1)
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
val pm = context.packageManager val pm = context.packageManager
val appName = runCatching {
pm.getPackageInfo(uid, pid)?.applicationInfo?.getLabel(pm)
}.getOrNull() ?: "[UID] $uid"
val policy = runCatching { notify(context, policy == SuPolicy.ALLOW, appName)
fromUid.toPolicy(pm, allow)
}.getOrElse {
GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) }
fromUid.toUidPolicy(pm, allow)
}
notify(context, policy)
} }
private fun notify(context: Context, policy: SuPolicy) { private fun notify(context: Context, granted: Boolean, appName: String) {
if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) { if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
val resId = if (policy.policy == SuPolicy.ALLOW) val resId = if (granted)
R.string.su_allow_toast R.string.su_allow_toast
else else
R.string.su_deny_toast R.string.su_deny_toast
Utils.toast(context.getString(resId, policy.appName), Toast.LENGTH_SHORT) Utils.toast(context.getString(resId, appName), Toast.LENGTH_SHORT)
} }
} }
} }

View File

@ -6,7 +6,8 @@ import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.magiskdb.PolicyDao import com.topjohnwu.magisk.core.magiskdb.PolicyDao
import com.topjohnwu.magisk.core.model.su.SuPolicy 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 com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -60,10 +61,12 @@ class SuRequestHandler(
private suspend fun init(intent: Intent) = withContext(Dispatchers.IO) { private suspend fun init(intent: Intent) = withContext(Dispatchers.IO) {
try { 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() } val uid = intent.getIntExtra("uid", -1).also { if (it < 0) throw SuRequestError() }
output = DataOutputStream(FileOutputStream(name).buffered()) val pid = intent.getIntExtra("pid", -1)
policy = uid.toPolicy(pm) val info = pm.getPackageInfo(uid, pid) ?: throw SuRequestError()
output = DataOutputStream(FileOutputStream(fifo).buffered())
policy = pm.createPolicy(info)
true true
} catch (e: Exception) { } catch (e: Exception) {
when (e) { when (e) {

View File

@ -1,30 +1,64 @@
package com.topjohnwu.magisk.core.utils package com.topjohnwu.magisk.core.utils
import android.app.ActivityManager
import android.content.ComponentName import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.Binder
import android.os.IBinder import android.os.IBinder
import androidx.core.content.getSystemService
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.ipc.RootService import com.topjohnwu.superuser.ipc.RootService
import timber.log.Timber import timber.log.Timber
import java.io.File
import java.util.concurrent.locks.AbstractQueuedSynchronizer import java.util.concurrent.locks.AbstractQueuedSynchronizer
class RootUtils(stub: Any?) : RootService() { class RootUtils(stub: Any?) : RootService() {
private val className: String = stub?.javaClass?.name ?: javaClass.name private val className: String = stub?.javaClass?.name ?: javaClass.name
private lateinit var am: ActivityManager
constructor() : this(null) { constructor() : this(null) {
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree())
} }
override fun onCreate() {
am = getSystemService()!!
}
override fun getComponentName(): ComponentName { override fun getComponentName(): ComponentName {
return ComponentName(packageName, className) return ComponentName(packageName, className)
} }
override fun onBind(intent: Intent): IBinder { override fun onBind(intent: Intent): IBinder {
return Binder() return object : IRootUtils.Stub() {
override fun getAppProcess(pid: Int) = safe(null) { getAppProcessImpl(pid) }
}
}
private inline fun <T> 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 { object Connection : AbstractQueuedSynchronizer(), ServiceConnection {

View File

@ -7,10 +7,10 @@ import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo 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.* import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources
import android.database.Cursor import android.database.Cursor
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
@ -19,40 +19,26 @@ import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.LayerDrawable import android.graphics.drawable.LayerDrawable
import android.net.Uri import android.net.Uri
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import android.text.PrecomputedText
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import android.view.inputmethod.InputMethodManager 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.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService 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.fragment.app.Fragment
import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.lifecycle.lifecycleScope
import androidx.transition.AutoTransition import androidx.transition.AutoTransition
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Const 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.core.utils.currentLocale
import com.topjohnwu.magisk.di.AppContext
import com.topjohnwu.magisk.utils.DynamicClassLoader import com.topjohnwu.magisk.utils.DynamicClassLoader
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.superuser.Shell 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 java.io.File
import kotlin.Array
import kotlin.String
import java.lang.reflect.Array as JArray import java.lang.reflect.Array as JArray
fun Context.rawResource(id: Int) = resources.openRawResource(id) fun Context.rawResource(id: Int) = resources.openRawResource(id)
@ -79,8 +65,6 @@ val Context.deviceProtectedContext: Context get() =
createDeviceProtectedStorageContext() createDeviceProtectedStorageContext()
} else { this } } else { this }
fun Intent.startActivity(context: Context) = context.startActivity(this)
fun Intent.startActivityWithRoot() { fun Intent.startActivityWithRoot() {
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString()) val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
val cmd = toCommand(args).joinToString(" ") val cmd = toCommand(args).joinToString(" ")
@ -185,8 +169,6 @@ fun Intent.toCommand(args: MutableList<String> = mutableListOf()): MutableList<S
return args return args
} }
fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)
fun Context.cachedFile(name: String) = File(cacheDir, name) fun Context.cachedFile(name: String) = File(cacheDir, name)
fun <Result> Cursor.toList(transformer: (Cursor) -> Result): List<Result> { fun <Result> Cursor.toList(transformer: (Cursor) -> Result): List<Result> {
@ -209,34 +191,6 @@ fun ApplicationInfo.getLabel(pm: PackageManager): String {
return loadLabel(pm).toString() 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<Int, Int> {
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 <reified T> T.createClassLoader(apk: File) = inline fun <reified T> T.createClassLoader(apk: File) =
DynamicClassLoader(apk, T::class.java.classLoader) DynamicClassLoader(apk, T::class.java.classLoader)
@ -308,3 +262,20 @@ fun getProperty(key: String, def: String): String {
} }
return def 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)
}
}

View File

@ -20,34 +20,28 @@ class PolicyRvItem(
var isExpanded = false var isExpanded = false
set(value) = set(value, field, { field = it }, BR.expanded) 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 // This property binds with the UI state
@get:Bindable @get:Bindable
var isEnabled var isEnabled
get() = policyState get() = item.policy == SuPolicy.ALLOW
set(value) = set(value, policyState, { viewModel.togglePolicy(this, it) }, BR.enabled) set(value) {
if (value != isEnabled)
@get:Bindable viewModel.togglePolicy(this, value)
var shouldNotify = item.notification
set(value) = set(value, field, { field = it }, BR.shouldNotify) {
viewModel.updatePolicy(updatedPolicy, isLogging = false)
} }
@get:Bindable @get:Bindable
var shouldLog = item.logging var shouldNotify
set(value) = set(value, field, { field = it }, BR.shouldLog) { get() = item.notification
viewModel.updatePolicy(updatedPolicy, isLogging = true) set(value) = set(value, shouldNotify, { item.notification = it }, BR.shouldNotify) {
viewModel.updatePolicy(item, isLogging = false)
} }
private val updatedPolicy @get:Bindable
get() = item.copy( var shouldLog
policy = if (policyState) SuPolicy.ALLOW else SuPolicy.DENY, get() = item.logging
notification = shouldNotify, set(value) = set(value, shouldLog, { item.logging = it }, BR.shouldLog) {
logging = shouldLog viewModel.updatePolicy(item, isLogging = true)
) }
fun toggleExpand() { fun toggleExpand() {
isExpanded = !isExpanded isExpanded = !isExpanded

View File

@ -47,12 +47,12 @@ class SuperuserViewModel(
return@launch return@launch
} }
state = State.LOADING state = State.LOADING
val (policies, diff) = withContext(Dispatchers.Default) { val (policies, diff) = withContext(Dispatchers.IO) {
db.deleteOutdated() db.deleteOutdated()
val policies = db.fetchAll { val policies = db.fetchAll().map {
PolicyRvItem(it, it.icon, this@SuperuserViewModel) PolicyRvItem(it, it.icon, this@SuperuserViewModel)
}.sortedWith(compareBy( }.sortedWith(compareBy(
{ it.item.appName.toLowerCase(currentLocale) }, { it.item.appName.lowercase(currentLocale) },
{ it.item.packageName } { it.item.packageName }
)) ))
policies to itemsPolicies.calculateDiff(policies) policies to itemsPolicies.calculateDiff(policies)
@ -107,14 +107,13 @@ class SuperuserViewModel(
fun togglePolicy(item: PolicyRvItem, enable: Boolean) { fun togglePolicy(item: PolicyRvItem, enable: Boolean) {
fun updateState() { fun updateState() {
item.policyState = enable
val policy = if (enable) SuPolicy.ALLOW else SuPolicy.DENY 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 { viewModelScope.launch {
db.update(app) db.update(item.item)
val res = if (app.policy == SuPolicy.ALLOW) R.string.su_snack_grant val res = if (item.item.policy == SuPolicy.ALLOW) R.string.su_snack_grant
else R.string.su_snack_deny else R.string.su_snack_deny
SnackbarEvent(res.asText(item.item.appName)).publish() SnackbarEvent(res.asText(item.item.appName)).publish()
} }

View File

@ -7,6 +7,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.Window import android.view.Window
import android.view.WindowManager import android.view.WindowManager
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.UIActivity import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.core.su.SuCallbackHandler 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.databinding.ActivityRequestBinding
import com.topjohnwu.magisk.di.viewModel import com.topjohnwu.magisk.di.viewModel
import com.topjohnwu.magisk.ui.theme.Theme import com.topjohnwu.magisk.ui.theme.Theme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
open class SuRequestActivity : UIActivity<ActivityRequestBinding>() { open class SuRequestActivity : UIActivity<ActivityRequestBinding>() {
@ -40,8 +44,12 @@ open class SuRequestActivity : UIActivity<ActivityRequestBinding>() {
if (action == REQUEST) { if (action == REQUEST) {
viewModel.handleRequest(intent) viewModel.handleRequest(intent)
} else { } else {
SuCallbackHandler.run(this, action, intent.extras) lifecycleScope.launch {
finish() withContext(Dispatchers.IO) {
SuCallbackHandler.run(this@SuRequestActivity, action, intent.extras)
}
finish()
}
} }
} else { } else {
finish() finish()

View File

@ -191,6 +191,7 @@ void app_notify(const su_context &ctx) {
vector<Extra> extras; vector<Extra> extras;
extras.reserve(2); extras.reserve(2);
extras.emplace_back("from.uid", ctx.info->uid); extras.emplace_back("from.uid", ctx.info->uid);
extras.emplace_back("pid", ctx.pid);
extras.emplace_back("policy", ctx.info->access.policy); extras.emplace_back("policy", ctx.info->access.policy);
exec_cmd("notify", extras, ctx.info); exec_cmd("notify", extras, ctx.info);
@ -198,21 +199,22 @@ void app_notify(const su_context &ctx) {
} }
} }
int app_request(const shared_ptr<su_info> &info) { int app_request(const su_context &ctx) {
// Create FIFO // Create FIFO
char fifo[64]; char fifo[64];
strcpy(fifo, "/dev/socket/"); strcpy(fifo, "/dev/socket/");
gen_rand_str(fifo + 12, 32, true); gen_rand_str(fifo + 12, 32, true);
mkfifo(fifo, 0600); 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"); setfilecon(fifo, "u:object_r:" SEPOL_FILE_TYPE ":s0");
// Send request // Send request
vector<Extra> extras; vector<Extra> extras;
extras.reserve(2); extras.reserve(2);
extras.emplace_back("fifo", fifo); extras.emplace_back("fifo", fifo);
extras.emplace_back("uid", info->eval_uid); extras.emplace_back("uid", ctx.info->eval_uid);
exec_cmd("request", extras, info, false); extras.emplace_back("pid", ctx.pid);
exec_cmd("request", extras, ctx.info, false);
// Wait for data input for at most 70 seconds // Wait for data input for at most 70 seconds
int fd = xopen(fifo, O_RDONLY | O_CLOEXEC | O_NONBLOCK); int fd = xopen(fifo, O_RDONLY | O_CLOEXEC | O_NONBLOCK);

View File

@ -60,4 +60,4 @@ struct su_context {
void app_log(const su_context &ctx); void app_log(const su_context &ctx);
void app_notify(const su_context &ctx); void app_notify(const su_context &ctx);
int app_request(const std::shared_ptr<su_info> &info); int app_request(const su_context &ctx);

View File

@ -193,20 +193,7 @@ static shared_ptr<su_info> get_su_info(unsigned uid) {
info->access = NO_SU_ACCESS; info->access = NO_SU_ACCESS;
return info; 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<policy_t>(ret);
close(fd);
}
return info; 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.shell);
read_string(client, ctx.req.command); 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<policy_t>(ret);
close(fd);
}
}
if (ctx.info->access.log) if (ctx.info->access.log)
app_log(ctx); app_log(ctx);
else if (ctx.info->access.notify) else if (ctx.info->access.notify)