diff options
| author | Mark Brown <broonie@kernel.org> | 2026-10-21 11:22:51 +0100 |
|---|---|---|
| committer | Mark Brown <broonie@kernel.org> | 2026-10-21 11:22:51 +0100 |
| commit | e52b2c0ec8860e82b285dbafb008ee424932964d (patch) | |
| tree | c3d15e3329be676e57c335120a24aac0bb61d92d /tools/testing | |
| parent | b7772aac9c8a7140895928034db43b6c6f827538 (diff) | |
| parent | 5a9745611d621ea160854936eb6370e610887bfb (diff) | |
| download | linux-next-e52b2c0ec8860e82b285dbafb008ee424932964d.tar.gz linux-next-e52b2c0ec8860e82b285dbafb008ee424932964d.zip | |
Merge branch 'for-next' of https://git.kernel.org/pub/scm/linux/kernel/git/mszeredi/fuse.git
Diffstat (limited to 'tools/testing')
9 files changed, 948 insertions, 84 deletions
diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore b/tools/testing/selftests/filesystems/fuse/.gitignore index fb51603fe419..7c787313e69b 100644 --- a/tools/testing/selftests/filesystems/fuse/.gitignore +++ b/tools/testing/selftests/filesystems/fuse/.gitignore @@ -1,4 +1,8 @@ # SPDX-License-Identifier: GPL-2.0-only fuse_mnt fusectl_test +test_syncfs write_extend_eof_test +fuse_acl_cache_test +fuse_symlink_cache_test +fuse_readdir_cache_test diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/testing/selftests/filesystems/fuse/Makefile index 95a1ee947ca7..e3ae3acff113 100644 --- a/tools/testing/selftests/filesystems/fuse/Makefile +++ b/tools/testing/selftests/filesystems/fuse/Makefile @@ -2,33 +2,35 @@ CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) -TEST_GEN_PROGS := fusectl_test +TEST_GEN_PROGS := fusectl_test test_syncfs TEST_GEN_PROGS += write_extend_eof_test -TEST_GEN_FILES := fuse_mnt - -# fuse_acl_cache_test requires libfuse3; add it only when the library is present. -ACL_CFLAGS := $(shell pkg-config fuse3 --cflags 2>/dev/null) -ACL_LDLIBS := $(shell pkg-config fuse3 --libs 2>/dev/null) -ifneq ($(ACL_CFLAGS),) TEST_GEN_PROGS += fuse_acl_cache_test -endif +TEST_GEN_PROGS += fuse_symlink_cache_test +TEST_GEN_PROGS += fuse_readdir_cache_test +TEST_GEN_FILES := fuse_mnt include ../../lib.mk $(OUTPUT)/write_extend_eof_test: LDLIBS += -lpthread -VAR_CFLAGS := $(shell pkg-config fuse --cflags 2>/dev/null) +VAR_CFLAGS := $(shell pkg-config fuse3 --cflags 2>/dev/null) ifeq ($(VAR_CFLAGS),) -VAR_CFLAGS := -D_FILE_OFFSET_BITS=64 -I/usr/include/fuse +VAR_CFLAGS := -D_FILE_OFFSET_BITS=64 -I/usr/include/fuse3 endif -VAR_LDLIBS := $(shell pkg-config fuse --libs 2>/dev/null) +VAR_LDLIBS := $(shell pkg-config fuse3 --libs 2>/dev/null) ifeq ($(VAR_LDLIBS),) -VAR_LDLIBS := -lfuse -pthread +VAR_LDLIBS := -lfuse3 -pthread endif +CFLAGS += $(VAR_CFLAGS) +LDLIBS += $(VAR_LDLIBS) + $(OUTPUT)/fuse_mnt: CFLAGS += $(VAR_CFLAGS) $(OUTPUT)/fuse_mnt: LDLIBS += $(VAR_LDLIBS) -$(OUTPUT)/fuse_acl_cache_test: CFLAGS += $(ACL_CFLAGS) -$(OUTPUT)/fuse_acl_cache_test: LDLIBS += $(ACL_LDLIBS) +$(OUTPUT)/fuse_acl_cache_test: fuse_common.c fuse_acl_cache_test.c +$(OUTPUT)/fuse_symlink_cache_test: fuse_common.c fuse_symlink_cache_test.c +$(OUTPUT)/fuse_readdir_cache_test: fuse_common.c fuse_readdir_cache_test.c + +EXTRA_CLEAN := fuse_common.o diff --git a/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c b/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c index 2411a6e285f1..9c8840fe816b 100644 --- a/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c +++ b/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c @@ -33,23 +33,15 @@ */ #define _GNU_SOURCE -#include <errno.h> #include <fcntl.h> #include <linux/limits.h> -#include <pthread.h> #include <stdint.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/stat.h> #include <sys/xattr.h> -#include <unistd.h> - -#define FUSE_USE_VERSION 31 -#include <fuse_lowlevel.h> #include "kselftest_harness.h" +#include "fuse_common.h" + /* ---- ACL binary encoding ------------------------------------------------ */ /* * POSIX ACL v2 xattr format (little-endian): @@ -88,9 +80,12 @@ static const uint8_t acl_b[] = { struct daemon_state { pthread_mutex_t lock; - const uint8_t *acl; + uint8_t *acl; size_t acl_size; int getxattr_count; + uid_t uid; + uid_t gid; + bool cache; }; /* @@ -99,9 +94,17 @@ struct daemon_state { */ static struct daemon_state g_ds = { .lock = PTHREAD_MUTEX_INITIALIZER, + .cache = false, }; /* ---- FUSE lowlevel callbacks -------------------------------------------- */ +static void fs_init(void *userdata, struct fuse_conn_info *conn) +{ + pthread_mutex_lock(&g_ds.lock); + if (g_ds.cache) + fuse_set_feature_flag(conn, FUSE_CAP_POSIX_ACL); + pthread_mutex_unlock(&g_ds.lock); +} static void fs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) { @@ -123,6 +126,10 @@ static void fs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) e.attr.st_ino = FILE_INO; e.attr.st_mode = S_IFREG | 0644; e.attr.st_nlink = 1; + pthread_mutex_lock(&g_ds.lock); + e.attr.st_uid = g_ds.uid; + e.attr.st_gid = g_ds.gid; + pthread_mutex_unlock(&g_ds.lock); fuse_reply_entry(req, &e); } @@ -140,6 +147,10 @@ static void fs_getattr(fuse_req_t req, fuse_ino_t ino, st.st_ino = FILE_INO; st.st_mode = S_IFREG | 0644; st.st_nlink = 1; + pthread_mutex_lock(&g_ds.lock); + st.st_uid = g_ds.uid; + st.st_gid = g_ds.gid; + pthread_mutex_unlock(&g_ds.lock); } else { fuse_reply_err(req, ENOENT); return; @@ -150,95 +161,112 @@ static void fs_getattr(fuse_req_t req, fuse_ino_t ino, static void fs_getxattr(fuse_req_t req, fuse_ino_t ino, const char *name, size_t size) { + uint8_t *acl = NULL; + size_t acl_size; + if (ino != FILE_INO || strcmp(name, "system.posix_acl_access") != 0) { fuse_reply_err(req, ENODATA); return; } + if (size) { + acl = malloc(size); + if (!acl) { + fuse_reply_err(req, ENOMEM); + return; + } + } pthread_mutex_lock(&g_ds.lock); - const uint8_t *acl = g_ds.acl; - size_t acl_size = g_ds.acl_size; + acl_size = g_ds.acl_size; + if (acl && acl_size && (size >= acl_size)) + memcpy(acl, g_ds.acl, acl_size); g_ds.getxattr_count++; pthread_mutex_unlock(&g_ds.lock); - if (size == 0) + if (acl_size == 0) + fuse_reply_err(req, ENODATA); + else if (size == 0) fuse_reply_xattr(req, acl_size); else if (size < acl_size) fuse_reply_err(req, ERANGE); else fuse_reply_buf(req, (const char *)acl, acl_size); + + free(acl); +} + +static void fs_setxattr(fuse_req_t req, fuse_ino_t ino, const char *name, + const char *value, size_t size, int flags) +{ + int ret = 0; + uint8_t *acl; + + if (ino != FILE_INO) { + ret = ENOENT; + goto out; + } else if (strcmp(name, "system.posix_acl_access") != 0) { + ret = ENOTSUP; + goto out; + } + acl = malloc(size); + if (!acl) { + ret = ENOMEM; + goto out; + } + memcpy(acl, value, size); + pthread_mutex_lock(&g_ds.lock); + if (g_ds.acl) + free(g_ds.acl); + g_ds.acl = acl; + g_ds.acl_size = size; + pthread_mutex_unlock(&g_ds.lock); + +out: + fuse_reply_err(req, ret); } static const struct fuse_lowlevel_ops fs_ops = { + .init = fs_init, .lookup = fs_lookup, .getattr = fs_getattr, .getxattr = fs_getxattr, + .setxattr = fs_setxattr, }; -/* ---- Daemon thread ------------------------------------------------------- */ - -static void *run_daemon(void *arg) -{ - fuse_session_loop((struct fuse_session *)arg); - return NULL; -} - /* ---- kselftest harness --------------------------------------------------- */ FIXTURE(acl_cache) { - struct fuse_session *se; - char mountpoint[PATH_MAX]; - char file_path[PATH_MAX]; - pthread_t thread; + struct fuse_common_ctx ctx; + char file_path[PATH_MAX]; }; FIXTURE_SETUP(acl_cache) { - char *fuse_argv[] = { "fuse_acl_cache_test", NULL }; - struct fuse_args args = FUSE_ARGS_INIT(1, fuse_argv); + char err[MAX_ERR_MSG]; - g_ds.acl = acl_a; - g_ds.acl_size = sizeof(acl_a); + pthread_mutex_lock(&g_ds.lock); + g_ds.acl_size = sizeof(acl_a); + g_ds.acl = malloc(g_ds.acl_size); + ASSERT_NE(g_ds.acl, NULL); + memcpy(g_ds.acl, acl_a, g_ds.acl_size); g_ds.getxattr_count = 0; + g_ds.uid = getuid(); + g_ds.gid = getgid(); + g_ds.cache = false; + pthread_mutex_unlock(&g_ds.lock); - strcpy(self->mountpoint, "/tmp/acl_cache_test_XXXXXX"); - if (!mkdtemp(self->mountpoint)) - SKIP(return, "mkdtemp: %s", strerror(errno)); + if (fs_setup(&fs_ops, &self->ctx, err)) + SKIP(return, err); snprintf(self->file_path, sizeof(self->file_path), - "%s/" FILE_NAME, self->mountpoint); - - self->se = fuse_session_new(&args, &fs_ops, sizeof(fs_ops), NULL); - if (!self->se) { - rmdir(self->mountpoint); - SKIP(return, "fuse_session_new failed"); - } - - if (fuse_session_mount(self->se, self->mountpoint)) { - fuse_session_destroy(self->se); - rmdir(self->mountpoint); - SKIP(return, "fuse_session_mount failed " - "(missing fusermount3 or insufficient privileges)"); - } - - if (pthread_create(&self->thread, NULL, run_daemon, self->se)) { - fuse_session_unmount(self->se); - fuse_session_destroy(self->se); - rmdir(self->mountpoint); - SKIP(return, "pthread_create: %s", strerror(errno)); - } - - fuse_opt_free_args(&args); + "%s/" FILE_NAME, self->ctx.mountpoint); } FIXTURE_TEARDOWN(acl_cache) { - fuse_session_exit(self->se); - fuse_session_unmount(self->se); - pthread_join(self->thread, NULL); - fuse_session_destroy(self->se); - rmdir(self->mountpoint); + fs_teardown(&self->ctx); + free(g_ds.acl); } static int do_force_statx(const char *path) @@ -254,6 +282,7 @@ TEST_F(acl_cache, stale_after_force_sync) char buf[512]; ssize_t sz; int count; + uint8_t *acl; /* * Step 1: two getxattr calls before any statx(FORCE_SYNC). @@ -314,8 +343,13 @@ TEST_F(acl_cache, stale_after_force_sync) * !fc->posix_acl mounts (it skips forget_all_cached_acls in that case). * On a fixed kernel the ACL was never cached, so this is moot. */ + acl = malloc(sizeof(acl_b)); + ASSERT_NE(acl, NULL); + memcpy(acl, acl_b, sizeof(acl_b)); + pthread_mutex_lock(&g_ds.lock); - g_ds.acl = acl_b; + free(g_ds.acl); + g_ds.acl = acl; g_ds.acl_size = sizeof(acl_b); pthread_mutex_unlock(&g_ds.lock); TH_LOG("step 4: daemon switched to ACL_B (%zu bytes)", sizeof(acl_b)); @@ -344,4 +378,145 @@ TEST_F(acl_cache, stale_after_force_sync) EXPECT_EQ(count, 4); } +FIXTURE(acl_cache_onoff) +{ + struct fuse_common_ctx ctx; + char pathname[PATH_MAX]; +}; + +FIXTURE_VARIANT(acl_cache_onoff) { bool cache; }; +FIXTURE_VARIANT_ADD(acl_cache_onoff, nocache) { .cache = false, }; +FIXTURE_VARIANT_ADD(acl_cache_onoff, docache) { .cache = true, }; + +FIXTURE_SETUP(acl_cache_onoff) +{ + char err[MAX_ERR_MSG]; + + pthread_mutex_lock(&g_ds.lock); + g_ds.acl = NULL; + g_ds.acl_size = 0; + g_ds.getxattr_count = 0; + g_ds.cache = variant->cache; + g_ds.uid = getuid(); + g_ds.gid = getgid(); + pthread_mutex_unlock(&g_ds.lock); + + if (fs_setup(&fs_ops, &self->ctx, err)) + SKIP(return, err); + + snprintf(self->pathname, sizeof(self->pathname), + "%s/" FILE_NAME, self->ctx.mountpoint); +} + +FIXTURE_TEARDOWN(acl_cache_onoff) +{ + fs_teardown(&self->ctx); + free(g_ds.acl); +} + +/* + * This is the most basic ACL caching test: verify that, when reading ACLs for + * an inode, user-space is called: + * - Only once if ACLs cache is enabled, or + * - Once per access if cache is disabled. + */ +TEST_F(acl_cache_onoff, test_acl_cache_enable_disable) +{ + char buf[512]; + ssize_t sz; + bool cache; + int counter; + int i; + + ASSERT_EQ(lsetxattr(self->pathname, "system.posix_acl_access", + acl_a, sizeof(acl_a), 0), 0); + + for (i = 0; i < 100; i++) { + sz = lgetxattr(self->pathname, "system.posix_acl_access", + buf, sizeof(buf)); + ASSERT_EQ(sz, sizeof(acl_a)); + ASSERT_EQ(memcmp(buf, acl_a, sz), 0); + } + + pthread_mutex_lock(&g_ds.lock); + counter = g_ds.getxattr_count; + cache = g_ds.cache; + pthread_mutex_unlock(&g_ds.lock); + + if (cache) { + ASSERT_EQ(counter, 1); + } else { + ASSERT_EQ(counter, 100); + } + + TH_LOG("User-space called %d time(s) with ACL caching %s", + counter, cache ? "enabled" : "disabled"); +} + +/* + * Test caching invalidation for several scenarios: + * 1. When a new ACL is set + * 2. When invalidating an inode (NOTIFY_INVAL_INODE) + */ +TEST_F(acl_cache_onoff, test_acl_cache_invalidation) +{ + char buf[512]; + ssize_t sz; + int counter; + bool cache; + int i; + + /* Set an ACL */ + ASSERT_EQ(lsetxattr(self->pathname, "system.posix_acl_access", + acl_a, sizeof(acl_a), 0), 0); + + for (i = 0; i < 100; i++) { + sz = lgetxattr(self->pathname, "system.posix_acl_access", + buf, sizeof(buf)); + ASSERT_EQ(sz, sizeof(acl_a)); + ASSERT_EQ(memcmp(buf, acl_a, sz), 0); + } + + /* 1. force cache invalidation by setting a new ACL */ + ASSERT_EQ(lsetxattr(self->pathname, "system.posix_acl_access", + acl_b, sizeof(acl_b), 0), 0); + + sz = lgetxattr(self->pathname, "system.posix_acl_access", + buf, sizeof(buf)); + ASSERT_EQ(sz, sizeof(acl_b)); + ASSERT_EQ(memcmp(buf, acl_b, sz), 0); + + pthread_mutex_lock(&g_ds.lock); + counter = g_ds.getxattr_count; + cache = g_ds.cache; + pthread_mutex_unlock(&g_ds.lock); + + if (cache) { + ASSERT_EQ(counter, 2); + } else { + ASSERT_EQ(counter, 101); + } + TH_LOG("Invalidation by setting new ACL: OK"); + + /* 2. send FUSE_NOTIFY_INVAL_INODE */ + fuse_lowlevel_notify_inval_inode(self->ctx.se, FILE_INO, 0, 0); + + sz = lgetxattr(self->pathname, "system.posix_acl_access", + buf, sizeof(buf)); + ASSERT_EQ(sz, sizeof(acl_b)); + ASSERT_EQ(memcmp(buf, acl_b, sz), 0); + + pthread_mutex_lock(&g_ds.lock); + counter = g_ds.getxattr_count; + cache = g_ds.cache; + pthread_mutex_unlock(&g_ds.lock); + + if (cache) { + ASSERT_EQ(counter, 3); + } else { + ASSERT_EQ(counter, 102); + } + TH_LOG("Invalidation through FUSE_NOTIFY_INVAL_INODE: OK"); +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/fuse/fuse_common.c b/tools/testing/selftests/filesystems/fuse/fuse_common.c new file mode 100644 index 000000000000..d1d3dfd2ea67 --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/fuse_common.c @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include "fuse_common.h" + +static void *run_daemon(void *arg) +{ + fuse_session_loop((struct fuse_session *)arg); + return NULL; +} + +int fs_setup(const struct fuse_lowlevel_ops *fs_ops, + struct fuse_common_ctx *ctx, char *err) +{ + char *fuse_argv[] = { "fuse_test", NULL }; + struct fuse_args args = FUSE_ARGS_INIT(1, fuse_argv); + + strcpy(ctx->mountpoint, MOUNTPOINT_TEMPLATE); + if (!mkdtemp(ctx->mountpoint)) { + snprintf(err, MAX_ERR_MSG, "mkdtemp: %s", strerror(errno)); + return -1; + } + + ctx->se = fuse_session_new(&args, fs_ops, sizeof(*fs_ops), NULL); + if (!ctx->se) { + rmdir(ctx->mountpoint); + snprintf(err, MAX_ERR_MSG, "fuse_session_new failed"); + return -1; + } + + if (fuse_session_mount(ctx->se, ctx->mountpoint)) { + fuse_session_destroy(ctx->se); + rmdir(ctx->mountpoint); + snprintf(err, MAX_ERR_MSG, "fuse_session_mount failed " + "(missing fusermount3 or insufficient privileges)"); + return -1; + } + + if (pthread_create(&ctx->thread, NULL, run_daemon, ctx->se)) { + fuse_session_unmount(ctx->se); + fuse_session_destroy(ctx->se); + rmdir(ctx->mountpoint); + snprintf(err, MAX_ERR_MSG, "pthread_create: %s", strerror(errno)); + return -1; + } + + fuse_opt_free_args(&args); + + return 0; +} + +void fs_teardown(struct fuse_common_ctx *ctx) +{ + fuse_session_exit(ctx->se); + fuse_session_unmount(ctx->se); + pthread_join(ctx->thread, NULL); + fuse_session_destroy(ctx->se); + rmdir(ctx->mountpoint); +} diff --git a/tools/testing/selftests/filesystems/fuse/fuse_common.h b/tools/testing/selftests/filesystems/fuse/fuse_common.h new file mode 100644 index 000000000000..5beb6bf89425 --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/fuse_common.h @@ -0,0 +1,33 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#ifndef __SELFTEST_FUSE_COMMON_H__ +#define __SELFTEST_FUSE_COMMON_H__ + +#define _GNU_SOURCE +#include <errno.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <limits.h> + +#define FUSE_USE_VERSION 31 +#include <fuse_lowlevel.h> + +#define MAX_ERR_MSG 256 + +#define MOUNTPOINT_TEMPLATE "/tmp/fuse_test_XXXXXX" +#define MOUNTPOINT_SZ 64 + +struct fuse_common_ctx { + struct fuse_session *se; + char mountpoint[MOUNTPOINT_SZ]; + pthread_t thread; +}; + +int fs_setup(const struct fuse_lowlevel_ops *fs_ops, + struct fuse_common_ctx *ctx, char *err); +void fs_teardown(struct fuse_common_ctx *ctx); + +#endif /* __SELFTEST_FUSE_COMMON_H__ */ diff --git a/tools/testing/selftests/filesystems/fuse/fuse_mnt.c b/tools/testing/selftests/filesystems/fuse/fuse_mnt.c index d12b17f30fad..5d335fa5cf05 100644 --- a/tools/testing/selftests/filesystems/fuse/fuse_mnt.c +++ b/tools/testing/selftests/filesystems/fuse/fuse_mnt.c @@ -4,7 +4,7 @@ * Creates a simple FUSE filesystem with a single read-write file (/test) */ -#define FUSE_USE_VERSION 26 +#define FUSE_USE_VERSION 31 #include <fuse.h> #include <stdio.h> @@ -20,7 +20,8 @@ static char *content; static size_t content_size = 0; static const char test_path[] = "/test"; -static int test_getattr(const char *path, struct stat *st) +static int test_getattr(const char *path, struct stat *st, + struct fuse_file_info *fi) { memset(st, 0, sizeof(*st)); @@ -41,14 +42,15 @@ static int test_getattr(const char *path, struct stat *st) } static int test_readdir(const char *path, void *buf, fuse_fill_dir_t filler, - off_t offset, struct fuse_file_info *fi) + off_t offset, struct fuse_file_info *fi, + enum fuse_readdir_flags flags) { if (strcmp(path, "/")) return -ENOENT; - filler(buf, ".", NULL, 0); - filler(buf, "..", NULL, 0); - filler(buf, test_path + 1, NULL, 0); + filler(buf, ".", NULL, 0, FUSE_FILL_DIR_DEFAULTS); + filler(buf, "..", NULL, 0, FUSE_FILL_DIR_DEFAULTS); + filler(buf, test_path + 1, NULL, 0, FUSE_FILL_DIR_DEFAULTS); return 0; } @@ -107,7 +109,8 @@ static int test_write(const char *path, const char *buf, size_t size, return size; } -static int test_truncate(const char *path, off_t size) +static int test_truncate(const char *path, off_t size, + struct fuse_file_info *fi) { if (strcmp(path, test_path) != 0) return -ENOENT; diff --git a/tools/testing/selftests/filesystems/fuse/fuse_readdir_cache_test.c b/tools/testing/selftests/filesystems/fuse/fuse_readdir_cache_test.c new file mode 100644 index 000000000000..59314f996449 --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/fuse_readdir_cache_test.c @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Simple filesystem to test FUSE readdir cache + * + * It will simply perform readdir operations on a directory checking how many + * times a request is sent to user-space using all the possible caching + * combination setting (FOPEN_KEEP_CACHE and FOPEN_CACHE_DIR flags). + */ + +#include <stdio.h> +#include <dirent.h> + +#include "kselftest_harness.h" + +#include "fuse_common.h" + +#define DIRNAME "mydir" +#define FILENAME "myfile" + +#define DIR_INO 42 +#define FILE_INO 43 +#define DOT_INO 40 +#define DOTDOT_INO 41 + +#define TIMEOUT 86400.0f + +struct test_state { + pthread_mutex_t lock; + time_t mtime; + int readdir_counter; + bool cache_readdir; + bool keep_cache; +} test_state = { + .lock = PTHREAD_MUTEX_INITIALIZER, +}; + +static void fs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) +{ + struct fuse_entry_param e = {}; + + if (parent != FUSE_ROOT_ID || strcmp(name, DIRNAME) != 0) { + fuse_reply_err(req, ENOENT); + return; + } + + e.ino = DIR_INO; + e.attr.st_mode = S_IFDIR | 0755; + e.attr.st_nlink = 1; + pthread_mutex_lock(&test_state.lock); + e.attr.st_mtime = test_state.mtime; + pthread_mutex_unlock(&test_state.lock); + e.attr_timeout = TIMEOUT; + e.entry_timeout = TIMEOUT; + + fuse_reply_entry(req, &e); +} + +static int fill_stat(fuse_ino_t ino, struct stat *st) +{ + int ret = 0; + + st->st_ino = ino; + pthread_mutex_lock(&test_state.lock); + st->st_mtime = test_state.mtime; + pthread_mutex_unlock(&test_state.lock); + + switch (ino) { + case FUSE_ROOT_ID: + st->st_mode = S_IFDIR | 0755; + st->st_nlink = 2; + break; + case DOT_INO: + case DOTDOT_INO: + case DIR_INO: + st->st_mode = S_IFDIR | 0755; + st->st_nlink = 1; + break; + case FILE_INO: + st->st_mode = S_IFREG | 0444; + st->st_nlink = 1; + break; + default: + ret = -1; + break; + } + + return ret; +} + +static void fs_getattr(fuse_req_t req, fuse_ino_t ino, + struct fuse_file_info *fi) +{ + struct stat st = {}; + + if (fill_stat(ino, &st) < 0) + fuse_reply_err(req, ENOENT); + else + fuse_reply_attr(req, &st, TIMEOUT); +} + +static void fs_opendir(fuse_req_t req, fuse_ino_t ino, + struct fuse_file_info *fi) +{ + pthread_mutex_lock(&test_state.lock); + fi->keep_cache = test_state.keep_cache; + fi->cache_readdir = test_state.cache_readdir; + pthread_mutex_unlock(&test_state.lock); + fuse_reply_open(req, fi); +} + +static void fs_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, + off_t offset, struct fuse_file_info *fi) +{ + struct stat st = {}; + char buf[1024]; + char *pbuf; + size_t rem = size; + size_t sz; + int nextoff = 0; + + if (ino != DIR_INO) { + fuse_reply_err(req, ENOTDIR); + return; + } + if (offset) { + fuse_reply_buf(req, NULL, 0); + return; + } + pbuf = buf; + fill_stat(DOT_INO, &st); + sz = fuse_add_direntry(req, pbuf, rem, ".", &st, nextoff++); + rem -= sz; + pbuf += sz; + fill_stat(DOTDOT_INO, &st); + sz = fuse_add_direntry(req, pbuf, rem, "..", &st, nextoff++); + rem -= sz; + pbuf += sz; + fill_stat(FILE_INO, &st); + sz = fuse_add_direntry(req, pbuf, rem, FILENAME, &st, nextoff++); + rem -= sz; + + fuse_reply_buf(req, buf, size - rem); + + pthread_mutex_lock(&test_state.lock); + test_state.readdir_counter++; + pthread_mutex_unlock(&test_state.lock); +} + +static const struct fuse_lowlevel_ops fs_ops = { + .lookup = fs_lookup, + .getattr = fs_getattr, + .opendir = fs_opendir, + .readdir = fs_readdir, +}; + +FIXTURE(readdir_cache) +{ + struct fuse_common_ctx ctx; +}; + +FIXTURE_VARIANT(readdir_cache) +{ + bool cache_readdir; + bool keep_cache; +}; +FIXTURE_VARIANT_ADD(readdir_cache, nocache) +{ + .cache_readdir = false, + .keep_cache = false, +}; +FIXTURE_VARIANT_ADD(readdir_cache, cache_readdir) +{ + .cache_readdir = true, + .keep_cache = false, +}; +FIXTURE_VARIANT_ADD(readdir_cache, keep_cache) +{ + .cache_readdir = false, + .keep_cache = true, +}; +FIXTURE_VARIANT_ADD(readdir_cache, cache) +{ + .cache_readdir = true, + .keep_cache = true, +}; + +FIXTURE_SETUP(readdir_cache) +{ + char err[MAX_ERR_MSG]; + + pthread_mutex_lock(&test_state.lock); + test_state.mtime = time(NULL); + test_state.readdir_counter = 0; + test_state.cache_readdir = variant->cache_readdir; + test_state.keep_cache = variant->keep_cache; + pthread_mutex_unlock(&test_state.lock); + + if (fs_setup(&fs_ops, &self->ctx, err)) + SKIP(return, err); +} + +FIXTURE_TEARDOWN(readdir_cache) +{ + fs_teardown(&self->ctx); +} + +TEST_F(readdir_cache, test_readdir_cache) +{ + struct dirent *dentry; + DIR *dir; + char pathname[PATH_MAX]; + int total_counter, rewind_counter; + int dentrycount; + + sprintf(pathname, "%s/%s", self->ctx.mountpoint, DIRNAME); + + dir = opendir(pathname); + if (dir == NULL) + TH_LOG("opendir(): %s", strerror(errno)); + ASSERT_NE(dir, NULL); + + errno = 0; + dentrycount = 0; + while ((dentry = readdir(dir))) + dentrycount++; + ASSERT_EQ(errno, 0); + ASSERT_EQ(dentrycount, 3); + + rewinddir(dir); + errno = 0; + dentrycount = 0; + while ((dentry = readdir(dir))) + dentrycount++; + ASSERT_EQ(errno, 0); + ASSERT_EQ(dentrycount, 3); + + ASSERT_EQ(closedir(dir), 0); + + pthread_mutex_lock(&test_state.lock); + rewind_counter = test_state.readdir_counter; + pthread_mutex_unlock(&test_state.lock); + + dir = opendir(pathname); + if (dir == NULL) + TH_LOG("opendir(): %s", strerror(errno)); + ASSERT_NE(dir, NULL); + + errno = 0; + dentrycount = 0; + while ((dentry = readdir(dir))) + dentrycount++; + ASSERT_EQ(errno, 0); + ASSERT_EQ(dentrycount, 3); + + ASSERT_EQ(closedir(dir), 0); + + pthread_mutex_lock(&test_state.lock); + total_counter = test_state.readdir_counter; + pthread_mutex_unlock(&test_state.lock); + + if (!variant->cache_readdir) { + ASSERT_EQ(rewind_counter, 2); + ASSERT_EQ(total_counter, 3); + } else if (!variant->keep_cache) { + ASSERT_EQ(rewind_counter, 1); + ASSERT_EQ(total_counter, 2); + } else { + ASSERT_EQ(rewind_counter, 1); + ASSERT_EQ(total_counter, 1); + } +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/fuse/fusectl_test.c b/tools/testing/selftests/filesystems/fuse/fusectl_test.c index 0d1d012c35ed..b828173c01bd 100644 --- a/tools/testing/selftests/filesystems/fuse/fusectl_test.c +++ b/tools/testing/selftests/filesystems/fuse/fusectl_test.c @@ -106,8 +106,15 @@ TEST_F(fusectl, abort) char path_buf[PATH_MAX]; int abort_fd, test_fd, ret; - sprintf(path_buf, "/sys/fs/fuse/connections/%d/abort", self->connection); + snprintf(path_buf, PATH_MAX, "%s/%d", FUSECTL_MOUNTPOINT, + self->connection); + if (access(path_buf, F_OK) != 0) + SKIP(return, + "fusectl doesn't seem to be mounted: %s", + strerror(errno)); + snprintf(path_buf, PATH_MAX, "%s/%d/abort", FUSECTL_MOUNTPOINT, + self->connection); ASSERT_EQ(0, access(path_buf, F_OK)); abort_fd = open(path_buf, O_WRONLY); diff --git a/tools/testing/selftests/filesystems/fuse/test_syncfs.c b/tools/testing/selftests/filesystems/fuse/test_syncfs.c new file mode 100644 index 000000000000..d3ca2613b45c --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/test_syncfs.c @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Test that FUSE_SYNCFS is propagated to a userspace server only when the + * server opts in with FUSE_HAS_SYNCFS *and* opened /dev/fuse with + * CAP_SYS_ADMIN in the initial user namespace. + * + * Unlike the libfuse-based selftests, this talks the raw FUSE wire protocol + * over /dev/fuse so it can (a) choose whether to advertise FUSE_HAS_SYNCFS in + * the INIT reply and (b) observe directly whether a FUSE_SYNCFS opcode arrives. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <poll.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/eventfd.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/syscall.h> +#include <sys/types.h> +#include <sys/uio.h> +#include <sys/wait.h> +#include <linux/capability.h> +#include <linux/fuse.h> + +#include "../../kselftest.h" + +#define FUSE_ROOT_ID 1 + +/* + * Add or drop CAP_SYS_ADMIN in the effective set via raw capget/capset + * (avoids a libcap dependency). Used to construct a server that opens + * /dev/fuse without the privilege FUSE_HAS_SYNCFS requires. + */ +static int set_sysadmin(int on) +{ + struct __user_cap_header_struct hdr = { + .version = _LINUX_CAPABILITY_VERSION_3, + .pid = 0, + }; + struct __user_cap_data_struct data[_LINUX_CAPABILITY_U32S_3] = {}; + unsigned int idx = CAP_SYS_ADMIN >> 5; + __u32 bit = 1U << (CAP_SYS_ADMIN & 31); + + if (syscall(SYS_capget, &hdr, data)) + return -1; + if (on) + data[idx].effective |= bit; + else + data[idx].effective &= ~bit; + return syscall(SYS_capset, &hdr, data); +} + +/* + * eventfd the server child writes once when it receives FUSE_SYNCFS; the + * parent poll()s it to observe (or rule out) propagation. + */ +static int syncfs_evfd; + +static void reply(int fd, uint64_t unique, int error, void *data, size_t len) +{ + struct fuse_out_header oh = { + .len = sizeof(oh) + len, + .error = error, + .unique = unique, + }; + struct iovec iov[2] = { + { &oh, sizeof(oh) }, + { data, len }, + }; + + if (writev(fd, iov, data ? 2 : 1) < 0) + ksft_print_msg("server writev failed: %s\n", strerror(errno)); +} + +static void fill_attr(struct fuse_attr *a, uint64_t ino, uint32_t mode, + uint32_t nlink) +{ + memset(a, 0, sizeof(*a)); + a->ino = ino; + a->mode = mode; + a->nlink = nlink; + a->blksize = 4096; +} + +/* + * Minimal FUSE server. Advertises FUSE_HAS_SYNCFS in its INIT reply iff + * @advertise is set. Signals syncfs_evfd when a FUSE_SYNCFS opcode arrives. + */ +#define SERVER_MAX_WRITE 65536 +static void run_server(int fd, int advertise) +{ + /* + * The kernel rejects reads (EINVAL) whose buffer is smaller than + * max_write + header, so size generously for the max_write we + * advertise in the INIT reply below. + */ + static char buf[SERVER_MAX_WRITE + 4096]; + + for (;;) { + ssize_t n = read(fd, buf, sizeof(buf)); + struct fuse_in_header *ih = (void *)buf; + + if (n < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; + return; /* device closed on unmount/abort */ + } + if (n < (ssize_t)sizeof(*ih)) + continue; + + switch (ih->opcode) { + case FUSE_INIT: { + struct fuse_init_in *in = (void *)(ih + 1); + struct fuse_init_out out = {0}; + uint64_t flags = FUSE_INIT_EXT; + + out.major = FUSE_KERNEL_VERSION; + out.minor = FUSE_KERNEL_MINOR_VERSION; + out.max_readahead = in->max_readahead; + out.max_write = SERVER_MAX_WRITE; + out.max_background = 16; + out.congestion_threshold = 12; + if (advertise) + flags |= FUSE_HAS_SYNCFS; + out.flags = flags; + out.flags2 = flags >> 32; + reply(fd, ih->unique, 0, &out, sizeof(out)); + break; + } + case FUSE_GETATTR: { + struct fuse_attr_out out = {0}; + + fill_attr(&out.attr, FUSE_ROOT_ID, S_IFDIR | 0755, 2); + reply(fd, ih->unique, 0, &out, sizeof(out)); + break; + } + case FUSE_SYNCFS: { + uint64_t one = 1; + + if (write(syncfs_evfd, &one, sizeof(one)) < 0) + ksft_print_msg("server eventfd write failed: %s\n", + strerror(errno)); + reply(fd, ih->unique, 0, NULL, 0); + break; + } + default: + /* + * Anything else (e.g. OPENDIR from opening the mount + * root) is not needed to drive this test; -ENOSYS lets + * the kernel proceed. + */ + reply(fd, ih->unique, -ENOSYS, NULL, 0); + break; + } + } +} + +/* + * Mount a fuse fs backed by a forked server, issue syncfs(), and report + * whether the server observed FUSE_SYNCFS. Returns 0 on success, -1 if the + * environment could not support the test (caller should skip). + * + * If @unpriv_open is set, /dev/fuse is opened with CAP_SYS_ADMIN dropped + * (regained only for the mount() syscall), so the device opener -- the + * server principal FUSE_HAS_SYNCFS is gated on -- lacks the privilege even + * though it remains in the initial user namespace. + */ +static int do_mount_and_syncfs(const char *mnt, int advertise, int unpriv_open, + int *seen) +{ + struct pollfd pfd = { .events = POLLIN }; + char opts[256]; + int fd, mfd = -1, i; + pid_t pid; + + syncfs_evfd = eventfd(0, EFD_CLOEXEC); + if (syncfs_evfd < 0) + return -1; + + if (unpriv_open && set_sysadmin(0)) + goto out_evfd; + fd = open("/dev/fuse", O_RDWR); + if (unpriv_open && set_sysadmin(1)) { + if (fd >= 0) + close(fd); + goto out_evfd; + } + if (fd < 0) + goto out_evfd; + + mkdir(mnt, 0755); + snprintf(opts, sizeof(opts), + "fd=%d,rootmode=40000,user_id=%d,group_id=%d", + fd, getuid(), getgid()); + + if (mount("fuse", mnt, "fuse", 0, opts) < 0) + goto out_fd; + + pid = fork(); + if (pid < 0) + goto out_umount; + if (pid == 0) { + run_server(fd, advertise); + _exit(0); + } + + /* + * The parent does not service the fuse fd; the child does. Close our + * copy so the kernel sees a single server, and so that if the child + * dies the connection aborts instead of hanging us forever. + */ + close(fd); + + /* + * mount() returns before the server has answered FUSE_INIT, so the + * first open() can race and fail with ENOTCONN; retry until the + * handshake settles. + */ + for (i = 0; i < 1000; i++) { + mfd = open(mnt, O_RDONLY | O_DIRECTORY); + if (mfd >= 0) + break; + usleep(1000); + } + if (mfd >= 0) { + syncfs(mfd); + close(mfd); + } + + /* + * No waiting is needed: the server writes syncfs_evfd before it replies + * to FUSE_SYNCFS, and that reply is what unblocks the synchronous + * syncfs() above. So once syncfs() has returned, the eventfd is already + * signalled if the opcode was propagated, and will never be otherwise. + * poll() with a zero timeout therefore decides both cases immediately. + */ + pfd.fd = syncfs_evfd; + *seen = poll(&pfd, 1, 0) > 0 && (pfd.revents & POLLIN); + + kill(pid, SIGKILL); + waitpid(pid, NULL, 0); + umount2(mnt, MNT_DETACH); + close(syncfs_evfd); + return 0; + +out_umount: + umount2(mnt, MNT_DETACH); +out_fd: + close(fd); +out_evfd: + close(syncfs_evfd); + return -1; +} + +int main(void) +{ + char mnt[] = "/tmp/fuse_syncfs_XXXXXX"; + int seen, ret; + + ksft_print_header(); + ksft_set_plan(3); + + /* Hard watchdog: never let a stuck syncfs hang the test runner. */ + signal(SIGALRM, SIG_DFL); + alarm(60); + + if (geteuid() != 0) + ksft_exit_skip("test requires root to mount fuse\n"); + + if (!mkdtemp(mnt)) + ksft_exit_fail_msg("mkdtemp failed\n"); + + /* T1: host-root mount, server opts in -> syncfs must reach server. */ + ret = do_mount_and_syncfs(mnt, 1, 0, &seen); + if (ret < 0) + ksft_test_result_skip("T1: could not mount fuse\n"); + else + ksft_test_result(seen, + "T1 host-root + FUSE_HAS_SYNCFS: server receives FUSE_SYNCFS\n"); + + /* T2: host-root mount, server does NOT opt in -> no FUSE_SYNCFS. */ + ret = do_mount_and_syncfs(mnt, 0, 0, &seen); + if (ret < 0) + ksft_test_result_skip("T2: could not mount fuse\n"); + else + ksft_test_result(!seen, + "T2 host-root, no opt-in: server does NOT receive FUSE_SYNCFS\n"); + + /* + * T3: server opts in but opened /dev/fuse without CAP_SYS_ADMIN while + * still in the initial user namespace -> kernel must withhold + * FUSE_SYNCFS. This is the case that distinguishes gating on the + * server's privilege from gating on the mount's user namespace. + */ + ret = do_mount_and_syncfs(mnt, 1, 1, &seen); + if (ret < 0) + ksft_test_result_skip("T3: could not mount fuse unprivileged\n"); + else + ksft_test_result(!seen, + "T3 init_userns, opener lacks CAP_SYS_ADMIN: FUSE_SYNCFS withheld\n"); + + rmdir(mnt); + ksft_finished(); +} |
