1. 基本介绍

Fanotify (Filesystem wide access notification) 是一个 notifier,即一种对文件系统变化产生通知的机制。fanotify是inotify的一个新进版本,主要是用于文件系统扫描的检测和分层存储的管理。最近几年对fanotify的完善也是很快的,查看了一下源码可以看出来fanotify支持的文件系统事件已经比inotify多了。

fanotify与inotify最大区别是fanotify加入了打开关闭等事件的许可判断:

即在打开或者关闭文件之前,需要程序员注册一个函数,根据程序所需要去判断是否允许打开文件或者关闭文件,然后将判断的结果再写入内核中,此时内核会执行该结果(相当于Ring0级别的Hook)。很明显,这几个文件系统事件可以用来实现一个文件监测控制系统,除了文件系统的扫描以外还可以控制文件的打开关闭等操作。很符合杀毒软件的开发。

1.1 基本原理

fanotify功能的使用分为以下几步:

  1. 用户调用fanotify_init()函数创建一个fd供监控者使用。在内核中这个fd对应一个fsnotify_group结构体。
  2. 用户调用fanotify_mark()函数来确定需要监控哪些文件对象以及哪些类型的事件消息。在内核中会把fsnotify_group和需要监控的文件对象链接起来。
  3. 用户调用read()函数来读取fd接收到的监控事件消息并处理。在内核中文件对象发生相应类型的事件时,会把事件消息发送到fsnotify_group的接收消息队列中。
  4. 如果注册了FAN_OPEN_PERM/FAN_ACCESS_PERM类型的监控,在接收到事件消息后,需要对当前的操作做许可判断(Allow/Deny),并调用write()函数把许可结果回写给内核。在内核中文件对象发生FAN_OPEN_PERM/FAN_ACCESS_PERM类型的事件后会把当前进程挂起并等待用户监控程序的判断,用户程序通过write()函数下发允许结果后,阻塞的进程恢复执行(Continue/Return)。

fanotify的内部数据关系如下图所示,用户创建了一个监控group,一个group可以监控多个文件对象(inode/mount)。同时一个文件对象(inode/mount)可以被多个group所监控。group和监控对象是多对多的关系:

在监控对象的事件发生时,通过fsnotify_mark建立起来的关系把事件发送到group的消息队列中。用户通过read()操作来读取消息队列,获得监控事件:

以下是访问控制功能的基本流程图:

Step 1. 被监控的inode对象被进程进行(FAN_OPEN_PERM|FAN_ACCESS_PERM)类型的操作时,会通过fsnotify_mark建立起来的关系,发送event消息到group的消息队列中。
Step 2. 判断有人在监控(FAN_OPEN_PERM|FAN_ACCESS_PERM)类型的消息,访问inode的进程会调用waie_event()把自己挂起,等待监控进程对自己的操作权限进行判断。
Step 3. 监控进程通过read()操作读到group消息队列中的所有监控消息并处理。
Step 4. 监控进程读取到(FAN_OPEN_PERM|FAN_ACCESS_PERM)类型的消息,判断目标进程是否有权限进行操作,通过write()操作将判断结果进行下发,并且唤醒目标进程来接收判断结果,进行继续(Allow)或者返回(Deny)操作。

1.2 fanotify基本功能

  1. 文件系统事件的通知
    这个功能和inotify的功能是一样的,即监听一些普遍的文件系统事件,例如读写打开关闭等操作的发生。当这些事件发生后,fanotify就会通知程序员发生了什么事件。

  2. 实现文件系统的监测管控
    该功能是实现全文件的系统的监测。相对于inotify来讲,fanotify在该功能上更具备扩展性。inotify在进行监控的时候,是需要一个wd对象进行监控的,所以当文件系统一复杂,wd的维护就变得麻烦。而fanotify所支持的三种模式可以使得该管理变得简洁。

Fanotify 有三个个基本的模式:directed,per-mount 和 global

directed 模式和 inotify 类似,直接工作在被监控的对象的 inode 上,一次只可以监控一个对象。因此需要监控大量目标时也很麻烦。
Global 模式则监控整个文件系统,任何变化都会通知 Listener。杀毒软件便工作在这种模式下。
Per-mount 模式工作在 mount 点上,比如磁盘 /dev/sda2 的 mount 点在 /home,则 /home 目录下的所有文件系统变化都可以被监控,这其实可以被看作另外一种 Global 模式。

