The Linux Kernel Mailing List
 help / color / mirror / Atom feed
* [RFC PATCH v1 0/5] fuse: caches documentation and testing
@ 2026-07-08 13:11 Luis Henriques
  2026-07-08 13:11 ` [RFC PATCH v1 1/5] Documentation: fuse: add document on caches being used by FUSE Luis Henriques
                   ` (4 more replies)
  0 siblings, 5 replies; 6+ messages in thread
From: Luis Henriques @ 2026-07-08 13:11 UTC (permalink / raw)
  To: Miklos Szeredi, Jonathan Corbet, Shuah Khan
  Cc: fuse-devel, linux-kernel, linux-kselftest, Matt Harvey,
	kernel-dev, Luis Henriques

Hi!

FUSE uses several different types of caches: directory, symlink, attributes,
ACLs, etc.  This RFC includes an attempt at documenting the usage of these
caches, in particular the rules for inserting/removing/revalidating data
into them.  The idea for this document came from Miklos during this year's
LSFMM -- he would like to see caches usage documented before he could merge
an initial version of fusex.

This is an early version of this document, which includes only two caches:
symlinks and ACLs.  Further caches will follow, assuming the proposed format
is acceptable.

In the meantime, while (slowly) working on this document, it became clear
that another useful thing to have would be some test cases that could
validate it.  And which could also be used as regression tests, of course.
Thus I'm also adding 2 very basic kselftests for these caches.  For now they
only do the obvious validation for whether caching is working or not, but
they can be extended to further validate different behaviours.

Finally, since the only existing FUSE kselftest is still using fuse2, I'm
also proposing to modify it so that it uses fuse3 instead.  There's the risk
this may be breaking some existing QA, but maybe that's an acceptable risk
(after all, fuse3 has around for ~10 years now!).

Anyway, this is an RFC for all the above.  Any feedback is welcome, as I
start looking into other cache types.

Luis Henriques (5):
  Documentation: fuse: add document on caches being used by FUSE
  selftests/filesystems: convert fusectl test to fuse3
  selftests/filesystems: check that fusectlfs is mounted
  selftests/filesystems: add fuse symlink caching test
  selftests/filesystems: add fuse ACLs caching test

 .../filesystems/fuse/fuse-caches.rst          |  86 +++++++
 .../selftests/filesystems/fuse/.gitignore     |   3 +
 .../selftests/filesystems/fuse/Makefile       |  23 +-
 .../selftests/filesystems/fuse/acl_fs.c       | 216 ++++++++++++++++++
 .../selftests/filesystems/fuse/acl_test.sh    |  66 ++++++
 .../selftests/filesystems/fuse/fuse_mnt.c     |  17 +-
 .../selftests/filesystems/fuse/fusectl_test.c |   7 +
 .../selftests/filesystems/fuse/setgetacl.c    |  62 +++++
 .../selftests/filesystems/fuse/symlink_fs.c   | 115 ++++++++++
 .../filesystems/fuse/symlink_test.sh          |  52 +++++
 10 files changed, 635 insertions(+), 12 deletions(-)
 create mode 100644 Documentation/filesystems/fuse/fuse-caches.rst
 create mode 100644 tools/testing/selftests/filesystems/fuse/acl_fs.c
 create mode 100755 tools/testing/selftests/filesystems/fuse/acl_test.sh
 create mode 100644 tools/testing/selftests/filesystems/fuse/setgetacl.c
 create mode 100644 tools/testing/selftests/filesystems/fuse/symlink_fs.c
 create mode 100755 tools/testing/selftests/filesystems/fuse/symlink_test.sh


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [RFC PATCH v1 1/5] Documentation: fuse: add document on caches being used by FUSE
  2026-07-08 13:11 [RFC PATCH v1 0/5] fuse: caches documentation and testing Luis Henriques
@ 2026-07-08 13:11 ` Luis Henriques
  2026-07-08 13:11 ` [RFC PATCH v1 2/5] selftests/filesystems: convert fusectl test to fuse3 Luis Henriques
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Luis Henriques @ 2026-07-08 13:11 UTC (permalink / raw)
  To: Miklos Szeredi, 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 the symlink and the ACLs caches are described.

Signed-off-by: Luis Henriques <luis@igalia.com>
---
 .../filesystems/fuse/fuse-caches.rst          | 86 +++++++++++++++++++
 1 file changed, 86 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..52b2558b5a68
