Skip to content

AVC Denial 日志

从内核 AVC 审计实现出发,拆解 scontext、tcontext、tclass、permission 和 permissive 字段,并建立从日志回到策略规则的排查路径。

基于android-17.0.0_r1
AndroidSELinuxAVC内核审计调试源码阅读

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

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

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

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

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

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

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 规则。这里的日志行是字段布局示例,不冒充某台设备的采样结果:

text
avc:  denied  { read open } for
    scontext=u:r:my_daemon:s0
    tcontext=u:object_r:vendor_configs_file:s0
    tclass=file permissive=0
日志字段规则位置排查问题
scontext 的 typesource实际发起者是否真是 my_daemon,是否发生 domain transition
tcontext 的 typetarget文件/节点标签是否正确,是否误落到 generic type
tclassclass规则是否写成 file、dir、chr_file 或其他正确 class
{ ... }permissions只缺 read、open,还是缺一组路径遍历权限
permissiveenforcement result本次是否真正阻断,不能替代构建策略检查

对应规则的最小形状是:

text
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_managerservice_contextsservicemanager add/find/list 检查
property_serviceproperty_contextsinit property service audit
binder进程/节点 SIDBinder transaction hook
chr_file/dev 节点 labelueventd/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

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

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 只读采集 ​

sh
# 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 策略查询 ​

sh
# 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

text
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

sh
# 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 字段异常 ​

现象可能原因下一步
没有 scontextSID 转换失败或记录不完整检查 ssid、策略加载状态和 srawcon
没有 tcontexttarget SID 无法转换检查对象生命周期和 trawcon
只有十六进制权限class map 没有对应名称按 tclass 查 permission 定义
没有 permissive不是 denied,或审计记录被截断查看完整 audit 行
permissive=1请求被记录但未阻断不要当作 enforcing 成功
同一 denial 重复很多次cache/audit 位、重复触发或多个进程结合 pid、comm、调用链去重

6.2 规则已存在仍拒绝 ​

当 sesearch 找到匹配 allow 但仍出现 denial,优先检查:

  1. 日志中的 type/class/permission 是否与查询完全一致;
  2. source 或 target 是否是 attribute,条件规则是否在当前 build variant 生效;
  3. 是否受到 MLS constraint、class 权限差异或 extended permission/ ioctl 检查影响;
  4. 查询工具使用的 policy 是否就是设备当前加载的 policy;
  5. denial 是否来自另一个 PID、线程或已变化的对象 label。

这类问题不能用“再加一条 allow”机械解决,因为 allow 规则只是 TE 判定的一部分。

7. 读码练习 ​

拿到一条真实 denial 后,按下面顺序写出结论:

  1. 从 scontext 的 type 找到调用进程当前 domain,并用 ps -Z 验证 PID 是否仍对应它;
  2. 从 tcontext 的 type 和 tclass 找到对象 label 生产者,判断是 file_contexts、property_contexts、service_contexts 还是 Binder/内核对象;
  3. 将 permission 集合映射到 allow source target:class { ... };,说明缺的是路径遍历、对象操作还是 capability;
  4. 根据 permissive 和系统调用返回值判断本次是否真的阻断;
  5. 用 sesearch 验证当前 policy,若命中则继续检查条件、MLS、DAC、对象 class 和 policy 来源。

再回到 avc_has_perm、avc_denied、avc_audit_pre_callback 和 avc_audit_post_callback,解释日志字段为什么分两段生成,以及为什么“无日志”“有 denial”“系统调用失败”是三个不同问题。

8. 源码导航 ​

  1. kernel/common/security/selinux/avc.c:AVC 查询、拒绝结果、审计位图和日志字段生成。
  2. kernel/common/security/selinux/hooks.c:文件/inode 等 LSM hook 如何准备 SELinux 权限检查。
  3. system/core/init/selinux.cpp:libselinux 日志回调和 AVC audit netlink 转发。
  4. system/core/init/selinux.h:init 暴露的 SELinux logging 接口。
  5. system/sepolicy/docs/validation_and_debugging.md:audit2allow、generic label、DAC 和 simpleperf 调试边界。
  6. TE 规则语法:把四元组映射回 allow 规则。
  7. 内核加载流程:确认设备当前 policy 的加载时机和失败边界。