Skip to content

Commit

Permalink
Work around READ_CALL_LOG being hard-restricted
Browse files Browse the repository at this point in the history
Since Android 10, READ_CALL_LOG has been marked as a hard-restricted
permission, which prevents it from being granted by the user unless the
app is given an exemption. There are three types of exemptions: system,
upgrade, and installer.

System exemptions can be given by `/system/etc/default-permissions`
(only on first boot) or via roles. Neither are usable for BCR. Upgrade
exemptions are only given during Android OS upgrades that further
restrict existing permissions. Installer exemptions are given by
whichever app installs another app, but there's no installer when it
comes to adding new system apps, like BCR.

Since AOSP has no sane built-in way to exempt BCR from the hard
restriction, we'll do it ourselves. This commit introduces a new CLI
utility baked in BCR that will talk to PermissionManager (Android 11+)
or PackageManager (Android 10) to adjust its permission flags. This will
unfortunately require two flashes (but only one reboot) for the initial
install, because BCR must have already been loaded by the package
manager for the flags to be changed. However, once the exemption has
been granted, it persists across future upgrades.

Fixes: #304

Signed-off-by: Andrew Gunnerson <[email protected]>
  • Loading branch information
chenxiaolong committed Apr 21, 2023
1 parent 0194da9 commit 62324b3
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 0 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ As the name alludes, BCR intends to be a basic as possible. The project will hav
* **For OnePlus and Realme devices running the stock firmware (or custom firmware based on the stock firmware)**, also extract the `.apk` from the zip and install it manually before rebooting. This is necessary to work around a bug in the firmware where the app data directory does not get created, causing BCR to open up to a blank screen.

* **For unrooted custom firmware**, flash the zip while booted into recovery.
* **NOTE**: The `READ_CALL_LOG` permission is hard restricted in Android 10+, which prevents it from being granted, even via Android's settings. To remove this restriction, run via adb:
```bash
# If rooted, run inside of `su`:
CLASSPATH=/system/priv-app/com.chiller3.bcr/app-release.apk app_process / com.chiller3.bcr.standalone.RemoveHardRestrictionsKt

# If unrooted, install BCR as both a user app and a system app:
pm install /system/priv-app/com.chiller3.bcr/app-release.apk
```
* **NOTE**: If the custom firmware's `system` partition is formatted with `erofs`, then the filesystem is read-only and it is not possible to use this method.
* Manually extracting the files from the `system/` folder in the zip will also work as long as the files have `644` permissions and the `u:object_r:system_file:s0` SELinux label.
Expand Down
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,8 @@ android.applicationVariants.all {
}
}

from(File(magiskDir, "customize.sh"))

from(File(rootDir, "LICENSE"))
from(File(rootDir, "README.md"))
}
Expand Down
24 changes: 24 additions & 0 deletions app/magisk/customize.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# READ_CALL_LOG is a hard-restricted permission in Android 10+. It cannot be
# granted by the user unless it is exempted by the system. The most common way
# to do this is via the installer, but that's not applicable when adding new
# system apps. Instead, we talk to the permission service directly over binder
# to alter the flags. This requires flashing a second time after a reboot so
# that the package manager is already aware of BCR.

app_id=$(grep '^id=' "${MODPATH}/module.prop" | cut -d= -f2)

CLASSPATH=$(find "${MODPATH}"/system/priv-app/"${app_id}" -name '*.apk') \
app_process \
/ \
com.chiller3.bcr.standalone.RemoveHardRestrictionsKt \
2>&1

case "${?}" in
0|2)
exit 0
;;
*)
rm -rv "${MODPATH}" 2>&1
exit 1
;;
esac
5 changes: 5 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@
-keepclassmembers class androidx.documentfile.provider.TreeDocumentFile {
<init>(androidx.documentfile.provider.DocumentFile, android.content.Context, android.net.Uri);
}

