Skip to content

Latest commit

 

History

History
2200 lines (1784 loc) · 84.7 KB

File metadata and controls

2200 lines (1784 loc) · 84.7 KB

Linux 内核 Audit 审计子系统深度分析

基于 Linux 内核源码深度分析,覆盖 kernel/audit.ckernel/auditsc.ckernel/auditfilter.ckernel/audit_watch.cinclude/linux/audit.h


目录

  1. 概述与设计目标
  2. 整体架构
  3. 核心数据结构
  4. 审计规则系统
  5. 系统调用审计
  6. 文件审计
  7. Netlink 接口
  8. SELinux AVC 审计
  9. 实时事件丢失处理
  10. 用户态工具与内核的交互
  11. 初始化流程
  12. 审计记录格式与完整事件示例
  13. 性能设计要点

1. 概述与设计目标

Linux Audit 子系统是内核级安全事件记录框架,最初由 Red Hat 的 Rickard E. (Rik) Faith 于 2003-2004 年开发,用于满足 Common Criteria(CC)EAL4+ 认证要求,在 Linux 2.6.6 合入主线。它被广泛用于满足 PCI-DSS、HIPAA、SOX、FISMA 等合规要求。

kernel/audit.c 开头(第 12-28 行)明确列出了六大设计目标:

Goals: 1) 与安全模块(LSM/SELinux)完全集成
       2) 最小运行时开销:
          a) audit_enable=0 时几乎零开销
          b) 启用但不生成记录时开销极小(延迟工作至记录生成时):
             i)   context 已分配
             ii)  getname 返回的名字无需复制,直接存储引用
             iii) inode 信息从 path_lookup 存储
       3) 可在启动时通过 audit=0 禁用
       4) 供内核其他部分使用(调用 audit_log* 自动生成 syscall 记录)
       5) Netlink 用户空间接口
       6) 支持低开销内核侧过滤(kernel-based filtering)

与 syslog 的本质区别:syslog 依赖应用程序自愿上报,可被绕过。Audit 挂钩在 syscall 进入/退出等内核路径,无法被用户态程序规避。loginuid(auid)在登录时通过 PAM 设置后不可更改,即使 sudo/su 切换身份也保持不变,实现真正的"追人不追号"。

核心源码文件一览:

文件 功能 规模
kernel/audit.c 核心:kauditd 线程、消息队列、Netlink 收发、audit_buffer ~2700 行
kernel/auditsc.c 系统调用审计:context 生命周期、记录格式化 ~3000 行
kernel/auditfilter.c 规则过滤引擎:解析、匹配、添加/删除规则 ~1100 行
kernel/audit_watch.c 单文件/目录 inode watch,基于 fsnotify ~450 行
kernel/audit_tree.c 目录树递归 watch ~900 行
kernel/audit_fsnotify.c 精确路径 fsnotify mark ~200 行
include/linux/audit.h 内核内部接口、audit_context、audit_krule 定义 ~600 行
include/uapi/linux/audit.h 用户空间 API:AUDIT_* 消息类型、规则字段常量 ~500 行

2. 整体架构

 用户空间 (User Space)
 +------------------------------------------------------------------------+
 |                                                                        |
 |   auditctl          ausearch / aureport          auditd               |
 |   (规则管理)         (日志查询与报告)             (守护进程)            |
 |      |                     |                       ^   |               |
 |      | AUDIT_ADD_RULE       |                       |   | AUDIT_SET    |
 |      | AUDIT_DEL_RULE       |                       |   | (注册 PID)   |
 |      | AUDIT_LIST_RULES     |                       |   |              |
 |      | AUDIT_SET/GET        |                       |   |              |
 +------|---------------------|---via NETLINK_AUDIT----|---|--+           |
        |                     |                        |   |              |
 =======|=====================|========================|===|==============|
        |                     |   Netlink 套接字        |   |              |
 +------|---------------------|------------------------+---|--------------|
 |      v                     v    内核空间              v   |              |
 |   audit_receive()      (多播监听)              kauditd_thread          |
 |   audit_receive_msg()   CAP_AUDIT_READ         (内核线程,kauditd)     |
 |      |                                              ^   |              |
 |      | AUDIT_ADD_RULE                               |   |              |
 |      v                                          唤醒 |   | 出队发送     |
 |   audit_rule_change()                               |   v              |
 |   audit_data_to_entry()                        +----+---+----+         |
 |   audit_add_rule()                             | audit_queue  |        |
 |                                                | audit_retry_q|        |
 |   +-------------------------------------------| audit_hold_q |        |
 |   |         过滤规则                            +------^-------+        |
 |   | audit_filter_list[8]    audit_inode_hash[32]      |               |
 |   |   TASK/EXIT/FS/USER...    inode 快速定位          |               |
 |   +---+---------------------------------------+       |               |
 |       |                                       |       |               |
 |       | 系统调用钩子                           |       |               |
 |   +---v-----------------------------------+   |       |               |
 |   | per-task: task_struct->audit_context  |   |       |               |
 |   |   __audit_syscall_entry()             |   |       |               |
 |   |   __audit_syscall_exit()              +---+       |               |
 |   |   __audit_inode()                     |           |               |
 |   |   __audit_getname()                   |           |               |
 |   +---------------------------------------+           |               |
 |                                                       |               |
 |   +---------------------------------------------------+               |
 |   | audit_log_start() -> audit_buffer_alloc()                        |
 |   | audit_log_format() -> skb 格式化                                 |
 |   | audit_log_end() -> skb_queue_tail(&audit_queue)                  |
 |   +-------------------------------------------------------------------+|
 |                                                                        |
 |   LSM/SELinux 钩子: avc_audit() -> audit_log_start(AUDIT_AVC)        |
 +------------------------------------------------------------------------+
                          Linux 内核

核心设计原则:推迟至记录生成时。syscall 进入时仅记录 major 号、4 个参数和时间戳;只有在 syscall 退出时规则匹配确认需要记录,才执行重量级的格式化和写出工作。


3. 核心数据结构

3.1 audit_context:每次系统调用的审计上下文

audit_context 是系统调用审计的枢纽结构,定义于 kernel/audit.h 第 109-217 行,通过 task_struct->audit_context 指针与每个任务关联。

/* kernel/audit.h: 109-217 */
struct audit_context {
    int            dummy;        /* 必须是第一个字段。
                                  * dummy=1 时 audit_dummy_context() 快速返回 */
    enum {
        AUDIT_CTX_UNUSED,        /* 未在 syscall/uring 中 */
        AUDIT_CTX_SYSCALL,       /* 正处于系统调用中 */
        AUDIT_CTX_URING,         /* 正处于 io_uring 操作中 */
    } context;
    enum audit_state   state;         /* 持久状态(从任务创建时确定) */
    enum audit_state   current_state; /* 当前有效状态(过滤后可升级) */
    struct audit_stamp stamp;         /* (时间戳, 序列号) 事件唯一标识 */
    int            major;             /* 系统调用号 */
    int            uring_op;          /* io_uring 操作码 */
    unsigned long  argv[4];           /* 系统调用前 4 个参数(a0-a3) */
    long           return_code;       /* 系统调用返回值 */
    u64            prio;              /* 规则优先级(多规则时最高优先级生效) */
    int            return_valid;      /* AUDITSC_INVALID/SUCCESS/FAILURE */

    /* 文件名列表:前 AUDIT_NAMES(=5) 个来自预分配槽,超出时动态分配 */
    struct audit_names preallocated_names[AUDIT_NAMES];
    int            name_count;        /* names_list 的总长度 */
    struct list_head names_list;      /* audit_names->list 锚点 */

    char           *filterkey;        /* 触发此记录的规则 -k key */
    struct path    pwd;               /* 当前工作目录(AUDIT_CWD 记录用) */
    struct audit_aux_data *aux;       /* 辅助数据链:IPC/capset/mmap 等 */
    struct audit_aux_data *aux_pids;  /* 信号目标 PID 辅助数据 */
    struct sockaddr_storage *sockaddr; /* bind/connect 的目标地址 */
    size_t         sockaddr_len;

    /* 任务凭证快照(syscall 进入时采集) */
    pid_t          ppid;
    kuid_t         uid, euid, suid, fsuid;
    kgid_t         gid, egid, sgid, fsgid;
    unsigned long  personality;
    int            arch;              /* CPU 架构(AUDIT_ARCH_X86_64 等) */

    /* ptrace/signal 目标进程信息 */
    pid_t          target_pid;
    kuid_t         target_auid;
    kuid_t         target_uid;
    unsigned int   target_sessionid;
    struct lsm_prop target_ref;
    char           target_comm[TASK_COMM_LEN];

    /* 目录树引用(audit_tree watch 使用) */
    struct audit_tree_refs *trees, *first_trees;
    struct list_head killed_trees;
    int            tree_count;

    /* 子类型 union,仅一个 type 有效 */
    int type;
    union {
        struct { int nargs; long args[6]; } socketcall;
        struct { kuid_t uid; kgid_t gid; umode_t mode;
                 struct lsm_prop oprop; int has_perm;
                 uid_t perm_uid; gid_t perm_gid;
                 umode_t perm_mode; unsigned long qbytes; } ipc;
        struct { mqd_t mqdes; struct mq_attr mqstat; } mq_getsetattr;
        struct { mqd_t mqdes; int sigev_signo; } mq_notify;
        struct { mqd_t mqdes; size_t msg_len;
                 unsigned int msg_prio;
                 struct timespec64 abs_timeout; } mq_sendrecv;
        struct { int oflag; umode_t mode; struct mq_attr attr; } mq_open;
        struct { pid_t pid; struct audit_cap_data cap; } capset;
        struct { int fd; int flags; } mmap;
        struct open_how openat2;
        struct { int argc; } execve;
        struct { const char *name; } module;
        struct { struct audit_ntp_data ntp_data;
                 struct timespec64 tk_injoffset; } time;
    };
    int            fds[2];            /* pipe/socketpair 的 fd 对 */
    struct audit_proctitle proctitle; /* 进程命令行(AUDIT_PROCTITLE 用) */
};

audit_state 枚举kernel/audit.h 第 28-42 行)控制三种行为模式:

enum audit_state {
    AUDIT_STATE_DISABLED, /* 不创建 per-task context,无 syscall 级记录 */
    AUDIT_STATE_BUILD,    /* 创建并填充 context;
                           * 仅在其他内核代码触发时才写出记录 */
    AUDIT_STATE_RECORD,   /* 始终在 syscall 退出时写出完整审计记录 */
};

dummy 字段的优化意义(include/linux/audit.h 第 342-346 行):

static inline bool audit_dummy_context(void)
{
    void *p = audit_context();
    return !p || *(int *)p;  /* dummy 是第一个字段,非零则跳过所有开销 */
}

几乎所有公开包装函数都以此作为快速跳过的条件:

static inline void audit_getname(struct filename *name)
{
    if (unlikely(!audit_dummy_context()))
        __audit_getname(name);
}

audit_reset_context() 函数(kernel/auditsc.c 第 972-1029 行)在每次 syscall 退出后将 context 重置为初始状态,以便下次 syscall 复用,避免重复分配/释放的开销。

3.2 audit_buffer 与事件记录机制

audit_buffer 定义于 kernel/audit.c 第 209-215 行,是单条审计消息的格式化容器:

/* kernel/audit.c: 209-215 */
struct audit_buffer {
    struct sk_buff      *skb;      /* 当前正在写入的 skb(Netlink 消息) */
    struct sk_buff_head skb_list;  /* 已格式化完毕、待发送的 skb 列表 */
    struct audit_context *ctx;     /* 关联的 syscall 上下文(可为 NULL) */
    struct audit_stamp  stamp;     /* 此记录的时间戳+序列号 */
    gfp_t               gfp_mask; /* 内存分配标志 */
};

