On Wed, Jul 8, 2026 at 3:11 PM Luis Henriques <[email protected]> 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 <[email protected]> > --- > .../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" >
As we discussed, consider doing this as a single c test program, but not a must as you wish. Thanks, Amir.