--- /dev/null
+++ b/Documentation/filesystems/fuse/fuse-caches.rst
@@ -0,0 +1,86 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+===========
+FUSE Caches
+===========
+
+Introduction
+============
+
+This document summarises the different types of caches that are used in FUSE.
+For each cache type, it attempts to document the rules that are followed to
+insert, validate and invalidate data into the cache.
+
+symlink caching
+===============
+
+Whenever there's a link resolution request, the VFS will call into
+``fuse_get_link()`` which will then send a ``FUSE_READLINK`` request to the
+user-space FUSE server. However, the server can ask the kernel to cache all
+links resolutions by setting the ``FUSE_CACHE_SYMLINKS`` flag during the
+``FUSE_INIT`` negotiation.
+
+If this flag is set, FUSE will immediately call into the VFS
+``__page_get_link()`` from the ``->get_link()`` inode operation. The first time
+this is done for a specific link, it will end-up sending the ``FUSE_READLINK``
+to user-space but the link contents will then be added into page-cache. The next
+time the link needs to be resolved, it will use the link content that is already
+cached, and will only fallback into sending the request to use-space if the
+folio isn't up-to-date.
+
+ACL caching
+===========
+
+FUSE has allowed the usage of POSIX ACLs for a long time as they could be set
+and accessed simply as extended attributes. However, it was only with the
+addition of the ``FUSE_POSIX_ACL`` flag that ACLs started to be fully supported.
+Without this flag, 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 ``FUSE_POSIX_ACL`` is set during ``FUSE_INIT``, when an
+ACL is accessed the VFS layer will first check if it's already cached. If it is
+not, FUSE ``->get_acl`` operation is called, which will eventually send a
+user-space request. Future accesses to this inode ACL will then use the cached
+data.
+
+Setting an ACL in an inode, however, won't cache it immediately. It will send
+user-space a request with the new ACL, and the FUSE server may perform some
+modifications before storing it.
+
+On the other hand, ACLs will be removed for the cache in the following
+situations:
+
+-  When setting an ACL in an inode and the user-space server has set the
+   ``FUSE_POSIX_ACL`` flag, all 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, so any
+   cached ACLs for this inode are also invalidated.
+-  While processing ``FUSE_READDIRPLUS`` and a new dentry is added (unless this
+   dentry is already being looked up (``DCACHE_PAR_LOOKUP``))
+-  In general, when there is the need to sent a ``FUSE_STATX`` or
+   ``FUSE_GETATTR`` to user-space (e.g. because the attributes have expired).
+   This may happen in the following cases:
+
+   -  When doing an ``->llseek()`` on a file with ``SEEK_END``, ``SEEK_HOLE`` or
+      ``SEEK_DATA``.
+   -  When the ``FUSE_AUTO_INVAL_DATA`` flag is set at ``INIT`` time (to
+      automatically invalidate cached pages), and a buffered read
+      (``->read_iter()``) past EOF is done on a non-passthrough file.
+   -  When the ``FUSE_WRITEBACK_CACHE`` flag is set at ``INIT`` time, and a
+      buffered write (``->write_iter()``) past EOF is done on a non-passthrough
+      file.
+   -  When the ``FUSE_AUTO_INVAL_DATA`` flag is set at ``INIT`` time and the VFS
+      needs to read a directory contents (``->iterate_shared()``) for a
+      directory that is allowed to be cached.

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [RFC PATCH v1 2/5] selftests/filesystems: convert fusectl test to fuse3
  2026-07-08 13:11 [RFC PATCH v1 0/5] fuse: caches documentation and testing Luis Henriques
  2026-07-08 13:11 ` [RFC PATCH v1 1/5] Documentation: fuse: add document on caches being used by FUSE Luis Henriques
@ 2026-07-08 13:11 ` Luis Henriques
  2026-07-08 13:11 ` [RFC PATCH v1 3/5] selftests/filesystems: check that fusectlfs is mounted Luis Henriques
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Luis Henriques @ 2026-07-08 13:11 UTC (permalink / raw)
  To: Miklos Szeredi, 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>
---
 .../testing/selftests/filesystems/fuse/Makefile |  8 ++++----
 .../selftests/filesystems/fuse/fuse_mnt.c       | 17 ++++++++++-------
 2 files changed, 14 insertions(+), 11 deletions(-)

diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/testing/selftests/filesystems/fuse/Makefile
index 612aad69a93a..422cd1b1688d 100644
--- a/tools/testing/selftests/filesystems/fuse/Makefile
+++ b/tools/testing/selftests/filesystems/fuse/Makefile
@@ -7,14 +7,14 @@ TEST_GEN_FILES := fuse_mnt
 
 include ../../lib.mk
 
-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)
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] 6+ messages in thread

* [RFC PATCH v1 3/5] selftests/filesystems: check that fusectlfs is mounted
  2026-07-08 13:11 [RFC PATCH v1 0/5] fuse: caches documentation and testing Luis Henriques
  2026-07-08 13:11 ` [RFC PATCH v1 1/5] Documentation: fuse: add document on caches being used by FUSE Luis Henriques
  2026-07-08 13:11 ` [RFC PATCH v1 2/5] selftests/filesystems: convert fusectl test to fuse3 Luis Henriques
@ 2026-07-08 13:11 ` Luis Henriques
  2026-07-08 13:11 ` [RFC PATCH v1 4/5] selftests/filesystems: add fuse symlink caching test Luis Henriques
  2026-07-08 13:11 ` [RFC PATCH v1 5/5] selftests/filesystems: add fuse ACLs " Luis Henriques
  4 siblings, 0 replies; 6+ messages in thread
