Skip to content

内核加载流程

追踪 Android 17 init 从 selinux_setup、策略读取、snapuserd 过渡到 security_load_policy 和二次 exec 的真实启动链。

基于android-17.0.0_r1
AndroidSELinuxinitsecurity_load_policy启动流程源码阅读

内核加载流程 ​

本文承接 版本化策略 对 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

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

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

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

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

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

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

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

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

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

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

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

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

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

cpp
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

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 构建侧 ​

sh
# 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. 读码练习 ​

给定一次启动失败日志,按顺序回答:

  1. 没有 Opening SELinux policy 时,是 first stage mount、exec 还是日志重定向先失败?
  2. 有 Compiling SELinux policy 但没有 Loading SELinux policy 时,应该检查哪个 split CIL、版本文件或 secilc argv?
  3. security_load_policy 成功后出现 restorecon FATAL,为什么不能把它归类为 AVC?
  4. snapuserd transition 已开始但 secilc 失败时,临时文件和 transition 状态由哪些代码负责清理?

最后对照 SetupSelinux 复述一遍 kernel → first stage → selinux_setup → policy in memory → policy installed → enforcing → init label → second stage,并指出 Microdroid 在哪一步换用了不同的策略来源。

9. 源码导航 ​

  1. system/core/init/main.cpp:selinux_setup 与 second_stage 参数分派。
  2. system/core/init/first_stage_init.cpp:first-stage mount、日志重定向和 exec。
  3. system/core/init/selinux.cpp:ReadPolicy、split/monolithic 选择、snapuserd 过渡、enforcing 与 restorecon。
  4. system/core/init/selinux.h:SELinux setup 对外接口。
  5. system/sepolicy/build/soong/versioned_policy.go:运行时所消费的 mapping/versioned CIL 构建来源。
  6. system/sepolicy/Android.bp:各分区 CIL、预编译 binary 和 hash 产物。