☰
Android 16预置第三方应用:SELinux avc denied 排查与解决
2026/10/20 5:53:00 网站建设 项目流程

如果你拿到过一批 Android 16 定制的终端设备,开机就已经预装了某款聊天软件,一登录就能收发消息,会觉得这套东西很顺。真正接手这个需求的时候才会发现,把一个第三方 APK 塞进系统镜像只是第一步,后面还跟着权限白名单、SELinux 策略、分区大小、平台签名一堆问题。这个项目之所以值得专门写一篇,是因为它在 Android 16 上踩到了一个很典型的坑:avc denied 刷屏,功能表面能跑,但底层权限被卡死。

我这次做的是预置某主流即时通讯应用,交给客户之后反馈“登录正常,但部分功能在设备重启后异常”。排查下来,一半是运行时权限没有预授予,另一半是 SELinux 的 avc denied 在作怪。Android 16 对系统应用、特权应用和分区权限的约束比之前更严格,第三方 APK 直接放进 system image 已经不够了,需要配套处理签名、白名单和 sepolicy 规则。这篇文章会把整个处理过程完整拆开,从方案选型到 avc 日志定位,再到规则编写和上线验证,尽量把每一步“为什么这么做”也讲清楚,给后面再做预置应用的同行留一份可直接参考的记录。

1. 项目背景与核心挑战

1.1 需求拆解:为什么会有预置应用的诉求

终端厂商、政企定制、行业平板这类项目里,预置即时通讯软件是很常见的需求。站在客户角度,设备发到用户手里就带着可用账号体系,开机完成向导之后直接进入工作环境,省去应用商店搜索和下载的步骤,也方便做统一版本管理。站在厂商角度,预置应用可以配合系统能力做深度定制,比如开机自启、静默升级、限制卸载、与设备管理策略联动等,这些都是普通安装方式做不到的。

但需求听起来简单,落地却有不少槛。第三方 APK 不像自有应用那样能随意修改,签名、架构、targetSdk 都是发行方定的。预置位置放错了,应用可能无法获得特权权限;权限白名单配置不全,应用在 Android 16 上可能直接跳过某些关键权限;AVC 权限处理不当,SELinux 在 enforcing 模式下会把应用访问特定节点、设备、属性的操作全部挡掉。很多团队第一次做预置项目,都是先遇到开局刷 avc denied 日志,才意识到这个需求没有想象中那么“把 APK 扔进镜像”就完了。

所以第一步不是写代码,而是把需求拆成几个明确的任务:确定预置位置、确认签名方案、补齐权限白名单、规划 sepolicy 规则、完成验证流程。这样后面每一步都有据可依。

1.2 Android 16 在预置场景下的主要变化

Android 16 对应的 API 版本是 36,这一代在应用兼容性、分区方案和安全模型上都有调整,对预置新应用的影响尤其明显。最直观的一点是 16 KB page size 的要求,第三方 APK 如果还停留在旧的内存对齐方式,安装到 Android 16 设备上可能根本跑不起来。预置之前最好先用兼容性工具或直接在设备上跑一遍,确认应用的 so 库支持 16 KB 对齐。

分区结构上,product、system、vendor 分区的边界更清晰,Treble 架构下,能放 product 分区的应用就不要放到 system,能放 vendor 分区的系统组件也不应该随便挪到 system。应用如果依赖系统和其他应用的交互,放进错误分区会导致启动阶段异常。尤其要注意的是,Android 16 对特权应用权限白名单的检查更加严格,priv-app 默认不再自动获得所有权限,权限不在白名单里时,系统会在 logcat 里提示 not whitelisted,而不再是之前的“只警告但继续放行”。

SELinux 策略本身也在演进。平台 sepolicy 和 vendor sepolicy 的编译合并规则越来越细,新增规则如果类型声明位置不对,可能在构建阶段就报错。即使规则写对了,也要在 enforcing 模式下做完整回归,不能只在 permissive 模式下看功能通了就收工。

1.3 整体风险识别:权限问题的影响面

预置应用的风险点不只在 SELinux 一处。按照常见排查路径,影响面可以分为四层。

