深入 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

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 对比

FAN_CLASS_NOTIF 仅通知,无需决策 TuxClocker、审计工具
特性 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

七、内核源码关键路径分析

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) 打赏

评论列表 共有 0 条评论

暂无评论
典型用途 反病毒、EDR、审计 同步工具(rsync)、文件管理器 细粒度分析、追踪