基于 Linux 内核源码深度分析,覆盖
kernel/audit.c、kernel/auditsc.c、kernel/auditfilter.c、kernel/audit_watch.c、include/linux/audit.h
- 概述与设计目标
- 整体架构
- 核心数据结构
- 审计规则系统
- 系统调用审计
- 文件审计
- Netlink 接口
- 7.1 AUDIT_* 消息类型分区
- 7.2 kauditd 内核线程
- 7.3 auditd 连接管理:auditd_connection
- 7.4 三级队列机制
- 7.5 audit_receive_msg():命令处理
- SELinux AVC 审计
- 8.1 AVC 缓存与审计触发
- 8.2 avc_audit 回调机制
- 8.3 AVC 记录与 syscall 记录的关联
- 实时事件丢失处理
- 用户态工具与内核的交互
- 初始化流程
- 审计记录格式与完整事件示例
- 性能设计要点
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 行 |
用户空间 (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 退出时规则匹配确认需要记录,才执行重量级的格式化和写出工作。
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 复用,避免重复分配/释放的开销。
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_cache,kernel/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;
}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。
/* 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)。
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 后的进程能力 */
};audit_krule 定义于 include/linux/audit.h 第 42-60 行,是审计规则在内核中的完整表示(用户态规则 audit_rule_data 经 audit_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) = 8(mask[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 /* 确定记录(白名单规则) *//* 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 比较)等。
/* 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 时直接写。
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 = 0x10(include/uapi/linux/audit.h 第 185 行)是一个标志位,加在 flags 上表示将规则插入到链头而非链尾,实现优先匹配。
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));
}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;
}系统调用进入的钩子路径(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 进程自身产生审计记录(防止正反馈死循环)。
/* 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);
}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()")
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);文件 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 行)。
__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() 的核心工作:
- 检查
AUDIT_FILTER_FS链,决定是否需要记录此 inode - 在
ctx->names_list中查找是否已有相同name/inode的条目 - 若未找到则从
preallocated_names[]槽或通过kzalloc()分配新的audit_names - 调用
audit_copy_inode()填充:n->ino = inode->i_inon->dev = inode->i_sb->s_devn->mode = inode->i_moden->uid = inode->i_uidn->gid = inode->i_gidsecurity_inode_getlsmprop()->n->oprop(LSM 安全属性)audit_copy_fcaps()->n->fcap(文件 capability)
audit_inode_parent_hidden() 是一个便捷包装(include/linux/audit.h 第 399-405 行),用于记录父目录但不在日志中显示。
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_group(kernel/audit_watch.c 第 52 行),在 audit_watch_init() 中通过 fsnotify_alloc_group() 创建,其 handle_event 回调为 audit_watch_handle_event()。
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 的命名空间中发来的控制请求返回 -ECONNREFUSED(kernel/audit.c 第 1086-1087 行),而非 -EPERM,是为了让非特权容器中的 PAM 认为审计未编译进内核而正常登录(而非直接拒绝)。
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。
/* 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 注册流程:
- auditd 启动,创建
NETLINK_AUDITsocket - 发送
AUDIT_SET(type=1001),mask = AUDIT_STATUS_PID,pid = 自身 PID - 内核
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 队列等待重连。
内核审计事件产生
|
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);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 的进程)
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(拒绝)。
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
当 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=1425 和 AUDIT_MAC_OBJ_CONTEXTS=1426 类型记录分别输出。
audit_backlog_limit(kernel/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 行)。
audit_rate_limit(kernel/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() 中用于控制警告日志的输出频率。
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;
}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_SET 将 audit_enabled 设为 AUDIT_LOCKED=2,此后任何 AUDIT_ADD_RULE/AUDIT_DEL_RULE 请求都会被拒绝并记录 AUDIT_CONFIG_CHANGE 事件,直到系统重启。
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 同样读取日志文件生成统计报告,不与内核交互。支持 --summary(汇总)、-f(文件访问)、-u(用户)、--avc(AVC 事件)、--login(登录事件)等多种报告模式。
auditd 通过标准 NETLINK_AUDIT socket 与内核通信:
- 注册:发送
AUDIT_SET(AUDIT_STATUS_PID=自身PID)建立连接 - 接收:在 socket 上
poll()/read()接收内核发来的审计记录 - 写日志:将记录追加写入
/var/log/audit/audit.log - 转发:通过 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/*.rules 在 auditd 启动时通过 augenrules 工具合并,再通过 auditctl -R 批量下发到内核。
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_id,audit_log_task_context() 和 audit_log_obj_ctx() 会循环调用这些 LSM 的 secctx 接口生成 subj=/obj= 字段。
规则: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):
# 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):
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
第一层:编译期禁用(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_syscall、audit_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_queue:skb_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() 按需扩展 |
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 分析生成