第一层是安装层。APK 能不能正确安装到 priv-app 目录、扫描时能不能被 PackageManager 识别、解压后是否完整。这一层挂了,应用会在开机时直接不出现,或者启动崩溃。

第二层是签名层。priv-app 应用如果要获得 privileged 权限,通常需要持有平台签名,或者至少与系统权限白名单的签名要求匹配。签名不一致,白名单直接失效,应用可能仍然能跑,但你会看到大量权限 granted=false。

第三层是权限层。privapp-permissions 白名单、运行时权限、Android 16 对通知权限的后置要求,都会影响应用的实际体验。很多即时通讯应用依赖通知、定位、麦克风等权限,预置时不处理,用户首次打开应用时依然会被权限弹窗打断。

第四层才是 SELinux。在 enforcing 模式下,应用访问设备的某个 socket、写一个系统属性、读一个系统分区文件,都可能触发 avc denied。这一层问题最隐蔽,因为它不会让你的应用直接闪退,而是表现为“某个功能偶尔不工作”,排查起来非常消耗时间。

2. 实施方案设计与资源准备

2.1 预置形态选型

预置应用不是只有一种放法,常见的有三类。

第一类是放在 system/priv-app,作为特权应用。优点是权限等级高,能使用 privileged 权限,适合需要深度系统集成的场景。缺点是 system 分区一般不适合频繁升级,覆盖更新复杂,而且对签名要求严格。

第二类是放在 product/priv-app 或 product/app。product 分区在 Android 10 之后独立出来,专门给厂商定制应用使用,配合动态分区机制,更新时更容易替换。我自己更推荐把这种第三方 IM 应用放到 product/priv-app,既保留特权应用能力,又不污染 system 分区的原始逻辑。

第三类是首次开机安装到 data 分区。这种方式在调试阶段很方便,但在量产设备上不稳定,恢复出厂设置后就没了,不算真正的预置。

选型时还要考虑卸载策略。如果客户要求“可卸载但恢复出厂后回归”,更适合用 product 分区加“可卸载系统应用”机制;如果要求完全不可卸载,就必须放在 system/priv-app,并且应用自己不能声明 allowBackup=false 之外的卸载入口。

对于这个项目,我最终选择了 product/priv-app 方案。原因是即时通讯应用需要后台推送、通知铃音、通话服务等常驻能力,特权等级不能太低,而 product 分区又比 system 分区更容易做后续升级,整体风险更可控。

2.2 构建产物与签名要求

预置一个第三方 APK 之前,必须确认它的签名状态。最理想的情况是对方提供一个使用平台签名重新签名的版本,这样可以在 priv-app 位置直接获得 privileged 能力。但现实往往是对方只给一个正式签名的 APK,不希望你改动它的签名。

不改签名的方案也有,只是要接受权限上限。应用可以作为普通应用预置,运行在 app 目录,所有运行时权限照常弹窗,这也能满足一部分客户需求。但如果客户要求“静默授予权限”“开机自动启动”这类系统能力,签名不够就做不了。

Android 16 对 targetSdk 也有要求。如果应用的 targetSdk 比较旧,安装在 Android 16 上会进入兼容模式,某些行为会被系统限制,后台活动、通知、前台服务类型都会受影响。我这次预置的应用 targetSdk 是 35,问题不大,但如果低于 31,需要考虑兼容模式带来的后台限制。

预置前建议先用工具检查 APK 的以下信息:包名、版本号、签名指纹、targetSdk、so 架构、是否包含 split APK。split APK 在预置时要配置多个 apk 文件,Android.bp 里需要显式声明,不然只会装上其中一个 base 包,运行时会报“缺少资源”。

2.3 权限白名单的编写与验证

priv-app 应用要使用 privileged 权限,必须配置 privapp-permissions 白名单。这个机制从 Android 9 开始就很严格了,Android 16 并没有放宽,反而在 CTS 检查中更看重它。

白名单 XML 的格式很固定,结构如下:

<permissions> <privapp-permissions package="com.example.im"> <permission name="android.permission.INTERNET"/> <permission name="android.permission.ACCESS_NETWORK_STATE"/> <permission name="android.permission.POST_NOTIFICATIONS"/> <permission name="android.permission.CAMERA"/> <permission name="android.permission.RECORD_AUDIO"/> <permission name="android.permission.ACCESS_FINE_LOCATION"/> <permission name="android.permission.READ_PHONE_STATE"/> <permission name="android.permission.FOREGROUND_SERVICE"/> <permission name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/> </privapp-permissions> </permissions>

白名单文件要放到 product 分区的 etc/permissions 目录下,常见路径是product/etc/permissions/privapp-permissions-imapp.xml,然后在产品配置里加入如下变量:

PRODUCT_COPY_FILES += \ $(LOCAL_PATH)/configs/privapp-permissions-imapp.xml:$(TARGET_COPY_OUT_PRODUCT)/etc/permissions/privapp-permissions-imapp.xml

写完白名单之后,用adb shell dumpsys package com.example.im查看权限状态。重点关注两个地方:一是 requested permissions 列表里有没有你声明的权限,二是 granted=true 的数量是否符合预期。如果出现 not whitelisted 的日志,说明 XML 文件路径没有生效,或者包名与 APK 的 package name 不一致。

有一点需要特别提醒:运行时权限不会因为写进白名单就自动变成已授予。Camera、Location、Microphone 这类权限,系统依然要求应用在运行时发起请求,只不过 priv-app 在白名单里的权限会被系统视为“已授权来源”,弹窗流程可能被跳过。应用本身还是要按照标准流程 requestPermissions,否则拿不到完整能力。

3. AVC 权限问题的根因分析与处理流程

3.1 SELinux 日志的识别方法

AVC 是 Android 里 SELinux 的审计记录,全称是 Access Vector Cache。系统在 enforcing 模式下拦截了一次访问,就会输出一条 denied 记录。这个日志直接用 logcat 就能抓到:

adb logcat -d | grep avc

也可以抓更底层的内核日志:

adb shell dmesg | grep avc

一条典型的 avc denied 日志长这样:

avc: denied { read write open } for pid=1234 comm="IMApp" name="kmem" dev="tmpfs" ino=34 scontext=u:r:priv_app:s0:c512,c768 tcontext=u:object_r:device:s0 tclass=chr_file permissive=0

这里信息量很大。scontext 是访问者的安全上下文,说明发起访问的进程跑在哪个 domain;tcontext 是被访问对象的类型;tclass 是对象分类;大括号里是具体要授予的权限。permissive=0 说明当前是 enforcing 模式,这次拦截真实生效了,不是调试模式下的假报警。

新手最常见的错误是把priv_app当成应用包名去理解。其实priv_app是 Android 系统给所有特权应用分配的默认 SELinux domain,凡是放在 priv-app 目录下的应用,如果没有特殊规则,都会落到这个 domain。所以 avc 日志里有priv_app不代表规则是针对某个应用的,它代表整个特权应用类型都缺这个权限。

3.2 编写 sepolicy 规则的常见路径

处理 avc 问题要想清楚一个前提:是给所有 priv-app 应用放权限,还是只给这个 IM 应用放权限。直接给priv_app域写 allow 规则最省事,但等于把所有特权应用的权限边界放大了,后面 CTS/GTS 审查以及安全评审都不好交代。更规范的做法是为这个应用建立独立 SELinux domain。

如果你在设备目录下创建 sepolicy 文件,比如device/oem/imapp/sepolicy/imapp.te,内容可以这样编写:

type imapp, domain, netdomain; type imapp_exec, exec_type, file_type, system_file_type; # 基本网络与文件读取 allow imapp self:tcp_socket create_socket_perms; allow imapp self:udp_socket create_socket_perms; allow imapp self:packet_socket create_socket_perms; allow imapp proc_net:file { open read getattr }; # 按需补充 avc 日志里的对象 allow imapp sysfs_type:file { open read getattr }; allow imapp device:chr_file { open read write ioctl };

然后需要把应用映射到这个 domain。seapp_contexts 文件里加一行:

user=system domain=imapp type=imapp_app_data_file levelFrom=user

这一步的作用是让系统根据 user、包名等条件,把com.example.im的应用进程分配到imappdomain,而不是默认的priv_app。如果这一步没做,之前 te 文件里的规则都不会命中。

还要在 file_contexts 里声明 APK 安装后的目录类型:

/system/product/priv-app/IMApp(/.*)? u:object_r:imapp_exec:s0

这样应用的可执行文件在 SELinux 视角里才是真正属于 imapp 域的。否则即使进程被 seapp_contexts 分配到了 imapp domain,文件标签不对,执行时依然会有 type mismatch 的问题。

写完这些之后,要在产品配置里把 sepolicy 目录指给构建系统:

BOARD_VENDOR_SEPOLICY_DIRS += device/oem/imapp/sepolicy/vendor BOARD_PLATFORM_PRIVATE_SEPOLICY_DIRS += device/oem/imapp/sepolicy/platform

构建时会遇到 neverallow 检查,如果新增规则违反了平台已有的 neverallow,编译会直接报错。这时候不要通过放宽属性来强行绕过,应该先看错误信息里提示的规则冲突点,再调整允许范围。

3.3 权限配置后的验证方法

规则写完不是终点,验证环节比写规则更容易踩坑。我先说一个很实用的小技巧:调试阶段不要急着让整套系统切 enforcing,可以先只给这个应用设置 permissive domain,也就是在 imapp.te 里加一行permissive imapp;。这样系统全局还是 enforcing,其他应用依然受保护,只有这个 IM 应用在 permissive 状态下运行,可以放行所有行为,同时继续输出 avc 日志,方便收集完整规则。

收集到足够日志后,去掉permissive imapp;,编译新的版本,重启设备,用以下命令确认最终状态:

adb shell getenforce

正常应该输出 Enforcing。再看应用进程的实际 domain:

adb shell ps -Z | grep com.example.im

如果进程上下文还是u:r:priv_app:s0,说明 seapp_contexts 没有命中;如果已经变成u:r:imapp:s0,说明映射成功。接下来就是回到功能测试,把 IM 应用的登录、语音、定位、通知、文件收发等路径全部跑一遍,跑完立刻抓一次 avc 日志,确认没有新的 denied 记录。

我建议在测试阶段写一份功能与日志对照表,每项功能操作完成后,记录是否有 avc 输出。不要等全部功能测完再回头看日志,那样对应的功能点容易记混。日志里同一时刻可能有多个模块的 avc 记录,没有对应关系很难定位。

4. 实操过程与问题排查实录

4.1 集成到构建系统的完整步骤

把 IM 应用预置进 Android 16 系统镜像,核心操作可以分为六个步骤。

第一步,准备 APK 文件。放到vendor/oem/imapp/prebuilt/IMApp.apk,避免直接放在项目根目录散落一堆文件。

第二步,创建 Android.bp 预置模块。我用的配置如下:

android_app_import { name: "IMApp", apk: "vendor/oem/imapp/prebuilt/IMApp.apk", privileged: true, presigned: true, owner: "oem", }

如果对方提供了平台签名版本,可以把presigned改为false,并配置签名文件。如果 APK 是 split 包,还需要加extra_apks字段把其他分片一起声明进去。

第三步,在产品的 mk 文件里加入模块名:

PRODUCT_PACKAGES += IMApp

第四步,配置 privapp 白名单,这一步在 2.3 已经说明,核心是确保 XML 能被拷贝到product/etc/permissions下。

第五步,配置 sepolicy 目录和规则。这一步要先确认自己新增的 te、file_contexts、seapp_contexts 文件路径是否被构建系统扫描到。很多情况下规则写对了但没生效,就是因为忘记加 BOARD 变量。

第六步,编译烧录并验证。打包出 system image 后,优先用模拟器或单台测试设备验证,不要直接在客户样机上刷。首轮验证过不了很正常,关键是看日志定位问题来源。

4.2 实测遇到的三个权限异常

这个项目里遇到的第一类异常是应用启动后登录超时。界面能够打开,但一直连不上服务。抓 logcat,看到大量avc: denied { ioctl } for name="wlan0"这样的记录。原因是应用尝试直接操作网络 socket 和网络接口文件,而 SELinux 的 net 相关规则没有覆盖到。我通过给 imapp 域补充网络相关 allow 规则解决。

完整规则片段:

allow imapp netd:fd use; allow imapp netd_socket:unix_stream_socket { connect read write }; allow imapp proc_net:file { open read getattr }; allow imapp self:udp_socket { create_socket_perms };

这类规则要谨慎,尤其是涉及网络字节流 socket 的,过度放开会让应用绕过系统网络管理。我只放了实际日志里出现的那几条。

第二类异常是写属性失败。应用启动时会向persist.imapp.last_login这类属性写值,logcat 里报avc: denied { write } for pid=... name="property"。这个问题的关键不是数据文件权限,而是 property_service 的类型标签。我在 property_contexts 里给自定义属性加了标签:

persist.imapp. u:object_r:imapp_prop:s0

然后在 te 规则中补充:

allow imapp imapp_prop:property_service { set };

第三类异常是数据目录下文件访问被拒。应用会创建自己的数据库、缓存文件,但 data 目录的标签没有被正确分配。这个问题的表象是应用启动后功能正常,一旦写入较大文件就失败。最后在 seapp_contexts 里的 domain 映射生效之后才解决,因为此前应用跑在 priv_app domain,data 目录类型与规则不匹配,写入失败也在预料之中。

4.3 常见问题速查表

现象日志特征常见原因解决方向
应用不在桌面显示开机日志无 package 扫描记录Android.bp 未声明或路径错误确认 PRODUCT_PACKAGES 与 APK 路径
启动崩溃dex2oat 失败、so 加载失败未适配 16 KB 页大小提供新版本 APK 或重新编译 so
特权权限无效not whitelistedprivapp-permissions 未生效检查 XML 路径和包名
功能偶发失败avc denied 出现SELinux 规则缺失按日志补充 allow 规则
写属性失败avc denied 涉及 propertyproperty_contexts 未配置添加属性类型标签
数据目录读写失败avc denied 涉及 data 目录seapp_contexts 未映射补充 imapp domain 映射
安装后无法使用运行时权限权限 granted=false权限弹窗被系统静默覆盖检查运行时请求流程与白名单

这张表是经过几轮迭代总结出来的。遇到问题先按“现象 -> 日志 -> 原因”的顺序查,不要一上来就改动 sepolicy。很多权限问题其实是白名单或签名的问题,改了 SELinux 也不会解决。

4.4 经验总结与避坑清单

踩过几次坑之后,我现在做预置应用的处理顺序已经固定了:先确认签名和 targetSdk,再放 priv-app 目录,配白名单,最后处理 SELinux。顺序反了很容易白费功夫。

给 imapp 这类第三方应用单独建 SELinux domain 的成本虽然比直接放宽 priv_app 高,但对后续维护和过审更有利。如果你在客户现场遇到“应用能跑,但功能不完整”的情况,先去抓 avc 日志,不要猜测权限问题。avc 日志会明确告诉你缺的是read、write还是ioctl,比盲目加规则准得多。

我还有一个习惯:每次上线前用同一个版本的 APK 做一次“冷启动测试”,也就是设备擦除数据后开机,不进第二空间,直接用主用户跑完所有功能。预置应用有一个坑是开发调试时数据还在,掩盖了首次启动的数据目录权限问题。擦除数据后冷启动,数据目录会重新创建,才能真正检验 seapp_contexts 和文件标签是否配置正确。

最后再分享一个小技巧:正式发版之前,把整份 avc 日志搜集起来,用audit2allow之类的工具生成建议规则,然后逐条与实际功能对应。工具生成的规则往往比实际需要的宽很多,不能一键全采纳。逐条对应虽然慢,但能保证每条规则都禁得起追问。对这个 IM 项目来说,最终提交的 sepolicy 规则一共只有十几条,每一条都能解释出对应的功能点和触发时机。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询