长期以来,人们都希望 Linux 的 notifier 可以支持 sub-tree 通知,比如图 2 的众多监控对象都在 /home 目录下面,假如 notifier 可以指定监控整个 /home 目录,其下任意文件或者子目录的变化都可以引起通知,监控程序便无需为每一个 /home 下面的子目录和文件一一添加 watch descriptor 了。
在很久以前,Fanotify 就暗示说实现 sub-tree notification 不是不可能的,但直到今天 fanotify 依然无法支持 sub-tree 监控。但比 inotify 进了一步的是,fanotify 可以监控某个目录下的直接子节点。比如可以监控 /home 和他的直接子节点,文件 /home/foo1,/home/foo2 等都可以被监控,但 /home/pics/foo1 就不可以了,因为 /home/pics/foo1 不是 /home 的直接子节点。希望在后续的 fanotify 版本中可以弥补这个不足。
面对 sub-tree 监控的需要,目前 fanotify 的折中方案是采用 Global 模式,然后在监控程序内部进行判断,剔除那些不感兴趣的文件系统对象。这虽然不完美,但也算一个可行的方案吧。相比 inotify,有一点儿总比完全没有好一些吧。

  1. 访问控制
    访问控制这个功能是之前inotify没有的,这是fanotify和inotify之间最大的不同之处。fanotify增加了访问控制的事件,例如:FAN_OPEN_PERM、FAN_CLOSE_PERM等,这些事件需要程序员通过程序需要判断该文件是否被允许打开或者关闭操作,并把该决策向内核进行写入和注册,最后让系统调用返回该结果。

  2. 监听者级别的划分
    该功能便是允许多个Listener监听同一个文件对象,并且可以设置Listener的级别。fanotify将所有的Listener设置了三个group,其优先从高到低分别为:FAN_CLASS_PRE_CONTENT、FAN_CLASS_CONTENT、FAN_CLASS_NOTIF

从上述宏的命名也大致可知:

FAN_CLASS_PRE_CONTENT 用于 HSM 等需要在应用程序使用文件的 CONTENT 之前就得到文件操作权的应用程序;
FAN_CLASS_CONTENT 适用于杀毒软件等需要检查文件 CONTENT 的软件;
FAN_CLASS_NOTIF 则用于纯粹的 notification 软件,不需要访问文件内容的应用程序。

  1. 监听者程序的PID过滤
    监听者程序也可能会触发fanotify中的事件,在fanotify中,如果触发者是Listener的PID的话,fanotify就会忽略该事件,避免两者进入死循环。能够实现该过滤的原因是fanotify在事件中包含了Listener的PID,而inotify并不具备该功能。

  2. 缓存机制
    例如一个文件没有进行任何修改,但是每次都需要扫描监测的话会很麻烦,这时候fanotify的缓存机制就有很好的体现,假设它对某个文件对象设置了ignore mask标志位的话,只会对该对象进行一次扫描,之后如果该文件对象没有进行任何修改的话,它的打开关闭操作就会正常执行,fanotify会忽略访问控制事件,始终允许访问。如果进行了修改的话,fanotify会自动清除它的ignore mask标志位,然后对它进行正常的访问控制等事件。

Fanotify 支持这种 cache,也叫做 ignore marks。它的工作原理很简单,假如对一个文件系统对象设置了 ignore marks,那么下次该文件被访问时,相应的事件便不会触发访问控制的代码,从而始终允许该文件的访问。
杀毒软件可以这样使用此特性,当应用程序第一次打开文件 file A 时,Fanotify 将通知杀毒软件 AV 进行文件内容扫描,如果 AV 软件发现该文件没有病毒,在允许本次访问的同时,对该文件设置一个 ignore mark。

2. 用户态实现

2.1 实例代码

这里有一份fanotify的用户态使用的代码:fanotify example userspace tools。

void synopsis(const char *progname, int status)
{
    FILE *file = status ? stderr : stdout;

    fprintf(file, "USAGE: %s [-" FANOTIFY_ARGUMENTS "] "
        "[-o {open,close,access,modify,open_perm,access_perm}] "
        "file ...\n"
        "-c: learn about events on children of a directory (not decendants)\n"
        "-d: send events which happen to directories\n"
        "-f: set premptive ignores (go faster)\n"
        "-h: this help screen\n"
        "-m: place mark on the whole mount point, not just the inode\n"
        "-n: do not ignore repeated permission checks\n"
        "-p: check permissions, not just notification\n"
        "-s N: sleep N seconds before replying to perm events\n",
        progname);
    exit(status);
}

