Skip to content

SELinux启动

解析 SetupSelinux 的策略选择、预编译校验、CIL 编译、内核加载、snapuserd 过渡、enforcing 和 second-stage re-exec。

基于android-17.0.0_r1
AndroidinitSELinuxsepolicysnapuserd

SELinux启动 ​

本文面向已经读过 FirstStageMount,理解 SELinux policy、domain、file context 和 enforcing 基本含义的读者。前文已经证明 /system、/vendor、/product 等策略来源可被读取,并由 PID 1 以 selinux_setup 参数重新执行 /system/bin/init;本文从 SetupSelinux() 开始,追踪策略如何被选择、读取、编译、加载和生效。

本文只讨论 Android 17 的启动阶段,不教授完整 SELinux 语法,也不逐条解释 platform/vendor allow 规则。核心问题是:PID 1 此时仍在 kernel domain,为什么必须先读取 policy、处理 snapuserd,再打开 enforcing;precompiled policy 在什么条件下可用;restorecon("/system/bin/init") 为什么必须发生在第二次 exec 之前。

读完后,你应能区分 policy 文件找不到、precompiled 哈希不匹配、secilc 失败、内核拒绝加载、enforcing 切换失败和 domain transition 失败,并能从日志判断系统走的是 precompiled、runtime compile、Microdroid 还是 overlay-remounter 路径。

1. 阶段边界 ​

SetupSelinux() 位于两个 exec 之间:

text
FirstStageMain
  exec /system/bin/init selinux_setup
SetupSelinux
  exec /system/bin/init second_stage
SecondStageMain

它没有 rc parser、property service 或 ServiceList。输入来自已挂载分区、bootconfig/cmdline、环境变量、selinuxfs 和 snapshot 状态;输出是内核中已加载的 policy、目标 enforcing 状态、带正确 label 的 /system/bin/init,以及交给二阶段的计时和 AVB 环境。

状态所有者消费者生效时机
policy bytesReadPolicysecurity_load_policyenforcing 前
avb_handle 状态first-stage mountdebug policy 选择读取 policy 前
snapuserd transitionSnapuserdSelinuxHelper动态分区 I/Opolicy 加载前后
enforcingSELinux kernel所有后续访问security_setenforce 后
init 文件 labelrestoreconexec domain transitionsecond-stage exec 时
SELINUX_STARTED_ATSetupSelinuxRecordStageBoottimes二阶段启动后

旧文常把“加载策略”和“进入 init domain”写成同一步。实际上 policy 先加载,enforcing 再切换,文件再 restorecon,最后 exec 才让进程按照 policy 中的 transition 进入 init domain。

2. 总入口 ​

Android 17 的真实入口如下:

源码文件:system/core/init/selinux.cpp

cpp
int SetupSelinux(char** argv) {
    SetStdioToDevNull(argv);
    InitKernelLogging(argv);

    if (REBOOT_BOOTLOADER_ON_PANIC && !AttemptingToBootNewSlot()) {
        InstallRebootSignalHandlers();
    }

    boot_clock::time_point start_time = boot_clock::now();
    SelinuxSetupKernelLogging();

    bool use_overlays = EarlySetupOverlays();

    if (IsMicrodroid()) {
        LoadSelinuxPolicyMicrodroid();
    } else {
        LoadSelinuxPolicyAndroid();
    }

    SelinuxSetEnforcement();
    // restorecon 与 exec 位于后续代码。
}

顺序不能交换。日志 callback 必须先安装,否则 libselinux 的编译和加载错误无法进入 kernel log;overlay tmpfs 必须在 enforcing 前去掉 NO_EXEC;policy 必须先加载,security_setenforce() 才有目标规则可执行。

REBOOT_BOOTLOADER_ON_PANIC 仍避开正在尝试的新 slot,延续首阶段的 OTA 回退边界。它只改变 fatal 后的重启目标,不改变 policy 判断。

3. Policy类型 ​

3.1 Split判断 ​

Android 使用 platform CIL 文件是否可读判断 split policy:

源码文件:system/core/init/selinux.cpp

相关函数/类型:IsSplitPolicyDevice

cpp
constexpr const char plat_policy_cil_file[] =
        "/system/etc/selinux/plat_sepolicy.cil";

bool IsSplitPolicyDevice() {
    return access(plat_policy_cil_file, R_OK) != -1;
}

ReadPolicy() 根据这个结果选择 split 或 monolithic:

源码文件:system/core/init/selinux.cpp

相关函数/类型:ReadPolicy

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;
    }
}

这里的 policy owner 是 PolicyFile:它同时保存 fd 和用于错误日志的路径。无论来自预编译文件、临时编译文件还是 /sepolicy,最终都会读入 std::string,使后续 snapuserd 过渡期间不再依赖 /system I/O。

3.2 Monolithic路径 ​

旧式设备只打开 ramdisk 根的 /sepolicy,并禁止跟随符号链接:

源码文件:system/core/init/selinux.cpp

cpp
bool OpenMonolithicPolicy(PolicyFile* policy_file) {
    static constexpr char kSepolicyFile[] = "/sepolicy";

    LOG(INFO) << "Opening SELinux policy from monolithic file "
              << kSepolicyFile;
    policy_file->fd.reset(
            open(kSepolicyFile, O_RDONLY | O_CLOEXEC | O_NOFOLLOW));
    if (policy_file->fd < 0) {
        PLOG(ERROR) << "Failed to open monolithic SELinux policy";
        return false;
    }
    policy_file->path = kSepolicyFile;
    return true;
}

这个 fallback 不能用于解释现代 split device 的 vendor 兼容问题。只要 plat_sepolicy.cil 可读,代码不会因为 split compile 失败而退回 /sepolicy。

3.3 Microdroid路径 ​

Microdroid 不走 Android split policy 合并,直接读取固定预编译文件:

源码文件:system/core/init/selinux.cpp

相关函数/类型:LoadSelinuxPolicyMicrodroid

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);
}

因此 Android device 与 Microdroid 的共同边界只在 LoadSelinuxPolicy() 之后;不能拿 Android 的 vendor mapping 文件去解释 Microdroid 启动。

4. 预编译策略 ​

4.1 候选位置 ​

split policy 优先使用 odm 的 precompiled_sepolicy,否则使用 vendor:

源码文件:system/core/init/selinux.cpp

cpp
static constexpr const char vendor_precompiled_sepolicy[] =
        "/vendor/etc/selinux/precompiled_sepolicy";
static constexpr const char odm_precompiled_sepolicy[] =
        "/odm/etc/selinux/precompiled_sepolicy";

if (access(odm_precompiled_sepolicy, R_OK) == 0) {
    precompiled_sepolicy = odm_precompiled_sepolicy;
} else if (access(vendor_precompiled_sepolicy, R_OK) == 0) {
    precompiled_sepolicy = vendor_precompiled_sepolicy;
} else {
    return ErrnoError() << "No precompiled sepolicy at "
                        << vendor_precompiled_sepolicy;
}

预编译文件存在只是第一个条件。它还必须与当前 system、system_ext、product 的 policy 和 mapping 身份一致。

4.2 哈希配对 ​

FindPrecompiledSplitPolicy() 比较平台实际 hash 与预编译文件旁的 hash:

源码文件:system/core/init/selinux.cpp

相关函数/类型:FindPrecompiledSplitPolicy

cpp
std::vector<std::pair<std::string, std::string>> sepolicy_hashes{
        {"/system/etc/selinux/plat_sepolicy_and_mapping.sha256",
         precompiled_sepolicy + ".plat_sepolicy_and_mapping.sha256"},
        {"/system_ext/etc/selinux/system_ext_sepolicy_and_mapping.sha256",
         precompiled_sepolicy + ".system_ext_sepolicy_and_mapping.sha256"},
        {"/product/etc/selinux/product_sepolicy_and_mapping.sha256",
         precompiled_sepolicy + ".product_sepolicy_and_mapping.sha256"},
};

