Skip to content

Commit

Permalink
Use ContentProvider call method for communication
Browse files Browse the repository at this point in the history
Previously, we use either BroadcastReceivers or Activities to receive
messages from our native daemon, but both have their own downsides.
Some OEMs blocks broadcasts if the app is not running in the background,
regardless of who the caller is. Activities on the other hand, despite
working 100% of the time, will steal the focus of the current foreground
app, even though we are just doing some logging and showing a toast.
In addition, since stubs for hiding Magisk Manager is introduced, our
only communication method is left with the broadcast option, as
only broadcasting allows targeting a specific package name, not a
component name (which will be obfuscated in the case of stubs).

To make sure root requests will work on all devices, Magisk had to do
some experiments every boot to test whether broadcast is deliverable or
not. This makes the whole thing even more complicated then ever.

So lets take a look at another kind of component in Android apps:
ContentProviders. It is a vital part of Android's ecosystem, and as far
as I know no OEMs will block requests to ContentProviders (or else
tons of functionality will break catastrophically). Starting at API 11,
the system supports calling a specific method in ContentProviders,
optionally sending extra data along with the method call. This is
perfect for the native daemon to start a communication with Magisk
Manager. Another cool thing is that we no longer need to know the
component name of the reciever, as ContentProviders identify themselves
with an "authority" name, which in Magisk Manager's case is tied to the
package name. We already have a mechanism to keep track of our current
manager package name, so this works out of the box.

So yay! No more flaky broadcast tests, no more stupid OEMs blocking
broadcasts for some bizzare reasons. This method should in theory
work on almost all devices and situations.
  • Loading branch information
