1. 特权应用和白名单机制:先把概念边界划清楚
做 Android 系统定制的同学,多半都遇到过一个很魔性的场景:应用代码没动、业务逻辑也没改,只是把它从/system/app挪到了/system/priv-app,重新刷机之后设备就卡在开机动画里再也起不来,串口日志刷得飞快,翻上去一看是一行Signature|privileged permissions not in privapp-permissions whitelist。我第一次碰到的时候愣了半天——权限明明在AndroidManifest.xml里声明了,protectionLevel也确实写着signature|privileged,凭什么不给我?
答案就藏在privapp-permissions这套白名单机制里。它是 Android 从 8.0 引入、到 9.0 变成强制的权限闸门,专门管一件事:哪些特权应用被允许申请哪些特权权限。你没写在白名单里,系统宁可起不来也绝不放行。这篇文章就把它从设计意图、文件格式、实操补齐一路讲到排查避坑,适合做 ROM 定制、系统应用预置、厂商适配的工程师,也适合刚接触系统源码、想知道“我这权限到底被谁卡住了”的开发者。
1.1/system/priv-app和/system/app的分界线在哪
很多新人以为这两个目录只是“重要程度不同”,其实它们是两道完全不同的处理路径。放在/system/app里的预置应用,跟从应用商店装进来的普通应用在权限视野上没有本质区别;而放在/system/priv-app里的应用,在PackageManagerService扫描阶段会被额外打上一个privileged标记,从此它才有资格持有protectionLevel中带privileged字样的权限。
这个特权目录最早是 Android 4.4 引入的,当时的动机很朴素:有些系统级功能确实需要更高权限,但又不该把所有预置应用都变成“想干什么就干什么”的超级用户。于是划出一块区域,只有这里的应用才能申请WRITE_SECURE_SETTINGS、MOUNT_UNMOUNT_FILESYSTEMS、PACKAGE_USAGE_STATS、CHANGE_CONFIGURATION、INSTALL_PACKAGES、DELETE_PACKAGES、REBOOT、MODIFY_PHONE_STATE这类动辄影响整机行为的权限。
但仅仅划目录是不够的。目录谁都能往里塞东西,OEM 塞、方案商塞、外购模块也塞,久而久之/system/priv-app就成了一个“权限提权后门”——只要往里面丢个 apk,声明一下特权权限,重启之后它就有了。Android 8.0 之前这套玩法畅通无阻,这才是后来自名单机制被引入的直接原因。
1.2 白名单机制解决的是“信任扩散”问题
换个生活化的类比:以前的/system/priv-app就像公司里一间没上锁的机房,只要你把工位搬进去,就能直接操作核心设备。现在加了一道门禁,门上贴着一张名单,写着“某某工号允许操作某某设备”,没写上去的人,哪怕人已经站在机房里,也碰不到设备。这张名单就是privapp-permissions-*.xml。
它的价值不在于“多一层校验”,而在于把权限授予从隐式变成显式。以前应用在 Manifest 里声明了特权权限就自动拿到,现在必须在系统侧的 XML 里再写一遍,等于系统工程师必须主动确认“这个应用确实需要这个权限”。这一步人工确认,恰好卡住了批量预置、来路不明的 apk 混入、第三方模块私自提权这几类最常见的风险。
从 Android 9 开始,这套校验默认进入enforce模式,也就是发现违规直接抛异常。PackageManagerService在扫描时如果发现某个特权应用请求了白名单之外的权限,会打印类似Signature|privileged permissions not in privapp-permissions whitelist: {com.demo.privapp: android.permission.WRITE_SECURE_SETTINGS}的日志,然后让system_server挂掉。system_server一起来就崩,起来就崩,表现出来就是无限开机动画。很多同学第一次遇到会以为是驱动问题、内核问题,绕了很大一圈才回到权限上。
1.3 哪些角色最容易被这套机制卡住
按照我的经验,被卡住的场景高度集中在这几类:一是 OEM 自研的系统应用,从旧版本平台迁移过来时 Manifests 里留着历史权限;二是外购或第三方提供的 apk,直接被丢进priv-app目录;三是把 AOSP 的某个模块移植到自家产品上,只搬了应用没搬白名单文件;四是刷机包定制玩家,兴致勃勃把某个应用放进特权目录,结果刷完开不了机。
这几类的共同点是:它们都绕过了“系统工程师主动确认权限”这一步。所以理解这套机制,重点不是背 XML 语法,而是建立“进 priv-app 必查权限、加权限必写白名单”的条件反射。后面所有实操内容,都是围绕这个反射展开的。
2. 权限模型底层:三类权限与它们的分工
要搞明白白名单为什么存在,绕不开 Android 权限体系本身。这个体系里权限的保护级别(protectionLevel)决定了“谁能拿到它”,而保护级别不是只有一个维度,它是可以按位或组合的。理解了这个组合规则,你就明白为什么有些权限写进白名单也没用。
2.1 signature、privileged、dangerous 三种保护级别对比
日常打交道最多的三类,可以简单概括成:签名权限看“血统”,特权权限看“位置”,运行时权限看“用户点不点头”。下表是我自己整理的一份速查对照,放在团队里给新人看很管用:
| 保护级别 | 判定依据 | 典型权限 | 是否需要写白名单 |
|---|---|---|---|
signature | 与定义该权限的应用同签名 | android.permission.BIND_DEVICE_ADMIN | 不需要,但必须同签名 |
signature|privileged | 位于 priv-app 分区且写入白名单 | WRITE_SECURE_SETTINGS、PACKAGE_USAGE_STATS | 需要 |
dangerous | 用户运行时弹窗授权 | ACCESS_FINE_LOCATION、CAMERA | 不需要,写了反而报错 |
normal | 安装即授予 | INTERNET、ACCESS_NETWORK_STATE | 不需要 |
signature|privileged|development | 特权 + 允许开发版授予 | 部分调试类权限 | 需要 |
这张表里最值得盯住的是signature|privileged这一行,它是白名单机制唯一管辖的对象。而dangerous那一行是重灾区——很多人以为“定位权限、相机权限也属于系统权限,应该一起写进白名单”,结果把ACCESS_FINE_LOCATION塞进privapp-permissions-*.xml,编译出来的系统在解析阶段就会报权限类型不匹配,或者干脆被静默忽略,白折腾半天。
2.2 为什么 privileged 权限要单独加锁
signature权限的防线是签名:你的应用必须和定义权限的那个应用用同一把密钥签,密钥拿不到,权限就永远拿不到。这条防线足够硬,因为密钥是物理隔离的资产。
privileged权限没法用签名来防——它面对的是“预装”这个场景,而预装 apk 千千万,不可能要求每个都跟平台同签名。于是只能用“位置 + 白名单”组合来兜底:位置决定你有没有资格看一眼这份权限,白名单决定你能拿走哪几个。两个条件必须同时满足。
这里有个容易混淆的点:signature|privileged不等于“特权应用一定能拿到”。如果某个权限的定义里同时带signature和privileged两个标志,那它其实有两条获取路径——要么跟定义者同签名,要么在特权分区且在白名单里。反过来,纯signature的权限即使应用在 priv-app 分区、白名单也写了,还是需要同签名才能拿到。这一点在做平台签名权限适配时最容易踩坑,我们后面第 5 章会专门讲。
2.3 白名单校验发生在启动的哪个时刻
这个时序问题很关键,它决定了你排查的方向。整个链路大致是:内核启动 →init解析build.prop和各个分区属性 → 拉起zygote→zygotefork 出system_server→system_server里PackageManagerService开始扫描所有分区 → 扫描到priv-app目录时读取etc/permissions/privapp-permissions-*.xml→ 逐个包比对请求的特权权限 → 有违规就记录日志并按当前策略处理。
ro.control_privapp_permissions这个只读属性控制的就是最后一步的处理方式,它有三个取值:disable表示完全关掉校验,log表示只记录日志放行,enforce表示严格执行、违规即崩。不同编译类型默认值不一样,正式发布的user版本普遍是enforce,调试版本更常见配置成log方便定位。这也是为什么很多问题只在量产版本上暴露——userdebug上跑得好好的,切到user就开不了机。
明白这个时序之后,排查思路就清晰了:既然校验发生在 PMS 扫描阶段,那么只要你能在日志里抓到 PMS 那几行报错,就知道是哪个包、缺哪个权限。与其盯着屏幕猜,不如直接把logcat的缓冲区全打开捞一遍。
3. 白名单文件的写法:路径、命名、XML 结构
这一章讲文件本身。看起来只有十几行 XML,但真正踩过坑的人都知道,出问题的地方往往不是“权限写错了”,而是“文件根本没被系统看见”。
3.1 存放路径与分区对应关系
Android 8 到 9 时期,权限白名单基本只有两个去处:/system/etc/permissions/和/vendor/etc/permissions/。Android 10 引入分区化之后,可选位置一下子变多了,常见的有这几个:
/system/etc/permissions/:平台级、核心系统模块/system_ext/etc/permissions/:系统扩展模块/product/etc/permissions/:产品定制模块/vendor/etc/permissions/:厂商私有模块/odm/etc/permissions/:ODM 层模块
PackageManagerService会把这些目录全扫一遍,所以理论上你把文件放哪个分区都能生效。但有一个隐含约束必须注意:权限定义文件所在的分区,通常要跟应用所在的分区有对应关系。比如应用装在/product/priv-app,白名单却在/system/etc/permissions里定义,早期版本上这是允许的,但在不同分区被独立升级的机制下容易失控。稳妥做法是让两边分区保持一致,或者把公共权限集中放到平台层统一管理。
我个人的习惯是:平台通用模块归/system/etc/permissions,产品特定模块归/product/etc/permissions,厂商硬件相关归/vendor/etc/permissions。这样做的好处是每个团队各管一摊,合并代码时冲突少。
3.2 文件命名规则:踩过坑的都懂
这是最容易翻车的一条。文件名必须严格以privapp-permissions-开头,以.xml结尾,中间那段后缀随便取,但要注意:后缀是用来区分不同来源的,AOSP 自带的是privapp-permissions-platform.xml,Google 相关的一般叫privapp-permissions-google.xml,厂商自己的通常拿公司名或产品线命名,比如privapp-permissions-acme.xml。
我亲眼见过一个同事把文件名写成priv-app-permissions-acme.xml,多了一个连字符,编译进镜像之后一点报错都没有,系统照常启动,权限却一个都没生效。他查了两个下午,最后用dumpsys对比才发现文件压根没被加载。所以记住:文件名不匹配前缀,系统不会报错,也不会解析,就是彻底的无视。
另一个小坑是同一个包里同一个权限不要在多份白名单文件里重复定义。早期版本对这种情况会抛重复定义错误,后期版本虽然容忍度提高了,但日志里会冒出一堆警告,排查噪音很大。规矩很简单:一个包的白名单条目,只在一个文件里出现一次。
3.3 XML 标签结构逐行拆解
结构本身非常朴素,只有两层:
<?xml version="1.0" encoding="utf-8"?> <permissions> <privapp-permissions package="com.demo.systemapp"> <permission name="android.permission.WRITE_SECURE_SETTINGS"/> <permission name="android.permission.PACKAGE_USAGE_STATS"/> </privapp-permissions> </permissions>根标签是<permissions>,这是 Android 权限配置文件的统一入口名,所有etc/permissions下的文件都用它。中间是零到多个<privapp-permissions>节点,每个节点代表一个应用,package属性必须写应用的真实包名,大小写敏感。再往里是若干<permission>节点,name属性写完整的权限字符串,注意是android.permission.XXX这种带前缀的全名,不能只写XXX。
新手最容易犯的两个语法错误:一是漏掉<?xml version="1.0" encoding="utf-8"?>声明,某些老版本的解析器会因此直接跳过整个文件;二是权限名前后混入空格或者换行符,看起来一样但实际上匹配不上。第二种尤其阴险,编辑器格式化之后看不出来,只能靠二进制比对或者cat -A查不可见字符。
3.4 把文件编译进镜像的几种姿势
写完 XML 只是第一步,还得让它进到镜像里。用 Android.bp 的话,最省事的是prebuilt_etc:
prebuilt_etc { name: "privapp-permissions-acme", src: "privapp-permissions-acme.xml", sub_dir: "permissions", filename_from_src: true, }然后把这模块名加进产品的PRODUCT_PACKAGES列表里。如果项目还在用 Android.mk,等价写法是:
include $(CLEAR_VARS) LOCAL_MODULE := privapp-permissions-acme.xml LOCAL_MODULE_CLASS := ETC LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/permissions LOCAL_SRC_FILES := $(LOCAL_MODULE) include $(BUILD_PREBUILT)还有一种更“图省事”的做法,让老板看了想骂人的那种,就是直接用PRODUCT_COPY_FILES硬拷。这个方式在快速验证阶段确实好用,但它绕过了模块依赖检查,容易在增量编译时出现“文件没更新”的诡异现象。正式产品里我强烈建议走prebuilt_etc,至少编译系统知道这个文件的存在和依赖关系。
4. 动手实操:给一个特权应用补全白名单
理论讲完,进入正题。假设现在有个包名com.demo.systemapp的应用要预置到/system/priv-app,它在 Manifest 里声明了三个权限。我们从零到一把它调通。
4.1 第一步:从日志里精确定位缺失的权限
最直接、最可靠的方式是抓日志,别猜。刷完机之后如果开机卡住,连上设备抓 logcat:
adb logcat -b all -d | grep -iE "privapp|not in privapp-permissions"正常情况下你会看到类似这样一行关键信息:
Signature|privileged permissions not in privapp-permissions whitelist: {com.demo.systemapp: android.permission.WRITE_SECURE_SETTINGS, android.permission.PACKAGE_USAGE_STATS}注意这里会一次性把该包所有缺失的权限都列出来,用大括号包着,包名和权限之间是冒号分隔。如果只有一行日志看不到全,可以用grep -A 5多带几行上下文。
如果设备已经起不来了、adb 也连不上,还有一条路:用adb shell dumpsys package com.demo.systemapp在能开机的时候先 dump 一份留底,或者直接从源码侧比对。不过在 enforce 模式下开机崩溃时dumpsys往往也调不出来,所以养成刷机前先把logcat全量落盘的习惯,能省下大量时间。
4.2 第二步:确认权限是否真的属于 privileged 类
抓到了缺失权限的名字,别急着往白名单里抄。先确认它到底是什么类型的权限,这一步能过滤掉大量“抄了也没用”的情况。
如果是 AOSP 定义的平台权限,直接去源码里搜定义:
grep -B 2 -A 6 'android.permission.WRITE_SECURE_SETTINGS' \ frameworks/base/core/res/AndroidManifest.xml你会看到类似<permission android:name="android.permission.WRITE_SECURE_SETTINGS" android:protectionLevel="signature|privileged" />的定义。看到privileged字样就可以放心写进白名单;如果只有signature,那就得走签名路线,写白名单没用;如果是dangerous或normal,说明这个权限压根不该出现在特权场景讨论里,多半是别的地方出了问题。
如果权限是 OEM 自己定义的,就去定义它的那个应用的 Manifest 里查。还有一种情况是权限来自framework-res.apk的编译产物,手边没有源码时可以用aapt从 apk 里反查:
aapt dump permissions framework-res.apk | grep -i "WRITE_SECURE_SETTINGS"这一步的产出是一份确认过的权限清单——只有这份清单上的权限才允许出现在白名单里。我通常会把这份清单和日志里的缺失清单做个交集,避免把运行时权限误加进去。
4.3 第三步:批量生成白名单条目
手工写几条还行,一旦产品线多了、预置应用多了,手工维护就变成灾难。AOSP 自带了一个辅助脚本,位置在development/tools/privapp_permissions/目录下,它的大致思路是读取dumpsys package的输出,把每个特权应用请求的权限和系统里定义的 privileged 权限做比对,然后生成一份推荐的白名单 XML。具体参数各版本略有差异,用之前先--help看一眼本地版本。
如果手边没有这套脚本,自己写一个也不难。核心逻辑就三步:拿到dumpsys package的文本、提取目标包requested permissions段、与signature|privileged权限集合求交集。给个骨架:
import re import subprocess PRIV_RE = re.compile(r'android\.permission\.[A-Z_0-9]+') def dump_package(pkg): out = subprocess.check_output( ['adb', 'shell', 'dumpsys', 'package', pkg]) return out.decode('utf-8', 'ignore') def requested_permissions(text): # 截取 requested permissions 段,直到下一个段落标题 m = re.search(r'requested permissions:(.*?)\n\s*\n', text, re.S) if not m: return [] return PRIV_RE.findall(m.group(1)) def main(pkg, allowed): perms = requested_permissions(dump_package(pkg)) hit = sorted(set(perms) & allowed) print('<privapp-permissions package="%s">' % pkg) for p in hit: print(' <permission name="%s"/>' % p) print('</privapp-permissions>') if __name__ == '__main__': # allowed 是从 AndroidManifest.xml 里提取出来的 privileged 权限集合 main('com.demo.systemapp', {'android.permission.WRITE_SECURE_SETTINGS'})allowed这个集合可以从frameworks/base/core/res/AndroidManifest.xml里用正则捞一遍,把所有protectionLevel含privileged的权限名收集起来。脚本跑出来的结果还要人工过一遍——自动生成只能保证语法正确,不能保证业务上真的需要,这一点后面第 6 章会细说。
4.4 第四步:编译验证与快速调试手法
改完 XML,接下来是验证。第一轮建议走完整编译,确认文件确实进了镜像:
source build/envsetup.sh lunch acme_device-userdebug make -j32编完之后可以直接从产物里确认文件在不在:
ls out/target/product/<device>/system/etc/permissions/ | grep privapp如果不想每次都完整编译,可以走 push 验证这条路,但要注意 Android 10 之后/system普遍是 system-as-root 且只读,remount 之前得先关掉校验:
adb root adb disable-verity # 设备会要求重启一次 adb reboot adb root && adb remount adb push privapp-permissions-acme.xml /system/etc/permissions/ adb reboot注意:
ro.control_privapp_permissions是只读属性,运行时用setprop改是改不动的,只会在日志里报一句只读属性不可修改。想临时切换成log模式,只能改编译配置里的属性覆盖项,或者 remount 之后编辑build.prop再重启。别在这上面浪费时间。
push 验证的坑在于:push 进去的文件属于“临时补丁”,下一次刷机就没了,所以验证通过之后一定要把改动落回源码,不然就会出现“我本地明明是好的”这种经典事故。
4.5 第五步:用 dumpsys 做结果核对
重启之后,用dumpsys确认权限是不是真的授予了:
adb shell dumpsys package com.demo.systemapp | grep -A 40 "requested permissions"输出里会分成两块:requested permissions是应用声明要的,install permissions是安装时实际授予的。如果某个特权权限出现在前者但不在后者里,说明白名单没生效或者权限类型判断有误;如果两边都有,说明这条权限已经稳稳拿到手了。
我通常还会顺手确认一下应用所在的分区,避免出现“明明装在/data却以为在 priv-app”的低级失误:
adb shell pm path com.demo.systemapp输出是/system/priv-app/DemoApp/DemoApp.apk这种形式,一眼就能看出分区和目录。如果显示的是/data/app/...,那么无论你怎么写白名单都不会生效,因为privileged标记只在扫描系统分区时才会打上。
5. 常见问题排查实录
权限这块的问题有个特点:现象一样,原因可能完全不同。我把自己和同事踩过的坑整理成下面这几类,基本覆盖了日常会碰到的九成情况。
5.1 白名单写了,权限还是没生效
这是问得最多的一个问题。按可能性从高到低排,主要查这几处:
第一,文件名前缀不对。必须严格是privapp-permissions-开头。少个字母、多个连字符,系统都不会报错,直接无视。
第二,XML 语法有误导致整个文件解析失败。常见的是标签没闭合、属性引号不配对、编码声明缺失。解析失败同样是静默的,最多在日志里留一行警告。验证方式是adb shell cat出来看看内容,或者用任意 XML 校验工具先过一遍。
第三,权限名写错。少一个字母、大小写不一致、前后有空格,都会导致匹配失败。拿grep -n精确定位,别靠眼睛看。
第四,分区不匹配。应用在/product/priv-app,权限写进了/system/etc/permissions,早期版本能生效,高版本上可能因为分区归属判定而失效。稳妥做法是保持分区一致。
第五,重复定义冲突。同一个包同一个权限在多份文件里都写了,某些版本会直接判定配置非法。
排查顺序建议从“文件有没有被加载”入手,而不是从“权限写得对不对”入手。因为前者出错时系统几乎不给提示,后者出错通常有日志。
5.2 签名不匹配导致的静默失败
signature|privileged和纯signature的差别前面讲过,但实际项目中还是经常混。最典型的场景是:应用申请了一个纯signature的权限,你把它预置进priv-app并写了白名单,结果install permissions里死活看不到它。
这种情况下,白名单不是问题所在,签名才是。解决方案只有两条:让应用改用平台签名,或者把权限定义改成带privileged标志——但后者意味着放宽了整个平台的权限约束,影响面很大,一般不建议为了单个应用这么做。
还有一个变种情况:应用在 priv-app 分区,权限也是signature|privileged,白名单也写了,但权限就是拿不到。这时候去看一下dumpsys package里该应用的signatures和pkgFlags,确认PRIVILEGED标志到底有没有打上。如果没打上,说明应用其实没被识别为特权应用,多半是安装路径或者 apk 头信息有问题。
5.3 覆盖升级、OTA 之后白名单丢失
这个坑在定制 ROM 上特别常见。原因通常有两种:一是 OTA 包只更新了应用 apk,没有同步更新对应的权限 XML,新版本应用新增了权限,白名单还是旧的,开机就崩;二是产品配置里白名单模块没有被加进PRODUCT_PACKAGES,只在某次手工 push 之后能跑,正式出包就丢了。
第一类的解法是在 OTA 打包流程里做一次校验:把新旧两个版本的白名单与应用声明的权限做差分,有新增特权权限就报警。第二类的解法更简单粗暴:把白名单模块名写进产品配置,然后编一个完整包,从产物目录里ls一遍确认文件在。这件事我建议固化成出包前的检查项。
5.4 常见报错与处理速查表
| 现象 / 日志关键词 | 可能原因 | 处理方向 |
|---|---|---|
not in privapp-permissions whitelist | 白名单缺条目 | 补齐所需特权权限 |
| 开机卡在动画,日志无 privapp 字样 | 其他模块崩溃,看system_server首个异常 | 全缓冲区抓日志定位首个崩溃点 |
权限写进白名单但install permissions为空 | 权限是纯signature类型 | 检查签名或权限定义 |
| 白名单文件存在但毫无效果 | 文件名前缀不匹配 | 核对命名规则 |
| 日志出现 XML 解析警告 | XML 语法错误 | 用校验工具过一遍 |
dumpsys里没有PRIVILEGED标志 | 应用不在特权分区 | pm path确认安装位置 |
| 同一个包在多份文件里出现 | 配置重复 | 合并到单一文件 |
| 只在 user 版本崩溃 | 属性策略差异 | 检查ro.control_privapp_permissions |
这张表我打印出来贴在工位上过,新人上手期真的省事。里面最有价值的一行是第二行——“日志里没有 privapp 字样”本身就说明问题不在权限白名单上,很多同学在权限上死磕半天,最后发现是别的模块导致的system_server崩溃,白白浪费一整天。
6. 工程化维护与经验沉淀
单次调通不难,难的是几十个产品、上百个预置应用长期维护。这一章聊聊落到工程层面的做法。
6.1 多产品线共用同一份白名单的坑
早期图省事,我们把所有产品的权限都堆在一个privapp-permissions-common.xml里。头两个月很舒服,第三个月开始出问题:A 产品的某个权限条目被 B 产品的同事删了,因为他在自己那边验证时觉得“没用到”;C 产品新增的权限被带到了 D 产品上,虽然不影响启动,但安全审计那边直接打回来问“为什么这个设备允许这个权限”。
后来改成三层结构:平台通用权限放privapp-permissions-platform.xml,产品线共用权限放privapp-permissions-<family>.xml,单个产品差异权限放privapp-permissions-<product>.xml,产品配置里按需拼装。这样职责清楚,合并冲突也少了很多。代价是文件变多、维护成本略升,但比起“删了别人的权限导致别人开不了机”,这点成本完全值得。
6.2 权限最小化:能不写就不写
自动生成白名单的脚本很好用,但它有个副作用:会把应用声明的所有特权权限一股脑写进去,包括那些历史遗留、实际上早就没用的。用过一两次之后我就改了规矩——生成的条目必须人工过一遍,逐条问“这个权限删掉会怎样”。
问不出答案的,就带着这个疑问去代码里搜调用点。搜不到调用点的,直接删掉拿去做回归测试。我印象很深的一次,清理后删掉了十几个权限,跑了一轮完整测试没有任何异常,反而整体安全扫描的告警少了一大截。
这件事的意义不只是“少写几行 XML”。权限一旦写进白名单,就等于平台盖了章承认这个应用有权做这件事。多一条,风险多一分。所以宁可写的时候费点劲,也别让白名单变成权限垃圾桶。
6.3 把校验塞进 CI 的做法
手工检查靠不住,得靠机器。我们在 CI 上加了三道检查,成本不高但很有效:
第一道,语法检查。用任意 XML 解析器把etc/permissions下所有文件跑一遍,解析失败直接报错。这个检查能拦住绝大多数低级错误。
第二道,命名检查。用脚本统一扫一遍文件名,不匹配privapp-permissions-*.xml的直接拦下。
第三道,一致性检查。把每个产品里所有priv-app应用声明的权限,和白名单里实际写的权限做交叉比对,输出两份清单:白名单里写了但应用没声明的(多余)、应用声明了但白名单没写的(缺失)。缺失的那份单独报警,多余的只提示。
第三道检查依赖aapt从编译产物里读权限,需要在编译完成之后跑,接入 CI 时放在构建后阶段。跑一次大概几秒钟,换来的是“再也不会有人因为权限问题把量产机器刷成砖”。
6.4 几个我踩过的坑
最后分享几个不成体系但很实在的经验。
别在生产机上验证权限改动。我见过有人直接拿手头唯一的样机 push 白名单,结果文件写错导致开机崩溃,又没有救砖手段,耽误了半天。改成在备用机或者模拟器上先验证,出问题随时刷回。
保留每一版白名单的变更记录。权限条目看起来不起眼,但每一行背后都是一个业务需求。有次审计追溯某个权限是哪个需求引入的,翻遍提交记录才找到三年前的一条提交,提交信息只有“add permission”五个字,白白花了两小时。
理解“权限组”在运行时权限里的角色。有些同学把privapp-permissions和运行时权限的权限组概念混在一起,其实两回事。权限组主要影响 UI 展示和同组自动授权行为,跟特权权限白名单没有交集。特权权限根本不走用户授权流程,是安装时静态授予的。想清楚这一点,排查时就不会东一榔头西一棒子。
遇到说不清的权限,先查定义再动手。这是最省时间的一条。花两分钟去AndroidManifest.xml里确认protectionLevel,比花两小时试错划算得多。
写到这里,其实privapp-permissions这套东西本身不复杂,难的从来不是 XML 语法,而是建立“权限改动必须有明确来源、必须显式确认、必须可追溯”的工作习惯。我个人的体会是,凡是能在 CI 上自动化的检查,就别指望人记住;凡是自动化覆盖不到的判断,就得在评审时多问一句“这权限删了会怎样”。这句话问多了,白名单就不会失控。