比较规则不是“任意缺失就失败”。若实际 hash 不存在,旁边的预编译 hash 也必须不存在;若预编译侧单独存在则返回错误。双方存在时读取首行,实际值为空或不相等都会拒绝该预编译 policy。

源码文件:system/core/init/selinux.cpp

相关函数/类型:FindPrecompiledSplitPolicy

cpp
if (actual_id.empty() || actual_id != precompiled_id) {
    return Error() << actual_id_path << " and "
                   << precompiled_id_path << " differ";
}

这个失败会让 OpenSplitPolicy() 记录原因并进入 runtime compile,而不是立即 fatal。因而看到 hash mismatch 日志时,应继续找 Compiling SELinux policy 和 secilc 结果。

4.3 Debug策略 ​

INIT_FORCE_DEBUGGABLE=true 还不够。只有设备 unlocked,且候选 userdebug policy 文件存在,才返回替代 platform policy:

源码文件:system/core/init/selinux.cpp

cpp
std::optional<const char*> GetUserdebugPlatformPolicyFile() {
    const char* force_debuggable_env = getenv("INIT_FORCE_DEBUGGABLE");
    if (force_debuggable_env && "true"s == force_debuggable_env &&
        AvbHandle::IsDeviceUnlocked()) {
        const std::vector<const char*> debug_policy_candidates = {
#if INSTALL_DEBUG_POLICY_TO_SYSTEM_EXT == 1
                "/system_ext/etc/selinux/userdebug_plat_sepolicy.cil",
#endif
                kDebugRamdiskSEPolicy,
        };
        for (const char* debug_policy : debug_policy_candidates) {
            if (access(debug_policy, F_OK) == 0) return debug_policy;
        }
    }
    return std::nullopt;
}

使用 userdebug policy 时必须重新编译,不能复用 vendor/odm 的 precompiled policy,因为其 hash 对应普通 platform policy。这也说明 IN002 中的 force_debuggable 只是一次性输入;真正决定 debug policy 是否参与加载的是设备解锁状态和候选文件是否存在。

5. 运行时编译 ​

5.1 版本输入 ​

没有可用预编译文件时,OpenSplitPolicy() 从 vendor 读取 platform mapping 版本:

源码文件:system/core/init/selinux.cpp

cpp
bool GetVendorMappingVersion(std::string* plat_vers) {
    if (!ReadFirstLine("/vendor/etc/selinux/plat_sepolicy_vers.txt",
                       plat_vers)) {
        PLOG(ERROR) << "Failed to read "
                    << "/vendor/etc/selinux/plat_sepolicy_vers.txt";
        return false;
    }
    if (plat_vers->empty()) {
        LOG(ERROR) << "No version present in plat_sepolicy_vers.txt";
        return false;
    }
    return true;
}

该版本用于选择 /system/etc/selinux/mapping/<version>.cil,不是直接使用当前 platform API。它表达 vendor policy 编译时看到的 platform public policy 版本。

5.2 必需与可选 ​

Android 17 对输入文件区分得很明确:

输入缺失行为角色
plat_sepolicy.cil不会进入 split 路径platform 主策略
vendor_sepolicy.cil返回 falsevendor 私有策略
plat_pub_versioned.cil返回 falsevendor 编译时 public policy
platform mapping交给 secilc,失败阻止启动版本兼容映射
system_ext/product policy清空路径并跳过可选分区策略
odm policy清空路径并跳过可选 ODM 策略
compat CIL不存在则跳过兼容补充
genfs CIL按 vendor genfs 版本决定genfs label 兼容

例如 vendor policy 是硬依赖:

源码文件:system/core/init/selinux.cpp

cpp
std::string vendor_policy_cil_file(
        "/vendor/etc/selinux/vendor_sepolicy.cil");