topjohnwu committed Nov 4, 2019
1 parent 472cde2 commit 25c5572
Show file tree
Hide file tree
Showing 19 changed files with 238 additions and 344 deletions.
2 changes: 2 additions & 0 deletions app/src/main/java/com/topjohnwu/magisk/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.topjohnwu.magisk.di.koinModules
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.unwrap
import com.topjohnwu.magisk.utils.RootInit
import com.topjohnwu.magisk.utils.SuHandler
import com.topjohnwu.magisk.utils.updateConfig
import com.topjohnwu.superuser.Shell
import org.koin.android.ext.koin.androidContext
Expand All @@ -34,6 +35,7 @@ open class App() : Application() {
Shell.Config.verboseLogging(BuildConfig.DEBUG)
Shell.Config.addInitializers(RootInit::class.java)
Shell.Config.setTimeout(2)
FileProvider.callHandler = SuHandler
Room.setFactory {
when (it) {
WorkDatabase::class.java -> WorkDatabase_Impl()
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/com/topjohnwu/magisk/Const.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ object Const {
const val MIN_VERSION = "v18.0"
const val MIN_VERCODE = 18000
const val CONNECT_MODE = 20100
const val PROVIDER_CONNECT = 20102
}

object ID {
Expand Down
5 changes: 0 additions & 5 deletions app/src/main/java/com/topjohnwu/magisk/Hacks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,15 @@ import android.content.res.AssetManager
import android.content.res.Configuration
import android.content.res.Resources
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import com.topjohnwu.magisk.extensions.langTagToLocale
import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
import com.topjohnwu.magisk.model.update.UpdateCheckService
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.ui.SplashActivity
import com.topjohnwu.magisk.ui.flash.FlashActivity
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
import com.topjohnwu.magisk.utils.currentLocale
import com.topjohnwu.magisk.utils.defaultLocale
import com.topjohnwu.magisk.utils.refreshLocale
import com.topjohnwu.magisk.utils.updateConfig
import java.util.*

fun AssetManager.addAssetPath(path: String) {
DynAPK.addAssetPath(this, path)
Expand Down
8 changes: 2 additions & 6 deletions app/src/main/java/com/topjohnwu/magisk/Info.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,13 @@ object Info {
val str = ShellUtils.fastCmd("magisk -v").split(":".toRegex())[0]
val code = ShellUtils.fastCmd("magisk -V").toInt()
val hide = Shell.su("magiskhide --status").exec().isSuccess
var mode = -1
if (code >= Const.Version.CONNECT_MODE)
mode = Shell.su("magisk --connect-mode").exec().code
Env(code, str, hide, mode)
Env(code, str, hide)
}.getOrElse { Env() }

class Env(
code: Int = -1,
val magiskVersionString: String = "",
hide: Boolean = false,
var connectionMode: Int = -1
hide: Boolean = false
) {
val magiskHide get() = Config.magiskHide
val magiskVersionCode = when (code) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,15 @@ fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
}

@Throws(PackageManager.NameNotFoundException::class)
fun Int.toPolicy(pm: PackageManager): MagiskPolicy {
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): MagiskPolicy {
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
?: throw PackageManager.NameNotFoundException()
val info = pm.getApplicationInfo(pkg, 0)
return MagiskPolicy(
uid = this,
packageName = pkg,
policy = policy,
applicationInfo = info,
appName = info.loadLabel(pm).toString()
appName = info.getLabel(pm)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,84 +2,41 @@ package com.topjohnwu.magisk.model.receiver

import android.content.ContextWrapper
import android.content.Intent
import android.os.Build.VERSION.SDK_INT
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.base.BaseReceiver
import com.topjohnwu.magisk.data.database.PolicyDao
import com.topjohnwu.magisk.data.database.base.su
import com.topjohnwu.magisk.extensions.reboot
import com.topjohnwu.magisk.extensions.startActivity
import com.topjohnwu.magisk.extensions.startActivityWithRoot
import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.entity.ManagerJson
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
import com.topjohnwu.magisk.utils.SuLogger
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.utils.SuHandler
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
import org.koin.core.inject
import timber.log.Timber

open class GeneralReceiver : BaseReceiver() {

private val policyDB: PolicyDao by inject()

companion object {
const val REQUEST = "request"
const val LOG = "log"
const val NOTIFY = "notify"
const val TEST = "test"
}

private fun getPkg(intent: Intent): String {
return intent.data?.encodedSchemeSpecificPart.orEmpty()
}

override fun onReceive(context: ContextWrapper, intent: Intent?) {
intent ?: return

// Debug messages
if (BuildConfig.DEBUG) {
Timber.d(intent.action)
intent.extras?.let { bundle ->
bundle.keySet().forEach {
Timber.d("[%s]=[%s]", it, bundle[it])
}
}
}

when (intent.action ?: return) {
Intent.ACTION_REBOOT -> {
when (val action = intent.getStringExtra("action") ?: return) {
REQUEST -> {
val i = context.intent<SuRequestActivity>()
.setAction(action)
.putExtra("socket", intent.getStringExtra("socket"))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
if (SDK_INT >= 29) {
// Android Q does not allow starting activity from background
i.startActivityWithRoot()
} else {
i.startActivity(context)
}
}
LOG -> SuLogger.handleLogs(context, intent)
NOTIFY -> SuLogger.handleNotify(context, intent)
TEST -> {
val mode = intent.getIntExtra("mode", 1 shl 1)
if (mode > Info.env.connectionMode)
Info.env.connectionMode = mode
Shell.su("magisk --connect-mode $mode").submit()
}
}
SuHandler(context, intent.getStringExtra("action"), intent.extras)
}
Intent.ACTION_PACKAGE_REPLACED ->
Intent.ACTION_PACKAGE_REPLACED -> {
// This will only work pre-O
if (Config.suReAuth)
policyDB.delete(getPkg(intent)).blockingGet()
}
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
val pkg = getPkg(intent)
policyDB.delete(pkg).blockingGet()
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package com.topjohnwu.magisk.ui
import android.app.Activity
import android.content.Context
import android.os.Bundle
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.intent
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.magisk.wrap
import com.topjohnwu.superuser.Shell

open class SplashActivity : Activity() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import com.topjohnwu.magisk.databinding.ActivityRequestBinding
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.events.DieEvent
import com.topjohnwu.magisk.model.events.ViewEvent
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
import com.topjohnwu.magisk.utils.SuLogger
import com.topjohnwu.magisk.utils.SuHandler
import org.koin.androidx.viewmodel.ext.android.viewModel

open class SuRequestActivity : BaseActivity<SuRequestViewModel, ActivityRequestBinding>() {
Expand All @@ -29,19 +28,13 @@ open class SuRequestActivity : BaseActivity<SuRequestViewModel, ActivityRequestB
lockOrientation()
super.onCreate(savedInstanceState)

val intent = intent

when (intent?.action) {
GeneralReceiver.REQUEST -> {
if (!viewModel.handleRequest(intent))
finish()
return
}
GeneralReceiver.LOG -> SuLogger.handleLogs(this, intent)
GeneralReceiver.NOTIFY -> SuLogger.handleNotify(this, intent)
if (intent?.action == SuHandler.REQUEST) {
if (!viewModel.handleRequest(intent))
finish()
} else {
SuHandler(this, intent.action, intent.extras)
finish()
}

finish()
}

override fun onEventDispatched(event: ViewEvent) {
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/com/topjohnwu/magisk/utils/PatchAPK.kt
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ object PatchAPK {
}

private fun patchAndHide(context: Context, label: String): Boolean {
// If not running as stub, and we are compatible with stub, use stub
val src = if (!isRunningAsStub && SDK_INT >= 28 && Info.env.connectionMode == 3) {
val src = if (!isRunningAsStub && SDK_INT >= 28 &&
Info.env.magiskVersionCode >= Const.Version.PROVIDER_CONNECT) {
// If not running as stub, and we are compatible with stub, use stub
val stub = File(context.cacheDir, "stub.apk")
val svc = get<GithubRawServices>()
runCatching {
Expand Down
137 changes: 137 additions & 0 deletions app/src/main/java/com/topjohnwu/magisk/utils/SuHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package com.topjohnwu.magisk.utils

import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Process
import android.widget.Toast
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.data.repository.LogRepository
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.startActivity
import com.topjohnwu.magisk.extensions.startActivityWithRoot
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.toLog
import com.topjohnwu.magisk.model.entity.toPolicy
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
import com.topjohnwu.superuser.Shell
import timber.log.Timber
import java.util.*

object SuHandler : ProviderCallHandler {

const val REQUEST = "request"
const val LOG = "log"
const val NOTIFY = "notify"
const val TEST = "test"

override fun call(context: Context, method: String, arg: String?, extras: Bundle?): Bundle? {
invoke(context.wrap(), method, extras)
return null
}

operator fun invoke(context: Context, action: String?, data: Bundle?) {
data ?: return

// Debug messages
if (BuildConfig.DEBUG) {
Timber.d(action)
data.let { bundle ->
bundle.keySet().forEach {
Timber.d("[%s]=[%s]", it, bundle[it])
}
}
}

when (action) {
REQUEST -> {
val intent = context.intent<SuRequestActivity>()
.setAction(action)
.putExtras(data)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
if (Build.VERSION.SDK_INT >= 29) {
// Android Q does not allow starting activity from background
intent.startActivityWithRoot()
} else {
intent.startActivity(context)
}
}
LOG -> handleLogs(context, data)
NOTIFY -> handleNotify(context, data)
TEST -> {
val mode = data.getInt("mode", 2)
Shell.su(
"magisk --connect-mode $mode",
"magisk --use-broadcast"
).submit()
}
}
}

private fun Any?.toInt(): Int? {
return when (this) {
is Int -> this
is Long -> this.toInt()
else -> null
}
}

private fun handleLogs(context: Context, data: Bundle) {
val fromUid = data["from.uid"].toInt() ?: return
if (fromUid == Process.myUid())
return

val pm = context.packageManager

val notify = data.getBoolean("notify", true)
val allow = data["policy"].toInt() ?: return

val policy = runCatching { fromUid.toPolicy(pm, allow) }.getOrElse { return }

if (notify)
notify(context, policy)

val toUid = data["to.uid"].toInt() ?: return
val pid = data["pid"].toInt() ?: return

val command = data.getString("command") ?: return
val log = policy.toLog(
toUid = toUid,
fromPid = pid,
command = command,
date = Date()
)

val logRepo = get<LogRepository>()
logRepo.put(log).subscribeK(onError = { Timber.e(it) })
}

private fun handleNotify(context: Context, data: Bundle) {
val fromUid = data["from.uid"].toInt() ?: return
if (fromUid == Process.myUid())
return

val pm = context.packageManager
val allow = data["policy"].toInt() ?: return

runCatching {
val policy = fromUid.toPolicy(pm, allow)
if (policy.policy >= 0)
notify(context, policy)
}
}

private fun notify(context: Context, policy: MagiskPolicy) {
if (policy.notification && Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
val resId = if (policy.policy == MagiskPolicy.ALLOW)
R.string.su_allow_toast
else
R.string.su_deny_toast

Utils.toast(context.getString(resId, policy.appName), Toast.LENGTH_SHORT)
}
}
}
Loading

0 comments on commit 25c5572

Please sign in to comment.