34、SELinux调试:avc日志、策略编写、enforce模式、permissive模式

SELinux,说实话,刚接触Android系统的人看到它头都大。我当年第一次遇到avc denial时,也是一脸懵——明明权限都给了,为什么还报错?后来才明白,SELinux是Android安全模型的最后一道防线。它不看你是什么用户,它只看进程的上下文。

这一章,我们就来聊聊SELinux调试的那些事。avc日志怎么看?策略怎么写?enforce和permissive模式到底怎么用?嗯,一个一个来。

1. avc日志:SELinux的“黑匣子”

avc日志,全称是Access Vector Cache。说白了,就是SELinux拒绝访问时留下的记录。你想想看,系统里某个服务突然挂了,或者某个功能用不了,第一反应是什么?看logcat。但logcat里不一定有avc信息,avc日志通常出现在dmesglogcat -b events里。

我个人习惯,遇到权限问题,先跑这条命令:

adb shell dmesg | grep avc

或者更直接一点:

adb shell logcat -b events | grep avc

avc日志长什么样?我给你看个真实例子:

type=1400 audit(0.0:123): avc: denied { read } for pid=1234 comm="mediaserver" name="config.xml" dev="mmcblk0p25" ino=5678 scontext=u:r:mediaserver:s0 tcontext=u:object_r:system_file:s0 tclass=file permissive=0

这条日志里,信息量很大。我来拆解一下:

  • denied { read }:被拒绝的操作是“读”。
  • comm="mediaserver":发起操作的进程是mediaserver。
  • name="config.xml":它想访问的文件是config.xml。
  • scontext=u:r:mediaserver:s0:源上下文,也就是mediaserver的安全上下文。
  • tcontext=u:object_r:system_file:s0:目标上下文,config.xml属于system_file类型。
  • tclass=file:目标类型是文件。
  • permissive=0:当前是enforce模式,所以真的拒绝了。

我在项目中遇到过,有一次某个第三方应用无法读取配置文件,查了半天logcat没发现异常。后来用dmesg一看,avc denial一大堆。所以,记住:avc日志是SELinux调试的第一手资料。

小技巧: 如果avc日志太多,可以用 grep -v 过滤掉已知的、无害的denial。比如 dmesg | grep avc | grep -v "kernel"

2. enforce模式 vs permissive模式

这两个模式,是SELinux的“开关”。

  • Enforce模式:严格模式。avc日志里记录的denial,会真的阻止操作。系统会报错,功能可能不可用。
  • Permissive模式:宽容模式。avc日志里记录的denial,只记录,不阻止。操作照常进行,但日志里会留下记录。

调试时,我建议先用permissive模式。为什么呢?因为你可以先让功能跑起来,同时收集所有avc denial。等收集全了,再切回enforce模式,一条一条修复。

切换模式的方法:

# 查看当前模式
adb shell getenforce

# 临时切换为permissive(重启后失效)
adb shell setenforce 0

# 切换回enforce
adb shell setenforce 1

注意,setenforce 0 需要root权限。如果你在userdebug版本上,通常没问题。但如果是user版本,可能被限制。

警告: 千万不要在生产环境或用户设备上长期使用permissive模式。这等于把SELinux的防护完全关掉了。我曾经见过有人为了省事,直接关掉SELinux,结果系统被恶意应用搞崩溃了。

3. 策略编写:从avc日志到.te文件

avc日志看懂了,模式也切了,接下来就是写策略。SELinux策略文件以.te结尾,放在system/sepolicy目录下。

还是拿刚才那条avc日志举例:

avc: denied { read } for comm="mediaserver" scontext=u:r:mediaserver:s0 tcontext=u:object_r:system_file:s0 tclass=file

这条日志告诉我们:mediaserver进程想读system_file类型的文件,但被拒绝了。要修复它,我们需要在mediaserver的te文件中添加一条allow规则。

找到mediaserver.te文件,添加:

allow mediaserver system_file:file read;

就这么简单?嗯,大部分情况是的。但有时候,你还需要考虑更多。比如,如果目标文件是system_file类型,但它的路径比较特殊,你可能需要创建一个新的文件类型,或者使用file_type_auto_trans

