内核加载流程
本文承接 版本化策略 对 platform/vendor mapping 的说明,聚焦策略已经生成之后如何进入运行中的内核。本文的入口是 /init selinux_setup,终点是 security_load_policy 成功后重新执行 /system/bin/init second_stage;不重复 policy.conf、CIL 和 mapping 的构建细节。读完后,读者应能解释 first stage 为什么先挂载并准备分区、init 如何在 split/monolithic 两种设备间选择文件、为什么 snapuserd 必须在加载前后切换,以及每个失败点由谁终止启动。
1. 启动边界
1.1 三次执行
源码文件:system/core/init/main.cpp
int main(int argc, char** argv) {
if (argc > 1) {
if (!strcmp(argv[1], "selinux_setup")) {
return SetupSelinux(argv);
}
if (!strcmp(argv[1], "second_stage")) {
return SecondStageMain(argc, argv);
}
}
#if defined(FIRST_STAGE_INIT) || defined(RECOVERY)
return FirstStageMain(argc, argv);
#else
LOG(FATAL) << "Second-stage init requires an argument to main()";
#endif
}第一次执行进入 FirstStageMain,负责早期挂载和切换 root;它随后 execv 自身并携带 selinux_setup。第二次执行进入 SetupSelinux,加载策略、设置 enforcing 并恢复 /system/bin/init 的标签;最后再以 second_stage 参数执行同一个 ELF。exec 不会复制 C++ 对象状态,跨阶段状态只能通过文件、环境变量或内核状态传递。
1.2 主线图
策略加载成功并不立即意味着 init 已经拥有 init domain。SetupSelinux 仍要对 /system/bin/init 执行 restorecon,让下一次 exec 能依据 file label 完成 domain transition;如果 restorecon 失败,策略虽然已在内核中,却不能安全进入 second stage。
2. first stage
2.1 分区与设备
FirstStageMain 会创建早期需要的设备和挂载点,并执行 first-stage mount。Android 17 的实现还处理动态分区、recovery、AVB 和 ramdisk 回收;这些动作决定 /system、/vendor 和 /system_ext 是否可被后续策略读取。
源码文件:system/core/init/first_stage_init.cpp
if (IsRecoveryMode()) {
LOG(INFO) << "First stage mount skipped (recovery mode)";
} else {
if (!fsm) {
fsm = CreateFirstStageMount(cmdline);
}
if (!fsm) {
LOG(FATAL) << "FirstStageMount not available";
}
if (!fsm->DoFirstStageMount()) {
LOG(FATAL) << "Failed to mount required partitions early ...";
}
}
const char* path = "/system/bin/init";
const char* args[] = {path, "selinux_setup", nullptr};
auto fd = open("/dev/kmsg", O_WRONLY | O_CLOEXEC);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
close(fd);
execv(path, const_cast<char**>(args));
PLOG(FATAL) << "execv(\"" << path << "\") failed";first stage 的 owner 是 init 进程本身;它不加载策略,只准备策略文件能够被访问的 mount namespace,并把日志导向 /dev/kmsg。mount 失败是 LOG(FATAL),不会降级到“无策略启动”。恢复模式跳过普通 first-stage mount,是因为 recovery 的文件布局和策略产物不同。
2.2 设置入口
源码文件:system/core/init/selinux.cpp
int SetupSelinux(char** argv) {
SetStdioToDevNull(argv);
InitKernelLogging(argv);
boot_clock::time_point start_time = boot_clock::now();
SelinuxSetupKernelLogging();
bool use_overlays = EarlySetupOverlays();
if (IsMicrodroid()) {
LoadSelinuxPolicyMicrodroid();
} else {
LoadSelinuxPolicyAndroid();
}
SelinuxSetEnforcement();
if (selinux_android_restorecon("/system/bin/init", 0) == -1) {
PLOG(FATAL) << "restorecon failed of /system/bin/init failed";
}
setenv(kEnvSelinuxStartedAt,
std::to_string(start_time.time_since_epoch().count()).c_str(), 1);
if (use_overlays) SetupOverlays();
const char* path = "/system/bin/init";
const char* args[] = {path, "second_stage", nullptr};
execv(path, const_cast<char**>(args));
PLOG(FATAL) << "execv(\"" << path << "\") failed";
}这里的顺序很重要:日志回调先安装,策略加载随后发生,enforcing 切换在策略进入内核之后,restorecon 又在二次 exec 之前。kEnvSelinuxStartedAt 只记录时间,不承担策略内容传递;策略内容通过 ReadPolicy 的字符串和内核的 security server 传递。
3. 策略选择
3.1 策略选择
源码文件: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;
}
}IsSplitPolicyDevice 通过 /system/etc/selinux/plat_sepolicy.cil 是否可读来判断 split policy;否则读取 /sepolicy 单文件。PolicyFile 同时保存文件描述符和路径,后者只用于错误日志。打开成功和读入成功是两个状态,任一失败都会让启动停止。
3.2 split输入
OpenSplitPolicy 会读取 vendor 版本、检查可选分区文件,并把 platform、mapping、system_ext、product、vendor、odm 和 genfs CIL 按顺序交给 secilc。具体版本映射由 版本化策略 解释;本节关注 init 作为消费者如何组装 argv。
源码文件:system/core/init/selinux.cpp
const std::string version_as_string = std::to_string(SEPOLICY_VERSION);
// /dev/null is not yet available this early; selinuxfs provides a null target.
std::vector<const char*> compile_args{
"/system/bin/secilc",
use_userdebug_policy ? *userdebug_plat_sepolicy : plat_policy_cil_file,
"-m", "-M", "true", "-G", "-N", "-v",
"-c", version_as_string.c_str(),
plat_mapping_file.c_str(),
"-o", compiled_sepolicy,
"-f", "/sys/fs/selinux/null",
};
if (!vendor_policy_cil_file.empty()) {
compile_args.push_back(vendor_policy_cil_file.c_str());
}compiled_sepolicy 是 /dev/sepolicy.XXXXXX 临时文件。-f /sys/fs/selinux/null 不是策略丢失,而是因为启动早期普通 /dev/null 尚未可用;它告诉 secilc 不生成 file_contexts 输出。可选文件只有在 access(..., F_OK) 成功时才加入 argv,缺少 optional system_ext/product/odm 不等同于缺少必需 vendor policy。
3.3 预编译回退
源码文件:system/core/init/selinux.cpp
if (!use_userdebug_policy) {
// A matching vendor/odm precompiled policy avoids runtime secilc work.
if (auto res = FindPrecompiledSplitPolicy(); res.ok()) {
unique_fd fd(open(res->c_str(), O_RDONLY | O_CLOEXEC | O_BINARY));
if (fd != -1) {
policy_file->fd = std::move(fd);
policy_file->path = std::move(*res);
return true;
}
}
}
LOG(INFO) << "Compiling SELinux policy";预编译命中要求 hash 与 system/system_ext/product 的策略和 mapping 匹配;不匹配只触发 fallback,不是立即失败。userdebug policy 明确跳过 vendor 预编译,因为它需要不同的 platform CIL。fallback 创建临时文件并在 secilc 非零时 unlink,避免将不完整二进制交给后续读取。
4. 加载内核
4.1 snapuserd过渡
源码文件:system/core/init/selinux.cpp
void LoadSelinuxPolicyAndroid() {
MountMissingSystemPartitions();
LOG(INFO) << "Opening SELinux policy";
// Read while snapuserd is still available for dynamic partitions.
std::string policy;
ReadPolicy(&policy);
auto snapuserd_helper = SnapuserdSelinuxHelper::CreateIfNeeded();
if (snapuserd_helper) {
// After StartTransition(), /system must not be read until FinishTransition().
snapuserd_helper->StartTransition();
}
LoadSelinuxPolicy(policy);
if (snapuserd_helper) {
// Complete the transition before enforcing mode is enabled.
snapuserd_helper->FinishTransition();
snapuserd_helper = nullptr;
}
}这段代码解释了“先读策略、后切换 snapuserd”的原因:策略可能位于动态分区,读取需要 snapuserd;但加载策略后继续让 kernel-privileged snapuserd 运行会产生 audit。因此状态顺序是 policy string ready → transition started → policy loaded → transition finished。任一步异常都不能跳过 FinishTransition 后继续 enforcing。
4.2 内核接口
源码文件:system/core/init/selinux.cpp
static void LoadSelinuxPolicy(std::string& policy) {
LOG(INFO) << "Loading SELinux policy";
set_selinuxmnt("/sys/fs/selinux");
if (security_load_policy(policy.data(), policy.size()) < 0) {
PLOG(FATAL) << "SELinux: Could not load policy";
}
}security_load_policy 是 libselinux 到内核 SELinux 接口的消费者。它接收已经读入内存的 monolithic binary;init 不把 CIL 直接写给内核。调用失败是致命错误,和运行期某个 domain 的 AVC 不同:前者说明 policy 本身无法安装,后者说明已安装 policy 拒绝了具体访问。
4.3 enforcing与标签
源码文件:system/core/init/selinux.cpp
void SelinuxSetEnforcement() {
bool kernel_enforcing = (security_getenforce() == 1);
bool is_enforcing = IsEnforcing();
if (kernel_enforcing != is_enforcing) {
if (security_setenforce(is_enforcing)) {
PLOG(FATAL) << "security_setenforce("
<< (is_enforcing ? "true" : "false") << ") failed";
}
}
}策略加载和 enforcing 设置是两个内核状态。IsEnforcing() 的产品决策可能要求 permissive,但只有在 policy 已安装后才有意义;security_setenforce 失败同样终止启动。随后 restorecon /system/bin/init 让文件标签和新策略一致,二次 exec 才能从 kernel domain 转入 init domain。
5. 条件路径
5.1 Microdroid
源码文件:system/core/init/selinux.cpp
void LoadSelinuxPolicyMicrodroid() {
constexpr const char kMicrodroidPrecompiledSepolicy[] =
"/system/etc/selinux/microdroid_precompiled_sepolicy";
unique_fd policy_fd(open(kMicrodroidPrecompiledSepolicy,
O_RDONLY | O_CLOEXEC | O_NOFOLLOW));
if (policy_fd < 0) {
PLOG(FATAL) << "Failed to open " << kMicrodroidPrecompiledSepolicy;
}
std::string policy;
if (!android::base::ReadFdToString(policy_fd, &policy)) {
PLOG(FATAL) << "Failed to read policy file: "
<< kMicrodroidPrecompiledSepolicy;
}
LoadSelinuxPolicy(policy);
}Microdroid 不走 Android split policy 的 vendor/mapping 选择,而是读取 system 内的预编译单文件;SetupSelinux 仍复用 enforcing、restorecon 和 second-stage exec。不能把 Microdroid 的固定路径外推到普通 Treble 设备。
5.2 日志回调
源码文件:system/core/init/selinux.cpp
void SelinuxSetupKernelLogging() {
selinux_callback cb;
cb.func_log = SelinuxKlogCallback;
selinux_set_callback(SELINUX_CB_LOG, cb);
}回调安装在策略加载前,使 libselinux 的 warning、info 和 AVC 能被 init 转发到 Android kernel logger;它不改变 policy,也不负责加载。排查早期错误时,如果只看 logcat 而没有读取 /dev/kmsg/kernel log,可能错过 OpenSplitPolicy 和 security_load_policy 的第一条错误。
5.3 模式判定
源码文件:system/core/init/selinux.cpp
EnforcingStatus StatusFromProperty() {
std::string value;
if (android::fs_mgr::GetKernelCmdline("androidboot.selinux", &value) &&
value == "permissive") {
return SELINUX_PERMISSIVE;
}
if (android::fs_mgr::GetBootconfig("androidboot.selinux", &value) &&
value == "permissive") {
return SELINUX_PERMISSIVE;
}
return SELINUX_ENFORCING;
}
bool IsEnforcing() {
if (ALLOW_PERMISSIVE_SELINUX) {
return StatusFromProperty() == SELINUX_ENFORCING;
}
return true;
}androidboot.selinux=permissive 只有在编译期开关 ALLOW_PERMISSIVE_SELINUX 允许时才有效;否则 IsEnforcing() 无条件返回 true。命令行属性改变的是“期望模式”,并不绕过策略加载。
5.4 overlay分支
源码文件:system/core/init/selinux.cpp
#ifdef ALLOW_REMOUNT_OVERLAYS
bool EarlySetupOverlays() {
bool has_overlays = false;
std::string contents;
if (!android::base::ReadFileToString("/proc/mounts", &contents, true)) {
PLOG(ERROR) << "Failed to read /proc/mounts";
return false;
}
for (const auto& line : android::base::Split(contents, "\n")) {
if (android::base::StartsWith(line, "overlay")) {
has_overlays = true;
break;
}
}
if (!has_overlays) return false;
if (mount("tmpfs", kSecondStageRes, "tmpfs",
MS_REMOUNT | MS_NOSUID | MS_NODEV,
"mode=0755,uid=0,gid=0") == -1) {
PLOG(FATAL) << "Failed to remount tmpfs on " << kSecondStageRes;
}
return true;
}
#else
bool EarlySetupOverlays() { return false; }
#endif只有定义了 ALLOW_REMOUNT_OVERLAYS 才会扫描 /proc/mounts;检测到 overlay 后,SetupOverlays 会复制并 fexecve overlay_remounter。这条分支只影响 debuggable 的 adb remount 场景,不是普通设备加载策略的必经步骤。
5.5 状态时序
6. 加载后辅助
6.1 restorecon集合
源码文件:system/core/init/selinux.cpp、system/core/init/selinux.h
void SelinuxRestoreContext() {
LOG(INFO) << "Running restorecon...";
selinux_android_restorecon("/dev", 0);
selinux_android_restorecon("/dev/console", 0);
selinux_android_restorecon("/dev/kmsg", 0);
selinux_android_restorecon("/dev/null", 0);
selinux_android_restorecon("/dev/ptmx", 0);
selinux_android_restorecon("/dev/socket", 0);
selinux_android_restorecon("/dev/random", 0);
selinux_android_restorecon("/dev/urandom", 0);
selinux_android_restorecon("/dev/__properties__", 0);
selinux_android_restorecon("/dev/block", SELINUX_ANDROID_RESTORECON_RECURSE);
selinux_android_restorecon("/apex", 0);
selinux_android_restorecon("/bootstrap-apex", 0);
selinux_android_restorecon("/linkerconfig", 0);
}实际函数还会处理 dm-user、device-mapper、loop-control、snapshot rollback indicator 和 /metadata/gsi 等条件路径;这里保留公共路径以突出职责。selinux.h 的注释说明它必须在 ueventd 填充 /dev 之前运行,所以这是加载后的 first-stage 收尾,不是 security_load_policy 的替代实现。
6.2 vendor版本查询
源码文件:system/core/init/selinux.cpp
int SelinuxGetVendorAndroidVersion() {
if (IsMicrodroid()) {
// Microdroid has no vendor code.
return __ANDROID_API_FUTURE__;
}
static int vendor_android_version = [] {
if (!IsSplitPolicyDevice()) {
return __ANDROID_API_FUTURE__;
}
std::string version;
if (!GetVendorMappingVersion(&version)) {
LOG(FATAL) << "Could not read vendor SELinux version";
}
int major_version;
std::string major_version_str(version, 0, version.find('.'));
if (!ParseInt(major_version_str, &major_version)) {
PLOG(FATAL) << "Failed to parse the vendor sepolicy major version "
<< major_version_str;
}
return major_version;
}();
return vendor_android_version;
}该查询只返回 vendor sepolicy 版本的 major 部分,供其他 init 逻辑判断兼容行为;它不重新编译策略。split policy 缺少版本文件或版本无法解析会 FATAL,monolithic 和 Microdroid 则返回未来 API sentinel。
7. 调试路径
7.1 构建侧
# Read-only: identify the policy flavor and its generated binary inputs.
adb shell 'test -r /system/etc/selinux/plat_sepolicy.cil && echo split || echo monolithic'
adb shell 'ls -l /system/etc/selinux /vendor/etc/selinux'
# Read-only: inspect the vendor-selected mapping version and policy load logs.
adb shell 'cat /vendor/etc/selinux/plat_sepolicy_vers.txt'
adb shell 'dmesg | grep -E "SELinux|secilc|sepolicy|snapuserd|avc: denied"'如果是 split 设备,先确认 plat_sepolicy.cil、mapping 和 vendor CIL 是否同在预期分区;如果只有 /sepolicy,应转向 monolithic 路径。日志中的 Compiling SELinux policy 表示预编译未命中,不是错误;随后出现 Could not load policy 才是内核安装失败。
7.2 证据边界
可以用三类证据逐层缩小范围:ReadPolicy 能证明文件可读,security_load_policy 成功能证明内核接受二进制,之后的 AVC 才能用于分析具体 domain/object 权限。任何一层失败都不能用下一层的命令替代;例如没有成功加载 policy 时,sesearch 查到的旧文件不能证明当前内核状态。
8. 读码练习
给定一次启动失败日志,按顺序回答:
- 没有
Opening SELinux policy时,是 first stage mount、exec 还是日志重定向先失败? - 有
Compiling SELinux policy但没有Loading SELinux policy时,应该检查哪个 split CIL、版本文件或 secilc argv? security_load_policy成功后出现 restorecon FATAL,为什么不能把它归类为 AVC?- snapuserd transition 已开始但 secilc 失败时,临时文件和 transition 状态由哪些代码负责清理?
最后对照 SetupSelinux 复述一遍 kernel → first stage → selinux_setup → policy in memory → policy installed → enforcing → init label → second stage,并指出 Microdroid 在哪一步换用了不同的策略来源。
9. 源码导航
system/core/init/main.cpp:selinux_setup与second_stage参数分派。system/core/init/first_stage_init.cpp:first-stage mount、日志重定向和 exec。system/core/init/selinux.cpp:ReadPolicy、split/monolithic 选择、snapuserd 过渡、enforcing 与 restorecon。system/core/init/selinux.h:SELinux setup 对外接口。system/sepolicy/build/soong/versioned_policy.go:运行时所消费的 mapping/versioned CIL 构建来源。system/sepolicy/Android.bp:各分区 CIL、预编译 binary 和 hash 产物。