From: Luis Henriques @ 2026-07-08 13:11 UTC (permalink / raw)
  To: Miklos Szeredi, 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 | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/tools/testing/selftests/filesystems/fuse/fusectl_test.c b/tools/testing/selftests/filesystems/fuse/fusectl_test.c
index 0d1d012c35ed..2dcfd3ebe0d5 100644
--- a/tools/testing/selftests/filesystems/fuse/fusectl_test.c
+++ b/tools/testing/selftests/filesystems/fuse/fusectl_test.c
@@ -48,6 +48,7 @@ FIXTURE_SETUP(fusectl)
 	uid_t uid = getuid();
 	gid_t gid = getgid();
 	char buf[32];
+	char path_buf[PATH_MAX];
 
 	/* Setup userns */
 	ASSERT_EQ(unshare(CLONE_NEWNS|CLONE_NEWUSER), 0);
@@ -93,6 +94,12 @@ FIXTURE_SETUP(fusectl)
 		     strerror(errno));
 
 	self->connection = statbuf.st_dev;
+
+	sprintf(path_buf, "/sys/fs/fuse/connections/%d", self->connection);
+	if (access(path_buf, F_OK) != 0)
+		SKIP(return,
+		     "fusectl doesn't seem to be mounted: %s\n",
+		     strerror(errno));
 }
 
 FIXTURE_TEARDOWN(fusectl)

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [RFC PATCH v1 4/5] selftests/filesystems: add fuse symlink caching test
  2026-07-08 13:11 [RFC PATCH v1 0/5] fuse: caches documentation and testing Luis Henriques
                   ` (2 preceding siblings ...)
  2026-07-08 13:11 ` [RFC PATCH v1 3/5] selftests/filesystems: check that fusectlfs is mounted Luis Henriques
@ 2026-07-08 13:11 ` Luis Henriques
  2026-07-08 13:11 ` [RFC PATCH v1 5/5] selftests/filesystems: add fuse ACLs " Luis Henriques
  4 siblings, 0 replies; 6+ messages in thread
From: Luis Henriques @ 2026-07-08 13:11 UTC (permalink / raw)
  To: Miklos Szeredi, 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       |   6 +-
 .../selftests/filesystems/fuse/symlink_fs.c   | 115 ++++++++++++++++++
 .../filesystems/fuse/symlink_test.sh          |  52 ++++++++
 4 files changed, 173 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/filesystems/fuse/symlink_fs.c
 create mode 100755 tools/testing/selftests/filesystems/fuse/symlink_test.sh

diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore b/tools/testing/selftests/filesystems/fuse/.gitignore
index 3e72e742d08e..cfdc5ca3ded4 100644
--- a/tools/testing/selftests/filesystems/fuse/.gitignore
+++ b/tools/testing/selftests/filesystems/fuse/.gitignore
@@ -1,3 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 fuse_mnt
 fusectl_test
+symlink_fs
diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/testing/selftests/filesystems/fuse/Makefile
index 422cd1b1688d..342da0878006 100644
--- a/tools/testing/selftests/filesystems/fuse/Makefile
+++ b/tools/testing/selftests/filesystems/fuse/Makefile
@@ -3,7 +3,8 @@
 CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
 
 TEST_GEN_PROGS := fusectl_test
