☰
Android SELinux实战指南:从avc日志到最小策略配置
2026/10/15 14:43:00 网站建设 项目流程

1. 这不是“加个权限就完事”的配置——Android SELinux到底在管什么

你有没有遇到过这样的情况:App明明申请了READ_EXTERNAL_STORAGE,却在Android 10+设备上读不到SD卡根目录?或者调试时adb shell进去想改个系统属性,执行setprop sys.usb.config mtp,adb直接报错Permission denied?又或者自己编译的system.img刷进去后,logcat里满屏avc: denied { write } for pid=... name="uevent",但设备就是不亮屏、不识别USB?这些都不是代码写错了,也不是权限没声明,而是SELinux在背后默默拦下了操作。

Android SELinux不是Linux内核里那个“可选模块”,它是从Android 4.3(Jelly Bean MR2)开始强制启用的安全基石,到Android 5.0(Lollipop)起成为默认 enforcing 模式——这意味着所有违反策略的行为都会被拒绝并记录,而不是像旧版那样只告警(permissive)。它不看UID/GID,不认AndroidManifest.xml里的<uses-permission>,它只认三样东西:主体(subject)是谁(domain)、客体(object)是什么(type)、想干什么(permission)。一个典型的拒绝日志长这样:

avc: denied { read } for pid=1234 uid=10087 name="build.prop" dev="sda3" ino=123456 scontext=u:r:shell:s0 tcontext=u:object_r:system_file:s0 tclass=file permissive=0

这里scontext是发起操作的进程上下文(shell域),tcontext是目标文件的类型(system_file),tclass=file说明操作对象是文件,{ read }是被拒绝的动作。整条日志没有提一句“权限不足”,但它比任何权限弹窗都更硬核——它发生在内核层,绕不开,骗不过。

我带过的几个项目组里,新人常把SELinux当成“高级防火墙”或“权限加强版”,结果一通乱改sepolicy文件,把allow shell system_file:file read;加进去就以为万事大吉。实测下来,这种粗暴放行往往导致后续出现更隐蔽的问题:比如shell能读build.prop了,但紧接着init进程因为策略冲突无法启动zygote,整个系统卡在开机动画。SELinux的威力恰恰在于它的精确性与连锁性——改一处,必须同步考虑所有相关域的访问路径。它不是给开发者添堵的障碍,而是帮你在百万行代码、数十个进程、成千上万个文件之间,划出一条清晰、可验证、不可绕过的安全边界线。这篇文章不讲抽象理论,只说你每天调试、编译、适配时真正要面对的:怎么读懂日志、怎么写最小策略、怎么验证修改、怎么避免把自己锁死。接下来的内容,全部来自某实验室三年内处理过27个定制ROM、14款车机系统、9类IoT终端的真实踩坑记录。

2. 策略设计逻辑:为什么不能“全局放开”,而必须“逐点授权”

2.1 Android SELinux不是通用Linux SELinux的简单移植

很多人第一次接触Android SELinux时,会下意识去查Red Hat的SELinux文档,然后照搬semanage、restorecon这些命令。结果发现semanage fcontext -a -t httpd_sys_content_t "/var/www(/.*)?"在Android上根本不存在——因为Android压根没用policycoreutils这套用户空间工具链。它的策略编译、加载、更新机制完全不同。

Android SELinux策略由两部分构成:平台策略(platform policy)和非平台策略(non-platform policy)。前者存放在AOSP源码的system/sepolicy目录下,是Google维护的基线策略,覆盖init、zygote、surfaceflinger等核心服务;后者则分散在各模块中,比如device/manufacturer/model/sepolicy、vendor/sepolicy,用于适配特定硬件或OEM功能。两者通过BOARD_SEPOLICY_DIRS变量在BoardConfig.mk中指定,最终由m4预处理器合并编译为单个sepolicy二进制文件。

