Permissive 域调试
本文承接 AVC Denial 日志 和 audit2allow 工具。前者解释 denial 字段如何生成,后者说明候选规则不能直接复制;本文专门研究 permissive 的状态语义:全局 permissive、策略声明的 per-domain permissive、user/userdebug 构建限制,以及如何在调试结束时证明目标域已经退出 permissive。
“permissive”在 Android 里至少有两层含义。全局模式由内核 enforcing 状态和 init 的 androidboot.selinux 判定共同决定;per-domain 模式则是 policydb 的 permissive_map 对某个 source type 的标记。前者影响所有 domain,后者只影响命中该 source type 的访问。两者都会审计拒绝,但只有 enforcing 且 source domain 不在 permissive map 时才把结果返回为 -EACCES。
1. 两种模式
1.1 全局模式
源码文件:system/core/init/selinux.cpp
enum EnforcingStatus { SELINUX_PERMISSIVE, SELINUX_ENFORCING };
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;
}init 只在编译期开关 ALLOW_PERMISSIVE_SELINUX 允许时接受 kernel cmdline 或 bootconfig 的 permissive;否则期望状态恒为 enforcing。这个函数只产生“期望全局模式”,真正切换由 SelinuxSetEnforcement 调用 security_setenforce 完成。
1.2 per-domain声明
源码文件:system/sepolicy/private/su.te
permissive su;permissive su; 是策略语言声明,不是 init 属性。编译器把 source type su 写入 policy binary 的 permissive map;运行时只有 source SID 解析为 su 时才会命中。这个例子还说明了调试域的安全边界:它位于 private policy,不能因为工具生成了候选 allow 就把任意 vendor domain 设为 permissive。
1.3 对比图
全局模式是内核状态,per-domain 是加载策略时建立的 map;两条路径在访问检查时汇合,但生命周期不同。全局模式可被 security_setenforce 改变,per-domain map 通常只能通过重新编译和加载 policy 改变。
2. 内核判定
2.1 policydb标记
源码文件:kernel/common/security/selinux/ss/services.c
scontext = sidtab_search(sidtab, ssid);
if (!scontext) {
pr_err("SELinux: %s: unrecognized SID %d\n",
__func__, ssid);
goto out;
}
/* permissive domain? */
if (ebitmap_get_bit(&policydb->permissive_map, scontext->type))
avd->flags |= AVD_FLAGS_PERMISSIVE;
/* neveraudit domain? */
if (ebitmap_get_bit(&policydb->neveraudit_map, scontext->type))
avd->flags |= AVD_FLAGS_NEVERAUDIT;
/* both permissive and neveraudit => allow */
if (avd->flags == (AVD_FLAGS_PERMISSIVE|AVD_FLAGS_NEVERAUDIT))
goto allow;内核先用 source SID 找到 scontext->type,再查 permissive_map;因此 per-domain permissive 的 key 是 source type,不是 PID、UID 或进程名。neveraudit_map 是另一张 map,和 permissive 同时命中时会直接跳到 allow,既放行又不审计。不要把两种 map 的效果混为“全局关闭日志”。
2.2 拒绝与放行
源码文件:kernel/common/security/selinux/avc.c
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;
// A permissive decision is cached as granted for this request.
avc_update_node(AVC_CALLBACK_GRANT, requested, driver, base_perm,
xperm, ssid, tsid, tclass, avd->seqno, NULL, flags);
return 0;
}AVC_STRICT 会忽略 permissive;普通路径只有在全局 enforcing 且 AVD_FLAGS_PERMISSIVE 未设置时返回 -EACCES。permissive domain 或全局 permissive 都会进入缓存 grant 路径,但审计仍可能由 avc_audit 记录。缓存中的 granted 不能理解为策略永久允许,它只是当前 enforcement 状态下对这次请求的结果。
2.3 cache快速路径
源码文件:kernel/common/security/selinux/hooks.c
denied = perms & ~avdc->allowed;
if (unlikely(denied && enforcing_enabled() &&
!avdc->permissive))
rc = -EACCES;
if (likely(!audited))
return rc;
rc2 = audit_inode_permission(inode, perms, audited, denied, rc);
if (rc2)
return rc2;
return rc;文件访问的 task AVC 快速路径缓存了 permissive 位。即使 cache hit,内核仍会用全局 enforcing 和缓存的 per-domain 标记决定返回值;audited 为零时直接返回,不产生审计。这个细节解释了为什么切换全局模式后,已有 cache entry 仍需结合 enforcing_enabled() 解读。
3. 构建约束
3.1 binary检查
源码文件:system/sepolicy/build/soong/policy.go
// permissive check is performed only in user build (not debuggable).
if !ctx.Config().Debuggable() {
permissiveDomains := pathForModuleOut(ctx, c.stem()+"_permissive")
cmd := rule.Command().BuiltTool("sepolicy-analyze").
Input(bin).
Text("permissive")
// Filter domains explicitly allowed by the product.
for _, d := range c.properties.Permissive_domains_on_user_builds {
cmd.FlagWithArg("-e ", proptools.ShellEscape(d))
}
cmd.Text(" > ").Output(permissiveDomains)
rule.Temporary(permissiveDomains)
rule.Command().Text("if test").
FlagWithInput("-s ", permissiveDomains).
Text("; then echo").
Text("ERROR: permissive domains not allowed in user builds").
Text("&& cat ").
Input(permissiveDomains).
Text("; exit 1; fi")
}检查对象是 secilc 刚生成的 binary,而不是 .te 文本;所以 attribute、条件宏和 CIL 版本化已经被编译器解析。non-debuggable 构建中只要过滤后的 permissive 列表非空,action 就以 1 退出;debuggable 构建跳过这条产品门槛,方便开发调试。
3.2 分析工具
源码文件:system/sepolicy/tools/sepolicy-analyze/perm.c、system/sepolicy/tools/sepolicy-analyze/README
static int list_permissive(policydb_t *policydb)
{
struct ebitmap_node *n;
unsigned int bit;
/* Iterate over every set bit in the permissive map. */
ebitmap_for_each_bit(&policydb->permissive_map, n, bit) {
if (ebitmap_node_get_bit(n, bit)) {
printf("%s\n", policydb->p_type_val_to_name[bit - 1]);
}
}
return 0;
}
int permissive_func(int argc, __attribute__((unused)) char **argv,
policydb_t *policydb)
{
if (argc != 1) {
USAGE_ERROR = true;
return -1;
}
return list_permissive(policydb);
}sepolicy-analyze <policy> permissive 遍历 binary policydb 的 permissive_map,用 bit - 1 从 type value 表恢复名字。它列出的是策略中声明的 permissive domain,不是当前正在产生 AVC 的进程列表;空输出表示 map 没有条目,不代表所有访问都已通过。
3.3 构建与运行时
构建检查和运行时 map 消费同一个 binary,但回答不同问题:Soong 检查“产品是否允许这些 permissive domain”,内核检查“这次 source SID 是否应绕过 enforcement”。
4. 真实调试
4.1 状态采集
# Read-only: query global enforcement and the policy's permissive domains.
adb shell getenforce
adb shell 'sepolicy-analyze /sys/fs/selinux/policy permissive'
# Read-only: capture AVC records with the source domain visible.
adb shell 'dmesg | grep -E "avc: denied|avc: denied"'
adb shell 'ps -AZ | grep -E "my_daemon|su"'getenforce 观察全局内核状态;sepolicy-analyze permissive 观察 binary map;ps -Z 验证实际进程 source domain。三者必须一起看:全局 Enforcing 且列表包含 su,表示只有 su 的拒绝被放行;全局 Permissive 时列表为空也不能推出访问会被阻断。
4.2 构建侧定位
# Read-only: find declarations and user-build checks in the source policy.
rg -n '^[[:space:]]*permissive |permissive_domains_on_user_builds|sepolicy-analyze.*permissive' \
system/sepolicy device vendor --glob '*.te' --glob '*.bp' --glob '*.go'
# Build validation: inspect the binary produced by the selected policy module.
m sepolicy构建后应对实际 binary 运行 sepolicy-analyze <binary> permissive,而不是只搜索 .te。如果源文件有 permissive foo; 但 binary 没有 foo,可能是 M4 条件未展开、module 未消费该目录或过滤/版本化移除了声明;如果 user build 在这里失败,错误来自 Soong 产品约束而非 AVC。
4.3 退出调试
移除 permissive <domain>; 后至少验证三件事:
- 重新生成的 binary 中
sepolicy-analyze permissive不再列出该 domain; - 设备
getenforce为 Enforcing,并复现原访问; - 原 AVC 现在返回失败时,补写的是最小 allow、正确 label 或 DAC 修复,而不是恢复 permissive。
如果 domain 必须在 userdebug 保留,应用 userdebug_or_eng(...) 的条件策略并确认 user 构建不会收到该声明;不要把 permissive_domains_on_user_builds 当成长期生产豁免清单。
5. 失败边界
5.1 全局模式失败
| 现象 | 首个检查点 | 结论边界 |
|---|---|---|
getenforce 是 Permissive | init IsEnforcing、bootconfig/cmdline | 所有 domain 可能放行,不能评价单域策略 |
setenforce 1 失败 | security_setenforce 返回值和权限 | 不是增加 allow 能解决的 policy map 问题 |
| cmdline 写 permissive 仍 Enforcing | ALLOW_PERMISSIVE_SELINUX 和构建类型 | release 构建可能编译期禁止 |
5.2 per-domain失败
| 现象 | 首个检查点 | 结论边界 |
|---|---|---|
| map 列出错误 domain | permissive 声明的 scope、M4 条件 | 不是 PID/UID 级开关 |
| denial 没有阻断 | permissive=1、source SID、全局模式 | 访问仍未被 allow 正式授权 |
| user build 拒绝生成 binary | sepolicy-analyze permissive 输出 | 需移除声明或使用明确产品 allowlist |
| 列表为空但仍有 AVC | dontaudit、审计位和 source context | map 为空不代表无 denial |
5.3 缓存与时机
permissive 标记在策略加载后进入内核 policydb,并可能被 AVC/task cache 记录。切换全局 enforcing 会重新影响 cache hit 的 enforcement gate,但不会把 policy binary 中的 permissive_map 删除;要彻底退出 per-domain permissive,必须生成并加载不含该声明的新 policy。调试时应记录重启/重新加载边界,避免把旧内核状态和新 binary 混在一起。
6. 读码练习
选取 AOSP 中的 su 声明完成闭环:
- 找到
private/su.te的permissive su;,判断它位于哪个 policy scope; - 用
sepolicy-analyze确认 binary map 是否列出su; - 在全局 Enforcing 下触发一次
sudenial,解释为什么日志仍出现而调用可能成功; - 对比
sepolicy-analyze permissive的 user build 检查,说明为什么同一声明在 debuggable 与 user 构建中的处理不同; - 删除声明后重新构建,确认原访问应转为真正的
-EACCES,再决定最小修复。
再阅读 services.c 的 permissive_map、neveraudit_map 分支和 avc_denied,解释“放行但审计”“放行且不审计”“拒绝且审计”三种结果如何组合。
7. 源码导航
kernel/common/security/selinux/ss/services.c:source type 查表、permissive/neveraudit 标记。kernel/common/security/selinux/avc.c:AVD_FLAGS_PERMISSIVE对拒绝返回值和缓存的影响。kernel/common/security/selinux/hooks.c:task AVC cache hit 时的 enforcement 判断。system/sepolicy/tools/sepolicy-analyze/perm.c:遍历 binarypermissive_map的分析器。system/sepolicy/tools/sepolicy-analyze/README:permissive 子命令输入、输出和 user build 边界。system/sepolicy/build/soong/policy.go:user build 的 permissive domain 检查和 allowlist。system/core/init/selinux.cpp:全局 enforcing 判定与security_setenforce。system/sepolicy/private/su.te:Android 17 真实 permissive domain 声明示例。
