Skip to content

自定义权限

解析自定义权限注册、权限树、动态权限和定义冲突时的状态处理。

AndroidPMSPackageManager权限

自定义权限 ​

自定义权限不是在 manifest 里写一行 permission 就结束。Android 17 会在包扫描时把声明转换为 Permission,按权限树校验命名空间,再与已有定义进行所有者和保护级别调和;运行时动态权限还要经过 Binder 调用者、权限树归属和大小上限检查。

本文聚焦四条源码链:manifest 权限注册、权限树匹配、addPermission/removePermission 动态操作,以及权限定义改变后的状态清理。

1. 三种来源 ​

来源Permission.type注册入口可否运行时修改
manifest <permission>manifestAppIdPermissionPolicy.addPermissions否
manifest <permission-tree>manifestaddPermissionTree树本身不可动态改名
addPermission()dynamicPermissionService.addPermission可更新/删除动态权限

动态权限不是任意包都能创建。调用者必须拥有覆盖该名字的 permission tree,且不能修改同名的非动态权限。

2. 注册入口 ​

源码文件:frameworks/base/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt

kotlin
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

kotlin
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

kotlin
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 树所有者检查 ​

kotlin
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 权限树大小上限 ​

kotlin
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

kotlin
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

kotlin
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

kotlin
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

kotlin
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

kotlin
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. 源码阅读路线 ​

  1. AppIdPermissionPolicy.addPermissions:包扫描时注册 manifest permission/tree。
  2. findPermissionTree:确认树名边界与 owner 查找。
  3. PermissionService.addPermission/removePermission:阅读动态权限安全检查、大小上限和写入模式。
  4. Permission.TYPE_DYNAMIC 与 policy map:确认动态对象如何存储和删除。
  5. updatePermissionIfDynamic:理解状态整理时如何重新绑定 tree owner。
  6. addPermissions 中的 changed permission 清理:观察权限定义变化如何影响所有用户和 appId。

自定义权限的核心不是一个字符串,而是名称、tree owner、保护级别、appId 和用户 flags 共同组成的状态对象。动态权限获得了受限写入口,但没有绕过权限注册、所有权和状态清理规则。