aboutsummaryrefslogtreecommitdiff
path: root/tools/testing
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-07-26 12:22:57 -0700
committerLinus Torvalds <torvalds@linux-foundation.org>2026-07-26 12:22:57 -0700
commit4235cb24ec1e8e96843f3671ba4da2a6ccca2c7b (patch)
tree9572f57056ea3d1870ae1611a845c0f302d245be /tools/testing
parent72841e8e8345ebf1005095149cc08eb6e2889a54 (diff)
parent749d7aa0377aae32af8c0a4ad43371e7bf830ab5 (diff)
Merge tag 'vfs-7.2-rc5.fixes' of git://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs
Pull vfs fixes from Christian Brauner: - vfs: Preserve the ACL_DONT_CACHE state in forget_cached_acl(). ACL_DONT_CACHE is meant to be a permanent opt-out from ACL caching which FUSE relies on for servers that don't negotiate FUSE_POSIX_ACL. The helper replaced it with ACL_NOT_CACHED, silently re-enabling the cache, and as fuse doesn't invalidate the cache for such servers a properly timed get_acl() returned stale ACLs. Comes with a fuse selftest reproducing this. - pidfs: - Preserve PIDFD_THREAD when a thread pidfd is reopened via open_by_handle_at(). PIDFD_THREAD shares the O_EXCL bit which do_dentry_open() strips after the flags have been validated, so the reopened pidfd silently became a process pidfd. Comes with a selftest. - Add a pidfs_dentry_open() helper so the regular pidfd allocation path and the file handle path share the code that forces O_RDWR and reapplies the pidfd flags that do_dentry_open() strips. - Handle FS_IOC32_GETVERSION in the compat ioctl path. - Make pidfs_ino_lock static. - iomap: - Fix the block range calculation in ifs_clear_range_dirty() so a partial clear doesn't drop the dirty state of blocks the range only partially covers. - Support invalidating partial folios so a partial truncate or hole punch with blocksize < foliosize doesn't leave stale dirty bits behind. - Only set did_zero when iomap_zero_iter() actually zeroed something. - Guard ifs_set_range_dirty() and ifs_set_range_uptodate() against zero-length ranges where the unsigned last-block calculation underflows and bitmap_set() writes far beyond the ifs->state allocation. - Don't merge ioends with different io_private values as the merge could leak or corrupt the private data of the individual ioends. - exec: - Raise bprm->have_execfd only once the binfmt_misc interpreter has actually been opened. The flag was set as soon as a matching 'O' or 'C' entry was found. If the interpreter open failed with ENOEXEC the exec fell through to the next binary format with have_execfd raised but no executable staged and begin_new_exec() NULL derefed past the point of no return. - Fix an unsigned loop counter wrap in transfer_args_to_stack() on nommu. An overlong argument or environment string pushes bprm->p below PAGE_SIZE, the stop index becomes zero, and the loop never terminates, wrapping its counter and copying garbage from in front of the page array into the new process stack. - Make binfmt_elf_fdpic only honour the first PT_INTERP like binfmt_elf does. Each additional PT_INTERP overwrote the previous interpreter, leaking the name allocation and the interpreter file reference together with the write denial open_exec() took, leaving the file unwritable for as long as the system runs. - overlayfs: - Compare the full escaped xattr prefix including the trailing dot. An xattr like "trusted.overlay.overlayfoo" was misclassified as an escaped overlay xattr. - Check read access to the copy_file_range() source with the source's mounter credentials. - super: Thawing a filesystem whose block device was frozen with bdev_freeze() deadlocked. Dropping the last block layer freeze reference from under s_umount ends up in fs_bdev_thaw() which reacquires s_umount on the same task. Pin the superblock with an active reference instead and call bdev_thaw() without holding s_umount. - procfs: Return EACCES instead of success when the ptrace access check for namespace links fails. - afs: Use afs_dir_get_block() rather than afs_dir_find_block() for block 0 in afs_edit_dir_remove(), matching afs_edit_dir_add(). - Push the memcg gating of ->nr_cached_objects() down into the btrfs and shmem callbacks instead of skipping every callback during non-root memcg reclaim. The blanket check short-circuited XFS whose inode reclaim hook is intentionally driven from per-memcg contexts to free memcg-charged slab. - eventpoll: Pin files while checking reverse paths. Since struct file became SLAB_TYPESAFE_BY_RCU a concurrent close could free and recycle the file under the check which then took and dropped the f_lock of whatever live file now occupies that slot. * tag 'vfs-7.2-rc5.fixes' of git://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs: (24 commits) super: fix emergency thaw deadlock on frozen block devices pidfs: make pidfs_ino_lock static eventpoll: pin files while checking reverse paths fs: push nr_cached_objects memcg gating into individual filesystems afs: Fix afs_edit_dir_remove() to get, not find, block 0 iomap: prevent ioend merge when io_private differs iomap: add comments for ifs_clear/set_range_dirty() iomap: fix out-of-bounds bitmap_set() with zero-length range iomap: fix incorrect did_zero setting in iomap_zero_iter() iomap: support invalidating partial folios iomap: correct the range of a partial dirty clear fs/super: fix emergency thaw double-unlock of s_umount pidfs: handle FS_IOC32_GETVERSION in compat ioctl ovl: check access to copy_file_range source with src mounter creds proc: Fix broken error paths for namespace links pidfs: add pidfs_dentry_open() helper selftests/pidfd: check PIDFD_THREAD survives open_by_handle_at() pidfs: preserve thread pidfds reopened by file handle ovl: fix trusted xattr escape prefix matching selftests/fuse: add ACL_DONT_CACHE regression test ...
Diffstat (limited to 'tools/testing')
-rw-r--r--tools/testing/selftests/filesystems/fuse/Makefile10
-rw-r--r--tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c347
-rw-r--r--tools/testing/selftests/pidfd/pidfd_file_handle_test.c1
3 files changed, 358 insertions, 0 deletions
diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/testing/selftests/filesystems/fuse/Makefile
index 612aad69a93a..f47141484275 100644
--- a/tools/testing/selftests/filesystems/fuse/Makefile
+++ b/tools/testing/selftests/filesystems/fuse/Makefile
@@ -5,6 +5,13 @@ CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
TEST_GEN_PROGS := fusectl_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
+
include ../../lib.mk
VAR_CFLAGS := $(shell pkg-config fuse --cflags 2>/dev/null)
@@ -19,3 +26,6 @@ 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)
diff --git a/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c b/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c
new file mode 100644
index 000000000000..2411a6e285f1
--- /dev/null
+++ b/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c
@@ -0,0 +1,347 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test: FUSE ACL caching bug triggered by AT_STATX_FORCE_SYNC
+ *
+ * A FUSE mount that does not negotiate FUSE_POSIX_ACL initialises every inode
+ * with i_acl = i_default_acl = ACL_DONT_CACHE. When a fresh stat is needed
+ * (e.g. AT_STATX_FORCE_SYNC), fuse_update_get_attr() calls
+ * forget_all_cached_acls() before issuing FUSE_GETATTR. On an unfixed kernel,
+ * __forget_cached_acl() replaces ACL_DONT_CACHE with ACL_NOT_CACHED,
+ * inadvertently enabling the kernel ACL cache for that inode. The next
+ * getxattr populates the cache. Because fuse_set_acl() skips
+ * forget_all_cached_acls() for !fc->posix_acl mounts, any subsequent change to
+ * the ACL leaves the stale kernel entry in place, and the next getxattr returns
+ * wrong data without ever reaching the FUSE daemon.
+ *
+ * Fix (fs/posix_acl.c): __forget_cached_acl() returns early when *p is
+ * ACL_DONT_CACHE, preserving the "never cache" invariant for the inode's
+ * lifetime.
+ *
+ * Test outline:
+ * 1. Mount a minimal FUSE fs (no FUSE_POSIX_ACL negotiated).
+ * 2. lgetxattr -> daemon called, ACL_A returned, NOT cached (ACL_DONT_CACHE).
+ * 3. statx(AT_STATX_FORCE_SYNC) -> forget_all_cached_acls() called.
+ * Buggy: ACL_DONT_CACHE -> ACL_NOT_CACHED (cache enabled).
+ * Fixed: ACL_DONT_CACHE preserved.
+ * 4. lgetxattr -> daemon called, ACL_A returned.
+ * Buggy: result now cached (ACL_NOT_CACHED -> cached).
+ * Fixed: result still not cached.
+ * 5. Daemon switches to ACL_B internally (different size).
+ * 6. lgetxattr -> should return ACL_B (44 bytes).
+ * Buggy: cache hit, returns stale ACL_A (28 bytes). FAIL.
+ * Fixed: no cache, daemon called, returns ACL_B (44 bytes). PASS.
+ */
+
+#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"
+
+/* ---- ACL binary encoding ------------------------------------------------ */
+/*
+ * POSIX ACL v2 xattr format (little-endian):
+ * header: u32 version (= 0x00000002)
+ * entry: u16 tag | u16 perm | u32 id
+ *
+ * Entries must appear in tag-ascending order; named USER/GROUP entries
+ * require a MASK entry. Both ACLs pass posix_acl_from_xattr() validation.
+ */
+
+/* ACL_A: 3 entries (USER_OBJ:rwx, GROUP_OBJ:r-x, OTHER:r-x) = 28 bytes */
+static const uint8_t acl_a[] = {
+ 0x02, 0x00, 0x00, 0x00, /* v2 header */
+ 0x01, 0x00, 0x07, 0x00, 0xff, 0xff, 0xff, 0xff, /* USER_OBJ rwx */
+ 0x04, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff, /* GROUP_OBJ r-x */
+ 0x20, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff, /* OTHER r-x */
+};
+
+/*
+ * ACL_B: 5 entries — adds USER uid=1 and MASK = 44 bytes.
+ * A named USER entry requires a MASK; all tags in ascending order.
+ */
+static const uint8_t acl_b[] = {
+ 0x02, 0x00, 0x00, 0x00, /* v2 header */
+ 0x01, 0x00, 0x07, 0x00, 0xff, 0xff, 0xff, 0xff, /* USER_OBJ rwx */
+ 0x02, 0x00, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00, /* USER uid=1 rwx */
+ 0x04, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff, /* GROUP_OBJ r-x */
+ 0x10, 0x00, 0x07, 0x00, 0xff, 0xff, 0xff, 0xff, /* MASK rwx */
+ 0x20, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff, /* OTHER r-x */
+};
+
+/* ---- Shared state (daemon thread <-> test thread) ----------------------- */
+
+#define FILE_INO 2
+#define FILE_NAME "testfile"
+
+struct daemon_state {
+ pthread_mutex_t lock;
+ const uint8_t *acl;
+ size_t acl_size;
+ int getxattr_count;
+};
+
+/*
+ * Global: callbacks are stateless fns so we use a single global.
+ * Safe because only one test instance runs at a time.
+ */
+static struct daemon_state g_ds = {
+ .lock = PTHREAD_MUTEX_INITIALIZER,
+};
+
+/* ---- FUSE lowlevel callbacks -------------------------------------------- */
+
+static void fs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name)
+{
+ if (parent != FUSE_ROOT_ID || strcmp(name, FILE_NAME)) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ struct fuse_entry_param e = {};
+
+ /*
+ * Long attr/entry timeouts so that normal stat() calls do not
+ * expire and trigger forget_all_cached_acls() on their own;
+ * only the explicit AT_STATX_FORCE_SYNC should trigger it.
+ */
+ e.ino = FILE_INO;
+ e.generation = 1;
+ e.attr_timeout = 10.0;
+ e.entry_timeout = 10.0;
+ e.attr.st_ino = FILE_INO;
+ e.attr.st_mode = S_IFREG | 0644;
+ e.attr.st_nlink = 1;
+ fuse_reply_entry(req, &e);
+}
+
+static void fs_getattr(fuse_req_t req, fuse_ino_t ino,
+ struct fuse_file_info *fi)
+{
+ struct stat st = {};
+
+ (void)fi;
+ if (ino == FUSE_ROOT_ID) {
+ st.st_ino = FUSE_ROOT_ID;
+ st.st_mode = S_IFDIR | 0755;
+ st.st_nlink = 2;
+ } else if (ino == FILE_INO) {
+ st.st_ino = FILE_INO;
+ st.st_mode = S_IFREG | 0644;
+ st.st_nlink = 1;
+ } else {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ fuse_reply_attr(req, &st, 10);
+}
+
+static void fs_getxattr(fuse_req_t req, fuse_ino_t ino, const char *name,
+ size_t size)
+{
+ if (ino != FILE_INO ||
+ strcmp(name, "system.posix_acl_access") != 0) {
+ fuse_reply_err(req, ENODATA);
+ return;
+ }
+
+ pthread_mutex_lock(&g_ds.lock);
+ const uint8_t *acl = g_ds.acl;
+ size_t acl_size = g_ds.acl_size;
+ g_ds.getxattr_count++;
+ pthread_mutex_unlock(&g_ds.lock);
+
+ if (size == 0)
+ fuse_reply_xattr(req, acl_size);
+ else if (size < acl_size)
+ fuse_reply_err(req, ERANGE);
+ else
+ fuse_reply_buf(req, (const char *)acl, acl_size);
+}
+
+static const struct fuse_lowlevel_ops fs_ops = {
+ .lookup = fs_lookup,
+ .getattr = fs_getattr,
+ .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 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);
+
+ 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));
+
+ 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);
+}
+
+static int do_force_statx(const char *path)
+{
+ struct statx stx;
+
+ return statx(AT_FDCWD, path, AT_STATX_FORCE_SYNC, STATX_BASIC_STATS,
+ &stx);
+}
+
+TEST_F(acl_cache, stale_after_force_sync)
+{
+ char buf[512];
+ ssize_t sz;
+ int count;
+
+ /*
+ * Step 1: two getxattr calls before any statx(FORCE_SYNC).
+ * i_acl == ACL_DONT_CACHE. __get_acl's cmpxchg(p, ACL_NOT_CACHED,
+ * sentinel) finds *p != ACL_NOT_CACHED on every call, so the sentinel
+ * is never placed and the result is never cached. Both calls must
+ * reach the daemon, proving ACL_DONT_CACHE suppresses caching.
+ */
+ sz = lgetxattr(self->file_path, "system.posix_acl_access",
+ buf, sizeof(buf));
+ ASSERT_EQ(sz, (ssize_t)sizeof(acl_a));
+
+ sz = lgetxattr(self->file_path, "system.posix_acl_access",
+ buf, sizeof(buf));
+ ASSERT_EQ(sz, (ssize_t)sizeof(acl_a));
+
+ pthread_mutex_lock(&g_ds.lock);
+ count = g_ds.getxattr_count;
+ pthread_mutex_unlock(&g_ds.lock);
+
+ ASSERT_EQ(count, 2);
+ TH_LOG("step 1 OK: both pre-trigger getxattrs reached daemon (count=%d), "
+ "ACL_DONT_CACHE is working", count);
+
+ /*
+ * Step 2: statx(AT_STATX_FORCE_SYNC).
+ * fuse_update_get_attr() calls forget_all_cached_acls() before sending
+ * FUSE_GETATTR.
+ * Buggy kernel: ACL_DONT_CACHE -> ACL_NOT_CACHED (cache enabled)
+ * Fixed kernel: ACL_DONT_CACHE preserved (no effect)
+ */
+ ASSERT_EQ(do_force_statx(self->file_path), 0);
+ TH_LOG("step 2 OK: statx(AT_STATX_FORCE_SYNC) succeeded");
+
+ /*
+ * Step 3: getxattr — cache population attempt after the trigger.
+ * Buggy: *p == ACL_NOT_CACHED -> sentinel placed -> fuse_get_inode_acl
+ * called -> ACL_A parsed and stored in the kernel cache.
+ * Fixed: *p == ACL_DONT_CACHE -> sentinel placement skipped ->
+ * fuse_get_inode_acl called but result not cached.
+ * Either way the correct ACL_A is returned here.
+ */
+ sz = lgetxattr(self->file_path, "system.posix_acl_access",
+ buf, sizeof(buf));
+ ASSERT_EQ(sz, (ssize_t)sizeof(acl_a));
+
+ pthread_mutex_lock(&g_ds.lock);
+ count = g_ds.getxattr_count;
+ pthread_mutex_unlock(&g_ds.lock);
+
+ ASSERT_EQ(count, 3);
+ TH_LOG("step 3 OK: post-trigger getxattr reached daemon (count=%d), "
+ "returned correct ACL_A (%zd bytes)", count, sz);
+
+ /*
+ * Step 4: switch daemon to ACL_B (different size: 44 vs 28 bytes).
+ * Simulates an ACL change that fuse_set_acl() would NOT invalidate for
+ * !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.
+ */
+ pthread_mutex_lock(&g_ds.lock);
+ g_ds.acl = acl_b;
+ 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));
+
+ /*
+ * Step 5: getxattr — the decisive check.
+ * Buggy kernel: cache hit -> stale ACL_A (28 bytes), count stays 3.
+ * Fixed kernel: no cache -> daemon called -> ACL_B (44 bytes), count 4.
+ */
+ sz = lgetxattr(self->file_path, "system.posix_acl_access",
+ buf, sizeof(buf));
+
+ pthread_mutex_lock(&g_ds.lock);
+ count = g_ds.getxattr_count;
+ pthread_mutex_unlock(&g_ds.lock);
+
+ if (sz == (ssize_t)sizeof(acl_a))
+ TH_LOG("step 5 BUG: stale ACL_A (%zd bytes) from kernel cache "
+ "(count=%d); ACL_DONT_CACHE corrupted by "
+ "forget_all_cached_acls()", sz, count);
+ else
+ TH_LOG("step 5 OK: daemon reached (count=%d), "
+ "fresh ACL_B (%zd bytes)", count, sz);
+
+ EXPECT_EQ(sz, (ssize_t)sizeof(acl_b));
+ EXPECT_EQ(count, 4);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/pidfd/pidfd_file_handle_test.c b/tools/testing/selftests/pidfd/pidfd_file_handle_test.c
index 68918734dcf3..1e03ae9575fe 100644
--- a/tools/testing/selftests/pidfd/pidfd_file_handle_test.c
+++ b/tools/testing/selftests/pidfd/pidfd_file_handle_test.c
@@ -373,6 +373,7 @@ TEST_F(file_handle, open_by_handle_at_valid_flags)
O_CLOEXEC |
O_EXCL);
ASSERT_GE(pidfd, 0);
+ ASSERT_NE(fcntl(pidfd, F_GETFL) & PIDFD_THREAD, 0);
ASSERT_EQ(fstat(pidfd, &st2), 0);
ASSERT_TRUE(st1.st_dev == st2.st_dev && st1.st_ino == st2.st_ino);