audit_buffer 通过 slab 缓存(audit_buffer_cachekernel/audit.c 第 157、1736 行)管理,避免频繁的内存分配。audit_buffer_alloc() 实现(kernel/audit.c 第 1828-1856 行):

static struct audit_buffer *audit_buffer_alloc(struct audit_context *ctx,
                                               gfp_t gfp_mask, int type)
{
    struct audit_buffer *ab;

    ab = kmem_cache_alloc(audit_buffer_cache, gfp_mask);
    skb_queue_head_init(&ab->skb_list);

    /* 分配 AUDIT_BUFSIZ(=1024) 字节的 skb,写入 nlmsghdr */
    ab->skb = nlmsg_new(AUDIT_BUFSIZ, gfp_mask);
    skb_queue_tail(&ab->skb_list, ab->skb);
    nlmsg_put(ab->skb, 0, 0, type, 0, 0);

    ab->ctx = ctx;
    ab->gfp_mask = gfp_mask;
    return ab;
}

audit_log_start() 的完整流程(kernel/audit.c 第 1906-1974 行):

audit_log_start(ctx, gfp_mask, type):
  1. 检查 audit_initialized == AUDIT_INITIALIZED
  2. 检查 AUDIT_FILTER_EXCLUDE 链(排除过滤)
  3. 背压等待逻辑(若 audit_queue 超出 audit_backlog_limit)
  4. audit_buffer_alloc(ctx, gfp_mask, type)
  5. audit_get_stamp(ctx, &ab->stamp)   # 从 ctx 继承时间戳,或生成新的
  6. if ctx: ctx->dummy = 0             # 取消 dummy 状态,确保 syscall 被记录
  7. audit_log_format(ab, "audit(%llu.%03lu:%u): ",
                      stamp.ctime.tv_sec,
                      stamp.ctime.tv_nsec/1000000,
                      stamp.serial)
  8. return ab

固定消息头格式:audit(1710000000.123:42): (Unix时间秒.毫秒:序列号)

audit_log_end() 实现(kernel/audit.c):

void audit_log_end(struct audit_buffer *ab)
{
    struct sk_buff *skb;

    /* 将所有格式化后的 skb 加入主发送队列 */
    while ((skb = skb_dequeue(&ab->skb_list)) != NULL) {
        skb_queue_tail(&audit_queue, skb);
    }
    /* 唤醒 kauditd 线程处理队列 */
    wake_up_interruptible(&kauditd_wait);
    /* 释放 audit_buffer 回 slab */
    kmem_cache_free(audit_buffer_cache, ab);
}

audit_log_vformat() 在写入 skb 时,若空间不足会调用 audit_expand()kernel/audit.c 第 1984-1998 行)动态扩展 skb:

static inline int audit_expand(struct audit_buffer *ab, int extra)
{
    struct sk_buff *skb = ab->skb;
    int oldtail = skb_tailroom(skb);
    int ret = pskb_expand_head(skb, 0, extra, ab->gfp_mask);
    /* ... */
    return newtail;
}

3.3 audit_names:文件名记录

audit_names 定义于 kernel/audit.h 第 72-95 行,记录一次 syscall 中访问的单个文件元数据:

/* kernel/audit.h: 72-95 */
struct audit_names {
    struct list_head list;       /* 挂在 audit_context->names_list */
    struct filename  *name;      /* 来自 getname() 的文件名引用 */
    int              name_len;   /* 日志中记录的字符数(-1=完整路径) */
    bool             hidden;     /* 不记录此条目(如父目录项) */
    unsigned long    ino;        /* inode 号 */
    dev_t            dev;        /* 设备号(主:次) */
    umode_t          mode;       /* 文件权限位(S_IFREG | 0644 等) */
    kuid_t           uid;        /* 文件属主 UID */
    kgid_t           gid;        /* 文件属主 GID */
    dev_t            rdev;       /* 字符/块设备的实际设备号 */
    struct lsm_prop  oprop;      /* LSM 对象安全属性(SELinux context 等) */
    struct audit_cap_data fcap;  /* 文件能力(capability) */
    unsigned int     fcap_ver;   /* 文件能力版本 */
    unsigned char    type;       /* 记录类型:AUDIT_TYPE_{NORMAL,PARENT,...} */
    bool             should_free; /* 是否需要 kfree(动态分配,非预分配) */
};

4 种记录类型(include/linux/audit.h 第 132-136 行):

#define AUDIT_TYPE_UNKNOWN      0  /* 未知 */
#define AUDIT_TYPE_NORMAL       1  /* 正常访问(目标文件) */
#define AUDIT_TYPE_PARENT       2  /* 父目录记录 */
#define AUDIT_TYPE_CHILD_DELETE 3  /* 被删除的子文件 */
#define AUDIT_TYPE_CHILD_CREATE 4  /* 被创建的子文件 */

预分配优化:audit_context 内嵌 AUDIT_NAMES=5 个槽(preallocated_names[5]),避免大多数情况下的动态分配。仅当访问文件数超过 5 个时,才通过 kzalloc() 动态分配新的 audit_names,并设置 should_free=true

3.4 audit_stamp:时间戳与序列号

/* kernel/audit.h: 103-106 */
struct audit_stamp {
    struct timespec64 ctime;  /* syscall 进入时的实时时钟(CLOCK_REALTIME_COARSE)*/
    unsigned int      serial; /* 单调递增序列号 */
};

序列号由全局原子计数器生成(kernel/audit.c 第 1875-1879 行):

unsigned int audit_serial(void)
{
    static atomic_t serial = ATOMIC_INIT(0);
    return atomic_inc_return(&serial);
}

(timestamp, serial) 二元组在用户空间工具中用于将同一次事件产生的多条记录(AUDIT_SYSCALL + AUDIT_PATH + AUDIT_CWD + AUDIT_PROCTITLE + AUDIT_EOE)关联合并成完整事件。

时间戳的获取(kernel/audit.c 第 1882-1888 行):

static inline void audit_get_stamp(struct audit_context *ctx,
                                   struct audit_stamp *stamp)
{
    if (!ctx || !auditsc_get_stamp(ctx, stamp)) {
        ktime_get_coarse_real_ts64(&stamp->ctime);
        stamp->serial = audit_serial();
    }
}

若有关联的 syscall context,从 context 继承时间戳(保证同一事件所有记录的时间戳一致);否则生成新的时间戳(用于异步事件,如 SELinux AVC)。

3.5 辅助数据:audit_aux_data

audit_aux_data 是辅助数据的基类(kernel/auditsc.c 第 89-92 行):

struct audit_aux_data {
    struct audit_aux_data *next;  /* 单链表,挂在 context->aux */
    int                    type;  /* AUDIT_BPRM_FCAPS/AUDIT_IPC 等 */
};

两个具体子类:

/* 信号目标 PID 信息(每个结构最多 16 个目标) */
struct audit_aux_data_pids {
    struct audit_aux_data d;           /* 必须是第一个字段 */
    pid_t         target_pid[AUDIT_AUX_PIDS=16];
    kuid_t        target_auid[16];
    kuid_t        target_uid[16];
    unsigned int  target_sessionid[16];
    struct lsm_prop target_ref[16];
    char          target_comm[16][TASK_COMM_LEN];
    int           pid_count;
};

/* bprm fcaps(exec 时文件 capability 变更) */
struct audit_aux_data_bprm_fcaps {
    struct audit_aux_data d;
    struct audit_cap_data fcap;       /* 文件能力 */
    unsigned int          fcap_ver;
    struct audit_cap_data old_pcap;   /* exec 前的进程能力 */
    struct audit_cap_data new_pcap;   /* exec 后的进程能力 */
};

4. 审计规则系统

4.1 audit_krule:内核规则表示

audit_krule 定义于 include/linux/audit.h 第 42-60 行,是审计规则在内核中的完整表示(用户态规则 audit_rule_dataaudit_data_to_entry() 转换而来):

/* include/linux/audit.h: 42-60 */
struct audit_krule {
    u32           pflags;         /* AUDIT_LOGINUID_LEGACY 等标志 */
    u32           flags;          /* 规则标志 */
    u32           listnr;         /* 挂在哪个过滤链(0-7) */
    u32           action;         /* AUDIT_NEVER / AUDIT_ALWAYS */
    u32           mask[AUDIT_BITMASK_SIZE]; /* syscall 号位图(64*u32=2048位)*/
    u32           buflen;         /* 动态数据长度 */
    u32           field_count;    /* fields 数组长度 */
    char          *filterkey;     /* 规则标签(-k 参数) */
    struct audit_field *fields;   /* 字段匹配数组 */
    struct audit_field *arch_f;   /* 快速访问 arch 字段的指针 */
    struct audit_field *inode_f;  /* 快速访问 inode 字段的指针 */
    struct audit_watch *watch;    /* 关联的 inode watch(-w 规则) */
    struct audit_tree  *tree;     /* 关联的目录树 watch(-F dir=) */
    struct audit_fsnotify_mark *exe; /* 关联的可执行文件 mark(-F exe=)*/
    struct list_head  rlist;      /* 在 watch/tree->rules 链表中的节点 */
    struct list_head  list;       /* 仅用于 AUDIT_LIST_RULES 的链表节点 */
    u64           prio;           /* 优先级(越大越先匹配,匹配后终止) */
};

mask[] 是 2048 位的系统调用位图。位操作宏(include/uapi/linux/audit.h 第 197-198 行):

#define AUDIT_WORD(nr) ((__u32)((nr)/32))
#define AUDIT_BIT(nr)  (1U << ((nr) - AUDIT_WORD(nr)*32))

例如 openat 系统调用号为 257(x86_64),则:

  • AUDIT_WORD(257) = 8mask[8]
  • AUDIT_BIT(257) = 1U << (257 - 8*32) = 1U << 1 = 0x2

audit_in_mask() 函数(kernel/auditsc.c 第 797-811 行)用此位图快速预过滤:

static int audit_in_mask(const struct audit_krule *rule, unsigned long val)
{
    int word, bit;
    if (val > 0xffffffff)
        return false;
    word = AUDIT_WORD(val);
    if (word >= AUDIT_BITMASK_SIZE)
        return false;
    bit = AUDIT_BIT(val);
    return rule->mask[word] & bit;
}

规则动作常量(include/uapi/linux/audit.h 第 188-190 行):

#define AUDIT_NEVER    0  /* 永不记录(黑名单规则) */
#define AUDIT_POSSIBLE 1  /* 已废弃 */
#define AUDIT_ALWAYS   2  /* 确定记录(白名单规则) */

4.2 audit_field:规则字段与比较运算符

/* include/linux/audit.h: 65-77 */
struct audit_field {
    u32 type;       /* 字段类型(AUDIT_UID、AUDIT_INODE 等) */
    union {
        u32    val;       /* 整数值 */
        kuid_t uid;       /* UID(内核态,含命名空间信息) */
        kgid_t gid;       /* GID */
        struct {
            char  *lsm_str;  /* LSM 安全上下文字符串(如 "httpd_t") */
            void  *lsm_rule; /* 经 security_audit_rule_init() 编译的规则 */
        };
    };
    u32 op;         /* 比较运算符 */
};

字段类型(include/uapi/linux/audit.h 第 257-305 行,重要字段):

