AVC Denial 日志
本文面向已经知道 SELinux domain/type 和 allow 规则的读者。前置可读 TE 规则语法 和 内核加载流程;前者解释规则三元组,后者解释策略何时进入内核。本文不把 audit2allow 输出当作修复方案,也不重复 selinux.cpp 的回调实现,而是回答一个更窄、也更实用的问题:一条 avc: denied 是由哪段内核代码拼出来的,字段分别代表什么,怎样从字段定位到真正的策略缺口。
AVC(Access Vector Cache)日志不是“某个文件拒绝了访问”的高层异常,而是一次 SELinux 权限判定的审计结果。内核先用 source SID、target SID、object class 和请求权限查询 AVC;发生 cache miss 时再让 security server 计算决策;只有审计位要求记录时,才由 slow_avc_audit 生成日志。日志中的 permissive=1 说明本次请求被放行但仍被记录,不能直接当作 enforcing 行为。
1. 日志来源
1.1 判定入口
源码文件:kernel/common/security/selinux/avc.c
/**
* avc_has_perm - Check permissions and perform any appropriate auditing.
* @ssid: source security identifier
* @tsid: target security identifier
* @tclass: target security class
* @requested: requested permissions, interpreted based on @tclass
*/
int avc_has_perm(u32 ssid, u32 tsid, u16 tclass,
u32 requested, struct common_audit_data *auditdata)
{
struct av_decision avd;
int rc, rc2;
rc = avc_has_perm_noaudit(ssid, tsid, tclass, requested, 0, &avd);
rc2 = avc_audit(ssid, tsid, tclass, requested, &avd, rc, auditdata);
if (rc2)
return rc2;
return rc;
}avc_has_perm 同时负责“得到访问结果”和“按策略审计”。avc_has_perm_noaudit 返回权限判定,avc_audit 决定是否记录;因此没有日志不等于没有检查,可能只是 dontaudit 或审计位没有要求输出。返回值为 0 表示请求权限全部放行,-EACCES 表示至少一项权限被拒绝,其他 errno 表示检查过程本身出错。
1.2 cache miss与拒绝
源码文件: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 强制返回拒绝;enforcing 且决策没有 AVD_FLAGS_PERMISSIVE 时返回 -EACCES;permissive 决策则更新 AVC 节点为 granted 并返回 0。后者解释了为什么 permissive 模式仍能看到 denial,但业务系统调用可能继续成功。审计字段和系统调用返回值必须分开判断。
1.3 日志路径
对普通文件访问,LSM hook 可能直接调用 avc_has_perm,也可能走 inode task AVC 的快速路径;两者最终都使用 avc_audit 的字段语义。Android init 的回调主要处理用户态 libselinux 日志,内核原生 AVC 则由 audit 子系统直接格式化。不要把所有 avc: denied 都想象成由 init 拼接。
2. 字段生成
2.1 拒绝与权限
源码文件:kernel/common/security/selinux/avc.c
static void avc_audit_pre_callback(struct audit_buffer *ab, void *a)
{
struct common_audit_data *ad = a;
struct selinux_audit_data *sad = ad->selinux_audit_data;
u32 av = sad->audited, perm;
const char *const *perms;
u32 i;
audit_log_format(ab, "avc: %s ", sad->denied ? "denied" : "granted");
if (av == 0) {
audit_log_format(ab, " null");
return;
}
perms = secclass_map[sad->tclass - 1].perms;
audit_log_format(ab, " {");
i = 0;
perm = 1;
while (i < (sizeof(av) * 8)) {
if ((perm & av) && perms[i]) {
audit_log_format(ab, " %s", perms[i]);
av &= ~perm;
}
i++;
perm <<= 1;
}
if (av)
audit_log_format(ab, " 0x%x", av);
audit_log_format(ab, " } for ");
}audited 是审计位图,不一定等于原始 requested。内核按 tclass 找到权限名称表,把已知 bit 转成 read、open 等名字;没有名称的剩余 bit 以十六进制输出。日志里的 { read write } 因而表示“被审计的权限集合”,不是任意字符串,也不是一定包含所有业务请求。
源码文件:kernel/common/security/selinux/avc.c
static inline u32 avc_xperms_audit_required(u32 requested,
struct av_decision *avd,
struct extended_perms_decision *xpd,
u8 perm, int result, u32 *deniedp)
{
u32 denied, audited;
denied = requested & ~avd->allowed;
if (unlikely(denied)) {
audited = denied & avd->auditdeny;
if (audited && xpd &&
avc_xperms_has_perm(xpd, perm, XPERMS_DONTAUDIT)) {
audited = 0;
}
} else if (result) {
audited = denied = requested;
} else {
audited = requested & avd->auditallow;
}
*deniedp = denied;
return audited;
}这个函数把 requested、策略允许位 allowed、auditdeny/auditallow 和系统调用结果合成两个不同的位图:denied 表示拒绝了什么,返回值 audited 表示要记录什么。dontaudit 可以让拒绝仍然存在但不产生日志;反过来,成功请求也可能因 auditallow 被记录。因此排查“没有 AVC”时,不能只看 allow,还要检查审计规则。
2.2 上下文与class
源码文件:kernel/common/security/selinux/avc.c
static void avc_audit_post_callback(struct audit_buffer *ab, void *a)
{
struct common_audit_data *ad = a;
struct selinux_audit_data *sad = ad->selinux_audit_data;
char *scontext = NULL;
char *tcontext = NULL;
const char *tclass = NULL;
u32 scontext_len;
u32 tcontext_len;
int rc;
rc = security_sid_to_context(sad->ssid, &scontext, &scontext_len);
if (rc)
audit_log_format(ab, " ssid=%d", sad->ssid);
else
audit_log_format(ab, " scontext=%s", scontext);
rc = security_sid_to_context(sad->tsid, &tcontext, &tcontext_len);
if (rc)
audit_log_format(ab, " tsid=%d", sad->tsid);
else
audit_log_format(ab, " tcontext=%s", tcontext);
tclass = secclass_map[sad->tclass - 1].name;
audit_log_format(ab, " tclass=%s", tclass);
if (sad->denied)
audit_log_format(ab, " permissive=%u", sad->result ? 0 : 1);
}ssid/tsid 是内核内部整数 SID,日志优先转换为安全上下文;转换失败时才退回打印数字 SID。security_sid_to_context 成功后,scontext 的 type 通常是 source domain,tcontext 的 type 是对象标签。tclass 从 class map 取名称,例如 file、dir、binder 或 property_service。permissive 只在 sad->denied 为真时追加,且由最终 result 反推:result 非零是实际拒绝,result 为零是 permissive 放行。
2.3 原始上下文
源码文件:kernel/common/security/selinux/avc.c
/* In case of invalid context, report the actual context string. */
rc = security_sid_to_context_inval(sad->ssid, &scontext, &scontext_len);
if (!rc && scontext) {
if (scontext_len && scontext[scontext_len - 1] == '\0')
scontext_len--;
audit_log_format(ab, " srawcon=");
audit_log_n_untrustedstring(ab, scontext, scontext_len);
kfree(scontext);
}
rc = security_sid_to_context_inval(sad->tsid, &scontext, &scontext_len);
if (!rc && scontext) {
if (scontext_len && scontext[scontext_len - 1] == '\0')
scontext_len--;
audit_log_format(ab, " trawcon=");
audit_log_n_untrustedstring(ab, scontext, scontext_len);
kfree(scontext);
}srawcon/trawcon 不是普通 denial 的必有字段,主要用于 SID 转换或上下文有效性异常时报告原始字符串。看到它们时,先怀疑标签无效、策略与对象状态不同步或上下文转换失败,不要只按常规 scontext/tcontext 解释。
2.4 记录时序
字段并不是在同一个函数里一次性计算:权限名称来自 pre callback,SID 到 context 的转换和 permissive 来自 post callback。这个分段解释了某些日志只有数字 SID、只有十六进制权限或缺少 permissive 的原因。
3. 字段到策略
3.1 四元组映射
一条常见 denial 的核心字段可以按下面的关系回到 TE 规则。这里的日志行是字段布局示例,不冒充某台设备的采样结果:
avc: denied { read open } for
scontext=u:r:my_daemon:s0
tcontext=u:object_r:vendor_configs_file:s0
tclass=file permissive=0| 日志字段 | 规则位置 | 排查问题 |
|---|---|---|
scontext 的 type | source | 实际发起者是否真是 my_daemon,是否发生 domain transition |
tcontext 的 type | target | 文件/节点标签是否正确,是否误落到 generic type |
tclass | class | 规则是否写成 file、dir、chr_file 或其他正确 class |
{ ... } | permissions | 只缺 read、open,还是缺一组路径遍历权限 |
permissive | enforcement result | 本次是否真正阻断,不能替代构建策略检查 |
对应规则的最小形状是:
allow my_daemon vendor_configs_file:file { read open };但“把这行 allow 加上”不是固定答案。若 tcontext 是 device、tmpfs 或其他过于通用的标签,正确修复可能是先在 file_contexts 中给具体路径打标签;若 denial 是 dac_override,应先修复 Unix owner/mode,而不是直接授予 capability。AOSP 的调试文档明确警告不能盲目复制 audit2allow 输出。
3.2 路径上下文
同一进程访问一个目录时,常见 denial 可能先后出现 dir:search、目标目录的 dir:read、文件的 file:open 和 file:read。这不是四条互相独立的业务需求,而是 Linux 路径解析和 SELinux class 检查的多个对象层级。应先确认路径上每一级目录的 label 和 Unix 权限,再决定策略是否需要一组最小权限。
3.3 非文件对象
service_manager、property_service、binder 等对象没有 inode 路径,不能套用“查 file_contexts”的排查习惯:
tclass | 目标 type 来源 | 典型源码入口 |
|---|---|---|
service_manager | service_contexts | servicemanager add/find/list 检查 |
property_service | property_contexts | init property service audit |
binder | 进程/节点 SID | Binder transaction hook |
chr_file | /dev 节点 label | ueventd/file_contexts + 设备节点访问 |
字段映射仍然是 source type → target type → class → permission,只是 target type 的生产者不同。看到 tclass=property_service 时去改 file_contexts,不会影响属性对象的 label。
4. Android转发
4.1 init回调
源码文件:system/core/init/selinux.cpp
int SelinuxKlogCallback(int type, const char* fmt, ...) {
android::base::LogSeverity severity = android::base::ERROR;
if (type == SELINUX_WARNING) {
severity = android::base::WARNING;
} else if (type == SELINUX_INFO) {
severity = android::base::INFO;
}
char buf[kKlogMessageSize];
va_list ap;
va_start(ap, fmt);
int length_written = vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
if (length_written <= 0)
return 0;
// libselinux may include a newline; Android logging supplies its own.
size_t str_len = strlen(buf);
if (buf[str_len - 1] == '\n')
buf[str_len - 1] = '\0';
if (type == SELINUX_AVC) {
SelinuxAvcLog(buf);
} else {
android::base::KernelLogger(android::base::MAIN, severity,
"selinux", nullptr, 0, buf);
}
return 0;
}这段代码消费 libselinux 回调,不是内核 avc_audit_post_callback 的替代品。普通 warning/info/error 进入 KernelLogger;SELINUX_AVC 进入 SelinuxAvcLog,由后者构造 AUDIT_USER_AVC netlink 消息。固定 1024 字节 buffer 还意味着超长用户态消息可能被截断,排查时应保留原始 kernel audit 行。
4.2 AVC转发
源码文件:system/core/init/selinux.cpp
void SelinuxAvcLog(char* buf) {
struct NetlinkMessage {
nlmsghdr hdr;
char buf[kKlogMessageSize];
} request = {};
request.hdr.nlmsg_flags = NLM_F_REQUEST;
request.hdr.nlmsg_type = AUDIT_USER_AVC;
request.hdr.nlmsg_len = sizeof(request);
strlcpy(request.buf, buf, sizeof(request.buf));
auto fd = unique_fd{socket(PF_NETLINK, SOCK_RAW | SOCK_CLOEXEC,
NETLINK_AUDIT)};
if (!fd.ok())
return;
TEMP_FAILURE_RETRY(send(fd.get(), &request, sizeof(request), 0));
}socket 创建失败时函数直接返回,日志转发失败不会改变原始 SELinux 判定;unique_fd 在函数退出时关闭 socket。因而“设备上没看到 logcat denial”不能直接证明“内核没有 denial”,还要检查 kmsg、audit buffer 和日志转发条件。
5. 排查方法
5.1 只读采集
# Read-only: capture recent kernel and Android SELinux records.
adb shell 'dmesg | grep -E "avc: denied|avc: denied|SELinux"'
adb logcat -b all -d -v threadtime | grep -E 'avc: denied|avc: denied|SELinux'
# Read-only: inspect the live labels of the source process and target path.
adb shell 'ps -AZ | grep my_daemon'
adb shell 'ls -Zd /vendor/etc/my_config /vendor/etc'先保存完整行,不要只复制 { permission }。ps -Z 提供进程当前 domain,ls -Z 提供对象当前 label;如果它们与 denial 的 scontext/tcontext 不同,说明你正在看另一次访问、标签已变化或日志来自旧启动。
5.2 策略查询
# Read-only: search allow/audit rules for the exact four-tuple.
sesearch -A -s my_daemon -t vendor_configs_file -c file
sesearch -A -s my_daemon -t vendor_configs_file -c file -p read
# Read-only: inspect attributes that may expand to the logged source/target type.
seinfo -a -x | grep -E 'my_daemon|vendor_configs_file'sesearch 查到 allow 只能证明加载到查询工具的 policy 中存在匹配规则;若设备仍拒绝,继续检查条件规则、attribute 展开、MLS constraint、neverallow、对象实际 class 和是否查询了正确 policy 文件。没有匹配规则时,先确认 type 是否写错或对象 label 是否泛化。
5.3 audit2allow边界
源码文件:system/sepolicy/docs/validation_and_debugging.md
Never blindly copy audit2allow output into policy. It suggests rules to make
the denial go away, not to make the system secure.
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.
If a dac_override denial occurs, do NOT grant the capability. Fix the underlying
Unix permissions of the file or process.这段官方调试说明给出两个反例:generic label 说明标签建模可能有问题,dac_override 说明 Unix DAC 可能先失败。audit2allow 是候选规则生成器,不知道你的服务调用方、数据 owner、生命周期和最小权限目标,不能替代源码阅读。
5.4 调用链采样
源码文件:system/sepolicy/docs/validation_and_debugging.md
# Requires a rooted/debuggable device and Linux >= 5.10 for this event path.
adb shell -t "cd /data/local/tmp && su root simpleperf record -a -g -e avc:selinux_audited"
# Trigger the access, stop recording, then inspect the full call graph.
adb shell -t "cd /data/local/tmp && su root simpleperf report -g --full-callgraph"当一条 denial 只能告诉你“谁和什么对象”,却不能告诉你“哪条业务调用触发”,AOSP 文档建议用 avc:selinux_audited 事件采样调用链。它证明的是触发路径和时间关系,不会自动证明应该增加哪条 allow。
6. 失败边界
6.1 字段异常
| 现象 | 可能原因 | 下一步 |
|---|---|---|
没有 scontext | SID 转换失败或记录不完整 | 检查 ssid、策略加载状态和 srawcon |
没有 tcontext | target SID 无法转换 | 检查对象生命周期和 trawcon |
| 只有十六进制权限 | class map 没有对应名称 | 按 tclass 查 permission 定义 |
没有 permissive | 不是 denied,或审计记录被截断 | 查看完整 audit 行 |
permissive=1 | 请求被记录但未阻断 | 不要当作 enforcing 成功 |
| 同一 denial 重复很多次 | cache/audit 位、重复触发或多个进程 | 结合 pid、comm、调用链去重 |
6.2 规则已存在仍拒绝
当 sesearch 找到匹配 allow 但仍出现 denial,优先检查:
- 日志中的 type/class/permission 是否与查询完全一致;
- source 或 target 是否是 attribute,条件规则是否在当前 build variant 生效;
- 是否受到 MLS constraint、class 权限差异或 extended permission/ ioctl 检查影响;
- 查询工具使用的 policy 是否就是设备当前加载的 policy;
- denial 是否来自另一个 PID、线程或已变化的对象 label。
这类问题不能用“再加一条 allow”机械解决,因为 allow 规则只是 TE 判定的一部分。
7. 读码练习
拿到一条真实 denial 后,按下面顺序写出结论:
- 从
scontext的 type 找到调用进程当前 domain,并用ps -Z验证 PID 是否仍对应它; - 从
tcontext的 type 和tclass找到对象 label 生产者,判断是 file_contexts、property_contexts、service_contexts 还是 Binder/内核对象; - 将 permission 集合映射到
allow source target:class { ... };,说明缺的是路径遍历、对象操作还是 capability; - 根据
permissive和系统调用返回值判断本次是否真的阻断; - 用
sesearch验证当前 policy,若命中则继续检查条件、MLS、DAC、对象 class 和 policy 来源。
再回到 avc_has_perm、avc_denied、avc_audit_pre_callback 和 avc_audit_post_callback,解释日志字段为什么分两段生成,以及为什么“无日志”“有 denial”“系统调用失败”是三个不同问题。
8. 源码导航
kernel/common/security/selinux/avc.c:AVC 查询、拒绝结果、审计位图和日志字段生成。kernel/common/security/selinux/hooks.c:文件/inode 等 LSM hook 如何准备 SELinux 权限检查。system/core/init/selinux.cpp:libselinux 日志回调和 AVC audit netlink 转发。system/core/init/selinux.h:init 暴露的 SELinux logging 接口。system/sepolicy/docs/validation_and_debugging.md:audit2allow、generic label、DAC 和 simpleperf 调试边界。- TE 规则语法:把四元组映射回 allow 规则。
- 内核加载流程:确认设备当前 policy 的加载时机和失败边界。
