* [RFC PATCH v3 1/8] Documentation: fuse: add document on caches being used by FUSE
2026-09-04 10:39 [RFC PATCH v3 0/8] fuse: caches documentation and testing Luis Henriques
@ 2026-09-04 10:39 ` Luis Henriques
2026-09-04 10:39 ` [RFC PATCH v3 2/8] selftests/fuse: convert fusectl test to fuse3 Luis Henriques
` (6 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Luis Henriques @ 2026-09-04 10:39 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Chen Linxuan, Jonathan Corbet,
Shuah Khan
Cc: fuse-devel, linux-kernel, linux-kselftest, Matt Harvey,
kernel-dev, Luis Henriques
This new file aims at documenting the caches that are used by FUSE. At
the moment only symlink, attributes, ACLs and readdir caches are described.
Signed-off-by: Luis Henriques <luis@igalia.com>
---
.../filesystems/fuse/fuse-caches.rst | 158 ++++++++++++++++++
1 file changed, 158 insertions(+)
create mode 100644 Documentation/filesystems/fuse/fuse-caches.rst
diff --git a/Documentation/filesystems/fuse/fuse-caches.rst b/Documentation/filesystems/fuse/fuse-caches.rst
new file mode 100644
index 000000000000..e9f60355d7ff
--- /dev/null
+++ b/Documentation/filesystems/fuse/fuse-caches.rst
@@ -0,0 +1,158 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+===========
+FUSE Caches
+===========
+
+Introduction
+============
+
+This document summarises the different types of caches used in FUSE. For each
+cache type, it documents the rules to insert data into it. It also documents the
+rules for validating and invalidating data in the cache.
+
+symlink caching
+===============
+
+Whenever there's a link resolution request for a FUSE filesystem, the VFS will
+call into ``fuse_get_link()``, the ``->get_link()`` inode operation. This
+function will then send a ``FUSE_READLINK`` request to the user-space FUSE
+server.
+
+The server can ask the kernel to cache all link resolutions by setting the
+``FUSE_CACHE_SYMLINKS`` flag during the ``FUSE_INIT`` negotiation. If this flag
+is set, when the VFS calls into the ``->get_link()`` operation, FUSE will
+immediately call ``__page_get_link()``. The first time this is done for a
+specific inode, it will result in sending the ``FUSE_READLINK`` request to
+user-space. But the result returned from this request will then be added into
+the page-cache. The next time this link needs to be resolved, it will use the
+link resolution already cached, and will only fallback to user-space if the
+folio isn't up-to-date.
+
+Attributes caching
+==================
+
+Inode attributes may be obtained from user-space by different FUSE operations.
+For example, ``FUSE_LOOKUP``, ``FUSE_GETATTR``, and also several other
+operations that create file system objects (e.g. ``FUSE_MKDIR``). These
+attributes obtained from user-space are cached by the kernel. They have,
+however, a timeout associated and once it expires, they are invalidated. The
+next time the attributes are needed, a request (``FUSE_GETATTR``) will be sent
+to the FUSE server.
+
+The ``FUSE_GETATTR`` request can be sent to user-space in three different
+scenarios:
+
+#. if the attributes for the inode aren't yet available in the kernel;
+#. if they are not valid any more (timed-out, or have been invalidated), or
+#. if there is an explicit request for forcing the request to be sent (for
+ example, by using the ``AT_STATX_FORCE_SYNC`` flag in ``statx``).
+
+Regarding the attributes invalidation, they may happen in several occasions:
+
+- Upon user-space request for invalidation:
+
+ - A ``FUSE_NOTIFY_INVAL_INODE`` will invalidate ``STATX_BASIC_STATS``;
+ - ``FUSE_NOTIFY_DELETE`` and ``FUSE_NOTIFY_INVAL_ENTRY`` invalidate
+ ``FUSE_STATX_MODDIR``.
+
+- When setting (or removing) an ACL on an inode ``STATX_CTIME`` is invalidated;
+ if the ``FUSE_POSIX_ACL`` flag was set by the FUSE server,
+ ``STATX_BASIC_STATS`` will also be invalidated.
+- On a ``->rename()`` operation, both the old and the new entities will have
+ it's ctime invalidated (``STATX_CTIME``). Also, the directories for both the
+ old and the new entities will also have their attributes invalidated
+ (``FUSE_STATX_MODDIR``)
+- When creating or deleting a new file system object (``->link()/->unlink()``,
+ ``->symlink()``, ``->mkdir()/->rmdir()``, ``->tmpfile()``, or
+ ``->atomic_open()``), the directory where the object is created/deleted will
+ have it's attributes invalidated (``FUSE_STATX_MODDIR``).
+- If a ``->link()`` operation is interrupted by a signal (``EINTR``) the inode
+ being linked will have it's attributes invalidated (``STATX_BASIC_STATS``).
+- When doing a readdir (``->iterate_shared()`` operation) and the directory
+ contents is not cached, ``STATX_ATIME`` attributes will be invalidated.
+- When doing a symlink resolution (by sending a ``FUSE_READLINK`` request)
+ ``STATX_ATIME`` will be invalidated.
+- When doing a ``->flush()`` (i.e. sending a ``FUSE_FLUSH`` request) and
+ writeback cache is enabled, ``STATX_BLOCKS`` will be invalidated.
+- When truncating a file on open using ``O_TRUNC`` open flag (and the FUSE
+ server has set ``FUSE_ATOMIC_O_TRUNC`` during ``FUSE_INIT``), then
+ ``FUSE_STATX_MODSIZE`` will be invalidated.
+- When setting attributes in an inode (``->setattr()``) and there's a signal
+ that interrupts the operation (``EINTR``), then ``STATX_BASIC_STATS`` will be
+ invalidated.
+- When data is read from a file, ``STATX_ATIME`` will be invalidated (unless the
+ file system is read-only).
+- When data is written info a file, ``FUSE_STATX_MODSIZE`` is invalidated.
+
+ACL caching
+===========
+
+FUSE has allowed the usage of POSIX Access Control Lists (ACLs) for a long time,
+as they can be set and accessed simply as extended attributes. However, it was
+only with the introduction of the ``FUSE_POSIX_ACL`` flag that ACLs started to
+be fully supported. Without this flag being set during the ``FUSE_INIT``
+negotiation, ACLs can still be set, but the VFS won't use them for performing
+permission checks - that would be the user-space server's responsibility.
+
+Also, without setting ``FUSE_POSIX_ACL``, ACLs will not be cached by the kernel.
+In this case, new inodes ``i_acl`` and ``i_default_acl`` fields will be set to
+``ACL_DONT_CACHE``.
+
+On the other hand, if the ``FUSE_POSIX_ACL`` flag is set then, when an inode ACL
+is accessed, VFS will first check if it's already cached. If it is not, FUSE
+``->get_acl()`` operation (``fuse_get_acl()``) is called, which will eventually
+send a user-space request. Future accesses to this inode ACL will use the cached
+data.
+
+Setting an ACL in an inode will also result in sending a request to the FUSE
+server for setting it. But this operation won't immediately cache the ACL -- it
+will only be cached after it is accessed again and requested from user-space.
+
+On the other hand, ACLs will be removed from the cache in the following
+situations:
+
+- When setting an ACL in an inode (and the ``FUSE_POSIX_ACL`` flag is set),
+ previously cached ACLs for this inode will be invalidated.
+- When invalidating an inode through the ``FUSE_NOTIFY_INVAL_INODE`` operation.
+- When ``->d_revalidate()`` is called for a dentry that requires a lookup (e.g.
+ it has expired) and that lookup operation is successful.
+- When the VFS needs to check access rights for an inode (by calling
+ ``->permission()``), attributes may need to be refreshed. If that happens, any
+ cached ACLs for that inode will be invalidated.
+- After setting an inode attribute (i.e. operation ``FUSE_SETATTR`` is sent to
+ user-space), the user-space server may have also updated the ACLs. Thus, any
+ cached ACLs for this inode are also invalidated.
+- While processing ``FUSE_READDIRPLUS`` and an already existing dentry needs to
+ be updated.
+- In general, when there is the need to send a ``FUSE_STATX`` or
+ ``FUSE_GETATTR`` to user-space (e.g. when attributes expired).
+
+readdir caching
+===============
+
+When opening a directory a ``FUSE_OPENDIR`` will be sent to the FUSE server, and
+server will be responsible for setting the open flags related with caching,
+namely ``FOPEN_KEEP_CACHE`` and ``FOPEN_CACHE_DIR``.
+
+If neither flags are set by the user-space FUSE server, then every ``readdir``
+will result in a ``FUSE_READDIR`` (or ``FUSE_READDIRPLUS``) request being sent.
+If ``FOPEN_CACHE_DIR`` is set by the server, then the result of a ``readdir``
+will be cached by the kernel and reused for the current open. If
+``FOPEN_KEEP_CACHE`` is also set, the cache will be kept and reused in the
+future, when the directory is open again for reading.
+
+The readdir cache will also expire and reset if the inode's ``mtime`` or
+``iversion`` don't match the cached values, or if the FUSE connection ``epoch``
+doesn't match the cache ``epoch``.
+
+dentry caching
+==============
+
+TBD
+
+data caching
+============
+
+TBD
+
^ permalink raw reply related [flat|nested] 9+ messages in thread* [RFC PATCH v3 2/8] selftests/fuse: convert fusectl test to fuse3
2026-09-04 10:39 [RFC PATCH v3 0/8] fuse: caches documentation and testing Luis Henriques
2026-09-04 10:39 ` [RFC PATCH v3 1/8] Documentation: fuse: add document on caches being used by FUSE Luis Henriques
@ 2026-09-04 10:39 ` Luis Henriques
2026-09-04 10:39 ` [RFC PATCH v3 3/8] selftests/fuse: check that fusectlfs is mounted Luis Henriques
` (5 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Luis Henriques @ 2026-09-04 10:39 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Chen Linxuan, Jonathan Corbet,
Shuah Khan
Cc: fuse-devel, linux-kernel, linux-kselftest, Matt Harvey,
kernel-dev, Luis Henriques
Since it is probably not worth adding new fuse kselftests based on fuse2,
it is a good idea to convert the single existing test to fuse3. The
conversion is trivial, as it only requires some changes to function
signatures (the gettattr and truncate fuse operations), and to the filler()
helper.
Signed-off-by: Luis Henriques <luis@igalia.com>
Reviewed-by: Amir Goldstein <amir73il@gmail.com>
---
.../selftests/filesystems/fuse/Makefile | 20 +++++++------------
.../selftests/filesystems/fuse/fuse_mnt.c | 17 +++++++++-------
2 files changed, 17 insertions(+), 20 deletions(-)
diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/testing/selftests/filesystems/fuse/Makefile
index 95a1ee947ca7..a3ee9b3a2f5d 100644
--- a/tools/testing/selftests/filesystems/fuse/Makefile
+++ b/tools/testing/selftests/filesystems/fuse/Makefile
@@ -4,31 +4,25 @@ CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
TEST_GEN_PROGS := fusectl_test
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_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
$(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: CFLAGS += $(VAR_CFLAGS)
+$(OUTPUT)/fuse_acl_cache_test: LDLIBS += $(VAR_LDLIBS)
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;
^ permalink raw reply related [flat|nested] 9+ messages in thread* [RFC PATCH v3 3/8] selftests/fuse: check that fusectlfs is mounted
2026-09-04 10:39 [RFC PATCH v3 0/8] fuse: caches documentation and testing Luis Henriques
2026-09-04 10:39 ` [RFC PATCH v3 1/8] Documentation: fuse: add document on caches being used by FUSE Luis Henriques
2026-09-04 10:39 ` [RFC PATCH v3 2/8] selftests/fuse: convert fusectl test to fuse3 Luis Henriques
@ 2026-09-04 10:39 ` Luis Henriques
2026-09-04 10:39 ` [RFC PATCH v3 4/8] selftests/fuse: factor-out test fixture setup/teardown Luis Henriques
` (4 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Luis Henriques @ 2026-09-04 10:39 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Chen Linxuan, Jonathan Corbet,
Shuah Khan
Cc: fuse-devel, linux-kernel, linux-kselftest, Matt Harvey,
kernel-dev, Luis Henriques
The control filesystem for FUSE needs to be mounted for the fusectl_test to
be successfully run. Skip the test is that is not the case.
Signed-off-by: Luis Henriques <luis@igalia.com>
---
tools/testing/selftests/filesystems/fuse/fusectl_test.c | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
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);
^ permalink raw reply related [flat|nested] 9+ messages in thread* [RFC PATCH v3 4/8] selftests/fuse: factor-out test fixture setup/teardown
2026-09-04 10:39 [RFC PATCH v3 0/8] fuse: caches documentation and testing Luis Henriques
` (2 preceding siblings ...)
2026-09-04 10:39 ` [RFC PATCH v3 3/8] selftests/fuse: check that fusectlfs is mounted Luis Henriques
@ 2026-09-04 10:39 ` Luis Henriques
2026-09-04 10:39 ` [RFC PATCH v3 5/8] selftests/fuse: use dynamically allocated memory to store ACLs Luis Henriques
` (3 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Luis Henriques @ 2026-09-04 10:39 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Chen Linxuan, Jonathan Corbet,
Shuah Khan
Cc: fuse-devel, linux-kernel, linux-kselftest, Matt Harvey,
kernel-dev, Luis Henriques
In order to reduce new tests setup/teardown code duplication, factor-out
these functions from the existing acl_cache test into a new fuse_common.c
file that can be reused in other tests.
Signed-off-by: Luis Henriques <luis@igalia.com>
---
.../selftests/filesystems/fuse/Makefile | 8 ++-
.../filesystems/fuse/fuse_acl_cache_test.c | 62 +++----------------
.../selftests/filesystems/fuse/fuse_common.c | 60 ++++++++++++++++++
.../selftests/filesystems/fuse/fuse_common.h | 25 ++++++++
4 files changed, 100 insertions(+), 55 deletions(-)
create mode 100644 tools/testing/selftests/filesystems/fuse/fuse_common.c
create mode 100644 tools/testing/selftests/filesystems/fuse/fuse_common.h
diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/testing/selftests/filesystems/fuse/Makefile
index a3ee9b3a2f5d..7744f796eb06 100644
--- a/tools/testing/selftests/filesystems/fuse/Makefile
+++ b/tools/testing/selftests/filesystems/fuse/Makefile
@@ -21,8 +21,12 @@ ifeq ($(VAR_LDLIBS),)
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 += $(VAR_CFLAGS)
-$(OUTPUT)/fuse_acl_cache_test: LDLIBS += $(VAR_LDLIBS)
+$(OUTPUT)/fuse_acl_cache_test: fuse_common.c fuse_acl_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..12cbf9753d03 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):
@@ -176,69 +168,33 @@ static const struct fuse_lowlevel_ops fs_ops = {
.getxattr = fs_getxattr,
};
-/* ---- 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 mountpoint[MOUNTPOINT_SZ];
char file_path[PATH_MAX];
pthread_t thread;
};
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);
+ g_ds.acl = acl_a;
+ g_ds.acl_size = sizeof(acl_a);
g_ds.getxattr_count = 0;
- strcpy(self->mountpoint, "/tmp/acl_cache_test_XXXXXX");
- if (!mkdtemp(self->mountpoint))
- SKIP(return, "mkdtemp: %s", strerror(errno));
+ if (fs_setup(&self->se, self->mountpoint, &fs_ops, &self->thread, 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);
}
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->se, self->thread, self->mountpoint);
}
static int do_force_statx(const char *path)
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..3a91cac25b81
--- /dev/null
+++ b/tools/testing/selftests/filesystems/fuse/fuse_common.c
@@ -0,0 +1,60 @@
+// 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(struct fuse_session **se, char *mountpoint,
+ const struct fuse_lowlevel_ops *fs_ops,
+ pthread_t *thread, char *err)
+{
+ char *fuse_argv[] = { "fuse_test", NULL };
+ struct fuse_args args = FUSE_ARGS_INIT(1, fuse_argv);
+
+ strcpy(mountpoint, MOUNTPOINT_TEMPLATE);
+ if (!mkdtemp(mountpoint)) {
+ snprintf(err, MAX_ERR_MSG, "mkdtemp: %s", strerror(errno));
+ return -1;
+ }
+
+ *se = fuse_session_new(&args, fs_ops, sizeof(*fs_ops), NULL);
+ if (!*se) {
+ rmdir(mountpoint);
+ snprintf(err, MAX_ERR_MSG, "fuse_session_new failed");
+ return -1;
+ }
+
+ if (fuse_session_mount(*se, mountpoint)) {
+ fuse_session_destroy(*se);
+ rmdir(mountpoint);
+ snprintf(err, MAX_ERR_MSG, "fuse_session_mount failed "
+ "(missing fusermount3 or insufficient privileges)");
+ return -1;
+ }
+
+ if (pthread_create(thread, NULL, run_daemon, *se)) {
+ fuse_session_unmount(*se);
+ fuse_session_destroy(*se);
+ rmdir(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_session *se, pthread_t thread, char *mountpoint)
+{
+ fuse_session_exit(se);
+ fuse_session_unmount(se);
+ pthread_join(thread, NULL);
+ fuse_session_destroy(se);
+ rmdir(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..77d5eb58550d
--- /dev/null
+++ b/tools/testing/selftests/filesystems/fuse/fuse_common.h
@@ -0,0 +1,25 @@
+#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>
+
+#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
+
+int fs_setup(struct fuse_session **se, char *mountpoint,
+ const struct fuse_lowlevel_ops *fs_ops,
+ pthread_t *thread, char *err);
+void fs_teardown(struct fuse_session *se, pthread_t thread, char *mountpoint);
+
+#endif /* __SELFTEST_FUSE_COMMON_H__ */
^ permalink raw reply related [flat|nested] 9+ messages in thread* [RFC PATCH v3 5/8] selftests/fuse: use dynamically allocated memory to store ACLs
2026-09-04 10:39 [RFC PATCH v3 0/8] fuse: caches documentation and testing Luis Henriques
` (3 preceding siblings ...)
2026-09-04 10:39 ` [RFC PATCH v3 4/8] selftests/fuse: factor-out test fixture setup/teardown Luis Henriques
@ 2026-09-04 10:39 ` Luis Henriques
2026-09-04 10:39 ` [RFC PATCH v3 6/8] selftests/fuse: add some extra ACL caching tests Luis Henriques
` (2 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Luis Henriques @ 2026-09-04 10:39 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Chen Linxuan, Jonathan Corbet,
Shuah Khan
Cc: fuse-devel, linux-kernel, linux-kselftest, Matt Harvey,
kernel-dev, Luis Henriques
Instead of directly using static arrays for the ACL value, allocate memory
for storing it. This will make it easier to implement ACL tests that also
set the xattr dynamically.
Signed-off-by: Luis Henriques <luis@igalia.com>
---
.../filesystems/fuse/fuse_acl_cache_test.c | 32 ++++++++++++++++---
1 file changed, 27 insertions(+), 5 deletions(-)
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 12cbf9753d03..c2d6658ff7de 100644
--- a/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c
+++ b/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c
@@ -80,7 +80,7 @@ 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;
};
@@ -142,15 +142,26 @@ 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 && (size >= acl_size))
+ memcpy(acl, g_ds.acl, acl_size);
g_ds.getxattr_count++;
pthread_mutex_unlock(&g_ds.lock);
@@ -160,6 +171,8 @@ static void fs_getxattr(fuse_req_t req, fuse_ino_t ino, const char *name,
fuse_reply_err(req, ERANGE);
else
fuse_reply_buf(req, (const char *)acl, acl_size);
+
+ free(acl);
}
static const struct fuse_lowlevel_ops fs_ops = {
@@ -181,8 +194,10 @@ FIXTURE_SETUP(acl_cache)
{
char err[MAX_ERR_MSG];
- g_ds.acl = acl_a;
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;
if (fs_setup(&self->se, self->mountpoint, &fs_ops, &self->thread, err))
@@ -195,6 +210,7 @@ FIXTURE_SETUP(acl_cache)
FIXTURE_TEARDOWN(acl_cache)
{
fs_teardown(self->se, self->thread, self->mountpoint);
+ free(g_ds.acl);
}
static int do_force_statx(const char *path)
@@ -210,6 +226,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).
@@ -270,8 +287,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));
^ permalink raw reply related [flat|nested] 9+ messages in thread* [RFC PATCH v3 6/8] selftests/fuse: add some extra ACL caching tests
2026-09-04 10:39 [RFC PATCH v3 0/8] fuse: caches documentation and testing Luis Henriques
` (4 preceding siblings ...)
2026-09-04 10:39 ` [RFC PATCH v3 5/8] selftests/fuse: use dynamically allocated memory to store ACLs Luis Henriques
@ 2026-09-04 10:39 ` Luis Henriques
2026-09-04 10:39 ` [RFC PATCH v3 7/8] selftests/fuse: add fuse symlink caching test Luis Henriques
2026-09-04 10:39 ` [RFC PATCH v3 8/8] selftests/fuse: add fuse readdir " Luis Henriques
7 siblings, 0 replies; 9+ messages in thread
From: Luis Henriques @ 2026-09-04 10:39 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Chen Linxuan, Jonathan Corbet,
Shuah Khan
Cc: fuse-devel, linux-kernel, linux-kselftest, Matt Harvey,
kernel-dev, Luis Henriques
This adds some extra tests to ACL caching:
- Verify that reading ACLs results in the expected number of requests
being sent user-space, depending on whether cache is enabled or disabled
- Verify caching behaviour on some caching invalidation scenarios
While there, add test binary to .gitignore.
Signed-off-by: Luis Henriques <luis@igalia.com>
---
.../selftests/filesystems/fuse/.gitignore | 1 +
.../filesystems/fuse/fuse_acl_cache_test.c | 179 ++++++++++++++++++
2 files changed, 180 insertions(+)
diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore b/tools/testing/selftests/filesystems/fuse/.gitignore
index fb51603fe419..f7f3dd345a50 100644
--- a/tools/testing/selftests/filesystems/fuse/.gitignore
+++ b/tools/testing/selftests/filesystems/fuse/.gitignore
@@ -2,3 +2,4 @@
fuse_mnt
fusectl_test
write_extend_eof_test
+fuse_acl_cache_test
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 c2d6658ff7de..9608a0adb967 100644
--- a/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c
+++ b/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c
@@ -83,6 +83,7 @@ struct daemon_state {
uint8_t *acl;
size_t acl_size;
int getxattr_count;
+ bool cache;
};
/*
@@ -91,9 +92,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)
{
@@ -115,6 +124,8 @@ 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;
+ e.attr.st_uid = getuid();
+ e.attr.st_gid = getgid();
fuse_reply_entry(req, &e);
}
@@ -175,10 +186,38 @@ static void fs_getxattr(fuse_req_t req, fuse_ino_t ino, const char *name,
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;
+ else if (!strcmp(name, "system.posix_acl_access")) {
+ acl = malloc(size);
+ if (acl) {
+ 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);
+ } else
+ ret = ENOMEM;
+ } else
+ ret = ENOTSUP;
+
+ 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,
};
/* ---- kselftest harness --------------------------------------------------- */
@@ -199,6 +238,7 @@ FIXTURE_SETUP(acl_cache)
ASSERT_NE(g_ds.acl, NULL);
memcpy(g_ds.acl, acl_a, g_ds.acl_size);
g_ds.getxattr_count = 0;
+ g_ds.cache = false;
if (fs_setup(&self->se, self->mountpoint, &fs_ops, &self->thread, err))
SKIP(return, err);
@@ -322,4 +362,143 @@ TEST_F(acl_cache, stale_after_force_sync)
EXPECT_EQ(count, 4);
}
+FIXTURE(acl_cache_onoff)
+{
+ struct fuse_session *se;
+ char mountpoint[MOUNTPOINT_SZ];
+ char pathname[PATH_MAX];
+ pthread_t thread;
+};
+
+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];
+
+ g_ds.acl = NULL;
+ g_ds.acl_size = 0;
+ g_ds.getxattr_count = 0;
+ g_ds.cache = variant->cache;
+
+ if (fs_setup(&self->se, self->mountpoint, &fs_ops, &self->thread, err))
+ SKIP(return, err);
+
+ snprintf(self->pathname, sizeof(self->pathname),
+ "%s/" FILE_NAME, self->mountpoint);
+}
+
+FIXTURE_TEARDOWN(acl_cache_onoff)
+{
+ fs_teardown(self->se, self->thread, self->mountpoint);
+ 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 i 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_INODE_INVAL)
+ */
+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->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
^ permalink raw reply related [flat|nested] 9+ messages in thread* [RFC PATCH v3 7/8] selftests/fuse: add fuse symlink caching test
2026-09-04 10:39 [RFC PATCH v3 0/8] fuse: caches documentation and testing Luis Henriques
` (5 preceding siblings ...)
2026-09-04 10:39 ` [RFC PATCH v3 6/8] selftests/fuse: add some extra ACL caching tests Luis Henriques
@ 2026-09-04 10:39 ` Luis Henriques
2026-09-04 10:39 ` [RFC PATCH v3 8/8] selftests/fuse: add fuse readdir " Luis Henriques
7 siblings, 0 replies; 9+ messages in thread
From: Luis Henriques @ 2026-09-04 10:39 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Chen Linxuan, Jonathan Corbet,
Shuah Khan
Cc: fuse-devel, linux-kernel, linux-kselftest, Matt Harvey,
kernel-dev, Luis Henriques
This patch adds a simple test that allows to verify that, when resolving a
symlink, user-space is called only the first time when caching is enabled
or, if caching is disabled, every time the symlink resolution is requested.
Signed-off-by: Luis Henriques <luis@igalia.com>
---
.../selftests/filesystems/fuse/.gitignore | 1 +
.../selftests/filesystems/fuse/Makefile | 2 +
.../fuse/fuse_symlink_cache_test.c | 167 ++++++++++++++++++
3 files changed, 170 insertions(+)
create mode 100644 tools/testing/selftests/filesystems/fuse/fuse_symlink_cache_test.c
diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore b/tools/testing/selftests/filesystems/fuse/.gitignore
index f7f3dd345a50..ebfe7133d811 100644
--- a/tools/testing/selftests/filesystems/fuse/.gitignore
+++ b/tools/testing/selftests/filesystems/fuse/.gitignore
@@ -3,3 +3,4 @@ fuse_mnt
fusectl_test
write_extend_eof_test
fuse_acl_cache_test
+fuse_symlink_cache_test
diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/testing/selftests/filesystems/fuse/Makefile
index 7744f796eb06..3a5a557dde7a 100644
--- a/tools/testing/selftests/filesystems/fuse/Makefile
+++ b/tools/testing/selftests/filesystems/fuse/Makefile
@@ -5,6 +5,7 @@ CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
TEST_GEN_PROGS := fusectl_test
TEST_GEN_PROGS += write_extend_eof_test
TEST_GEN_PROGS += fuse_acl_cache_test
+TEST_GEN_PROGS += fuse_symlink_cache_test
TEST_GEN_FILES := fuse_mnt
include ../../lib.mk
@@ -28,5 +29,6 @@ $(OUTPUT)/fuse_mnt: CFLAGS += $(VAR_CFLAGS)
$(OUTPUT)/fuse_mnt: LDLIBS += $(VAR_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
EXTRA_CLEAN := fuse_common.o
diff --git a/tools/testing/selftests/filesystems/fuse/fuse_symlink_cache_test.c b/tools/testing/selftests/filesystems/fuse/fuse_symlink_cache_test.c
new file mode 100644
index 000000000000..ca3c5cdaf578
--- /dev/null
+++ b/tools/testing/selftests/filesystems/fuse/fuse_symlink_cache_test.c
@@ -0,0 +1,167 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Simple filesystem to test FUSE symlink cache
+ *
+ * This is a simple FUSE filesystem that contains two objects: a file named
+ * 'file' and a symlink to that file named 'link'. If symlink caching is
+ * disabled (i.e. FUSE_CAP_CACHE_SYMLINKS is reset during FUSE_INIT), whenever
+ * the ->readlink() is executed to resolve 'link' a counter will be incremented.
+ *
+ * If symlink caching is enabled (i.e. FUSE_CAP_CACHE_SYMLINKS is set during
+ * FUSE_INIT), resolving a symlink will only call into user-space the first
+ * time.
+ */
+
+#define FUSE_USE_VERSION 31
+
+#include <stdio.h>
+#include <limits.h>
+#include <fuse_lowlevel.h>
+
+#include "kselftest_harness.h"
+
+#include "fuse_common.h"
+
+#define FILENAME "file"
+#define FILE_INO 42
+
+#define LINKNAME "link"
+#define LINK_INO 43
+
+#define TIMEOUT 86400.0f
+
+struct test_state {
+ pthread_mutex_t lock;
+ bool cache;
+ int readlink_counter;
+} test_state = {
+ .lock = PTHREAD_MUTEX_INITIALIZER,
+};
+
+static void fs_init(void *userdata, struct fuse_conn_info *conn)
+{
+ pthread_mutex_lock(&test_state.lock);
+ if (test_state.cache)
+ fuse_set_feature_flag(conn, FUSE_CAP_CACHE_SYMLINKS);
+ else
+ fuse_unset_feature_flag(conn, FUSE_CAP_CACHE_SYMLINKS);
+ pthread_mutex_unlock(&test_state.lock);
+}
+
+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, FILENAME) && !(strcmp(name, LINKNAME))))
+ fuse_reply_err(req, ENOENT);
+ else {
+ if (!strcmp(name, FILENAME)) {
+ e.ino = FILE_INO;
+ e.attr.st_mode = S_IFREG | 0444;
+ e.attr.st_nlink = 2;
+ } else if (!strcmp(name, LINKNAME)) {
+ e.ino = LINK_INO;
+ e.attr.st_mode = S_IFLNK | 0444;
+ e.attr.st_nlink = 1;
+ e.attr.st_size = strlen(FILENAME);
+ } else {
+ e.ino = FUSE_ROOT_ID;
+ e.attr.st_mode = S_IFDIR | 0755;
+ e.attr.st_nlink = 2;
+ }
+ e.attr_timeout = TIMEOUT;
+ e.entry_timeout = TIMEOUT;
+ fuse_reply_entry(req, &e);
+ }
+}
+
+static void fs_readlink(fuse_req_t req, fuse_ino_t ino)
+{
+ char buf[PATH_MAX];
+ size_t sz = strlen(FILENAME);
+
+ if (ino != LINK_INO) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+
+ memcpy(buf, FILENAME, sz);
+ buf[sz] = '\0';
+ pthread_mutex_lock(&test_state.lock);
+ test_state.readlink_counter++;
+ pthread_mutex_unlock(&test_state.lock);
+
+ fuse_reply_readlink(req, buf);
+}
+
+static const struct fuse_lowlevel_ops fs_ops = {
+ .init = fs_init,
+ .lookup = fs_lookup,
+ .readlink = fs_readlink,
+};
+
+FIXTURE(symlink_cache)
+{
+ struct fuse_session *se;
+ char mountpoint[MOUNTPOINT_SZ];
+ pthread_t thread;
+};
+FIXTURE_VARIANT(symlink_cache)
+{
+ const bool cache;
+};
+FIXTURE_VARIANT_ADD(symlink_cache, symlinks_nocache)
+{
+ /* Variant with symlink cache disabled */
+ .cache = false,
+};
+FIXTURE_VARIANT_ADD(symlink_cache, symlinks_cache)
+{
+ /* Variant with symlink cache enabled */
+ .cache = true,
+};
+
+FIXTURE_SETUP(symlink_cache)
+{
+ char err[MAX_ERR_MSG];
+
+ pthread_mutex_lock(&test_state.lock);
+ test_state.readlink_counter = 0;
+ test_state.cache = variant->cache;
+ pthread_mutex_unlock(&test_state.lock);
+
+ if (fs_setup(&self->se, self->mountpoint, &fs_ops, &self->thread, err))
+ SKIP(return, err);
+}
+
+FIXTURE_TEARDOWN(symlink_cache)
+{
+ fs_teardown(self->se, self->thread, self->mountpoint);
+}
+
+TEST_F(symlink_cache, test_symlink_cache)
+{
+ char pathname[PATH_MAX];
+ char buf[PATH_MAX];
+ ssize_t sz;
+ int counter;
+ int i;
+
+ sprintf(pathname, "%s/%s", self->mountpoint, LINKNAME);
+ for (i = 0; i < 100; i++) {
+ sz = readlink(pathname, buf, PATH_MAX);
+ ASSERT_NE(sz, -1);
+ }
+ pthread_mutex_lock(&test_state.lock);
+ counter = test_state.readlink_counter;
+ pthread_mutex_unlock(&test_state.lock);
+
+ if (variant->cache) {
+ ASSERT_EQ(counter, 1);
+ } else {
+ ASSERT_EQ(counter, 100);
+ }
+}
+
+TEST_HARNESS_MAIN
^ permalink raw reply related [flat|nested] 9+ messages in thread* [RFC PATCH v3 8/8] selftests/fuse: add fuse readdir caching test
2026-09-04 10:39 [RFC PATCH v3 0/8] fuse: caches documentation and testing Luis Henriques
` (6 preceding siblings ...)
2026-09-04 10:39 ` [RFC PATCH v3 7/8] selftests/fuse: add fuse symlink caching test Luis Henriques
@ 2026-09-04 10:39 ` Luis Henriques
7 siblings, 0 replies; 9+ messages in thread
From: Luis Henriques @ 2026-09-04 10:39 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Chen Linxuan, Jonathan Corbet,
Shuah Khan
Cc: fuse-devel, linux-kernel, linux-kselftest, Matt Harvey,
kernel-dev, Luis Henriques
This new test will check the caching behaviour using combinations of two
opendir flags: FOPEN_KEEP_CACHE and FOPEN_CACHE_DIR.
Signed-off-by: Luis Henriques <luis@igalia.com>
---
.../selftests/filesystems/fuse/.gitignore | 1 +
.../selftests/filesystems/fuse/Makefile | 2 +
.../fuse/fuse_readdir_cache_test.c | 274 ++++++++++++++++++
3 files changed, 277 insertions(+)
create mode 100644 tools/testing/selftests/filesystems/fuse/fuse_readdir_cache_test.c
diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore b/tools/testing/selftests/filesystems/fuse/.gitignore
index ebfe7133d811..ffa356f23e8c 100644
--- a/tools/testing/selftests/filesystems/fuse/.gitignore
+++ b/tools/testing/selftests/filesystems/fuse/.gitignore
@@ -4,3 +4,4 @@ fusectl_test
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 3a5a557dde7a..570ced181168 100644
--- a/tools/testing/selftests/filesystems/fuse/Makefile
+++ b/tools/testing/selftests/filesystems/fuse/Makefile
@@ -6,6 +6,7 @@ TEST_GEN_PROGS := fusectl_test
TEST_GEN_PROGS += write_extend_eof_test
TEST_GEN_PROGS += fuse_acl_cache_test
TEST_GEN_PROGS += fuse_symlink_cache_test
+TEST_GEN_PROGS += fuse_readdir_cache_test
TEST_GEN_FILES := fuse_mnt
include ../../lib.mk
@@ -30,5 +31,6 @@ $(OUTPUT)/fuse_mnt: LDLIBS += $(VAR_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_readdir_cache_test.c b/tools/testing/selftests/filesystems/fuse/fuse_readdir_cache_test.c
new file mode 100644
index 000000000000..7c49cc6a23e3
--- /dev/null
+++ b/tools/testing/selftests/filesystems/fuse/fuse_readdir_cache_test.c
@@ -0,0 +1,274 @@
+// 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 <limits.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;
+ bool cache_readdir;
+ bool keep_cache;
+ int readdir_counter;
+} 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);
+ else {
+ if (!strcmp(name, DIRNAME)) {
+ e.ino = DIR_INO;
+ e.attr.st_mode = S_IFDIR | 0755;
+ e.attr.st_nlink = 1;
+ } else {
+ e.ino = FUSE_ROOT_ID;
+ e.attr.st_mode = S_IFDIR | 0755;
+ e.attr.st_nlink = 2;
+ }
+ e.attr.st_mtime = time(NULL);
+ 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;
+ st->st_mtime = time(NULL);
+
+ 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_session *se;
+ char mountpoint[MOUNTPOINT_SZ];
+ pthread_t thread;
+};
+
+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.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(&self->se, self->mountpoint, &fs_ops, &self->thread, err))
+ SKIP(return, err);
+}
+
+FIXTURE_TEARDOWN(readdir_cache)
+{
+ fs_teardown(self->se, self->thread, self->mountpoint);
+}
+
+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->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
^ permalink raw reply related [flat|nested] 9+ messages in thread