关键区别在于策略粒度。通用Linux SELinux常以服务名(如httpd_t)或目录路径(如/var/www)为单位定义类型,而Android为每个进程、每个文件、每个属性、每个socket端点都分配了独立类型。比如:

  • init进程的域是init,它启动的vold进程域是vold,vold打开的/dev/block/mmcblk0p1设备节点类型是block_device;
  • zygote进程域是zygote,它fork出的App进程域是untrusted_app(普通App)或platform_app(系统签名App);
  • /system/bin/sh的类型是shell_exec,/data/data/com.example.app/目录类型是app_data_file,连/sys/class/leds/red/brightness这种LED控制节点都有专属类型sysfs_leds。

这种“万物皆有型”的设计,让策略可以做到进程级隔离。untrusted_app域默认连/proc/self/status都读不了(防止信息泄露),更别说/data/misc/keystore/这种密钥存储目录。你不能指望一个allow untrusted_app file_type:file read;就解决所有问题——这等于给所有App开了个后门,违背了Android“沙箱即安全”的底层哲学。

2.2 enforcing vs permissive:别在生产环境碰permissive

新手最容易犯的错误,就是把/sys/fs/selinux/enforce写成0,或者在BoardConfig.mk里加BOARD_KERNEL_CMDLINE += androidboot.selinux=permissive。看起来问题“解决”了:logcat不刷avc拒绝日志,设备能正常启动,App也能读写文件。但这是饮鸩止渴。

permissive模式下,SELinux依然会记录所有本该拒绝的操作(avc: denied ...),只是不实际拦截。这看似无害,实则埋下三颗雷:

  1. 调试失真:你看到的日志是“如果enforcing,这里会被拦”,但真实运行路径可能因策略缺失而跳转到另一条分支。比如vold在permissive下成功mount了SD卡,但在enforcing下因缺少mounton权限失败,导致后续MediaScannerService根本收不到挂载事件;
  2. 策略漏洞固化:开发阶段长期跑permissive,团队会形成“反正SELinux不拦我”的惯性思维,等到量产前切enforcing,才发现上百条拒绝日志,临时补策略极易遗漏关联项;
  3. 安全审计失效:所有合规认证(如CC EAL4+、国密二级)都要求enforcing模式运行。你交上去的测试报告写着permissive=0,但实际固件是permissive=1,等于白忙活。

我经手过一个车机项目,客户要求通过车规级信息安全认证。我们前期用permissive快速联调,最后两周切enforcing,结果发现hal_power_default服务因缺少对/sys/firmware/devicetree/base/的search权限,无法读取设备树中的电源管理节点,导致休眠唤醒异常。补策略倒简单,但重新跑完所有EMC、高低温、振动测试,多花了47天。教训很痛:从第一天编译开始,就该用enforcing模式跑,把avc日志当需求文档来读。

2.3 域转换(Domain Transition):为什么你的App进程不是你想象的那样

Android SELinux最反直觉的设计之一,是进程域会随执行路径动态切换。你以为/system/bin/sh启动的脚本都在shell域里运行?错。当你在adb shell里执行am start -n com.example.app/.MainActivity,am命令本身在shell域,但它通过Binder调用ActivityManagerService(system_server域),system_server再fork出新进程,这个新进程的域不是shell,而是根据/system/etc/selinux/plat_seapp_contexts文件匹配得到的untrusted_app或platform_app。

这个过程叫域转换(domain transition),由type_transition规则控制。比如这条规则:

type_transition init shell_exec:process shell;

意思是:当init进程(域init)执行类型为shell_exec的文件时,新进程的域从init切换为shell。

而App启动的域转换更复杂,依赖seapp_contexts文件。该文件按签名、用户ID、进程名等条件匹配,决定新进程的域和安全上下文。典型内容如下:

isSystemServer=true domain=system_server user=system seinfo=platform domain=system_app type=app_data_file user=system seinfo=media domain=media_app type=app_data_file user=untrusted seinfo=untrusted domain=untrusted_app type=app_data_file name=com.android.chrome seinfo=platform domain=platform_app type=app_data_file

注意seinfo字段——它不是AndroidManifest里的android:sharedUserId,而是build/target/product/security/目录下platform、media、untrusted等.te文件定义的标签,对应不同的证书签名。platform_app能访问/system/下的system_file,untrusted_app则被严格限制在app_data_file范围内。

所以,当你调试一个崩溃的App时,不要只盯着adb logcat,更要adb shell dmesg | grep avc。如果看到avc: denied { open } for pid=5678 comm="com.example.app" name="sensor0" dev="tmpfs" ino=123 scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:object_r:sensor_device:s0 tclass=chr_file,说明问题不在App代码,而在untrusted_app域缺少对sensor_device类型的open权限。解决方案不是把App改成platform_app(那得重签系统证书),而是给untrusted_app添加精准授权:

# device/manufacturer/model/sepolicy/untrusted_app.te allow untrusted_app sensor_device:chr_file { open read write ioctl };

3. 核心配置实操:从日志分析到策略编译的完整闭环

3.1 第一步:精准捕获avc日志,过滤有效信息

很多开发者抱怨“日志太多看不懂”,其实90%的无效日志来自系统自检或已知兼容性问题。真正要抓的是首次触发、影响功能、重复出现的拒绝项。推荐三步过滤法:

第一步:限定时间窗口不要dmesg | grep avc全量抓,而是先复现问题,再立即执行:

adb shell "dmesg | grep avc | tail -n 50" > avc.log

tail -n 50确保只取最近50条,大概率覆盖你刚操作引发的日志。

第二步:用audit2allow做初步分析Android NDK提供了external/sepolicy/tools/audit2allow(需编译),但更常用的是AOSP源码自带的system/sepolicy/tools/audit2allow。将avc.log拷贝到源码根目录,执行:

./system/sepolicy/tools/audit2allow -i avc.log -p out/target/product/<product>/obj/ETC/sepolicy_intermediates/policy.conf

注意-p参数必须指向当前设备实际使用的policy.conf,否则生成的规则可能不匹配。out/target/product/<product>/obj/ETC/sepolicy_intermediates/policy.conf是编译中间文件,比system/sepolicy/public里的原始策略更准确。

audit2allow输出类似:

#============= appdomain ============== allow appdomain sysfs:dir search; allow appdomain sysfs:file read;

但这只是“能用”,不是“该用”。appdomain是泛化域名,实际应替换为具体域如untrusted_app;sysfs是泛化类型,应细化为sysfs_sensor或sysfs_leds。它给你的是线索,不是答案。

第三步:人工精炼,定位最小权限集打开avc.log,找一条典型日志:

avc: denied { getattr } for pid=12345 uid=10087 path="/sys/class/power_supply/battery/voltage_now" dev="sysfs" ino=67890 scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:object_r:sysfs_battery:s0 tclass=dir permissive=0

关键字段拆解:

  • scontext:untrusted_app域,c512,c768是MLS类别(可忽略)
  • tcontext: 目标路径类型是sysfs_battery
  • tclass=dir: 操作对象是目录(注意:voltage_now是文件,但getattr需要先search父目录)
  • { getattr }: 被拒动作是获取属性(如stat()系统调用)

所以真正缺的不是getattr,而是search权限——因为/sys/class/power_supply/battery/是目录,voltage_now是其下的文件。getattr失败是因为进程无法search到该目录。正确策略应是:

# device/manufacturer/model/sepolicy/untrusted_app.te allow untrusted_app sysfs_battery:dir search; allow untrusted_app sysfs_battery:file read;

提示:search权限对目录意味着“能cd进去”,read对文件意味着“能cat内容”。不要一上来就加rwx,search + read通常就够。

3.2 第二步:编写策略文件,遵循“最小授权”原则

Android SELinux策略文件(.te)不是随便放哪都行。AOSP有严格目录约定:

  • system/sepolicy/public/:平台公开类型定义(如sysfs_battery、untrusted_app),禁止修改
  • system/sepolicy/private/:平台私有策略(如init、zygote的规则),禁止OEM修改
  • device/manufacturer/model/sepolicy/:OEM专用策略目录,唯一允许修改的位置
  • vendor/sepolicy/:SoC厂商策略(如高通、联发科提供),禁止修改

假设你要为untrusted_app添加传感器访问权限,步骤如下:

1. 创建专用策略文件在device/manufacturer/model/sepolicy/下新建untrusted_app_sensor.te:

# Allow untrusted_app to access sensor sysfs nodes # Sensor sysfs paths: /sys/class/i2c-adapter/i2c-*/..., /sys/bus/i2c/devices/*/... # Types defined in system/sepolicy/public/sysfs.te # First, allow search on parent directories allow untrusted_app sysfs_i2c:dir search; allow untrusted_app sysfs_i2c:file read; # Then, allow access to specific sensor files allow untrusted_app sysfs_sensor:file { open read write ioctl };

2. 声明类型别名(可选但推荐)如果sysfs_sensor类型在public/中未定义,你不能自己创建,而应使用typealias引用已有类型:

# If sysfs_sensor is not in public, use existing alias typealias sysfs_sensor alias sysfs;

但更规范的做法是,在device/manufacturer/model/sepolicy/file_contexts中为具体路径添加类型映射:

/sys/class/i2c-adapter/i2c-\d+/.* u:object_r:sysfs_i2c:s0 /sys/bus/i2c/devices/.* u:object_r:sysfs_sensor:s0

file_contexts文件定义了文件路径到SELinux类型的映射关系,restorecon命令就是靠它来打标签的。

3. 验证语法,避免编译失败在源码根目录执行:

