一文看懂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,不妨心平气和地看看日志,给它补一张"通行证"就好。毕竟,安全这东西,麻烦点,总比被黑了强。

相关推荐
客卿1233 小时前
用两个栈实现队列
android·java·开发语言
studyForMokey3 小时前
【Android面试】Gradle专题
android·面试·职场和发展
向上_503582915 小时前
配置Protobuf输出Java文件或kotlin文件
android·java·开发语言·kotlin
陆业聪5 小时前
AI 时代最被低估的工程师技能:把需求写清楚
android·人工智能·aigc
夏沫琅琊5 小时前
Android 的 Activity 启动模式
android
zh_xuan6 小时前
Android compose Navigation 页面导航
android·compose
luanma1509806 小时前
PHP vs C#:30字秒懂两大语言核心差异
android·开发语言·python·php·laravel
luanma1509807 小时前
Laravel 7.X核心特性深度解析
android·开发语言·php·lua·laravel
运维老曾7 小时前
Flink 1.20 使用自带jdbc source 操作步骤
android·adb·flink