-TEST_GEN_FILES := fuse_mnt
+TEST_PROGS := symlink_test.sh
+TEST_GEN_FILES := fuse_mnt symlink_fs
 
 include ../../lib.mk
 
@@ -19,3 +20,6 @@ endif
 
 $(OUTPUT)/fuse_mnt: CFLAGS += $(VAR_CFLAGS)
 $(OUTPUT)/fuse_mnt: LDLIBS += $(VAR_LDLIBS)
+
+$(OUTPUT)/symlink_fs: CFLAGS += $(VAR_CFLAGS)
+$(OUTPUT)/symlink_fs: LDLIBS += $(VAR_LDLIBS)
diff --git a/tools/testing/selftests/filesystems/fuse/symlink_fs.c b/tools/testing/selftests/filesystems/fuse/symlink_fs.c
new file mode 100644
index 000000000000..1aee26c91b3f
--- /dev/null
+++ b/tools/testing/selftests/filesystems/fuse/symlink_fs.c
@@ -0,0 +1,115 @@
+// 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'.  Whenever the ->readlink() is
+ * executed to resolve 'link' a counter will be incremented.  A ->read() to any
+ * filesystem object will return the value in this counter.
+ *
+ * A '--cache' argument will allow to enable symlink caching (disabled by
+ * default).  This means that, if caching is enabled, resolving a symlink will
+ * only call into user-space the first time.
+ */
+
+#define FUSE_USE_VERSION 31
+
+#include <fuse.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stddef.h>
+#include <assert.h>
+
+#define FILE	"file"
+#define LINK	"link"
+
+static struct options {
+	int cache_symlinks;
+} options;
+
+static const struct fuse_opt option_spec[] = {
+	{ "--cache", offsetof(struct options, cache_symlinks), 1 },
+	FUSE_OPT_END
+};
+
+static int readlink_counter = 0;
+
+static void *symlink_init(struct fuse_conn_info *conn, struct fuse_config *cfg)
+{
+	if (options.cache_symlinks)
+		fuse_set_feature_flag(conn, FUSE_CAP_CACHE_SYMLINKS);
+
+	return NULL;
+}
+
+static int symlink_getattr(const char *path, struct stat *stbuf,
+			   struct fuse_file_info *fi)
+{
+	int res = 0;
+
+	memset(stbuf, 0, sizeof(struct stat));
+	if (strcmp(path, "/") == 0) {
+		stbuf->st_mode = S_IFDIR | 0755;
+		stbuf->st_nlink = 2;
+	} else if (strcmp(path + 1, FILE) == 0) {
+		char data[64];
+
+		stbuf->st_mode = S_IFREG | 0444;
+		stbuf->st_nlink = 1;
+		stbuf->st_size = sprintf(data, "%d\n", readlink_counter);
+	} else if (strcmp(path + 1, LINK) == 0) {
+		stbuf->st_mode = S_IFLNK | 0444;
+		stbuf->st_nlink = 1;
+		stbuf->st_size = strlen(FILE);
+	} else
+		res = -ENOENT;
+
+	return res;
+}
+
+static int symlink_readlink(const char *path, char *buf, size_t size)
+{
+	if (strcmp(path + 1, LINK) != 0)
+		return -ENOENT;
+
+	memcpy(buf, FILE, strlen(FILE));
+	readlink_counter++;
+
+	return 0;
+}
+
+static int symlink_read(const char *path, char *buf, size_t sz, off_t off,
+			struct fuse_file_info *fi)
+{
+	char data[64];
+	int len;
+
+	len = sprintf(data, "%d\n", readlink_counter);
+	memcpy(buf, data, len);
+
+	return len;
+}
+
+static const struct fuse_operations symlink_oper = {
+	.init           = symlink_init,
+	.getattr	= symlink_getattr,
+	.readlink	= symlink_readlink,
+	.read		= symlink_read,
+};
+
+int main(int argc, char *argv[])
+{
+	int ret;
+	struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
+
+	options.cache_symlinks = 0;
+	if (fuse_opt_parse(&args, &options, option_spec, NULL) == -1)
+		return 1;
+
+	ret = fuse_main(args.argc, args.argv, &symlink_oper, NULL);
+	fuse_opt_free_args(&args);
+
+	return ret;
+}
diff --git a/tools/testing/selftests/filesystems/fuse/symlink_test.sh b/tools/testing/selftests/filesystems/fuse/symlink_test.sh
new file mode 100755
index 000000000000..546541c1920e
--- /dev/null
+++ b/tools/testing/selftests/filesystems/fuse/symlink_test.sh
@@ -0,0 +1,52 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+
+exit_cleanup()
+{
+	fusermount -u ./mnt
+	rmdir ./mnt
+}
+
+assert_value ()
+{
+	if [ $1 -ne $2 ]; then
+		echo "FAILED"
+		echo $3
+		exit 1
+	fi
+}
+
+set -e
+
+trap exit_cleanup EXIT
+
+mkdir -p mnt
+
+./symlink_fs ./mnt
+
+echo -n "Testing symlink without cached: "
+
+# When symlink caching is disabled every access to a symlink is expected to
+# result in a call to user-space
+for i in $(seq 1 10); do
+	readlink ./mnt/link > /dev/null
+done
+
+res=$(cat ./mnt/file)
+assert_value $res $i "Got $res, expected $i"
+echo "PASSED"
+
+fusermount -u ./mnt
+
+./symlink_fs --cache ./mnt
+
+echo -n "Testing symlink with cache: "
+
+# With caching enabled, there will only be a single call into user-space
+for i in $(seq 0 10); do
+	readlink ./mnt/link > /dev/null
+done
+
+res=$(cat ./mnt/file)
+assert_value 1 $res "Got $ res, expected 1"
+echo "PASSED"

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [RFC PATCH v1 5/5] selftests/filesystems: add fuse ACLs caching test
  2026-07-08 13:11 [RFC PATCH v1 0/5] fuse: caches documentation and testing Luis Henriques
                   ` (3 preceding siblings ...)
  2026-07-08 13:11 ` [RFC PATCH v1 4/5] selftests/filesystems: add fuse symlink caching test Luis Henriques
