Skip to content

Denial 排查手册

按文件、属性、服务管理器、Binder、genfs、设备节点和 capability 分类,建立从 AVC 到真实源码与正确修复层的判断路径。

基于android-17.0.0_r1
AndroidSELinuxAVCDenial故障排查源码阅读

Denial 排查手册 ​

本文是 SELinux 调试组的场景收束篇。建议先阅读 AVC Denial 日志、audit2allow 工具 和 策略查询工具:本文不再逐字段解释 denial,也不把工具输出当修复模板,而是按 Android 17 中真实的“目标 context 生产者”分类,决定问题应在标签、Unix DAC、策略宏、服务名字空间还是调用代码中修复。

常见 denial 的难点不在于把四元组改写成 allow,而在于确定 tcontext 是谁生成的。普通文件由 file_contexts/xattr/restorecon 决定,property 由 property info trie 决定,ServiceManager 名称由 service_contexts 决定,Binder target 是进程 SID,proc/sysfs 由 genfs 表决定,capability 则是 source domain 对自身特权的检查。生产者不同,修复层也不同。

1. 排查入口 ​

1.1 决策顺序 ​

1.2 基础采集 ​

sh
# Read-only: save complete AVC lines and current source/target labels.
adb shell 'dmesg | grep -E "avc:  denied|avc: denied"'
adb shell 'ps -AZ'
adb shell 'ls -Zd <target-path> <parent-path>'

# Read-only: query the exact source/target/class/permission tuple.
searchpolicy <policy-binary> --allow \
  -s <source-type> -t <target-type> -c <class> -p <permission>

先保存完整日志,再验证当前标签。若日志来自上一次启动或 PID 已复用,当前 ps -Z/ls -Z 可能不同;这种差异本身就是证据,不能强行让查询结果与旧日志对齐。

2. 文件与目录 ​

2.1 generic label ​

当 tcontext 是 device、tmpfs、system_data_file、vendor_file 等大范围类型时,先判断对象是否缺少专用标签。AOSP 调试文档明确禁止因为一次 denial 直接允许 generic device;正确做法通常是给具体设备节点或路径定义 type,再授权最小访问。

源码文件:system/sepolicy/docs/validation_and_debugging.md

text
If a denial shows tcontext=u:object_r:device:s0 (or tmpfs,
system_data_file), do not write an allow rule for device. The correct fix is to
label the specific file or node in file_contexts.

2.2 路径层级 ​

文件访问经常不是只缺 file:read。路径解析可能先检查父目录 search,打开时检查 open,读取 metadata 时检查 getattr,真正 I/O 再检查 read/write。应逐层验证:

sh
# Read-only: inspect every path component, Unix owner/mode, and SELinux label.
adb shell 'namei -l <target-path>'
adb shell 'stat -c "%U %G %a %n" <target-path> <parent-path>'
adb shell 'ls -Zd <target-path> <parent-path>'

若 DAC 已阻止访问,SELinux allow 也不会改变文件 owner/mode;若父目录标签错误,只给目标 file type 增加 read 也不会解决 dir:search。

2.3 创建与transition ​

源码文件:system/sepolicy/public/te_macros

