13. Android SELinux(二):域转换、类型转换、文件上下文

好,咱们接着聊 SELinux。上一章我把 SELinux 的基础模型讲清楚了——主体、客体、策略,说白了就是「谁能在哪里对什么东西做什么」。但光有这些还不够,你想想看,一个 App 启动的时候,它的进程一开始是什么域?它访问的文件又该是什么类型?这就引出了我们今天要啃的三个硬骨头:域转换类型转换文件上下文

13.1 域转换:进程的「身份升级」机制

先问一个问题:init 进程启动一个 App,这个 App 的进程一开始是什么域?

答案是 init 域。对,你没看错。所有由 init 直接 fork 出来的进程,初始域都是 init。但一个 App 显然不应该用 init 的权限跑,否则整个系统就乱套了。所以 SELinux 提供了一套机制,让进程在执行特定程序时「切换」到另一个域。这就是域转换

域转换的核心规则长这样:

allow init_t apk_exec:file { read getattr execute };
allow untrusted_app_t apk_exec:file { read getattr execute };
type_transition init_t apk_exec:process untrusted_app_t;

这三行缺一不可。第一行允许 init 域读取并执行 APK 文件;第二行允许目标域也执行同一个文件;第三行才是真正的转换规则——当 init 域执行一个类型为 apk_exec 的文件时,进程域自动切换为 untrusted_app_t

关键点:域转换不是强制的。如果只写了 allow 规则但没有 type_transition,进程会留在原域。我曾经在调试一个系统服务时,发现它的子进程怎么都切不到目标域,查了半天才发现 type_transition 写错了文件名通配符。

我个人习惯把域转换理解为「换工作证」。你拿着 init 的工作证进了大楼,但走到 APK 安装目录这个房间时,门禁系统自动给你换了一张 untrusted_app 的工作证。之后你只能去 untrusted_app 能去的地方。

13.2 类型转换:文件标签的「动态更新」

域转换管的是进程,那文件呢?一个进程创建了一个新文件,这个文件该是什么类型?

默认情况下,新文件的类型继承父目录的类型。但很多时候这并不合理。比如 /data/data/com.example.app/ 目录下创建的文件,应该被打上 app_data_file 的标签,而不是继承 data_file 类型。这就需要类型转换

类型转换的规则格式和域转换很像:

type_transition untrusted_app_t app_data_file:file app_data_file;

这条规则的意思是:当 untrusted_app_t 域的进程在 app_data_file 类型的目录下创建文件时,新文件的类型自动设为 app_data_file

嗯,这里要注意:类型转换只对新建的文件生效。已经存在的文件不会自动变类型。我记得有一次帮同事排查问题,他往 /data/local/tmp 里 push 了一个测试脚本,结果 SELinux 一直报 avc 拒绝。原因就是那个目录的类型是 shell_data_file,但 push 进去的文件类型还是 tmpfs 的默认类型。手动 chcon 一下就好了。

小技巧:调试时可以用 ls -Z 查看文件当前的安全上下文。如果发现类型不对,先用 restorecon 恢复默认标签,别急着改策略。

13.3 文件上下文:标签的「静态映射表」

域转换和类型转换解决的是运行时的问题。但系统启动时,所有文件都还没有标签,谁来给它们打上初始标签?答案是文件上下文(file_contexts)。

文件上下文是一个静态配置文件,通常位于 /system/etc/selinux//vendor/etc/selinux/ 下。它定义了文件路径到安全上下文的映射关系。格式如下:

/system/bin/(.*)    u:object_r:system_file:s0
/data/data/(.*)     u:object_r:app_data_file:s0
/dev/socket/(.*)    u:object_r:socket_device:s0

每一行包含两部分:文件路径的正则表达式,以及对应的安全上下文。系统启动时,init 进程会读取这个文件,然后调用 restoreconsetfiles 给所有文件打标签。

我曾经踩过一个坑:在 file_contexts 里写了一个路径规则,但忘了加通配符 (.*),结果只有那个目录本身被打了标签,里面的子文件全是 unlabeled。后果就是 App 读写子文件时疯狂报 avc 拒绝。

避坑指南:写 file_contexts 时,目录路径一定要用 /data/data/com.example/(.*) 这种形式,不要只写 /data/data/com.example/。否则子文件和子目录不会自动继承标签。

13.4 三者的协作关系

好,现在我们把三个概念串起来。我画了一张图,帮你理清它们的关系:

域转换 · 类型转换 · 文件上下文 协作关系 文件上下文 静态映射表 file_contexts 域转换 进程身份切换 type_transition:process 类型转换 文件标签更新 type_transition:file 提供初始标签 触发创建 执行流程 1. 系统启动 → init 读取 file_contexts → 给所有文件打初始标签 2. 进程执行 → 检查 type_transition:process → 触发域转换 3. 进程创建文件 → 检查 type_transition:file → 触发类型转换 4. 运行时访问 → 基于当前域和文件类型进行 allow 检查 常见问题 • 域转换失败:检查 type_transition 规则 + allow 规则是否都写了 • 文件标签不对:检查 file_contexts 路径正则 + 运行 restorecon

从图中可以看到,文件上下文是基础,它保证了系统启动时所有文件都有正确的标签。域转换和类型转换则是在运行时动态发生的。三者配合,才构成了完整的 SELinux 标签管理体系。

13.5 实战:调试一个域转换问题

光说不练假把式。我分享一个真实案例。

有一次我在移植一个系统服务到新平台,服务启动后一直报 avc 拒绝。用 dmesg | grep avc 一看:

avc: denied { transition } for pid=1234 comm="myservice" path="/system/bin/myservice" dev=mmcblk0p25 ino=5678 scontext=init:init_t tcontext=init:myservice_exec tclass=process permissive=0

这个错误信息很明确:init 域想执行 myservice_exec 类型的文件,但 SELinux 不允许它转换到目标域。问题出在哪里?

我检查了策略文件,发现 type_transition 规则写对了,但 allow init_t myservice_exec:file execute 这条规则没写。说白了,init 连执行这个文件的权限都没有,更别提域转换了。

加上 allow 规则后,问题解决。这个教训让我养成了一个习惯:写域转换规则时,先确认三件事——

  1. 源域是否有执行目标文件的权限?
  2. 目标域是否有执行目标文件的权限?
  3. type_transition 规则是否指向了正确的目标域?

调试命令速查

  • dmesg | grep avc — 查看实时拒绝日志
  • ls -Z /path/to/file — 查看文件安全上下文
  • ps -Z | grep 进程名 — 查看进程安全上下文
  • restorecon -R /path/ — 递归恢复文件标签
  • sesearch -T | grep 域名 — 查看域转换规则

13.6 小结

域转换、类型转换、文件上下文,这三者构成了 SELinux 标签管理的完整闭环。文件上下文是静态的,负责初始化;域转换和类型转换是动态的,负责运行时切换。理解了这个闭环,你就能看懂 SELinux 策略中大部分与标签相关的规则了。

嗯,最后说一句:别怕 avc 日志。它虽然看起来吓人,但其实是 SELinux 给你的最直接的调试线索。每条 avc 都明确告诉了你谁、想做什么、被谁拒绝了。学会读 avc,你就掌握了 SELinux 调试的钥匙。

核心记忆点:域转换管进程身份,类型转换管文件标签,文件上下文管初始映射。三者缺一不可。


公众号:蓝海资料掘金营,微信deep3321