我整理了一个常见的策略编写对照表:

avc日志中的操作 对应的allow规则 说明
{ read } allow src_type tgt_type:file read; 读文件
{ write } allow src_type tgt_type:file write; 写文件
{ open } allow src_type tgt_type:file open; 打开文件
{ create } allow src_type tgt_type:file create; 创建文件
{ execute } allow src_type tgt_type:file execute; 执行文件
{ search } allow src_type tgt_type:dir search; 搜索目录
{ connectto } allow src_type tgt_type:tcp_socket connectto; TCP连接

写策略时,有几点要注意:

  • 不要滥用allow *:我曾经见过有人为了省事,直接写allow mediaserver *:* *;。这等于把SELinux的防护全废了。一定要精确到具体的类型和操作。
  • 使用宏定义:Android SELinux提供了很多宏,比如unix_socket_connectfile_type_auto_trans等。用宏可以减少代码量,也更容易维护。
  • 编译后验证:写完策略,编译刷机后,记得切回enforce模式,然后复现之前的操作,看看avc日志是否还有新的denial。

4. 实战:一次完整的SELinux调试流程

光说不练假把式。我拿一个真实案例,带你走一遍流程。

场景: 某个系统服务无法读取/data/local/tmp/test.txt文件。

第一步:查看avc日志

adb shell dmesg | grep avc

输出:

avc: denied { read } for pid=5678 comm="system_server" name="test.txt" dev="mmcblk0p29" ino=1234 scontext=u:r:system_server:s0 tcontext=u:object_r:shell_data_file:s0 tclass=file permissive=0

第二步:分析

源上下文是system_server,目标上下文是shell_data_file。system_server想读shell_data_file类型的文件,被拒绝了。

第三步:临时切到permissive模式

adb shell setenforce 0

确认功能恢复正常。然后收集所有avc日志。

第四步:编写策略

system/sepolicy/private/system_server.te中添加:

allow system_server shell_data_file:file read;

第五步:编译并验证

编译后刷机,切回enforce模式,复现操作。再次查看avc日志,确认没有新的denial。

核心要点: SELinux调试的本质,就是“看日志 → 写策略 → 验证”的循环。不要怕avc日志多,一条一条处理,总能搞定。

5. 知识体系:一张图看懂SELinux调试

下面这张SVG图,总结了SELinux调试的核心流程。你可以把它当作一个快速参考。

SELinux调试核心流程 发现问题 功能异常/权限错误 查看avc日志 dmesg | grep avc 分析上下文 scontext / tcontext 切Permissive模式 setenforce 0 编写策略 allow ... .te文件 编译 & 验证 切回enforce模式 仍有denial?继续循环 核心原则:先放行(permissive),再收紧(enforce) 不要跳过avc日志分析,直接写策略——那是在碰运气

这张图里,我特意把“切Permissive模式”放在“编写策略”之前。为什么?因为只有先放行,你才能确认问题确实出在SELinux上,而不是其他原因。我曾经跳过这一步,直接写策略,结果发现根本就不是SELinux的问题——白忙活一场。

6. 避坑指南

最后,分享几个我踩过的坑:

  • 不要忽略neverallow规则:Android SELinux有全局的neverallow规则,你写的allow规则如果和neverallow冲突,编译会报错。这时候,你需要考虑是不是设计上就有问题,而不是强行绕过。
  • 注意文件上下文:有时候,avc日志里显示的是system_file类型,但你以为文件是data_file类型。这是因为文件上下文(file_contexts)没有正确配置。记得检查file_contexts文件。
  • permissive模式下的“假象”:在permissive模式下,功能正常不代表策略写对了。因为permissive模式只是不阻止,但avc日志里可能还有大量denial。一定要切回enforce模式验证。

嗯,SELinux调试其实没那么可怕。掌握avc日志、理解enforce和permissive模式、学会写策略,这三板斧用好了,大部分问题都能解决。记住,耐心一点,一条一条处理avc denial,系统会越来越安全。


专注资料整理