任务级字段(所有过滤链可用):
  AUDIT_PID=0       进程 ID(task_tgid_nr)
  AUDIT_UID=1       真实 UID
  AUDIT_EUID=2      有效 UID
  AUDIT_SUID=3      保存的 UID
  AUDIT_FSUID=4     文件系统 UID
  AUDIT_GID=5       真实 GID(含组成员检查)
  AUDIT_EGID=6      有效 GID
  AUDIT_SGID=7      保存的 GID
  AUDIT_FSGID=8     文件系统 GID
  AUDIT_LOGINUID=9  登录 UID(auid,核心追踪字段)
  AUDIT_PERS=10     personality
  AUDIT_ARCH=11     CPU 架构(区分 32/64 位 ABI)
  AUDIT_MSGTYPE=12  消息类型
  AUDIT_SUBJ_USER=13..AUDIT_SUBJ_CLR=17  SELinux 主体标签(5 个字段)
  AUDIT_PPID=18     父进程 PID
  AUDIT_OBJ_USER=19..AUDIT_OBJ_LEV_HIGH=23  SELinux 对象标签(5 个字段)
  AUDIT_LOGINUID_SET=24  loginuid 是否已设置
  AUDIT_SESSIONID=25     会话 ID

syscall 退出时可用字段:
  AUDIT_DEVMAJOR=100  设备主号
  AUDIT_DEVMINOR=101  设备次号
  AUDIT_INODE=102     inode 号
  AUDIT_EXIT=103      syscall 退出码
  AUDIT_SUCCESS=104   是否成功(exit >= 0)
  AUDIT_WATCH=105     watch 路径匹配
  AUDIT_PERM=106      文件访问权限位(r/w/x/a)
  AUDIT_DIR=107       目录树匹配
  AUDIT_FILETYPE=108  文件类型(S_IFREG 等)
  AUDIT_OBJ_UID=109   对象 UID
  AUDIT_OBJ_GID=110   对象 GID
  AUDIT_FIELD_COMPARE=111  两个字段间比较(如 uid == obj_uid)
  AUDIT_EXE=112       可执行文件路径
  AUDIT_SADDR_FAM=113 socket 地址族

系统调用参数:
  AUDIT_ARG0=200..AUDIT_ARG3=203  直接访问 ctx->argv[0-3]
  AUDIT_FILTERKEY=210             规则 key(始终匹配,仅用于标记)

支持的比较运算符(include/uapi/linux/audit.h 第 336-346 行):

enum {
    Audit_equal,        /* = */
    Audit_not_equal,    /* != */
    Audit_bitmask,      /* & (位掩码检查) */
    Audit_bittest,      /* &= (位测试,等同于 & 后 !=0) */
    Audit_lt,           /* < */
    Audit_gt,           /* > */
    Audit_le,           /* <= */
    Audit_ge,           /* >= */
    Audit_bad
};

AUDIT_FIELD_COMPARE 字段支持跨字段比较(kernel/auditsc.c 第 380-454 行),共 25 种组合,如 AUDIT_COMPARE_UID_TO_OBJ_UID=1(进程 UID 与文件 UID 比较)、AUDIT_COMPARE_AUID_TO_EUID=16(loginuid 与 euid 比较)等。

4.3 audit_entry:规则列表项与 RCU 保护

/* kernel/audit.h: 50-54 */
struct audit_entry {
    struct list_head list;   /* 挂在 audit_filter_list[n] 或 audit_rules_list[n] */
    struct rcu_head  rcu;    /* RCU 回调,安全延迟释放 */
    struct audit_krule rule; /* 内嵌的 krule(不是指针,直接嵌入) */
};

规则存储在两个并行的列表数组中(kernel/auditfilter.c 第 39-61 行):

  • audit_filter_list[AUDIT_NR_FILTERS]:用于实际过滤,通过 RCU 读锁无锁遍历
  • audit_rules_list[AUDIT_NR_FILTERS]:用于响应 AUDIT_LIST_RULES 请求,保持添加顺序

加锁约定(kernel/auditfilter.c 第 25-36 行注释):

audit_filter_mutex:
    保护 filterlist 的写操作和阻塞读操作。
    RCU 用于过滤期间的遍历和 struct audit_entry/audit_watch 的读取。
    修改时必须复制结构并用新版本替换旧版本(copy-on-write)。
    audit_parent 不经过过滤路径,可在持 audit_filter_mutex 时直接写。

4.4 8 条过滤链

8 条过滤链在 include/uapi/linux/audit.h 第 173-183 行定义:

#define AUDIT_FILTER_USER       0x00  /* 用户态可信消息(AUDIT_USER_*) */
#define AUDIT_FILTER_TASK       0x01  /* 任务创建时(fork/clone),用于黑名单任务 */
#define AUDIT_FILTER_ENTRY      0x02  /* syscall 进入时(已废弃,保留兼容) */
#define AUDIT_FILTER_WATCH      0x03  /* 文件系统 inode watch */
#define AUDIT_FILTER_EXIT       0x04  /* syscall 退出时(最常用) */
#define AUDIT_FILTER_EXCLUDE    0x05  /* 记录创建前排除(AUDIT_FILTER_TYPE 别名) */
#define AUDIT_FILTER_FS         0x06  /* __audit_inode_child 时 */
#define AUDIT_FILTER_URING_EXIT 0x07  /* io_uring 操作退出时 */

#define AUDIT_NR_FILTERS        8

各过滤链的触发时机与可用字段范围:

触发点 主要用途
TASK(1) audit_filter_task(),fork 时 对特定 uid/gid 关闭审计
EXIT(4) audit_filter_syscall(),syscall 退出 最主要的规则链
EXCLUDE(5) audit_log_start() 入口 快速排除特定消息类型
FS(6) audit_filter_inodes(),inode 匹配 文件系统事件细粒度控制
URING_EXIT(7) audit_filter_uring(),io_uring 退出 io_uring 操作审计

AUDIT_FILTER_PREPEND = 0x10include/uapi/linux/audit.h 第 185 行)是一个标志位,加在 flags 上表示将规则插入到链头而非链尾,实现优先匹配。

4.5 规则匹配引擎:audit_filter_rules()

audit_filter_rules() 是规则匹配的核心函数(kernel/auditsc.c 第 464-772 行),它遍历一条规则的所有 fields,所有字段全部匹配才返回 1:

audit_filter_rules(tsk, rule, ctx, name, &state, task_creation):

  /* 优先级检查:低优先级规则不覆盖高优先级的决定 */
  if (ctx && rule->prio <= ctx->prio)
      return 0;

  for i in 0..rule->field_count:
    f = &rule->fields[i]
    result = 0

    switch f->type:
      AUDIT_PID:        result = comparator(task_tgid_nr(tsk), f->op, f->val)
      AUDIT_PPID:       result = comparator(ctx->ppid, f->op, f->val)
      AUDIT_EXE:        result = audit_exe_compare(tsk, rule->exe)
      AUDIT_UID:        result = uid_comparator(cred->uid, f->op, f->uid)
      AUDIT_EUID:       result = uid_comparator(cred->euid, f->op, f->uid)
      AUDIT_LOGINUID:   result = uid_comparator(tsk->loginuid, f->op, f->uid)
      AUDIT_GID:        result = gid_comparator(cred->gid, f->op, f->gid)
                        /* GID 还需检查附加组 groups_search() */
      AUDIT_SESSIONID:  result = comparator(audit_get_sessionid(tsk), f->op, f->val)
      AUDIT_ARCH:       result = comparator(ctx->arch, f->op, f->val)
      AUDIT_EXIT:       result = comparator(ctx->return_code, f->op, f->val)
      AUDIT_SUCCESS:    result = comparator(ctx->return_valid, f->op, ...)
      AUDIT_DEVMAJOR:   result = 遍历 ctx->names_list 比较 MAJOR(n->dev)
      AUDIT_INODE:      result = 遍历 ctx->names_list 比较 n->ino
      AUDIT_OBJ_UID:    result = 遍历 ctx->names_list 比较 n->uid
      AUDIT_WATCH:      result = audit_watch_compare(rule->watch, name->ino, name->dev)
      AUDIT_DIR:        result = match_tree_refs(ctx, rule->tree)
      AUDIT_SUBJ_USER~CLR: result = security_audit_rule_match(&prop, ...)  /* LSM */
      AUDIT_OBJ_USER~HIGH: result = security_audit_rule_match(&name->oprop, ...)
      AUDIT_ARG0~3:     result = comparator(ctx->argv[n], f->op, f->val)
      AUDIT_FIELD_COMPARE: result = audit_field_compare(tsk, cred, f, ctx, name)
      AUDIT_FILTERKEY:  result = 1  /* filterkey 字段始终匹配,仅用于标记 */
      AUDIT_PERM:       result = audit_match_perm(ctx, f->val)
      AUDIT_FILETYPE:   result = audit_match_filetype(ctx, f->val)
      ...

    if !result: return 0  /* 任一字段不匹配,整条规则不匹配 */

  /* 所有字段匹配 */
  if ctx:
    ctx->filterkey = kstrdup(rule->filterkey)  /* 记录规则 key */
    ctx->prio = rule->prio                     /* 更新优先级 */

  switch rule->action:
    AUDIT_NEVER:  *state = AUDIT_STATE_DISABLED
    AUDIT_ALWAYS: *state = AUDIT_STATE_RECORD

  return 1

对 inode 相关规则,内核维护哈希表 audit_inode_hash[32]kernel/audit.c 第 155 行),以 inode 号低 5 位为键快速定位相关规则,避免每次 syscall 退出都扫描所有 EXIT 链规则:

/* kernel/audit.h: 225-231 */
#define AUDIT_INODE_BUCKETS 32
extern struct list_head audit_inode_hash[AUDIT_INODE_BUCKETS];

static inline int audit_hash_ino(u32 ino)
{
    return (ino & (AUDIT_INODE_BUCKETS - 1));
}

5. 系统调用审计

5.1 任务创建:audit_alloc()

audit_alloc()copy_process() 中调用(kernel/auditsc.c 第 1057-1083 行),为新进程决定是否分配审计上下文:

int audit_alloc(struct task_struct *tsk)
{
    struct audit_context *context;
    enum audit_state      state;
    char *key = NULL;

    /* 全局快速路径:若审计从未被启用过,直接跳过 */
    if (likely(!audit_ever_enabled))
        return 0;

    /* 检查 TASK 过滤链:决定此进程的初始审计状态 */
    state = audit_filter_task(tsk, &key);
    if (state == AUDIT_STATE_DISABLED) {
        clear_task_syscall_work(tsk, SYSCALL_AUDIT);  /* 关闭 syscall 钩子 */
        return 0;
    }

    context = audit_alloc_context(state);    /* kzalloc context */
    if (!context) {
        kfree(key);
        audit_log_lost("out of memory in audit_alloc");
        return -ENOMEM;
    }
    context->filterkey = key;

    audit_set_context(tsk, context);         /* tsk->audit_context = context */
    set_task_syscall_work(tsk, SYSCALL_AUDIT); /* 激活 syscall entry/exit 钩子 */
    return 0;
}

audit_alloc_context() 初始化要点(kernel/auditsc.c 第 1031-1046 行):

static inline struct audit_context *audit_alloc_context(enum audit_state state)
{
    struct audit_context *context;

    context = kzalloc_obj(*context);
    context->context = AUDIT_CTX_UNUSED;
    context->state = state;
    /* prio 的初始值:RECORD 状态为 ~0ULL(最高),BUILD 为 0 */
    context->prio = state == AUDIT_STATE_RECORD ? ~0ULL : 0;
    INIT_LIST_HEAD(&context->killed_trees);
    INIT_LIST_HEAD(&context->names_list);
    context->fds[0] = -1;
    context->return_valid = AUDITSC_INVALID;
    return context;
}

5.2 __audit_syscall_entry():syscall 进入钩子

系统调用进入的钩子路径(include/linux/audit.h 第 367-373 行 + kernel/auditsc.c 第 1986-2022 行):

/* 公共包装,架构层调用 */
static inline void audit_syscall_entry(int major, unsigned long a0, ...)
{
    if (unlikely(audit_context()))  /* 有 context 才进入快速路径 */
        __audit_syscall_entry(major, a0, a1, a2, a3);
}

