一文看懂Android SELinux 策略,从“拒绝”到“允许”的距离

对于刚做 Android 系统开发的同学,总会遇到那么几个让人抓狂的时刻。比如你写了一个再简单不过的 Native 守护进程,代码逻辑无懈可击,文件权限也给到了 777,甚至 root 都拿到了,但程序就是跑不起来。

这时候,如果你看到日志里跳出一行 avc: denied,别慌,也别急着把 SELinux 关了了事(setenforce 0)。那是系统在试图保护你,只是它有点"过度紧张",把你当成了陌生人。

今天想聊聊 SELinux。这东西在 Android 5.0 之后成了强制标准,很多开发者对它的态度是"能关则关"。其实,配置 SELinux 并没有那么玄学,它更像是在给系统里的每个进程和文件发"身份证"和"通行证"。

咱们不整那些复杂的理论,就顺着一次真实的开发经历,看看怎么把这层安全网织好。

故事的开始:一个无法启动的服务

假设场景是这样的:我在 AOSP 里加了一个自定义服务叫 my_service,放在 /system/bin/ 下,通过 init.rc 启动。

代码编译烧录,重启手机。满心欢喜地期待服务跑起来,结果 logcat 里冷冷清清。打开 dmesg 或者 logcat -b kernel 一看,满屏都是这样的日志:

** 核心日志分析**

这行日志就是 SELinux 的"判决书"。别被它吓到了,我们像法医一样解剖一下:

第一步:给文件发"身份证"

日志里的 tcontext 暴露了问题:我们的 my_service 可执行文件,虽然躺在 /system/bin/ 下,但系统默认给它贴了个通用的 system_file 标签。

但在我们的策略里,我们希望它是一个专门的服务。所以得先给它换个"马甲"。

这就得提到 file_contexts 文件。这文件就像个户籍登记簿,告诉系统哪个路径对应哪个标签。

在设备的 sepolicy 目录下(通常是 device/<厂商>/<机型>/sepolicy/file_contexts),我们得加上这么一行:

bash 复制代码
/system/bin/my_service    u:object_r:my_service_exec:s0

拆解来看:

  • /system/bin/my_service:这是文件的路径。
  • u:object_r:这是固定的前缀,表示这是个文件对象。
  • my_service_exec:这是重点。这是我们要定义的"类型"。在 SELinux 的语境里,给可执行文件加 _exec 后缀是个约定俗成的习惯,意思是"这是启动服务的源头"。
  • s0:这是安全级别,Android 里一般都用 s0,不用管它。

加上这一行,编译进镜像后,系统就知道:"哦,这个文件不是普通文件,它是 my_service 的启动器。"

第二步:定义"域"与规则

文件有了身份,那进程呢?

init 进程启动这个服务时,SELinux 会执行一个很酷的操作,叫域转换 。它会看:"嘿,init 正在执行一个类型为 my_service_exec 的文件,那新生成的进程就应该进入 my_service 这个域。"

但这还不够,我们得在 .te 文件里把这些规则写清楚。这是整个 SELinux 策略的心脏

my_service.te 文件里,我们通常会这么写:

bash 复制代码
# 1. 定义类型
type my_service, domain;
type my_service_exec, exec_type, file_type;

# 2. 这一行宏非常关键,它自动处理了 init 启动服务时的域转换逻辑
init_daemon_domain(my_service)

这几行代码就像是给 my_service 建立了一个专属的"房间"。以后这个程序跑起来,就被关在这个房间里。

第三步:开门与放行

现在,服务能启动了,但新的问题又来了。比如 my_service 想读写一个硬件节点 /dev/my_hw,或者想访问数据目录。

这时候,那个尽职的保安(SELinux)又出来了。它会检查:my_service 域的进程,有没有权限操作 my_hw_device 类型的文件?

如果没有策略,日志里就会疯狂刷 avc: denied

这时候,很多新手会怎么做?直接 setenforce 0,把保安撤了。但这在发布版本里是绝对禁止的。正确的做法是,根据报错,把"通行证"补上。

.te 文件里加上允许规则:

perl 复制代码
# 允许 my_service 域,对 my_hw_device 类型的字符设备,进行读写打开
allow my_service my_hw_device:chr_file { read write open ioctl };

你看,逻辑是不是很清晰?

谁( my_service) -> 对什么( my_hw_device) -> 做什么( read write...)。

全景视角:SELinux 的"身份体系"

刚才我们只用了 file_contexts.te 文件,但这只是冰山一角。为了让你对整个配置体系有个全景的认识,我们需要区分两类文件:一类是 "贴标签的" (上下文文件),一类是 "定规则的" (策略文件)。

在 Android 开发中,最常用的贴标签的主要有以下 4 个,它们各司其职,共同维护着系统的安全:

1. file_contexts(管文件)

这就是我们刚才用的。它负责给文件系统中的文件、目录、设备节点贴标签。

  • 例子: /system/bin/my_service u:object_r:my_service_exec:s0
  • 作用: 告诉系统,这个路径下的东西是什么类型。

2. seapp_contexts(管App)

这个文件对应用开发者非常重要。它负责给 App 进程和 App 数据目录贴标签。

  • 例子:
ini 复制代码
# 普通第三方 App
user=_app domain=untrusted_app type=app_data_file
# 系统特权 App
user=_app seinfo=platform domain=platform_app type=app_data_file
  • 作用: 当你安装一个 App 时,系统会读取这个文件。如果是普通 App,就把它关进 untrusted_app 的笼子里;如果是系统 App,就给它 platform_apppriv_app 的身份。

3. property_contexts(管属性)