@ 2026-07-08 13:11 ` Luis Henriques
  4 siblings, 0 replies; 6+ messages in thread
From: Luis Henriques @ 2026-07-08 13:11 UTC (permalink / raw)
  To: Miklos Szeredi, 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 ACLs caching is
working as expected.  I.e. it verifies that, if FUSE_POSIX_ACL flag is set,
user-space is only called once when accessing an inode ACL and that
subsequent accesses are cached in the kernel.

Signed-off-by: Luis Henriques <luis@igalia.com>
---
 .../selftests/filesystems/fuse/.gitignore     |   2 +
 .../selftests/filesystems/fuse/Makefile       |  13 +-
 .../selftests/filesystems/fuse/acl_fs.c       | 216 ++++++++++++++++++
 .../selftests/filesystems/fuse/acl_test.sh    |  66 ++++++
 .../selftests/filesystems/fuse/setgetacl.c    |  62 +++++
 5 files changed, 357 insertions(+), 2 deletions(-)
 create mode 100644 tools/testing/selftests/filesystems/fuse/acl_fs.c
 create mode 100755 tools/testing/selftests/filesystems/fuse/acl_test.sh
 create mode 100644 tools/testing/selftests/filesystems/fuse/setgetacl.c

diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore b/tools/testing/selftests/filesystems/fuse/.gitignore
index cfdc5ca3ded4..1733ebb9d7fd 100644
--- a/tools/testing/selftests/filesystems/fuse/.gitignore
+++ b/tools/testing/selftests/filesystems/fuse/.gitignore
@@ -2,3 +2,5 @@
 fuse_mnt
 fusectl_test
 symlink_fs
+acl_fs
+setgetacl
diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/testing/selftests/filesystems/fuse/Makefile
index 342da0878006..3bbf9d837e9a 100644
--- a/tools/testing/selftests/filesystems/fuse/Makefile
+++ b/tools/testing/selftests/filesystems/fuse/Makefile
@@ -3,8 +3,8 @@
 CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
 
 TEST_GEN_PROGS := fusectl_test
-TEST_PROGS := symlink_test.sh
-TEST_GEN_FILES := fuse_mnt symlink_fs
+TEST_PROGS := symlink_test.sh acl_test.sh
+TEST_GEN_FILES := fuse_mnt symlink_fs acl_fs setgetacl
 
 include ../../lib.mk
 
@@ -18,8 +18,17 @@ ifeq ($(VAR_LDLIBS),)
 VAR_LDLIBS := -lfuse3 -pthread
 endif
 
+ACL_CFLAGS := $(shell pkg-config libacl --cflags 2>/dev/null)
+ACL_LDLIBS := $(shell pkg-config libacl --libs 2>/dev/null)
+
 $(OUTPUT)/fuse_mnt: CFLAGS += $(VAR_CFLAGS)
 $(OUTPUT)/fuse_mnt: LDLIBS += $(VAR_LDLIBS)
 
 $(OUTPUT)/symlink_fs: CFLAGS += $(VAR_CFLAGS)
 $(OUTPUT)/symlink_fs: LDLIBS += $(VAR_LDLIBS)
+
+$(OUTPUT)/acl_fs: CFLAGS += $(VAR_CFLAGS)
+$(OUTPUT)/acl_fs: LDLIBS += $(VAR_LDLIBS)
+
+$(OUTPUT)/setgetacl: CFLAGS += $(ACL_CFLAGS)
+$(OUTPUT)/setgetacl: LDLIBS += $(ACL_LDLIBS)
diff --git a/tools/testing/selftests/filesystems/fuse/acl_fs.c b/tools/testing/selftests/filesystems/fuse/acl_fs.c
new file mode 100644
index 000000000000..4b5e0073ab4f
--- /dev/null
+++ b/tools/testing/selftests/filesystems/fuse/acl_fs.c
@@ -0,0 +1,216 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/*
+ * Simple filesystem to test FUSE ACLs cache
+ *
+ * This is a simple FUSE filesystem that contains a single object (a file named
+ * 'file') and which allows to set the 'system.posix_acl_access' ACL on that
+ * object.  Whenever this ACL is read from the FUSE filesystem, a counter is
+ * incremented.  And value for this counter can be obtained from reading from
+ * this file.
+ *
+ * When ACLs are being cached (the '--cache' argument was used to mount this
+ * filesystem), this counter will only be incremented the first time the ACL is
+ * read, as the kernel won't be calling into user-space again until that cache
+ * is invalidated.
+ */
+#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 12)
+
+#include <fuse_lowlevel.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <time.h>
+#include <sys/stat.h>
+#include <sys/param.h>
+#include <sys/xattr.h>
+
+#define FILE	"file"
+#define TIMEOUT	86400.0f
+
+static struct options {
+	int cache_acls;
+} options;
+
+static const struct fuse_opt option_spec[] = {
+	{ "--cache", offsetof(struct options, cache_acls), 1 },
+	FUSE_OPT_END
+};
+
+static int getxattr_counter = 0;
+
+static void acl_init(void *userdata, struct fuse_conn_info *conn)
+{
+	if (options.cache_acls)
+		fuse_set_feature_flag(conn, FUSE_CAP_POSIX_ACL);
+	else
+		fuse_unset_feature_flag(conn, FUSE_CAP_POSIX_ACL);
+}
+
+static int acl_stat(fuse_ino_t ino, struct stat *stbuf)
+{
+	char data[64];
+
+	stbuf->st_ino = ino;
+	switch (ino) {
+	case 1:
+		stbuf->st_mode = S_IFDIR | 0755;
+		stbuf->st_nlink = 2;
+		break;
+	case 42:
+		stbuf->st_mode = S_IFREG | 0444;
+		stbuf->st_nlink = 1;
+		stbuf->st_size = sprintf(data, "%d\n", getxattr_counter);
+		break;
+	default:
+		return -1;
+	}
+
+	stbuf->st_uid = getuid();
+	stbuf->st_gid = getgid();
+	stbuf->st_atime = stbuf->st_mtime = stbuf->st_ctime = time(NULL);
+
+	return 0;
+}
+
+static void acl_lookup(fuse_req_t req, fuse_ino_t parent, const char *name)
+{
+	struct fuse_entry_param e;
+
+	memset(&e, 0, sizeof(e));
+
+	if (parent != 1 || strcmp(name, FILE) != 0)
+		fuse_reply_err(req, ENOENT);
+	else {
+		e.ino = 42;
+		e.attr_timeout = TIMEOUT;
+		e.entry_timeout = TIMEOUT;
+		acl_stat(e.ino, &e.attr);
+		fuse_reply_entry(req, &e);
+	}
+}
+
+static void acl_getattr(fuse_req_t req, fuse_ino_t ino,
+			struct fuse_file_info *fi)
+{
+	struct stat attr;
+
+	memset(&attr, 0, sizeof(struct stat));
+	if (acl_stat(ino, &attr) == -1)
+		fuse_reply_err(req, ENOENT);
+	else
+		fuse_reply_attr(req, &attr, TIMEOUT);
+}
+
+static void acl_read(fuse_req_t req, fuse_ino_t ino, size_t size,
+		     off_t off, struct fuse_file_info *fi)
+{
+	char data[64];
+	int len;
+
+	len = sprintf(data, "%d\n", getxattr_counter);
+	if (off < len)
+		fuse_reply_buf(req, data + off, MIN(len - off, size));
+	else
+		fuse_reply_buf(req, NULL, 0);
+}
+
+char *xattr_value = NULL;
+size_t xattr_value_sz = 0;
+
+static void acl_setxattr(fuse_req_t req, fuse_ino_t ino, const char *name,
+			 const char *value, size_t size, int flags)
+{
+	int ret = 0;
+
+	if (ino != 42)
+		ret = ENOENT;
+	else if (!strcmp(name, "system.posix_acl_access")) {
+		if (xattr_value)
+			free(xattr_value);
+		xattr_value = malloc(size);
+		memcpy(xattr_value, value, size);
+		xattr_value_sz = size;
+	} else
+		ret = ENOTSUP;
+
+	fuse_reply_err(req, ret);
+}
+
+static void acl_getxattr(fuse_req_t req, fuse_ino_t ino, const char *name,
+			 size_t size)
+{
+	if (ino != 42)
+		fuse_reply_err(req, ENOENT);
+	else if (!xattr_value || strcmp(name, "system.posix_acl_access"))
+		fuse_reply_err(req, ENODATA);
+	else if (size) {
+		fuse_reply_buf(req, xattr_value, xattr_value_sz);
+		getxattr_counter++;
+	} else
+		fuse_reply_xattr(req, xattr_value_sz);
+}
+
+static const struct fuse_lowlevel_ops acl_op = {
+	.init           = acl_init,
+	.lookup		= acl_lookup,
+	.getattr	= acl_getattr,
+	.getxattr	= acl_getxattr,
+	.setxattr	= acl_setxattr,
+	.read		= acl_read,
+};
+
+int main(int argc, char *argv[])
+{
+	struct fuse_session *se;
+	struct fuse_loop_config *config;
+	struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
+	struct fuse_cmdline_opts opts;
+	int ret = -1;
+
+	options.cache_acls = 0;
+
+	if (fuse_parse_cmdline(&args, &opts) != 0)
+		return ret;
+
+	if (opts.mountpoint == NULL) {
+		printf("usage: %s [options] <mountpoint>\n", argv[0]);
+		goto out_args;
+	}
+
+	if (fuse_opt_parse(&args, &options, option_spec, NULL) == -1)
+		goto out_args;
+
+	se = fuse_session_new(&args, &acl_op, sizeof(acl_op), NULL);
+	if (!se)
+		goto out_args;
+	if (fuse_set_signal_handlers(se))
+		goto out_session;
+	if (fuse_session_mount(se, opts.mountpoint))
+		goto out_signal;
+
+	fuse_daemonize(opts.foreground);
+	if (opts.singlethread) {
+		ret = fuse_session_loop(se);
+	} else {
+		config = fuse_loop_cfg_create();
+		fuse_loop_cfg_set_clone_fd(config, opts.clone_fd);
+		fuse_loop_cfg_set_max_threads(config, opts.max_threads);
+		ret = fuse_session_loop_mt(se, config);
+		fuse_loop_cfg_destroy(config);
+		config = NULL;
+	}
+	fuse_session_unmount(se);
+
+out_signal:
+	fuse_remove_signal_handlers(se);
+out_session:
+	fuse_session_destroy(se);
+out_args:
+	free(opts.mountpoint);
+	fuse_opt_free_args(&args);
+
+	return ret;
+}
diff --git a/tools/testing/selftests/filesystems/fuse/acl_test.sh b/tools/testing/selftests/filesystems/fuse/acl_test.sh
new file mode 100755
index 000000000000..9070c51c6784
--- /dev/null
+++ b/tools/testing/selftests/filesystems/fuse/acl_test.sh
@@ -0,0 +1,66 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+
+exit_cleanup()
+{
+	fusermount -u ./mnt
+	rmdir ./mnt
+}
+
+assert_value ()
+{
+	if [ $1 -ne $2 ]; then
+		echo "FAILED"
+		echo $3
+		exit 1
+	fi
+}
+
+set -e
+
+trap exit_cleanup EXIT
+
+mkdir -p mnt
+
+#sudo su -
+#cd /home/miguel/kernel/linux/tools/testing/selftests/filesystems/fuse
+#dmesg -c > /dev/null
+#tmux -S ./bla -f a
+#./acl_fs -d --cache ./mnt
+#setfacl -m u:daemon:r mnt/file
+#getfacl mnt/file
+
+./acl_fs ./mnt
+
+echo -n "Testing ACLs without cache: "
+
+# set ACL
+./setgetacl ACCESS u::rw-,g::rw-,o::rw-,u:nobody:r--,m::rw- mnt/file
+
+# When symlink caching is disabled every access to a symlink is expected to
+# result in a call to user-space
+for i in $(seq 1 10); do
+	./setgetacl ACCESS mnt/file > /dev/null
+done
+
+res=$(cat ./mnt/file)
+assert_value $res $i "Got $res, expected $i"
+echo "PASSED"
+
+fusermount -u ./mnt
+
+./acl_fs --cache ./mnt
+
+echo -n "Testing ACLs with cache: "
+
+# set ACL
+./setgetacl ACCESS u::rw-,g::rw-,o::rw-,u:nobody:r--,m::rw- mnt/file
+
+# With caching enabled, there will be a single call into user-space
+for i in $(seq 1 10); do
+	./setgetacl ACCESS mnt/file > /dev/null
+done
+res=$(cat ./mnt/file)
+assert_value $res 1 "Got $res, expected 1"
+
+echo "PASSED"
diff --git a/tools/testing/selftests/filesystems/fuse/setgetacl.c b/tools/testing/selftests/filesystems/fuse/setgetacl.c
new file mode 100644
index 000000000000..035e7daa1c5d
--- /dev/null
+++ b/tools/testing/selftests/filesystems/fuse/setgetacl.c
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/* Simple ACL set/get wrapper */
+
+#include <stdio.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/acl.h>
+
+int main(int argc, char *argv[])
+{
+	acl_t acl;
+	acl_type_t type;
+	char *buf;
+
+	if ((argc != 3) && (argc != 4)) {
+		fprintf(stderr, "Usage: %s <acl type> [<value>] <file>\n",
+			argv[0]);
+		fprintf(stderr, "where <acl type> is ACCESS or DEFAULT\n");
+		return 1;
+	}
+
+	if (!strcmp(argv[1], "ACCESS"))
+		type = ACL_TYPE_ACCESS;
+	else if (!strcmp(argv[1], "DEFAULT"))
+		type = ACL_TYPE_DEFAULT;
+	else {
+		fprintf(stderr, "Invalid ACL type\n");
+		return 1;
+	}
+
+	if (argc == 3) {
+		/* Get ACL */
+		acl = acl_get_file(argv[2], type);
+		if (acl == NULL) {
+			perror("acl_get_file");
+			return 1;
+		}
+		buf = acl_to_text(acl, NULL);
+		if (buf == NULL)
+			perror("acl_from_text");
+		else {
+			fprintf(stdout, "%s\n", buf);
+			acl_free(buf);
+		}
+	} else {
+		/* Set ACL */
+		acl = acl_from_text(argv[2]);
+		if (acl == NULL) {
+			perror("acl_from_text");
+			return 1;
+		}
+		if (acl_set_file(argv[3], type, acl) != 0)
+			perror("acl_set_file");
+	}
+
+	acl_free(acl);
+
+	return 0;
+}

^ permalink raw reply related	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-07-08 13:11 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-08 13:11 [RFC PATCH v1 0/5] fuse: caches documentation and testing Luis Henriques
2026-07-08 13:11 ` [RFC PATCH v1 1/5] Documentation: fuse: add document on caches being used by FUSE Luis Henriques
2026-07-08 13:11 ` [RFC PATCH v1 2/5] selftests/filesystems: convert fusectl test to fuse3 Luis Henriques
2026-07-08 13:11 ` [RFC PATCH v1 3/5] selftests/filesystems: check that fusectlfs is mounted Luis Henriques
2026-07-08 13:11 ` [RFC PATCH v1 4/5] selftests/filesystems: add fuse symlink caching test Luis Henriques
2026-07-08 13:11 ` [RFC PATCH v1 5/5] selftests/filesystems: add fuse ACLs " Luis Henriques

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox