GKI 与 SELinux
本文承接 内核加载流程、Treble 与 sepolicy 和 APEX 与 SELinux。本文解决 GKI 场景中的三个混淆:GKI kernel 是否携带设备策略、vendor_boot 是否是 sepolicy 的存放分区,以及 vendor kernel 新增 proc/sysfs 节点时为什么需要 genfs label 版本协商。
源码结论是:GKI 内核提供 SELinux LSM 实现、policydb parser、AVC、security server 和 selinuxfs;设备的 type/allow/neverallow/CIL 仍由 system/vendor 等用户空间分区提供。vendor_boot 中的 vendor ramdisk 参与 first-stage mount 和设备准备,但正常 split policy 路径仍从 /system/etc/selinux、/vendor/etc/selinux 等目录读取策略。GKI 的兼容边界不是“把 policy 烧进 kernel”,而是“通用内核能解析并执行用户空间提供的 policy,同时为 vendor kernel 对象选择兼容标签”。
1. 引擎与数据
1.1 内核组成
源码文件:kernel/common/security/selinux/Kconfig、kernel/common/security/selinux/Makefile
config SECURITY_SELINUX
bool "SELinux Support"
depends on SECURITY_NETWORK && AUDIT && NET && INET
select NETWORK_SECMARK
config SECURITY_SELINUX_DEVELOP
bool "SELinux Development Support"
depends on SECURITY_SELINUX
config SECURITY_SELINUX_AVC_STATS
bool "SELinux AVC Statistics"
depends on SECURITY_SELINUXKconfig 决定功能是否进入 GKI;Makefile 再列出 SELinux LSM 的具体对象文件,两者分别是配置 owner 和链接 owner。
obj-$(CONFIG_SECURITY_SELINUX) := selinux.o
selinux-y := avc.o hooks.o selinuxfs.o netlink.o nlmsgtab.o \
netif.o netnode.o netport.o status.o \
ss/ebitmap.o ss/hashtab.o ss/symtab.o ss/sidtab.o \
ss/avtab.o ss/policydb.o ss/services.o \
ss/conditional.o ss/mls.o ss/context.o这些对象实现访问 hook、AVC、policydb 反序列化、MLS、context/SID 和 selinuxfs。Kconfig 说明启用 SELinux 仍需要“policy configuration and a labeled filesystem”;内核代码本身没有 Android 的 system_server、HAL 或 app allow 规则。
1.2 用户空间策略
源码文件:system/core/init/selinux.cpp
void ReadPolicy(std::string* policy) {
PolicyFile policy_file;
bool ok = IsSplitPolicyDevice() ? OpenSplitPolicy(&policy_file)
: OpenMonolithicPolicy(&policy_file);
if (!ok) {
LOG(FATAL) << "Unable to open SELinux policy";
}
if (!android::base::ReadFdToString(policy_file.fd, policy)) {
PLOG(FATAL) << "Failed to read policy file: " << policy_file.path;
}
}
static void LoadSelinuxPolicy(std::string& policy) {
set_selinuxmnt("/sys/fs/selinux");
if (security_load_policy(policy.data(), policy.size()) < 0) {
PLOG(FATAL) << "SELinux: Could not load policy";
}
}上面的 OpenSplitPolicy 是聚焦职责的注释摘录,完整输入组合见 内核加载流程。init 在用户空间生成/读取一个 monolithic binary,再通过 libselinux 写入内核;GKI kernel 只消费 binary。
2. 三种版本
2.1 policydb格式
源码文件:kernel/common/security/selinux/include/security.h
#define POLICYDB_VERSION_XPERMS_IOCTL 30
#define POLICYDB_VERSION_INFINIBAND 31
#define POLICYDB_VERSION_GLBLUB 32
#define POLICYDB_VERSION_COMP_FTRANS 33
#define POLICYDB_VERSION_COND_XPERMS 34
#define POLICYDB_VERSION_NEVERAUDIT 35
#define POLICYDB_VERSION_MIN POLICYDB_VERSION_BASE
#define POLICYDB_VERSION_MAX POLICYDB_VERSION_NEVERAUDITpolicydb version 是 binary 序列化格式能力,不是 Android release 或 Treble sepolicy API。当前内核最大支持 35,其中版本 30 引入 ioctl extended permissions,35 引入 neveraudit type map。
2.2 Android编译版本
源码文件:system/sepolicy/build/soong/policy.go
const (
MlsSens = 1
MlsCats = 1024
PolicyVers = 30
)
secilcCmd := rule.Command().BuiltTool("secilc").
Flag("-m").
FlagWithArg("-M ", "true").
Flag("-G").
FlagWithArg("-c ", strconv.Itoa(PolicyVers))Android 17 构建固定让 secilc 输出 policy version 30,即使 GKI 内核 parser 支持到 35。这是 userspace 选择的兼容格式,不代表内核能力只有 30。修改 PolicyVers 会影响 binary 格式和所有加载端,不能与 PLATFORM_SEPOLICY_VERSION=202604 混为一谈。
2.3 版本坐标
| 名称 | 示例 | owner | 用途 |
|---|---|---|---|
| policydb version | 30 | secilc/kernel parser | binary 格式能力 |
| platform sepolicy version | 202604 | system/vendor Treble | public API mapping |
| genfs labels version | 202504 等 | vendor board | proc/sysfs 标签集合 |
三者解决不同问题。policydb version 不决定 public type API;platform version 不决定 kernel parser 格式;genfs version 只选择内核伪文件系统的兼容标签补丁。
3. 内核加载契约
3.1 版本检查
源码文件:kernel/common/security/selinux/ss/policydb.c
p->policyvers = le32_to_cpu(buf[0]);
if (p->policyvers < POLICYDB_VERSION_MIN ||
p->policyvers > POLICYDB_VERSION_MAX) {
pr_err("SELinux: policydb version %d does not match my version range %d-%d\n",
le32_to_cpu(buf[0]), POLICYDB_VERSION_MIN,
POLICYDB_VERSION_MAX);
goto bad;
}
if (le32_to_cpu(buf[1]) & POLICYDB_CONFIG_MLS) {
p->mls_enabled = 1;
if (p->policyvers < POLICYDB_VERSION_MLS) {
pr_err("SELinux: security policydb version %d (MLS) not backwards compatible\n",
p->policyvers);
goto bad;
}
}内核首先验证版本范围,再检查配置位与格式版本是否兼容。格式过新、过旧或 MLS 标志与旧格式冲突都会让 policydb 读取失败,最终表现为 init security_load_policy FATAL,而不是某条 AVC。
3.2 selinuxfs加载
源码文件:kernel/common/security/selinux/selinuxfs.c
static ssize_t sel_write_load(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
struct selinux_load_state load_state;
ssize_t length;
void *data = NULL;
if (*ppos || !count)
return -EINVAL;
mutex_lock(&selinux_state.policy_mutex);
length = avc_has_perm(current_sid(), SECINITSID_SECURITY,
SECCLASS_SECURITY, SECURITY__LOAD_POLICY, NULL);
if (length)
goto out;
data = vmalloc(count);
if (!data) {
length = -ENOMEM;
goto out;
}
if (copy_from_user(data, buf, count) != 0) {
length = -EFAULT;
goto out;
}
length = security_load_policy(data, count, &load_state);
if (length)
goto out;
selinux_policy_commit(&load_state);
length = count;
out:
mutex_unlock(&selinux_state.policy_mutex);
return length;
}selinuxfs 拒绝 partial/empty write,以 mutex 序列化 policy 更新,并要求 caller 有 security:load_policy。新 policy 先构建到 load_state,selinuxfs 节点初始化成功后才 commit;失败路径不会把半解析 policy 替换为当前状态。
3.3 policy能力
源码文件:kernel/common/security/selinux/include/policycap_names.h、kernel/common/security/selinux/ss/services.c
const char *const selinux_policycap_names[__POLICYDB_CAP_MAX] = {
"network_peer_controls",
"open_perms",
"extended_socket_class",
"always_check_network",
"cgroup_seclabel",
"nnp_nosuid_transition",
"genfs_seclabel_symlinks",
"ioctl_skip_cloexec",
"userspace_initial_context",
"netlink_xperm",
"netif_wildcard",
"genfs_seclabel_wildcard",
"functionfs_seclabel",
"memfd_class",
};名称表定义可观察的 capability;加载新 policy 时,security server 再从 policydb bitmap 把每一位复制到全局运行状态并打印值。
for (i = 0; i < ARRAY_SIZE(selinux_state.policycap); i++)
WRITE_ONCE(selinux_state.policycap[i],
ebitmap_get_bit(&p->policycaps, i));
for (i = 0; i < ARRAY_SIZE(selinux_policycap_names); i++)
pr_info("SELinux: policy capability %s=%d\n",
selinux_policycap_names[i],
ebitmap_get_bit(&p->policycaps, i));policy capability 是 binary policy 与 kernel hook 之间的功能开关,例如 open permission、genfs symlink label、memfd class。它不同于 Kconfig:内核代码可能支持某 capability,但只有 policydb 设置对应 bit 后 hook 才采用新语义。
4. vendor_boot边界
4.1 vendor ramdisk
源码文件:build/soong/etc/prebuilt_etc.go、build/soong/android/paths.go
// Make this module available when building for vendor ramdisk.
// On devices without a dedicated recovery partition, the module is only
// available after switching root into /first_stage_ramdisk.
Vendor_ramdisk_available *bool这个 property 决定模块是否产生 vendor ramdisk 变体;路径层再根据设备是否把 recovery 资源移入 vendor_boot,选择普通 vendor ramdisk 或 first_stage_ramdisk 子目录。
} else if ctx.InstallInVendorRamdisk() {
if ctx.DeviceConfig().BoardMoveRecoveryResourcesToVendorBoot() &&
!ctx.InstallPathSkipFirstStageRamdisk() {
partition = "vendor_ramdisk/first_stage_ramdisk"
} else {
partition = "vendor_ramdisk"
}
}vendor_boot 承载 vendor ramdisk 变体和 first-stage 可用资源。它影响 first-stage init 能看到哪些配置、fstab、模块/工具,但正常 split sepolicy 的 CIL/binary 安装路径仍是 system/vendor/odm 的 etc/selinux。不能因为 init 从 vendor ramdisk 启动,就推断 policy 存储在 vendor_boot。
4.2 first-stage交接
源码文件:system/core/init/first_stage_init.cpp
if (!fsm->DoFirstStageMount()) {
LOG(FATAL) << "Failed to mount required partitions early ...";
}
const char* path = "/system/bin/init";
const char* args[] = {path, "selinux_setup", nullptr};
execv(path, const_cast<char**>(args));
PLOG(FATAL) << "execv failed";first-stage owner 负责使用 ramdisk/fstab/设备节点挂载 /system、/vendor 等,之后才执行 system init 的 selinux_setup。策略加载依赖挂载结果,但不由 vendor_boot ramdisk 直接向内核注入规则。
4.3 OTA文件类型
源码文件:system/sepolicy/public/file.te、system/sepolicy/private/file_contexts
type vendor_boot_ota_file, vendor_file_type, file_type;
/vendor/boot_otas(/.*)? u:object_r:vendor_boot_ota_file:s0vendor_boot_ota_file 标注的是 /vendor/boot_otas 下 OTA 资源文件,不是 vendor_boot 分区整体的 mount label,也不是内核 sepolicy。update_engine/system_app 获得读取权限,说明 SELinux 控制的是用户空间 OTA 文件工作流。
5. genfs版本
5.1 vendor声明
源码文件:build/make/core/config.mk、system/sepolicy/Android.bp
BOARD_GENFS_LABELS_VERSION ?= $(BOARD_API_LEVEL)
ifeq ($(call math_gt,$(BOARD_API_LEVEL),$(BOARD_GENFS_LABELS_VERSION)),true)
$(error BOARD_GENFS_LABELS_VERSION must be greater than or equal to BOARD_API_LEVEL)
endifMake 层确定并校验 board 选择,Soong 层再把这个值物化为 vendor 分区中的版本文件。
genrule {
name: "genfs_labels_version.txt.gen",
out: ["genfs_labels_version.txt"],
cmd: select(soong_config_variable("ANDROID", "BOARD_GENFS_LABELS_VERSION"), {
any @ value: "echo " + value + " > $(out)",
default: "echo > $(out)",
}),
}
prebuilt_etc {
name: "genfs_labels_version.txt",
src: ":genfs_labels_version.txt.gen",
relative_install_path: "selinux",
vendor: true,
}vendor image 安装 /vendor/etc/selinux/genfs_labels_version.txt,声明它期望的 genfs 标签版本。新 board 构建不能选择低于 board API 的版本;旧设备没有文件时由读取库提供兼容默认值。
5.2 读取与默认
源码文件:system/sepolicy/compat/libgenfslabelsversion/src/lib.rs
const GENFS_LABELS_VERSION_TXT_PATH: &str =
"/vendor/etc/selinux/genfs_labels_version.txt";
const DEFAULT_GENFS_LABELS_VERSION: i32 = 202404;
#[no_mangle]
pub extern "C" fn get_genfs_labels_version() -> i32 {
match fs::read_to_string(GENFS_LABELS_VERSION_TXT_PATH) {
Ok(contents) => match contents.trim().parse::<i32>() {
Ok(version) => version,
Err(_) => DEFAULT_GENFS_LABELS_VERSION,
},
Err(_) => DEFAULT_GENFS_LABELS_VERSION,
}
}读取失败和解析失败都回退到 202404,而不是当前 platform version。这个 fallback 服务旧 vendor 镜像;调试时若文件内容错误,init 日志可能仍显示使用默认版本,不能只检查文件是否存在。
5.3 init选择
源码文件:system/core/init/selinux.cpp
int vendor_genfs_version = get_genfs_labels_version();
std::string genfs_cil_file = std::format(
"/system/etc/selinux/plat_sepolicy_genfs_{}.cil",
vendor_genfs_version);
if (access(genfs_cil_file.c_str(), F_OK) != 0) {
if (vendor_genfs_version >= 202504) {
LOG(ERROR) << "Missing " << genfs_cil_file;
return false;
}
LOG(INFO) << "Missing " << genfs_cil_file << "; skipping";
genfs_cil_file.clear();
} else {
LOG(INFO) << "Using " << genfs_cil_file << " for genfs labels";
}版本 202504 及以上缺少对应 CIL 是启动错误;更旧版本缺文件可以跳过。选中的 genfs CIL 被追加到 secilc 输入,最终成为单一 binary 的 genfs table。
5.4 兼容标签
源码文件:system/sepolicy/compat/plat_sepolicy_genfs_202504.cil、plat_sepolicy_genfs_202604.cil
(genfscon sysfs "/class/udc" (u object_r sysfs_udc ((s0) (s0))))
(genfscon sysfs "/power/mem_sleep" (u object_r sysfs_mem_sleep ((s0) (s0))))202504 层提供 UDC 和 suspend mode 标签;后续版本在此基础上追加新的内核对象标签,而不是替换整个 genfs 表。
(genfscon sysfs "/class/typec" (u object_r sysfs_typec ((s0) (s0))))
(genfscon proc "/sys/kernel/firmware_config"
(u object_r proc_firmware_config ((s0) (s0))))版本文件是增量兼容标签集合。vendor kernel/module 暴露新 sysfs/proc 路径时,platform policy 需要在适当 genfs version 引入 label;旧 vendor 选择旧版本时不会看到超出其契约的新标签变化。
6. GKI APEX边界
6.1 安装钩子
源码文件:system/sepolicy/apex/com.android.gki-file_contexts、system/sepolicy/private/gki_apex_prepostinstall.te
(/.*)? u:object_r:system_file:s0
/bin/(.*)? u:object_r:gki_apex_prepostinstall_exec:s0payload 标签只选择 exec type;真正的进程 domain、Binder 能力和工具权限必须提前写入 platform policy。
type gki_apex_prepostinstall, domain, coredomain;
type gki_apex_prepostinstall_exec, system_file_type, exec_type, file_type;
allow gki_apex_prepostinstall shell_exec:file rx_file_perms;
allow gki_apex_prepostinstall toolbox_exec:file rx_file_perms;
binder_use(gki_apex_prepostinstall)
allow gki_apex_prepostinstall
update_engine_stable_service:service_manager find;
binder_call(gki_apex_prepostinstall, update_engine)
allow gki_apex_prepostinstall apexd:fd use;GKI APEX 安装脚本运行在预定义 domain,通过 update_engine 执行更新;它不是把内核 policy 或新 domain 动态注入设备。exec type、Binder 和工具权限都必须在 system policy 中预先存在。
6.2 分区边界
GKI APEX 是用户空间更新容器;vendor_boot 是启动分区/ramdisk;GKI kernel 是通用内核 binary;SELinux binary policy 又来自 system/vendor CIL 合并。四者的签名、更新和生命周期不同,不能用“GKI 策略”一词将它们合并成一个产物。
7. 排查方法
7.1 版本与能力
# Read-only: inspect kernel policy format support and loaded policy capability nodes.
adb shell 'cat /sys/fs/selinux/policyvers'
adb shell 'find /sys/fs/selinux/policy_capabilities -maxdepth 1 -type f -print -exec cat {} \;'
# Read-only: inspect Android and vendor compatibility selectors.
adb shell 'cat /vendor/etc/selinux/plat_sepolicy_vers.txt'
adb shell 'cat /vendor/etc/selinux/genfs_labels_version.txt 2>/dev/null || true'policyvers 显示内核最大 parser version,不等于当前 binary 一定使用该版本;Android 构建常量需从 Soong/构建日志确认。policy capability 节点显示已加载 policy 启用的功能,而不是 Kconfig 是否编译代码。
7.2 启动日志
# Read-only: inspect policy and genfs selection during early boot.
adb shell 'dmesg | grep -E "policydb version|policy capability|genfs labels|SELinux|sepolicy"'
# Read-only: verify a kernel-generated object label.
adb shell 'ls -Zd /sys/class/typec /proc/sys/kernel/firmware_config 2>/dev/null'policydb version mismatch 会在内核 parser 阶段失败;genfs CIL 缺失会在 init 组装阶段失败;某个 sysfs 节点标签不符合预期则应检查 vendor genfs version 和对应 compatibility CIL。三者是不同 owner。
7.3 新增内核对象
vendor kernel module 新增 sysfs/proc 路径时,先确定旧 vendor/system 组合是否需要保持 generic label,再选择引入 genfs version 的时机。若直接修改当前 genfs_contexts 而不维护版本 CIL,system OTA 可能改变旧 vendor 观察到的标签,导致已有 allow 失效。
8. 读码练习
- 对比
PolicyVers=30与/sys/fs/selinux/policyvers,说明两者为何可以不同; - 从
sel_write_load复述 binary policy 的权限检查、解析、节点初始化和 commit 顺序; - 找到一个 policy capability,说明 Kconfig 支持、binary bit 和 hook 分支三者的关系;
- 追踪 vendor genfs version 文件从 BoardConfig/Soong 到 Rust reader,再到 init CIL 路径;
- 假设 vendor module 新增
/sys/class/foo,设计一个不破坏旧 vendor system-only OTA 的标签演进方案; - 解释 vendor_boot、GKI APEX、GKI kernel 和 sepolicy binary 分别由谁加载、何时生效。
9. 源码导航
kernel/common/security/selinux/Kconfig、Makefile:GKI 内核中的 SELinux 引擎组成。kernel/common/security/selinux/include/security.h:policydb 格式版本范围。kernel/common/security/selinux/ss/policydb.c:binary 版本和 MLS 配置校验。kernel/common/security/selinux/selinuxfs.c:load policy 事务和 commit。kernel/common/security/selinux/include/policycap_names.h、ss/services.c:policy capability。system/core/init/first_stage_init.cpp、selinux.cpp:first-stage mount、策略读取和 genfs CIL 选择。build/soong/etc/prebuilt_etc.go、build/soong/android/paths.go:vendor ramdisk 安装变体。system/sepolicy/compat/libgenfslabelsversion/:vendor genfs version 读取和默认值。system/sepolicy/compat/plat_sepolicy_genfs_*.cil:版本化 genfs 标签。system/sepolicy/apex/com.android.gki-file_contexts、private/gki_apex_prepostinstall.te:GKI APEX 更新钩子的预定义策略。
