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 基础采集
# 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
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。应逐层验证:
# 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
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
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
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
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
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 同时表达唯一注册者和可发现性,下面继续看宏展开。
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
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
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
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:s0proc/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。
# 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
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 与设计一致 |
| DAC | namei、stat、进程 UID/GID | 不依赖不必要 capability |
| policydb | searchpolicy、sepolicy-analyze | allow/attribute/neverallow 符合预期 |
| 构建 | m selinux_policy | M4、checkpolicy、CIL、neverallow、Treble 通过 |
| 运行 | enforcing 下复现相同操作 | 原 denial 消失且无新增宽泛 denial |
8.2 现象与方向
| denial 类别 | 首选修复层 | 常见错误修法 |
|---|---|---|
| generic file/device type | contexts/restorecon | allow generic type |
property_service:set | property_contexts + set_prop | 只加 set,漏 socket/read |
service_manager:add | service ownership + add_service | 给多个 server add |
service_manager:find | client find allow | 错加 binder_call |
binder:call/transfer | 双方 domain 与接口数据流 | 把 find 当 call 或反之 |
| proc/sysfs generic type | genfs_contexts | 给整个 proc/sysfs 权限 |
dac_override | Unix owner/group/mode | 直接授 capability |
9. 读码练习
选择一条真实 denial,先按 tclass 找到本篇对应章节,再完成以下检查:
tcontext的生产者是 file_contexts、property info、service_contexts、对端进程 SID 还是 genfs?- 失败发生在名字 lookup、SELinux access check、DAC、路径遍历还是 capability?
- AOSP 是否已有宏表达完整交互,如
set_prop、add_service、binder_call? - 宏中是否包含额外 allow 或 neverallow,为什么不能拆成单条候选规则?
- 修改后,binary 查询、neverallow、Treble 和 enforcing 回归分别证明什么?
10. 源码导航
system/core/init/property_service.cpp:property target context、read/set class 与 audit 数据。frameworks/native/cmds/servicemanager/Access.cpp:service_contexts lookup 和 add/find/list 检查。kernel/common/security/selinux/hooks.c:Binder call、transfer、impersonate 的内核 hook。system/sepolicy/public/te_macros:set_prop、get_prop、binder_call、add_service、file transition。system/sepolicy/private/genfs_contexts:proc、sysfs 等伪文件系统标签。system/sepolicy/private/domain.te:capability 全局 neverallow 和 DAC 设计边界。system/sepolicy/docs/validation_and_debugging.md:generic label、DAC、audit2allow 的官方警告。- AVC Denial 日志:字段生成和 permissive 语义。
- 策略查询工具:binary policy 的 allow、attribute 和 genfs 查询。