如果你需要在代码里 setpropgetprop(操作系统属性),就需要配这个。

  • 例子: persist.vendor.myapp.enable u:object_r:my_app_prop:s0
  • 作用: 定义哪些进程可以读/写哪些系统属性(如 ro.bootloaderpersist.sys.xxx)。

4. service_contexts(管Binder)

如果你的服务是通过 Binder 暴露给其他 App 调用的,就需要配这个。

  • 例子: my_daemon u:object_r:my_daemon_service:s0
  • 作用: 给 ServiceManager 里的服务注册名打标签,控制谁能调用这个服务。

补充说明: 除了这 4 个"上下文"文件,还有像 hwservice_contexts(管 HIDL 硬件服务)、genfs_contexts(管 /sys /proc 等内核生成文件)等,它们都属于"贴标签"的范畴。而所有的权限逻辑,最终都要汇聚到 .te 文件中通过 allow 规则来落实。

它是如何生效的?

你可能会问,改完这些文件,为什么必须重新编译?

因为 Android 的编译系统(Build System)在编译时,会把这些散落在各个目录下的 .te 文件和 file_contexts 收集起来,通过 checkpolicy 工具编译成一个二进制的 sepolicy 文件,最后打包进 boot.img 或者 system.img 里。

内核启动时,会加载这个二进制策略。所以,你改了源码,如果不重新编译刷机,内核里的保安手里拿的还是旧名单,当然不认你的新规则。

附录:

1. 偷懒神器:audit2allow

手动写规则虽然精准,但有时候太慢了。Android 提供了一个名为 audit2allow 的神器,它能自动分析日志并生成规则建议。

使用姿势:

  1. 抓取日志:
c 复制代码
adb logcat | grep "avc:" > avc.log
  1. 生成建议:
c 复制代码
audit2allow -i avc.log

️ 警告 :工具生成的规则有时过于宽泛(比如直接给了 dac_override),千万不要无脑复制粘贴。一定要结合我们上面讲的逻辑,人工审核一遍,确保遵循"最小权限原则"。

2. 权限天梯:普通 App 与系统 App 的鸿沟

很多开发者会问:"为什么我的 App 获取不到某些系统权限?"这往往不是代码写错了,而是 seapp_contexts 定义的"出身"决定了你的上限。

下表直观展示了普通 App 与系统 App 在 SELinux 策略上的核心差异:

维度 普通 App (untrusted_app) 系统 App (platform_app / priv_app)
判定依据 第三方签名 / 无特殊签名 platform 签名 (LOCAL_CERTIFICATE := platform)
进程域 untrusted_app platform_apppriv_app
数据目录 untrusted_app_data_file privapp_data_file
硬件访问 几乎为零(必须通过系统服务中转) 可直接访问部分硬件节点(需策略允许)
典型场景 第三方应用、用户安装的应用 电话、短信、设置、系统桌面

核心结论:普通 App 被关在沙盒里,想碰硬件必须走 Binder 找系统服务;而系统 App 则是"特权阶级",拥有更广阔的视野和操作空间。

3. 实战排错:当 setprop 失败时

假设你在代码里写了一句 SystemProperties.set("persist.vendor.myapp.state", "1"),结果应用崩溃了,Logcat 报出如下错误:

** 核心日志分析**

排错三部曲

  1. 看目标标签tcontext=u:object_r:default_prop:s0。说明系统默认把你这个属性归类为了 default_prop

  2. 看当前权限default_prop 通常只允许系统进程修改,普通 App(untrusted_app)是被禁止 set 的。

  3. 修改策略

    • 第一步 :在 property_contexts 中定义专属标签:

    persist.vendor.myapp.state u:object_r:my_app_prop:s0

markdown 复制代码
- **第二步**:在 `.te` 文件中赋予权限:
arduino 复制代码
allow untrusted_app my_app_prop:property_service { set };
markdown 复制代码
- **第三步**:重新编译刷机。

理解了这一点,你就明白为什么改了源码必须重新编译------因为内核只认那个编译好的二进制文件,它不读源码。

写在最后

配置 SELinux 其实就是一场"贴标签"的游戏。

刚开始接触时,你会觉得那些 type, domain, context 很绕。但当你习惯了这种思维方式------先定义身份,再定义权限------你会发现它其实是系统稳定性的最后一道防线。

它强迫我们把代码隔离好,不让一个不起眼的服务拥有操作整个系统的权力。下次再遇到 avc: denied,不妨心平气和地看看日志,给它补一张"通行证"就好。毕竟,安全这东西,麻烦点,总比被黑了强。

相关推荐
宝杰X75 小时前
Android Room3 多平台数据库
android·数据库
huibin1478523697 小时前
热缓解学习记录
android·学习
小田的博客10 小时前
SAP MM 供应商银行主数据更新报错!message R1228!
android·java·服务器
一笑的小酒馆11 小时前
Android智能猫砂盆视频加载慢卡顿问题分析
android
只会cv的小前端13 小时前
七巧低代码服务端脚本使用方法
android·低代码·rxjava
13 小时前
Android 自定义 View 实战:从零打造工业级全向摇杆控件(OmniJoystickView)
android·kotlin·自定义view·摇杆控件
雨白13 小时前
深入理解 Kotlin 协程 (十一):以逸待劳,探秘 select 多路复用与并发安全策略
android·kotlin
hunterandroid16 小时前
[Android 从零到一] Android 深度链接与 App Links:从 URI Scheme 到可验证的应用跳转
android
我命由我1234518 小时前
Jetpack Compose - Material Design 断点范围、WindowSizeClass、针对不同屏幕尺寸创建预览、四类导航栏
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
2501_9151063218 小时前
Python爬虫从零开始:七天掌握HTTP协议、数据解析与反爬策略
android·ios·小程序·https·uni-app·iphone·webview