int main(int argc, char *argv[])
{
    int opt;
    uint64_t fan_mask = FAN_OPEN | FAN_CLOSE | FAN_ACCESS | FAN_MODIFY;
    unsigned int mark_flags = FAN_MARK_ADD, init_flags = 0;
    bool opt_child, opt_on_mount, opt_add_perms, opt_fast, opt_ignore_perm;
    int opt_sleep;
    ssize_t len;
    char buf[4096];
    fd_set rfds;
    struct sigaction sa;

    sa.sa_flags = SA_SIGINFO | SA_RESTART;
    sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = usr1_handler;
    if (sigaction(SIGUSR1, &sa, NULL) == -1)
        goto fail;

    opt_child = opt_on_mount = opt_add_perms = opt_fast = false;
    opt_ignore_perm = true;
    opt_sleep = 0;

    /* (0) 命令的参数解析 */
    while ((opt = getopt(argc, argv, "o:s:"FANOTIFY_ARGUMENTS)) != -1) {
        switch(opt) {
            case 'o': {
                char *str, *tok;

                fan_mask = 0;
                str = optarg;
                while ((tok = strtok(str, ",")) != NULL) {
                    str = NULL;
                    if (strcmp(tok, "open") == 0)
                        fan_mask |= FAN_OPEN;
                    else if (strcmp(tok, "close") == 0)
                        fan_mask |= FAN_CLOSE;
                    else if (strcmp(tok, "access") == 0)
                        fan_mask |= FAN_ACCESS;
                    else if (strcmp(tok, "modify") == 0)
                        fan_mask |= FAN_MODIFY;
                    else if (strcmp(tok, "open_perm") == 0)
                        fan_mask |= FAN_OPEN_PERM;
                    else if (strcmp(tok, "access_perm") == 0)
                        fan_mask |= FAN_ACCESS_PERM;
                    else
                        synopsis(argv[0], 1);
                }
                break;
            }
            case 'c':
                opt_child = true;
                break;
            case 'd':
                fan_mask |= FAN_ONDIR;
                break;
            case 'f':
                opt_fast = true;
                opt_ignore_perm = true;
                break;
            case 'm':
                opt_on_mount = true;
                break;
            case 'n':
                opt_fast = false;
                opt_ignore_perm = false;
                break;
            case 'p':
                opt_add_perms = true;
                break;
            case 's':
                opt_sleep = atoi(optarg);
                break;
            case 'h':
                synopsis(argv[0], 0);
            default:  /* '?' */
                synopsis(argv[0], 1);
        }
    }
    if (optind == argc)
        synopsis(argv[0], 1);

    if (opt_child)
        fan_mask |= FAN_EVENT_ON_CHILD;

    if (opt_on_mount)
        mark_flags |= FAN_MARK_MOUNT;

    if (opt_add_perms)
        fan_mask |= FAN_ALL_PERM_EVENTS;

    if (fan_mask & FAN_ALL_PERM_EVENTS)
        init_flags |= FAN_CLASS_CONTENT;
    else
        init_flags |= FAN_CLASS_NOTIF;

    /* (1) 创建fanotify对应的文件句柄fd */
    fan_fd = fanotify_init(init_flags, O_RDONLY | O_LARGEFILE);
    if (fan_fd < 0)
        goto fail;

    /* (2) 配置fd上需要监控的对象和操作类型 */
    for (; optind < argc; optind++)
        if (mark_object(fan_fd, argv[optind], AT_FDCWD, fan_mask, mark_flags) != 0)
            goto fail;

    FD_ZERO(&rfds);
    FD_SET(fan_fd, &rfds);

    while (select(fan_fd+1, &rfds, NULL, NULL, NULL) < 0)
        if (errno != EINTR)
            goto fail;

    /* (3) 通过fd的read()操作来接收监控消息 */
    while ((len = read(fan_fd, buf, sizeof(buf))) > 0) {
        struct fanotify_event_metadata *metadata;
        char path[PATH_MAX];
        int path_len;

        /* (4) 逐个取出监控event消息并处理 */
        metadata = (void *)buf;
        while(FAN_EVENT_OK(metadata, len)) {
            if (metadata->vers < 2) {
                fprintf(stderr, "Kernel fanotify version too old\n");
                goto fail;
            }

            /* (4.1) 忽略后续的重复消息 */
            if (metadata->fd >= 0 &&
                opt_fast &&
                set_ignored_mask(fan_fd, metadata->fd,
                         FAN_ALL_EVENTS | FAN_ALL_PERM_EVENTS))
                goto fail;

            if (metadata->fd >= 0) {
                sprintf(path, "/proc/self/fd/%d", metadata->fd);
                path_len = readlink(path, path, sizeof(path)-1);
                if (path_len < 0)
                    goto fail;
                path[path_len] = '\0';
                printf("%s:", path);
            } else
                printf("?:");

            /* (4.2) 对一些特殊目录,忽略重复消息 */
            set_special_ignored(fan_fd, metadata->fd, path);

            printf(" pid=%ld", (long)metadata->pid);

            if (metadata->mask & FAN_ACCESS)
                printf(" access");
            if (metadata->mask & FAN_OPEN)
                printf(" open");
            if (metadata->mask & FAN_MODIFY)
                printf(" modify");
            if (metadata->mask & FAN_CLOSE) {
                if (metadata->mask & FAN_CLOSE_WRITE)
                    printf(" close(writable)");
                if (metadata->mask & FAN_CLOSE_NOWRITE)
                    printf(" close");
            }
            if (metadata->mask & FAN_OPEN_PERM)
                printf(" open_perm");
            if (metadata->mask & FAN_ACCESS_PERM)
                printf(" access_perm");

            /* (4.3) 权限允许消息的处理 */
            if (metadata->mask & FAN_ALL_PERM_EVENTS) {
                if (opt_sleep)
                    sleep(opt_sleep);

                /* (4.3.1) fd的write()操作来发送允许的结果 */
                if (handle_perm(fan_fd, metadata))
                    goto fail;

                /* (4.3.2) 忽略后续的重复消息 */
                if (metadata->fd >= 0 &&
                    opt_ignore_perm &&
                    set_ignored_mask(fan_fd, metadata->fd,
                             metadata->mask))
                    goto fail;
            }

            printf("\n");
            fflush(stdout);

            /* (4.4) 关闭消息中的fd,并且取下一个消息 */
            if (metadata->fd >= 0 && close(metadata->fd) != 0)
                goto fail;
            metadata = FAN_EVENT_NEXT(metadata, len);
        }
        while (select(fan_fd+1, &rfds, NULL, NULL, NULL) < 0)
            if (errno != EINTR)
                goto fail;
    }
    if (len < 0)
        goto fail;
    return 0;

fail:
    fprintf(stderr, "%s\n", strerror(errno));
    return 1;
}


