Skip to content

Permissive 域调试

结合 Android 17 内核、init 和 Soong 源码,解释全局 permissive 与 per-domain permissive 的判定、审计、构建约束和调试退出路径。

基于android-17.0.0_r1
AndroidSELinuxpermissivesepolicy-analyze调试源码阅读

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

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

text
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

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

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

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

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

c
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 状态采集 ​

sh
# 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 构建侧定位 ​

sh
# 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>; 后至少验证三件事:

  1. 重新生成的 binary 中 sepolicy-analyze permissive 不再列出该 domain;
  2. 设备 getenforce 为 Enforcing,并复现原访问;
  3. 原 AVC 现在返回失败时,补写的是最小 allow、正确 label 或 DAC 修复,而不是恢复 permissive。

如果 domain 必须在 userdebug 保留,应用 userdebug_or_eng(...) 的条件策略并确认 user 构建不会收到该声明;不要把 permissive_domains_on_user_builds 当成长期生产豁免清单。

5. 失败边界 ​

5.1 全局模式失败 ​

现象首个检查点结论边界
getenforce 是 Permissiveinit IsEnforcing、bootconfig/cmdline所有 domain 可能放行,不能评价单域策略
setenforce 1 失败security_setenforce 返回值和权限不是增加 allow 能解决的 policy map 问题
cmdline 写 permissive 仍 EnforcingALLOW_PERMISSIVE_SELINUX 和构建类型release 构建可能编译期禁止

5.2 per-domain失败 ​

现象首个检查点结论边界
map 列出错误 domainpermissive 声明的 scope、M4 条件不是 PID/UID 级开关
denial 没有阻断permissive=1、source SID、全局模式访问仍未被 allow 正式授权
user build 拒绝生成 binarysepolicy-analyze permissive 输出需移除声明或使用明确产品 allowlist
列表为空但仍有 AVCdontaudit、审计位和 source contextmap 为空不代表无 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 声明完成闭环:

  1. 找到 private/su.te 的 permissive su;,判断它位于哪个 policy scope;
  2. 用 sepolicy-analyze 确认 binary map 是否列出 su;
  3. 在全局 Enforcing 下触发一次 su denial,解释为什么日志仍出现而调用可能成功;
  4. 对比 sepolicy-analyze permissive 的 user build 检查,说明为什么同一声明在 debuggable 与 user 构建中的处理不同;
  5. 删除声明后重新构建,确认原访问应转为真正的 -EACCES,再决定最小修复。

再阅读 services.c 的 permissive_map、neveraudit_map 分支和 avc_denied,解释“放行但审计”“放行且不审计”“拒绝且审计”三种结果如何组合。

7. 源码导航 ​

  1. kernel/common/security/selinux/ss/services.c:source type 查表、permissive/neveraudit 标记。
  2. kernel/common/security/selinux/avc.c:AVD_FLAGS_PERMISSIVE 对拒绝返回值和缓存的影响。
  3. kernel/common/security/selinux/hooks.c:task AVC cache hit 时的 enforcement 判断。
  4. system/sepolicy/tools/sepolicy-analyze/perm.c:遍历 binary permissive_map 的分析器。
  5. system/sepolicy/tools/sepolicy-analyze/README:permissive 子命令输入、输出和 user build 边界。
  6. system/sepolicy/build/soong/policy.go:user build 的 permissive domain 检查和 allowlist。
  7. system/core/init/selinux.cpp:全局 enforcing 判定与 security_setenforce。
  8. system/sepolicy/private/su.te:Android 17 真实 permissive domain 声明示例。