if (access(vendor_policy_cil_file.c_str(), F_OK) == -1) {
    LOG(ERROR) << "Missing " << vendor_policy_cil_file;
    return false;
}

5.3 Secilc调用 ​

编译输出放在 /dev/sepolicy.XXXXXX,因为 /dev 是此时可用的 tmpfs。代码通过 mkostemp() 持有 fd,然后构造参数:

源码文件:system/core/init/selinux.cpp

cpp
char compiled_sepolicy[] = "/dev/sepolicy.XXXXXX";
unique_fd compiled_sepolicy_fd(mkostemp(compiled_sepolicy, O_CLOEXEC));
if (compiled_sepolicy_fd < 0) {
    PLOG(ERROR) << "Failed to create temporary file " << compiled_sepolicy;
    return false;
}

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",
};

随后按存在性追加 system_ext、product、vendor、odm 和 genfs CIL,并执行:

源码文件:system/core/init/selinux.cpp

相关函数/类型:OpenSplitPolicy

cpp
if (ForkExecveAndWaitForCompletion(
        compile_args[0], (char**)compile_args.data()) != 0) {
    unlink(compiled_sepolicy);
    return false;
}
unlink(compiled_sepolicy);

policy_file->fd = std::move(compiled_sepolicy_fd);
policy_file->path = compiled_sepolicy;

这里先 unlink 再保留 fd 是标准的临时文件生命周期:目录项消失,但已打开 fd 仍能被 ReadFdToString() 读取;进程关闭 fd 后数据自然释放,不在 /dev 留下 policy 文件。

6. Snapuserd过渡 ​

6.1 读取在前 ​

Android policy 加载函数首先补挂缺失的 system_ext/product,再把完整 policy 读入内存:

源码文件:system/core/init/selinux.cpp

cpp
void LoadSelinuxPolicyAndroid() {
    MountMissingSystemPartitions();

    LOG(INFO) << "Opening SELinux policy";
    std::string policy;
    ReadPolicy(&policy);

    auto snapuserd_helper = SnapuserdSelinuxHelper::CreateIfNeeded();
    if (snapuserd_helper) {
        snapuserd_helper->StartTransition();
    }

    LoadSelinuxPolicy(policy);

    if (snapuserd_helper) {
        snapuserd_helper->FinishTransition();
        snapuserd_helper = nullptr;
    }
}

读取必须在 StartTransition() 前完成。snapshot 动态分区依赖 kernel-privileged snapuserd 提供数据,但 policy 加载后这个进程会进入新的权限边界;代码先把 policy 留在内存,再重写 dm-user 表、停止旧 snapuserd,加载 policy,最后启动处于正确域的 snapuserd 并重新附着设备。

6.2 临界窗口 ​

StartTransition() 与 FinishTransition() 之间不能再读取 /system 或其他动态分区。这个约束解释了为什么 ReadPolicy() 返回 std::string,而不是把 policy fd 留给 security_load_policy() 边读边用。

若 helper 创建失败但当前并不需要 snapshot transition,正常路径继续;若 transition 内部失败,其实现会返回错误或触发 fatal。本文只证明调用顺序,snapuserd socket、dm-user 表和 COW 合并细节属于 IN058。

7. 内核加载 ​

所有路径最终调用同一个函数:

源码文件: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 与 selinuxfs 内核接口交互。它消费的是已经编译好的 binary policy;不能把这里理解为内核直接解析 .te 或 .cil。

加载成功只建立规则数据库,尚未保证目标 enforcement 状态,也尚未让 PID 1 进入 init domain。加载失败是 fatal,没有“退回旧 policy”或 permissive 继续的恢复分支。

8. Enforcing ​

8.1 目标状态 ​

运行参数只在构建允许 permissive 时才生效:

源码文件: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;
}

所以 user build 上添加 androidboot.selinux=permissive 不能绕过编译期约束。参数是输入,ALLOW_PERMISSIVE_SELINUX 才决定输入是否有消费者。