int handle_perm(int fan_fd, struct fanotify_event_metadata *metadata)
{
    struct fanotify_response response_struct;
    int ret;

    /* (4.3.1.1) 对操作进行判断,是否允许 */
    response_struct.fd = metadata->fd;
    response_struct.response = FAN_ALLOW;

    /* (4.3.1.2) fd的write()操作来发送允许的结果 */
    ret = write(fan_fd, &response_struct, sizeof(response_struct));
    if (ret < 0)
        return ret;

    return 0;
}

2.2 API介绍

fanotify_init()
#include <fcntl.h>
#include <sys/fanotify.h>
int fanotify_init(unsigned int flags, unsigned int event_f_flags);

该函数初始化fanotify事件组,并且该fanotify组的事件队列的int类型句柄。它的另一个优势,在这里可以看出,可以通过epoll、select、kqueue等监听。

第1个flags参数包含一个多位字段,该字段定义侦听应用程序的通知类,并进一步包含一个位字段,指定文件描述符的行为。其中包括:

FAN_CLASS_PRE_CONTENT
此值允许接收通知文件已被访问的事件,以及可能访问文件时用于权限决策的事件。它适用于需要在包含最终数据之前访问文件的事件侦听器。例如,分层存储管理器可能使用这个通知类。

FAN_CLASS_CONTENT
此值允许接收通知文件已被访问的事件,以及可能访问文件时用于权限决策的事件。它是为那些需要访问已经包含最终内容的文件的事件侦听器而设计的。例如,恶意软件检测程序可能会使用这个通知类。

FAN_REPORT_FID (since Linux 5.1)
此值允许接收包含有关与事件关联的底层文件系统对象的附加信息的事件。附加结构封装了关于对象的信息,并与通用事件元数据结构一起包含。用来表示与事件相关的对象的文件描述符被替换为文件句柄。它适用于可能发现使用文件句柄标识对象比使用文件描述符更合适的应用程序。此外,它还可以用于对目录条目事件感兴趣的应用程序,例如FAN_CREATE、FAN_ATTRIB、FAN_MOVE和FAN_DELETE。注意,在监视挂载点时不支持使用目录修改事件。此标志不允许使用FAN_CLASS_CONTENT或FAN_CLASS_PRE_CONTENT,并将导致错误EINVAL。更多信息请参见fanotify(7)。

FAN_CLASS_NOTIF(默认值)
这是默认值。它不需要指定。此值只允许接收通知文件已被访问的事件。不可能在访问文件之前做出权限决定。

第2个参数event_f_flags和open函数的第二个参数意义相同。event_f_flags参数定义将在为fanotify事件创建的打开文件描述上设置的文件状态标志。有关这些标志的详细信息,请参见open(2)中的标志值描述。event_f_flags包含一个用于访问模式的多位字段。

该字段可以取以下值:O_RDONLY、O_WRONLY、O_RDWR.

fanotify_mark()
#include <sys/fanotify.h>
int fanotify_mark(int fanotify_fd, unsigned int flags,
                         uint64_t mask, int dirfd, const char *pathname);

fanotify_mark()在文件系统对象上添加、删除或修改fanotify标记。调用者必须对要标记的文件系统对象具有读权限。

第1个参数fanotify_fd为fanotify_init()函数的返回值。

第2个参数flags标志位是描述要执行的操作:

#define FAN_MARK_ADD        0x00000001
#define FAN_MARK_REMOVE     0x00000002
#define FAN_MARK_DONT_FOLLOW    0x00000004
#define FAN_MARK_ONLYDIR    0x00000008
#define FAN_MARK_MOUNT      0x00000010
#define FAN_MARK_IGNORED_MASK   0x00000020
#define FAN_MARK_IGNORED_SURV_MODIFY    0x00000040
#define FAN_MARK_FLUSH      0x00000080