/* 实际实现 */
void __audit_syscall_entry(int major, unsigned long a1, unsigned long a2,
                           unsigned long a3, unsigned long a4)
{
    struct audit_context *context = audit_context();
    enum audit_state      state;

    if (!audit_enabled || !context)
        return;

    /* 断言:进入 syscall 时 context 必须是 UNUSED 状态 */
    WARN_ON(context->context != AUDIT_CTX_UNUSED);

    state = context->state;
    if (state == AUDIT_STATE_DISABLED)
        return;

    /* 关键优化:若当前无任何审计规则,dummy=1,syscall 退出时直接跳过 */
    context->dummy = !audit_n_rules;

    /* BUILD 状态下,若是 auditd 自身,跳过(防止 auditd 审计自身死循环) */
    if (!context->dummy && state == AUDIT_STATE_BUILD) {
        context->prio = 0;
        if (auditd_test_task(current))
            return;
    }

    /* 填入 syscall 基本信息(延迟到 exit 时才做重量级工作) */
    context->arch    = syscall_get_arch(current);
    context->major   = major;
    context->argv[0] = a1;
    context->argv[1] = a2;
    context->argv[2] = a3;
    context->argv[3] = a4;
    context->context = AUDIT_CTX_SYSCALL;
    context->current_state = state;
    ktime_get_coarse_real_ts64(&context->stamp.ctime);
}

auditd_test_task() 检查(kernel/audit.c 第 230-241 行):通过 RCU 读取 auditd_conn->pid 并与 task_tgid() 比较,避免对 auditd 进程自身产生审计记录(防止正反馈死循环)。

5.3 __audit_syscall_exit():syscall 退出钩子

/* include/linux/audit.h: 374-382 */
static inline void audit_syscall_exit(void *pt_regs)
{
    if (unlikely(audit_context())) {
        int success = is_syscall_success(pt_regs);
        long return_code = regs_return_value(pt_regs);
        __audit_syscall_exit(success, return_code);
    }
}

/* kernel/auditsc.c: 2035-2058 */
void __audit_syscall_exit(int success, long return_code)
{
    struct audit_context *context = audit_context();

    /* 三种快速跳过条件:无 context、dummy(无规则)、非 syscall context */
    if (!context || context->dummy ||
        context->context != AUDIT_CTX_SYSCALL)
        goto out;

    /* 处理被杀死的目录树(可能产生 CONFIG_CHANGE 记录) */
    if (!list_empty(&context->killed_trees))
        audit_kill_trees(context);

    /* 修正返回值:某些架构对 ERESTART* 有特殊处理 */
    audit_return_fixup(context, success, return_code);

    /* 两阶段过滤:
     * 1. EXIT 链:按 syscall 号位图快速过滤 + 字段匹配
     * 2. inode 哈希链:按被访问文件的 inode 号匹配
     * 两者任一匹配即可将 current_state 提升至 RECORD */
    audit_filter_syscall(current, context);
    audit_filter_inodes(current, context);

    if (context->current_state != AUDIT_STATE_RECORD)
        goto out;

    /* 确认需要记录,生成完整审计事件 */
    audit_log_exit();

out:
    audit_reset_context(context);  /* 重置 context 供下次 syscall 复用 */
}

audit_filter_syscall() 实现(kernel/auditsc.c 第 869-879 行):

static void audit_filter_syscall(struct task_struct *tsk,
                                 struct audit_context *ctx)
{
    if (auditd_test_task(tsk))  /* auditd 跳过 */
        return;

    rcu_read_lock();
    __audit_filter_op(tsk, ctx,
                      &audit_filter_list[AUDIT_FILTER_EXIT],
                      NULL, ctx->major);  /* 按 syscall 号位图预过滤 */
    rcu_read_unlock();
}

audit_filter_inodes() 实现(kernel/auditsc.c 第 900-914 行):

void audit_filter_inodes(struct task_struct *tsk, struct audit_context *ctx)
{
    struct audit_names *n;

    if (auditd_test_task(tsk))
        return;

    rcu_read_lock();
    list_for_each_entry(n, &ctx->names_list, list) {
        if (audit_filter_inode_name(tsk, n, ctx))
            break;  /* 有一个 inode 匹配即可 */
    }
    rcu_read_unlock();
}

audit_return_fixup() 处理信号打断的特殊情况(kernel/auditsc.c 第 1848-1865 行):

static void audit_return_fixup(struct audit_context *ctx, int success, long code)
{
    /* 被信号打断时,某些架构会将返回值从 ERESTARTSYS 等转换为 EINTR
     * 需要在此时进行修正,确保记录的返回值与用户态看到的一致 */
    if (unlikely(code <= -ERESTARTSYS) &&
        (code >= -ERESTART_RESTARTBLOCK) &&
        (code != -ENOIOCTLCMD))
        ctx->return_code = -EINTR;
    else
        ctx->return_code = code;
    ctx->return_valid = (success ? AUDITSC_SUCCESS : AUDITSC_FAILURE);
}

5.4 audit_log_exit():多记录事件写出

audit_log_exit() 是将 audit_context 中收集的所有信息格式化输出的核心函数(kernel/auditsc.c 第 1652-1794 行),按固定顺序生成一个完整的多记录事件:

audit_log_exit():

  context->personality = current->personality

  switch context->context:
    AUDIT_CTX_SYSCALL:
      /* 1. AUDIT_SYSCALL (type=1300) 主记录 */
      ab = audit_log_start(context, GFP_KERNEL, AUDIT_SYSCALL)
      audit_log_format(ab, "arch=%x syscall=%d",
                       context->arch, context->major)
      if context->personality != PER_LINUX:
          audit_log_format(ab, " per=%lx", context->personality)
      if context->return_valid != AUDITSC_INVALID:
          audit_log_format(ab, " success=%s exit=%ld",
                           str_yes_no(...), context->return_code)
      audit_log_format(ab, " a0=%lx a1=%lx a2=%lx a3=%lx items=%d",
                       context->argv[0..3], context->name_count)
      audit_log_task_info(ab)    # ppid/pid/auid/uid/.../tty/ses/comm/exe/subj
      audit_log_key(ab, context->filterkey)
      audit_log_end(ab)

    AUDIT_CTX_URING:
      audit_log_uring(context)   # AUDIT_URINGOP (type=1336)

  /* 2. 遍历 context->aux 辅助数据链 */
  for aux in context->aux:
    ab = audit_log_start(context, GFP_KERNEL, aux->type)
    switch aux->type:
      AUDIT_BPRM_FCAPS (1321): 记录文件 capability 变更
    audit_log_end(ab)

  /* 3. 特殊类型记录(context->type 非零时) */
  if context->type:
    show_special(context, &call_panic)
    # 根据 context->type 生成对应记录:
    # AUDIT_SOCKETCALL(1304) / AUDIT_IPC(1303) / AUDIT_MQ_*(1312-1315)
    # AUDIT_CAPSET(1322) / AUDIT_MMAP(1323) / AUDIT_OPENAT2(1337)
    # AUDIT_NETFILTER_CFG(1325) / AUDIT_TIME_ADJNTPVAL(1333) 等

  /* 4. FD 对记录 */
  if context->fds[0] >= 0:
    ab = audit_log_start(context, GFP_KERNEL, AUDIT_FD_PAIR)  # type=1317
    audit_log_format(ab, "fd0=%d fd1=%d", ...)
    audit_log_end(ab)

  /* 5. socket 地址记录 */
  if context->sockaddr_len:
    ab = audit_log_start(context, GFP_KERNEL, AUDIT_SOCKADDR)  # type=1306
    audit_log_format(ab, "saddr=")
    audit_log_n_hex(ab, ...)
    audit_log_end(ab)

  /* 6. 目标 PID 记录(信号发送等) */
  for each aux_pids entry:
    for each pid in axs->target_pid[]:
      audit_log_pid_context(context, ...)  # AUDIT_OBJ_PID (type=1318)

  /* 7. AUDIT_CWD 当前工作目录 (type=1307) */
  if context->pwd.dentry && context->pwd.mnt:
    ab = audit_log_start(context, GFP_KERNEL, AUDIT_CWD)
    audit_log_d_path(ab, "cwd=", &context->pwd)
    audit_log_end(ab)

  /* 8. AUDIT_PATH 记录 (type=1302),每个文件一条 */
  list_for_each_entry(n, &context->names_list, list):
    if n->hidden: continue
    audit_log_name(context, n, NULL, i++, &call_panic)
    # 格式:item=N name="..." inode=N dev=N:N mode=N ouid=N ogid=N
    #        rdev=N:N obj=<lsm_ctx> nametype={NORMAL|PARENT|DELETE|CREATE}
    #        cap_fp=N cap_fi=N cap_fe=N cap_fver=N cap_frootid=N

  /* 9. AUDIT_PROCTITLE 进程标题 (type=1327) */
  if context->context == AUDIT_CTX_SYSCALL:
    audit_log_proctitle()
    # 格式:proctitle=<hex_encoded_cmdline>

  /* 10. AUDIT_EOE 事件结束标记 (type=1320) */
  ab = audit_log_start(context, GFP_KERNEL, AUDIT_EOE)
  audit_log_end(ab)

  if call_panic:
    audit_panic("error in audit_log_exit()")

5.5 io_uring 审计扩展

Linux 5.15 起,审计子系统扩展支持 io_uring 操作,新增 AUDIT_FILTER_URING_EXIT = 0x07 过滤链和 AUDIT_URINGOP = 1336 消息类型。

__audit_uring_entry()__audit_uring_exit() 与 syscall 版本逻辑类似(kernel/auditsc.c 第 1876-1968 行),但有一个特殊情况:若 io_uring 操作发生在 syscall 上下文内(io_uring_enter 本身是 syscall),则仅发出 AUDIT_URINGOP 记录;正常的 syscall 退出记录由 __audit_syscall_exit() 另行处理。

AUDIT_URINGOP 记录格式(kernel/auditsc.c 第 1618-1650 行):

audit_log_format(ab, "uring_op=%d", ctx->uring_op);
if (ctx->return_valid != AUDITSC_INVALID)
    audit_log_format(ab, " success=%s exit=%ld", ...);
audit_log_format(ab, " items=%d ppid=%d pid=%d uid=%u gid=%u "
                     "euid=%u suid=%u fsuid=%u egid=%u sgid=%u fsgid=%u",
                 ...);
audit_log_task_context(ab);  /* subj= */
audit_log_key(ab, ctx->filterkey);

6. 文件审计

6.1 audit_watch 与 fsnotify 集成

文件 watch 通过 kernel/audit_watch.c 实现,将审计规则与 fsnotify 框架集成。

两个核心结构(kernel/audit_watch.c 第 36-49 行):

/* 被 watch 的单个文件 */
struct audit_watch {
    refcount_t        count;   /* 引用计数 */
    dev_t             dev;     /* 设备号 */
    char             *path;    /* 规则添加时的路径字符串(如 "/etc/passwd") */
    unsigned long     ino;     /* inode 号(初始为 AUDIT_INO_UNSET,
                                * 路径解析后填入,文件移动时更新) */
    struct audit_parent *parent; /* 父目录 audit_parent 指针 */
    struct list_head  wlist;   /* 在 parent->watches 中的节点 */
    struct list_head  rules;   /* 附加到此 watch 的 audit_krule 列表 */
};

/* 父目录的 fsnotify 标记 */
struct audit_parent {
    struct list_head  watches; /* 此目录下所有 audit_watch 的锚点 */
    struct fsnotify_mark mark; /* 挂在父目录 inode 上的 fsnotify mark */
};

watch 监听的 fsnotify 事件集合(kernel/audit_watch.c 第 55-56 行):

#define AUDIT_FS_WATCH (FS_MOVE | FS_CREATE | FS_DELETE | \
                        FS_DELETE_SELF | FS_MOVE_SELF | FS_UNMOUNT)

watch 的安装流程(kernel/audit_watch.c 第 178-199、audit_add_watch()):

audit_to_watch(krule, path="/etc/passwd", len, op):
    if !audit_watch_group: return -EOPNOTSUPP
    watch = audit_init_watch(path)
    # 仅设置 path,ino=AUDIT_INO_UNSET,dev=AUDIT_DEV_UNSET

audit_add_watch(krule, &list):
    kern_path("/etc/passwd", LOOKUP_FOLLOW, &path)
    parent_inode = d_backing_inode(path.dentry->d_parent)

    parent = audit_find_parent(parent_inode)
    if !parent:
        parent = audit_init_parent(&path)
        # fsnotify_init_mark(&parent->mark, audit_watch_group)
        # parent->mark.mask = AUDIT_FS_WATCH
        # fsnotify_add_inode_mark(&parent->mark, parent_inode, 0)

    audit_update_watch(parent, dname, dev, ino, 0)
    # 设置 watch->ino 和 watch->dev(实际的 inode 号)
    # 将 watch 加入 audit_inode_hash[hash(ino)]

当文件系统事件发生时(kernel/audit_watch.c 第 244 行):

audit_update_watch(parent, dname, dev, ino, invalidating):
    /* 遍历 parent->watches,找到名字匹配的 watch */
    for each owatch in parent->watches:
        if strcmp(owatch->path 末部分, dname) == 0:
            if invalidating:
                /* 文件被删除:将 ino 重置为 AUDIT_INO_UNSET */
                nwatch->ino = AUDIT_INO_UNSET
            else:
                /* 文件被创建/移动:更新 ino 和 dev */
                nwatch->ino = ino
                nwatch->dev = dev
            /* 更新 audit_inode_hash 中的位置 */

audit_watch_log_rule_change() 在 watch 变化时记录 AUDIT_CONFIG_CHANGE 审计记录(kernel/audit_watch.c 第 227-242 行)。

6.2 audit_inode 与路径跟踪

__audit_inode()__audit_getname() 在 VFS 层被调用,将路径和 inode 信息记录到 audit_context->names_list

/* include/linux/audit.h: 321-326 */
extern void __audit_inode(struct filename *name,
                          const struct dentry *dentry,
                          unsigned int flags);
extern void __audit_getname(struct filename *name);
extern void __audit_inode_child(struct inode *parent,
                                const struct dentry *dentry,
                                const unsigned char type);

flags 控制记录类型(include/linux/audit.h 第 304-306 行):

#define AUDIT_INODE_PARENT  1  /* dentry 代表父目录(而非目标文件) */
#define AUDIT_INODE_HIDDEN  2  /* 不在日志中输出此条目 */
#define AUDIT_INODE_NOEVAL  4  /* 记录不完整(如 inode 无法获取) */

__audit_inode() 的核心工作:

  1. 检查 AUDIT_FILTER_FS 链,决定是否需要记录此 inode
  2. ctx->names_list 中查找是否已有相同 name/inode 的条目
  3. 若未找到则从 preallocated_names[] 槽或通过 kzalloc() 分配新的 audit_names
  4. 调用 audit_copy_inode() 填充:
    • n->ino = inode->i_ino
    • n->dev = inode->i_sb->s_dev
    • n->mode = inode->i_mode
    • n->uid = inode->i_uid
    • n->gid = inode->i_gid
    • security_inode_getlsmprop() -> n->oprop(LSM 安全属性)
    • audit_copy_fcaps() -> n->fcap(文件 capability)

audit_inode_parent_hidden() 是一个便捷包装(include/linux/audit.h 第 399-405 行),用于记录父目录但不在日志中显示。

6.3 fanotify 与 audit 的关系

fanotify 和 audit watch 都使用 fsnotify 框架,但用途和层次不同:

架构层次关系

应用程序(如 inotify/fanotify 工具)
    |
fanotify API (sys_fanotify_mark, sys_fanotify_init)
    |
fsnotify 核心框架
    |
+---+---------+
|             |
audit_watch   fsnotify 通知用户态
(audit group) (fanotify group)

关键区别

特性 audit watch fanotify
用途 内核审计决策 用户态文件访问通知/控制
监听对象 父目录(track file by name) 文件/目录 inode
响应方式 syscall exit 时规则匹配 异步通知或同步阻塞
访问控制 无(仅记录) 支持(FAN_ACCESS_PERM)
内核接口 内核内部 NETLINK_AUDIT + sysfs

fanotify 与 audit 的集成点include/linux/audit.h 第 438-439 行):

extern void __audit_fanotify(u32 response,
                             struct fanotify_response_info_audit_rule *friar);

当 fanotify 程序使用 FAN_AUDIT 标志对访问请求作出裁决时,内核调用 audit_fanotify() 生成 AUDIT_FANOTIFY = 1331 类型记录,将 fanotify 的访问控制决策纳入审计日志。fanotify_response_info_audit_rule 结构(include/uapi/linux/fanotify.h)包含触发事件的审计规则信息。

审计记录格式:

type=FANOTIFY msg=audit(...): resp=allow fan_type=1 fan_info=...
  subj_trust=1 obj_trust=1

audit_watch_group 是全局的 fsnotify_groupkernel/audit_watch.c 第 52 行),在 audit_watch_init() 中通过 fsnotify_alloc_group() 创建,其 handle_event 回调为 audit_watch_handle_event()


7. Netlink 接口

7.1 AUDIT_* 消息类型分区

include/uapi/linux/audit.h 第 31-53 行对 Netlink 消息类型空间作了完整注释:

1000-1099: 控制命令(双向,内核 <-> 用户)
  AUDIT_GET=1000          获取审计子系统状态
  AUDIT_SET=1001          设置状态(enabled/failure/pid/rate_limit/backlog_limit)
  AUDIT_LIST=1002         列出规则(已废弃)
  AUDIT_ADD=1003          添加规则(已废弃)
  AUDIT_DEL=1004          删除规则(已废弃)
  AUDIT_LOGIN=1006        设置 loginuid(PAM 调用)
  AUDIT_WATCH_INS=1007    插入 inode watch
  AUDIT_WATCH_REM=1008    删除 inode watch
  AUDIT_SIGNAL_INFO=1010  获取发信号进程信息
  AUDIT_ADD_RULE=1011     添加过滤规则(当前版本)
  AUDIT_DEL_RULE=1012     删除过滤规则
  AUDIT_LIST_RULES=1013   列出所有过滤规则
  AUDIT_TRIM=1014         清理孤立目录树
  AUDIT_MAKE_EQUIV=1015   添加目录树等价路径
  AUDIT_TTY_GET=1016      获取 TTY 审计状态
  AUDIT_TTY_SET=1017      设置 TTY 审计状态
  AUDIT_SET_FEATURE=1018  设置特性开关(loginuid_immutable 等)
  AUDIT_GET_FEATURE=1019  查询特性开关状态

1100-1199: 用户态可信消息(需要 CAP_AUDIT_WRITE)
  AUDIT_USER_AVC=1107     用户态 AVC 消息
  AUDIT_USER_TTY=1124     TTY 输入审计

1200-1299: auditd 守护进程内部消息
  AUDIT_DAEMON_START=1200, AUDIT_DAEMON_END=1201,
  AUDIT_DAEMON_ABORT=1202, AUDIT_DAEMON_CONFIG=1203

1300-1399: 内核事件(内核 -> 用户)
  AUDIT_SYSCALL=1300      系统调用主记录
  AUDIT_PATH=1302         文件路径信息
  AUDIT_IPC=1303          IPC 权限记录
  AUDIT_SOCKETCALL=1304   socketcall 参数
  AUDIT_CONFIG_CHANGE=1305 审计配置变更
  AUDIT_SOCKADDR=1306     socket 地址
  AUDIT_CWD=1307          当前工作目录
  AUDIT_EXECVE=1309       execve 参数(argv)
  AUDIT_IPC_SET_PERM=1311 IPC 权限变更
  AUDIT_MQ_OPEN=1312      POSIX MQ open
  AUDIT_EOE=1320          事件结束标记(多记录事件终结符)
  AUDIT_BPRM_FCAPS=1321   文件 capability 变更
  AUDIT_CAPSET=1322       capset 操作
  AUDIT_MMAP=1323         mmap 操作
  AUDIT_NETFILTER_PKT=1324 netfilter 数据包
  AUDIT_NETFILTER_CFG=1325 netfilter 规则变更
  AUDIT_SECCOMP=1326      seccomp 策略触发
  AUDIT_PROCTITLE=1327    进程标题
  AUDIT_FEATURE_CHANGE=1328 特性变更
  AUDIT_KERN_MODULE=1330  内核模块加载
  AUDIT_FANOTIFY=1331     fanotify 裁决
  AUDIT_TIME_INJOFFSET=1332 时间注入偏移
  AUDIT_TIME_ADJNTPVAL=1333 NTP 值调整
  AUDIT_BPF=1334          BPF 子系统
  AUDIT_URINGOP=1336      io_uring 操作
  AUDIT_OPENAT2=1337      openat2 how 参数
  AUDIT_DM_CTRL=1338      Device Mapper 控制
  AUDIT_DM_EVENT=1339     Device Mapper 事件

1400-1426: 访问控制事件
  AUDIT_AVC=1400          SELinux AVC 拒绝/授权
  AUDIT_MAC_POLICY_LOAD=1403 MAC 策略加载
  AUDIT_MAC_STATUS=1404   enforcing/permissive 切换
  AUDIT_IPE_ACCESS=1420   IPE 访问控制
  AUDIT_LANDLOCK_ACCESS=1423 Landlock 拒绝
  AUDIT_MAC_TASK_CONTEXTS=1425 多 LSM 任务上下文
  AUDIT_MAC_OBJ_CONTEXTS=1426  多 LSM 对象上下文

1700-1799: 内核异常
  AUDIT_ANOM_ABEND=1701   进程异常终止(core dump)
  AUDIT_ANOM_LINK=1702    可疑的硬链接操作

1800-1808: 完整性验证(IMA/EVM)
  AUDIT_INTEGRITY_DATA=1800, AUDIT_INTEGRITY_METADATA=1801 等

2000: 异步内核消息
  AUDIT_KERNEL=2000       不关联 syscall 的内核审计消息

权限控制(kernel/audit.c 第 1071-1125 行):

AUDIT_GET, AUDIT_SET, AUDIT_ADD_RULE, AUDIT_DEL_RULE,
AUDIT_LIST_RULES, AUDIT_SIGNAL_INFO, AUDIT_TTY_*, AUDIT_TRIM,
AUDIT_MAKE_EQUIV  ->  需要 CAP_AUDIT_CONTROL

AUDIT_USER, AUDIT_FIRST_USER_MSG..AUDIT_LAST_USER_MSG,
AUDIT_FIRST_USER_MSG2..AUDIT_LAST_USER_MSG2  -> 需要 CAP_AUDIT_WRITE

init_user_ns 的命名空间中发来的控制请求返回 -ECONNREFUSEDkernel/audit.c 第 1086-1087 行),而非 -EPERM,是为了让非特权容器中的 PAM 认为审计未编译进内核而正常登录(而非直接拒绝)。

7.2 kauditd 内核线程

kauditd_thread() 是 Audit 子系统的专用内核线程(kernel/audit.c 第 882-957 行),在 audit_init() 中通过 kthread_run() 启动:

static int kauditd_thread(void *dummy)
{
#define UNICAST_RETRIES 5

    set_freezable();
    while (!kthread_should_stop()) {
        /* 1. 获取当前 auditd 连接(RCU 读锁) */
        rcu_read_lock();
        ac = rcu_dereference(auditd_conn);
        if (!ac) {
            rcu_read_unlock();
            goto main_queue;
        }
        net = get_net(ac->net);
        sk = audit_get_sk(net);
        portid = ac->portid;
        rcu_read_unlock();

        /* 2. 优先刷新 hold 队列(auditd 重连后补发历史记录) */
        rc = kauditd_send_queue(sk, portid,
                                &audit_hold_queue, UNICAST_RETRIES,
                                NULL, kauditd_rehold_skb);
        if (rc < 0) { sk = NULL; auditd_reset(ac); goto main_queue; }

        /* 3. 刷新 retry 队列(上次发送临时失败的记录) */
        rc = kauditd_send_queue(sk, portid,
                                &audit_retry_queue, UNICAST_RETRIES,
                                NULL, kauditd_hold_skb);
        if (rc < 0) { sk = NULL; auditd_reset(ac); goto main_queue; }

main_queue:
        /* 4. 处理主队列:先多播,再单播 */
        rc = kauditd_send_queue(sk, portid, &audit_queue, 1,
                                kauditd_send_multicast_skb,  /* 多播钩子 */
                                (sk ? kauditd_retry_skb : kauditd_hold_skb));

        if (ac && rc < 0)
            auditd_reset(ac);
        sk = NULL;

        if (net) { put_net(net); net = NULL; }

        /* 5. 唤醒所有因背压被阻塞的调用者 */
        wake_up(&audit_backlog_wait);

        /* 6. 等待新记录(冻结安全) */
        wait_event_freezable(kauditd_wait,
                             (skb_queue_len(&audit_queue) ? 1 : 0));
    }
    return 0;
}

多播发送(kernel/audit.c 第 847-876 行):

static void kauditd_send_multicast_skb(struct sk_buff *skb)
{
    struct sock *sock = audit_get_sk(&init_net);

    if (!netlink_has_listeners(sock, AUDIT_NLGRP_READLOG))
        return;

    /* 注意:必须用 skb_copy() 而非 skb_get(),
     * 因为单播发送会非规范地修改原始 skb(历史遗留行为),
     * 多播必须使用独立副本 */
    copy = skb_copy(skb, GFP_KERNEL);
    nlh = nlmsg_hdr(copy);
    nlh->nlmsg_len = skb->len;
    nlmsg_multicast(sock, copy, 0, AUDIT_NLGRP_READLOG, GFP_KERNEL);
}

kauditd_send_queue() 内部有重试逻辑(kernel/audit.c 第 784-836 行),单次调用可重试最多 retry_limit 次(主队列为 1,hold/retry 队列为 5)。发送失败时根据错误类型调用不同的 err_hook

7.3 auditd 连接管理:auditd_connection

/* kernel/audit.c: 115-120 */
struct auditd_connection {
    struct pid  *pid;     /* auditd 进程的 PID(跨命名空间稳定) */
    u32          portid;  /* Netlink portid(用于单播发送) */
    struct net  *net;     /* auditd 所在的网络命名空间 */
    struct rcu_head rcu;  /* RCU 延迟释放 */
};
static struct auditd_connection __rcu *auditd_conn;
static DEFINE_SPINLOCK(auditd_conn_lock);

auditd 注册流程

  1. auditd 启动,创建 NETLINK_AUDIT socket
  2. 发送 AUDIT_SET(type=1001),mask = AUDIT_STATUS_PIDpid = 自身 PID
  3. 内核 audit_receive_msg() 处理:
    • 调用 audit_replace(old_pid) 通知旧 auditd 让位(发送 AUDIT_REPLACE=1329 消息)
    • 调用 auditd_set(pid, portid, net, skb, &ack) 原子更新连接
/* kernel/audit.c: 538-572 */
static int auditd_set(struct pid *pid, u32 portid, struct net *net,
                      struct sk_buff *skb, bool *ack)
{
    ac_new = kzalloc_obj(*ac_new);
    ac_new->pid = get_pid(pid);
    ac_new->portid = portid;
    ac_new->net = get_net(net);

    /* 先发 ack,避免与 backlog 队列竞争 */
    if (*ack) {
        netlink_ack(skb, nlmsg_hdr(skb), 0, NULL);
        *ack = false;
    }

    spin_lock_irqsave(&auditd_conn_lock, flags);
    ac_old = rcu_dereference_protected(auditd_conn, ...);
    rcu_assign_pointer(auditd_conn, ac_new);  /* 原子替换 */
    spin_unlock_irqrestore(&auditd_conn_lock, flags);

    if (ac_old)
        call_rcu(&ac_old->rcu, auditd_conn_free);  /* RCU 延迟释放 */
    return 0;
}

auditd_reset() 断开连接(kernel/audit.c 第 690-715 行):将 auditd_conn 置 NULL,把 retry 队列内容移至 hold 队列等待重连。

7.4 三级队列机制

内核审计事件产生
      |
      v audit_log_end()
  audit_queue (主队列)
      |
      | kauditd_thread 消费
      |
  +---+--------------------------------------------+
  |   多播给 AUDIT_NLGRP_READLOG                    |
  |   kauditd_send_multicast_skb()                 |
  |                                                 |
  |   单播给 auditd                                 |
  |   netlink_unicast(sk, skb, portid, 0)          |
  +---+--------------------------------------------+
      |
      | 发送结果
      |
  +---+----------+----------+
  |              |          |
  成功          -EAGAIN    -ECONNREFUSED/-EPERM
  |              |          |
  消费完     retry_queue  auditd 断开
              重试队列    -> auditd_reset()
              满了?       -> hold_queue (等待重连)
              -> hold_q    满了?
                           -> audit_log_lost()
                           -> kfree_skb()

hold_queue 的处理:
  auditd 重连后,kauditd 优先刷新 hold_queue,
  保证历史记录不丢失(在 backlog_limit 范围内)

三个队列的 skb 容量限制(kernel/audit.c 第 618-676 行的 kauditd_hold_skb()kauditd_retry_skb()):

/* hold 队列满时策略 */
if (!audit_backlog_limit ||
    skb_queue_len(&audit_hold_queue) < audit_backlog_limit) {
    skb_queue_tail(&audit_hold_queue, skb);
    return;
}
audit_log_lost("kauditd hold queue overflow");
/* drop: */ kfree_skb(skb);

7.5 audit_receive_msg():命令处理

audit_receive_msg() 是处理来自 auditctl/auditd 的所有 Netlink 命令的核心分发函数(kernel/audit.c 第 1254 行),关键 case 处理:

case AUDIT_GET:
    构造 struct audit_status 响应,包含:
    s.enabled = audit_enabled
    s.failure = audit_failure
    s.pid     = auditd_pid_vnr()
    s.rate_limit = audit_rate_limit
    s.backlog_limit = audit_backlog_limit
    s.lost    = atomic_read(&audit_lost)
    s.backlog  = skb_queue_len(&audit_queue)
    s.backlog_wait_time = audit_backlog_wait_time
    s.backlog_wait_time_actual = ...
    audit_send_reply(skb, seq, AUDIT_GET, ...)

case AUDIT_SET:
    按 s.mask 字段处理各项设置:
    AUDIT_STATUS_ENABLED    -> audit_set_enabled(s.enabled)
    AUDIT_STATUS_FAILURE    -> audit_set_failure(s.failure)
    AUDIT_STATUS_PID        -> auditd_set(pid, portid, net, ...)
    AUDIT_STATUS_RATE_LIMIT -> audit_set_rate_limit(s.rate_limit)
    AUDIT_STATUS_BACKLOG_LIMIT -> audit_set_backlog_limit(s.backlog_limit)
    AUDIT_STATUS_BACKLOG_WAIT_TIME -> audit_set_backlog_wait_time(...)
    AUDIT_STATUS_LOST       -> atomic_xchg(&audit_lost, 0) (重置计数)

case AUDIT_ADD_RULE / AUDIT_DEL_RULE:
    if audit_enabled == AUDIT_LOCKED: 拒绝(返回 AUDIT_CONFIG_CHANGE 记录)
    audit_rule_change(msg_type, seq, data, data_len)

case AUDIT_LIST_RULES:
    audit_list_rules_send(skb, seq)
    # 在独立线程中发送,避免阻塞

case AUDIT_TRIM:
    audit_trim_trees()

case AUDIT_SIGNAL_INFO:
    返回 audit_sig_uid, audit_sig_pid, audit_sig_lsm(最后一个发信号给 auditd 的进程)

8. SELinux AVC 审计

8.1 AVC 缓存与审计触发

SELinux 的访问向量缓存(Access Vector Cache,AVC)是 SELinux 权限决策的快速路径,定义于 security/selinux/avc.c 第 72-78 行:

struct avc_cache {
    struct hlist_head slots[AVC_CACHE_SLOTS]; /* 哈希表,桶数 = 2^CONFIG_...AVC_HASH_BITS */
    spinlock_t        slots_lock[AVC_CACHE_SLOTS];
    atomic_t          lru_hint;               /* LRU 提示,用于缓存驱逐扫描 */
    atomic_t          active_nodes;           /* 当前缓存条目数 */
    u32               latest_notif;           /* 最新的策略失效通知序列号 */
};

缓存条目结构(security/selinux/avc.c 第 48-54 行):

struct avc_entry {
    u32              ssid;    /* 主体 SID(Subject Security Identifier)*/
    u32              tsid;    /* 客体 SID(Target/Object SID)*/
    u16              tclass;  /* 对象类(SECCLASS_FILE / SECCLASS_PROCESS 等)*/
    struct av_decision avd;   /* 访问向量决策:allowed/auditallow/auditdeny 位图 */
    struct avc_xperms_node *xp_node; /* 扩展权限(ioctl 号等)*/
};

AVC 审计的触发时机:avc_has_perm() -> slow_avc_audit() -> common_lsm_audit(),条件为 avd.auditallow & av(需要审计授权)或 !avd.allowed & av(拒绝)。

8.2 avc_audit 回调机制

security/selinux/avc.c 注册了两个回调函数,通过 common_lsm_audit() 被调用:

前置回调security/selinux/avc.c 第 651-684 行):

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;
    const char *const *perms;

    /* 输出决策类型:denied 或 granted */
    audit_log_format(ab, "avc:  %s ",
                     sad->denied ? "denied" : "granted");

    /* 遍历 av 位图,输出每个被拒绝/授权的权限名 */
    perms = secclass_map[sad->tclass - 1].perms;
    audit_log_format(ab, " {");
    for (i = 0, perm = 1; i < sizeof(av)*8; i++, perm <<= 1) {
        if ((perm & av) && perms[i])
            audit_log_format(ab, " %s", perms[i]);
    }
    audit_log_format(ab, " } for ");
}

后置回调security/selinux/avc.c 第 692-740 行):

static void avc_audit_post_callback(struct audit_buffer *ab, void *a)
{
    /* 输出安全上下文:
     * scontext=<主体SELinux上下文>
     * tcontext=<客体SELinux上下文>
     * tclass=<对象类名>
     * permissive=<0|1>
     */
    audit_log_format(ab, "scontext=%s tcontext=%s tclass=%s permissive=%u",
                     scontext, tcontext, tclass, sad->result ? 0 : 1);
}

完整的 AVC 拒绝记录格式(type=AUDIT_AVC=1400):

type=AVC msg=audit(1710000000.001:100):
  avc:  denied  { read write } for
  pid=1234 comm="httpd"
  name="shadow" dev="dm-0" ino=131075
  scontext=system_u:system_r:httpd_t:s0
  tcontext=system_u:object_r:shadow_t:s0
  tclass=file permissive=0

8.3 AVC 记录与 syscall 记录的关联

当 AVC 拒绝发生在系统调用上下文中时,audit_log_start() 的第 1966 行会将 ctx->dummy = 0

/* kernel/audit.c: 1965-1967 */
/* cancel dummy context to enable supporting records */
if (ctx)
    ctx->dummy = 0;

这使得关联的 syscall 在退出时也会产生记录,最终输出的完整事件包含多条记录:

type=AVC msg=audit(T:N): avc:  denied  { read } for ...
type=SYSCALL msg=audit(T:N): arch=c000003e syscall=2 success=no exit=-13 ...
type=PATH msg=audit(T:N): item=0 name="/etc/shadow" ...
type=CWD msg=audit(T:N): cwd="/var/www"
type=EOE msg=audit(T:N):

多 LSM 支持(kernel/audit.c 第 91-94、304-320 行):通过 audit_subj_lsms[]audit_obj_lsms[] 数组追踪所有提供安全上下文的 LSM,每个 LSM 通过 audit_cfg_lsm() 注册。当有多个 LSM 同时提供上下文时,audit_log_task_context() 调用 security_current_getlsmprop() 获取所有上下文,通过 AUDIT_MAC_TASK_CONTEXTS=1425AUDIT_MAC_OBJ_CONTEXTS=1426 类型记录分别输出。


9. 实时事件丢失处理

9.1 audit_backlog_limit 与背压机制

audit_backlog_limitkernel/audit.c 第 131-133 行,默认 64)控制 audit_queue 的最大 skb 数量:

static u32 audit_backlog_limit = 64;
#define AUDIT_BACKLOG_WAIT_TIME (60 * HZ)
static u32 audit_backlog_wait_time = AUDIT_BACKLOG_WAIT_TIME;

audit_log_start() 中的背压等待逻辑(kernel/audit.c 第 1926-1955 行):

/* 豁免条件:auditd 自身和持有 audit_cmd_mutex 的任务不受背压限制
 * (防止 auditd 因无法消费队列而永久阻塞) */
if (!(auditd_test_task(current) || audit_ctl_owner_current())) {
    long stime = audit_backlog_wait_time;  /* 最长等待 60s */

    while (audit_backlog_limit &&
           (skb_queue_len(&audit_queue) > audit_backlog_limit)) {
        /* 唤醒 kauditd 尝试排空队列 */
        wake_up_interruptible(&kauditd_wait);

        if (gfpflags_allow_blocking(gfp_mask) && (stime > 0)) {
            /* 可睡眠上下文:加入等待队列,最多等 stime jiffies */
            DECLARE_WAITQUEUE(wait, current);
            add_wait_queue_exclusive(&audit_backlog_wait, &wait);
            set_current_state(TASK_UNINTERRUPTIBLE);
            stime = schedule_timeout(rtime);
            /* 累加实际等待时间(可通过 AUDIT_STATUS_BACKLOG_WAIT_TIME_ACTUAL 查询)*/
            atomic_add(rtime - stime, &audit_backlog_wait_time_actual);
            remove_wait_queue(&audit_backlog_wait, &wait);
        } else {
            /* 不可睡眠上下文(atomic context):直接丢弃 */
            if (audit_rate_check() && printk_ratelimit())
                pr_warn("audit_backlog=%d > audit_backlog_limit=%d\n", ...);
            audit_log_lost("backlog limit exceeded");
            return NULL;  /* 返回 NULL,调用者得不到 audit_buffer */
        }
    }
}

audit_backlog_wait_time_actual 记录系统因背压累计等待的时间,可用于监控审计队列的健康状态(kernel/audit.c 第 152 行)。

9.2 audit_rate_limit 速率限制

audit_rate_limitkernel/audit.c 第 127 行,默认 0 = 无限制)控制每秒最多处理的审计记录数:

/* kernel/audit.c: 356-382 */
static int audit_rate_check(void)
{
    static unsigned long last_check = 0;
    static int           messages   = 0;
    static DEFINE_SPINLOCK(lock);

    if (!audit_rate_limit)
        return 1;  /* 0 = 无限制,快速通过 */

    spin_lock_irqsave(&lock, flags);
    if (++messages < audit_rate_limit) {
        retval = 1;   /* 未达上限,允许 */
    } else {
        now = jiffies;
        if (time_after(now, last_check + HZ)) {
            /* 新的 1s 周期:重置计数器 */
            last_check = now;
            messages   = 0;
            retval     = 1;
        }
        /* 同一秒内超过上限:拒绝(retval 保持 0) */
    }
    spin_unlock_irqrestore(&lock, flags);
    return retval;
}

速率检查在 audit_log_start() 的背压逻辑中被使用(不可睡眠上下文分支),同时也在 audit_log_lost() 中用于控制警告日志的输出频率。

9.3 audit_lost 计数与失败策略

audit_lost 是全局原子计数器(kernel/audit.c 第 147 行),跟踪因各种原因丢失的记录总数:

static atomic_t audit_lost = ATOMIC_INIT(0);

丢失原因(注释在 kernel/audit.c 第 140-146 行):

0) [suppressed in audit_alloc] - TASK 过滤链拒绝
1) out of memory in audit_log_start [kmalloc of struct audit_buffer]
2) out of memory in audit_log_move [alloc_skb]
3) suppressed due to audit_rate_limit
4) suppressed due to audit_backlog_limit

audit_log_lost() 实现(kernel/audit.c 第 392-422 行):

void audit_log_lost(const char *message)
{
    static unsigned long last_msg = 0;
    static DEFINE_SPINLOCK(lock);

    atomic_inc(&audit_lost);  /* 始终递增计数 */

    /* 强制输出条件:PANIC 模式或无速率限制 */
    print = (audit_failure == AUDIT_FAIL_PANIC || !audit_rate_limit);

    if (!print) {
        /* 每秒至多输出一次 printk 警告 */
        spin_lock_irqsave(&lock, flags);
        now = jiffies;
        if (time_after(now, last_msg + HZ)) {
            print = 1;
            last_msg = now;
        }
        spin_unlock_irqrestore(&lock, flags);
    }

    if (print) {
        if (printk_ratelimit())
            pr_warn("audit_lost=%u audit_rate_limit=%u audit_backlog_limit=%u\n",
                    atomic_read(&audit_lost),
                    audit_rate_limit,
                    audit_backlog_limit);
        audit_panic(message);  /* 按 audit_failure 策略处理 */
    }
}

三种失败策略(include/uapi/linux/audit.h 第 381-383 行):

#define AUDIT_FAIL_SILENT   0  /* 静默丢弃,仅递增 audit_lost */
#define AUDIT_FAIL_PRINTK   1  /* 通过 printk 输出(默认,不影响系统运行) */
#define AUDIT_FAIL_PANIC    2  /* 内核 panic(高安全场景,绝不允许丢失记录) */

audit_do_config_change() 用于统一处理参数修改(kernel/audit.c 第 443-467 行),会在参数改变时先生成 AUDIT_CONFIG_CHANGE 记录,并检查 audit_enabled == AUDIT_LOCKED(状态 2 = 锁定)来拒绝不允许的修改:

static int audit_do_config_change(char *function_name, u32 *to_change, u32 new)
{
    /* 锁定状态下拒绝修改 */
    if (audit_enabled == AUDIT_LOCKED)
        allow_changes = 0;

    if (audit_enabled != AUDIT_OFF) {
        /* 记录此次配置变更 */
        rc = audit_log_config_change(function_name, new, old, allow_changes);
    }

    if (allow_changes == 1)
        *to_change = new;
    return rc;
}

10. 用户态工具与内核的交互

auditctl 规则管理

auditctl -a always,exit -F arch=b64 -S openat -F uid=1000 -k file_open 的内核路径:

用户态 auditctl:
  构造 struct audit_rule_data {
      flags = AUDIT_FILTER_EXIT | AUDIT_FILTER_PREPEND(可选)
      action = AUDIT_ALWAYS
      field_count = N
      fields[] = [{AUDIT_ARCH, Audit_equal, AUDIT_ARCH_X86_64},
                  {AUDIT_UID, Audit_equal, uid=1000},
                  {AUDIT_FILTERKEY, Audit_equal, "file_open"}]
      mask[8] = AUDIT_BIT(257)  # openat 系统调用号 257
  }
  sendmsg(fd, AUDIT_ADD_RULE, ...)

内核 audit_receive_msg():
  case AUDIT_ADD_RULE:
    audit_rule_change(AUDIT_ADD_RULE, seq, data, data_len)
      -> audit_data_to_entry(data, data_len)
         -> audit_to_entry_common(rule)     # 验证 flags/action
         -> 遍历 field_count:
              AUDIT_ARCH: f->val = AUDIT_ARCH_X86_64
              AUDIT_UID:  f->uid = make_kuid(&init_user_ns, 1000)
              AUDIT_FILTERKEY: entry->rule.filterkey = kstrdup("file_open")
         -> return entry
      -> audit_add_rule(entry)
         -> mutex_lock(&audit_filter_mutex)
         -> 检查重复(audit_find_rule)
         -> list_add_tail_rcu(&entry->list,
                              &audit_filter_list[AUDIT_FILTER_EXIT])
         -> list_add_tail(&entry->rule.list,
                          &audit_rules_list[AUDIT_FILTER_EXIT])
         -> audit_n_rules++
         -> mutex_unlock(&audit_filter_mutex)
      -> 记录 AUDIT_CONFIG_CHANGE (type=1305)

auditctl -s(查询状态)对应 AUDIT_GET(type=1000),内核返回 struct audit_status 包含所有全局参数当前值。

auditctl -e 2(锁定规则)通过 AUDIT_SETaudit_enabled 设为 AUDIT_LOCKED=2,此后任何 AUDIT_ADD_RULE/AUDIT_DEL_RULE 请求都会被拒绝并记录 AUDIT_CONFIG_CHANGE 事件,直到系统重启。

ausearch 工作原理

ausearch 不直接与内核交互,而是解析 auditd 写入的 /var/log/audit/audit.log 文件。它通过 (timestamp, serial) 元组将属于同一事件的多条记录关联合并:

type=SYSCALL msg=audit(T:N): ...  <- 事件开始
type=PATH    msg=audit(T:N): ...  <- 同一 (T:N)
type=CWD     msg=audit(T:N): ...  <- 同一 (T:N)
type=EOE     msg=audit(T:N):      <- 事件结束

常用检索维度:-ua(loginuid)、-f(文件名)、-k(规则 key)、-p(PID)、--success(是否成功)、--start/--end(时间范围)。

aureport 报告生成

aureport 同样读取日志文件生成统计报告,不与内核交互。支持 --summary(汇总)、-f(文件访问)、-u(用户)、--avc(AVC 事件)、--login(登录事件)等多种报告模式。

auditd 守护进程

auditd 通过标准 NETLINK_AUDIT socket 与内核通信:

  1. 注册:发送 AUDIT_SET(AUDIT_STATUS_PID=自身PID) 建立连接
  2. 接收:在 socket 上 poll()/read() 接收内核发来的审计记录
  3. 写日志:将记录追加写入 /var/log/audit/audit.log
  4. 转发:通过 audisp(audit dispatcher)插件转发给 syslog/SIEM 等

/etc/audit/auditd.conf 关键参数与内核的映射:

backlog_limit  <->  audit_backlog_limit (AUDIT_STATUS_BACKLOG_LIMIT)
rate_limit     <->  audit_rate_limit (AUDIT_STATUS_RATE_LIMIT)
priority_boost <->  设置 auditd 进程的 nice 值(用户态)

规则文件 /etc/audit/rules.d/*.rulesauditd 启动时通过 augenrules 工具合并,再通过 auditctl -R 批量下发到内核。


11. 初始化流程

Audit 子系统在 postcore_initcall 阶段初始化(kernel/audit.c 第 1729-1766 行),在内存分配器(core_initcall)和 Netlink 子系统(subsys_initcall)之后,设备模型(device_initcall)之前:

static int __init audit_init(void)
{
    int i;

    if (audit_initialized == AUDIT_DISABLED)  /* audit=0 参数 */
        return 0;

    /* 1. 创建 audit_buffer slab 缓存 */
    audit_buffer_cache = KMEM_CACHE(audit_buffer, SLAB_PANIC);

    /* 2. 初始化三级 skb 队列 */
    skb_queue_head_init(&audit_queue);
    skb_queue_head_init(&audit_retry_queue);
    skb_queue_head_init(&audit_hold_queue);

    /* 3. 初始化 inode 哈希表(32 个桶) */
    for (i = 0; i < AUDIT_INODE_BUCKETS; i++)
        INIT_LIST_HEAD(&audit_inode_hash[i]);

    /* 4. 初始化控制命令互斥锁 */
    mutex_init(&audit_cmd_mutex.lock);
    audit_cmd_mutex.owner = NULL;

    pr_info("initializing netlink subsys (%s)\n",
            str_enabled_disabled(audit_default));

    /* 5. 在所有网络命名空间注册 NETLINK_AUDIT 套接字 */
    register_pernet_subsys(&audit_net_ops);
    /* audit_net_init() 调用:
     *   netlink_kernel_create(net, NETLINK_AUDIT, &cfg)
     *   cfg.input = audit_receive
     *   cfg.groups = AUDIT_NLGRP_MAX
     *   aunet->sk->sk_sndtimeo = HZ/10  (100ms 发送超时) */

    audit_initialized = AUDIT_INITIALIZED;

    /* 6. 启动 kauditd 内核线程 */
    kauditd_task = kthread_run(kauditd_thread, NULL, "kauditd");
    if (IS_ERR(kauditd_task))
        panic("audit: failed to start the kauditd thread (%d)\n",
              PTR_ERR(kauditd_task));

    /* 7. 记录初始化完成(AUDIT_KERNEL=2000) */
    audit_log(NULL, GFP_KERNEL, AUDIT_KERNEL,
              "state=initialized audit_enabled=%u res=1",
              audit_enabled);

    return 0;
}
postcore_initcall(audit_init);

内核命令行参数处理(kernel/audit.c 第 1772-1814 行):

/* audit=0|off|1|on */
static int __init audit_enable(char *str)
{
    if (!strcasecmp(str, "off") || !strcmp(str, "0"))
        audit_default = AUDIT_OFF;
    else
        audit_default = AUDIT_ON;

    if (audit_default == AUDIT_OFF)
        audit_initialized = AUDIT_DISABLED;  /* 完全跳过初始化 */
    audit_set_enabled(audit_default);
    return 1;
}
__setup("audit=", audit_enable);

/* audit_backlog_limit=N */
static int __init audit_backlog_limit_set(char *str)
{
    kstrtouint(str, 0, &audit_backlog_limit);
    return 1;
}
__setup("audit_backlog_limit=", audit_backlog_limit_set);

LSM 集成初始化:各 LSM 通过 audit_cfg_lsm() 注册其提供的安全上下文类型(kernel/audit.c 第 304-320 行)。SELinux 在初始化时调用:

audit_cfg_lsm(&selinux_lsmid,
              AUDIT_CFG_LSM_SECCTX_SUBJECT | AUDIT_CFG_LSM_SECCTX_OBJECT);

之后 audit_subj_lsms[]audit_obj_lsms[] 中有 SELinux 的 lsm_idaudit_log_task_context()audit_log_obj_ctx() 会循环调用这些 LSM 的 secctx 接口生成 subj=/obj= 字段。


12. 审计记录格式与完整事件示例

完整事件:open("/etc/passwd", O_RDONLY)

规则:auditctl -a always,exit -F arch=b64 -S open -F key=passwd_read

# 系统调用记录(AUDIT_SYSCALL=1300)
type=SYSCALL msg=audit(1710000000.000:42):
  arch=c000003e syscall=2 success=yes exit=3
  a0=7ffed4e2a000 a1=0 a2=1b6 a3=0 items=1
  ppid=1000 pid=1234 auid=1000 uid=0 gid=0
  euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0
  tty=pts0 ses=1 comm="cat" exe="/bin/cat"
  subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
  key="passwd_read"

# 路径记录(AUDIT_PATH=1302)
type=PATH msg=audit(1710000000.000:42):
  item=0 name="/etc/passwd" inode=131074 dev=fd:00
  mode=0100644 ouid=0 ogid=0 rdev=00:00
  obj=system_u:object_r:passwd_file_t:s0
  nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0
  cap_fver=0 cap_frootid=0

# 当前工作目录(AUDIT_CWD=1307)
type=CWD msg=audit(1710000000.000:42): cwd="/root"

# 进程标题(AUDIT_PROCTITLE=1327,hex 编码)
type=PROCTITLE msg=audit(1710000000.000:42):
  proctitle=636174002F6574632F706173737764
  # 解码:cat\0/etc/passwd

# 事件结束(AUDIT_EOE=1320)
type=EOE msg=audit(1710000000.000:42):

SELinux AVC 拒绝事件(关联 syscall)

# AVC 拒绝(AUDIT_AVC=1400)
type=AVC msg=audit(1710000000.001:100):
  avc:  denied  { read } for
  pid=2345 comm="httpd" name="shadow"
  dev="dm-0" ino=131075
  scontext=system_u:system_r:httpd_t:s0
  tcontext=system_u:object_r:shadow_t:s0
  tclass=file permissive=0

# 关联的 syscall(AUDIT_SYSCALL=1300)
type=SYSCALL msg=audit(1710000000.001:100):
  arch=c000003e syscall=2 success=no exit=-13
  a0=7f... a1=0 a2=1b6 a3=0 items=1
  ppid=1 pid=2345 auid=4294967295 uid=48 gid=48
  euid=48 suid=48 fsuid=48 egid=48 sgid=48 fsgid=48
  tty=(none) ses=4294967295 comm="httpd"
  exe="/usr/sbin/httpd"
  subj=system_u:system_r:httpd_t:s0 key=(null)

type=PATH msg=audit(1710000000.001:100): item=0 name="/etc/shadow" ...
type=CWD msg=audit(1710000000.001:100): cwd="/var/www"
type=EOE msg=audit(1710000000.001:100):

审计配置变更(AUDIT_CONFIG_CHANGE=1305)

type=CONFIG_CHANGE msg=audit(1710000000.002:101):
  auid=0 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0
  op=add_rule key="passwd_read"
  list=4 res=1

13. 性能设计要点

三层快速路径

第一层:编译期禁用CONFIG_AUDIT=n

所有 audit_* 公共函数编译为空内联函数(include/linux/audit.h 第 218 行的 #else /* CONFIG_AUDIT */ 分支),零代码生成,零运行时开销。

第二层:运行时禁用audit_enabled = AUDIT_OFF

__audit_syscall_entry() 第一行检查 audit_enabled,立即返回。公共包装检查 audit_context()(对于从未分配过 context 的任务为 NULL),立即返回。

第三层:无规则快速路径context->dummy

/* kernel/auditsc.c: 2006 */
context->dummy = !audit_n_rules;

当系统无任何审计规则时(audit_n_rules == 0),dummy=1__audit_syscall_exit() 的检查(第 2039-2041 行):

if (!context || context->dummy ||
    context->context != AUDIT_CTX_SYSCALL)
    goto out;  /* 直接跳到 audit_reset_context */

此时每次 syscall 的审计开销约为:一次 audit_context() 内联检查(读取 task_struct->audit_context)+ 一次 dummy 字段检查,共约 2-3 个 CPU 指令。

关键同步机制

RCU 读锁:过滤链遍历(audit_filter_syscallaudit_filter_inodes)全程持 rcu_read_lock(),无自旋锁争用,支持多核并行。

规则写操作audit_filter_mutex 保护写操作,采用 copy-on-write 模式:

mutex_lock(&audit_filter_mutex)
new_entry = audit_dupe_rule(old_entry)  # 复制
# 修改 new_entry
list_replace_rcu(&old->list, &new->list)  # 原子替换
mutex_unlock(&audit_filter_mutex)
call_rcu(&old->rcu, audit_free_rule_rcu)  # 延迟释放

auditd 连接auditd_conn_lock 自旋锁保护写操作,RCU 保护读操作。kauditd_thread 频繁读取 auditd_conn,RCU 无锁读取是性能关键。

audit_queueskb_queue 内置自旋锁,但生产者(audit_log_end)和消费者(kauditd)在不同 CPU 上并行运行,锁竞争较少。

内存分配优化

结构 分配方式 说明
audit_buffer kmem_cache_alloc(audit_buffer_cache) slab 缓存,避免频繁 alloc/free
audit_context kzalloc_obj() 任务创建时一次分配,syscall 间复用(仅 reset)
audit_names 前 5 个:内嵌 preallocated_names[] 避免绝大多数情况下的动态分配
audit_names(超出) kzalloc_obj() + should_free=true 少见场景
skb(Netlink 消息) nlmsg_new(AUDIT_BUFSIZ=1024) 1KB 初始大小,pskb_expand_head() 按需扩展

关于 ktime_get_coarse_real_ts64()

audit_context 中的时间戳使用 CLOCK_REALTIME_COARSE(粗粒度实时钟),精度为 1 个 jiffy(通常 1-10ms),但读取开销远低于精确版本的 CLOCK_REALTIME,是性能与精度的平衡选择(kernel/auditsc.c 第 2021 行)。


数据流完整总览

系统调用被调用(arch/x86/entry/entry_64.S 等)
         |
         v
audit_syscall_entry(major, a0, a1, a2, a3)
  if audit_context():
    __audit_syscall_entry(major, a0, a1, a2, a3)
      context->arch = syscall_get_arch()
      context->major = major
      context->argv[0..3] = a0..a3
      context->context = AUDIT_CTX_SYSCALL
      ktime_get_coarse_real_ts64(&context->stamp.ctime)

系统调用执行过程中(VFS/net/ipc/... 层):
  audit_getname(filename)       -> names_list += {name}
  audit_inode(name, dentry, 0)  -> names_list[i].{ino,dev,mode,uid,gid,oprop,fcap}
  audit_inode_child(parent, dentry, type) -> names_list += child info
  __audit_ipc_obj(ipcp)         -> context->ipc.{uid,gid,mode,oprop}
  __audit_socketcall(nargs,args) -> context->socketcall.{nargs,args}
  avc_has_perm() 失败时:
    -> slow_avc_audit()
    -> audit_log_start(AUDIT_AVC) -> skb -> audit_queue

系统调用返回(arch/x86/entry/entry_64.S 等):
         |
         v
audit_syscall_exit(pt_regs)
  if audit_context():
    __audit_syscall_exit(success, return_code)
      audit_return_fixup(...)
      audit_filter_syscall(current, ctx)    # EXIT 链
      audit_filter_inodes(current, ctx)     # inode 哈希链
      if ctx->current_state == RECORD:
        audit_log_exit()
          audit_log_start/format/end * N   # 生成多条记录
          每条 audit_log_end():
            skb_queue_tail(&audit_queue, skb)
            wake_up(&kauditd_wait)
      audit_reset_context(ctx)             # 重置供下次使用

kauditd_thread(内核线程,被 wake_up 唤醒):
  while queue 非空:
    kauditd_send_multicast_skb()           # 多播给监听者
    netlink_unicast(sk, skb, portid, 0)    # 单播给 auditd
    失败 -> retry/hold queue

auditd 守护进程(用户空间):
  read(netlink_fd) -> 接收审计记录
  write(log_fd, ...) -> 追加到 /var/log/audit/audit.log
  audisp -> 转发给 syslog/SIEM

由 Claude Code 分析生成