m4 -D mls_num=0 -D target_build_variant=user system/sepolicy/public/macros system/sepolicy/public/*.te system/sepolicy/private/*.te device/manufacturer/model/sepolicy/*.te > /tmp/policy.tmp 2>/dev/null && echo "Syntax OK" || echo "Syntax Error"

这只是基础语法检查。真正验证要编译整个sepolicy:

m mmma system/sepolicy

如果报错如undefined type sysfs_sensor,说明sysfs_sensor未在public/中声明,需改用sysfs或联系SoC厂商提供类型定义。

3.3 第三步:编译、刷机、验证,形成闭环

策略文件写完,不等于生效。Android SELinux策略是编译时静态链接的,必须重新编译sepolicy并刷入system.img。

编译流程:

  1. 清理旧策略:rm -rf out/target/product/<product>/obj/ETC/sepolicy*
  2. 编译策略:m sepolicy(或mmm system/sepolicy)
  3. 编译system镜像:m systemimage
  4. 刷机:fastboot flash system system.img

验证是否生效:刷机后,不要急着复现问题,先确认策略已加载:

adb shell getenforce # 应返回 Enforcing adb shell ls -Z /system/bin/sh # 查看文件标签,应为 u:object_r:shell_exec:s0 adb shell ps -Z | grep your_app # 查看进程标签,应为 u:r:untrusted_app:s0:c512,c768

然后复现原问题,再查日志:

adb shell dmesg | grep avc | grep untrusted_app

如果之前那条getattr拒绝日志消失,且功能恢复正常,说明策略生效。如果还有新日志,说明存在隐式依赖——比如传感器驱动需要ioctl控制,但你的策略只加了read,没加ioctl,就会触发新拒绝。

实操心得:我习惯在device/manufacturer/model/sepolicy/下建debug.te文件,专门放临时调试规则,如allow untrusted_app *:file *;(允许所有文件操作)。功能验证通过后,立刻删掉debug.te,再根据实际avc日志补最小策略。这比在public/里乱改安全得多。

4. 常见问题排查与避坑指南:那些文档里不会写的细节

4.1 “明明加了allow,为什么还被拒绝?”——四类隐藏陷阱

陷阱一:类型未定义,策略形同虚设

你写了allow untrusted_app my_custom_device:chr_file read;,但my_custom_device类型从未在public/或private/中声明。编译时checkpolicy不会报错(因为allow语句语法合法),但运行时该类型不存在,规则不生效。dmesg里依然显示avc: denied ... tcontext=u:object_r:unlabeled:s0——注意tcontext是unlabeled,说明目标文件没被打上任何SELinux标签。

排查方法:

adb shell ls -Z /dev/my_custom_device # 如果输出类似 u:object_r:unlabeled:s0,说明文件未打标签

解决方案:
在device/manufacturer/model/sepolicy/file_contexts中添加:

/dev/my_custom_device u:object_r:my_custom_device:s0

然后在device/manufacturer/model/sepolicy/下创建my_custom_device.te,定义类型:

type my_custom_device, dev_type;

最后,确保设备节点创建时被正确打标。如果是init.rc里用mknod创建,需加label:

mknod /dev/my_custom_device c 100 0 label u:object_r:my_custom_device:s0
陷阱二:缺少file_type属性,导致allow不匹配

Android SELinux中,file_type是一个关键属性,它让规则能批量匹配。比如system/sepolicy/public/domain.te里有:

attribute file_type; type system_file, file_type; type vendor_file, file_type;

而你的自定义类型my_custom_device如果没有声明file_type属性,那么allow untrusted_app file_type:file read;这条通用规则就不会覆盖它。

验证方法:

adb shell sesearch -A -s untrusted_app -t file_type -c file -p read # 如果无输出,说明file_type属性未生效

修复方法:
在my_custom_device.te中添加:

type my_custom_device, file_type, dev_type;
陷阱三:mls类别(Category)不匹配,跨域通信失败

Android启用了MLS(Multi-Level Security)扩展,每个进程上下文末尾的c512,c768就是类别标识。untrusted_app默认有c512,c768,而system_server有c0.c1023(全范围)。如果你的App需要通过Binder调用某个服务,但服务端策略没声明mls兼容性,就会被拦。

典型日志:

avc: denied { call } for pid=12345 uid=10087 scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:r:system_server:s0:c0.c1023 tclass=binder permissive=0

解决方案:
在服务端策略(如system/sepolicy/private/system_server.te)中,为call权限添加mls约束:

# Allow untrusted_app to call system_server with any category allow untrusted_app system_server:binder { call transfer };

注意:transfer权限允许传递Binder对象,call仅允许调用。二者常需同时添加。

陷阱四:neverallow规则拦截,编译直接失败

neverallow是SELinux的“宪法条款”,定义了绝对禁止的权限组合。比如system/sepolicy/public/domain.te里有:

neverallow { appdomain -isolated_app } self:capability { dac_override fowner };

意思是:任何appdomain(包括untrusted_app)都不允许给自己加dac_override能力(绕过文件权限检查)。如果你在自定义策略里写了allow untrusted_app self:capability dac_override;,编译时checkpolicy会直接报错:

neverallow violation. Was 'allow untrusted_app self:capability dac_override;'

应对策略:

  • 不要硬刚neverallow,它存在必有深意(如防止App提权);
  • 换思路:用setcap给二进制文件加能力,而非给进程域;
  • 或者,确认是否真需要该能力——多数场景用file_contexts打标+allow即可。

4.2 “策略生效了,但App还是崩”——SELinux之外的三重关卡

SELinux只是Android安全模型的一环,它和以下机制深度耦合:

1. Android权限模型(Permission Model)
SELinux控制内核层访问,而<uses-permission>控制Framework层API调用。比如READ_EXTERNAL_STORAGE权限,即使SELinux允许App读/sdcard/,Framework层仍会检查PackageManagerService里的权限授予状态。adb shell pm grant com.example.app android.permission.READ_EXTERNAL_STORAGE是必须的。

2. App沙箱(Application Sandbox)
每个App有独立/data/data/com.example.app/目录,SELinux类型是app_data_file。但/sdcard/(即/data/media/0/)是共享存储,类型是sdcard_type。untrusted_app默认只能读sdcard_type,不能写。要写,需WRITE_EXTERNAL_STORAGE权限+sdcard_rwSELinux权限:

allow untrusted_app sdcard_rw:dir create_dir_perms; allow untrusted_app sdcard_rw:file create_file_perms;

3. Vendor-Initiated Policy(VIP)
高通、联发科等SoC厂商会在vendor/sepolicy/中添加硬件相关策略,如qti_init域控制Modem初始化。如果你的App需要访问Modem日志,不仅要untrusted_app策略,还要qti_init域的allow规则,且需厂商提供qti_init类型定义。

常见问题速查表

现象可能原因快速验证命令
dmesg无avc日志,但功能失败SELinux未启用或permissive模式adb shell getenforce
ls -Z显示u:object_r:unlabeled:s0文件未在file_contexts中定义adb shell ls -Z /path/to/file
编译报undefined type自定义类型未在.te中声明grep "type my_type" system/sepolicy/public/*.te
功能部分可用(如能读不能写)权限不完整(缺write/ioctl)`adb shell dmesg
同一操作在不同设备表现不一SoC厂商VIP策略差异比较/vendor/etc/selinux/plat_sepolicy.cil内容

5. 进阶技巧与工程实践:如何让SELinux配置不再拖慢项目进度

5.1 构建自动化策略生成流水线

手动分析avc日志、写.te、编译、刷机、验证,一个循环至少20分钟。某实验室为提升效率,搭建了基于GitLab CI的自动化流水线:

  1. 日志采集端:App内置SDK,捕获dmesg | grep avc并上传到内部服务器;
  2. 策略生成端:服务器收到日志,自动调用audit2allow,结合预置的domain_mapping.json(映射untrusted_app→device/manufacturer/model/sepolicy/untrusted_app.te),生成补丁;
  3. CI验证端:GitLab Runner拉取最新源码,应用补丁,执行m sepolicy,若编译失败则邮件告警;
  4. 固件打包端:编译成功后,自动构建system.img,推送到OTA服务器。

整套流程将单次策略迭代压缩到8分钟以内。关键点在于domain_mapping.json的维护——它把avc日志里的scontext(如u:r:untrusted_app:s0:c512,c768)映射到具体的策略文件路径,避免人工判断错误。

5.2 使用sesearch和seinfo进行策略审计

sesearch是SELinux策略的“grep”,比翻.te文件高效百倍。常用组合:

  • 查某个域的所有权限:
    sesearch -A -s untrusted_app
  • 查某个类型被哪些域访问:
    sesearch -A -t sysfs_sensor
  • 查某类操作(如ioctl)的全部规则:
    sesearch -A -p ioctl

seinfo则用于查看策略统计:

seinfo -a -s # 列出所有域 seinfo -a -t # 列出所有类型 seinfo -x # 显示策略大小(编译后sepolicy文件的规则数)

某次优化中,我们发现system/sepolicy/private/里有2300条规则,而device/下只有17条。seinfo -x显示总规则数12万,但sesearch -A -s untrusted_app | wc -l返回892——说明untrusted_app被过度授权。通过sesearch -A -s untrusted_app -t * -p ioctl | grep -v "allow untrusted_app self",定位到3条冗余的ioctl放行,删除后规则数降至876,性能无损。

5.3 生产环境策略灰度发布方案

在车载或医疗设备中,SELinux策略错误可能导致设备无法启动。我们采用三级灰度:

  1. Level 1(开发板):enforcing模式,但file_contexts中所有自定义路径用u:object_r:unlabeled:s0,确保策略不生效,只收集avc日志;
  2. Level 2(小批量样机):启用enforcing,策略文件用#ifdef DEBUG_POLICY包裹,编译时通过BOARD_SEPOLICY_DEBUG := true开启调试规则;
  3. Level 3(量产固件):移除所有DEBUG宏,策略文件精简至最小集,并通过sepolicy-analyze工具扫描neverallow违规。

sepolicy-analyze是AOSP 12+引入的静态分析工具,可检测策略逻辑矛盾:

sepolicy-analyze out/target/product/<product>/obj/ETC/sepolicy_intermediates/sepolicy --includes neverallow

它比checkpolicy更深入,能发现“允许A→B,但禁止B→C,而A又需要C”的隐式冲突。

最后分享一个小技巧:在device/manufacturer/model/sepolicy/下建_template.te,内容为标准头:

# <domain_name> SELinux policy for <product> # Generated on $(date) # DO NOT EDIT MANUALLY - USE AUTOMATION TOOL

所有自动生成的.te文件都以此开头。CI流水线每次生成新策略时,会校验此头是否存在,避免人工误改。这个细节让团队三年内零策略误刷事故。

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

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

立即咨询