8.2 状态切换 ​

代码先读取内核当前状态,只在与目标不一致时调用 security_setenforce():

源码文件: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";
        }
    }
}

Android 17 的这条启动路径没有旧稿所写的 checkreqprot 修改代码。分析当前版本时应搜索真实符号,而不是沿用旧版本或通用 SELinux 初始化模板。

9. Domain切换 ​

9.1 Init标签 ​

加载 policy 和设置 enforcement 后,PID 1 仍在 kernel domain。代码必须给 exec 入口恢复 label:

源码文件:system/core/init/selinux.cpp

相关函数/类型:SetupSelinux

cpp
if (selinux_android_restorecon("/system/bin/init", 0) == -1) {
    PLOG(FATAL) << "restorecon failed of /system/bin/init failed";
}

ext4 等能在 xattr 中保存 label 的文件系统可能已经有正确标签,但 recovery ramdisk 等文件系统需要显式 restorecon。源码因此无条件执行,保证不同启动介质使用同一 transition 前提。

Microdroid OpenDice 分支还会在 kernel context 中递归 restorecon /microdroid_resources,避免以后给 init 域授予不必要的 tmpfs:file relabelfrom 权限。这个动作体现了最小权限设计:能在特权窗口完成的 relabel,不把能力带入长期运行域。

9.2 计时交接 ​

源码文件:system/core/init/selinux.cpp

相关函数/类型:SetupSelinux

cpp
setenv(kEnvSelinuxStartedAt,
       std::to_string(start_time.time_since_epoch().count()).c_str(), 1);

二阶段 RecordStageBoottimes() 用它与 first-stage、second-stage 时间计算 ro.boottime.init.selinux,读取后 unsetenv()。该值记录阶段起点,不是单纯的 policy compile 耗时;它还包含日志、overlay 探测、snapuserd、加载、enforcing 和 restorecon。

9.3 SecondStage ​

无 overlay 时直接 exec:

源码文件:system/core/init/selinux.cpp

相关函数/类型:SetupSelinux

cpp
const char* path = "/system/bin/init";
const char* args[] = {path, "second_stage", nullptr};
execv(path, const_cast<char**>(args));

PLOG(FATAL) << "execv(\"" << path << "\") failed";

policy 中的 type transition、可执行文件 label 和当前 kernel domain 共同决定 exec 后的进程 domain。execv 成功不返回;若文件 label 不正确或 policy 不允许 transition,问题通常要结合 AVC、exec errno 和随后二阶段访问一起定位。

10. Overlay分支 ​

仅在编译启用 ALLOW_REMOUNT_OVERLAYS 时,EarlySetupOverlays() 扫描 /proc/mounts。发现 overlay 后,它在 enforcing 前把 /second_stage_resources 重新挂载为可执行 tmpfs:

源码文件:system/core/init/selinux.cpp

cpp
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
                << " to remove NO_EXEC";
}

policy 加载和 init restorecon 完成后,SetupOverlays() 把 /system/xbin/overlay_remounter 复制到该 tmpfs,restorecon、打开、unlink,再通过 fd exec:

源码文件:system/core/init/selinux.cpp

相关函数/类型:SetupOverlays

cpp
auto dest = unique_fd(open(or_dest.c_str(), O_RDONLY | O_CLOEXEC));
if (dest.get() == -1) {
    PLOG(FATAL) << "Failed to reopen " << or_dest;
}
if (unlink(or_dest.c_str()) == -1) {
    PLOG(FATAL) << "Failed to unlink " << or_dest;
}
const char* args[] = {or_dest.c_str(), nullptr};
fexecve(dest.get(), const_cast<char**>(args), environ);

SetupOverlays() 成功时不返回。overlay_remounter 在专用域中重新挂载 overlay,随后再进入 second-stage init。这条路径解释了为什么流程图中 overlay 不是挂载完成后的普通函数调用,而是额外一次进程映像切换。

