前阵子接到一个终端定制的需求:一台基于 Android 16 的工程机上,开机就要带一个能直接用的微信。说白了就是预置应用,这事儿我一开始觉得很简单——把 APK 丢进系统镜像,再打包刷机,三十分钟搞定。结果真正做起来才发现,预置本身确实不难,难的是 Android 16 这套 SELinux 权限模型。开机之后微信的日志里一屏一屏的 avc denied,应用连自己该读的厂商属性都被挡在外面,光排查就花了两天。这篇文章就把这个项目从头到尾复盘一遍:预置方案怎么选、Android.bp 怎么写、avc 日志怎么抓怎么解读、sepolicy 策略怎么补,以及我在真机上踩过的那些坑。准备做系统定制、整机方案集成或者 ROM 开发的朋友,可以直接照着这条链路走。
1. 预置方案的整体设计与选型
1.1 先想清楚:微信该放进哪个分区
Android 系统里“预置一个应用”不是简单地把 APK 拷进镜像。APK 放的位置,直接决定它的身份和权限等级。常见的选择有这么几类:
/system/app/:普通系统应用/system/priv-app/:特权系统应用/product/app/或/product/priv-app/:产品分区应用/vendor/app/:厂商分区应用
Google 对预置应用的权限划分很明确:放进/system/app只是普通应用,放进/system/priv-app才拥有特权权限,比如可以直接调用某些系统 API、参与权限白名单。但微信这种第三方应用,我强烈建议第一步别碰 priv-app,原因有两个。
第一个是签名冲突。priv-app 里如果放的是非平台签名应用,Android 9 之后会触发 privapp-permissions 强制校验。应用在 AndroidManifest 里申请的敏感权限如果没写进白名单,系统会直接忽略,表现出来就是微信有些功能时好时坏,非常诡异。微信是官方签名,不是平台签名,硬放进 priv-app 等于给自己埋雷。
第二个是升级和卸载的逻辑问题。priv-app 里的包在 OTA 和系统重置后,表现跟普通应用完全不一样。微信这种高频更新的应用,预置版本一旦比用户后装的版本低,会产生降级冲突,轻则无法覆盖安装,重则直接导致启动崩溃。
所以这个项目里,我最终选择的是/system/app/WeChat/,也就是普通系统应用。它不参与特权白名单,又能开机常驻,即使被用户卸载,恢复出厂设置后也会重新出现。这个折中方案是行业里预置第三方应用最常用的位置。
1.2 Android 16 对第三方预置应用的隐藏限制
Android 16 这条开发线上,系统对“预置第三方应用”的态度又收紧了一档。最直观的变化是 SELinux 策略结构做了不少调整,很多系统服务组件被挪进了 APEX 模块,文件上下文类型也跟着变。这就导致一个现象:以前同一套 allow 规则在 Android 14 上能跑,换到 Android 16 上就会被 neverallow 拦下来,编译直接报错。
另外,Android 16 上第三方应用进程的默认域仍然是untrusted_app,跟用户自己从应用市场下载的应用用的是同一套基础策略。换句话说,系统并不会因为你把微信预置进了 system 分区,就额外给它开什么后门。它能访问什么、不能访问什么,sepolicy 里写得很死。很多做定制的同学第一次遇到这种情况会懵,觉得“我都把应用放 system 里了,怎么还没权限”,其实这跟分区位置没关系,SELinux 看的是进程域,不是安装路径。
这带来一个非常现实的问题:厂商在系统里自定义的硬件节点、系统属性、特殊路径,默认策略里根本不会有针对第三方应用的 allow 规则。微信一旦需要访问这些东西,就会产生 avc denied。这也是这篇文章要重点解决的核心矛盾。
2. 把微信编进系统镜像
2.1 准备 APK 和目录结构
这一步看起来简单,但细节不少。我先把微信官方渠道的 ARM64 正式版 APK 下载下来,放到源码树的vendor/demo/apps/WeChat/目录下。注意几个容易踩的坑:
- 不要用从其他机器上导出的“备份版”微信,那种 APK 很可能已经被某些工具重签过,功能正常但后续升级会出问题。
- 下载时确认 APK 包含 arm64-v8a 的原生库。现在很多终端是纯 64 位系统,如果 APK 里只有 armeabi-v7a 的 so,虽然能通过兼容层跑,但性能和稳定性都堪忧。
- 如果设备内存和存储都比较紧张,可以考虑微信的轻量版或定制版,不过那需要商务渠道,普通项目一般拿不到。
目录结构很简单:
vendor/demo/apps/WeChat/ ├── Android.bp ├── wechat.apk我当时把 APK 重命名成了wechat.apk,避免文件名里有空格或版本号造成脚本解析问题。命名越简单越好,后面所有配置都围绕这个名字来。
2.2 编写 Android.bp 并决定签名策略
在这个 demo 项目里,我用的是android_app_import模块类型,因为它就是专门用来导入已经签名好的 APK 的。Android.bp 内容如下:
android_app_import { name: "WeChat", apk: "wechat.apk", presigned: true, privileged: false, dex_preopt: { enabled: true, }, installable: true, sdk_version: "current", }这里最关键的配置是presigned: true,表示不重新签名,直接用微信官方签名。为什么不改成certificate: "platform"让它变成平台签名应用?我实测过,一旦用 platform key 重签微信,系统的包管理器确实会把它当作平台签名应用,但微信自己的代码在运行过程中有签名校验逻辑,检测到签名不是官方的话,部分功能会认为“签名异常”然后拒绝运行。轻则登录态异常,重则直接闪退。
所以结论很简单:预置微信这种由第三方开发者签名的应用,保留原签名是最省事的。每个字段的意义也顺便说一下:
presigned: true:跳过签名步骤。privileged: false:明确按普通系统应用处理。dex_preopt: enabled: true:让微信的 dex 在系统编译阶段就预编译成 oat,显著加快首次启动速度。这个字段对微信这种大型应用尤其重要,不开启的话,开机后第一次点开微信要等好几秒的 dex 优化,观感很差。sdk_version: "current":用当前系统的 SDK 版本编译。
另外强调一点,android_app_import默认情况下会在 out 目录生成一个可复制的 APK 产物,安装包里是否要附带这个 APK,取决于你产品是否允许用户卸载并恢复出厂版本。如果业务上希望微信不可卸载,可以加preserve_in_ota: true,这样系统升级时不会因为分区空间清理把预置应用删掉。
2.3 注册到产品配置并编译
Android.bp 只是定义了模块,要让编译系统真正把它打进去,还要在产品 mk 文件里注册。设备相关的配置一般在device/demo/device.mk里,我加了一行:
PRODUCT_PACKAGES += WeChatPRODUCT_PACKAGES是 Soong 和 make 之间传输模块列表的通道,编译系统会把里面列出的模块组装进对应的系统镜像分区。这里有个执行顺序的坑,如果你的产品 mk 里有多处PRODUCT_PACKAGES +=,有些在 include 某些公共 mk 之后,有些在之前,实际生效顺序可能不一致。我的建议是把预置应用相关的行集中放在设备主 mk 文件末尾,并加注释,避免后续维护的人重复注册。
注册好之后就可以编译验证了:
source build/envsetup.sh lunch demo-device-userdebug make WeChat -j32单独编译 WeChat 模块,是因为第一次不要直接全量 build,先确认 APK 能正确打包。编译完成后去 out 目录检查:
find out/target/product/demo-device/system -name "WeChat*"如果能看到WeChat.apk或者对应的目录,说明模块编译和组装都没有问题。这时候再执行全量编译和打包镜像,后面排查问题时也能排除“根本没编进去”这个低级原因。
3. avc 权限问题定位与抓取
3.1 avc denied 是怎么来的
先补个基础背景:Android 的 SELinux 默认是 enforcing 强制模式,每个进程都有安全上下文。比如微信这种第三方应用进程,上下文一般是:
u:r:untrusted_app:s0:c512,c768后面那串c512,c768是 MCS category,用来隔离不同应用的私有数据。当微信去访问一个文件、属性、socket 或者 binder 接口时,SELinux 会拿三个关键信息去查策略库:来源上下文(scontext)、目标上下文(tcontext)、目标类别(tclass),再加上请求的操作许可(permission)。策略库里如果找不到显式的 allow 规则,内核直接打回请求,并记录一条 avc denied 到内核日志。
很多定制工程师第一次遇到 avc denied 会慌,觉得是不是系统配置错了。其实这是 SELinux 的正常工作方式:没有明确允许的权限,一律默认拒绝。定制系统里出现 avc denied,通常不是系统故意不给微信权限,而是那些厂商自定义的节点、属性、外设路径,原来的策略文件里根本没覆盖到。你需要做的不是诅咒 SELinux,而是把这些缺口按规范补上。
3.2 抓取 avc 日志的正确姿势
抓日志的方法我推荐三种组合起来用:
# 方法一:直接从内核日志抓 adb shell dmesg | grep avc # 方法二:通过 logcat 的内核 buffer 抓 adb logcat -b kernel -d | grep avc # 方法三:实时监控,触发问题时边操作边看 adb shell dmesg -w | grep avc --line-buffered第三种方法是我最常用的。把微信打开,然后逐个功能去点,比如扫码、选图片、看视频,每个动作触发的问题都会实时刷出来。这样后面定位“是哪步功能导致的 denied”就非常精准,不用对着几百条日志瞎猜。
一条典型的 denied 日志长这样:
type=1400 audit(0.0:1251): avc: denied { read } for pid=1234 comm="WeChat" name="demo_status" dev="tmpfs" scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:object_r:vendor_demo_prop:s0 tclass=file怎么读?我拆解一下:
denied { read }:被拒绝的操作类型。pid=1234 comm="WeChat":哪个进程触发的。name="demo_status":被访问对象的名字。scontext=u:r:untrusted_app:s0:c512,c768:来源域,微信进程。tcontext=u:object_r:vendor_demo_prop:s0:目标类型,厂商自定义的 demo_prop 属性。tclass=file:目标类别是文件。
后面写策略时,就是根据这些字段反推出对应的 allow 规则。所以抓日志的时候别偷懒,最好按功能把日志分段保存,每条标注上“当前在操作什么”,这样写策略时一目了然。
3.3 微信在定制终端上最常见的几类 denied
我们这个项目里,微信集中遇到的拒绝有以下几类,应该能代表大多数定制场景。
第一类:读厂商自定义属性。终端设备加了扫码枪、钱箱检测之类的硬件能力,厂商在系统里定义了一个vendor.demo.hw.status属性,SELinux 类型是vendor_demo_prop。微信的一个辅助模块会尝试读这个属性来判断设备形态,于是被拒:
avc: denied { read } for name="demo_status" dev="tmpfs" scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:object_r:vendor_demo_prop:s0 tclass=file第二类:访问外设节点。终端外接打印机、刷卡器时,微信如果想要通过vendor_demo_device类型的节点探测硬件状态,会触发对目录或设备文件的 denied:
avc: denied { write } for name="printer" dev="tmpfs" scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:object_r:vendor_demo_device:s0 tclass=dir第三类:尝试设置调试属性。微信某些版本为了排查问题,会尝试写debug.xxx属性。这种属性在 userdebug 固件上通常能写,在 user 版本上会被严格拒绝:
avc: denied { set } for property="debug.wechat.log" scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:object_r:debug_prop:s0 tclass=property_service这里要提醒一句:这些 denied 日志并不代表微信在“做坏事”,很多就是厂商自定义节点缺少预置规则。你要做的是逐条判断是否合理,而不是一股脑全 allow。
4. 编写 SELinux 策略与编译
4.1 先想清楚:动全局还是给微信单独建域
面对 avc denied,最直接的想法是往system/sepolicy/private/untrusted_app.te里加 allow 规则。这样做有效,但它是全局生效的,意味着所有第三方应用,包括用户之后自己安装的任何 App,都会继承这个权限。如果只是读一个无关紧要的属性,问题不大;但如果是外设节点或者调试属性,等于给所有第三方应用开了一扇门,风险很高。
我当时做了一个取舍:项目时间紧,目标设备是行业专用终端,不开放第三方随意安装应用,安装源可控,所以先在untrusted_app域上把必要的读属性权限补上,联调通过后再考虑把微信收进独立域。如果你的产品是公版设备、应用市场随意安装,就一定不要图省事,建议给微信单独分配一个 SELinux 域。
给应用单独建域主要走这几步:
- 在
system/sepolicy/private/seapp_contexts里,给微信的包名和签名指纹分配一个自定义 domain。 - 在
system/sepolicy/private/mac_permissions.xml里,把微信的签名指纹对应到 seinfo 标签。 - 新建一个
wechat_app.te,声明type wechat_app, domain;,然后通过typeattribute wechat_app app_api;让它继承所有普通 App 的基础权限,再额外补厂商自定义节点的规则。
seapp_contexts里的写法大致是这样的:
user=untrusted_app seinfo=wechat domain=wechat_app name=com.tencent.mm type=app_data_file levelFrom=all user=untrusted_app domain=untrusted_app注意最后一行必须保留兜底规则,否则任何不在白名单里的第三方应用都会因为找不到匹配项而无法创建进程。
wechat_app.te里则类似:
type wechat_app, domain; typeattribute wechat_app app_api; allow wechat_app vendor_demo_prop:file { read open getattr }; allow wechat_app vendor_demo_device:dir { open read search }; allow wechat_app vendor_demo_device:file { read write open getattr };这个方案的好处是权限边界非常清晰,只给微信开的口子,不影响其他应用。缺点是要维护 mac_permissions.xml 里的签名指纹,如果 APK 升级换了签名,还需要同步改配置,维护成本略高。
4.2 怎么从 avc 日志生成可用的规则
如果 denied 条目很多,手工一条条写会漏。这时可以直接用audit2allow工具。源码树里编译产物自带这个工具,也可以把设备上的策略文件拉回来解析:
adb shell cat /sys/fs/selinux/policy > policy audit2allow -p policy -i dmesg_avc.txt它会输出一组 allow 建议,例如:
allow untrusted_app vendor_demo_prop:file { read open getattr };但记住一点:audit2allow 给的只是“满足日志”的最小集合,不是“安全”的最小集合。比如日志里出现{ read },它可能会顺带把open getattr ioctl都补上,这些关联权限在多数场景下合理,但你要自己判断是否过界。我在项目里会把生成结果跟设备需求逐条过一遍,拿不准的规则宁可先不 allow,用setenforce 0临时验证当前功能到底缺的是不是这条权限。
4.3 编译、验证与回归
写完策略后,需要重新编译对应镜像。因为策略文件在system/sepolicy下,正常情况下需要重新打包整个系统镜像:
make -j32刷机后先确认 SELinux 处于 enforcing 模式,再看看微信相关的 denied 是否消失:
adb shell getenforce adb shell dmesg | grep "avc: denied" | grep WeChat如果微信相关 denied 已经清空,再做一轮完整功能回归。我自己的回归清单包括:微信冷启动、扫码/拍照/相册选择、收发消息、文件下载、语音/视频通话。每一项都要实际操作一遍,因为有些权限是延迟申请的,启动不触发,一拍照才触发。
另外要特别提醒,setenforce 0只能在 userdebug 或 eng 版本上临时用,user 版本不允许执行。它只用来做定位,定位完马上setenforce 1恢复强制模式,千万不要带着宽容模式去验收,那样所有权限问题都会被掩盖。
5. 常见问题与排查技巧实录
5.1 问题速查表
我把这个项目里遇到的高频问题和对应的处理思路整理成了表格,方便后面直接查。
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 开机后没有微信图标 | PRODUCT_PACKAGES 未生效或 APK 没编译进去 | 检查 out 目录,单独 make WeChat 后重新打包 |
| 微信首次启动卡在加载界面 | dex 未预编译,或 SELinux 阻止读取某些数据目录 | 确认 dex_preopt 已开启,抓 avc 日志定位 |
| 微信点开就闪退 | 签名被重签,或 APK 架构不匹配 | 重新使用官方 APK,去掉 platform 签名配置 |
| 无法扫码或相机黑屏 | 相机权限未授予,或相机 HAL 的 SELinux 规则缺失 | 先检查运行时权限,再抓 avc 日志 |
| 收不到消息推送 | 后台限制、doze 省电策略,或厂商自定义电池管理拦截 | 检查电量和后台白名单,先别怀疑 SELinux |
| 相册选图空白 | 分区存储权限问题,或 FUSE 路径访问被拒 | 检查本地存储权限,抓 avc 日志确认 |
| OTA 升级后微信消失 | 预置 APK 所在分区被覆盖或清理 | 将预置目录写入持久化配置,并开启 preserve_in_ota |
表格里的前两项是最常见的,几乎每个做预置的项目都会遇到。如果你发现自己改了 PRODUCT_PACKAGES 但编译产物里没有,优先检查是不是模块名写错了,或者 Android.bp 里用了android_app_import但 APK 路径没配对,Soong 在解析阶段就会报错。
5.2 几个容易被忽略的低级坑
第一个坑是 APK 更新后没有清掉 out 目录里的旧产物。我遇到过改完 APK 重新编译,刷机后还是旧版本的情况,排查半天发现是 out 目录缓存没刷新。做预置应用调试时,每次替换 APK 后建议顺手删掉out/target/product/xxx/system/WeChat目录,再重新 make,避免走弯路。
第二个坑是忽略老数据残留。如果这台工程机之前通过 adb install 装过同签名的微信,预置新微信后系统会做包管理校验,报INSTALL_FAILED_UPDATE_INCOMPATIBLE。最干净的办法是刷机前做一次 userdata 全清,或者在验证脚本里加一步pm clear com.tencent.mm。否则你会以为是 SELinux 问题,实际上纯粹是数据残留冲突。
第三个坑是日志抓取时机太晚。avc denied 是内核实时打的,如果问题已经发生完,回头再看日志可能会被其他应用的海量 denied 刷屏。尽量用dmesg -w实时看,配合复现操作,一次抓一条,精准定位。
第四个坑是编译选错策略目录。Android 的 sepolicy 分散在system/sepolicy、vendor/sepolicy和device/.../sepolicy等多个目录,不同分区有不同归属。如果你改的规则涉及 vendor 属性,却写进了 system 目录,有可能引用不到对应的类型。所以加规则前先确认tcontext里的对象类型归属哪个目录,再决定策略文件放哪里。如果是 vendor 自定义节点的 deny,很多情况下要写在vendor/sepolicy下。
5.3 排查顺序建议
最后给一个我自己习惯的排查顺序,可以帮你少走很多冤枉路:
- 先用
pm list packages | grep tencent确认微信已经安装。 - 再用
adb shell dmesg | grep avc抓一次日志,确认有没有 avc denied。 - 没有 denied 但功能异常,就去查运行时权限和组件禁用状态。
- 有 denied,则按 3.2 的方式实时跟踪,逐条分析确认该不该补规则。
- 补完规则重新编译,再回归,回归通过才收工。
把“先确认权限,再调功能”这个顺序刻在脑子里。我见过不少同事一上来就改 sepolicy,最后发现其实是应用自身的权限弹窗没点,白忙活一场。
最后再分享一点个人体会
说实话,这次预置微信处理 avc 权限,整个过程最耗时的不是写 allow 规则,而是判断每一条 denied 到底合不合理。SELinux 的设计思路本来就是“默认拒绝”,它不会告诉你怎么做才安全,只会告诉你“不行”。你需要自己去理解业务场景,理解微信为什么访问这个节点,才能做出准确的授权决策。
另外,独立域方案虽然前期麻烦一点,但长期维护省心。下次如果再做类似项目,我会第一时间就把微信单独拉一个 SELinux 域,而不是先用全局授权跑通再回头改。很多问题当时觉得时间紧先凑合,后面会花更多时间还债,这个教训我算是深刻体会到了。希望这篇复盘能帮正在跟 avc 较劲的同学省下几个晚上。