From 1711b6ed6953cee5940ca4c3a6e77f1b3798cee2 Mon Sep 17 00:00:00 2001 From: Nirmoy Das Date: Thu, 14 May 2026 07:42:57 -0700 Subject: [PATCH 01/10] ovl: keep err zero after successful ovl_cache_get() ovl_iterate_merged() stores PTR_ERR(cache) in err before checking IS_ERR(cache). On success err holds the truncated cache pointer and can be returned as a bogus non-zero error. The syzbot reproducer reaches this through overlay-on-overlay readdir: getdents64 iterate_dir(outer overlay file) ovl_iterate_merged() ovl_cache_get() ovl_dir_read_merged() ovl_dir_read() iterate_dir(inner overlay file) ovl_iterate_merged() Only compute PTR_ERR(cache) on the error path. Fixes: d25e4b739f83 ("ovl: refactor ovl_iterate() and port to cred guard") Reported-by: syzbot+a16fb0cce329a320661c@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=a16fb0cce329a320661c Cc: stable@vger.kernel.org Signed-off-by: Nirmoy Das Link: https://patch.msgid.link/20260514144258.3068715-1-nirmoyd@nvidia.com Signed-off-by: Christian Brauner --- fs/overlayfs/readdir.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/fs/overlayfs/readdir.c b/fs/overlayfs/readdir.c index 1dcc75b3a90f..e7fe29cb6028 100644 --- a/fs/overlayfs/readdir.c +++ b/fs/overlayfs/readdir.c @@ -838,15 +838,14 @@ static int ovl_iterate_merged(struct file *file, struct dir_context *ctx) struct ovl_dir_file *od = file->private_data; struct dentry *dentry = file->f_path.dentry; struct ovl_cache_entry *p; - int err = 0; + int err; if (!od->cache) { struct ovl_dir_cache *cache; cache = ovl_cache_get(dentry); - err = PTR_ERR(cache); if (IS_ERR(cache)) - return err; + return PTR_ERR(cache); od->cache = cache; ovl_seek_cursor(od, ctx->pos); @@ -869,7 +868,7 @@ static int ovl_iterate_merged(struct file *file, struct dir_context *ctx) od->cursor = p->l_node.next; ctx->pos++; } - return err; + return 0; } static bool ovl_need_adjust_d_ino(struct file *file) From 62c4d31d78294bd61cf3403626b789e854357177 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 18 May 2026 10:32:11 +0200 Subject: [PATCH 02/10] pidfd: refuse access to tasks that have started exiting harder The recent ptrace fix closed a hole where someone could rely on task->mm becoming NULL during do_exit() to bypass dumpability checks. This api here leans on on the very same check and so inherits the fix. But there is no good reason to let it succeed at all once the target has entered do_exit(). PF_EXITING is set by exit_signals() at the very top of do_exit(), before exit_mm() and exit_files() run. Once we observe it, the task is committed to dying and exit_files() will release the fdtable shortly. Fixes: 8649c322f75c ("pid: Implement pidfd_getfd syscall") Cc: stable@vger.kernel.org Link: https://patch.msgid.link/20260518-obgleich-petersilie-2d77ccccf9b9@brauner Signed-off-by: Christian Brauner (Amutable) --- kernel/pid.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/kernel/pid.c b/kernel/pid.c index fd5c2d4aa349..f55189a3d07d 100644 --- a/kernel/pid.c +++ b/kernel/pid.c @@ -885,10 +885,12 @@ static struct file *__pidfd_fget(struct task_struct *task, int fd) if (ret) return ERR_PTR(ret); - if (ptrace_may_access(task, PTRACE_MODE_ATTACH_REALCREDS)) - file = fget_task(task, fd); - else + if (!ptrace_may_access(task, PTRACE_MODE_ATTACH_REALCREDS)) file = ERR_PTR(-EPERM); + else if (task->flags & PF_EXITING) + file = ERR_PTR(-ESRCH); + else + file = fget_task(task, fd); up_read(&task->signal->exec_update_lock); From 0eb307d61317b42b120ab02099b597226318358a Mon Sep 17 00:00:00 2001 From: Geliang Tang Date: Mon, 20 Apr 2026 17:45:28 +0800 Subject: [PATCH 03/10] selftests: harness: fix pidfd leak in __wait_for_test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix the pidfd leak in kselftest_harness.h's __wait_for_test() where childfd = syscall(__NR_pidfd_open, t->pid, 0) is never closed. Fixes: 73a3cde97677 ("selftests: harness: Implement test timeouts through pidfd") Signed-off-by: Geliang Tang Link: https://patch.msgid.link/a82e275ccfb2609a1984d90ab559fa3af78f1e81.1776678050.git.tanggeliang@kylinos.cn Reviewed-by: Thomas Weißschuh Signed-off-by: Christian Brauner (Amutable) --- tools/testing/selftests/kselftest_harness.h | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/testing/selftests/kselftest_harness.h b/tools/testing/selftests/kselftest_harness.h index cfdce9cd252e..261e4df94d9d 100644 --- a/tools/testing/selftests/kselftest_harness.h +++ b/tools/testing/selftests/kselftest_harness.h @@ -996,6 +996,7 @@ static void __wait_for_test(struct __test_metadata *t) poll_child.fd = childfd; poll_child.events = POLLIN; ret = poll(&poll_child, 1, t->timeout * 1000); + close(childfd); if (ret == -1) { t->exit_code = KSFT_FAIL; fprintf(TH_LOG_STREAM, From 4e3d1b2c48ca6c55f1e9ca7f8dccc76f120f276c Mon Sep 17 00:00:00 2001 From: Jann Horn Date: Tue, 19 May 2026 16:40:34 +0200 Subject: [PATCH 04/10] fuse: limit FUSE_NOTIFY_RETRIEVE to uptodate folios FUSE_NOTIFY_RETRIEVE must be limited to uptodate folios; !uptodate folios can contain uninitialized data. Since FUSE_NOTIFY_RETRIEVE is intended to only return data that is already in the page cache and not wait for data from the FUSE daemon, treat !uptodate folios as if they weren't present. This only has security impact on systems that don't enable automatic zero-initialization of all page allocations via CONFIG_INIT_ON_ALLOC_DEFAULT_ON or init_on_alloc=1. Cc: stable@kernel.org Fixes: 2d45ba381a74 ("fuse: add retrieve request") Signed-off-by: Jann Horn Link: https://patch.msgid.link/20260519-fuse-retrieve-uptodate-v1-1-a7a1912a37f9@google.com Acked-by: Miklos Szeredi Signed-off-by: Christian Brauner (Amutable) --- fs/fuse/dev.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index 5dda7080f4a9..08d364ed7d6c 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -1912,6 +1912,10 @@ static int fuse_retrieve(struct fuse_mount *fm, struct inode *inode, folio = filemap_get_folio(mapping, index); if (IS_ERR(folio)) break; + if (!folio_test_uptodate(folio)) { + folio_put(folio); + break; + } folio_offset = offset_in_folio(folio, pos); nr_bytes = min(folio_size(folio) - folio_offset, num); From 9c954499d43aefac01c5dfb57a82b13d2dcf4b94 Mon Sep 17 00:00:00 2001 From: Jann Horn Date: Tue, 19 May 2026 16:29:38 +0200 Subject: [PATCH 05/10] fuse: reject fuse_notify() pagecache ops on directories The operations FUSE_NOTIFY_STORE and FUSE_NOTIFY_RETRIEVE allow the FUSE daemon to actively write/read pagecache contents. For directories with FOPEN_CACHE_DIR, the pagecache is used as kernel-internal cache storage, and userspace is not supposed to have direct access to this cache - in particular, fuse_parse_cache() will hit WARN_ON() if the cache contains bogus data. Reject FUSE_NOTIFY_STORE and FUSE_NOTIFY_RETRIEVE on anything other than regular files with -EINVAL. Fixes: 5d7bc7e8680c ("fuse: allow using readdir cache") Cc: stable@vger.kernel.org Signed-off-by: Jann Horn Link: https://patch.msgid.link/20260519-fuse-dir-pagecache-v2-1-5428fa48e175@google.com Acked-by: Miklos Szeredi Signed-off-by: Christian Brauner (Amutable) --- fs/fuse/dev.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c index 08d364ed7d6c..c105aaf9ff5d 100644 --- a/fs/fuse/dev.c +++ b/fs/fuse/dev.c @@ -1793,6 +1793,10 @@ static int fuse_notify_store(struct fuse_conn *fc, unsigned int size, inode = fuse_ilookup(fc, nodeid, NULL); if (!inode) goto out_up_killsb; + if (!S_ISREG(inode->i_mode)) { + err = -EINVAL; + goto out_iput; + } mapping = inode->i_mapping; file_size = i_size_read(inode); @@ -1970,7 +1974,10 @@ static int fuse_notify_retrieve(struct fuse_conn *fc, unsigned int size, inode = fuse_ilookup(fc, nodeid, &fm); if (inode) { - err = fuse_retrieve(fm, inode, &outarg); + if (!S_ISREG(inode->i_mode)) + err = -EINVAL; + else + err = fuse_retrieve(fm, inode, &outarg); iput(inode); } up_read(&fc->killsb); From 90918794a4e2c3b440f8fcf3847765a8b1d81b25 Mon Sep 17 00:00:00 2001 From: Aleksandr Nogikh Date: Thu, 21 May 2026 16:22:40 +0200 Subject: [PATCH 06/10] signal: clear JOBCTL_PENDING_MASK for caller in zap_other_threads() When a multi-threaded process receives a stop signal (e.g., SIGSTOP), do_signal_stop() sets JOBCTL_STOP_PENDING and JOBCTL_STOP_CONSUME on all threads and sets signal->group_stop_count to the number of threads. If one of the threads concurrently calls execve(), de_thread() invokes zap_other_threads() to kill all other threads. zap_other_threads() aborts the pending group stop by resetting signal->group_stop_count to 0 and clears the JOBCTL_PENDING_MASK for all other threads. However, it fails to clear the job control flags for the calling thread. When execve() completes, the calling thread returns to user mode and checks for pending signals. Seeing the stale JOBCTL_STOP_PENDING flag, it calls do_signal_stop(), which invokes task_participate_group_stop(). Since JOBCTL_STOP_CONSUME is still set, it attempts to decrement the already-zero signal->group_stop_count, triggering a warning: sig->group_stop_count == 0 WARNING: CPU: 1 PID: 6475 at kernel/signal.c:373 task_participate_group_stop+0x215/0x2d0 Call Trace: do_signal_stop+0x3be/0x5c0 kernel/signal.c:2619 get_signal+0xa8c/0x1330 kernel/signal.c:2884 arch_do_signal_or_restart+0xbc/0x840 arch/x86/kernel/signal.c:337 exit_to_user_mode_loop+0x8c/0x4d0 kernel/entry/common.c:98 do_syscall_64+0x33e/0xf80 arch/x86/entry/syscall_64.c:100 entry_SYSCALL_64_after_hwframe+0x77/0x7f Fix this race condition by clearing the JOBCTL_PENDING_MASK for the calling thread in zap_other_threads(), ensuring it does not retain any stale job control state after the thread group is destroyed. This aligns with other functions that tear down a thread group and abort group stops, such as zap_process() and complete_signal(), which correctly clear these flags for all threads including the current one. Fixes: 39efa3ef3a37 ("signal: Use GROUP_STOP_PENDING to stop once for a single group stop") Assisted-by: Gemini:gemini-3.1-pro-preview Gemini:gemini-3-flash-preview syzbot Reported-by: syzbot+b109633ea805cac54a61@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=b109633ea805cac54a61 Link: https://syzkaller.appspot.com/ai_job?id=d70208cc-862b-4fe3-bf02-3031e10cd0b3 Signed-off-by: Aleksandr Nogikh Link: https://patch.msgid.link/20260521142240.2973022-1-nogikh@google.com Signed-off-by: Christian Brauner (Amutable) --- kernel/signal.c | 1 + 1 file changed, 1 insertion(+) diff --git a/kernel/signal.c b/kernel/signal.c index 2d102e025883..9c2b32c4d755 100644 --- a/kernel/signal.c +++ b/kernel/signal.c @@ -1338,6 +1338,7 @@ int zap_other_threads(struct task_struct *p) int count = 0; p->signal->group_stop_count = 0; + task_clear_jobctl_pending(p, JOBCTL_PENDING_MASK); for_other_threads(p, t) { task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK); From e824bbd4d224cce4b5fb59cc9dcd3447fe0b7e44 Mon Sep 17 00:00:00 2001 From: NeilBrown Date: Mon, 25 May 2026 16:23:45 +1000 Subject: [PATCH 07/10] VFS: fix possible failure to unlock in nfsd4_create_file() atomic_create() in fs/namei.c drops the reference to the dentry when it returns an error. This behaviour was imported into dentry_create() so that it will drop the reference if an error is returned from atomic_create(), though not if vfs_create() returns an error (in the case where ->atomic_create is not supported). The caller - nfsd4_create_file() - is made aware of this by checking path->dentry, which will either be a counted reference to a dentry, or an error pointer. However the change to use start_creating()/end_creating() (which landed shortly before the dentry_create() change landed, though was likely developed around the same time) means that nfsd4_create_file() *needs* a valid dentry so that it can unlock the parent. The net result is that if NFSD exports a filesystem which uses ->atomic_create, and if a call to ->atomic_create returns an error, then nfsd4_create_file() will pass an error pointer to end_creating() and the parent will not be unlocked. Fix this by changing dentry_create() to make sure path->dentry is always a valid dentry, never an error-pointer. The actual error is already returned a different way. Note that if ->atomic_create() returns a different dentry (which may not be possible in practice) we are guaranteed (because it is only ever provided by d_spliace_alias()) that it will have the same d_parent and so it will have the same effect when passed to end_creating(). Fixes: 64a989dbd144 ("VFS/knfsd: Teach dentry_create() to use atomic_open()") Signed-off-by: NeilBrown Link: https://patch.msgid.link/177969022571.3379282.16448744624428323496@noble.neil.brown.name Reviewed-by: Jeff Layton Reviewed-by: Benjamin Coddington Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/namei.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/fs/namei.c b/fs/namei.c index c7fac83c9a85..4787244ca4a7 100644 --- a/fs/namei.c +++ b/fs/namei.c @@ -5024,6 +5024,7 @@ struct file *dentry_create(struct path *path, int flags, umode_t mode, { struct file *file __free(fput) = NULL; struct dentry *dentry = path->dentry; + struct dentry *orig_dentry = dentry; struct dentry *dir = dentry->d_parent; struct inode *dir_inode = d_inode(dir); struct mnt_idmap *idmap; @@ -5043,9 +5044,18 @@ struct file *dentry_create(struct path *path, int flags, umode_t mode, if (create_error) flags &= ~O_CREAT; + /* atomic_open will dput(dentry) on error */ + dget(orig_dentry); dentry = atomic_open(path, dentry, file, flags, mode); error = PTR_ERR_OR_ZERO(dentry); + if (IS_ERR(dentry)) + /* keep the original */ + dentry = orig_dentry; + else + /* Drop the extra reference */ + dput(orig_dentry); + if (unlikely(create_error) && error == -ENOENT) error = create_error; From 89c4a1167f3a0a0efd2ec3e1801036d2eb65ae1a Mon Sep 17 00:00:00 2001 From: Arpith Kalaginanavoor Date: Tue, 26 May 2026 05:38:58 -0700 Subject: [PATCH 08/10] fs/qnx6: fix pointer arithmetic in directory iteration The conversion to qnx6_get_folio() in commit b2aa61556fcf ("qnx6: Convert qnx6_get_page() to qnx6_get_folio()") introduced a regression in directory iteration. The pointer 'de' and the 'limit' address were calculated using byte offsets from a char pointer without scaling by the size of a QNX6 directory entry. This causes the driver to read from incorrect memory offsets, leading to "invalid direntry size" errors and premature termination of directory scans. Fix this by casting 'kaddr' to 'struct qnx6_dir_entry *' before applying the offset and last_entry(...) increments. This allows the compiler to correctly scale the pointer arithmetic by the 32-byte stride of the directory entry structure. Fixes: b2aa61556fcf ("qnx6: Convert qnx6_get_page() to qnx6_get_folio()") Cc: stable@vger.kernel.org Signed-off-by: Arpith Kalaginanavoor Link: https://patch.msgid.link/20260526123858.1683035-1-arpithk@nvidia.com Signed-off-by: Christian Brauner (Amutable) --- fs/qnx6/dir.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fs/qnx6/dir.c b/fs/qnx6/dir.c index 135fb42f6936..56bbaffe4b44 100644 --- a/fs/qnx6/dir.c +++ b/fs/qnx6/dir.c @@ -132,16 +132,16 @@ static int qnx6_readdir(struct file *file, struct dir_context *ctx) struct qnx6_dir_entry *de; struct folio *folio; char *kaddr = qnx6_get_folio(inode, n, &folio); - char *limit; + struct qnx6_dir_entry *limit; if (IS_ERR(kaddr)) { pr_err("%s(): read failed\n", __func__); ctx->pos = (n + 1) << PAGE_SHIFT; return PTR_ERR(kaddr); } - de = (struct qnx6_dir_entry *)(kaddr + offset); - limit = kaddr + last_entry(inode, n); - for (; (char *)de < limit; de++, ctx->pos += QNX6_DIR_ENTRY_SIZE) { + de = (struct qnx6_dir_entry *)kaddr + offset; + limit = (struct qnx6_dir_entry *)kaddr + last_entry(inode, n); + for (; de < limit; de++, ctx->pos += QNX6_DIR_ENTRY_SIZE) { int size = de->de_size; u32 no_inode = fs32_to_cpu(sbi, de->de_inode); From 40ab6644b99685755f740b872c00ef40d9aa870e Mon Sep 17 00:00:00 2001 From: Jann Horn Date: Wed, 3 Jun 2026 21:31:57 +0200 Subject: [PATCH 09/10] fhandle: fix UAF due to unlocked ->mnt_ns read in may_decode_fh() may_decode_fh() accesses mount::mnt_ns without holding any locks; that means the mount can concurrently be unmounted, and the mnt_namespace can concurrently be freed after an RCU grace period. This race can happens as follows, assuming that the mount point was created by open_tree(..., OPEN_TREE_CLONE): thread 1 thread 2 RCU __do_sys_open_by_handle_at do_handle_open handle_to_path may_decode_fh is_mounted [mount::mnt_ns access] [mount::mnt_ns access] __do_sys_close fput_close_sync __fput dissolve_on_fput umount_tree class_namespace_excl_destructor namespace_unlock free_mnt_ns mnt_ns_tree_remove call_rcu(mnt_ns_release_rcu) mnt_ns_release_rcu mnt_ns_release kfree [mnt_namespace::user_ns access] **UAF** Fix it by taking rcu_read_lock() around the mount::mnt_ns access, like in __prepend_path(). Additionally, document the semantics of mount::mnt_ns, and use WRITE_ONCE() for writers that can race with lockless readers. This bug is unreachable unless one of the following is set: - CONFIG_PREEMPTION - CONFIG_RCU_STRICT_GRACE_PERIOD because it requires an RCU grace period to happen during a syscall without an explicit preemption. This doesn't seem to have interesting security impact; worst-case, it could leak the result of an integer comparison to userspace (from the level check in cap_capable()), cause an endless loop, or crash the kernel by dereferencing an invalid address. Fixes: 620c266f3949 ("fhandle: relax open_by_handle_at() permission checks") Cc: stable@vger.kernel.org Signed-off-by: Jann Horn Link: https://patch.msgid.link/20260603-vfs-fhandle-uaf-fix-v2-1-d05db76a5084@google.com Signed-off-by: Christian Brauner (Amutable) --- fs/fhandle.c | 16 ++++++++++++++-- fs/mount.h | 10 +++++++++- fs/namespace.c | 6 +++--- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/fs/fhandle.c b/fs/fhandle.c index 642e3d569497..1ca7eb3a6cb5 100644 --- a/fs/fhandle.c +++ b/fs/fhandle.c @@ -285,6 +285,19 @@ static int do_handle_to_path(struct file_handle *handle, struct path *path, return 0; } +static bool capable_wrt_mount(struct mount *mount) +{ + struct mnt_namespace *mnt_ns; + + /* + * For ->mnt_ns access. + * The following READ_ONCE() is semantically rcu_dereference(). + */ + guard(rcu)(); + mnt_ns = READ_ONCE(mount->mnt_ns); + return ns_capable(mnt_ns->user_ns, CAP_SYS_ADMIN); +} + static inline int may_decode_fh(struct handle_to_path_ctx *ctx, unsigned int o_flags) { @@ -320,8 +333,7 @@ static inline int may_decode_fh(struct handle_to_path_ctx *ctx, if (ns_capable(root->mnt->mnt_sb->s_user_ns, CAP_SYS_ADMIN)) ctx->flags = HANDLE_CHECK_PERMS; else if (is_mounted(root->mnt) && - ns_capable(real_mount(root->mnt)->mnt_ns->user_ns, - CAP_SYS_ADMIN) && + capable_wrt_mount(real_mount(root->mnt)) && !has_locked_children(real_mount(root->mnt), root->dentry)) ctx->flags = HANDLE_CHECK_PERMS | HANDLE_CHECK_SUBTREE; else diff --git a/fs/mount.h b/fs/mount.h index e0816c11a198..5c120f8361bd 100644 --- a/fs/mount.h +++ b/fs/mount.h @@ -71,7 +71,15 @@ struct mount { struct hlist_head mnt_slave_list;/* list of slave mounts */ struct hlist_node mnt_slave; /* slave list entry */ struct mount *mnt_master; /* slave is on master->mnt_slave_list */ - struct mnt_namespace *mnt_ns; /* containing namespace */ + /* + * Containing namespace (active or deactivating, non-refcounted). + * Normally protected by namespace_sem. + * Can also be accessed locklessly under RCU. RCU readers can't rely on + * the namespace still being active, but implicitly hold a passive + * reference (because an RCU delay happens between a namespace being + * deactivated and the corresponding passive refcount drop). + */ + struct mnt_namespace *mnt_ns; struct mountpoint *mnt_mp; /* where is it mounted */ union { struct hlist_node mnt_mp_list; /* list mounts with the same mountpoint */ diff --git a/fs/namespace.c b/fs/namespace.c index fe919abd2f01..f5905f4ec560 100644 --- a/fs/namespace.c +++ b/fs/namespace.c @@ -1079,7 +1079,7 @@ static void mnt_add_to_ns(struct mnt_namespace *ns, struct mount *mnt) bool mnt_first_node = true, mnt_last_node = true; WARN_ON(mnt_ns_attached(mnt)); - mnt->mnt_ns = ns; + WRITE_ONCE(mnt->mnt_ns, ns); while (*link) { parent = *link; if (mnt->mnt_id_unique < node_to_mount(parent)->mnt_id_unique) { @@ -1434,7 +1434,7 @@ EXPORT_SYMBOL(mntget); void mnt_make_shortterm(struct vfsmount *mnt) { if (mnt) - real_mount(mnt)->mnt_ns = NULL; + WRITE_ONCE(real_mount(mnt)->mnt_ns, NULL); } /** @@ -1806,7 +1806,7 @@ static void umount_tree(struct mount *mnt, enum umount_tree_flags how) ns->nr_mounts--; __touch_mnt_namespace(ns); } - p->mnt_ns = NULL; + WRITE_ONCE(p->mnt_ns, NULL); if (how & UMOUNT_SYNC) p->mnt.mnt_flags |= MNT_SYNC_UMOUNT; From 2eea7f44b9c8b42fd7d3a1a87c06a7cd1b99c327 Mon Sep 17 00:00:00 2001 From: Joanne Koong Date: Wed, 3 Jun 2026 18:18:58 -0700 Subject: [PATCH 10/10] iomap: avoid potential null folio->mapping deref during error reporting When a buffered read fails, iomap_finish_folio_read() reports the error with fserror_report_io(folio->mapping->host, ...). This is called after ifs->read_bytes_pending has been decremented by the bytes attempted to be read. For a folio split across multiple read completions, the folio is only guaranteed to stay locked while read_bytes_pending > 0. Once iomap_finish_folio_read() decrements read_bytes_pending, another in-flight read can complete and end the read on the folio, which unlocks it. This allows truncate logic to run and detach the folio (set folio->mapping to NULL). The error reporting path then can dereference a NULL folio->mapping. As reported by Sam Sun, this is the race that can occur: CPU0: failed completion CPU1: final completion CPU2: truncate ----------------------- ---------------------- -------------- read_bytes_pending -= len finished = false /* preempted before fserror_report_io() */ read_bytes_pending -= len finished = true folio_end_read() truncate clears folio->mapping fserror_report_io( folio->mapping->host, ...) ^ NULL deref Fix this by reporting the error first before decrementing ifs->read_bytes_pending. Fixes: a9d573ee88af ("iomap: report file I/O errors to the VFS") Cc: stable@vger.kernel.org Reported-by: Sam Sun Closes: https://lore.kernel.org/linux-fsdevel/CAEkJfYPhWdd59RKmuNLJg-bkypHz7xiOwaWyNVu3A8CUqQCnvg@mail.gmail.com/ Signed-off-by: Joanne Koong Link: https://patch.msgid.link/20260604011858.2297561-1-joannelkoong@gmail.com Reviewed-by: "Darrick J. Wong" Signed-off-by: Christian Brauner (Amutable) --- fs/iomap/buffered-io.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c index d7b648421a70..d55b936e6986 100644 --- a/fs/iomap/buffered-io.c +++ b/fs/iomap/buffered-io.c @@ -400,6 +400,11 @@ void iomap_finish_folio_read(struct folio *folio, size_t off, size_t len, bool uptodate = !error; bool finished = true; + if (error) + fserror_report_io(folio->mapping->host, FSERR_BUFFERED_READ, + folio_pos(folio) + off, len, error, + GFP_ATOMIC); + if (ifs) { unsigned long flags; @@ -411,11 +416,6 @@ void iomap_finish_folio_read(struct folio *folio, size_t off, size_t len, spin_unlock_irqrestore(&ifs->state_lock, flags); } - if (error) - fserror_report_io(folio->mapping->host, FSERR_BUFFERED_READ, - folio_pos(folio) + off, len, error, - GFP_ATOMIC); - if (finished) folio_end_read(folio, uptodate); }