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 之间:
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 bytes | ReadPolicy | security_load_policy | enforcing 前 |
avb_handle 状态 | first-stage mount | debug policy 选择 | 读取 policy 前 |
| snapuserd transition | SnapuserdSelinuxHelper | 动态分区 I/O | policy 加载前后 |
| enforcing | SELinux kernel | 所有后续访问 | security_setenforce 后 |
| init 文件 label | restorecon | exec domain transition | second-stage exec 时 |
SELINUX_STARTED_AT | SetupSelinux | RecordStageBoottimes | 二阶段启动后 |
旧文常把“加载策略”和“进入 init domain”写成同一步。实际上 policy 先加载,enforcing 再切换,文件再 restorecon,最后 exec 才让进程按照 policy 中的 transition 进入 init domain。
2. 总入口
Android 17 的真实入口如下:
源码文件:system/core/init/selinux.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
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
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
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
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
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
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
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
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
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 | 返回 false | vendor 私有策略 |
plat_pub_versioned.cil | 返回 false | vendor 编译时 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
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
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
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
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
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
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
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
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
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
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
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
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
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 compile | vendor/odm 文件 |
| hash files differ | 身份校验 | 进入 runtime compile | system/vendor 镜像组合 |
Missing vendor_sepolicy.cil | 编译输入 | 最终 fatal | vendor policy 产物 |
secilc 非零退出 | runtime compile | 最终 fatal | CIL、mapping、版本 |
Could not load policy | 内核加载 | fatal | binary policy 与内核版本 |
| snapuserd transition 失败 | 动态分区过渡 | fatal/中止 | snapshot、dm-user、socket |
security_setenforce 失败 | enforcement | fatal | 内核状态与权限 |
| init restorecon 失败 | domain 前置 | fatal | file_contexts 与文件系统 |
| overlay remounter exec 失败 | overlay 交接 | fatal | tmpfs、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 后运行 secilc | fallback 决策正确 | 所有 CIL 可编译 |
androidboot.selinux=permissive | 允许构建显示 Permissive | 参数被消费 | user build 可 permissive |
| 正常 boot | PID 1 为 u:r:init:s0 | exec domain transition 完成 | 所有服务 domain 正确 |
| snapshot OTA boot | snapuserd transition 完成 | policy 加载窗口可读 | COW merge 算法正确 |
源码核对:
rg -n "SetupSelinux|OpenSplitPolicy|FindPrecompiledSplitPolicy|LoadSelinuxPolicyAndroid" \
system/core/init/selinux.cpp
rg -n "SelinuxSetEnforcement|GetUserdebugPlatformPolicyFile|SetupOverlays" \
system/core/init/selinux.cpp设备验证:
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 启动阶段的真实边界。
