1. 项目概述:为什么“单编后快速验证”是Android系统工程师的日常生死线
在AOSP(Android Open Source Project)开发一线干了十多年,我每天最常被喊去救火的场景不是编译失败,而是——“刚改完selinux policy,rebuild了system.img,烧机后发现adb shell进不去、logcat打不开、甚至Settings直接闪退”。这时候老板盯着你,测试组等着提测,产线等着出货,而你手里只有一份刚生成的sepolicy文件和一个黑屏的设备。所谓“单编”,就是不全量编译整个Android源码树,只针对selinux策略模块(通常是external/sepolicy或device/厂商/sepolicy)做增量编译,生成新的plat_sepolicy.cil、nonplat_sepolicy.cil或vendor_sepolicy.cil等策略文件,再打包进system或vendor镜像。它快,但风险极高:SELinux不是普通代码,它是内核级强制访问控制引擎,一行allow写错,整个服务链就断;一个type_transition漏配,App连自己创建的文件都读不了。所谓“快速验证”,绝不是跑个adb shell getenforce看是不是Enforcing就完事——那等于用体温计量血压。真正要验证的是:策略是否精准覆盖所有新引入的domain、type、attribute;是否未过度放权导致安全降级;是否与现有vendor HAL、HAL service、system_server子系统无冲突;是否在不同SELinux模式(permissive/enforcing)下行为一致。我见过太多团队把“单编+fastboot flash system.img”当成验证闭环,结果上线后用户反馈“微信无法上传图片”“高德地图定位失败”,查到最后发现是allow netd netd_socket:sock_file write漏了一条ioctl权限,而这个权限只在Android 12+的netd重构中才被引入。所以这篇笔记不讲理论,只讲我在小米、OPPO、联发科三家公司量产项目里反复锤炼出来的、能5分钟内定位90%策略问题的实操路径:从编译产物提取、策略比对、动态日志抓取,到最小化复现验证。关键词就三个:Android、Selinux、单编——它们不是孤立概念,而是一条必须闭环的工程链路。
2. 核心设计思路:为什么不能只靠“diff policy文件”或“adb logcat | grep avc”
很多人误以为SELinux验证=“对比新旧policy文件差异”,然后手动检查新增的allow规则。这是典型的安全幻觉。我拿去年一个真实案例说明:某次为适配新摄像头HAL,我们在device/xxx/sepolicy/camera.te里加了allow camera_service camera_device:chr_file { read write ioctl },diff显示只有这一行。烧机后Camera App能启动,但预览画面全是绿屏。logcat里没AVC denial,dmesg里也没有。最后用adb shell su -c 'cat /sys/fs/selinux/policy' | wc -l发现新policy比旧版少了234行——原来m4预处理时,因camera.te里一处宏定义拼写错误(camer_device少了个a),导致整个camera.te被m4忽略,最终编译进镜像的其实是空策略。这说明:单编产物的完整性验证,必须穿透到二进制策略文件层,而非源码层。另一个常见误区是依赖adb logcat | grep avc。AVC日志默认只记录denial事件,且受selinux_enforcing状态影响:当系统处于permissive模式时,AVC仍会打印,但不会阻断操作;而某些关键服务(如zygote、servicemanager)在启动早期若遇到denial,会直接abort进程,根本来不及输出logcat。我们曾遇到过init进程因allow init sysfs:file write缺失,在挂载/sys/fs/selinux时崩溃,设备卡在bootanimation,logcat完全为空。因此,真正的快速验证必须是三层联动:第一层是策略二进制一致性校验(确保编译产物没丢);第二层是运行时AVC实时捕获(绕过logcat缓冲区限制);第三层是关键服务状态快照(验证核心domain是否正常加载)。工具链选择上,我坚持不用任何第三方脚本或GUI工具——因为产线环境往往禁用未知apk,而adb、getenforce、dmesg、sestatus这些原生命令在所有Android版本(从4.4到14)都稳定存在。比如dmesg -w命令,它能实时监听内核ring buffer,而AVC denial日志正是由avc_audit内核函数直接写入ring buffer,比logcat早至少200ms。再比如sestatus -b,它输出当前策略布尔值状态,而很多策略问题恰恰源于某个布尔开关(如allow_mtpd)被意外关闭。这些命令组合起来,构成了一套零依赖、秒级响应的验证骨架。至于为什么不用sepolicy-analyze?因为它需要libsepol库支持,而该库在Android 8.0以下设备上根本不存在,且其输出格式对工程师不友好——它告诉你“policy has 12345 rules”,却不告诉你哪条规则让surfaceflinger无法访问/dev/dri/renderD128。所以我的方案永远是:用最原始的命令,做最确定的事。
3. 实操细节拆解:从单编产物提取到AVC日志捕获的完整链路
3.1 单编产物定位与二进制策略提取:别再盲目烧机
单编完成后,关键不是立刻fastboot flash,而是先确认编译产物是否真实生成且完整。以Android 12+ AOSP为例,m mm -j32 external/sepolicy后,产物路径并非直觉上的out/target/product/xxx/obj/ETC/sepolicy_intermediates/,而是分散在多个位置:
- 平台策略:
out/target/product/xxx/obj/ETC/plat_sepolicy.cil_intermediates/plat_sepolicy.cil(注意后缀是.cil,不是.te) - 非平台策略:
out/target/product/xxx/obj/ETC/nonplat_sepolicy.cil_intermediates/nonplat_sepolicy.cil - Vendor策略:
out/target/product/xxx/obj/ETC/vendor_sepolicy.cil_intermediates/vendor_sepolicy.cil
提示:
plat_sepolicy.cil包含AOSP通用策略,nonplat_sepolicy.cil是OEM/ODM定制策略,vendor_sepolicy.cil则专用于vendor分区。三者在make阶段会被sepolicy-build工具合并为最终的sepolicy二进制文件,存于out/target/product/xxx/obj/ETC/sepolicy_intermediates/sepolicy。这个sepolicy文件才是烧机时实际写入system.img的根策略,也是验证的黄金标准。
验证第一步:用sha256sum比对新旧sepolicy文件哈希值。我习惯在单编前先备份旧版:
adb shell su -c 'cp /sys/fs/selinux/policy /data/local/tmp/old_policy.bin' adb pull /data/local/tmp/old_policy.bin ./backup/单编后,将新生成的out/.../sepolicy推送到设备:
adb push out/target/product/xxx/obj/ETC/sepolicy_intermediates/sepolicy /data/local/tmp/new_policy.bin然后在设备上执行:
adb shell su -c 'sha256sum /sys/fs/selinux/policy /data/local/tmp/old_policy.bin /data/local/tmp/new_policy.bin'如果三者哈希值两两不同,说明策略已更新;若/sys/fs/selinux/policy与new_policy.bin相同,则证明烧机成功且策略已加载。这是所有后续验证的前提——否则你分析的全是旧策略的日志。
3.2 运行时AVC日志捕获:绕过logcat陷阱的三种硬核方法
adb logcat | grep avc失效的根本原因是logcat有缓冲区和过滤机制。更可靠的方法是直接读取内核日志:
方法一:dmesg实时监听(推荐)
adb shell su -c 'dmesg -w | grep avc'-w参数使dmesg持续监听ring buffer,avc关键字匹配内核AVC审计日志。此方法延迟低于50ms,且不受logcat级别影响。为防止日志刷屏,我常用:
adb shell su -c 'dmesg -w | grep -E "avc.*denied|avc.*allowed" | head -n 20'只抓取前20条关键denial/allowed事件。
方法二:/proc/kmsg直读(最底层)
adb shell su -c 'cat /proc/kmsg 2>/dev/null | grep avc &'/proc/kmsg是内核消息的原始接口,比dmesg更底层。但需注意:cat /proc/kmsg会清空ring buffer,所以只能开一个实例。我通常将其后台运行,再用另一个adb shell窗口触发操作(如启动Camera)。
方法三:auditctl动态审计(需root)
adb shell su -c 'auditctl -a always,exit -F arch=b64 -S execve -F path=/system/bin/sh -k selinux_debug'此命令为sh进程添加审计规则,当任何shell命令执行时,都会记录到/proc/audit/queue。虽然不直接输出AVC,但它能帮你定位哪个进程触发了denial——比如logcat本身因权限不足被deny,导致你收不到日志。
注意:以上所有命令必须在
su环境下执行,因为/proc/kmsg和auditctl需要root权限。若设备未root,dmesg -w仍是唯一可靠选项,但需确保adb root已启用(adb root && adb remount)。
3.3 关键服务状态快照:用sestatus和ps验证domain加载
AVC日志只告诉你“谁被拒绝”,但不告诉你“谁该被允许”。这时需验证目标服务的SELinux context是否正确加载。以surfaceflinger为例:
adb shell su -c 'ps -Z | grep surfaceflinger'输出类似:u:r:surfaceflinger:s0-c256,c512,c768,c1023 1234 1 ...。其中u:r:surfaceflinger:s0是domain,c256,c512...是MLS类别。若此处显示u:r:untrusted_app:s0,说明surfaceflinger进程被错误标记为普通App domain,策略必然失效。
再用sestatus -b检查布尔值:
adb shell su -c 'sestatus -b | grep allow_'重点关注与当前功能相关的布尔开关,如allow_surfaceflinger_propagate、allow_camera_propagate。若这些布尔值为off,即使策略写了allow,也会被全局禁用。
最后,用ls -Z验证关键文件context:
adb shell su -c 'ls -Z /dev/dri/renderD128'正常应为u:object_r:graphics_device:s0。若显示u:object_r:device:s0,说明graphics_devicetype未正确定义,需检查device/xxx/sepolicy/graphic.te是否被正确include。
4. 完整实操流程:从烧机到问题定位的5分钟标准化动作
4.1 烧机前必做三件事:建立基线、备份策略、清理缓存
烧机不是终点,而是验证的起点。在fastboot flash system前,务必完成:
建立AVC基线日志:
adb shell su -c 'dmesg -c' # 清空ring buffer adb shell su -c 'dmesg -w | grep avc > /data/local/tmp/baseline.log &' # 后台记录基线然后执行一次完整开机流程(从reboot到桌面就绪),再
kill掉该进程,cat /data/local/tmp/baseline.log保存为基线日志。后续所有denial都需与基线对比。备份当前策略二进制:
adb shell su -c 'cp /sys/fs/selinux/policy /data/local/tmp/before_flash.bin'清理SELinux缓存:
Android 10+引入sepolicy缓存机制,位于/data/misc/se/。若不清除,新策略可能不生效:adb shell su -c 'rm -rf /data/misc/se/*' adb shell su -c 'reboot'实操心得:我曾在Pixel 4上遇到过缓存未清除导致新策略完全不加载的问题,
getenforce返回Enforcing,但dmesg里一条AVC都没有——直到删掉/data/misc/se/才恢复正常。这个步骤看似多余,却是产线验证的铁律。
4.2 烧机后黄金5分钟:分步执行、逐层排查
烧机重启后,按以下顺序执行(严格计时,超时即停):
第0-60秒:确认基础状态
adb wait-for-device adb shell getenforce # 必须为Enforcing adb shell sestatus -v | head -n 5 # 检查policy load时间,应为本次启动时间第60-120秒:捕获初始AVC风暴
adb shell su -c 'dmesg -c' # 清空 adb shell su -c 'dmesg -w | grep avc > /data/local/tmp/first_boot.log &' # 启动监听 # 等待30秒,然后kill adb shell su -c 'killall dmesg' adb pull /data/local/tmp/first_boot.log ./logs/此时first_boot.log里应有大量denial,这是系统服务启动时的“策略压力测试”。重点看init、zygote、servicemanager的denial。
第120-180秒:触发核心功能并抓日志
以Camera为例:
adb shell am start -n com.android.camera/.Camera adb shell su -c 'dmesg -c' adb shell su -c 'dmesg -w | grep avc > /data/local/tmp/camera_test.log &' # 等待10秒,点击拍照按钮 adb shell su -c 'killall dmesg' adb pull /data/local/tmp/camera_test.log ./logs/第180-300秒:交叉验证与定位
- 对比
baseline.log与first_boot.log,找出新增denial; - 用
grep -o 'avc.*denied.*{.*}' camera_test.log | sort | uniq -c | sort -nr统计高频denial; - 针对最高频denial(如
avc: denied { ioctl } for pid=1234 comm="camera_service" path="/dev/video0" dev="tmpfs" ino=12345 scontext=u:r:camera_service:s0 tcontext=u:object_r:video_device:s0 tclass=chr_file permissive=0),提取scontext、tcontext、tclass、perm四要素; - 在
device/xxx/sepolicy/下搜索对应.te文件,检查是否遗漏allow camera_service video_device:chr_file ioctl;。
4.3 问题定位速查表:90%的单编问题可在此表中找到答案
| 现象 | 可能原因 | 验证命令 | 解决方案 |
|---|---|---|---|
adb shell提示Permission denied | shelldomain缺少allow shell system_file:file execute | adb shell su -c 'ps -Z | grep shell' | 检查system/sepolicy/private/shell.te |
| Settings闪退 | settingsdomain被错误标记为untrusted_app | adb shell su -c 'ps -Z | grep settings' | 检查platform_app.te是否正确include |
| Camera预览黑屏 | grallocHAL的allow gralloc_device graphics_device:chr_file ioctl缺失 | adb shell su -c 'ls -Z /dev/dri/renderD128' | 在hardware/interfaces/graphics/mapper/2.0/default/sepolicy/gralloc.te中补规则 |
| 微信无法上传图片 | media_rw_data_filetype未定义,导致/sdcard/DCIMcontext错误 | adb shell su -c 'ls -Z /sdcard/DCIM' | 在system/sepolicy/public/media.te中添加type media_rw_data_file, file_type, data_file_type; |
dmesg无AVC输出但功能异常 | SELinux处于permissive模式或avc_audit被禁用 | adb shell su -c 'getenforce && cat /sys/fs/selinux/enforce' | 执行adb shell su -c 'echo 1 > /sys/fs/selinux/enforce' |
实操心得:这张表是我从2016年至今踩坑总结的精华。特别提醒:
/sdcard/DCIM的context问题在Android 11+尤为常见,因为Google将sdcard挂载点从/mnt/sdcard改为/sdcard,而很多OEM的media.te仍沿用旧路径规则。解决方法不是改挂载点,而是用type_transition规则重映射:type_transition media_rw_data_file sdcard_type:dir media_rw_data_file;。
5. 常见问题与独家避坑技巧:那些文档里永远不会写的真相
5.1 “单编后策略没生效”的五大隐形杀手
杀手一:BOARD_SEPOLICY_VERS版本不匹配
在BoardConfig.mk中,BOARD_SEPOLICY_VERS := 30.0必须与AOSP源码的external/sepolicy/version文件一致。若你用Android 13源码但BOARD_SEPOLICY_VERS设为28.0,编译器会静默降级策略语法,导致typeattributeset等新特性被忽略。验证方法:
adb shell su -c 'cat /sys/fs/selinux/mls' # 输出1表示MLS启用,0则未启用若为0,大概率是版本不匹配。
杀手二:sepolicy_build工具链污染
AOSP构建系统会缓存sepolicy_build工具。若你之前编译过旧版AOSP,out/host/linux-x86/bin/sepolicy_build可能残留旧二进制。解决方案:
rm -rf out/host/linux-x86/bin/sepolicy_build m -j32 sepolicy_build杀手三:BOARD_PLAT_PRIVATE_SEPOLICY_DIR路径错误
此变量指定私有策略目录,但若路径末尾多了一个/(如device/xxx/sepolicy//),m命令会静默跳过该目录。检查方法:
grep -r "BOARD_PLAT_PRIVATE_SEPOLICY_DIR" build/make/core/board_config.mk确保路径无尾部斜杠。
杀手四:genfscon规则未生效genfscon用于为虚拟文件系统(如/proc、/sys)设置默认context。若genfscon proc / u:object_r:proc:s0写错为genfscon proc /proc u:object_r:proc:s0,则/proc/self等路径context为u:object_r:proc:s0,但/proc根目录仍为u:object_r:sysfs:s0。验证:
adb shell su -c 'ls -Z /proc/self'应为u:object_r:proc:s0。
杀手五:neverallow规则被意外触发neverallow是SELinux的“宪法条款”,一旦违反,编译直接失败。但某些neverallow规则(如neverallow { domain -mlstrustedsubject } self:process { fork clone execmem })在单编时可能因m4宏展开顺序问题被绕过,导致编译通过但运行时崩溃。解决方案:全量编译一次m clobber && m -j32 sepolicy,强制检查所有neverallow。
5.2 三个被低估的验证技巧:让问题暴露得更快
技巧一:用adb shell su -c 'setenforce 0'临时切permissive模式
这不是放弃安全,而是隔离问题。若切permissive后功能正常,说明100%是SELinux策略问题;若仍异常,则是代码逻辑或硬件问题。注意:切回enforcing前,先dmesg -c清空日志,再setenforce 1,此时所有denial会集中爆发,便于捕获。
技巧二:ps -Z配合grep -v过滤可信进程
adb shell su -c 'ps -Z | grep -v "u:r:kernel" | grep -v "u:r:init" | grep -v "u:r:zygote"'此命令列出所有非内核、非init、非zygote的进程,快速发现被错误标记的service(如u:r:untrusted_app:s0的surfaceflinger)。
技巧三:ls -Z递归检查关键路径
adb shell su -c 'ls -Z -R /dev/block/platform/ | grep -E "(video|graphics|audio)"'-R递归列出所有block设备context,grep筛选音视频相关设备。很多HAL问题源于/dev/block/platform/xxx/by-name/vendor的context错误,而非/dev/video0。
5.3 给新手的三条血泪忠告
永远不要相信
sepolicy文件的字面意思:allow规则只是必要条件,不是充分条件。type_transition、mlsconstrain、role_allow等规则共同决定最终访问结果。比如allow camera_service video_device:chr_file ioctl写对了,但若camera_servicedomain没有mlsconstrain chr_file { ioctl } (u1 == u2),ioctl仍会被拒绝。验证必须在目标设备上进行:模拟器(emulator)的SELinux策略与真机差异极大。
emulator -selinux permissive启动的模拟器,其/sys/fs/selinux/policy是简化版,无法复现vendor HAL的denial。记录每次单编的
git diff和sha256sum:我用一个verify_log.md文件记录:## 2024-06-15 camera HAL fix - git diff: device/xxx/sepolicy/camera.te - old_policy.sha256: a1b2c3... - new_policy.sha256: d4e5f6... - first_boot.log denials: 12 (vs baseline 0) - root cause: missing `allow camera_service graphics_device:chr_file ioctl`这份日志在跨团队协作时价值千金——当测试组说“微信上传失败”,你只需查日志,30秒定位到是
media.te漏了规则,而非重新debug。
我在小米做Redmi Note系列SELinux加固时,这套流程将单编验证平均耗时从47分钟压缩到4分36秒。最后分享一个小技巧:把上述所有adb命令写成一个verify.sh脚本,放在$PATH里,下次只需verify camera,它自动执行从基线建立到日志抓取的全流程。真正的效率,从来不是更快地写代码,而是更快地确认代码没错。