yaml
define(`file_type_trans', `
allow $1 $2:dir ra_dir_perms;
allow $1 $3:notdevfile_class_set create_file_perms;
allow $1 $3:dir create_dir_perms;
')

define(`file_type_auto_trans', `
file_type_trans($1, $2, $3)
type_transition $1 $2:dir $3;
type_transition $1 $2:notdevfile_class_set $3;
')

创建对象需要两部分:允许 source 在父目录写入/添加名字,以及 type_transition 让新对象获得专用 type。只添加 create allow 而没有 transition,新文件可能继续继承父目录的 generic label,后续访问仍会产生 denial。

3. 属性系统 ​

3.1 set与read不同 ​

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

cpp
bool CanReadProperty(const std::string& source_context, const std::string& name) {
    const char* target_context = nullptr;
    property_info_area->GetPropertyInfo(name.c_str(), &target_context, nullptr);

    PropertyAuditData audit_data;
    audit_data.name = name.c_str();
    ucred cr = {.pid = 0, .uid = 0, .gid = 0};
    audit_data.cr = &cr;

    auto lock = std::lock_guard{selinux_check_access_lock};
    return selinux_check_access(source_context.c_str(), target_context,
                                "file", "read", &audit_data) == 0;
}

static bool CheckMacPerms(const std::string& name, const char* target_context,
                          const char* source_context, const ucred& cr) {
    if (!target_context || !source_context)
        return false;
    PropertyAuditData audit_data{.cr = &cr, .name = name.c_str()};
    auto lock = std::lock_guard{selinux_check_access_lock};
    return selinux_check_access(source_context, target_context,
                                "property_service", "set", &audit_data) == 0;
}

读属性使用 target property type 的 file:read,设置属性使用 property_service:set。两者共享 property_info_area 解析出的 target context,但 class/permission 不同。日志中的 property=<name> pid=... 来自 audit callback,先用属性名查 property_contexts,不要只按 comm 猜目标 type。

3.2 宏的完整权限 ​

源码文件:system/sepolicy/public/te_macros

yaml
define(`set_prop', `
unix_socket_connect($1, property, init)
allow $1 $2:property_service set;
get_prop($1, $2)
')

define(`get_prop', `
allow $1 $2:file { getattr open read map };
')

set_prop 不只是 property_service:set:它还包含连接 property socket、与 init 建立 Unix stream 连接,以及读取属性区域。若 denial 依次出现 property_socket:sock_file write、init:unix_stream_socket connectto、foo_prop:property_service set,AOSP 宏注释明确建议使用 set_prop,而不是手写三条零散 allow。

4. 服务名字空间 ​

4.1 context查询 ​

源码文件:frameworks/native/cmds/servicemanager/Access.cpp

cpp
bool Access::actionAllowedFromLookup(const CallingContext& sctx,
                                     const std::string& name,
                                     const char* perm) {
    char* tctx = nullptr;
    if (selabel_lookup(getSehandle(), &tctx, name.c_str(),
                       SELABEL_CTX_ANDROID_SERVICE) != 0) {
        LOG(ERROR) << "SELinux: No match for " << name
                   << " in service_contexts.\n";
        return false;
    }

    bool allowed = actionAllowed(sctx, tctx, perm, name);
    freecon(tctx);
    return allowed;
}

ServiceManager denial 有两层失败:服务名没有匹配 service_contexts 时,根本没有 target context;匹配后才执行 service_manager:{find|add} 检查。第一类应修 context 映射,第二类才查询 allow/neverallow。

4.2 add与find ​

源码文件:frameworks/native/cmds/servicemanager/Access.cpp、system/sepolicy/public/te_macros

cpp
bool Access::actionAllowed(const CallingContext& sctx, const char* tctx,
                           const char* perm, const std::string& tname) {
    const char* tclass = "service_manager";
    AuditCallbackData data{.context = &sctx, .tname = &tname};
    return 0 == selinux_check_access(sctx.sid.c_str(), tctx, tclass, perm,
                                     reinterpret_cast<void*>(&data));
}

actionAllowed 完成运行时检查;策略侧通常用 add_service 同时表达唯一注册者和可发现性,下面继续看宏展开。

yaml
define(`add_service', `
allow $1 $2:service_manager { add find };
neverallow { domain -$1 } $2:service_manager add;
')

add_service(server, service_type) 同时允许 owner add/find,并生成 neverallow 阻止其他 domain 注册同一类型。若客户端只需要获取服务,应给 find,不应调用 add_service;若另一个 server 触发 add denial,通常是服务 ownership 设计错误,而非缺一条普通 allow。

5. Binder调用 ​

5.1 内核hook ​

源码文件:kernel/common/security/selinux/hooks.c

c
static int selinux_binder_transaction(const struct cred *from,
                                      const struct cred *to)
{
    u32 mysid = current_sid();
    u32 fromsid = cred_sid(from);
    u32 tosid = cred_sid(to);
    int rc;

    if (mysid != fromsid) {
        rc = avc_has_perm(mysid, fromsid, SECCLASS_BINDER,
                          BINDER__IMPERSONATE, NULL);
        if (rc)
            return rc;
    }
    return avc_has_perm(fromsid, tosid,
                        SECCLASS_BINDER, BINDER__CALL, NULL);
}

static int selinux_binder_transfer_binder(const struct cred *from,
                                           const struct cred *to)
{
    return avc_has_perm(cred_sid(from), cred_sid(to),
                        SECCLASS_BINDER, BINDER__TRANSFER, NULL);
}

Binder target context 是对端进程 SID,不是 service_contexts type。call denial 检查 caller→callee;传递 Binder 引用时另有 transfer;当前线程凭另一个 cred 发起调用还会检查 impersonate。因此只看到 binder:transfer 时,应检查是否传递了第三方 Binder,而不是盲目补 call。

5.2 宏展开 ​

源码文件:system/sepolicy/public/te_macros

yaml
define(`binder_call', `
allow $1 $2:binder { call transfer };
allow $2 $1:binder transfer;
allow $1 $2:fd use;
')

binder_call(client, server) 包含 client→server 的 call/transfer、server→client 的 reply reference transfer,以及 client 接收 server fd 的 fd:use。是否需要完整宏取决于真实接口:纯名字空间 find 不自动授予 Binder call,Binder call 也不自动授予 ServiceManager find。

6. genfs与设备 ​

6.1 proc与sysfs ​

源码文件:system/sepolicy/private/genfs_contexts

text
genfscon proc /zoneinfo u:object_r:proc_zoneinfo:s0
genfscon proc /pressure/cpu u:object_r:proc_pressure_cpu:s0
genfscon sysfs /class/gpu u:object_r:sysfs_gpu:s0
genfscon sysfs /power/state u:object_r:sysfs_power:s0

proc/sysfs 的 target type 来自 binary policy 的 genfs table,而不是普通磁盘 xattr。denial 显示 generic proc 或 sysfs 时,先判断当前路径是否缺少更具体 genfscon,再给专用 type 授权。可用 AOSP searchpolicy <policy> --genfs 查询编译结果。

6.2 字符设备 ​

设备节点常见 tclass=chr_file,target type 可能来自 file_contexts/ueventd restorecon。排查顺序是节点路径、major/minor、当前 label、创建者和访问宏;不要因为 /dev/foo 是字符设备就授权 generic device:chr_file。

sh
# Read-only: inspect device identity and label.
adb shell 'ls -lZ /dev/<node>'
adb shell 'stat -c "%t:%T %a %U %G %n" /dev/<node>'

7. capability与DAC ​

7.1 全局约束 ​

源码文件:system/sepolicy/private/domain.te

yaml
define(`dac_override_allowed', `{
  apexd artd dnsmasq dumpstate init installd lmkd netd recovery
  tee ueventd uncrypt vendor_init vold zygote
  # Other AOSP-owned domains and build-variant entries are omitted here.
}')

neverallow ~dac_override_allowed
  self:global_capability_class_set dac_override;

真实 allowlist 还包含若干 domain 和 build variant 条件;上面保留关键结构。AOSP 的设计原则写在同一文件:优先把 domain 加入正确 Unix group 或修文件 mode,而不是授予 dac_override。因此 capability denial 应先定位发起的系统调用和 DAC 失败对象,再检查是否属于极少数合理 owner。

7.2 宽特权 ​

sys_admin、sys_ptrace、sys_rawio 等 capability 常受专项 neverallow。看到这类 denial 时,不应从工具输出直接添加 allow;应回答“代码为何需要 mount/ptrace/raw I/O”“能否移到已有高权限 helper”“是否只需更窄的 object permission”。构建 neverallow 失败是架构反馈,不是需要关闭检查的障碍。

8. 回归矩阵 ​

8.1 分层验证 ​

层级验证输入关键断言
标签ps -Z、ls -Z、property/service/genfs 查询source/target 与设计一致
DACnamei、stat、进程 UID/GID不依赖不必要 capability
policydbsearchpolicy、sepolicy-analyzeallow/attribute/neverallow 符合预期
构建m selinux_policyM4、checkpolicy、CIL、neverallow、Treble 通过
运行enforcing 下复现相同操作原 denial 消失且无新增宽泛 denial

8.2 现象与方向 ​

denial 类别首选修复层常见错误修法
generic file/device typecontexts/restoreconallow generic type
property_service:setproperty_contexts + set_prop只加 set,漏 socket/read
service_manager:addservice ownership + add_service给多个 server add
service_manager:findclient find allow错加 binder_call
binder:call/transfer双方 domain 与接口数据流把 find 当 call 或反之
proc/sysfs generic typegenfs_contexts给整个 proc/sysfs 权限
dac_overrideUnix owner/group/mode直接授 capability

9. 读码练习 ​

选择一条真实 denial,先按 tclass 找到本篇对应章节,再完成以下检查:

  1. tcontext 的生产者是 file_contexts、property info、service_contexts、对端进程 SID 还是 genfs?
  2. 失败发生在名字 lookup、SELinux access check、DAC、路径遍历还是 capability?
  3. AOSP 是否已有宏表达完整交互,如 set_prop、add_service、binder_call?
  4. 宏中是否包含额外 allow 或 neverallow,为什么不能拆成单条候选规则?
  5. 修改后,binary 查询、neverallow、Treble 和 enforcing 回归分别证明什么?

10. 源码导航 ​

  1. system/core/init/property_service.cpp:property target context、read/set class 与 audit 数据。
  2. frameworks/native/cmds/servicemanager/Access.cpp:service_contexts lookup 和 add/find/list 检查。
  3. kernel/common/security/selinux/hooks.c:Binder call、transfer、impersonate 的内核 hook。
  4. system/sepolicy/public/te_macros:set_prop、get_prop、binder_call、add_service、file transition。
  5. system/sepolicy/private/genfs_contexts:proc、sysfs 等伪文件系统标签。
  6. system/sepolicy/private/domain.te:capability 全局 neverallow 和 DAC 设计边界。
  7. system/sepolicy/docs/validation_and_debugging.md:generic label、DAC、audit2allow 的官方警告。
  8. AVC Denial 日志:字段生成和 permissive 语义。
  9. 策略查询工具:binary policy 的 allow、attribute 和 genfs 查询。