fanotify-util: add helpers for reading events and marking inodes

Add src/basic/fanotify-util.[ch] with:

- union fanotify_event_buffer and FOREACH_FANOTIFY_EVENT() to read and iterate
  over events;
- fanotify_event_pin_fd(), which returns an O_PATH fd for the object an event
  refers to. The fd is opened via open_by_handle_at() using the file handle
  from the FID.
- fanotify_mark_fd(), to add or remove a mark for the inode referred to by a
  file descriptor; unlike fanotify_mark(), it also works for O_PATH file
  descriptors.
This commit is contained in:
Yu Watanabe committed 2026-10-12 03:40:42 +09:00
1 parent 23f243f364
commit fbe2857a7b
5 files changed
+299

No files matched your search

+116
View File
@@ -0,0 +1,116 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <fcntl.h>
#include <string.h>
#include "errno-util.h"
#include "fanotify-util.h"
#include "fd-util.h"
#include "stat-util.h"
#include "unaligned.h"
bool fanotify_event_iterator_next(FanotifyEventIterator *i, const struct fanotify_event_metadata **ret) {
assert(i);
assert(ret);
if (i->left < sizeof(struct fanotify_event_metadata))
return false;
/* Read event_len without going through struct fanotify_event_metadata: the struct requires 64-bit
* alignment (due to the __aligned_u64 mask field), but 'ptr' may only be 4-byte aligned, as the
* kernel pads events to FANOTIFY_EVENT_ALIGN (4). */
uint32_t event_len = unaligned_read_ne32(i->ptr);
if (event_len < sizeof(struct fanotify_event_metadata) ||
event_len > i->left ||
event_len > sizeof(i->aligned.raw))
return false;
/* Copy the whole event (metadata + info records) into an 8-byte aligned buffer, so the metadata and
* the trailing records can be dereferenced safely. */
memcpy(&i->aligned, i->ptr, event_len);
i->ptr += event_len;
i->left -= event_len;
*ret = &i->aligned.em;
return true;
}
static const struct fanotify_event_info_header* fanotify_event_info_header_first(const struct fanotify_event_metadata *e) {
assert(e);
return (const struct fanotify_event_info_header*) ((const uint8_t*) e + e->metadata_len);
}
static bool fanotify_event_info_header_is_valid(
const struct fanotify_event_info_header *h,
const struct fanotify_event_metadata *e) {
assert(h);
assert(e);
return (const uint8_t*) h + sizeof(*h) <= (const uint8_t*) e + e->event_len &&
h->len >= sizeof(*h) &&
(const uint8_t*) h + h->len <= (const uint8_t*) e + e->event_len;
}
static const struct fanotify_event_info_header* fanotify_event_info_header_next(const struct fanotify_event_info_header *h) {
assert(h);
return (const struct fanotify_event_info_header*) ((const uint8_t*) h + h->len);
}
/* This evaluates arguments multiple times */
#define FOREACH_FANOTIFY_EVENT_INFO(h, e) \
for (const struct fanotify_event_info_header *h = fanotify_event_info_header_first(e); \
fanotify_event_info_header_is_valid(h, e); \
h = fanotify_event_info_header_next(h))
#define FID_MIN_SIZE (offsetof(struct fanotify_event_info_fid, handle) + offsetof(struct file_handle, f_handle))
int fanotify_event_pin_fd(
const struct fanotify_event_metadata *e,
int mount_fd,
const struct statfs *sfs) {
assert(e);
assert(mount_fd >= 0);
FOREACH_FANOTIFY_EVENT_INFO(h, e) {
if (h->info_type != FAN_EVENT_INFO_TYPE_FID)
continue;
if (h->len < FID_MIN_SIZE)
return -EBADMSG;
const struct fanotify_event_info_fid *fid = (const struct fanotify_event_info_fid*) h;
struct file_handle *fh = (struct file_handle*) fid->handle;
if (h->len < FID_MIN_SIZE + fh->handle_bytes)
return -EBADMSG;
struct statfs our_sfs;
if (!statfs_is_set(sfs)) {
if (fstatfs(mount_fd, &our_sfs) < 0)
return -errno;
sfs = &our_sfs;
}
if (memcmp(&fid->fsid, &sfs->f_fsid, sizeof(fid->fsid)) != 0)
return -ESTALE;
return RET_NERRNO(open_by_handle_at(mount_fd, fh, O_CLOEXEC | O_PATH));
}
return -ENODATA;
}
int fanotify_mark_fd(int fanotify_fd, unsigned flags, uint64_t mask, int fd) {
assert(fanotify_fd >= 0);
assert(fd >= 0);
/* This also works for O_PATH fds, which fanotify_mark() rejects as dirfd. */
return RET_NERRNO(fanotify_mark(fanotify_fd, flags, mask, AT_FDCWD, FORMAT_PROC_FD_PATH(fd)));
}
+41
View File
@@ -0,0 +1,41 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include <sys/fanotify.h> /* IWYU pragma: export */
#include <sys/vfs.h> /* IWYU pragma: export */
#include "forward.h"
union fanotify_event_buffer {
struct fanotify_event_metadata em;
uint8_t raw[4096];
};
typedef struct FanotifyEventIterator {
const uint8_t *ptr; /* cursor into the raw read buffer */
size_t left; /* remaining bytes */
union fanotify_event_buffer aligned; /* 8-byte aligned scratch for one event */
} FanotifyEventIterator;
bool fanotify_event_iterator_next(FanotifyEventIterator *i, const struct fanotify_event_metadata **ret);
/* Iterates over the events in a buffer filled by read() on an fanotify fd. Each event is copied into an
* 8-byte aligned scratch buffer first, so 'e' is always suitably aligned. 'buffer' must be a
* union fanotify_event_buffer, 'sz' the number of bytes read (must be >= 0). FAN_EVENT_OK()/
* FAN_EVENT_NEXT() can't be used, as they dereference the metadata in place while possibly unaligned. */
#define _FOREACH_FANOTIFY_EVENT(e, buffer, sz, i) \
for (FanotifyEventIterator i = \
{ .ptr = (buffer).raw, .left = (sz), }; \
i.ptr; \
i.ptr = NULL) \
for (const struct fanotify_event_metadata *e; \
fanotify_event_iterator_next(&i, &e); )
#define FOREACH_FANOTIFY_EVENT(e, buffer, sz) \
_FOREACH_FANOTIFY_EVENT(e, buffer, sz, UNIQ_T(i, UNIQ))
int fanotify_event_pin_fd(
const struct fanotify_event_metadata *e,
int mount_fd, /* O_PATH fd cannot be used */
const struct statfs *sfs); /* can be NULL */
int fanotify_mark_fd(int fanotify_fd, unsigned flags, uint64_t mask, int fd);
+1
View File
@@ -36,6 +36,7 @@ basic_sources = files(
'escape.c',
'ether-addr-util.c',
'extract-word.c',
'fanotify-util.c',
'fd-util.c',
'fiber-ops.c',
'fileio.c',
+1
View File
@@ -105,6 +105,7 @@ simple_tests += files(
'test-execve.c',
'test-exit-status.c',
'test-extract-word.c',
'test-fanotify-util.c',
'test-fdset.c',
'test-fiemap.c',
'test-fileio.c',
+140
View File
@@ -0,0 +1,140 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-event.h"
#include "alloc-util.h"
#include "capability-util.h"
#include "errno-util.h"
#include "fanotify-util.h"
#include "fd-util.h"
#include "fs-util.h"
#include "mountpoint-util.h"
#include "rm-rf.h"
#include "set.h"
#include "stat-util.h"
#include "stdio-util.h"
#include "time-util.h"
#include "tmpfile-util.h"
#include "tests.h"
#define N_FILES 10
typedef struct Context {
int mount_fd;
struct statfs sfs;
Set *stats;
} Context;
static void context_done(Context *c) {
assert(c);
safe_close(c->mount_fd);
set_free(c->stats);
}
static int on_fanotify(sd_event_source *s, int fanotify_fd, uint32_t revents, void *userdata) {
Context *c = ASSERT_PTR(userdata);
union fanotify_event_buffer buffer;
ssize_t n = ASSERT_OK_ERRNO(read(fanotify_fd, &buffer, sizeof buffer));
FOREACH_FANOTIFY_EVENT(e, buffer, n) {
ASSERT_EQ(e->vers, (uint8_t) FANOTIFY_METADATA_VERSION);
if (!FLAGS_SET(e->mask, FAN_CLOSE_WRITE))
continue;
_cleanup_close_ int fd = ASSERT_OK_ERRNO(fanotify_event_pin_fd(e, c->mount_fd, &c->sfs));
/* The sfs argument is optional. */
safe_close(ASSERT_OK_ERRNO(fanotify_event_pin_fd(e, c->mount_fd, /* sfs= */ NULL)));
struct stat st;
ASSERT_OK_ERRNO(fstat(fd, &st));
_unused_ _cleanup_free_ struct stat *found = ASSERT_NOT_NULL(set_remove(c->stats, &st));
/* Stop watching this inode. */
ASSERT_OK(fanotify_mark_fd(fanotify_fd, FAN_MARK_REMOVE | FAN_MARK_INODE, FAN_CLOSE_WRITE, fd));
}
if (set_isempty(c->stats))
return sd_event_exit(sd_event_source_get_event(s), 0);
return 0;
}
TEST(fanotify) {
int r;
_cleanup_(rm_rf_physical_and_freep) char *t = NULL;
ASSERT_OK(mkdtemp_malloc(/* template= */ NULL, &t));
_cleanup_(context_done) Context c = {
.mount_fd = ASSERT_OK_ERRNO(open(t, O_DIRECTORY | O_CLOEXEC)),
};
ASSERT_OK_ERRNO(fstatfs(c.mount_fd, &c.sfs));
if (memeqzero(&c.sfs.f_fsid, sizeof(c.sfs.f_fsid)))
return (void) log_tests_skipped("FAN_REPORT_FID requires non-zero fsid, but it is zero");
_cleanup_free_ struct file_handle *fh = NULL;
r = name_to_handle_at_loop(
AT_FDCWD, t, &fh,
/* ret_mnt_id= */ NULL,
/* ret_unique_mnt_id= */ NULL,
/* flags= */ 0);
if (ERRNO_IS_NEG_NOT_SUPPORTED(r))
return (void) log_tests_skipped("file handles not supported");
ASSERT_OK(r);
_cleanup_close_ int fd = RET_NERRNO(open_by_handle_at(c.mount_fd, fh, O_CLOEXEC | O_PATH));
if (ERRNO_IS_NEG_NOT_SUPPORTED(fd) || ERRNO_IS_NEG_PRIVILEGE(fd))
return (void) log_tests_skipped_errno(fd, "open_by_handle_at() failed");
ASSERT_OK(fd);
unsigned flags = FAN_CLASS_NOTIF | FAN_CLOEXEC | FAN_NONBLOCK | FAN_REPORT_FID;
_cleanup_close_ int fanotify_fd = RET_NERRNO(fanotify_init(flags, O_RDONLY | O_CLOEXEC));
/* May be blocked by seccomp, or require CAP_SYS_ADMIN in the initial user namespace
* (e.g. inside an unprivileged container), or fanotify may be disabled in the kernel. */
if (ERRNO_IS_NEG_NOT_SUPPORTED(fanotify_fd) || ERRNO_IS_NEG_PRIVILEGE(fanotify_fd))
return (void) log_tests_skipped_errno(fanotify_fd, "fanotify_init() failed");
ASSERT_OK(fanotify_fd);
for (size_t i = 0; i < N_FILES; i++) {
_cleanup_free_ char *p = ASSERT_NOT_NULL(asprintf_safe("%s/f%zu", t, i));
ASSERT_OK(touch(p));
r = fanotify_mark(fanotify_fd, FAN_MARK_ADD | FAN_MARK_INODE, FAN_CLOSE_WRITE, AT_FDCWD, p);
if (r < 0 && errno == EXDEV) /* btrfs with kernel older than 6.8 */
return (void) log_tests_skipped("fanotify_mark() failed");
ASSERT_OK_ERRNO(r);
_cleanup_free_ struct stat *st = ASSERT_NOT_NULL(new(struct stat, 1));
ASSERT_OK_ERRNO(stat(p, st));
ASSERT_OK(set_ensure_consume(&c.stats, &inode_hash_ops, TAKE_PTR(st)));
/* Trigger a FAN_CLOSE_WRITE for this file. */
safe_close(ASSERT_OK_ERRNO(open(p, O_WRONLY | O_CLOEXEC)));
}
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
ASSERT_OK(sd_event_new(&event));
ASSERT_OK(sd_event_add_io(event, /* ret= */ NULL, fanotify_fd, EPOLLIN, on_fanotify, &c));
ASSERT_OK(sd_event_add_time_relative(event, /* ret= */ NULL,
CLOCK_MONOTONIC, 30 * USEC_PER_SEC, /* accuracy= */ 0,
/* callback= */ NULL, INT_TO_PTR(-ETIMEDOUT)));
ASSERT_OK(sd_event_loop(event));
}
static int intro(void) {
if (have_effective_cap(CAP_SYS_ADMIN) <= 0)
return log_tests_skipped("missing CAP_SYS_ADMIN");
if (have_effective_cap(CAP_DAC_READ_SEARCH) <= 0)
return log_tests_skipped("missing CAP_DAC_READ_SEARCH");
return EXIT_SUCCESS;
}
DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro);