mirror of
https://github.com/systemd/systemd.git
synced 2026-11-06 05:17:24 +00:00
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:
1 parent
23f243f364
commit
fbe2857a7b
5 files changed
+299
No files matched your search
@@ -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)));
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
Reference in new issue
Block a user