权限调试
权限问题的难点不在命令名称,而在于命令查看的是哪一层状态。pm grant 修改 runtime permission flags,dumpsys permissionmgr 展示 AccessCheckingService 的系统与用户状态,pm list permissions 查看注册定义,而 AppOps 又是另一张表。本文沿 Android 17 源码把这些输出对应起来。
1. 调试对象分层
| 现象 | 应查看的状态 | 入口 |
|---|---|---|
| 权限是否注册 | systemState.permissions | pm list permissions |
| 包是否获授 | appIdPermissionFlags | dumpsys permissionmgr --package |
| AppOps 是否放行 | appId/package AppOp mode | dumpsys permissionmgr |
| 特权权限失败 | allowlist 与 protection policy | 白名单查询、启动日志 |
| GID 未更新 | getGidsForUid 与 UID 生命周期 | permission service |
2. 授予撤销入口
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
private int runGrantRevokePermission(boolean grant) throws RemoteException {
int userId = UserHandle.USER_SYSTEM;
String opt;
boolean allPermissions = false;
while ((opt = getNextOption()) != null) {
if (opt.equals("--user")) {
userId = UserHandle.parseUserArg(getNextArgRequired());
}
if (opt.equals("--all-permissions")) {
allPermissions = true;
}
}
String pkg = getNextArg();
if (!allPermissions && pkg == null) {
getErrPrintWriter().println("Error: no package specified");
return 1;
}
String perm = getNextArg();
if (!allPermissions && perm == null) {
getErrPrintWriter().println("Error: no permission specified");
return 1;
}
if (allPermissions && perm != null) {
getErrPrintWriter().println("Error: permission specified but not expected");
return 1;
}
final UserHandle translatedUser = UserHandle.of(translateUserId(
userId, UserHandle.USER_NULL, "runGrantRevokePermission"));默认用户是 system user;--user 先经过 translateUserId。参数错误在 shell 层返回 1,不会进入权限策略。--all-permissions 不是一个独立的“授予全部”后端接口,而是先枚举包,再逐个调用单权限 API。
List<PackageInfo> packageInfos;
PackageManager pm = mContext.createContextAsUser(
translatedUser, 0).getPackageManager();
if (pkg == null) {
packageInfos = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS);
} else {
try {
packageInfos = Collections.singletonList(
pm.getPackageInfo(pkg, PackageManager.GET_PERMISSIONS));
} catch (NameNotFoundException e) {
getErrPrintWriter().println("Error: package not found");
getOutPrintWriter().println("Failure [package not found]");
return 1;
}
}
for (PackageInfo packageInfo : packageInfos) {
List<String> permissions = Collections.singletonList(perm);
if (allPermissions) {
permissions = getRequestedRuntimePermissions(packageInfo);
}
for (String permission : permissions) {
if (grant) {
mPermissionManager.grantRuntimePermission(
packageInfo.packageName, permission, translatedUser);
} else {
mPermissionManager.revokeRuntimePermission(
packageInfo.packageName, permission, translatedUser, null);
}
}
}
return 0;指定包模式异常会返回调用失败;批量模式对单个权限异常记录 warning 后继续。因此批量命令返回 0 不等于每个权限都成功。
3. 批量筛选
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
private List<String> getRequestedRuntimePermissions(PackageInfo info) {
if (info.requestedPermissions == null) {
return new ArrayList<>();
}
List<String> result = new ArrayList<>();
PackageManager pm = mContext.getPackageManager();
for (String permission : info.requestedPermissions) {
PermissionInfo pi;
try {
pi = pm.getPermissionInfo(permission, 0);
} catch (NameNotFoundException e) {
continue;
}
if (pi.getProtection() != PermissionInfo.PROTECTION_DANGEROUS) {
continue;
}
result.add(permission);
}
return result;
}--all-permissions 只保留 dangerous 权限。normal、signature、privileged 权限不应通过该参数推断授予结果,而应回到各自 protection policy 排查。
4. 状态修改
源码文件:frameworks/base/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
override fun grantRuntimePermission(
packageName: String,
permissionName: String,
deviceId: String,
userId: Int,
) {
setRuntimePermissionGranted(
packageName, userId, permissionName, deviceId, isGranted = true)
}
override fun revokeRuntimePermission(
packageName: String,
permissionName: String,
deviceId: String,
userId: Int,
reason: String?,
) {
setRuntimePermissionGranted(
packageName, userId, permissionName, deviceId, isGranted = false,
reason = reason)
}两个 Binder 接口共享 setRuntimePermissionGranted,区别只是目标状态。调用方可以是 shell 或 PermissionController,消费者是 appId/user/device 维度的 flags。
5. dump 输出
源码文件:frameworks/base/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>?) {
if (!DumpUtils.checkDumpPermission(context, LOG_TAG, pw)) return
val writer = IndentingPrintWriter(pw, " ")
if (args.isNullOrEmpty() || args[0] == "-a") {
service.getState {
writer.dumpSystemState(state)
getAllAppIdPackageNames(state).forEachIndexed { _, appId, names ->
if (appId != Process.INVALID_UID) {
writer.dumpAppIdState(appId, state, names)
}
}
}
} else if (args[0] == "--app-id" && args.size == 2) {
val appId = args[1].toInt()
service.getState {
val names = getAllAppIdPackageNames(state)
if (appId in names) writer.dumpAppIdState(appId, state, names[appId])
else writer.println("Unknown app ID $appId.")
}
} else if (args[0] == "--package" && args.size == 2) {
val packageName = args[1]
service.getState {
val packageState = state.externalState.packageStates[packageName]
if (packageState != null) {
writer.dumpAppIdState(
packageState.appId, state, indexedSetOf(packageName))
} else writer.println("Unknown package $packageName.")
}
} else {
writer.println("Usage: dumpsys permissionmgr [--app-id <APP_ID>] " +
"[--package <PACKAGE_NAME>]")
}
}dump 先经过 DumpUtils.checkDumpPermission。--package 由 package state 找 appId 后输出用户状态;shared UID 下可能看到同 UID 其他包的影响。--app-id 适合排查共享 UID,-a 才输出系统定义和全部 appId。
5.1 system state
println("Permissions:")
state.systemState.permissions.forEachIndexed { _, _, permission ->
val level = PermissionInfo.protectionToString(permission.protectionLevel)
println("${permission.name}: " +
"type=${Permission.typeToString(permission.type)}, " +
"packageName=${permission.packageName}, appId=${permission.appId}, " +
"gids=${permission.gids.contentToString()}, " +
"protectionLevel=[$level]")
}这段输出用于确认注册定义:type、owner、appId、GID 和 protection level。它不表示某个用户已经授予权限。
5.2 appId/user state
userState.appIdPermissionFlags[appId]?.forEachIndexed {
_, permissionName, flags ->
val granted = PermissionFlags.isPermissionGranted(flags)
println("$permissionName: granted=$granted, flags=" +
PermissionFlags.toString(flags))
}
userState.appIdAppOpModes[appId]?.forEachIndexed {
_, opName, mode ->
println("$opName: mode=${AppOpsManager.modeToName(mode)}")
}granted=true 只表示 permission flags 的 granted 位;AppOps 仍可能是 MODE_IGNORED 或 MODE_ERRORED,访问失败时必须同时查看两段。
6. 定义查询
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
List<PermissionInfo> ps = mPermissionManager.queryPermissionsByGroup(
groupList.get(i), 0);
for (PermissionInfo pi : ps) {
int base = pi.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE;
if (base < startProtectionLevel || base > endProtectionLevel) continue;
pw.println(prefix + "permission:" + pi.name);
}pm list permissions 适合确认权限是否注册、所属 group 和 protection;不适合确认包是否已授予。-d 筛选 dangerous,-g 按组,-f 增加元数据,-s 输出摘要。查询结果还受 package visibility 过滤,instant app 不能使用这些查询路径。
7. flags 修改命令
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
static {
SUPPORTED_PERMISSION_FLAGS_LIST = List.of(
"review-required", "revoked-compat",
"revoke-when-requested", "user-fixed", "user-set");
}
int flagMask = 0;
String flagName = getNextArg();
while (flagName != null) {
if (!SUPPORTED_PERMISSION_FLAGS.containsKey(flagName)) {
getErrPrintWriter().println(
"Error: specified flag " + flagName + " is not one of " +
SUPPORTED_PERMISSION_FLAGS_LIST);
return 1;
}
flagMask |= SUPPORTED_PERMISSION_FLAGS.get(flagName);
flagName = getNextArg();
}
int flagSet = setFlags ? flagMask : 0;
mPermissionManager.updatePermissionFlags(
pkg, perm, flagMask, flagSet, translatedUser);set 与 clear 都是 mask 操作;clear 不是把所有 flags 清零。排查自动撤销、用户固定或兼容性撤销时,应先记录原 flags,再只操作目标位。
8. 完整排查流程
9. 显式权限检查
源码文件:frameworks/base/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
override fun checkUidPermission(
uid: Int,
permissionName: String,
): Int {
if (UserHandle.getAppId(uid) >= Process.FIRST_APPLICATION_UID) {
val packageState = packageManagerLocal.withUnfilteredSnapshot().use {
it.getPackageStateByUid(uid)
}
if (packageState == null) return PackageManager.PERMISSION_DENIED
val granted = service.getState {
isPermissionGranted(
packageState, UserHandle.getUserId(uid), permissionName,
VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)
}
return if (granted) PackageManager.PERMISSION_GRANTED
else PackageManager.PERMISSION_DENIED
}
return if (isSystemUidPermissionGranted(uid, permissionName)) {
PackageManager.PERMISSION_GRANTED
} else {
PackageManager.PERMISSION_DENIED
}
}这段代码解释了一个常见误区:应用 UID 走 package state、用户和设备维度的 permission flags;系统 UID 则走 systemPermissions。因此排查 shell、system_server 或 media UID 时,不能只 dump 某个 APK。
10. AppOps 对照
权限检查成功后,调用方还可能经过 PermissionChecker 和 AppOps。dumpsys permissionmgr 输出的 appIdAppOpModes、packageAppOpModes 只能说明 PMS 保存的模式;实际数据访问还可能受 AppOpsService 的 note/start 结果影响。
// frameworks/base/core/java/android/app/AppOpsManager.java
public static String modeToName(int mode) {
switch (mode) {
case MODE_ALLOWED: return "allow";
case MODE_IGNORED: return "ignore";
case MODE_ERRORED: return "deny";
case MODE_DEFAULT: return "default";
case MODE_FOREGROUND: return "foreground";
default: return "mode=" + mode;
}
}若 dump 显示 granted=true 但访问仍失败,下一步应检查对应 op 的 mode、调用是否为 data delivery,以及是否存在前台限制,而不是重复执行 pm grant。
11. 调试命令示例
adb shell pm list permissions -d -f
adb shell dumpsys permissionmgr --package com.example.app
adb shell dumpsys permissionmgr --app-id 10123
adb shell pm grant --user 0 com.example.app android.permission.CAMERA
adb shell pm revoke --user 0 com.example.app android.permission.CAMERA
adb shell pm set-permission-flags com.example.app android.permission.CAMERA user-fixed
adb shell pm clear-permission-flags com.example.app android.permission.CAMERA user-fixed命令顺序对应源码层次:先确认定义,再查看 user flags,再检查 AppOps,最后才修改状态。--app-id 的值应来自包状态或 UID 查询;错误的 appId 会得到 Unknown app ID,不会自动猜测包名。
12. 源码阅读路线
PackageManagerShellCommand.runGrantRevokePermission:参数、用户转换和批量异常语义。PermissionService.grantRuntimePermission/revokeRuntimePermission:最终修改的 flags。PermissionService.dump:--package、--app-id和-a的状态范围。dumpSystemState与dumpAppIdState:定义状态、用户 flags 和 AppOps 的对应关系。queryPermissionsByGroup:定义查询与 visibility 过滤。- flags 命令解析:mask 修改与错误码。
权限调试必须建立“定义 → flags → AppOps → GID/进程”的证据链。只看 pm grant 退出码,或只看 manifest 中的 uses-permission,都无法解释 Android 17 中权限访问失败的完整原因。
