From 960994f5d1883b6a293f66a886cf2e42be7b8669 Mon Sep 17 00:00:00 2001 From: Sasha Levin Date: Tue, 20 Oct 2026 12:13:46 -0400 Subject: [PATCH 1/3] xprtrdma: fix up the nfs/nfs-anna merge of the Write chunk head trim Two fixes for the same Write chunk payload corruption meet in linux-next. In the nfs tree, commit a0db57fee35e ("xprtrdma: Fix Write chunk payload corruption on a short reply header") trims head[0].iov_len in rpcrdma_decode_msg(). In the nfs-anna tree, commit 9ee534a75912 ("xprtrdma: trim head iovec when the payload arrives via a Write chunk") trims it in rpcrdma_inline_fixup() and reduces buflen by the same amount. The merge resolution keeps the caller-side trim. By the time rpcrdma_inline_fixup() runs, head[0].iov_len already equals copy_len, so the block added by the nfs-anna commit never runs and its buflen reduction in rq_rcv_buf and rq_private_buf is lost. The helper is also still passed xdr_pad_size(writelist), while its kernel-doc now describes @writelist as the number of bytes conveyed by the Write chunk. Drop the caller-side trim and pass the full Write chunk length, so that rpcrdma_inline_fixup() is the only place that trims head[0] and reduces buflen. Compute the pad with xdr_pad_size() in the helper, so the padding from the nfs commit stays correct for lengths that are not a multiple of four. Its rpclen calculation and empty reply check stay in rpcrdma_decode_msg(). Fixes: 53ab189bdc14 ("Merge branch 'linux-next' of git://git.linux-nfs.org/projects/anna/linux-nfs.git") Link: https://lore.kernel.org/all/as4aacUf2Q78FMvY@sirena.org.uk/ Signed-off-by: Sasha Levin Tested-by: Tim Menninger Reviewed-by: Tim Menninger Signed-off-by: Linus Torvalds --- net/sunrpc/xprtrdma/rpc_rdma.c | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/net/sunrpc/xprtrdma/rpc_rdma.c b/net/sunrpc/xprtrdma/rpc_rdma.c index c0d5042f3b66..69d4e35d49ef 100644 --- a/net/sunrpc/xprtrdma/rpc_rdma.c +++ b/net/sunrpc/xprtrdma/rpc_rdma.c @@ -1010,7 +1010,7 @@ rpcrdma_inline_fixup(struct rpc_rqst *rqst, char *srcp, int copy_len, char *destp; struct page **ppages; int page_base; - int pad = writelist & 3; + int pad = xdr_pad_size(writelist); unsigned int delta; /* The head iovec is redirected to the RPC reply message @@ -1267,14 +1267,9 @@ rpcrdma_decode_msg(struct rpcrdma_xprt *r_xprt, struct rpcrdma_rep *rep, rpclen &= ~3; if (unlikely(!rpclen)) return -EIO; - if (writelist && rpclen < rqst->rq_rcv_buf.head[0].iov_len) { - rqst->rq_rcv_buf.head[0].iov_len = rpclen; - rqst->rq_private_buf.head[0].iov_len = rpclen; - } r_xprt->rx_stats.fixup_copy_count += - rpcrdma_inline_fixup(rqst, base, rpclen, - xdr_pad_size(writelist)); + rpcrdma_inline_fixup(rqst, base, rpclen, writelist); r_xprt->rx_stats.total_rdma_reply += writelist; return rpclen + xdr_align_size(writelist); From 1ea806165ec5fb8bca5d9990392fdb4f53a97977 Mon Sep 17 00:00:00 2001 From: Sasha Levin Date: Mon, 26 Oct 2026 11:26:50 -0400 Subject: [PATCH 2/3] fuse: fix link error with CONFIG_FUSE_PASSTHROUGH=n Commit 2f19d54c055d ("fuse: support 64 bit, server allocated backing ID") moved the CONFIG_FUSE_PASSTHROUGH check out of the backing ioctl handlers and into fuse_get_conn_for_passthrough(). The calls from the handlers into backing.c, which is only built with CONFIG_FUSE_PASSTHROUGH, are now only removed if the compiler inlines that helper. It does not have to, and with gcc 11 and CONFIG_CC_OPTIMIZE_FOR_SIZE it doesn't: dev.c:(.text+0x355c): undefined reference to `fuse_backing_open' nios2-linux-ld: dev.c:(.text+0x3594): undefined reference to `fuse_backing_open_64' nios2-linux-ld: dev.c:(.text+0x35d0): undefined reference to `fuse_backing_close' Check CONFIG_FUSE_PASSTHROUGH in each handler again, as fuse_notify_backing_remove() does. The check still comes after the fuse_get_dev() and -ENOTCONN checks, so the errors userspace sees do not change. Fixes: 2f19d54c055d ("fuse: support 64 bit, server allocated backing ID") Reported-by: kernel test robot Closes: https://lore.kernel.org/oe-kbuild-all/202610171310.uqRzJkIx-lkp@intel.com/ Closes: https://lore.kernel.org/oe-kbuild-all/202610260719.sj08mXBr-lkp@intel.com/ Signed-off-by: Sasha Levin Signed-off-by: Linus Torvalds --- fs/fuse/dev.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index 1a40b60afb98..3641abb90d6b 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -2334,9 +2334,6 @@ static struct fuse_conn *fuse_get_conn_for_passthrough(struct file *file) if (!smp_load_acquire(&fud->chan->initialized)) return ERR_PTR(-ENOTCONN); - if (!IS_ENABLED(CONFIG_FUSE_PASSTHROUGH)) - return ERR_PTR(-EOPNOTSUPP); - return fud->chan->conn; } @@ -2349,6 +2346,9 @@ static long fuse_dev_ioctl_backing_open(struct file *file, if (IS_ERR(fc)) return PTR_ERR(fc); + if (!IS_ENABLED(CONFIG_FUSE_PASSTHROUGH)) + return -EOPNOTSUPP; + if (copy_from_user(&map, argp, sizeof(map))) return -EFAULT; @@ -2364,6 +2364,9 @@ static long fuse_dev_ioctl_backing_create(struct file *file, if (IS_ERR(fc)) return PTR_ERR(fc); + if (!IS_ENABLED(CONFIG_FUSE_PASSTHROUGH)) + return -EOPNOTSUPP; + if (copy_from_user(&map, argp, sizeof(map))) return -EFAULT; @@ -2378,6 +2381,9 @@ static long fuse_dev_ioctl_backing_close(struct file *file, __u32 __user *argp) if (IS_ERR(fc)) return PTR_ERR(fc); + if (!IS_ENABLED(CONFIG_FUSE_PASSTHROUGH)) + return -EOPNOTSUPP; + if (get_user(backing_id, argp)) return -EFAULT; From aba9df180668b4c0b1e021980f3bff9ad502d26c Mon Sep 17 00:00:00 2001 From: Sasha Levin Date: Mon, 26 Oct 2026 15:40:08 -0400 Subject: [PATCH 3/3] selftests/fuse: add the missing symlink caching test Commit 3e14f079d7a0 ("selftests/fuse: add fuse symlink caching test") registered fuse_symlink_cache_test in the fuse selftest Makefile and .gitignore, but the test source was not part of the applied commit, so building the fuse selftests fails: make[1]: *** No rule to make target 'fuse_symlink_cache_test.c', needed by '.../fuse_symlink_cache_test'. Add fuse_symlink_cache_test.c as posted in Luis Henriques's v5 submission. The test serves a file and a symlink to it from a libfuse low-level filesystem, resolves the symlink 100 times, and counts the readlink requests that reach the server: one when FUSE_CAP_CACHE_SYMLINKS is set during FUSE_INIT, and all 100 when it is not. Fixes: 3e14f079d7a0 ("selftests/fuse: add fuse symlink caching test") Reported-by: kernel test robot Closes: https://lore.kernel.org/r/202610170822.oEJ1Autq-lkp@intel.com/ Link: https://lore.kernel.org/r/20261001164916.15311-8-luis@igalia.com Originally-by: Luis Henriques Signed-off-by: Sasha Levin Reviewed-by: Luis Henriques Signed-off-by: Linus Torvalds --- .../fuse/fuse_symlink_cache_test.c | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 tools/testing/selftests/filesystems/fuse/fuse_symlink_cache_test.c 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..7b5373499573 --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/fuse_symlink_cache_test.c @@ -0,0 +1,162 @@ +// 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 +#include + +#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; + 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) +{ + struct fuse_common_ctx ctx; +}; +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(&fs_ops, &self->ctx, err)) + SKIP(return, err); +} + +FIXTURE_TEARDOWN(symlink_cache) +{ + fs_teardown(&self->ctx); +} + +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->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