深入 Linux fanotify 架构:从文件系统监控到安全审计的工程实践
一、引言:从 inotify 到 fanotify 的演进
文件系统监控是安全审计、入侵检测、防病毒引擎和配置管理的基础能力。Linux 内核提供了三种主要的文件事件通知机制:dnotify(早期,2.4 内核)、inotify(2005 年合并)和 fanotify(2010 年合并,2.6.36)。
inotify 通过 inotify_init() + inotify_add_watch() 可以监控文件/目录的打开、关闭、修改等事件,但它存在两个关键缺陷:无法拦截访问决策(只能事后通知),以及在高并发场景下性能差。
fanotify(File Access Notification)的设计目标是解决 inotify 的安全感知缺陷。它能够在 VFS 层注入访问决策点,允许监控程序决定是否允许某次文件操作,这是现代 EDR(端点检测与响应)引擎和反病毒软件的核心基础设施。
本文将深入剖析 fanotify 的内核实现架构,从系统调用接口、内核事件分发机制、访问权限控制模型到生产环境中的典型应用场景,带你全面理解这一关键的内核子系统。
二、fanotify 系统调用与用户态接口
2.1 初始化与标记
#include <sys/fanotify.h>
// 创建 fanotify 文件描述符
// 参数:event_f_flags 为 open()/read() 使用的文件标志
// fanotify_flags 为 FAN_CLASS_NOTIF 或 FAN_CLASS_CONTENT
int fanotify_init(unsigned int event_f_flags, unsigned int fanotify_flags);
fanotify_init() 有几种关键模式:
Fanotify 标记
语义
典型用户
FAN_CLASS_PRE_CONTENT
读取前通知,需决策
企业级反病毒
FAN_CLASS_CONTENT
读取就绪后通知
Linux 桌面 ClamAV
FAN_CLASS_NOTIF
仅通知,无需决策
TuxClocker、审计工具
2.2 标记目标
// 对目录/文件/挂载点添加监控标记
int fanotify_mark(int fanotify_fd, unsigned int flags,
__u64 mask, int dirfd, const char *pathname);
关键的 mark 标记(flags):
FAN_MARK_ADD:添加规则
FAN_MARK_FLUSH:清空已有规则
FAN_MARK_MOUNT:监控整个挂载点(使用 dirfd 指定挂载路径)
FAN_MARK_ONLYDIR:仅匹配目录
FAN_MARK_IGNORE:忽略特定路径(Linux 6.0+)
FAN_MARK_EVICTABLE:允许内核在内存压力下驱逐
FAN_MARK_IGNORE_SURV:仅忽略指定路径本身,不忽略其子目录
2.3 事件掩码
// 常用事件掩码
#define FAN_ACCESS 0x01 // 文件被访问(read)
#define FAN_MODIFY 0x02 // 文件被修改(write)
#define FAN_ATTRIB 0x04 // 元数据修改(chmod/chown/utimes)
#define FAN_CLOSE_WRITE 0x08 // 以写模式打开的文件关闭
#define FAN_CLOSE_NOWRITE 0x10 // 以只读模式打开的文件关闭
#define FAN_OPEN 0x20 // 文件/目录被打开
#define FAN_MOVED_FROM 0x40 // 文件从监控区域移出
#define FAN_MOVED_TO 0x80 // 文件移入监控区域
#define FAN_CREATE 0x100 // 在监控目录下创建文件
#define FAN_DELETE 0x200 // 在监控目录下删除文件
#define FAN_DELETE_SELF 0x400 // 监控的目录本身被删除
#define FAN_MOVE_SELF 0x800 // 监控的目录本身被移动
#define FAN_OPEN_EXEC 0x1000 // 文件被 exec 打开(Linux 5.0+)
#define FAN_OPEN_EXEC_PERM 0x40000000 // exec 访问权限检查(Linux 5.0+)
#define FAN_ACCESS_PERM 0x10000000 // 文件访问权限检查
#define FAN_ONDIR 0x40000000 // 对目录的事件
#define FAN_EVENT_ON_CHILD 0x08000000 // 对目录下子文件/子目录的事件
三、内核架构:从 VFS 到 fanotify 事件分发
3.1 内核关键数据结构
┌─────────────────────────────────────────────────────────────┐
│ fanotify 内核架构 │
├─────────────────────────────────────────────────────────────┤
│ 用户进程 │
│ ├── ClamAV → FAN_CLASS_CONTENT │
│ ├── Audit → FAN_CLASS_NOTIF │
│ └── EDR → FAN_CLASS_PRE_CONTENT │
├─────────────────────────────────────────────────────────────┤
│ fanotify_group ── 每个 fd 对应一个,持有规则链表和事件队列 │
│ fanotify_event ── 事件对象,包含 pid、fd、文件名等 │
│ fanotify_fa_info ── FAN_REPORT_FID 时的文件句柄信息 │
├─────────────────────────────────────────────────────────────┤
│ VFS 拦截点 │
│ vfs_open() → fanotify_path_event() │
│ vfs_read() → fanotify_file_event() │
│ vfs_write() → fanotify_file_event() │
│ inode_permission() → fanotify_inode() │
├─────────────────────────────────────────────────────────────┤
│ LSM(Linux Security Module)钩子 │
│ security_file_open() │
│ security_file_permission() │
│ security_file_receive() │
│ security_file_truncate() │
└─────────────────────────────────────────────────────────────┘
3.2 fanotify_group 核心实现
// include/linux/fanotify.h
struct fanotify_group {
// 事件等待队列
wait_queue_head_t access_wait;
// 规则链表头
struct list_head fanotify_list;
// 已缓存但未发送的事件队列
struct list_head access_list;
// 当前组的角色:notification 或 permission
enum fanotify_state state;
// 支持的事件类型特征集
u32 f_flags;
// fd 引用
struct file *fanotify_file;
// 用于 cookie 递增——队列满时合并 PERM 事件
u32 q_len;
// 是否启用队列满时的合并
u32 max_queue_size;
};
3.3 事件分发流程
当用户进程读文件时的完整调用链:
vfs_read(file, buf, count, pos)
└── security_file_permission(file, MAY_READ)
└── fanotify_file_mask_path()
└── fanotify_groups_list
└── 遍历匹配的 fanotify_group
└── fanotify_handle_event() // 创建 event 对象
└── 将 event 加入 group 的 pending 队列
└── 有 PERM 事件时:唤醒 access_wait
如果需要权限决策(FAN_CLASS_PRE_CONTENT 或 FAN_ACCESS_PERM),内核会阻塞发起文件操作的进程,直到用户态监控程序通过 write() 向 fanotify fd 响应:
struct fanotify_response {
__s32 fanotify_fd; // 对应事件中的 fd
__u32 response; // FAN_ALLOW 或 FAN_DENY
};
四、关键特性深度解析
4.1 FAN_REPORT_FID 与文件句柄
Linux 5.1 引入的 FAN_REPORT_FID 允许 fanotify 报告完整路径的文件句柄(file handle),而非传统的事件产生时刻的 fd。这是 fanotify 的一次重大架构变更:
传统模式:event.fd = 打开文件的 fd(进程私有,监控程序不一定能使用)
FID 模式:event.info_type = FAN_EVENT_INFO_TYPE_FD 或 FAN_EVENT_INFO_TYPE_DFID_NAME
event.info = struct fanotify_event_info_fid {
struct file_handle handle; // 内核文件句柄
__u32 pid; // 发起操作的进程 pid
}
这一设计的重大意义在于:监控程序可以通过 name_to_handle_at() / open_by_handle_at() 获取文件的稳定标识,即使原始文件已被关闭。
4.2 LSM 文件钩子扩展
fanotify 通过 LSM hook 实现了深度集成:
// security/security.c
static struct security_hook_list fanotify_hooks[] __lsm_ro_after_init = {
LSM_HOOK_INIT(file_open, fanotify_file),
LSM_HOOK_INIT(file_permission, fanotify_file), // 已废弃,替换为专用 hook
LSM_HOOK_INIT(file_receive, fanotify_file),
};
在 Linux 5.1 中重构为专用 hook:
// include/linux/lsm_hook_defs.h
LSM_HOOK(int, 0, fanotify_access, struct file *file, mask)
这允许其他 LSM(如 SELinux、AppArmor)在 fanotify 决策前后执行自己的策略检查。
4.3 内存压力驱逐(Evictable Marks)
Linux 6.0 引入 FAN_MARK_EVICTABLE,允许内核在内存压力下驱逐 fanotify 的监控标记。这对以下场景至关重要:
// 监控程序崩溃但未清理 mark → 标记驻留内核
// 大量 mark 导致内存占用过高 → 新标记创建失败
// 解决方式:使用 FAN_MARK_EVICTABLE,内核可回收
五、生产环境实战
5.1 ClamAV Clamd 中的 fanotify 集成
ClamAV 的反病毒引擎使用 FAN_CLASS_CONTENT 模式在文件读取时实时扫描:
// clamd/fanotifypid.c 简要实现
int setup_fanotify(void) {
fan_fd = fanotify_init(FAN_CLASS_CONTENT | FAN_CLOEXEC | FAN_NONBLOCK,
O_RDONLY | O_LARGEFILE);
if (fan_fd < 0) {
logg("!Unable to initialize fanotify: %s", strerror(errno));
return -1;
}
// 标记根目录,递归监控
if (fanotify_mark(fan_fd,
FAN_MARK_ADD | FAN_MARK_MOUNT,
FAN_ALL_EVENTS,
AT_FDCWD, "/") < 0) {
logg("!Unable to mark /: %s", strerror(errno));
}
return 0;
}
void fanotify_loop(void) {
struct fanotify_event_metadata *metadata;
char path[PATH_MAX];
int path_len;
while ((len = read(fan_fd, buf, sizeof(buf))) > 0) {
metadata = (struct fanotify_event_metadata *) buf;
// 获取被访问文件路径
snprintf(path, sizeof(path), "/proc/self/fd/%d", metadata->fd);
path_len = readlink(path, sizeof(path), path);
// 内存扫描
result = cl_scanfile(path, &virname, NULL, &scanopts);
// 发送响应
struct fanotify_response response = {
.fanotify_fd = metadata->fd,
.response = (result == CL_VIRUS) ? FAN_DENY : FAN_ALLOW
};
write(fan_fd, &response, sizeof(response));
close(metadata->fd);
}
}
5.2 EDR 系统监控模型
企业级 EDR 产品使用 fanotify 监控关键路径:
监控目标:
/etc/passwd, /etc/shadow → 账户信息变更检测
/bin, /usr/bin, /usr/sbin → 可执行文件篡改检测
/root/.ssh, /home/*/.ssh → SSH 密钥窃取检测
/var/log → 日志篡改检测
*.php, *.py → Webshell 检测
处理策略:
扫描文件哈希 → 对比威胁情报
检测 ELF → 分析 ELF 头异常
检测脚本 → 匹配恶意代码模式
不符合策略 → FAN_DENY 阻断
5.3 性能优化要点
fanotify 在高并发场景下的调优关键:
# Python + ctypes 的高性能 fanotify 实现要点
import os
import ctypes
import select
# 1. 使用 NONBLOCK 模式避免阻塞
fan_fd = libc.fanotify_init(
0x02 | 0x0004, # FAN_CLASS_CONTENT | FAN_NONBLOCK
os.O_RDONLY | os.O_NONBLOCK
)
# 2. 使用 epoll 就绪通知,而非忙等待
epoll_fd = select.epoll()
epoll_fd.register(fan_fd, select.EPOLLIN)
# 3. 批量读取事件(一次 read 返回多个事件)
while True:
events = epoll_fd.poll(1)
for fd, event in events:
buf = os.read(fan_fd, 65536) # 64KB buffer
# 遍历所有 event metadata 处理
# 4. 使用 FAN_REPORT_FID 避免 fd 耗尽
# 5. 限制 mask 范围,避免不必要的事件上报
# FAN_OPEN_PERM | FAN_ACCESS_PERM 不要叠加所有事件
六、fanotify vs inotify vs eBPF 对比
特性
fanotify
inotify
eBPF (fsnotify)
引入内核版本
2.6.36
2.6.13
4.x+(fsnotify 监听)
可阻断访问
支持 PERM 类型
否
有限制
挂载点监控
原生支持
需手动递归
支持
文件句柄信息
FAN_REPORT_FID
否
可通过 kprobe 获取
事件队列弹性
可驱逐
不可驱逐
通过 map 人工控制
性能开销
中(维护 group 匹配)
高(每进程 watch)
低(内核内过滤)
权限要求
需要 CAP_SYS_ADMIN
无需特殊权限
需要 CAP_BPF
典型用途
反病毒、EDR、审计
同步工具(rsync)、文件管理器
细粒度分析、追踪
七、内核源码关键路径分析
7.1 事件创建路径
// fs/notify/fanotify/fanotify.c
static long fanotify_handle_event(struct fsnotify_group *group, u32 mask,
const void *data, int data_type,
struct inode *dir,
const struct qstr *file_name, u32 cookie)
{
struct fanotify_event *event;
struct fanotify_event_info_fid *fid = NULL;
struct file *idle_file = NULL;
// 检查是否需要上报
if (!fsnotify_notify_enabled(mask, inode_mark_ignored_mask))
return 0;
// 分配事件对象
event = kmem_cache_alloc(fanotify_event_cachep, GFP_KERNEL);
if (!event)
return -ENOMEM;
// 填充事件元数据
event->mask = mask;
event->pid = pid;
// 如果需要 FID 信息,创建 file_handle
if (FAN_GROUP(group)->flags & FAN_REPORT_FID) {
fid = fanotify_alloc_fid_event(group, file, mask, &fid_tmp);
// 构造 file handle:调用 exportfs
}
// 加入事件队列
fsnotify_add_event(group, &event->fse, fanotify_merge);
return 0;
}
7.2 权限决策路径
// fs/notify/fanotify/fanotify.c
static int fanotify_handle_perm(struct fsnotify_group *group,
struct fanotify_event *event)
{
// 1. 将事件加入 access_list
list_add_tail(&event->access_list, &group->access_list);
// 2. 唤醒等待队列
wake_up(&group->access_wait);
// 3. 阻塞发起操作的进程,等待用户态响应
wait_event(group->access_wait,
event->response != FAN_RESPONSE_INVALID);
// 4. 返回响应结果
return event->response == FAN_ALLOW ? 0 : -EPERM;
}
这一设计确保了决策的原子性——即使用户态程序存在延迟,也不会导致未授权访问。
八、安全考量与最佳实践
8.1 安全边界
fanotify 监控程序运行在 CAP_SYS_ADMIN 权限下,需特别注意:
- 隔离失败风险:监控程序崩溃不应导致系统无法访问文件(使用 FAN_MARK_EVICTABLE)
- TOCTOU 竞争:在 PERM 决策和响应之间,文件内容可能被其他不受监控的路径修改
- 权限提升防护:监控程序自身需防范被注入修改响应为 FAN_ALLOW
8.2 生产环境检查清单
☐ 使用 FAN_MARK_EVICTABLE 防止内存泄漏
☐ 实现看门狗/ heartbeat 机制,监控程序异常时自动 fail-open
☐ 限制监控范围——只监控关键路径,避免全量文件监控
☐ 使用 FAN_REPORT_FID 获取稳定文件标识
☐ 对 deny 操作记录详细日志(pid、path、ppid)
☐ 定期清理不活跃的 group(fd 关闭时自动清理)
☐ 处理队列满(FAN_Q_OVERFLOW)——优先保证系统可用性
8.3 eBPF 融合趋势
Linux 6.x 内核正在探索将 fanotify 与 eBPF 结合,在用户态响应前进行内核侧的快速过滤:
[文件访问] → [VFS hook] → [fanotify] → [eBPF filter] → 匹配?快速决策
↓ 不匹配
[用户态监控程序] → 最终决策
这种混合架构可以在保证灵活性的同时,降低绝大多数正常访问的延迟开销。
九、总结
fanotify 是 Linux 安全架构中连接 VFS 层与用户态安全策略的关键桥梁。它为反病毒引擎、EDR 系统和文件审计提供了不可替代的架构能力。
理解 fanotify 的核心——从 fanotify_init 的类别选择、FAN_REPORT_FID 的文件句柄机制、到 PERM 事件的决策流程,是构建高性能安全监控系统的基础。
现代 Linux 环境中,几乎没有反病毒或 EDR 产品能在不依赖 fanotify 的前提下实现对文件访问的实时阻断能力。它是内核安全基础设施的基石之一。
评论列表 共有 0 条评论

发表评论 取消回复