SELinux运行模式
本文面向已经读过 SELinux架构、DAC与MAC 和 类型强制 的读者。本文不把运行模式写成“Disabled、Permissive、Enforcing 三行定义”,而是区分四种不同状态:内核是否编译 SELinux、启动时模块是否启用、全局 enforcing 标志、source domain 是否在 permissive map 中。只有先确定状态 owner,才能解释 getenforce、setenforce、androidboot.selinux 和 permissive domain 各自改变了什么。
全文基于 Android 17 init、system/sepolicy 和 common kernel 源码。读完后,你应能从 ALLOW_PERMISSIVE_SELINUX 追到 init 的目标模式,从 /sys/fs/selinux/enforce 追到 security:setenforce 权限与 AVC reset,从 avc_denied() 解释全局与单域 permissive 的组合,并说明为什么 permissive 不能修复 DAC、错误标签、无效 context、策略编译或加载失败。
1. 四种状态
| 状态 | owner | 何时决定 | 运行期表现 |
|---|---|---|---|
| 未编译 | CONFIG_SECURITY_SELINUX=n | kernel 构建 | 没有 SELinux LSM 实现 |
| 已编译但启动禁用 | selinux_enabled_boot=0 | kernel cmdline,且启用 bootparam 支持 | SELinux 不注册/不完成正常运行主线 |
| 全局 permissive/enforcing | selinux_state.enforcing | kernel 启动、Android init、selinuxfs 写入 | 所有非 strict denial 放行或阻断 |
| 单域 permissive | policydb.permissive_map | policy 编译与加载 | 仅 source type 对应 domain 的 denial 放行 |
“Disabled”不是 setenforce 0。运行时 disable 已不支持,selinuxfs 明确要求使用启动参数;permissive 仍加载策略、解析 context、计算 access vector 和生成审计,只改变普通 denial 是否返回 -EACCES。
单域 permissive 不是全局状态转换,图中用分支表示它只影响特定 source type。目标对象 type 是否 permissive 不决定请求是否放行,内核查询的是 source context type。
2. 内核条件
2.1 编译开关
CONFIG_SECURITY_SELINUX 决定内核是否包含 SELinux,依赖 network、audit 和基础 security 框架。CONFIG_SECURITY_SELINUX_BOOTPARAM 才允许 selinux=0 在启动时禁用模块;没有该选项时,命令行参数不会提供这条关闭路径。
源码文件:kernel/common/security/selinux/Kconfig
config SECURITY_SELINUX
bool "SELinux Support"
depends on SECURITY_NETWORK && AUDIT && NET && INET
select NETWORK_SECMARK
default n
config SECURITY_SELINUX_BOOTPARAM
bool "SELinux boot parameter"
depends on SECURITY_SELINUX
default n
config SECURITY_SELINUX_DEVELOP
bool "SELinux Development Support"
depends on SECURITY_SELINUX
default ySECURITY_SELINUX_DEVELOP 决定内核 enforcing 状态是否可变。关闭时 enforcing_enabled() 固定返回 true,enforcing_set() 为空函数,selinuxfs 的 enforce 文件也没有 write handler。
2.2 启动参数
开发配置中,kernel 参数 enforcing= 设置 selinux_enforcing_boot;bootparam 配置中,selinux= 设置 selinux_enabled_boot。非开发配置把启动 enforcing 固定为 1。
源码文件:kernel/common/security/selinux/hooks.c
相关位置:boot parameter parsers
#ifdef CONFIG_SECURITY_SELINUX_DEVELOP
static int selinux_enforcing_boot __initdata;
static int __init enforcing_setup(char *str)
{
unsigned long enforcing;
if (!kstrtoul(str, 0, &enforcing))
selinux_enforcing_boot = enforcing ? 1 : 0;
return 1;
}
__setup("enforcing=", enforcing_setup);
#else
#define selinux_enforcing_boot 1
#endif
int selinux_enabled_boot __initdata = 1;
#ifdef CONFIG_SECURITY_SELINUX_BOOTPARAM
static int __init selinux_enabled_setup(char *str)
{
unsigned long enabled;
if (!kstrtoul(str, 0, &enabled))
selinux_enabled_boot = enabled ? 1 : 0;
return 1;
}
__setup("selinux=", selinux_enabled_setup);
#endifAndroid bootloader 的 androidboot.selinux 由用户空间 init 读取,和 kernel 的 selinux=、enforcing= 不是同一个参数入口。前者决定 init 想把已启用 SELinux 设置成何种 enforcing 状态,后两者在更早的 kernel 初始化期生效。
2.3 Enforcing状态
源码文件:kernel/common/security/selinux/include/security.h
相关函数:enforcing_enabled、enforcing_set
#ifdef CONFIG_SECURITY_SELINUX_DEVELOP
static inline bool enforcing_enabled(void)
{
return READ_ONCE(selinux_state.enforcing);
}
static inline void enforcing_set(bool value)
{
WRITE_ONCE(selinux_state.enforcing, value);
}
#else
static inline bool enforcing_enabled(void)
{
return true;
}
static inline void enforcing_set(bool value)
{
}
#endif这段代码是 user/production 内核“运行时不能降到 permissive”的最终边界之一:即使用户空间能打开某个接口,没有 development support,内核状态仍固定 enforcing。
3. Android启动
3.1 构建变体
Android init 默认以 ALLOW_PERMISSIVE_SELINUX=0 编译;debuggable 产品覆盖为 1。这个宏属于 init 用户空间程序,不等于 kernel CONFIG_SECURITY_SELINUX_DEVELOP,两侧都允许才可能完成全局切换。
源码文件:system/core/init/Android.bp
cflags: [
"-DALLOW_FIRST_STAGE_CONSOLE=0",
"-DALLOW_LOCAL_PROP_OVERRIDE=0",
"-DALLOW_PERMISSIVE_SELINUX=0",
],
product_variables: {
debuggable: {
cppflags: [
"-UALLOW_PERMISSIVE_SELINUX",
"-DALLOW_PERMISSIVE_SELINUX=1",
],
},
},因此“userdebug 一定 permissive”是错误的。debuggable 只允许 init 尊重 permissive 启动参数,默认没有该参数时仍选择 enforcing。
3.2 目标模式
init 先查 kernel cmdline,再查 bootconfig 中的 androidboot.selinux;只有值精确等于 permissive 才返回 permissive,其余情况默认 enforcing。user build 直接忽略该结果并返回 true。
源码文件:system/core/init/selinux.cpp
相关函数:StatusFromProperty、IsEnforcing
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;
}这里没有读取 ro.debuggable,因为 debuggable 已在编译 init 时转成宏。运行中的属性不能动态改变 ALLOW_PERMISSIVE_SELINUX。
3.3 状态同步
策略读入和加载后,SelinuxSetEnforcement() 比较 kernel 当前状态与 init 目标状态,只在不一致时调用 security_setenforce();失败是 fatal。
源码文件: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";
}
}
}状态同步发生在 SetupSelinux() 加载 policy 之后、restorecon init 与进入 second stage 之前。策略加载失败不会通过 permissive 降级继续启动;permissive 只处理已加载策略产生的普通 access denial。
4. selinuxfs切换
4.1 读取状态
/sys/fs/selinux/enforce 的 read handler 直接输出 enforcing_enabled()。getenforce 最终依赖这类状态接口,而不是重新读取 Android boot 参数。
源码文件:kernel/common/security/selinux/selinuxfs.c
static ssize_t sel_read_enforce(
struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
char tmpbuf[TMPBUFLEN];
ssize_t length;
length = scnprintf(
tmpbuf, TMPBUFLEN, "%d",
enforcing_enabled());
return simple_read_from_buffer(
buf, count, ppos,
tmpbuf, length);
}4.2 写入权限
写 enforce 文件并非“root 即可”。内核解析 0/1 后,以当前 SID 到 security initial SID 查询 security:setenforce;只有权限通过才更新状态、写 audit、通知 netlink/status page 和 IMA。
源码文件:kernel/common/security/selinux/selinuxfs.c
相关函数:sel_write_enforce
new_value = !!scan_value;
old_value = enforcing_enabled();
if (new_value != old_value) {
length = avc_has_perm(
current_sid(), SECINITSID_SECURITY,
SECCLASS_SECURITY,
SECURITY__SETENFORCE, NULL);
if (length)
goto out;
audit_log(audit_context(), GFP_KERNEL,
AUDIT_MAC_STATUS,
"enforcing=%d old_enforcing=%d ...",
new_value, old_value);
enforcing_set(new_value);
if (new_value)
avc_ss_reset(0);
selnl_notify_setenforce(new_value);
selinux_status_update_setenforce(new_value);
if (!new_value)
call_blocking_lsm_notifier(
LSM_POLICY_CHANGE, NULL);
selinux_ima_measure_state();
}切回 enforcing 时清空 AVC,防止 permissive 期间临时 grant 的 cache entry 继续影响强制模式;切到 permissive 时通知 LSM policy change 消费者。状态更新不是单一 bool 写入。
4.3 编译条件
sel_write_enforce 只在 CONFIG_SECURITY_SELINUX_DEVELOP 下编译;否则 file operations 的 write 指针为 null。
#else
#define sel_write_enforce NULL
#endif
static const struct file_operations sel_enforce_ops = {
.read = sel_read_enforce,
.write = sel_write_enforce,
.llseek = generic_file_llseek,
};因此 setenforce 失败可能来自没有 write handler、SELinux policy 拒绝 setenforce、文件系统权限或用户空间工具不可用。不能把所有失败都解释为“不是 root”。
4.4 运行时禁用
selinuxfs 的旧 disable 接口只记录错误,明确要求用 kernel cmdline;运行中从 enforcing/permissive 切到 disabled 不被支持。
if (new_value) {
pr_err("SELinux: Runtime disable is not supported, "
"use selinux=0 on the kernel cmdline.\n");
}Disabled 会改变 LSM 是否参与对象创建和标签生命周期,无法用运行时 flag 安全撤销。
5. AVC行为
5.1 全局模式
AVC 发现 requested permission 不在 allowed bit 中时进入 avc_denied()。AVC_STRICT 无条件返回 -EACCES;普通请求只有在全局 enforcing 且 source domain 不是 permissive 时拒绝。
源码文件:kernel/common/security/selinux/avc.c
相关函数:avc_denied
static noinline int avc_denied(
u32 ssid, u32 tsid,
u16 tclass, u32 requested,
u8 driver, u8 base_perm, u8 xperm,
unsigned int flags,
struct av_decision *avd)
{
if (flags & AVC_STRICT)
return -EACCES;
if (enforcing_enabled() &&
!(avd->flags & AVD_FLAGS_PERMISSIVE))
return -EACCES;
avc_update_node(
AVC_CALLBACK_GRANT, requested,
driver, base_perm, xperm,
ssid, tsid, tclass,
avd->seqno, NULL, flags);
return 0;
}全局 permissive 时普通 denial 会作为临时 grant 更新 AVC node 并返回 0;审计由上层 avc_has_perm() 负责。strict 调用即使全局 permissive 也拒绝,所以“permissive 放行所有 SELinux 查询”并不准确。
5.2 单域模式
security server 计算 access vector 时读取 source context 的 type,并查询 policydb.permissive_map。命中后在 decision 上设置 AVD_FLAGS_PERMISSIVE。
源码文件:kernel/common/security/selinux/ss/services.c
相关位置:security_compute_av
scontext = sidtab_search(sidtab, ssid);
if (!scontext) {
pr_err("SELinux: unrecognized SID %d\n", ssid);
goto out;
}
if (ebitmap_get_bit(
&policydb->permissive_map,
scontext->type)) {
avd->flags |= AVD_FLAGS_PERMISSIVE;
}
if (ebitmap_get_bit(
&policydb->neveraudit_map,
scontext->type)) {
avd->flags |= AVD_FLAGS_NEVERAUDIT;
}查询 key 是 source type,不是 target type。把文件 type 标记为 permissive 没有“让所有人访问该文件”的语义;policy permissive 声明适用于 domain/source type。
5.3 neveraudit组合
当 decision 同时标记 permissive 与 neveraudit,security server 直接把 allowed 设为全 bit,并清空审计向量。
if (avd->flags ==
(AVD_FLAGS_PERMISSIVE |
AVD_FLAGS_NEVERAUDIT)) {
goto allow;
}
// ... 正常计算TE与constraints。
if (avd->flags & AVD_FLAGS_NEVERAUDIT)
avd->auditallow = avd->auditdeny = 0;
allow:
avd->allowed = 0xffffffff;这说明“permissive 一定有所有 denial 日志”也不成立。dontaudit/neveraudit 可以抑制审计,日志采样、rate limit 和请求是否到达 SELinux hook也会影响可见性。
6. 构建约束
6.1 permissive_map
policy source 中的 permissive <domain>; 会进入 binary policy 的 permissive bitmap。Android 平台源码当前不依赖一组普遍 permissive domain;调试宏或设备策略可能在特定构建中引入。
Permissive_domains_on_user_builds 是 Soong module 属性,用于极少数明确豁免;它不是运行期 allowlist。
源码文件:system/sepolicy/build/soong/policy.go
相关结构:policyBinaryProperties
type policyBinaryProperties struct {
Stem *string
Srcs []string `android:"path"`
Ignore_neverallow *bool
Installable *bool
// List of domains that are allowed to be in permissive mode on user builds.
Permissive_domains_on_user_builds []string
}6.2 User构建检查
非 debuggable 构建完成 secilc 后,Soong 用 sepolicy-analyze permissive 提取 permissive domains,过滤显式豁免后要求文件为空。
源码文件:system/sepolicy/build/soong/policy.go
相关函数:policyBinary.GenerateAndroidBuildActions
if !ctx.Config().Debuggable() {
permissiveDomains := pathForModuleOut(
ctx, c.stem()+"_permissive")
cmd := rule.Command().BuiltTool("sepolicy-analyze").
Input(bin).
Text("permissive")
allowedDomains :=
c.properties.Permissive_domains_on_user_builds
if len(allowedDomains) != 0 {
cmd.Text("| { grep -Fxv")
for _, d := range allowedDomains {
cmd.FlagWithArg(
"-e ", proptools.ShellEscape(d))
}
cmd.Text(" || true; }")
}
cmd.Text(" > ").Output(permissiveDomains)
rule.Command().Text("if test").
FlagWithInput("-s ", permissiveDomains).
Text("; then echo").
Flag("-e").
Text("ERROR: permissive domains not allowed in user builds").
Text("&& cat ").
Input(permissiveDomains).
Text("; exit 1; fi")
}失败发生在构建期,设备不会加载这份策略。它验证“user policy 没有未豁免 permissive domain”,不验证设备当前全局 enforcing 状态,也不验证 bootloader 是否传了正确参数。
7. 模式矩阵
| 条件 | Policy加载 | SELinux检查 | 普通denial | Strict检查 | 日志 |
|---|---|---|---|---|---|
| SELinux未编译/启动禁用 | 否 | 否或不完整 | 不适用 | 不适用 | 无SELinux AVC |
| 全局permissive | 是 | 是 | 返回0 | 仍拒绝 | 按audit策略,可能被静默 |
| 全局enforcing,source domain permissive | 是 | 是 | 该domain返回0 | 仍拒绝 | 按audit策略 |
| 全局enforcing,普通domain | 是 | 是 | 返回-EACCES | 拒绝 | 按audit策略 |
这里的 “返回0” 只描述 SELinux hook 的普通 denial。VFS DAC、read-only filesystem、capability、seccomp、Binder UID 检查和其他 LSM 仍可拒绝同一操作。
8. 故障边界
| 现象 | 第一检查点 | 不应直接下的结论 |
|---|---|---|
getenforce 输出 Enforcing | /sys/fs/selinux/enforce | 不说明每个domain都非permissive |
setenforce 0 Permission denied | kernel develop、write handler、security:setenforce、身份 | 不只是root问题 |
| permissive 下仍返回 EACCES | DAC、strict AVC、其他LSM、只读/immutable、应用层检查 | 不说明SELinux仍在强制普通denial |
| 没有 AVC 日志 | dontaudit/neveraudit、提前失败、未到hook、rate limit | 不说明policy允许 |
| policy加载失败 | CIL/binary/version/class映射 | permissive不会绕过加载错误 |
| 标签错误 | contexts/restorecon/seapp | setenforce不会修复标签 |
| user build出现permissive domain | 构建输入、豁免列表、实际binary来源 | 不能只看源码里有无permissive字符串 |
| 重启后模式恢复 | bootconfig/cmdline与init构建宏 | 运行时setenforce不持久化启动配置 |
9. 验证实验
9.1 只读状态实验
下面命令不修改设备,分别观察模块是否启用、全局 enforcing、活动 LSM、进程 context 和 policy 状态页。
# 收集模式、LSM列表和当前context。
adb shell getenforce
adb shell 'cat /sys/fs/selinux/enforce 2>/dev/null || true'
adb shell 'cat /sys/kernel/security/lsm 2>/dev/null || true'
adb shell 'id -Z; cat /proc/self/attr/current'
# 查看selinuxfs与AVC统计是否存在。
adb shell 'mount | grep selinuxfs || true'
adb shell 'cat /sys/fs/selinux/avc/cache_stats 2>/dev/null || true'实验输入是不修改状态的运行设备。关键断言是:getenforce 与 enforce 文件应一致;活动 LSM 应包含 selinux;当前 context 应能读取。缺少某一项时应区分工具缺失、securityfs 未挂载、SELinux 未启用和权限不足。该实验不能证明单 domain permissive 列表,也不能证明 policy 来自当前源码 checkout。
9.2 Build变体实验
可在已初始化的 AOSP 构建环境比较 user 与 userdebug 的 init 编译命令或中间 flags,确认 ALLOW_PERMISSIVE_SELINUX 的差异。
# 选择对应产品后查看init编译命令中的宏;具体目标名随产品变化。
m init -n 2>&1 | rg 'ALLOW_PERMISSIVE_SELINUX'
# 查询生成的binary policy中是否存在permissive domain。
sepolicy-analyze \
out/target/product/<product>/vendor/etc/selinux/precompiled_sepolicy \
permissive关键断言是 user init 命令含 ALLOW_PERMISSIVE_SELINUX=0,debuggable 产品覆盖为 1;user binary policy 的 permissive 输出应为空或只包含明确豁免。此实验依赖产品实际输出路径,不能把示例路径当成所有设备固定位置。
9.3 Enforcing回归
在允许修改模式的隔离测试设备上,切换实验必须记录原状态并恢复;生产设备不应执行。
# 仅限可丢弃的userdebug/eng测试设备;先记录原值并注册自动恢复。
original_mode=$(adb shell getenforce | tr -d '\r')
restore_selinux_mode() {
if [ "$original_mode" = Enforcing ]; then
adb shell su 0 setenforce 1
elif [ "$original_mode" = Permissive ]; then
adb shell su 0 setenforce 0
fi
}
trap restore_selinux_mode EXIT INT TERM
adb shell su 0 setenforce 0
adb shell getenforce
adb shell su 0 setenforce 1
adb shell getenforce
# 退出脚本或中断时,trap会恢复实验前状态。输入是支持 development mode、具备 security:setenforce 权限的测试设备。关键断言是 0/1 写入分别反映为 Permissive/Enforcing,切回 enforcing 后正常业务仍通过。它没有验证任何具体 denial,也不授权在用户数据设备上制造安全窗口。
10. 源码导航
先区分 kernel 编译、启动 enable 和 enforcing 三层开关:
# 查找Kconfig、boot参数和enforcing状态实现。
rg -n 'SECURITY_SELINUX|SECURITY_SELINUX_BOOTPARAM|SECURITY_SELINUX_DEVELOP|selinux_enabled_boot|selinux_enforcing_boot|enforcing_enabled' \
kernel/common/security/selinux/Kconfig \
kernel/common/security/selinux/hooks.c \
kernel/common/security/selinux/include/security.h再检查 Android init 如何把 build variant 和 bootconfig 转成目标模式:
# 查看init编译宏、androidboot.selinux和状态同步。
rg -n 'ALLOW_PERMISSIVE_SELINUX|StatusFromProperty|IsEnforcing|SelinuxSetEnforcement' \
system/core/init/Android.bp \
system/core/init/selinux.cpp运行时切换问题要进入 selinuxfs,而不是只读用户空间工具源码:
# 查看enforce文件的read/write handler及安全权限检查。
rg -n 'sel_read_enforce|sel_write_enforce|SECURITY__SETENFORCE|enforcing_set|avc_ss_reset' \
kernel/common/security/selinux/selinuxfs.c单 domain permissive 与普通 denial 的最终行为位于 security server 和 AVC:
# 追踪permissive_map、decision flag和avc_denied返回值。
rg -n 'permissive_map|AVD_FLAGS_PERMISSIVE|AVD_FLAGS_NEVERAUDIT|avc_denied|AVC_STRICT' \
kernel/common/security/selinux/ss/services.c \
kernel/common/security/selinux/avc.c \
kernel/common/security/selinux/include/security.huser build 的 permissive domain 检查属于策略构建链:
# 查看binary policy生成后的permissive分析与失败条件。
rg -n 'Permissive_domains_on_user_builds|sepolicy-analyze|permissive domains not allowed' \
system/sepolicy/build/soong/policy.go11. 闭环复述
- 分别解释
CONFIG_SECURITY_SELINUX=n、selinux=0、enforcing=0、androidboot.selinux=permissive和 policy permissive domain 的 owner 与生效时机。 - 从
ALLOW_PERMISSIVE_SELINUX开始,追到StatusFromProperty()、IsEnforcing()、SelinuxSetEnforcement()和 kernel enforce state,说明为什么 userdebug 仍默认 enforcing。 - 从写
/sys/fs/selinux/enforce开始,复述security:setenforce检查、audit、状态写入、AVC reset、netlink/status 通知与 IMA measurement。 - 从
policydb.permissive_map追到AVD_FLAGS_PERMISSIVE和avc_denied(),说明为什么判断的是 source domain,以及 global enforcing 下单域 denial 如何返回 0。 - 给出 permissive 下操作仍失败的至少四个非 SELinux 普通 denial 原因,并说明没有 AVC 日志为什么也不能证明 allow。
- 用 Soong 的
sepolicy-analyze permissive路径解释 user build 如何在设备启动前阻断未豁免 permissive domain。
SELinux 运行模式不是一个三值枚举,而是一组分层状态:kernel 是否包含并启用模块,Android init 期望何种全局 enforcing,policy 是否把 source domain 标为 permissive,以及具体检查是否 strict。只有把问题放回正确层次,getenforce、boot 参数、setenforce、AVC 日志和构建检查才能形成可验证的因果链。