system/core/overlay_remounter/overlay_remounter.cpp 在完成逐个卸载和只读重挂后显式执行二阶段:

源码文件:system/core/overlay_remounter/overlay_remounter.cpp

相关函数/类型:main

cpp
const char* path = "/system/bin/init";
const char* args[] = {path, "second_stage", nullptr};
execv(path, const_cast<char**>(args));

PLOG(FATAL) << "execv(\"" << path << "\") failed";

11. 故障矩阵 ​

现象失败阶段是否继续主要检查
No precompiled sepolicy候选查找进入 runtime compilevendor/odm 文件
hash files differ身份校验进入 runtime compilesystem/vendor 镜像组合
Missing vendor_sepolicy.cil编译输入最终 fatalvendor policy 产物
secilc 非零退出runtime compile最终 fatalCIL、mapping、版本
Could not load policy内核加载fatalbinary policy 与内核版本
snapuserd transition 失败动态分区过渡fatal/中止snapshot、dm-user、socket
security_setenforce 失败enforcementfatal内核状态与权限
init restorecon 失败domain 前置fatalfile_contexts 与文件系统
overlay remounter exec 失败overlay 交接fataltmpfs、label、可执行文件
second-stage exec 失败阶段交接fatal路径、label、exec errno

预编译 policy 被拒绝不是启动失败,只要 runtime compile 成功;secilc 失败才把这个 fallback 变成 fatal。排查时必须保留完整日志顺序,不能只搜索第一条 sepolicy 错误。

12. 启动观测 ​

这个阶段没有一个 host 单测能够证明真实设备的 system/vendor policy 组合和内核加载。验证应分别覆盖输入选择、编译/加载结果、enforcement 和 domain transition:

输入断言证明范围未证明范围
匹配的 precompiled hashes无 Compiling,直接打开 policy预编译选择成功policy 行为正确
故意不匹配的 hash 组合出现 mismatch 后运行 secilcfallback 决策正确所有 CIL 可编译
androidboot.selinux=permissive允许构建显示 Permissive参数被消费user build 可 permissive
正常 bootPID 1 为 u:r:init:s0exec domain transition 完成所有服务 domain 正确
snapshot OTA bootsnapuserd transition 完成policy 加载窗口可读COW merge 算法正确

源码核对:

bash
rg -n "SetupSelinux|OpenSplitPolicy|FindPrecompiledSplitPolicy|LoadSelinuxPolicyAndroid" \
  system/core/init/selinux.cpp
rg -n "SelinuxSetEnforcement|GetUserdebugPlatformPolicyFile|SetupOverlays" \
  system/core/init/selinux.cpp

设备验证:

bash
adb shell getenforce
adb shell 'cat /proc/1/attr/current'
adb shell 'getprop ro.boottime.init.selinux'
adb shell 'dmesg | grep -E "Opening SELinux policy|Compiling SELinux policy|Loading SELinux policy|security_setenforce|restorecon"'
adb shell 'ls -Z /system/bin/init'

正常 user build 应为 Enforcing,PID 1 通常显示 u:r:init:s0。ro.boottime.init.selinux 证明计时变量被二阶段消费,不单独证明使用了 precompiled policy;是否编译必须结合早期 kernel log。

13. 设备边界 ​

本文基于 Android 17 真实源码证明了 SetupSelinux() 的阶段顺序、split/monolithic/Microdroid 选择、precompiled hash 配对、runtime secilc 输入、snapuserd 临界窗口、内核 policy 加载、enforcing、init restorecon、overlay-remounter 和 second-stage exec。

本文没有证明 CIL 语法、Treble public policy 兼容规则的全部生成过程、内核 SELinux hook、具体 allow/neverallow、file_contexts 匹配算法或 snapuserd transition 内部协议。后续 SELinux 模块将分别承担这些机制;本篇只建立 init 启动阶段的真实边界。