Re: [RFC PATCH v4 7/8] selftests/fuse: add fuse symlink caching test

From: Amir Goldstein

Date: Thu Sep 17 2026 - 14:13:08 EST


On Wed, Sep 16, 2026 at 5:55 PM Luis Henriques <luis@xxxxxxxxxx> wrote:
>
> 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@xxxxxxxxxx>
> ---
> .../selftests/filesystems/fuse/.gitignore | 1 +
> .../selftests/filesystems/fuse/Makefile | 2 +
> .../fuse/fuse_symlink_cache_test.c | 163 ++++++++++++++++++
> 3 files changed, 166 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..dd001736ebca
> --- /dev/null
> +++ b/tools/testing/selftests/filesystems/fuse/fuse_symlink_cache_test.c
> @@ -0,0 +1,163 @@
> +// 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 <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;
> + struct fuse_common_ctx ctx;
> + int readlink_counter;
> + bool cache;
> +} 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) != 0 && strcmp(name, LINKNAME) != 0)) {
> + fuse_reply_err(req, ENOENT);
> + return;
> + }
> +
> + 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);
> + }
> + 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) {};
> +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;
> +
> + if (fs_setup(&fs_ops, &test_state.ctx, err))
> + SKIP(goto out, err);
> +out:
> + pthread_mutex_unlock(&test_state.lock);
> +}
> +
> +FIXTURE_TEARDOWN(symlink_cache)
> +{
> + pthread_mutex_lock(&test_state.lock);
> + fs_teardown(&test_state.ctx);
> + pthread_mutex_unlock(&test_state.lock);
> +}
> +

Here too, unless you have a good reason not to,
please put fuse_common_ctx in the test fixture object.

With that fix, feel free to add

Reviewed-by: Amir Goldstein <amir73il@xxxxxxxxx>

Thanks,
Amir.

> +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", test_state.ctx.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