From bbf5f639918dc011aaf60aab8480218758ee68c5 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 20 Jul 2026 14:36:49 +0200 Subject: [PATCH 01/24] binfmt_misc: set have_execfd only once the interpreter is opened load_misc_binary() raises bprm->have_execfd as soon as it sees the 'O' (or 'C') flag. This happens well before it opens the interpreter. If that open fails the flag stays set on the bprm. binfmt_misc is at the head of the format list so an interpreter open failure that returns -ENOEXEC lets the search fall through to a later format. This means it runs the matched binary directly having never staged an interpreter. So bprm->executable is NULL while have_execfd falsely claims a descriptor is present. Consequently, begin_new_exec() dereferences the missing executable: would_dump(bprm, bprm->executable); and NULL derefs. Had it not, the hand-off later in the same function would have failed anyway. FD_ADD(0, bprm->executable) rejects a NULL file with -ENOMEM. Both sites are past the point of no return so the exec cannot be unwound either way. This can be reached by unprivileged users as binfmt_misc can be mounted in user namespaces. So a user can register an 'O' entry whose interpreter lives on a FUSE mount, have the FUSE server fail the open with -ENOEXEC and execute a native ELF file that matches the entry. have_execfd only means anything alongside the executable it describes which is not set until the interpreter has been opened and staged. So lets raise it there, next to execfd_creds, which is already set at that point. An open failure now leaves it clear, so the fallback format derives credentials from the binary and emits no AT_EXECFD, as it would for any native exec. The argv rewrite load_misc_binary() performs before the open is still not undone. This means the binary sees the interpreter path in argv[0] and its own path in argv[1] but that predates this change and only became observable once the exec stopped faulting. Link: https://patch.msgid.link/20260720-beglichen-kognitiv-organismus-5e1e55326c56@brauner Fixes: bc2bf338d54b ("exec: Remove recursion from search_binary_handler") Cc: stable@vger.kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 84349fcb93f1..5de615ca7a75 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -228,9 +228,6 @@ static int load_misc_binary(struct linux_binprm *bprm) goto ret; } - if (fmt->flags & MISC_FMT_OPEN_BINARY) - bprm->have_execfd = 1; - /* make argv[1] be the path to the binary */ retval = copy_string_kernel(bprm->interp, bprm); if (retval < 0) @@ -260,6 +257,8 @@ static int load_misc_binary(struct linux_binprm *bprm) goto ret; bprm->interpreter = interp_file; + if (fmt->flags & MISC_FMT_OPEN_BINARY) + bprm->have_execfd = 1; if (fmt->flags & MISC_FMT_CREDENTIALS) bprm->execfd_creds = 1; From 16cc4f5c1c4b9e45eca7f7deefa5410a292db599 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 12:08:49 +0200 Subject: [PATCH 02/24] exec: fix unsigned loop counter wrap in transfer_args_to_stack() The stop value is derived from bprm->p >> PAGE_SHIFT. The index variable is an unsigned long. If bprm->p drops below PAGE_SIZE and stop becomes zero the loop condition index >= stop is always true. After the index == 0 iteration the decrement wraps to ULONG_MAX and bprm->page[ULONG_MAX] reads sizeof(void *) bytes in front of the array. The pointer has wrapped to -1. That garbage pointer is then passed to kmap_local_page() and PAGE_SIZE bytes are copied from wherever that lands into the stack of the process being created. And the loop doesn't terminate either... Getting there only requires bprm->p < PAGE_SIZE. On !MMU bprm_set_stack_limit() and bprm_hit_stack_limit() are empty. So the only constraint on how far bprm->p is pushed down is valid_arg_len(), i.e. that each individual string still fits in what is left. bprm->p starts at PAGE_SIZE * MAX_ARG_PAGES - sizeof(void *) so a single argument or environment string of a little over 31 pages leaves it in the first page: Oops - load access fault [#1] CPU: 0 UID: 0 PID: 1 Comm: victim Not tainted 7.2.0-rc4 #1 epc : __memcpy+0xd4/0xf8 ra : transfer_args_to_stack+0xaa/0xae s4 : ffffffffffffffff s2 : 0000000000000000 a1 : ffffffdc98000000 a2 : 0000000000001000 status: 0000000a00001880 badaddr: ffffffdc98000000 cause: 0000000000000005 [<801a5324>] __memcpy+0xd4/0xf8 [<800d5f6a>] load_flat_binary+0x43a/0x65e [<800a2de4>] bprm_execve+0x1d4/0x316 [<800a351a>] do_execveat_common+0x12e/0x138 [<800a3d44>] __riscv_sys_execve+0x38/0x4e Kernel panic - not syncing: Fatal exception in interrupt This is an arcane bug but we should still fix it. Count down from MAX_ARG_PAGES so the loop ends when index reaches stop, stop == 0 included. The iterations performed are unchanged for every other value of stop. Only CONFIG_MMU=n builds are affected, transfer_args_to_stack() is used by binfmt_flat and binfmt_elf_fdpic on nommu only. The loop predates git history. commit 7e7ec6a93434 ("elf_fdpic_transfer_args_to_stack(): make it generic") only moved it from binfmt_elf_fdpic.c into fs/exec.c and narrowed the copy to the used part of the first page. The condition and the decrement are unchanged from 2.6.12-rc2. Link: https://patch.msgid.link/20260721-hochachtung-staumauer-pigmente-15d71f7d7d04@brauner Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Cc: stable@vger.kernel.org Reviewed-by: David Hildenbrand (Arm) Signed-off-by: Christian Brauner (Amutable) --- fs/exec.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/exec.c b/fs/exec.c index d5993cedc829..c7b8f2d6366c 100644 --- a/fs/exec.c +++ b/fs/exec.c @@ -740,7 +740,7 @@ int transfer_args_to_stack(struct linux_binprm *bprm, stop = bprm->p >> PAGE_SHIFT; sp = *sp_location; - for (index = MAX_ARG_PAGES - 1; index >= stop; index--) { + for (index = MAX_ARG_PAGES; index-- > stop; ) { unsigned int offset = index == stop ? bprm->p & ~PAGE_MASK : 0; char *src = kmap_local_page(bprm->page[index]) + offset; sp -= PAGE_SIZE - offset; From 3349ef6a366a61d631f6a263d12cea240957719d Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 13:20:45 +0200 Subject: [PATCH 03/24] binfmt_elf_fdpic: only honour the first PT_INTERP The program header scan handles PT_INTERP from a switch nested in the scan loop, so its break leaves the switch and not the loop. A binary carrying more than one PT_INTERP runs the case again and overwrites both interpreter_name and interpreter. The previous name allocation leaks and so does the previous interpreter reference, along with the write denial open_exec() took on it. The denial is never released, so the file stays unwritable for as long as the system runs. An unprivileged caller reaches this with a crafted binary and repeats it at will. binfmt_elf stops at the first PT_INTERP. Do the same here. The flaw dates back to the driver's introduction in the pre-git history tree introduced in v2.6.11 by 91808d6ebe39 ("[PATCH] FRV: Add FDPIC ELF binary format driver"). Link: https://patch.msgid.link/20260721-gezittert-medium-kreide-b41fc1f0277e@brauner Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Cc: stable@vger.kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_elf_fdpic.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fs/binfmt_elf_fdpic.c b/fs/binfmt_elf_fdpic.c index 7e3108489c83..fe0b5c5ed2bc 100644 --- a/fs/binfmt_elf_fdpic.c +++ b/fs/binfmt_elf_fdpic.c @@ -231,6 +231,10 @@ static int load_elf_fdpic_binary(struct linux_binprm *bprm) for (i = 0; i < exec_params.hdr.e_phnum; i++, phdr++) { switch (phdr->p_type) { case PT_INTERP: + /* elf ABI allows only one interpreter */ + if (interpreter_name) + continue; + retval = -ENOMEM; if (phdr->p_filesz > PATH_MAX) goto error; From 4b9a5458d02e214ef2b384124ca626e3e381d778 Mon Sep 17 00:00:00 2001 From: Amir Goldstein Date: Tue, 14 Jul 2026 00:09:31 +0200 Subject: [PATCH 04/24] fs: preserve ACL_DONT_CACHE state in forget_cached_acl() The ACL_DONT_CACHE state is meant to be a constant state for the inode for filesystems that want to opt out of posix acl caching. Commit facd61053cff1 ("fuse: fixes after adapting to new posix acl api") used this facility to opt out of posix acl caching for fuse inodes with fuse server that does not negotiate FUSE_POSIX_ACL (fc->posix_acl). The commit also takes care to gate the forget_all_cached_acls() call in fuse_set_acl() on fc->posix_acl because there is no need for it, but there are other placed in fuse code which call forget_all_cached_acls() unconditional to fc->posix_acl and those cause the loss of the ACL_DONT_CACHE state. This is not only a functional bug. Properly timed, a get_acl() from this fuse filesystem can return a stale cached value, as was observed in tests, because set_acl() does not invalidate the unintentional acl cache. We could fix this in fuse, but it actually makes no sense for the vfs helper forget_cached_acl() to invalidate the ACL_DONT_CACHE state, so let it not do that to fix fuse and future users of ACL_DONT_CACHE. Fixes: facd61053cff1 ("fuse: fixes after adapting to new posix acl api") Cc: stable@vger.kernel.org Signed-off-by: Amir Goldstein Link: https://patch.msgid.link/20260713220932.413004-2-amir73il@gmail.com Reviewed-by: Luis Henriques Signed-off-by: Christian Brauner (Amutable) --- fs/posix_acl.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fs/posix_acl.c b/fs/posix_acl.c index b4bfe4ddf64e..3dc62c1c2708 100644 --- a/fs/posix_acl.c +++ b/fs/posix_acl.c @@ -93,6 +93,13 @@ static void __forget_cached_acl(struct posix_acl **p) { struct posix_acl *old; + /* + * ACL_DONT_CACHE is expected to be a "const" value and xchg it with + * ACL_NOT_CACHED would enable acl caching for the inode - + * clearly not what the caller has intended. + */ + if (READ_ONCE(*p) == ACL_DONT_CACHE) + return; old = xchg(p, ACL_NOT_CACHED); if (!is_uncached_acl(old)) posix_acl_release(old); From 9acb102522b92f24fba6b238b3668a4d9dfcb592 Mon Sep 17 00:00:00 2001 From: Amir Goldstein Date: Tue, 14 Jul 2026 00:09:32 +0200 Subject: [PATCH 05/24] selftests/fuse: add ACL_DONT_CACHE regression test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a test that reproduces the stale ACL bug fixed by: "fs: preserve ACL_DONT_CACHE state in forget_cached_acl()" A FUSE mount that does not negotiate FUSE_POSIX_ACL initialises inodes with i_acl = ACL_DONT_CACHE. Before the fix, calling forget_all_cached_acls() (e.g. from fuse_update_get_attr() on a statx(AT_STATX_FORCE_SYNC)) would silently replace ACL_DONT_CACHE with ACL_NOT_CACHED, enabling the kernel ACL cache. A subsequent getxattr would populate the cache, and because fuse_set_acl() skips forget_all_cached_acls() for !fc->posix_acl, later ACL changes were not visible to callers — getxattr returned stale data. The test mounts a minimal libfuse3 lowlevel filesystem (no FUSE_POSIX_ACL negotiated) and: 1. Issues two getxattrs — both must reach the daemon, proving ACL_DONT_CACHE suppresses caching before any trigger. 2. Calls statx(AT_STATX_FORCE_SYNC) to trigger forget_all_cached_acls(). 3. Issues another getxattr (populates the cache on a buggy kernel). 4. Switches the daemon to a different-sized ACL (ACL_B). 5. Issues a final getxattr — expects ACL_B (44 bytes) and daemon call count 4; a buggy kernel returns stale ACL_A (28 bytes). fuse_acl_cache_test is only built when libfuse3 is detected via pkg-config. Christian Brauner says: Changed do_force_statx() to call the statx() libc wrapper instead of syscall(SYS_statx, ...) as requested by Amir after review feedback from Luis Henriques, and dropped the now unused include. Signed-off-by: Amir Goldstein Link: https://patch.msgid.link/20260713220932.413004-3-amir73il@gmail.com Signed-off-by: Christian Brauner (Amutable) --- .../selftests/filesystems/fuse/Makefile | 10 + .../filesystems/fuse/fuse_acl_cache_test.c | 347 ++++++++++++++++++ 2 files changed, 357 insertions(+) create mode 100644 tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FUSE_USE_VERSION 31 +#include + +#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 From a8e72879cd0d8422c0b47d6d3c1802274fe73b98 Mon Sep 17 00:00:00 2001 From: Yichong Chen Date: Wed, 8 Jul 2026 16:22:21 +0800 Subject: [PATCH 06/24] ovl: fix trusted xattr escape prefix matching In the trusted.* xattr namespace, ovl_is_escaped_xattr() compares one byte less than the escaped overlay xattr prefix length. This makes it match "trusted.overlay.overlay" without requiring the trailing dot. As a result, an xattr such as "trusted.overlay.overlayfoo" is incorrectly treated as an escaped overlay xattr. This can be reproduced by setting "trusted.overlay.overlayfoo" on a lower file and listing xattrs through an overlay mount. listxattr() then exposes it as "trusted.overlay.oo", and a following getxattr() on that listed name fails with ENODATA. Compare the full escaped prefix, including the trailing dot, so similarly-prefixed private xattrs are not misclassified. Fixes: dad02fad84cbc ("ovl: Support escaped overlay.* xattrs") Signed-off-by: Yichong Chen Link: https://patch.msgid.link/20260708082221.633602-1-chenyichong@uniontech.com Reviewed-by: Amir Goldstein Signed-off-by: Christian Brauner (Amutable) --- fs/overlayfs/xattrs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/overlayfs/xattrs.c b/fs/overlayfs/xattrs.c index aa95855c7023..859e80ae6f40 100644 --- a/fs/overlayfs/xattrs.c +++ b/fs/overlayfs/xattrs.c @@ -13,7 +13,7 @@ static bool ovl_is_escaped_xattr(struct super_block *sb, const char *name) OVL_XATTR_ESCAPE_USER_PREFIX_LEN) == 0; else return strncmp(name, OVL_XATTR_ESCAPE_TRUSTED_PREFIX, - OVL_XATTR_ESCAPE_TRUSTED_PREFIX_LEN - 1) == 0; + OVL_XATTR_ESCAPE_TRUSTED_PREFIX_LEN) == 0; } static bool ovl_is_own_xattr(struct super_block *sb, const char *name) From bb6bc13c53e211d9148ed2eab3e689c5cd5c75da Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 22 Jul 2026 13:50:53 +0200 Subject: [PATCH 07/24] pidfs: preserve thread pidfds reopened by file handle PIDFD_THREAD shares O_EXCL. do_dentry_open() clears O_EXCL after pidfs_export_open() validates the flags, so open_by_handle_at() silently turns a thread pidfd into a process pidfd. Restore PIDFD_THREAD on the opened file, matching pidfs_alloc_file(). Signed-off-by: Li Chen Reviewed-by: Jan Kara Link: https://patch.msgid.link/20260716052726.1032092-1-me@linux.beauty Signed-off-by: Christian Brauner (Amutable) --- fs/pidfs.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fs/pidfs.c b/fs/pidfs.c index aaa609ddab04..c20ffd747ff5 100644 --- a/fs/pidfs.c +++ b/fs/pidfs.c @@ -939,12 +939,18 @@ static int pidfs_export_permission(struct handle_to_path_ctx *ctx, static struct file *pidfs_export_open(const struct path *path, unsigned int oflags) { + struct file *file; + /* * Clear O_LARGEFILE as open_by_handle_at() forces it and raise * O_RDWR as pidfds always are. */ oflags &= ~O_LARGEFILE; - return dentry_open(path, oflags | O_RDWR, current_cred()); + file = dentry_open(path, oflags | O_RDWR, current_cred()); + /* do_dentry_open() strips O_EXCL, which encodes PIDFD_THREAD. */ + if (!IS_ERR(file)) + file->f_flags |= oflags & PIDFD_THREAD; + return file; } static const struct export_operations pidfs_export_operations = { From 58af123ec7aa7eab19e938b3777c41c02abda1b8 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 22 Jul 2026 13:50:53 +0200 Subject: [PATCH 08/24] selftests/pidfd: check PIDFD_THREAD survives open_by_handle_at() Verify that a thread pidfd reopened via open_by_handle_at() still reports PIDFD_THREAD in F_GETFL. Signed-off-by: Li Chen Reviewed-by: Jan Kara Link: https://patch.msgid.link/20260716052726.1032092-1-me@linux.beauty Signed-off-by: Christian Brauner (Amutable) --- tools/testing/selftests/pidfd/pidfd_file_handle_test.c | 1 + 1 file changed, 1 insertion(+) 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); From 927b89700e9fdba61902f8828dbf9b5f29f40ea7 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 22 Jul 2026 13:56:28 +0200 Subject: [PATCH 09/24] pidfs: add pidfs_dentry_open() helper Both pidfs_alloc_file() and pidfs_export_open() need to force O_RDWR and reapply the pidfd flags that do_dentry_open() strips. Move the common logic into a helper. PIDFD_AUTOKILL is now part of the restore mask in the file handle path as well, but pidfs_export_permission() rejects O_TRUNC, so this is a no-op there. But warn nonetheless. Link: https://patch.msgid.link/20260722-esszimmer-umsetzen-nennt-ed5fc604300a@brauner Signed-off-by: Christian Brauner (Amutable) --- fs/pidfs.c | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/fs/pidfs.c b/fs/pidfs.c index c20ffd747ff5..695215aa2a58 100644 --- a/fs/pidfs.c +++ b/fs/pidfs.c @@ -915,6 +915,20 @@ static struct dentry *pidfs_fh_to_dentry(struct super_block *sb, return path.dentry; } +static struct file *pidfs_dentry_open(const struct path *path, + unsigned int flags, + const struct cred *cred) +{ + struct file *file; + + /* pidfds are always O_RDWR. */ + file = dentry_open(path, flags | O_RDWR, cred); + /* do_dentry_open() strips O_EXCL and O_TRUNC. */ + if (!IS_ERR(file)) + file->f_flags |= flags & (PIDFD_THREAD | PIDFD_AUTOKILL); + return file; +} + /* * Make sure that we reject any nonsensical flags that users pass via * open_by_handle_at(). Note that PIDFD_THREAD is defined as O_EXCL, and @@ -939,18 +953,14 @@ static int pidfs_export_permission(struct handle_to_path_ctx *ctx, static struct file *pidfs_export_open(const struct path *path, unsigned int oflags) { - struct file *file; - /* - * Clear O_LARGEFILE as open_by_handle_at() forces it and raise - * O_RDWR as pidfds always are. + * Opening via file handle may never raise PIDFD_AUTOKILL. That can + * only be done at task creation! */ - oflags &= ~O_LARGEFILE; - file = dentry_open(path, oflags | O_RDWR, current_cred()); - /* do_dentry_open() strips O_EXCL, which encodes PIDFD_THREAD. */ - if (!IS_ERR(file)) - file->f_flags |= oflags & PIDFD_THREAD; - return file; + if (WARN_ON_ONCE(oflags & PIDFD_AUTOKILL)) + return ERR_PTR(-EINVAL); + /* Clear O_LARGEFILE as open_by_handle_at() forces it. */ + return pidfs_dentry_open(path, oflags & ~O_LARGEFILE, current_cred()); } static const struct export_operations pidfs_export_operations = { @@ -1114,7 +1124,6 @@ static struct file_system_type pidfs_type = { struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags) { - struct file *pidfd_file; struct path path __free(path_put) = {}; int ret; @@ -1132,16 +1141,7 @@ struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags) VFS_WARN_ON_ONCE(!pid->attr); flags &= ~PIDFD_STALE; - flags |= O_RDWR; - pidfd_file = dentry_open(&path, flags, current_cred()); - /* - * Raise PIDFD_THREAD and PIDFD_AUTOKILL explicitly as - * do_dentry_open() strips O_EXCL and O_TRUNC. - */ - if (!IS_ERR(pidfd_file)) - pidfd_file->f_flags |= (flags & (PIDFD_THREAD | PIDFD_AUTOKILL)); - - return pidfd_file; + return pidfs_dentry_open(&path, flags, current_cred()); } void __init pidfs_init(void) From 425224c2d700391729be7fe6929a88ef4e2d7a4e Mon Sep 17 00:00:00 2001 From: Jann Horn Date: Mon, 6 Jul 2026 20:22:42 +0200 Subject: [PATCH 10/24] proc: Fix broken error paths for namespace links Don't return the return value of down_read_killable() (0) when a ptrace access check fails, return -EACCES as intended. Reported-by: Magnus Lindholm Closes: https://lore.kernel.org/r/20260706170735.2941493-1-linmag7@gmail.com Fixes: 6650527444da ("proc: protect ptrace_may_access() with exec_update_lock (part 1)") Cc: stable@vger.kernel.org Signed-off-by: Jann Horn Link: https://patch.msgid.link/20260706-procfs-ns-eacces-fix-v1-1-a69ab14c02e6@google.com Tested-by: Magnus Lindholm Signed-off-by: Christian Brauner (Amutable) --- fs/proc/namespaces.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fs/proc/namespaces.c b/fs/proc/namespaces.c index 2f46f1396744..ea6ec61a0430 100644 --- a/fs/proc/namespaces.c +++ b/fs/proc/namespaces.c @@ -46,7 +46,7 @@ static const char *proc_ns_get_link(struct dentry *dentry, const struct proc_ns_operations *ns_ops = PROC_I(inode)->ns_ops; struct task_struct *task; struct path ns_path; - int error = -EACCES; + int error; if (!dentry) return ERR_PTR(-ECHILD); @@ -59,6 +59,7 @@ static const char *proc_ns_get_link(struct dentry *dentry, if (error) goto out_put_task; + error = -EACCES; if (!ptrace_may_access(task, PTRACE_MODE_READ_FSCREDS)) goto out; @@ -90,6 +91,7 @@ static int proc_ns_readlink(struct dentry *dentry, char __user *buffer, int bufl if (res) goto out_put_task; + res = -EACCES; if (ptrace_may_access(task, PTRACE_MODE_READ_FSCREDS)) { res = ns_get_name(name, sizeof(name), task, ns_ops); if (res >= 0) From a1e0eb8f55cfe09bb31a202a388babc411292656 Mon Sep 17 00:00:00 2001 From: Amir Goldstein Date: Sun, 12 Jul 2026 14:24:21 +0200 Subject: [PATCH 11/24] ovl: check access to copy_file_range source with src mounter creds Commit 5dae222a5ff0c ("vfs: allow copy_file_range to copy across devices") allowed filesystems that implement the copy_file_range() f_op to decide if they want to access cross-sb copy from/to the same fs type. The same commit added checks to verify same sb copy for filesystems that implement ->copy_file_range() and do not support cross-sb copy at the time, namely, to ceph, fuse and nfs. The two remaining fs which implement ->copy_file_range(), cifs and overlayfs started to support cross-sb copy from this time. While overlayfs does support cross-sb copy when the two underlying files are on the same base fs, the copy operation on the two real files from two different overalyfs filesystems is performed with the mounter creds of the destination overlayfs and the read permission access hook for the source file was called with the wrong creds. This could cause either deny of access to copy which would otherwise be allowed (e.g. with splice) or allow read access to file which would otherwise be denied. Fix the latter case by explicitly verifying read access to source file with the source overlayfs mounter creds. The former case remains a quirk of cross-sb overlayfs copy, but userspace could fall back to regular copy so no harm done. Fixes: 5dae222a5ff0c ("vfs: allow copy_file_range to copy across devices") Signed-off-by: Amir Goldstein Link: https://patch.msgid.link/20260712122421.203113-1-amir73il@gmail.com Signed-off-by: Christian Brauner (Amutable) --- fs/overlayfs/file.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/fs/overlayfs/file.c b/fs/overlayfs/file.c index 27cc07738f33..f3d97eb146e8 100644 --- a/fs/overlayfs/file.c +++ b/fs/overlayfs/file.c @@ -528,6 +528,7 @@ static loff_t ovl_copyfile(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, loff_t len, unsigned int flags, enum ovl_copyop op) { + struct inode *inode_in = file_inode(file_in); struct inode *inode_out = file_inode(file_out); struct file *realfile_in, *realfile_out; loff_t ret; @@ -551,7 +552,20 @@ static loff_t ovl_copyfile(struct file *file_in, loff_t pos_in, if (IS_ERR(realfile_in)) goto out_unlock; - with_ovl_creds(file_inode(file_out)->i_sb) { + /* + * For cross-sb copy, vfs_copy_file_range() will verify read access with + * the mounter creds of the dest fs mounter, so we need to explicitly + * verify read access with the source mounter creds. + */ + if (unlikely(inode_in->i_sb != inode_out->i_sb)) { + with_ovl_creds(inode_in->i_sb) { + ret = rw_verify_area(READ, realfile_in, &pos_in, len); + if (unlikely(ret)) + goto out_unlock; + } + } + + with_ovl_creds(inode_out->i_sb) { switch (op) { case OVL_COPY: ret = vfs_copy_file_range(realfile_in, pos_in, From 1d0cff74d8c8d798a64c1e7fe442659aecb64130 Mon Sep 17 00:00:00 2001 From: Li Chen Date: Thu, 16 Jul 2026 13:28:20 +0800 Subject: [PATCH 12/24] pidfs: handle FS_IOC32_GETVERSION in compat ioctl FS_IOC32_GETVERSION has a distinct compat command encoding. Passing it through compat_ptr_ioctl() leaves pidfd_ioctl() unable to recognize the otherwise architecture-independent inode generation query. Translate the compat command to FS_IOC_GETVERSION before dispatching it through the native pidfd ioctl implementation. Signed-off-by: Li Chen Link: https://patch.msgid.link/20260716052822.1034228-1-me@linux.beauty Reviewed-by: Jan Kara Signed-off-by: Christian Brauner (Amutable) --- fs/pidfs.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/fs/pidfs.c b/fs/pidfs.c index 695215aa2a58..d7fe9abdd6f1 100644 --- a/fs/pidfs.c +++ b/fs/pidfs.c @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-2.0 #include +#include #include #include #include @@ -659,6 +660,17 @@ static long pidfd_ioctl(struct file *file, unsigned int cmd, unsigned long arg) return open_namespace(ns_common); } +#ifdef CONFIG_COMPAT +static long pidfd_compat_ioctl(struct file *file, unsigned int cmd, + unsigned long arg) +{ + if (cmd == FS_IOC32_GETVERSION) + cmd = FS_IOC_GETVERSION; + + return pidfd_ioctl(file, cmd, (unsigned long)compat_ptr(arg)); +} +#endif + static int pidfs_file_release(struct inode *inode, struct file *file) { struct pid *pid = inode->i_private; @@ -686,7 +698,9 @@ static const struct file_operations pidfs_file_operations = { .show_fdinfo = pidfd_show_fdinfo, #endif .unlocked_ioctl = pidfd_ioctl, - .compat_ioctl = compat_ptr_ioctl, +#ifdef CONFIG_COMPAT + .compat_ioctl = pidfd_compat_ioctl, +#endif }; struct pid *pidfd_pid(const struct file *file) From 503d67fbaec6fdeaba391cb497675071db9d16ea Mon Sep 17 00:00:00 2001 From: Chen Changcheng Date: Tue, 21 Jul 2026 14:41:40 +0800 Subject: [PATCH 13/24] fs/super: fix emergency thaw double-unlock of s_umount do_thaw_all() iterates over all superblocks via __iterate_supers() with SUPER_ITER_EXCL, which acquires s_umount exclusively before calling the callback and releases it afterwards. However, the callback do_thaw_all_callback() calls thaw_super_locked() which unconditionally releases s_umount on every code path. This results in a second unlock attempt in __iterate_supers() that corrupts the rwsem state, triggering a DEBUG_RWSEMS warning: [ 182.601148] sysrq: Emergency Thaw of all frozen filesystems [ 182.601865] ------------[ cut here ]------------ [ 182.602375] DEBUG_RWSEMS_WARN_ON((rwsem_owner(sem) != current) && !rwsem_test_oflags(sem, RWSEM_NONSPINNABLE)): count = 0x0, magic = 0xffff99b1011e5870, owner = 0x0, curr 0xffff99b101b06c80, list not empty [ 182.603817] WARNING: kernel/locking/rwsem.c:1412 at up_write+0xa3/0x170, CPU#2: kworker/2:1/53 [ 182.604578] Modules linked in: [ 182.604864] CPU: 2 UID: 0 PID: 53 Comm: kworker/2:1 Not tainted 7.2.0-rc4-00001-gbd3bd93ea98a-dirty #4 PREEMPT(lazy) [ 182.605711] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.13.0-1kylin1 04/01/2014 [ 182.606417] Workqueue: events do_thaw_all [ 182.606750] RIP: 0010:up_write+0xaf/0x170 [ 182.607076] Code: 19 3a 92 48 0f 44 c2 48 8b 55 08 48 8b 55 00 4c 8b 45 08 48 8b 55 00 48 8d 3d ad 91 e0 01 48 8b 4d 20 50 48 c7 c6 f0 8c 26 92 <67> 48 0f b9 3a e8 d7 93 4e 00 58 eb 81 48 83 7f 18 00 48 c7 c2 8d [ 182.608563] RSP: 0018:ffffb670001d7e08 EFLAGS: 00010246 [ 182.609007] RAX: ffffffff92349e8d RBX: 0000000000000000 RCX: ffff99b1011e5870 [ 182.609595] RDX: 0000000000000000 RSI: ffffffff92268cf0 RDI: ffffffff92914d10 [ 182.610283] RBP: ffff99b1011e5870 R08: 0000000000000000 R09: ffff99b101b06c80 [ 182.610847] R10: ffff99b10139a808 R11: fefefefefefefeff R12: 0000000000000000 [ 182.611414] R13: ffffffff90cf74d0 R14: 0000000000000000 R15: ffff99b1011e5800 [ 182.612009] FS: 0000000000000000(0000) GS:ffff99b1eaaee000(0000) knlGS:0000000000000000 [ 182.612670] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 [ 182.613146] CR2: 00000000005c631c CR3: 00000000013ee000 CR4: 00000000000006f0 [ 182.613722] Call Trace: [ 182.613946] [ 182.614130] __iterate_supers+0x128/0x150 [ 182.614463] do_thaw_all+0x1b/0x30 [ 182.614759] process_scheduled_works+0xbb/0x3f0 [ 182.615150] ? __pfx_worker_thread+0x10/0x10 [ 182.615499] worker_thread+0x129/0x270 [ 182.615816] ? __pfx_worker_thread+0x10/0x10 [ 182.616201] kthread+0xe2/0x120 [ 182.616469] ? __pfx_kthread+0x10/0x10 [ 182.616792] ret_from_fork+0x15b/0x240 [ 182.617115] ? __pfx_kthread+0x10/0x10 [ 182.617426] ret_from_fork_asm+0x1a/0x30 [ 182.617761] [ 182.617968] ---[ end trace 0000000000000000 ]--- [ 182.618412] Emergency Thaw complete Fix this by switching to SUPER_ITER_UNLOCKED and acquiring s_umount in the callback via super_lock_excl() before calling thaw_super_locked(). This matches the locking pattern expected by thaw_super_locked() and eliminates the double unlock. While at it, remove the dead 'return;' at the end of do_thaw_all_callback(). Fixes: 2992476528ae ("super: use a common iterator (Part 1)") Cc: stable@vger.kernel.org Signed-off-by: Chen Changcheng Link: https://patch.msgid.link/20260721064140.152305-1-chenchangcheng@kylinos.cn Signed-off-by: Christian Brauner (Amutable) --- fs/super.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/fs/super.c b/fs/super.c index a8fd61136aaf..70dcb07e7fa5 100644 --- a/fs/super.c +++ b/fs/super.c @@ -1084,16 +1084,19 @@ void emergency_remount(void) static void do_thaw_all_callback(struct super_block *sb, void *unused) { + if (!super_lock_excl(sb)) + return; + if (IS_ENABLED(CONFIG_BLOCK)) while (sb->s_bdev && !bdev_thaw(sb->s_bdev)) pr_warn("Emergency Thaw on %pg\n", sb->s_bdev); + thaw_super_locked(sb, FREEZE_HOLDER_USERSPACE, NULL); - return; } static void do_thaw_all(struct work_struct *work) { - __iterate_supers(do_thaw_all_callback, NULL, SUPER_ITER_EXCL); + __iterate_supers(do_thaw_all_callback, NULL, SUPER_ITER_UNLOCKED); kfree(work); printk(KERN_WARNING "Emergency Thaw complete\n"); } From 88c26515313169806a412a362b32a1eca53d21bd Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Tue, 14 Jul 2026 16:23:21 +0800 Subject: [PATCH 14/24] iomap: correct the range of a partial dirty clear The block range calculation in ifs_clear_range_dirty() is incorrect when partially clearing a range in a folio. We cannot clear the dirty bit of the first block or the last block if the start or end offset is not blocksize-aligned. This has not yet caused any issues since we always clear a whole folio in iomap_writeback_folio(). Fix this by rounding up the first block to blocksize alignment, and calculate the last block by rounding down (using truncation). Correct the nr_blks calculation accordingly. Fixes: 4ce02c679722 ("iomap: Add per-block dirty state tracking to improve performance") Signed-off-by: Zhang Yi Link: https://patch.msgid.link/20260714082325.325163-2-yi.zhang@huaweicloud.com Reviewed-by: Joanne Koong Reviewed-by: "Darrick J. Wong" Reviewed-by: Christoph Hellwig Signed-off-by: Christian Brauner (Amutable) --- fs/iomap/buffered-io.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c index 276720bc18dc..238b8b1dea91 100644 --- a/fs/iomap/buffered-io.c +++ b/fs/iomap/buffered-io.c @@ -177,13 +177,17 @@ static void ifs_clear_range_dirty(struct folio *folio, { struct inode *inode = folio->mapping->host; unsigned int blks_per_folio = i_blocks_per_folio(inode, folio); - unsigned int first_blk = (off >> inode->i_blkbits); - unsigned int last_blk = (off + len - 1) >> inode->i_blkbits; - unsigned int nr_blks = last_blk - first_blk + 1; + unsigned int first_blk = round_up(off, i_blocksize(inode)) >> + inode->i_blkbits; + unsigned int last_blk = (off + len) >> inode->i_blkbits; unsigned long flags; + if (first_blk >= last_blk) + return; + spin_lock_irqsave(&ifs->state_lock, flags); - bitmap_clear(ifs->state, first_blk + blks_per_folio, nr_blks); + bitmap_clear(ifs->state, first_blk + blks_per_folio, + last_blk - first_blk); spin_unlock_irqrestore(&ifs->state_lock, flags); } From 562d192c43459d70d955775e8a17eebd995539d4 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Tue, 14 Jul 2026 16:23:22 +0800 Subject: [PATCH 15/24] iomap: support invalidating partial folios Current iomap_invalidate_folio() can only invalidate an entire folio. If we truncate a partial folio on a filesystem where the block size is smaller than the folio size, it will leave behind dirty bits for the truncated or punched blocks. During the write-back process, it will attempt to map the invalid hole range. Fortunately, this has not caused any real problems so far because the ->writeback_range() function corrects the length. However, the implementation of FALLOC_FL_ZERO_RANGE in ext4 depends on the support for invalidating partial folios. When ext4 partially zeroes out a dirty and unwritten folio, it does not perform a flush first like XFS. Therefore, if the dirty bits of the corresponding area cannot be cleared, the zeroed area after writeback remains in the written state rather than reverting to the unwritten state. Fix this by supporting invalidation of partial folios. Signed-off-by: Zhang Yi Link: https://patch.msgid.link/20260714082325.325163-3-yi.zhang@huaweicloud.com Reviewed-by: "Darrick J. Wong" Reviewed-by: Joanne Koong Reviewed-by: Christoph Hellwig Signed-off-by: Christian Brauner (Amutable) --- fs/iomap/buffered-io.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c index 238b8b1dea91..b482e112321f 100644 --- a/fs/iomap/buffered-io.c +++ b/fs/iomap/buffered-io.c @@ -815,6 +815,8 @@ void iomap_invalidate_folio(struct folio *folio, size_t offset, size_t len) WARN_ON_ONCE(folio_test_writeback(folio)); folio_cancel_dirty(folio); ifs_free(folio); + } else { + iomap_clear_range_dirty(folio, offset, len); } } EXPORT_SYMBOL_GPL(iomap_invalidate_folio); From 7a6fd6b21d7e1737b40de1a210acf9e6a1e4d59e Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Tue, 14 Jul 2026 16:23:23 +0800 Subject: [PATCH 16/24] iomap: fix incorrect did_zero setting in iomap_zero_iter() The did_zero output parameter was unconditionally set after the loop, which is incorrect. It should only be set when the zeroing operation actually completes, not when IOMAP_F_STALE is set or when IOMAP_F_FOLIO_BATCH is set but !folio causes the loop to break early, or when iomap_iter_advance() returns an error. This causes did_zero to be incorrectly set when zeroing a clean unwritten extent because the loop exits early without actually zeroing any data. Fix it by using a local variable to track whether any folio was actually zeroed, and only set did_zero after the loop if zeroing happened. Fixes: 98eb8d95025b ("iomap: set did_zero to true when zeroing successfully") Signed-off-by: Zhang Yi Link: https://patch.msgid.link/20260714082325.325163-4-yi.zhang@huaweicloud.com Reviewed-by: "Darrick J. Wong" Reviewed-by: Christoph Hellwig Signed-off-by: Christian Brauner (Amutable) --- fs/iomap/buffered-io.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c index b482e112321f..0cf62e516827 100644 --- a/fs/iomap/buffered-io.c +++ b/fs/iomap/buffered-io.c @@ -1625,6 +1625,7 @@ static int iomap_zero_iter(struct iomap_iter *iter, bool *did_zero, const struct iomap_write_ops *write_ops) { u64 bytes = iomap_length(iter); + bool zeroed = false; int status; do { @@ -1645,6 +1646,8 @@ static int iomap_zero_iter(struct iomap_iter *iter, bool *did_zero, /* a NULL folio means we're done with a folio batch */ if (!folio) { status = iomap_iter_advance_full(iter); + if (status) + return status; break; } @@ -1655,6 +1658,7 @@ static int iomap_zero_iter(struct iomap_iter *iter, bool *did_zero, bytes); folio_zero_range(folio, offset, bytes); + zeroed = true; folio_mark_accessed(folio); ret = iomap_write_end(iter, bytes, bytes, folio); @@ -1664,10 +1668,10 @@ static int iomap_zero_iter(struct iomap_iter *iter, bool *did_zero, status = iomap_iter_advance(iter, bytes); if (status) - break; + return status; } while ((bytes = iomap_length(iter)) > 0); - if (did_zero) + if (did_zero && zeroed) *did_zero = true; return status; } From 9c7d8f7c8994c790fca501dc45ce66e7356cbe05 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Tue, 14 Jul 2026 16:23:24 +0800 Subject: [PATCH 17/24] iomap: fix out-of-bounds bitmap_set() with zero-length range ifs_set_range_dirty() and ifs_set_range_uptodate() compute last_blk as (off + len - 1) >> i_blkbits. When off is 0 and len is 0, the unsigned subtraction underflows to SIZE_MAX, producing a huge last_blk and nr_blks value that causes bitmap_set() to write far beyond the ifs->state allocation. Regarding ifs_set_range_uptodate(), it is temporarily safe because len cannot be passed in as 0. However, for ifs_set_range_dirty() this is reachable from __iomap_write_end(): when copy_folio_from_iter_atomic() returns 0 (e.g. user buffer fault) and the folio is already uptodate, the guard at the top of __iomap_write_end() does not trigger because !folio_test_uptodate() is false, and iomap_set_range_dirty() is called with copied == 0. Add a !len guard to both functions before the computation, so that a zero-length range is a no-op. Fixes: 4ce02c679722 ("iomap: Add per-block dirty state tracking to improve performance") Cc: stable@vger.kernel.org # v6.6 Signed-off-by: Zhang Yi Link: https://patch.msgid.link/20260714082325.325163-5-yi.zhang@huaweicloud.com Reviewed-by: Joanne Koong Reviewed-by: "Darrick J. Wong" Reviewed-by: Christoph Hellwig Signed-off-by: Christian Brauner (Amutable) --- fs/iomap/buffered-io.c | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c index 0cf62e516827..3a3ac3051fb0 100644 --- a/fs/iomap/buffered-io.c +++ b/fs/iomap/buffered-io.c @@ -68,11 +68,13 @@ static bool ifs_set_range_uptodate(struct folio *folio, struct iomap_folio_state *ifs, size_t off, size_t len) { struct inode *inode = folio->mapping->host; - unsigned int first_blk = off >> inode->i_blkbits; - unsigned int last_blk = (off + len - 1) >> inode->i_blkbits; - unsigned int nr_blks = last_blk - first_blk + 1; + unsigned int first_blk, last_blk; - bitmap_set(ifs->state, first_blk, nr_blks); + if (len) { + first_blk = off >> inode->i_blkbits; + last_blk = (off + len - 1) >> inode->i_blkbits; + bitmap_set(ifs->state, first_blk, last_blk - first_blk + 1); + } return ifs_is_fully_uptodate(folio, ifs); } @@ -204,13 +206,17 @@ static void ifs_set_range_dirty(struct folio *folio, { struct inode *inode = folio->mapping->host; unsigned int blks_per_folio = i_blocks_per_folio(inode, folio); - unsigned int first_blk = (off >> inode->i_blkbits); - unsigned int last_blk = (off + len - 1) >> inode->i_blkbits; - unsigned int nr_blks = last_blk - first_blk + 1; + unsigned int first_blk, last_blk; unsigned long flags; + if (!len) + return; + + first_blk = off >> inode->i_blkbits; + last_blk = (off + len - 1) >> inode->i_blkbits; spin_lock_irqsave(&ifs->state_lock, flags); - bitmap_set(ifs->state, first_blk + blks_per_folio, nr_blks); + bitmap_set(ifs->state, first_blk + blks_per_folio, + last_blk - first_blk + 1); spin_unlock_irqrestore(&ifs->state_lock, flags); } From 09b53b0787ee80b71b1dcceb99d004a33e55b823 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Tue, 14 Jul 2026 16:23:25 +0800 Subject: [PATCH 18/24] iomap: add comments for ifs_clear/set_range_dirty() The range alignment strategy differs between ifs_clear_range_dirty() and ifs_set_range_dirty(). The former rounds inwards to clear only fully-covered blocks, while the latter rounds outwards to mark any partially-touched block as dirty. Add comments to document this asymmetry in block range calculation. Suggested-by: "Darrick J. Wong" Signed-off-by: Zhang Yi Link: https://patch.msgid.link/20260714082325.325163-6-yi.zhang@huaweicloud.com Reviewed-by: Joanne Koong Reviewed-by: Christoph Hellwig Signed-off-by: Christian Brauner (Amutable) --- fs/iomap/buffered-io.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c index 3a3ac3051fb0..6d9a2efd4bee 100644 --- a/fs/iomap/buffered-io.c +++ b/fs/iomap/buffered-io.c @@ -174,6 +174,13 @@ static unsigned iomap_find_dirty_range(struct folio *folio, u64 *range_start, return range_end - *range_start; } +/* + * Clear the per-block dirty bits for the range [@off, @off + @len) within a + * folio. The range is rounded inwards so that only blocks fully covered by + * the range are cleared. This is required for operations like folio + * invalidation, where we must ensure a block is fully clean before discarding + * it. + */ static void ifs_clear_range_dirty(struct folio *folio, struct iomap_folio_state *ifs, size_t off, size_t len) { @@ -201,6 +208,13 @@ static void iomap_clear_range_dirty(struct folio *folio, size_t off, size_t len) ifs_clear_range_dirty(folio, ifs, off, len); } +/* + * Set the per-block dirty bits for the range [@off, @off + @len) within a + * folio. The range is rounded outwards so that any block partially touched + * by the range is marked dirty. This ensures blocks containing even a + * single dirty byte will be included in subsequent writeback, preventing + * data loss when partial blocks are written. + */ static void ifs_set_range_dirty(struct folio *folio, struct iomap_folio_state *ifs, size_t off, size_t len) { From c97cd6f447d8727af3d457bca3a9283a77dd70f8 Mon Sep 17 00:00:00 2001 From: Zhang Yi Date: Mon, 13 Jul 2026 15:42:06 +0800 Subject: [PATCH 19/24] iomap: prevent ioend merge when io_private differs Different io_private values indicate distinct completion contexts that must not be merged together, as this could leak or corrupt the private data associated with each ioend. Signed-off-by: Zhang Yi Link: https://patch.msgid.link/20260713074206.1768006-1-yi.zhang@huaweicloud.com Reviewed-by: Christoph Hellwig Reviewed-by: Ojaswin Mujoo Signed-off-by: Christian Brauner (Amutable) --- fs/iomap/ioend.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fs/iomap/ioend.c b/fs/iomap/ioend.c index 0565328764c1..30468d51b5ad 100644 --- a/fs/iomap/ioend.c +++ b/fs/iomap/ioend.c @@ -385,6 +385,8 @@ static bool iomap_ioend_can_merge(struct iomap_ioend *ioend, if (ioend->io_bio.bi_status != next->io_bio.bi_status) return false; + if (ioend->io_private != next->io_private) + return false; if (next->io_flags & IOMAP_IOEND_BOUNDARY) return false; if ((ioend->io_flags & IOMAP_IOEND_NOMERGE_FLAGS) != From 62d9853aa4ce6e9797b6949804891be14b219752 Mon Sep 17 00:00:00 2001 From: David Howells Date: Mon, 13 Jul 2026 16:22:55 +0100 Subject: [PATCH 20/24] afs: Fix afs_edit_dir_remove() to get, not find, block 0 Fix afs_edit_dir_remove() to use afs_dir_get_block() to get block 0 rather than afs_dir_find_block() as the latter caches the found block in the afs_dir_iter and may[*] switch out the page it's on if another afs_dir_find_block() is done. This parallels what afs_edit_dir_add() does. [*] There's more than one block per page. Fixes: a5b5beebcf96 ("afs: Use the contained hashtable to search a directory") Closes: https://sashiko.dev/#/patchset/20260706153408.1231650-1-dhowells%40redhat.com Signed-off-by: David Howells Link: https://patch.msgid.link/2380759.1783956175@warthog.procyon.org.uk cc: Marc Dionne cc: linux-afs@lists.infradead.org cc: linux-fsdevel@vger.kernel.org cc: stable@vger.kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/afs/dir_edit.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/afs/dir_edit.c b/fs/afs/dir_edit.c index fd3aa9f97ce6..3ead36a07048 100644 --- a/fs/afs/dir_edit.c +++ b/fs/afs/dir_edit.c @@ -415,7 +415,7 @@ void afs_edit_dir_remove(struct afs_vnode *vnode, if (!afs_dir_init_iter(&iter, name)) return; - meta = afs_dir_find_block(&iter, 0); + meta = afs_dir_get_block(&iter, 0); if (!meta) return; From 0ef8faff490be6aa1a1e5dfcb0c8492689e91c0f Mon Sep 17 00:00:00 2001 From: Usama Arif Date: Wed, 15 Jul 2026 03:35:16 -0700 Subject: [PATCH 21/24] fs: push nr_cached_objects memcg gating into individual filesystems Commit 0baad6f9b997 ("fs/super: skip non-memcg-aware nr_cached_objects in memcg slab shrink") added a check in fs/super.c that skipped every ->nr_cached_objects() hook whenever the shrinker was invoked for a non-root memcg, on the assumption that none of them honour sc->memcg. That assumption is wrong for XFS, whose inode-reclaim hook is intentionally driven from per-memcg contexts to free memcg-charged slab. Encoding a blanket "never memcg-aware" policy in fs/super.c short-circuits that path. Push the check down into the callbacks whose counters really are irrelevant to per-memcg reclaim - btrfs_nr_cached_objects() and shmem_unused_huge_count() - and drop the fs/super.c gate. Each filesystem can now lift the restriction independently if its counter later grows memcg awareness, without touching fs/super.c. Introduce mem_cgroup_shrink_is_root() in so the callbacks don't open-code "sc->memcg is NULL or root". Fixes: 0baad6f9b997 ("fs/super: skip non-memcg-aware nr_cached_objects in memcg slab shrink") Acked-by: Qi Zheng Reviewed-by: Jan Kara Reviewed-by: Shakeel Butt Signed-off-by: Usama Arif Link: https://patch.msgid.link/20260715103516.2410175-1-usama.arif@linux.dev Acked-by: David Sterba Reviewed-by: Baolin Wang Signed-off-by: Christian Brauner (Amutable) --- fs/btrfs/super.c | 10 ++++++++++ include/linux/memcontrol.h | 21 +++++++++++++++++++++ mm/shmem.c | 10 ++++++++++ 3 files changed, 41 insertions(+) diff --git a/fs/btrfs/super.c b/fs/btrfs/super.c index a7d804219bec..cc4537435399 100644 --- a/fs/btrfs/super.c +++ b/fs/btrfs/super.c @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -2434,6 +2435,15 @@ static long btrfs_nr_cached_objects(struct super_block *sb, struct shrink_contro struct btrfs_fs_info *fs_info = btrfs_sb(sb); const s64 nr = percpu_counter_read_positive(&fs_info->evictable_extent_maps); + /* + * The evictable extent map counter is filesystem-global and does not + * honour sc->memcg, so it is only meaningful on the global (kswapd or + * root direct reclaim) shrink path. Skip the per-memcg iterations of + * shrink_slab_memcg() to avoid queueing duplicate global work. + */ + if (!mem_cgroup_shrink_is_root(sc)) + return 0; + trace_btrfs_extent_map_shrinker_count(fs_info, nr); return nr; diff --git a/include/linux/memcontrol.h b/include/linux/memcontrol.h index e1f46a0016fc..5407e4200460 100644 --- a/include/linux/memcontrol.h +++ b/include/linux/memcontrol.h @@ -520,6 +520,22 @@ static inline bool mem_cgroup_is_root(struct mem_cgroup *memcg) return (memcg == root_mem_cgroup); } +/** + * mem_cgroup_shrink_is_root - is this a global or root-memcg shrink invocation? + * @sc: shrink_control describing the current shrinker call + * + * Returns true when @sc represents a global reclaim shrink (sc->memcg == NULL) + * or a root-memcg shrink, i.e. not a per-memcg iteration of + * shrink_slab_memcg(). Filesystems whose ->nr_cached_objects()/ + * ->free_cached_objects() implementations operate on filesystem-global state + * and do not honour sc->memcg can use this to early-return 0 in per-memcg + * contexts. + */ +static inline bool mem_cgroup_shrink_is_root(struct shrink_control *sc) +{ + return !sc->memcg || mem_cgroup_is_root(sc->memcg); +} + static inline bool obj_cgroup_is_root(const struct obj_cgroup *objcg) { return objcg->is_root; @@ -1071,6 +1087,11 @@ static inline bool mem_cgroup_is_root(struct mem_cgroup *memcg) return true; } +static inline bool mem_cgroup_shrink_is_root(struct shrink_control *sc) +{ + return true; +} + static inline bool obj_cgroup_is_root(const struct obj_cgroup *objcg) { return true; diff --git a/mm/shmem.c b/mm/shmem.c index b51f83c970bb..9001aaf3b7b9 100644 --- a/mm/shmem.c +++ b/mm/shmem.c @@ -846,6 +846,16 @@ static long shmem_unused_huge_count(struct super_block *sb, struct shrink_control *sc) { struct shmem_sb_info *sbinfo = SHMEM_SB(sb); + + /* + * The per-superblock shrinklist is filesystem-global and does not + * honour sc->memcg, so it is only meaningful on the global (kswapd or + * root direct reclaim) shrink path. Skip the per-memcg iterations of + * shrink_slab_memcg() to avoid queueing duplicate global work. + */ + if (!mem_cgroup_shrink_is_root(sc)) + return 0; + return READ_ONCE(sbinfo->shrinklist_len); } #else /* !CONFIG_TRANSPARENT_HUGEPAGE */ From 8b7e8245e2293078f657521236ac92c045552e5a Mon Sep 17 00:00:00 2001 From: Guidong Han <2045gemini@gmail.com> Date: Sat, 18 Jul 2026 18:44:06 +0800 Subject: [PATCH 22/24] eventpoll: pin files while checking reverse paths Commit 319c15174757 ("epoll: take epitem list out of struct file") intentionally removed temporary file references from the reverse path check list. At the time, both epitems and their files were freed after an RCU grace period, so unlist_file() could obtain file->f_lock through an epitem while clear_tfile_check_list() held rcu_read_lock(). Commit 0ede61d8589c ("file: convert to SLAB_TYPESAFE_BY_RCU") made struct file SLAB_TYPESAFE_BY_RCU and removed its RCU-delayed freeing. RCU still protects the epitem, but no longer keeps the referenced file from being freed and reused. A concurrent close can therefore make unlist_file() lock or unlock f_lock in a recycled file object. This violates the documented SLAB_TYPESAFE_BY_RCU rule requiring a reference before acquiring an object's lock. The race was reproduced, causing a wild unlock of f_lock in a recycled file and breaking its mutual exclusion. Add ->file to epitems_head to remember the pinned file independently of ->epitems. A concurrent EPOLL_CTL_DEL can empty ->epitems before the head is unlisted, leaving no epi->ffd.file from which to drop the reference. In list_file(), acquire the reference before adding the head to the check list. The caller either owns a reference or holds the ep->mtx for the epitem leading to the file. In the latter case, file_ref_get() can fail after the last reference is dropped, but eventpoll_release_file() must acquire the same mutex before the file can be freed. The dying leaf can be skipped because removing links cannot increase the reverse path count. In unlist_file(), epnested_mutex excludes another list_file() or unlist_file(), while head->next prevents a concurrent EPOLL_CTL_DEL from freeing the head. Save head->file locally, clear it with head->next under f_lock, and drop the reference after the RCU-protected operation. Christian Brauner quotes: > SLAB_TYPESAFE_BY_RCU allows a slab slot to be reused while an RCU reader > still holds its old address. Once that address contains a new live > struct file, KASAN sees valid, unpoisoned memory and cannot distinguish > the stale object identity. CONFIG_DEBUG_SPINLOCK exposes the failure > instead. > > The failing interleaving is: > > CPU0: nested EPOLL_CTL_ADD CPU1: close/open churn > ------------------------------------ --------------------------------- > p = hlist_first_rcu(&head->epitems) > epi = container_of(p, ...) > close(victim) > __fput() > eventpoll_release_file() > file_free(victim) > // the slot is free; f_lock remains > spin_lock(&epi->ffd.file->f_lock) > open() reuses the slot as new_file > spin_lock_init(&new_file->f_lock) > spin_unlock(&epi->ffd.file->f_lock) // wild unlock of new_file's lock > > CONFIG_DEBUG_SPINLOCK reports: > > BUG: spinlock already unlocked on CPU#0, poc_unlist/150 > lock: 0xffff8880067fb200, .magic: dead4ead, .owner: /-1, .owner_cpu: -1 > CPU: 0 UID: 1000 PID: 150 Comm: poc_unlist Not tainted 7.2.0-rc3-dirty #22 PREEMPTLAZY > Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014 > Call Trace: > > dump_stack_lvl+0x64/0x80 > do_raw_spin_unlock+0x75/0xb0 > _raw_spin_unlock+0xe/0x30 > clear_tfile_check_list+0x88/0xe0 > do_epoll_ctl_file+0x519/0xcf0 > ? __pfx_ep_ptable_queue_proc+0x10/0x10 > do_epoll_ctl+0x8f/0x100 > __x64_sys_epoll_ctl+0x6f/0xa0 > do_syscall_64+0xdc/0x520 > ? srso_alias_return_thunk+0x5/0xfbef5 > entry_SYSCALL_64_after_hwframe+0x76/0x7e > RIP: 0033:0x42034e > Code: 48 c7 c1 b8 ff ff ff f7 d8 64 89 01 48 83 c8 ff c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 49 89 ca b8 e9 00 00 00 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 b8 ff ff ff f7 d8 64 89 01 48 > RSP: 002b:00007a657ff3c198 EFLAGS: 00000202 ORIG_RAX: 00000000000000e9 > RAX: ffffffffffffffda RBX: 00007a657ff3ccdc RCX: 000000000042034e > RDX: 0000000000000003 RSI: 0000000000000001 RDI: 0000000000000004 > RBP: 00007a657ff3c2f0 R08: 0000000000000000 R09: 00007a657ff3c6c0 > R10: 00007a657ff3c1a4 R11: 0000000000000202 R12: 00007a657ff3c6c0 > R13: ffffffffffffffb8 R14: 000000000000000d R15: 00007fffb7de0210 > > ------------[ cut here ]------------ > > unlist_file() does not appear as a separate frame because it was inlined > into clear_tfile_check_list(). This report was obtained with mdelay() > instrumentation immediately before spin_lock() and spin_unlock() in > unlist_file() to widen the two race windows. > > More importantly, this is a wild unlock. The stale unlock can target > f_lock of a different live file and invalidate mutual exclusion for > state protected by that lock. Turning this into a reliable exploit > would require precise scheduling and same-slot reuse and is likely > difficult, but the primitive is potentially exploitable. Reported-by: Qi Tang Reported-by: Junxi Qian Fixes: 0ede61d8589c ("file: convert to SLAB_TYPESAFE_BY_RCU") Cc: stable@vger.kernel.org Signed-off-by: Guidong Han <2045gemini@gmail.com> Link: https://patch.msgid.link/20260718104406.27897-1-2045gemini@gmail.com Signed-off-by: Christian Brauner (Amutable) --- fs/eventpoll.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/fs/eventpoll.c b/fs/eventpoll.c index 0e65c7431dfc..eed8cecd94e3 100644 --- a/fs/eventpoll.c +++ b/fs/eventpoll.c @@ -459,11 +459,14 @@ static struct kmem_cache *pwq_cache __ro_after_init; * Wrapper anchor for file->f_ep when the watched file is not itself an * eventpoll; for the epoll-watches-epoll case, file->f_ep points at * &watched_ep->refs directly. The ->next field threads - * ctx->tfile_check_list during one EPOLL_CTL_ADD path check. + * ctx->tfile_check_list during one EPOLL_CTL_ADD path check. The ->file + * field holds a reference to the associated file while the head is on + * the list. */ struct epitems_head { struct hlist_head epitems; struct epitems_head *next; + struct file *file; }; static struct kmem_cache *ephead_cache __ro_after_init; @@ -480,6 +483,16 @@ static void list_file(struct file *file, struct ep_ctl_ctx *ctx) head = container_of(file->f_ep, struct epitems_head, epitems); if (!head->next) { + /* + * The caller owns a reference to @file or holds the ep->mtx for the + * epitem that led here. The latter blocks eventpoll_release_file() + * before the file allocation can be freed and reused. A dying leaf + * can be skipped since removing links cannot increase the reverse + * path count. + */ + if (!file_ref_get(&file->f_ref)) + return; + head->file = file; head->next = ctx->tfile_check_list; ctx->tfile_check_list = head; } @@ -489,15 +502,18 @@ static void unlist_file(struct epitems_head *head) { struct epitems_head *to_free = head; struct hlist_node *p = rcu_dereference(hlist_first_rcu(&head->epitems)); + struct file *file = head->file; if (p) { struct epitem *epi= container_of(p, struct epitem, fllink); spin_lock(&epi->ffd.file->f_lock); if (!hlist_empty(&head->epitems)) to_free = NULL; head->next = NULL; + head->file = NULL; spin_unlock(&epi->ffd.file->f_lock); } free_ephead(to_free); + fput(file); } #ifdef CONFIG_SYSCTL From c1d04c1bce98f9dd984a9c6657278a7761854c9c Mon Sep 17 00:00:00 2001 From: Mateusz Guzik Date: Thu, 23 Jul 2026 18:01:13 +0200 Subject: [PATCH 23/24] pidfs: make pidfs_ino_lock static Fixes: 87caaeef7995 ("pidfs: implement ino allocation without the pidmap lock") Reported-by: kernel test robot Closes: https://lore.kernel.org/oe-kbuild-all/202607231547.ehCQxi0L-lkp@intel.com/ Signed-off-by: Mateusz Guzik Link: https://patch.msgid.link/20260723160114.291515-1-mjguzik@gmail.com Signed-off-by: Christian Brauner (Amutable) --- fs/pidfs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/pidfs.c b/fs/pidfs.c index d7fe9abdd6f1..b57ecc96e967 100644 --- a/fs/pidfs.c +++ b/fs/pidfs.c @@ -108,7 +108,7 @@ struct pidfs_attr { #if BITS_PER_LONG == 32 -DEFINE_SPINLOCK(pidfs_ino_lock); +static DEFINE_SPINLOCK(pidfs_ino_lock); static u64 pidfs_ino_nr = 1; static inline unsigned long pidfs_ino(u64 ino) From 749d7aa0377aae32af8c0a4ad43371e7bf830ab5 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 23 Jul 2026 11:37:05 +0200 Subject: [PATCH 24/24] super: fix emergency thaw deadlock on frozen block devices do_thaw_all_callback() calls bdev_thaw() while holding sb->s_umount exclusively. If the block device was frozen via bdev_freeze() dropping the last block layer freeze reference calls fs_bdev_thaw() which reacquires s_umount: do_thaw_all_callback(sb) super_lock_excl(sb) # holds sb->s_umount bdev_thaw(sb->s_bdev) mutex_lock(&bdev->bd_fsfreeze_mutex) # bd_fsfreeze_count drops 1 -> 0 bd_holder_ops->thaw == fs_bdev_thaw get_bdev_super(bdev) bdev_super_lock(bdev, true) super_lock(sb, true) down_write(&sb->s_umount) # same task: deadlock The emergency thaw worker deadlocks against itself holding both s_umount and bd_fsfreeze_mutex. That fscks any subsequent unmount, freeze, or thaw of that filesystem and block device. [ 81.878470] sysrq: Show Blocked State [ 81.880140] task:kworker/0:1 state:D stack:0 pid:11 tgid:11 ppid:2 task_flags:0x4208060 flags:0x00080000 [ 81.884876] Workqueue: events do_thaw_all [ 81.886656] Call Trace: [ 81.887759] [ 81.888763] __schedule+0x579/0x1420 [ 81.890372] schedule+0x3a/0x100 [ 81.891794] schedule_preempt_disabled+0x15/0x30 [ 81.893848] rwsem_down_write_slowpath+0x1ea/0x900 [ 81.895191] ? __pfx_do_thaw_all_callback+0x10/0x10 [ 81.896528] down_write+0xbd/0xc0 [ 81.897505] super_lock+0x91/0x180 [ 81.898457] ? __mutex_lock+0xa99/0x1140 [ 81.900748] ? __mutex_unlock_slowpath+0x1f/0x400 [ 81.902069] bdev_super_lock+0x5b/0x150 [ 81.903132] get_bdev_super+0x10/0x60 [ 81.904042] fs_bdev_thaw+0x23/0xf0 [ 81.904755] bdev_thaw+0x82/0x100 [ 81.905484] do_thaw_all_callback+0x2c/0x50 [ 81.906298] __iterate_supers+0x5d/0x130 [ 81.907067] do_thaw_all+0x20/0x40 [ 81.907739] process_one_work+0x206/0x5e0 [ 81.908545] worker_thread+0x1e2/0x3c0 [ 81.909339] ? __pfx_worker_thread+0x10/0x10 [ 81.910171] kthread+0xf4/0x130 [ 81.910799] ? __pfx_kthread+0x10/0x10 [ 81.911528] ret_from_fork+0x2e2/0x3b0 [ 81.912259] ? __pfx_kthread+0x10/0x10 [ 81.913010] ret_from_fork_asm+0x1a/0x30 [ 81.913806] bdev_super_lock() even documents the violated requirement with lockdep_assert_not_held(&sb->s_umount). Acquiring bd_fsfreeze_mutex under s_umount also inverts the bd_fsfreeze_mutex vs. s_umount ordering established by bdev_{freeze,thaw}() and can thus ABBA against a concurrent block-layer freeze even when the recursive path isn't hit. Fix this by not holding s_umount around the bdev_thaw() loop at all. Pin the superblock with an active reference instead as filesystems_freeze_callback() does. The active reference keeps the superblock from being shut down and so ->s_bdev stays valid without holding s_umount. The block-layer-held freeze is dropped by fs_bdev_thaw() with FREEZE_MAY_NEST | FREEZE_HOLDER_USERSPACE exactly as a regular unfreeze would and thaw_super_locked() handles filesystem-level freezes as before. The emergency thaw path has deadlocked like this in one form or another for a long long time but the current exclusively-held shape dates back to commit [1] where thaw_bdev() already ended in thaw_super() with s_umount held by do_thaw_all_callback(). Fixes: 08fdc8a0138a ("buffer.c: call thaw_super during emergency thaw") [1] Cc: stable@vger.kernel.org Link: https://patch.msgid.link/20260723-work-super-emergency_thaw-v1-1-7c315c600245@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/super.c | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/fs/super.c b/fs/super.c index 70dcb07e7fa5..ffdcc6a2e0de 100644 --- a/fs/super.c +++ b/fs/super.c @@ -1082,16 +1082,30 @@ void emergency_remount(void) } } +static inline bool get_active_super(struct super_block *sb) +{ + bool active = false; + + if (super_lock_excl(sb)) { + active = atomic_inc_not_zero(&sb->s_active); + super_unlock_excl(sb); + } + return active; +} + static void do_thaw_all_callback(struct super_block *sb, void *unused) { - if (!super_lock_excl(sb)) + if (!get_active_super(sb)) return; + /* fs_bdev_thaw() acquires s_umount so it must not be held here */ if (IS_ENABLED(CONFIG_BLOCK)) while (sb->s_bdev && !bdev_thaw(sb->s_bdev)) pr_warn("Emergency Thaw on %pg\n", sb->s_bdev); - thaw_super_locked(sb, FREEZE_HOLDER_USERSPACE, NULL); + if (super_lock_excl(sb)) + thaw_super_locked(sb, FREEZE_HOLDER_USERSPACE, NULL); + deactivate_super(sb); } static void do_thaw_all(struct work_struct *work) @@ -1117,17 +1131,6 @@ void emergency_thaw_all(void) } } -static inline bool get_active_super(struct super_block *sb) -{ - bool active = false; - - if (super_lock_excl(sb)) { - active = atomic_inc_not_zero(&sb->s_active); - super_unlock_excl(sb); - } - return active; -} - static const char *filesystems_freeze_ptr = "filesystems_freeze"; static void filesystems_freeze_callback(struct super_block *sb, void *freeze_all_ptr)