Fanotify (Filesystem wide access notification) 是一个 notifier,即一种对文件系统变化产生通知的机制。fanotify是inotify的一个新进版本,主要是用于文件系统扫描的检测和分层存储的管理。最近几年对fanotify的完善也是很快的,查看了一下源码可以看出来fanotify支持的文件系统事件已经比inotify多了。
fanotify与inotify最大区别是fanotify加入了打开关闭等事件的许可判断:
即在打开或者关闭文件之前,需要程序员注册一个函数,根据程序所需要去判断是否允许打开文件或者关闭文件,然后将判断的结果再写入内核中,此时内核会执行该结果(相当于Ring0级别的Hook)。很明显,这几个文件系统事件可以用来实现一个文件监测控制系统,除了文件系统的扫描以外还可以控制文件的打开关闭等操作。很符合杀毒软件的开发。
fanotify功能的使用分为以下几步:
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)操作。
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,有一点儿总比完全没有好一些吧。
从上述宏的命名也大致可知:
FAN_CLASS_PRE_CONTENT 用于 HSM 等需要在应用程序使用文件的 CONTENT 之前就得到文件操作权的应用程序; FAN_CLASS_CONTENT 适用于杀毒软件等需要检查文件 CONTENT 的软件; FAN_CLASS_NOTIF 则用于纯粹的 notification 软件,不需要访问文件内容的应用程序。
Fanotify 支持这种 cache,也叫做 ignore marks。它的工作原理很简单,假如对一个文件系统对象设置了 ignore marks,那么下次该文件被访问时,相应的事件便不会触发访问控制的代码,从而始终允许该文件的访问。 杀毒软件可以这样使用此特性,当应用程序第一次打开文件 file A 时,Fanotify 将通知杀毒软件 AV 进行文件内容扫描,如果 AV 软件发现该文件没有病毒,在允许本次访问的同时,对该文件设置一个 ignore mark。
这里有一份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;
}
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来确定路径。
访问一个文件触发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
用户态的处理:
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;
}
fanotify_read()
用户态通过read()系统调用读取event消息,最终调用到了fanotify_read()。该函数的主要作用就是在group的消息链表(group->notification_list)中读取消息。
fanotify_write()
用户态通过write()系统来通知fanotify的判断结果,最终调用到了fanotify_write()。该函数的主要作用在process_access_response()中体现。参考文档: 1.利用fanotify进行文件系统实时监测的认识 2.linux fanotify和inotify 3.fanotify example userspace tools