# Keep standalone CLI utilities
-keep class com.chiller3.bcr.standalone.* {
*;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
@file:SuppressLint(
"BlockedPrivateApi",
"DiscouragedPrivateApi",
"PrivateApi",
"SoonBlockedPrivateApi",
)

package com.chiller3.bcr.standalone

import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Build
import android.os.Process
import android.system.ErrnoException
import androidx.annotation.RequiresApi
import com.chiller3.bcr.BuildConfig
import kotlin.system.exitProcess

private object ActivityThreadProxy {
private val CLS = Class.forName("android.app.ActivityThread")
private val METHOD_GET_PACKAGE_MANAGER = CLS.getDeclaredMethod("getPackageManager")
private val METHOD_GET_PERMISSION_MANAGER = CLS.getDeclaredMethod("getPermissionManager")

fun getPackageManager(): PackageManagerProxy {
val iface = METHOD_GET_PACKAGE_MANAGER.invoke(null)!!
return PackageManagerProxy(iface)
}

@RequiresApi(Build.VERSION_CODES.R)
fun getPermissionManager(): PermissionManagerProxy {
val iface = METHOD_GET_PERMISSION_MANAGER.invoke(null)!!
return PermissionManagerProxy(iface)
}
}

private class PackageManagerProxy(private val iface: Any) {
companion object {
private val CLS = Class.forName("android.content.pm.IPackageManager")
private val METHOD_IS_PACKAGE_AVAILABLE = CLS.getDeclaredMethod(
"isPackageAvailable", String::class.java, Int::class.java)
// Android 10 only
private val METHOD_GET_PERMISSION_FLAGS by lazy {
CLS.getDeclaredMethod(
"getPermissionFlags",
String::class.java,
String::class.java,
Int::class.java,
)
}
// Android 10 only
private val METHOD_UPDATE_PERMISSION_FLAGS by lazy {
CLS.getDeclaredMethod(
"updatePermissionFlags",
String::class.java,
String::class.java,
Int::class.java,
Int::class.java,
Int::class.java,
)
}

private val WRAPPER_CLS = PackageManager::class.java
val FLAG_PERMISSION_APPLY_RESTRICTION = WRAPPER_CLS.getDeclaredField(
"FLAG_PERMISSION_APPLY_RESTRICTION").getInt(null)
val FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT = WRAPPER_CLS.getDeclaredField(
"FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT").getInt(null)
val FLAG_PERMISSION_RESTRICTION_ANY_EXEMPT = WRAPPER_CLS.getDeclaredField(
"FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT").getInt(null)
}

fun isPackageAvailable(packageName: String, userId: Int): Boolean {
return METHOD_IS_PACKAGE_AVAILABLE.invoke(iface, packageName, userId) as Boolean
}

fun getPermissionFlags(permissionName: String, packageName: String, userId: Int): Int {
return METHOD_GET_PERMISSION_FLAGS.invoke(iface, permissionName, packageName, userId) as Int
}

fun updatePermissionFlags(
permissionName: String,
packageName: String,
flagMask: Int,
flagValues: Int,
userId: Int,
) {
METHOD_UPDATE_PERMISSION_FLAGS.invoke(
iface,
permissionName,
packageName,
flagMask,
flagValues,
userId,
)
}

}

@RequiresApi(Build.VERSION_CODES.R)
private class PermissionManagerProxy(private val iface: Any) {
companion object {
private val CLS = Class.forName("android.permission.IPermissionManager")
private val METHOD_GET_PERMISSION_FLAGS =
CLS.getDeclaredMethod(
"getPermissionFlags",
String::class.java,
String::class.java,
Int::class.java,
)
private val METHOD_UPDATE_PERMISSION_FLAGS =
CLS.getDeclaredMethod(
"updatePermissionFlags",
String::class.java,
String::class.java,
Int::class.java,
Int::class.java,
Boolean::class.java,
Int::class.java,
)
}

fun getPermissionFlags(packageName: String, permissionName: String, userId: Int): Int {
return METHOD_GET_PERMISSION_FLAGS(iface, packageName, permissionName, userId) as Int
}

fun updatePermissionFlags(
packageName: String,
permissionName: String,
flagMask: Int,
flagValues: Int,
checkAdjustPolicyFlagPermission: Boolean,
userId: Int,
) {
METHOD_UPDATE_PERMISSION_FLAGS.invoke(
iface,
packageName,
permissionName,
flagMask,
flagValues,
checkAdjustPolicyFlagPermission,
userId,
)
}
}

private fun switchToSystemUid() {
if (Process.myUid() != Process.SYSTEM_UID) {
val setUid = Process::class.java.getDeclaredMethod("setUid", Int::class.java)
val errno = setUid.invoke(null, Process.SYSTEM_UID) as Int

if (errno != 0) {
throw Exception("Failed to switch to SYSTEM (${Process.SYSTEM_UID}) user",
ErrnoException("setuid", errno))
}
if (Process.myUid() != Process.SYSTEM_UID) {
throw IllegalStateException("UID didn't actually change: " +
"${Process.myUid()} != ${Process.SYSTEM_UID}")
}
}
}

@Suppress("SameParameterValue")
private fun removeRestriction(packageName: String, permission: String, userId: Int): Boolean {
val packageManager = ActivityThreadProxy.getPackageManager()
if (!packageManager.isPackageAvailable(packageName, userId)) {
throw IllegalArgumentException("Package $packageName is not installed for user $userId")
}

val (getFlags, updateFlags) = if (Build.VERSION.SDK_INT in
Build.VERSION_CODES.R..Build.VERSION_CODES.TIRAMISU) {
val permissionManager = ActivityThreadProxy.getPermissionManager()

Pair(
{ permissionManager.getPermissionFlags(packageName, permission, userId) },
{ mask: Int, set: Int ->
permissionManager.updatePermissionFlags(
packageName, permission, mask, set, false, userId)
},
)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Pair(
{ packageManager.getPermissionFlags(permission, packageName, userId) },
{ mask: Int, set: Int ->
packageManager.updatePermissionFlags(permission, packageName, mask, set, userId)
},
)
} else {
throw IllegalStateException("Not supported on SDK version ${Build.VERSION.SDK_INT}")
}

val oldFlags = getFlags()

updateFlags(
PackageManagerProxy.FLAG_PERMISSION_RESTRICTION_ANY_EXEMPT or
PackageManagerProxy.FLAG_PERMISSION_APPLY_RESTRICTION,
PackageManagerProxy.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT,
)

val newFlags = getFlags()
if (newFlags and PackageManagerProxy.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT == 0) {
throw IllegalStateException("RESTRICTION_SYSTEM_EXEMPT flag did not get added")
}
if (newFlags and PackageManagerProxy.FLAG_PERMISSION_APPLY_RESTRICTION != 0) {
throw IllegalStateException("APPLY_RESTRICTION flag did not get removed")
}

return newFlags != oldFlags
}

fun mainInternal() {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
// Android 9 does not have FLAG_PERMISSION_APPLY_RESTRICTION
System.err.println("Android 9 does not have hard-restricted permissions")
return
}

switchToSystemUid()

val packageManager = ActivityThreadProxy.getPackageManager()
if (!packageManager.isPackageAvailable(BuildConfig.APPLICATION_ID, 0)) {
System.err.println("""
---------------- NOTE ----------------
Android 10+ marks the READ_CALL_LOG
permission as being hard restricted.
This makes it impossible to grant the
(optional) permission, even from
Android's settings. To remove this
restriction for BCR only, reboot and
reflash one more time. This procedure
only needs to be done once and will
persist across upgrades. This does not
affect other apps and the changes go
away when BCR is uninstalled.
--------------------------------------
""".trimIndent())
exitProcess(2)
}

val changed = removeRestriction(BuildConfig.APPLICATION_ID, Manifest.permission.READ_CALL_LOG, 0)
val suffix = "from ${BuildConfig.APPLICATION_ID} for ${Manifest.permission.READ_CALL_LOG}"

if (changed) {
println("Successfully removed hard restriction $suffix")
} else {
println("Hard restriction already removed $suffix")
}
}

fun main() {
try {
mainInternal()
} catch (e: Exception) {
// Otherwise, exceptions go to the logcat
e.printStackTrace()
exitProcess(1)
}
}

0 comments on commit 62324b3

Please sign in to comment.