
做 Android 客制的朋友应该都撞过这一面墙费劲把 APK 塞进/system/priv-app重启后应用确实预置成功了可一调用系统接口就抛Permission Denial翻 logcat 只看到一行Privileged permission ... for package ... not in privapp-permissions allowlist。这个报错说白了就是权限白名单没跟上。很多刚接触客制的同学会把预置和给权限当成一回事实际上在 Android 9 之后这两件事被系统严格拆开了APK 进入 priv-app 目录只代表它有资格使用特权权限不代表它真的获得了特权权限。这篇内容就是讲清楚这一块机制并且给出一套可直接照抄的落地流程覆盖源码预置、纯文件预置、不同 Android 版本差异和高频踩坑点适合做 ROM 客制、企业定制机、行业终端方案的技术人员参考。1. 预置进 priv-app 只是开始权限白名单缺失时的真实报错1.1 一个再常见不过的现场我最早接触这个问题是在给某行业终端客制一套系统工具箱的时候。应用编译完用Android.bp配上privileged: true烧进机器应用确实出现在/system/priv-app/FooTool/下面了启动也正常看起来一切完美。结果一调用PACKAGE_USAGE_STATS拿应用使用时长直接抛异常Permission Denial: getUsageStats(int) from pid1234, uid10123 requires android.permission.PACKAGE_USAGE_STATS当时第一反应是权限没在 Manifest 里声明或者运行时权限没授予查了一圈发现都不对最后是在 logcat 里看到这样一行关键日志PackageManager: Privileged permission android.permission.PACKAGE_USAGE_STATS for package com.foo.tool not in privapp-permissions allowlist这行日志就是整个问题的根源应用是 priv-app 没错但系统在扫描时发现这个包没有任何对应的权限白名单声明于是直接不再给它授予特权权限。1.2 PMS 报错的两种典型形态PackageManager 在扫描 priv-app 时权限白名单缺失的报错主要有两种应用有privileged级别权限但整包都没有对应privapp-permissions记录日志类似Package com.foo.tool has declarations that are not in the privapp-permissions allowlist。包的权限白名单里有条目但具体某个权限没写进去日志就是上面那种Privileged permission ... not in privapp-permissions allowlist。这两种报错都说明一件事需要把应用需要的特权权限逐条写进一个 XML 白名单文件里并且让这个文件被打进系统分区。1.3 一个反直觉的事实platform 签名也不能豁免这里必须先纠正一个客制开发里流传很广的误解。很多人以为只要用 platform 签名系统权限就全部自动给我这在 Android 8 及以前的版本里基本成立但 Android 9 之后就不行了。平台签名能帮你拿到的是signature保护级别的权限比如android.permission.SIGNATURE这类但signature|privileged、privileged保护级别的权限额外要求包在 priv-app 目录 在白名单里这两个条件同时满足platform 签名并不能替代白名单。从源码角度理解PackageManagerService 扫描系统应用是在 SystemConfig 阶段加载权限文件随后在PackageManagerService.scanSystemPackageLI()/scanPackageNewLP()里针对priv-app做专门的 allowlist 校验。这个校验逻辑从 Android 9 开始默认是 enforce 状态配置开关是ro.control_privapp_permissions默认值就是enforce。所以最省事的做法就是预置 priv-app 的同时把权限文件准备好一次性解决不要绕开也不要指望签名豁免。2. 白名单文件在 PMS 里的匹配逻辑谁需要、谁不需要2.1 文件位置与基础结构priv-app 的权限白名单文件约定放在系统分区的/system/etc/permissions/目录下文件名一般是privapp-permissions-xxx.xml。这个目录下所有*.xml都会在 SystemConfig 启动阶段被解析。内容结构很简单permissions privapp-permissions packagecom.foo.tool permission nameandroid.permission.PACKAGE_USAGE_STATS/ permission nameandroid.permission.WRITE_SETTINGS/ permission nameandroid.permission.READ_PHONE_STATE/ /privapp-permissions /permissions如果想显式禁止某个权限用deny-permissions节点permissions deny-permissions packagecom.foo.tool permission nameandroid.permission.CAMERA/ /deny-permissions /permissions一个文件里可以包含多个privapp-permissions条目也可以一个包一个文件。系统对文件名没有强约束只要路径正确、XML 语法合法即可。我习惯按产品模块拆分文件比如privapp-permissions-foo-tool.xml、privapp-permissions-bar-service.xml排查问题的时候一眼能看到是哪个模块的。2.2 并不是所有权限都需要写进白名单这是容易出问题的另一个点把应用 Manifest 里的所有uses-permission全部抄进白名单看起来稳妥实际没有必要甚至可能引入风险。有一点需要先明确normal级别权限不需要应用安装时自动授予。dangerous级别权限不需要写进 privapp 白名单这类权限走运行时授权流程预置应用如果想让用户免授权靠的是/system/etc/default-permissions/*.xml或appops默认配置不是privapp-permissions.xml。signature级别权限如果是 platform 签名且目标包就在系统镜像里PMS 会直接授予不强依赖白名单。privileged或signature|privileged级别权限这一类才是白名单文件真正要覆盖的对象。如果把所有权限都堆进白名单也不会立刻爆炸但会给后续排查增加噪音。更关键的是有些权限的保护级别会随 Android 版本变化比如android.permission.WRITE_SETTINGS从signature|appop到后续版本都会保留 privileged 属性这类才需要关注。所以正确做法是先看 App 实际用到的权限里哪些是 privileged / signature|privileged 级别再决定白名单内容。2.3 匹配规则的核心包名精确匹配白名单匹配是按包名精确匹配的不支持通配符、不支持前缀匹配。也就是说privapp-permissions packagecom.foo permission nameandroid.permission.INTERNET/ /privapp-permissions这种写法只对com.foo这个包生效com.foo.tool、com.foo.bar都不会被匹配到。我见过有人为了图省事给一整个应用族共用一个前缀然后问为什么没生效——就是被这个精确匹配规则坑的。另外如果包里配置了多个签名或使用sharedUserId匹配逻辑会更复杂一点但绝大多数客制项目不会走到这一步先把单包场景做好就够用了。2.4 和 runtime 权限授权别混为一谈顺手提一句容易混淆的概念。有些团队拿到预置应用需要授权摄像头、定位这种需求优先想到去写privapp-permissions.xml其实方向是错的。dangerous权限的默认授权应该在/system/etc/default-permissions/下写类似default-permissions-xxx.xml的文件exceptions exception packagecom.foo.tool permission nameandroid.permission.CAMERA fixedtrue/ permission nameandroid.permission.ACCESS_FINE_LOCATION fixedtrue/ /exception /exceptions这个文件同样通过产品配置打入系统分区。它和 privapp 白名单是两个完全不同的机制一个是权限有没有资格拿一个是运行时权限拿到后是否默认授予。搞清楚这一点能省掉非常多的排查时间。3. 从编译产物到 /system/etc/permissions三种落地方式3.1 方式一源码应用随 AOSP 一起编译如果应用源码就在 AOSP 源码树里这是最干脆的做法。假设模块目录是packages/apps/FooTool/编译配置用Android.bpandroid_app { name: FooTool, srcs: [src/**/*.java], resource_dirs: [res], manifest: AndroidManifest.xml, privileged: true, certificate: platform, installable: true, product_specific: true, }privileged: true对应输出到/system/priv-app/FooTool/。certificate: platform表示用平台签名。product_specific: true可选如果产品希望放入 product 分区可以按需加。老项目还在用Android.mk的话关键配置是LOCAL_MODULE_CLASS : APPS LOCAL_PRIVILEGED_MODULE : true LOCAL_CERTIFICATE : platform LOCAL_MODULE_TAGS : optional然后在产品配置 mk 文件比如device/xxx/xxx.mk里加白名单文件PRODUCT_COPY_FILES \ packages/apps/FooTool/privapp-permissions-footool.xml:$(TARGET_COPY_OUT_SYSTEM)/etc/permissions/privapp-permissions-footool.xml3.2 方式二只有 APK没有源码实际客制项目里更常遇到的情况是供应商给了个成品 APK要求预置成系统应用。这种情况下不需要建 Android.bp 编译只需要把 APK 放进源码树的某个目录比如device/xxx/preinstall/FooTool/FooTool.apk然后在产品 mk 里拷贝到 priv-appPRODUCT_COPY_FILES \ device/xxx/preinstall/FooTool/FooTool.apk:$(TARGET_COPY_OUT_SYSTEM)/priv-app/FooTool/FooTool.apk \ device/xxx/preinstall/FooTool/privapp-permissions-footool.xml:$(TARGET_COPY_OUT_SYSTEM)/etc/permissions/privapp-permissions-footool.xml注意一个细节APK 路径里的文件名最好和包名或模块名一致避免 PMS 扫描时出现奇怪的解析问题。另一个点是 APK 必须提前用 platform 证书签好或者至少保证它是可以被系统接受的签名状态。如果 APK 用的是供应商自己的签名可能还需要调整签名方案这点后面第五部分单独展开。3.3 方式三直接改 AOSP 刷机包有些项目不维护完整源码而是直接拿别人编好的出厂包做二次开发或者临时验证需求。这种情况下可以先用mount -o rw,remount /system把系统分区重新挂载为可写然后把 XML 文件 push 进去adb root adb remount adb push privapp-permissions-footool.xml /system/etc/permissions/ adb reboot这种方式适合快速验证白名单内容对不对不建议作为正式量产交付流程。因为/system/etc/permissions/是 dm-verity 校验的核心区域如果直接修改会导致系统分区校验失败OTA 升级时也会因为分区差异出问题。正式做量产款还是要把文件纳入源码树重新打包。3.4 落地后如何确认白名单已经被 PMS 接受文件 push 进去并重启后先别急着跑业务逻辑直接查包信息adb shell dumpsys package com.foo.tool | grep -A 200 requested permissions重点看两个位置requested permissions列表里目标权限是否存在grantedPermissions或privFlags里是否有PRIVILEGED标志。更直接的方式是过滤adb shell dumpsys package com.foo.tool | grep -i privapp\|privileged如果看到权限出现在privilegedPermissions里说明白名单生效了。如果还在报not in privapp-permissions allowlist优先检查 XML 是否真的进入了/system/etc/permissions/adb shell ls -l /system/etc/permissions/privapp-permissions-footool.xml文件在但还报错那就看包名是否严格一致、权限名拼写是否正确。权限名的拼写错误是我见过的最低级却最频繁的坑。4. Android 9 到 14privapp-permissions 的版本演进与分区变化4.1 Android 9API 28强制检查开始的版本Android 9 是分水岭。在这之前AOSP 也提供了privapp-permissions-platform.xml之类的文件但强制执行力度不够很多情况下 platform 签名能绕过。Android 9 之后PMS 默认在enforce模式下运行priv-app 必须匹配白名单记录。这也是网上很多老资料说我啥也没配也能跑的原因你要先确认对方用的 Android 版本。4.2 Android 11system_ext 分区带来的新写法Android 11 引入了system_ext分区Google 推荐把厂商客制的系统应用和对应权限文件放进这个分区避免直接改 system也方便 system 分区做更严格的校验和升级。此时的权限文件路径变为PRODUCT_COPY_FILES \ device/xxx/FooTool/privapp-permissions-footool.xml:$(TARGET_COPY_OUT_SYSTEM_EXT)/etc/permissions/privapp-permissions-footool.xmlAPK 对应放到/system_ext/priv-app/。如果你的 Android 版本 ≥ 11建议优先采用 system_ext 方案这是后续版本持续支持的方向。对应在 Android.bp 里product_specific: true或system_ext_specific: true要写对。4.3 Android 12 及之后保护级别收紧的细节Android 12 开始系统对privileged保护级别权限的授予逻辑更严格了。应用即使有了白名单如果权限本身的保护级别在新版本里发生变化比如某个权限从signature改成了signature|privilegedPMS 会重新计算是否授予。客制项目里最容易看到的现象是同一套权限文件在 Android 11 上没问题升到 Android 12/13 后某个系统接口突然权限被拒。这时候不要慌去查新版本里这个权限的保护级别是否变了再决定是改白名单还是换接口。4.4 版本差异对照表Android 版本强制检查推荐分区权限文件路径常见注意点Android 8.x弱强制system/system/etc/permissions/platform 签名尚可绕过Android 9 / 10强制system/system/etc/permissions/必须写白名单Android 11强制system_ext/system_ext/etc/permissions/优先用 system_extAndroid 12强制system_ext/system_ext/etc/permissions/保护级别变更频繁需回归验证Android 14强制system_ext/system_ext/etc/permissions/权限分离逻辑更细建议按模块拆分这个表是我在维护多套 Android 版本定制方案时总结出来的整理成文档给团队新人看能省掉很多不必要的试错。5. 真实踩坑复盘签名、SEPolicy、解析失败与缓存问题5.1 签名不一致导致的隐形失败预置 APK 是用供应商签名打的但你想让它成为 platform 签名应用于是直接用 apksigner 重新签了一遍。这样装进去后包能出现在/system/priv-app/但 PMS 在解析包时发现签名和系统不匹配白名单即使写了也拿不到特权权限。更隐蔽的是升级场景设备上已经通过adb install安装过一个测试签名的com.foo.tool再刷入包含 platform 签名版本的系统镜像会出现INSTALL_FAILED_UPDATE_INCOMPATIBLE或签名冲突。排查时不要只看编译产物要先确认设备上是否残留旧签名应用adb shell pm list packages -f | grep com.foo.tool adb shell dumpsys package com.foo.tool | grep signatures有残留就先卸载旧包再刷机或者直接整机fastboot -w清 data。5.2 SEPolicy 拦截权限文件生效了但服务调不通白名单解决了 Permission Denial不代表万事大吉。另一类高频问题是应用拥有权限但 SELinux 策略没放开调用系统服务时出现avc: denied { find } for serviceactivity service_manager这类问题的本质是权限是 Android 上层的一个门禁而 SELinux 是内核层的另一个门禁两个都要过。客制系统应用如果要访问某些系统服务需要在 SEPolicy 里补充allow priv_app xxx_service:service_manager find;之类的规则。这块排查链路通常是dumpsys package确认权限已授予仍然报错logcat看有没有avc: denied有的话adb shell dmesg | grep avc确认是哪条 rule missing在对应 SEPolicy 目录补规则重新编译 boot 或 vendor 镜像。5.3 ro.control_privapp_permissionspermissive 为什么我不建议用网上有人为了解决白名单问题直接在build.prop里加ro.control_privapp_permissionspermissive这样确实能让 PMS 跳过强制校验权限问题立刻消失但带来的隐患是所有 priv-app 应用都可以无条件拿到特权权限一旦预置应用里混入一个有漏洞的第三方 SDK整个系统的高危权限都会被滥用。我在量产项目里从不用这个开关宁可花时间把白名单文件写全。5.4 XML 解析失败导致权限消失有时候文件放进去了路径也对但 PMS 就是没读到。最常见的原因是 XML 语法错误比如permissions标签没闭合、多了一个非法字符、或者中文注释编码不对。SystemConfig 解析失败时通常会在 logcat 里打PackageManager: Failed to parse /system/etc/permissions/privapp-permissions-footool.xml这个报错往往不会导致系统重启失败只会让整个文件的所有条目失效很隐蔽。我吃过一次亏后养成了一个习惯所有权限 XML 在进版本前先在本机用 XML 解析脚本或者在线校验工具过一遍。5.5 修改白名单后不生效的缓存问题如果权限文件是在已经运行的设备上调试的改了 XML 并重启偶尔还是会遇到权限不生效。这是因为 PMS 在启动时已经把权限状态固化到了/data/system/下的包管理数据库里简单重启 framework 可能不会重新计算权限。这时候可以尝试adb shell pm disable com.foo.tool adb shell pm enable com.foo.tool如果再不行直接adb shell pm clear com.foo.tool清掉应用数据或者adb shell stop adb shell start重启 framework。我在实际项目里发现pm disable/enable通常就能触发权限重新计算可以先试这个。6. 可直接照抄的权限清单模板与维护工具6.1 一个多包白名单文件模板如果项目里要预置不止一个应用可以放在同一个文件里维护permissions privapp-permissions packagecom.foo.tool permission nameandroid.permission.PACKAGE_USAGE_STATS/ permission nameandroid.permission.WRITE_SETTINGS/ permission nameandroid.permission.READ_PHONE_STATE/ permission nameandroid.permission.ACCESS_NETWORK_STATE/ /privapp-permissions privapp-permissions packagecom.foo.service permission nameandroid.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS/ permission nameandroid.permission.WRITE_SECURE_SETTINGS/ /privapp-permissions /permissions注意不是把 Manifest 里所有权限都搬进来。哪些权限需要判断标准就是看这个权限是不是 privileged / signature|privileged 级别。保守做法是可以先全部写上跑一轮抓 log再按需精简但最终交付前我会把dumpsys package里实际授予的privilegedPermissions拉出来和白名单文件做一次 diff确保没有多余条目。6.2 目录结构建议维护多个 Android 版本的产品线时我建议把权限文件放在产品目录下统一管理不要散落在各个 app 模块里device/xxx/products/ ├── permissions/ │ ├── privapp-permissions-footool.xml │ ├── privapp-permissions-barservice.xml │ └── default-permissions-footool.xml ├── xxx.mk └── AndroidProducts.mk在xxx.mk里集中 CopyPRODUCT_COPY_FILES \ device/xxx/products/permissions/privapp-permissions-footool.xml:$(TARGET_COPY_OUT_SYSTEM_EXT)/etc/permissions/privapp-permissions-footool.xml \ device/xxx/products/permissions/privapp-permissions-barservice.xml:$(TARGET_COPY_OUT_SYSTEM_EXT)/etc/permissions/privapp-permissions-barservice.xml \ device/xxx/products/permissions/default-permissions-footool.xml:$(TARGET_COPY_OUT_SYSTEM_EXT)/etc/default-permissions/default-permissions-footool.xml这样每个产品的权限清单是一份独立目录新增版本时整体移植不用东拼西凑。6.3 一个简单的差集校验脚本与其靠肉眼对比 Manifest 和白名单不如写个脚本自动查。核心逻辑是解析 APK 里的uses-permission再解析 XML 白名单输出差集。下面这段 Python 脚本够用import subprocess import xml.etree.ElementTree as ET def get_manifest_permissions(apk_path, aapt_pathaapt): out subprocess.check_output([aapt_path, dump, badging, apk_path], textTrue) perms [] for line in out.splitlines(): if line.startswith(uses-permission:): # 形如: uses-permission: nameandroid.permission.READ_PHONE_STATE perms.append(line.split()[1]) return set(perms) def get_allowlist_permissions(xml_path): tree ET.parse(xml_path) root tree.getroot() perms set() for p in root.iter(permission): perms.add(p.get(name)) return perms if __name__ __main__: apk FooTool.apk xml privapp-permissions-footool.xml manifest_perms get_manifest_permissions(apk) allowlist_perms get_allowlist_permissions(xml) print(Manifest 声明但白名单缺失, manifest_perms - allowlist_perms) print(白名单里 Manifest 未声明, allowlist_perms - manifest_perms)这个脚本不能 100% 判断一个权限是不是 privileged 级别需要结合adb shell pm dump的实际结果来过滤但作为第一道防线已经足够高效。6.4 最后分享一个调试技巧在实际项目里如果预置应用功能异常我的排查顺序永远是先dumpsys package看权限授予状态再logcat看 PMS 有没有白名单相关日志这两步能定位 80% 的问题。我遇到过最折腾的一次是权限文件写对了、白名单也生效了但应用调用接口还是被拒最后发现是新版 Android 把权限保护级别改了原来signature|privileged的权限变成了仅privileged导致普通system_app域的应用不再自动获得。那次之后每升级一个 Android 大版本我都会重新做一遍权限清单审查而不是沿用老配置。这个习惯一直保留到现在也是我想重点提醒你的一件事。