第3个参数mask描述的是需要监控的事件:

/* the following events that user-space can register for */
#define FAN_ACCESS      0x00000001  /* File was accessed */
#define FAN_MODIFY      0x00000002  /* File was modified */
#define FAN_CLOSE_WRITE     0x00000008  /* Writtable file closed */
#define FAN_CLOSE_NOWRITE   0x00000010  /* Unwrittable file closed */
#define FAN_OPEN        0x00000020  /* File was opened */
#define FAN_Q_OVERFLOW      0x00004000  /* Event queued overflowed */

#define FAN_OPEN_PERM       0x00010000  /* File open in perm check */
#define FAN_ACCESS_PERM     0x00020000  /* File accessed in perm check */

#define FAN_ONDIR       0x40000000  /* event occurred against dir */
#define FAN_EVENT_ON_CHILD  0x08000000  /* interested in child events */

第4个参数dirfd和第5个参数pathname描述的是监控点路径。优先使用pathname来确定路径,否则使用dirfd来确定路径。

3. 内核实现

3.1 配置fanotify

  1. fanotify_init()
    相对于用户态的fanotify_init()函数,内核有一个同名的系统调用与之对应。该函数的主要作用是分配了一个fsnotify_group,并且分配了一个anon inode和fd方便用户态来操作。

    /* fanotify syscalls */
    SYSCALL_DEFINE2(fanotify_init, unsigned int, flags, unsigned int, event_f_flags)
    {
     struct fsnotify_group *group;
     int f_flags, fd;
     struct user_struct *user;
     struct fanotify_event_info *oevent;
    
     pr_debug("%s: flags=%d event_f_flags=%d\n",
         __func__, flags, event_f_flags);
    
     if (!capable(CAP_SYS_ADMIN))
         return -EPERM;
    
     if (flags & ~FAN_ALL_INIT_FLAGS)
         return -EINVAL;
    
     if (event_f_flags & ~FANOTIFY_INIT_ALL_EVENT_F_BITS)
         return -EINVAL;
    
     switch (event_f_flags & O_ACCMODE) {
     case O_RDONLY:
     case O_RDWR:
     case O_WRONLY:
         break;
     default:
         return -EINVAL;
     }
    
     user = get_current_user();
     if (atomic_read(&user->fanotify_listeners) > FANOTIFY_DEFAULT_MAX_LISTENERS) {
         free_uid(user);
         return -EMFILE;
     }
    
     f_flags = O_RDWR | FMODE_NONOTIFY;
     if (flags & FAN_CLOEXEC)
         f_flags |= O_CLOEXEC;
     if (flags & FAN_NONBLOCK)
         f_flags |= O_NONBLOCK;
    
     /* fsnotify_alloc_group takes a ref.  Dropped in fanotify_release */
     /* (1) 分配核心数据结构:fsnotify_group
         fanotify_fsnotify_ops包含了group接收到消息后的处理函数
      */
     group = fsnotify_alloc_group(&fanotify_fsnotify_ops);
     if (IS_ERR(group)) {
         free_uid(user);
         return PTR_ERR(group);
     }
    
     /* (2)  */
     group->fanotify_data.user = user;
     atomic_inc(&user->fanotify_listeners);
    
     /* (3) 预先分配好overflow event */
     oevent = fanotify_alloc_event(NULL, FS_Q_OVERFLOW, NULL);
     if (unlikely(!oevent)) {
         fd = -ENOMEM;
         goto out_destroy_group;
     }
     group->overflow_event = &oevent->fse;
    
     if (force_o_largefile())
         event_f_flags |= O_LARGEFILE;
     group->fanotify_data.f_flags = event_f_flags;
    #ifdef CONFIG_FANOTIFY_ACCESS_PERMISSIONS
     spin_lock_init(&group->fanotify_data.access_lock);
     init_waitqueue_head(&group->fanotify_data.access_waitq);
     INIT_LIST_HEAD(&group->fanotify_data.access_list);
    #endif
    
     /* (4) 根据传入flags参数中的标志,设置group的优先级 */
     switch (flags & FAN_ALL_CLASS_BITS) {
     case FAN_CLASS_NOTIF:
         group->priority = FS_PRIO_0;
         break;
     case FAN_CLASS_CONTENT:
         group->priority = FS_PRIO_1;
         break;
     case FAN_CLASS_PRE_CONTENT:
         group->priority = FS_PRIO_2;
         break;
     default:
         fd = -EINVAL;
         goto out_destroy_group;
     }
    
     /* (5) 配置group消息队列的大小 */
     if (flags & FAN_UNLIMITED_QUEUE) {
         fd = -EPERM;
         if (!capable(CAP_SYS_ADMIN))
             goto out_destroy_group;
         group->max_events = UINT_MAX;
     } else {
         group->max_events = FANOTIFY_DEFAULT_MAX_EVENTS;
     }
    
     if (flags & FAN_UNLIMITED_MARKS) {
         fd = -EPERM;
         if (!capable(CAP_SYS_ADMIN))
             goto out_destroy_group;
         group->fanotify_data.max_marks = UINT_MAX;
     } else {
         group->fanotify_data.max_marks = FANOTIFY_DEFAULT_MAX_MARKS;
     }
    
     /* (6) 分配inode/fd,和group链接起来
         fanotify_fops包含了group的文件接口操作
      */
     fd = anon_inode_getfd("[fanotify]", &fanotify_fops, group, f_flags);
     if (fd < 0)
         goto out_destroy_group;
    
     return fd;
    out_destroy_group:
     fsnotify_destroy_group(group);
     return fd;
    }
  2. fanotify_mark()
    相对于用户态的fanotify_mark()函数,内核有一个同名的系统调用与之对应。该函数给每一个监控点创建了中间变量fsnotify_mark,一个fsnotify_group通过多个fsnotify_mark和多个监控点建立起了链接关系。

    SYSCALL_DEFINE5(fanotify_mark, int, fanotify_fd, unsigned int, flags,
                   __u64, mask, int, dfd,
                   const char  __user *, pathname)
    {
     struct inode *inode = NULL;
     struct vfsmount *mnt = NULL;
     struct fsnotify_group *group;
     struct fd f;
     struct path path;
     int ret;
    
     pr_debug("%s: fanotify_fd=%d flags=%x dfd=%d pathname=%p mask=%llx\n",
          __func__, fanotify_fd, flags, dfd, pathname, mask);
    
     /* we only use the lower 32 bits as of right now. */
     if (mask & ((__u64)0xffffffff << 32))
         return -EINVAL;
    
     /* (0) flag和mask的一系列合法性判断 */
     if (flags & ~FAN_ALL_MARK_FLAGS)
         return -EINVAL;
     switch (flags & (FAN_MARK_ADD | FAN_MARK_REMOVE | FAN_MARK_FLUSH)) {
     case FAN_MARK_ADD:      /* fallthrough */
     case FAN_MARK_REMOVE:
         if (!mask)
             return -EINVAL;
         break;
     case FAN_MARK_FLUSH:
         if (flags & ~(FAN_MARK_MOUNT | FAN_MARK_FLUSH))
             return -EINVAL;
         break;
     default:
         return -EINVAL;
     }
    
     if (mask & FAN_ONDIR) {
         flags |= FAN_MARK_ONDIR;
         mask &= ~FAN_ONDIR;
     }
    #ifdef CONFIG_FANOTIFY_ACCESS_PERMISSIONS
     if (mask & ~(FAN_ALL_EVENTS | FAN_ALL_PERM_EVENTS | FAN_EVENT_ON_CHILD))
    #else
     if (mask & ~(FAN_ALL_EVENTS | FAN_EVENT_ON_CHILD))
    #endif
         return -EINVAL;
    
     f = fdget(fanotify_fd);
     if (unlikely(!f.file))
         return -EBADF;
    
     /* verify that this is indeed an fanotify instance */
     ret = -EINVAL;
     if (unlikely(f.file->f_op != &fanotify_fops))
         goto fput_and_out;
    
     /* (1) 通过fd找到对应的group结构 */
     group = f.file->private_data;
    
     /*
      * group->priority == FS_PRIO_0 == FAN_CLASS_NOTIF.  These are not
      * allowed to set permissions events.
      */
     ret = -EINVAL;
     if (mask & FAN_ALL_PERM_EVENTS &&
         group->priority == FS_PRIO_0)
         goto fput_and_out;
    
     /* (2) FAN_MARK_FLUSH,清理掉group上所有的监控点 */
     if (flags & FAN_MARK_FLUSH) {
         ret = 0;
         if (flags & FAN_MARK_MOUNT)
             fsnotify_clear_vfsmount_marks_by_group(group);
         else
             fsnotify_clear_inode_marks_by_group(group);
         goto fput_and_out;
     }
    
     /* (3) 根据pathname或者dfd,找到监控点对应的path结构
         普通模式监控inode
         mount模式监控mnt
      */
     ret = fanotify_find_path(dfd, pathname, &path, flags);
     if (ret)
         goto fput_and_out;
    
     /* inode held in place by reference to path; group by fget on fd */
     if (!(flags & FAN_MARK_MOUNT))
         inode = path.dentry->d_inode;
     else
         mnt = path.mnt;
    
     /* create/update an inode mark */
     /* (4) 根据flags中的FAN_MARK_ADD/FAN_MARK_REMOVE命令,
         来给group增加/移除监控点
      */
     switch (flags & (FAN_MARK_ADD | FAN_MARK_REMOVE)) {
     case FAN_MARK_ADD:
         if (flags & FAN_MARK_MOUNT)
             ret = fanotify_add_vfsmount_mark(group, mnt, mask, flags);
         else
             ret = fanotify_add_inode_mark(group, inode, mask, flags);
         break;
     case FAN_MARK_REMOVE:
         if (flags & FAN_MARK_MOUNT)
             ret = fanotify_remove_vfsmount_mark(group, mnt, mask, flags);
         else
             ret = fanotify_remove_inode_mark(group, inode, mask, flags);
         break;
     default:
         ret = -EINVAL;
     }
    
     path_put(&path);
    fput_and_out:
     fdput(f);
     return ret;
    }
  3. fanotify_add_inode_mark()
    普通inode模式,将group和inode监控点链接起来:

    static int fanotify_add_inode_mark(struct fsnotify_group *group,
                    struct inode *inode, __u32 mask,
                    unsigned int flags)
    {
     struct fsnotify_mark *fsn_mark;
     __u32 added;
    
     pr_debug("%s: group=%p inode=%p\n", __func__, group, inode);
    
     /*
      * If some other task has this inode open for write we should not add
      * an ignored mark, unless that ignored mark is supposed to survive
      * modification changes anyway.
      */
     if ((flags & FAN_MARK_IGNORED_MASK) &&
         !(flags & FAN_MARK_IGNORED_SURV_MODIFY) &&
         (atomic_read(&inode->i_writecount) > 0))
         return 0;
    
     mutex_lock(&group->mark_mutex);
     fsn_mark = fsnotify_find_inode_mark(group, inode);
     if (!fsn_mark) {
         /* (4.1) 分配fsnotify_mark结构用来记录group和inode监控点的对应关系
             将fsnotify_mark加入group->marks_list链表
             将fsnotify_mark加入inode->i_fsnotify_marks链表
          */
         fsn_mark = fanotify_add_new_mark(group, inode, NULL);
         if (IS_ERR(fsn_mark)) {
             mutex_unlock(&group->mark_mutex);
             return PTR_ERR(fsn_mark);
         }
     }
    
     /* (4.2) 根据mask指定的监控类型给fsnotify_mark赋值
         Normal:tmask = fsn_mark->mask | mask;
         Ignore:tmask = fsn_mark->ignored_mask | mask;
      */
     added = fanotify_mark_add_to_mask(fsn_mark, mask, flags);
     mutex_unlock(&group->mark_mutex);
    
     if (added & ~inode->i_fsnotify_mask)
         fsnotify_recalc_inode_mask(inode);
    
     fsnotify_put_mark(fsn_mark);
     return 0;
    }
  4. fanotify_add_vfsmount_mark()
    mount模式,将group和mount监控点链接起来:

    static int fanotify_add_vfsmount_mark(struct fsnotify_group *group,
                       struct vfsmount *mnt, __u32 mask,
                       unsigned int flags)
    {
     struct fsnotify_mark *fsn_mark;
     __u32 added;
    
     mutex_lock(&group->mark_mutex);
     fsn_mark = fsnotify_find_vfsmount_mark(group, mnt);
     if (!fsn_mark) {
         /* (4.1) 分配fsnotify_mark结构用来记录group和mount监控点的对应关系
             将fsnotify_mark加入group->marks_list链表
             将fsnotify_mark加入m->mnt_fsnotify_marks链表
          */
         fsn_mark = fanotify_add_new_mark(group, NULL, mnt);
         if (IS_ERR(fsn_mark)) {
             mutex_unlock(&group->mark_mutex);
             return PTR_ERR(fsn_mark);
         }
     }
    
     /* (4.2) 根据mask指定的监控类型给fsnotify_mark赋值
         Normal:tmask = fsn_mark->mask | mask;
         Ignore:tmask = fsn_mark->ignored_mask | mask;
      */
     added = fanotify_mark_add_to_mask(fsn_mark, mask, flags);
     mutex_unlock(&group->mark_mutex);
    
     if (added & ~real_mount(mnt)->mnt_fsnotify_mask)
         fsnotify_recalc_vfsmount_mask(mnt);
    
     fsnotify_put_mark(fsn_mark);
     return 0;
    }

