自定义权限
自定义权限不是在 manifest 里写一行 permission 就结束。Android 17 会在包扫描时把声明转换为 Permission,按权限树校验命名空间,再与已有定义进行所有者和保护级别调和;运行时动态权限还要经过 Binder 调用者、权限树归属和大小上限检查。
本文聚焦四条源码链:manifest 权限注册、权限树匹配、addPermission/removePermission 动态操作,以及权限定义改变后的状态清理。
1. 三种来源
| 来源 | Permission.type | 注册入口 | 可否运行时修改 |
|---|---|---|---|
manifest <permission> | manifest | AppIdPermissionPolicy.addPermissions | 否 |
manifest <permission-tree> | manifest | addPermissionTree | 树本身不可动态改名 |
addPermission() | dynamic | PermissionService.addPermission | 可更新/删除动态权限 |
动态权限不是任意包都能创建。调用者必须拥有覆盖该名字的 permission tree,且不能修改同名的非动态权限。
2. 注册入口
源码文件:frameworks/base/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
private fun MutateStateScope.addPermissions(
packageState: PackageState,
changedPermissionNames: MutableIndexedSet<String>,
) {
val androidPackage = packageState.androidPackage!!
androidPackage.permissions.forEachIndexed { _, parsedPermission ->
val newPermissionInfo = PackageInfoUtils.generatePermissionInfo(
parsedPermission, PackageManager.GET_META_DATA.toLong())!!
val permissionName = newPermissionInfo.name
val oldPermission = if (parsedPermission.isTree) {
newState.systemState.permissionTrees[permissionName]
} else {
newState.systemState.permissions[permissionName]
}
val permissionTree = findPermissionTree(permissionName)
val newPackageName = newPermissionInfo.packageName
if (permissionTree != null &&
newPackageName != permissionTree.packageName
) {
Slog.w(LOG_TAG, "Ignoring permission $permissionName declared in " +
"package $newPackageName: base permission tree " +
"${permissionTree.name} is declared in another package")
return@forEachIndexed
}
// 后续处理已有定义、签名和 protection 变化
}
}调用方是包扫描/包更新时的权限策略,owner 是 AppIdPermissionPolicy,消费者是 systemState.permissions 与 permissionTrees。先判断树归属,再处理同名定义,避免一个包把另一个包树下的名字注册进来。
3. 权限树匹配
源码文件:frameworks/base/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
fun GetStateScope.findPermissionTree(permissionName: String): Permission? =
state.systemState.permissionTrees.firstNotNullOfOrNullIndexed {
_, permissionTreeName, permissionTree ->
if (
permissionName.startsWith(permissionTreeName) &&
permissionName.length > permissionTreeName.length &&
permissionName[permissionTreeName.length] == '.'
) {
permissionTree
} else {
null
}
}匹配不是简单的 startsWith:权限名必须更长,且树名后紧跟 .。树 com.example.permission 能覆盖 com.example.permission.READ,不能覆盖 com.example.permissions.READ,也不能把树本身当作动态子权限。
4. 动态添加检查
源码文件:frameworks/base/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
override fun addPermission(permissionInfo: PermissionInfo, async: Boolean): Boolean {
val permissionName = permissionInfo.name
requireNotNull(permissionName) { "permissionName cannot be null" }
val callingUid = Binder.getCallingUid()
if (packageManagerLocal.withUnfilteredSnapshot()
.use { it.isUidInstantApp(callingUid) }) {
throw SecurityException("Instant apps cannot add permissions")
}
if (permissionInfo.labelRes == 0 &&
permissionInfo.nonLocalizedLabel == null
) {
throw SecurityException("Label must be specified in permission")
}
service.mutateState {
val permissionTree = getAndEnforcePermissionTree(permissionName)
enforcePermissionTreeSize(permissionInfo, permissionTree)
val oldPermission = with(policy) { getPermissions()[permissionName] }
if (oldPermission != null && !oldPermission.isDynamic) {
throw SecurityException(
"Not allowed to modify non-dynamic permission $permissionName"
)
}
permissionInfo.packageName = permissionTree.permissionInfo.packageName
permissionInfo.protectionLevel =
PermissionInfo.fixProtectionLevel(permissionInfo.protectionLevel)
val newPermission = Permission(
permissionInfo, true, Permission.TYPE_DYNAMIC, permissionTree.appId)
with(policy) { addPermission(newPermission, !async) }
}
return oldPermission == null
}检查顺序体现安全边界:instant app 禁止调用;动态权限必须有 label;名字必须落在调用者拥有的树下;同名 manifest 权限不能被动态修改。async 只决定写入模式,返回值表示是否新建,不表示是否授权给某个应用。
4.1 树所有者检查
private fun GetStateScope.getAndEnforcePermissionTree(
permissionName: String
): Permission {
val callingUid = Binder.getCallingUid()
val permissionTree = with(policy) { findPermissionTree(permissionName) }
if (permissionTree != null &&
permissionTree.appId == UserHandle.getAppId(callingUid)
) {
return permissionTree
}
throw SecurityException(
"Calling UID $callingUid is not allowed to add to or remove from " +
"the permission tree"
)
}比较的是 appId,不是完整 UID。这样同一应用在不同 user 下仍归属于同一个 tree owner;不同应用即使知道树名,也不能动态添加子权限。
4.2 权限树大小上限
private fun GetStateScope.enforcePermissionTreeSize(
permissionInfo: PermissionInfo,
permissionTree: Permission,
) {
if (permissionTree.appId != Process.SYSTEM_UID) {
val footprint = calculatePermissionTreeFootprint(permissionTree)
if (footprint + permissionInfo.calculateFootprint() >
MAX_PERMISSION_TREE_FOOTPRINT
) {
throw SecurityException("Permission tree size cap exceeded")
}
}
}非 system UID 的 tree 受 footprint 上限约束。系统会把该 appId 已有权限的 footprint 加上新 PermissionInfo,防止应用通过动态权限树无限扩大注册状态;system UID 走特权例外路径。
5. 动态权限删除
源码文件:frameworks/base/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
override fun removePermission(permissionName: String) {
val callingUid = Binder.getCallingUid()
if (packageManagerLocal.withUnfilteredSnapshot()
.use { it.isUidInstantApp(callingUid) }) {
throw SecurityException("Instant applications don't have access")
}
service.mutateState {
getAndEnforcePermissionTree(permissionName)
val permission = with(policy) { getPermissions()[permissionName] }
?: return@mutateState
if (!permission.isDynamic) {
throw SecurityException(
"Not allowed to modify non-dynamic permission $permissionName"
)
}
with(policy) { removePermission(permission) }
}
}删除先做 tree owner 校验,再拒绝删除 manifest 权限。未知动态权限是幂等返回;动态权限从 policy map 移除后,后续查询不再把它当作已注册权限。
源码文件:frameworks/base/services/permission/java/com/android/server/permission/access/permission/Permission.kt
data class Permission(
val permissionInfo: PermissionInfo,
val isReconciled: Boolean,
val type: Int,
val appId: Int,
val gids: IntArray = EmptyArray.INT,
val areGidsPerUser: Boolean = false,
) {
inline val isDynamic: Boolean
get() = type == TYPE_DYNAMIC
}动态权限以 TYPE_DYNAMIC 存储,owner appId 使用 tree 的 appId,而不是调用者随意传入的 packageName。这样 tree 重新扫描后,策略仍能把动态权限归回 tree owner。
6. 所有权冲突
源码文件:frameworks/base/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
if (oldPermission != null &&
newPackageName != oldPermission.packageName
) {
if (!packageState.isSystem) {
Slog.w(
LOG_TAG,
"Ignoring permission $permissionName declared in package " +
"$newPackageName: already declared in another package " +
oldPermission.packageName,
)
return@forEachIndexed
}
if (
oldPermission.isReconciled &&
newState.externalState.packageStates[
oldPermission.packageName]?.isSystem == true
) {
Slog.w(LOG_TAG, "Ignoring permission declared in another system package")
return@forEachIndexed
}
}普通应用不能抢占已有权限;新声明者是 system app 时,也不能覆盖一个已经由 system app 调和过的定义。冲突发生在注册阶段,不是等到 checkPermission 时才决定 owner。权限名落在别人的 tree 下时,policy 同样直接忽略该声明。
7. 动态权限重新归属
源码文件:frameworks/base/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
private fun MutateStateScope.updatePermissionIfDynamic(
permission: Permission
): Permission {
if (!permission.isDynamic) return permission
val permissionTree = findPermissionTree(permission.name)
?: return permission
@Suppress("DEPRECATION")
return permission.copy(
permissionInfo = PermissionInfo(permission.permissionInfo).apply {
packageName = permissionTree.packageName
},
appId = permissionTree.appId,
isReconciled = true,
)
}状态整理时,dynamic permission 会重新查找 tree,并把 packageName、appId 和 reconciled 状态对齐到 tree owner。若 tree 已不存在,函数保持原对象,不会凭空创建新的 owner。
8. 定义变化后的清理
源码文件:frameworks/base/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
if (isPermissionGroupChanged || isPermissionProtectionChanged) {
newState.externalState.userIds.forEachIndexed { _, userId ->
newState.externalState.appIdPackageNames.forEachIndexed { _, appId, _ ->
setPermissionFlags(appId, userId, permissionName, 0)
}
}
}权限组改变,或保护级别从非 runtime 变成 runtime 等情况,会清除所有 appId/user 的该权限 flags。旧 flags 可能代表旧的自动授予或检查语义,继续保留会把旧定义的授权带入新定义。
9. 源码阅读路线
AppIdPermissionPolicy.addPermissions:包扫描时注册 manifest permission/tree。findPermissionTree:确认树名边界与 owner 查找。PermissionService.addPermission/removePermission:阅读动态权限安全检查、大小上限和写入模式。Permission.TYPE_DYNAMIC与 policy map:确认动态对象如何存储和删除。updatePermissionIfDynamic:理解状态整理时如何重新绑定 tree owner。addPermissions中的 changed permission 清理:观察权限定义变化如何影响所有用户和 appId。
自定义权限的核心不是一个字符串,而是名称、tree owner、保护级别、appId 和用户 flags 共同组成的状态对象。动态权限获得了受限写入口,但没有绕过权限注册、所有权和状态清理规则。
