21、SELinux策略版本管理:策略版本号管理,策略兼容性处理,策略回滚机制

说到SELinux策略的版本管理,很多人第一反应就是「不就是改个版本号嘛」。嗯,我刚开始也是这么想的。直到有一次,我在一个车载项目上,因为策略版本号没管理好,导致整台设备OTA升级后直接变砖——所有应用都打不开,连adb shell都进不去。那场面,真是记忆犹新。

说白了,SELinux策略的版本管理,不是简单的数字递增。它涉及到三个核心问题:怎么给策略打版本号新旧策略怎么兼容出问题了怎么回滚。今天我就把这三点掰开揉碎了讲清楚。

策略版本号管理

Android系统里,SELinux策略文件通常放在 system/sepolicy 目录下。编译后会生成一个 sepolicy 二进制文件,这个文件里就嵌着版本号。

我个人习惯的做法是,在 system/sepolicy/Android.mkAndroid.bp 里定义一个 PLATFORM_SEPOLICY_VERSION。这个版本号怎么定?我建议用 主版本号.次版本号.修订号 的格式:

  • 主版本号:Android大版本升级时递增,比如从Android 12到13
  • 次版本号:新增重要策略规则时递增
  • 修订号:修复bug或微调权限时递增

核心要点:版本号必须与系统编译产物绑定。我见过有人手动改版本号,结果编译出来的策略和版本号对不上,排查问题的时候简直要疯。

举个例子,在 system/sepolicy/Android.bp 里可以这样定义:

se_policy_version {
    name: "plat_sepolicy_version",
    version: "12.3.1",
}

编译时,这个版本号会被写入 sepolicy 文件的头部。运行时,init 进程会读取这个版本号,用来判断策略是否匹配。

策略兼容性处理

这里有个坑,我踩过。你想想看,系统升级时,新策略可能删掉了一些旧的type,或者改了某些域的权限。如果旧的应用还在用旧的上下文,那直接就崩了。

兼容性处理的核心思路是:向前兼容,向后有限兼容

  • 向前兼容:新策略能处理旧系统留下的文件上下文。比如旧系统里有个 u:r:old_app:s0,新策略里虽然删了这个type,但得有个映射规则,把它映射到 u:r:default_app:s0
  • 向后有限兼容:新系统上跑旧策略?原则上不允许。但调试阶段可以放宽。

具体怎么做?我一般用 策略版本号检查条件编译。在 system/sepolicy 里,可以写这样的条件判断:

# 如果策略版本号大于等于 12.3.0,才允许这个规则
if (plat_sepolicy_version >= "12.3.0") {
    allow appdomain app_data_file:file read;
}

这样,旧策略编译时不会包含新规则,新策略编译时才会加上。避免了策略膨胀和冲突。

小技巧:我建议在 system/sepolicy/prebuilts/ 目录下保留最近3个版本的策略文件。这样万一需要回退,可以直接用预编译的版本,不用重新编译整个系统。

策略回滚机制

回滚,说白了就是「后悔药」。但SELinux策略的回滚,比你想的要复杂。因为策略一旦加载到内核,就生效了。如果新策略有问题,系统可能连回滚操作都执行不了——因为回滚脚本本身也需要权限。

我曾经在一个项目上遇到过:新策略把 init 进程的权限收得太紧,导致 init 无法执行 restorecon 命令。结果就是,策略加载后,系统直接卡死在开机动画。那叫一个尴尬。

所以,回滚机制必须设计在 策略加载之前系统启动早期。我推荐的做法是:

  1. 双分区策略:在 boot 分区和 system 分区各放一份策略。如果 system 分区的策略加载失败,自动回退到 boot 分区的策略。
  2. 策略签名验证:每次加载策略前,先验证签名。如果签名不匹配或版本号不对,就不加载,用上一次成功的策略。
  3. 回滚计数器:在 misc 分区记录策略加载次数。如果连续3次启动都回滚了,就强制进入恢复模式。

警告:千万不要在策略加载后才做回滚判断。那时候已经晚了。回滚逻辑必须写在 init 进程的早期阶段,最好在 selinux_init 函数里。

来看一个实际的回滚流程:

// 伪代码,展示回滚逻辑
int load_sepolicy() {
    // 1. 尝试加载新策略
    if (selinux_load_policy("/system/etc/selinux/plat_sepolicy.cil") == 0) {
        // 加载成功,记录版本号
        write_rollback_counter(SUCCESS);
        return 0;
    }
    
    // 2. 加载失败,尝试回滚到上一个版本
    if (selinux_load_policy("/data/security/current/sepolicy") == 0) {
        write_rollback_counter(ROLLBACK);
        return 0;
    }
    
    // 3. 都失败了,用内核默认策略
    write_rollback_counter(CRITICAL_FAILURE);
    return -1;
}

这个流程里,/data/security/current/sepolicy 是上一次成功加载的策略备份。每次成功加载新策略后,都会把当前策略复制到这个路径。

知识体系总览

下面这张图,是我梳理的SELinux策略版本管理核心逻辑。你可以看到,版本号、兼容性、回滚三者是环环相扣的:

SELinux策略版本管理核心逻辑 版本号管理 主版本号.次版本号.修订号 与编译产物绑定 兼容性处理 向前兼容 + 向后有限兼容 条件编译 + 版本检查 回滚机制 双分区策略 签名验证 + 回滚计数器 依赖 触发 策略加载流程:版本号检查 → 兼容性判断 → 加载策略 → 记录回滚点 回滚触发条件:策略加载失败 | 签名验证失败 | 版本号不匹配 | 连续启动失败 最佳实践:保留最近3个版本策略 | 回滚逻辑写在init早期 | 双分区冗余部署

你看,这三个环节是串在一起的。版本号是基础,兼容性是桥梁,回滚是最后的安全网。少了任何一个,策略管理都会出问题。

个人经验:我建议你在开发阶段就建立策略版本管理的规范。别等到项目快交付了才想起来。那时候改起来,牵一发而动全身,成本高得吓人。

好了,关于SELinux策略版本管理,核心就是这些。记住:版本号要规范、兼容性要考虑周全、回滚机制要提前设计。这三板斧用好了,策略管理就不会出大乱子。


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