3.2 触发fanotify

访问一个文件触发fanotify事件。

read() -> vfs_read() -> fsnotify_access() -> fsnotify() -> send_to_group() -> group->ops->handle_event() -> fanotify_handle_event() -> fanotify_get_response() :

static int fanotify_handle_event(...)
{
    ...

    /* (1) 上报event消息,给需要侦测这个inode/mnt节点的group/fd */
    event = fanotify_alloc_event(inode, mask, data);
    if (unlikely(!event))
        return -ENOMEM;

    fsn_event = &event->fse;
    /* (1.1) 将event消息加入到group->notification_list消息链表中 */
    ret = fsnotify_add_event(group, fsn_event, fanotify_merge);
    if (ret) {
        /* Permission events shouldn't be merged */
        BUG_ON(ret == 1 && mask & FAN_ALL_PERM_EVENTS);
        /* Our event wasn't used in the end. Free it. */
        fsnotify_destroy_event(group, fsn_event);

        return 0;
    }

    /* (2) 如果是权限拦截类型的消息,需要阻塞住当前进程,等待用户的策略处理 */
#ifdef CONFIG_FANOTIFY_ACCESS_PERMISSIONS
    if (mask & FAN_ALL_PERM_EVENTS) {
        ret = fanotify_get_response(group, FANOTIFY_PE(fsn_event));
        fsnotify_destroy_event(group, fsn_event);
    }
#endif
    return ret;
}
#ifdef CONFIG_FANOTIFY_ACCESS_PERMISSIONS
static int fanotify_get_response(struct fsnotify_group *group,
                 struct fanotify_perm_event_info *event)
{
    int ret;

