summaryrefslogtreecommitdiff
path: root/tools/testing
diff options
context:
space:
mode:
authorMark Brown <broonie@kernel.org>2026-10-21 11:22:51 +0100
committerMark Brown <broonie@kernel.org>2026-10-21 11:22:51 +0100
commite52b2c0ec8860e82b285dbafb008ee424932964d (patch)
treec3d15e3329be676e57c335120a24aac0bb61d92d /tools/testing
parentb7772aac9c8a7140895928034db43b6c6f827538 (diff)
parent5a9745611d621ea160854936eb6370e610887bfb (diff)
downloadlinux-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')
-rw-r--r--tools/testing/selftests/filesystems/fuse/.gitignore4
-rw-r--r--tools/testing/selftests/filesystems/fuse/Makefile30
-rw-r--r--tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c299
-rw-r--r--tools/testing/selftests/filesystems/fuse/fuse_common.c58
-rw-r--r--tools/testing/selftests/filesystems/fuse/fuse_common.h33
-rw-r--r--tools/testing/selftests/filesystems/fuse/fuse_mnt.c17
-rw-r--r--tools/testing/selftests/filesystems/fuse/fuse_readdir_cache_test.c273
-rw-r--r--tools/testing/selftests/filesystems/fuse/fusectl_test.c9
-rw-r--r--tools/testing/selftests/filesystems/fuse/test_syncfs.c309
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();
+}