    pr_debug("%s: group=%p event=%p\n", __func__, group, event);

    /* (2.1) 阻塞进消息队列,等待用户的策略处理  */
    wait_event(group->fanotify_data.access_waitq, event->response);

    /* (2.2) 根据用户选择的策略,决定放行还是拦截 */
    /* userspace responded, convert to something usable */
    switch (event->response) {
    case FAN_ALLOW:
        ret = 0;
        break;
    case FAN_DENY:
    default:
        ret = -EPERM;
    }
    event->response = 0;

    pr_debug("%s: group=%p event=%p about to return ret=%d\n", __func__,
         group, event, ret);

    return ret;
}
#endif

3.3 响应fanotify

用户态的处理:

int main(int argc, char *argv[])
{
    ...

    /* (1) 创建fanotify监控的fd */
    fan_fd = fanotify_init(init_flags, O_RDONLY | O_LARGEFILE);
    if (fan_fd < 0)
        goto fail;

    /* (2) 使用fd,配置需要监控那些文件的那些事件 */
    for (; optind < argc; optind++)
        if (mark_object(fan_fd, argv[optind], AT_FDCWD, fan_mask, mark_flags) != 0)
            goto fail;

    /* (3) 读取fd中的fanotify事件消息,并处理 */
    while ((len = read(fan_fd, buf, sizeof(buf))) > 0) {
        struct fanotify_event_metadata *metadata;
        char path[PATH_MAX];
        int path_len;

        metadata = (void *)buf;
        while(FAN_EVENT_OK(metadata, len)) {

            /* (3.1) 权限拦截类的消息处理 */
            if (metadata->mask & FAN_ALL_PERM_EVENTS) {
                if (opt_sleep)
                    sleep(opt_sleep);

                if (handle_perm(fan_fd, metadata))
                    goto fail;
                if (metadata->fd >= 0 &&
                    opt_ignore_perm &&
                    set_ignored_mask(fan_fd, metadata->fd,
                             metadata->mask))
                    goto fail;
            }

            printf("\n");
            fflush(stdout);

            if (metadata->fd >= 0 && close(metadata->fd) != 0)
                goto fail;
            metadata = FAN_EVENT_NEXT(metadata, len);
        }
        ...
    }
    ...
}

int handle_perm(int fan_fd, struct fanotify_event_metadata *metadata)
{
    struct fanotify_response response_struct;
    int ret;

    response_struct.fd = metadata->fd;
    response_struct.response = FAN_ALLOW;

    /* (3.1.1) 通过fd的写操作来发送处理策略 */
    ret = write(fan_fd, &response_struct, sizeof(response_struct));
    if (ret < 0)
        return ret;

    return 0;
}

内核态处理:

fanotify_write() -> process_access_response():

static int process_access_response(struct fsnotify_group *group,
                   struct fanotify_response *response_struct)
{
    struct fanotify_perm_event_info *event;
    int fd = response_struct->fd;
    int response = response_struct->response;

    pr_debug("%s: group=%p fd=%d response=%d\n", __func__, group,
         fd, response);
    /*
     * make sure the response is valid, if invalid we do nothing and either
     * userspace can send a valid response or we will clean it up after the
     * timeout
     */
    switch (response) {
    case FAN_ALLOW:
    case FAN_DENY:
        break;
    default:
        return -EINVAL;
    }

    if (fd < 0)
        return -EINVAL;

    /* (3.1.1.1) 设置处理结果 */
    event = dequeue_event(group, fd);
    if (!event)
        return -ENOENT;

    event->response = response;

    /* (3.1.1.2) 唤醒等待处理策略的文件操作线程 */
    wake_up(&group->fanotify_data.access_waitq);

    return 0;
}
  1. fanotify_read()
    用户态通过read()系统调用读取event消息,最终调用到了fanotify_read()。该函数的主要作用就是在group的消息链表(group->notification_list)中读取消息。

  2. fanotify_write()
    用户态通过write()系统来通知fanotify的判断结果,最终调用到了fanotify_write()。该函数的主要作用在process_access_response()中体现。

参考文档:
1.利用fanotify进行文件系统实时监测的认识
2.linux fanotify和inotify
3.fanotify example userspace tools