From 3af83c11cf5990fda0bfe3959aa6391ff903ffdd Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Thu, 8 Oct 2026 13:08:49 +0200 Subject: [PATCH 01/22] lib/base64: silence clang-24 -Wconstant-conversion with diag pragmas After a recent change in clang to warn on signed char conversions within array initializers [1], there are several instances of this warning from base64_rev_maps in lib/base64.c, which can break the build with W=e or CONFIG_WERROR=y: lib/base64.c:58:18: error: implicit conversion from 'int' to 's8' (aka 'signed char') changes value from 131 to -125 [-Werror,-Wconstant-conversion] 58 | [BASE64_IMAP] = BASE64_REV_INIT('+', ',') | ^~~~~~~~~~~~~~~~~~~~~~~~~ lib/base64.c:52:2: note: expanded from macro 'BASE64_REV_INIT' 48 | #define BASE64_REV_INIT(ch_62, ch_63) { \ | ~ 49 | [0 ... 0x1f] = -1, \ 50 | INIT_32(0x20, ch_62, ch_63), \ 51 | INIT_32(0x40, ch_62, ch_63), \ 52 | INIT_32(0x60, ch_62, ch_63), \ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ ... lib/base64.c:34:42: note: expanded from macro 'INIT_1' 34 | : (v) >= '0' && (v) <= '9' ? (v) - '0' + 52 \ | ~~~~~~~~~~^~~~ lib/base64.c:58:18: error: implicit conversion from 'int' to 's8' (aka 'signed char') changes value from 130 to -126 [-Werror,-Wconstant-conversion] 58 | [BASE64_IMAP] = BASE64_REV_INIT('+', ',') | ^~~~~~~~~~~~~~~~~~~~~~~~~ lib/base64.c:52:2: note: expanded from macro 'BASE64_REV_INIT' 48 | #define BASE64_REV_INIT(ch_62, ch_63) { \ | ~ 49 | [0 ... 0x1f] = -1, \ 50 | INIT_32(0x20, ch_62, ch_63), \ 51 | INIT_32(0x40, ch_62, ch_63), \ 52 | INIT_32(0x60, ch_62, ch_63), \ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ ... lib/base64.c:34:42: note: expanded from macro 'INIT_1' 34 | : (v) >= '0' && (v) <= '9' ? (v) - '0' + 52 \ | ~~~~~~~~~~^~~~ ... These are false positives, as the branch where the wraparound could happen is unreachable with the values that clang reports. This is not considered a bug by some clang folks [2][3], so silence the warnings using the __diag macros the kernel has to workaround compiler warnings when necessary. Link: https://lore.kernel.org/20261008-base64-silence-clang-24-constant-conversion-v1-1-0858b60b23c8@kernel.org Link: https://github.com/llvm/llvm-project/commit/a5ef934a8d295dc03be3960f2b3744ec2e53238e [1] Link: https://github.com/llvm/llvm-project/issues/223923#issuecomment-6056856245 [2] Link: https://github.com/llvm/llvm-project/pull/226775#pullrequestreview-5454407389 [3] Fixes: c4eb7ad32eab ("lib/base64: optimize base64_decode() with reverse lookup tables") Signed-off-by: Nathan Chancellor Signed-off-by: Andrew Morton Closes: https://github.com/ClangBuiltLinux/linux/issues/2181 Acked-by: Kuan-Wei Chiu Cc: Nick Desaulniers Cc: Bill Wendling Cc: Justin Stitt Cc: Guan-Chun Wu <409411716@gms.tku.edu.tw> Cc: --- lib/base64.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/base64.c b/lib/base64.c index 325c7332b049..e46be3a55585 100644 --- a/lib/base64.c +++ b/lib/base64.c @@ -52,11 +52,14 @@ static const char base64_tables[][65] = { INIT_32(0x60, ch_62, ch_63), \ [0x80 ... 0xff] = -1 } +__diag_push(); +__diag_ignore(clang, all, "-Wconstant-conversion", "https://github.com/llvm/llvm-project/issues/223923"); static const s8 base64_rev_maps[][256] = { [BASE64_STD] = BASE64_REV_INIT('+', '/'), [BASE64_URLSAFE] = BASE64_REV_INIT('-', '_'), [BASE64_IMAP] = BASE64_REV_INIT('+', ',') }; +__diag_pop(); #undef BASE64_REV_INIT #undef INIT_32 From 8ff981aa283a0ce516c2dc65a48a185ed336c657 Mon Sep 17 00:00:00 2001 From: Andrew Morton Date: Wed, 28 Oct 2026 18:24:22 -0700 Subject: [PATCH 02/22] openrisc: export __mulsi3 for modules OpenRISC configurations without hardware multiplication fail to build modules which contain compiler-generated calls to __mulsi3. The failure is widespread, affecting filesystems, drivers and KUnit tests at modpost. When CONFIG_OPENRISC_HAVE_INST_MUL is disabled, the architecture Makefile passes -msoft-mul to GCC. The multiplication helper is supplied by the libgcc archive linked into vmlinux, but or32_ksyms.c does not export it. Built-in callers can therefore link while module callers cannot resolve the symbol. Export __mulsi3 alongside the other compiler-generated helpers. The omission predates 7c3f4d699f34 ("resource, kunit: stop selecting GET_FREE_REGION"), which the robot identified when reporting an additional failing module. It was already present in the initial OpenRISC export list. Fixes: 616257669e1a ("OpenRISC: Miscellaneous") Signed-off-by: Andrew Morton Reported-by: kernel test robot Closes: https://lore.kernel.org/oe-kbuild-all/202610290106.o8Fs3NvO-lkp@intel.com/ Reviewed-by: Bradley Morgan Cc: Jonas Bonn Cc: Stefan Kristiansson Cc: Stafford Horne Cc: Arnd Bergmann Cc: Karl Mehltretter Cc: --- arch/openrisc/kernel/or32_ksyms.c | 1 + 1 file changed, 1 insertion(+) diff --git a/arch/openrisc/kernel/or32_ksyms.c b/arch/openrisc/kernel/or32_ksyms.c index 84a937a64e2a..5aabef30ca09 100644 --- a/arch/openrisc/kernel/or32_ksyms.c +++ b/arch/openrisc/kernel/or32_ksyms.c @@ -35,6 +35,7 @@ DECLARE_EXPORT(__divsi3); DECLARE_EXPORT(__umodsi3); DECLARE_EXPORT(__modsi3); DECLARE_EXPORT(__muldi3); +DECLARE_EXPORT(__mulsi3); DECLARE_EXPORT(__ashrdi3); DECLARE_EXPORT(__ashldi3); DECLARE_EXPORT(__lshrdi3); From 0a8f258c0f012314c36b9e4818cdc0178c68f891 Mon Sep 17 00:00:00 2001 From: Eklavya Singh Date: Wed, 21 Oct 2026 02:53:39 +0530 Subject: [PATCH 03/22] ocfs2/heartbeat: serialize concurrent writes to a region's dev attribute o2hb_region_dev_store() checks reg->hr_bdev_file to see if the region is already started, but nothing stops two writers from doing that at the same time. configfs only serializes writes through the same open file, so two processes that each open the dev attribute can both get past the check: A: hr_bdev_file is NULL, opens file A, sets hr_bdev_file = A B: hr_bdev_file is NULL, opens file B, sets hr_bdev_file = B A: start fails, fput(hr_bdev_file) drops B, sets it to NULL B: start fails, fput(NULL) Both calls also use the same hr_slots, hr_slot_data and hr_tmp_block, so the error paths can free the slot data twice. syzbot hit the fput(NULL) with an unbound loop device, where every start fails with -EIO. With four threads doing the same thing on a KASAN kernel I get both crashes within a second: BUG: KASAN: double-free in o2hb_unmap_slot_data+0x14d/0x2f0 BUG: kernel NULL pointer dereference, address: 0000000000000158 Add a mutex to the region and hold it in dev_store(), in the block_bytes, start_block and blocks stores, and in dev_show(), since they all look at hr_bdev_file. dev_store() cleans up with goto, so it takes the mutex in a small wrapper rather than with guard(). The mutex is dropped while dev_store() waits for the heartbeat to become steady, which takes seconds, so a read of dev doesn't stall behind a start. hr_bdev_file is already set at that point, so other writers still get -EINVAL, and the cleanup after the wait retakes the mutex. With the patch the same test ran for 10 minutes with lockdep on and nothing was reported. So did a second one, with a working loop device, that interrupts starts during the wait while other threads read and write the attributes. Link: https://lore.kernel.org/20261020212339.46591-1-eklavyasingh2106@gmail.com Fixes: a7f6a5fb4bde ("[PATCH] OCFS2: The Second Oracle Cluster Filesystem") Signed-off-by: Eklavya Singh Signed-off-by: Andrew Morton Reported-by: Closes: https://syzkaller.appspot.com/bug?extid=5d4e96acd0febccb5e87 Reviewed-by: Joseph Qi Cc: Mark Fasheh Cc: Joel Becker Cc: Heming Zhao Cc: Deepanshu Kartikey Cc: Cen Zhang Cc: --- fs/ocfs2/cluster/heartbeat.c | 40 ++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/fs/ocfs2/cluster/heartbeat.c b/fs/ocfs2/cluster/heartbeat.c index b45adc39aacc..7a18e71b507b 100644 --- a/fs/ocfs2/cluster/heartbeat.c +++ b/fs/ocfs2/cluster/heartbeat.c @@ -269,6 +269,8 @@ struct o2hb_region { struct list_head hr_handler_list; /* Serializes timeout arming against failed-start and teardown. */ struct mutex hr_arming_mutex; + /* Serializes the dev, block_bytes, start_block and blocks attributes. */ + struct mutex hr_dev_mutex; bool hr_stopping; /* last hb status, 0 for success, other value for error. */ @@ -1681,6 +1683,7 @@ static ssize_t o2hb_region_block_bytes_store(struct config_item *item, unsigned long block_bytes; unsigned int block_bits; + guard(mutex)(®->hr_dev_mutex); if (reg->hr_bdev_file) return -EINVAL; @@ -1710,6 +1713,7 @@ static ssize_t o2hb_region_start_block_store(struct config_item *item, char *p = (char *)page; ssize_t ret; + guard(mutex)(®->hr_dev_mutex); if (reg->hr_bdev_file) return -EINVAL; @@ -1736,6 +1740,7 @@ static ssize_t o2hb_region_blocks_store(struct config_item *item, char *p = (char *)page; int ret; + guard(mutex)(®->hr_dev_mutex); if (reg->hr_bdev_file) return -EINVAL; @@ -1753,10 +1758,12 @@ static ssize_t o2hb_region_blocks_store(struct config_item *item, static ssize_t o2hb_region_dev_show(struct config_item *item, char *page) { + struct o2hb_region *reg = to_o2hb_region(item); unsigned int ret = 0; - if (to_o2hb_region(item)->hr_bdev_file) - ret = sprintf(page, "%pg\n", reg_bdev(to_o2hb_region(item))); + guard(mutex)(®->hr_dev_mutex); + if (reg->hr_bdev_file) + ret = sprintf(page, "%pg\n", reg_bdev(reg)); return ret; } @@ -1872,11 +1879,9 @@ static int o2hb_populate_slot_data(struct o2hb_region *reg) * this is acting as commit; we set up all of hr_bdev_file and hr_task or * nothing */ -static ssize_t o2hb_region_dev_store(struct config_item *item, - const char *page, - size_t count) +static ssize_t __o2hb_region_dev_store(struct o2hb_region *reg, + const char *page, size_t count) { - struct o2hb_region *reg = to_o2hb_region(item); struct task_struct *hb_task; long fd; int sectsize; @@ -1884,6 +1889,8 @@ static ssize_t o2hb_region_dev_store(struct config_item *item, ssize_t ret = -EINVAL; int live_threshold; + lockdep_assert_held(®->hr_dev_mutex); + if (reg->hr_bdev_file) return -EINVAL; @@ -1987,9 +1994,15 @@ static ssize_t o2hb_region_dev_store(struct config_item *item, wake_up_process(hb_task); + /* + * Don't hold hr_dev_mutex across the wait, which can take seconds. + * hr_bdev_file is already set, so other writers still get -EINVAL. + */ + mutex_unlock(®->hr_dev_mutex); ret = wait_event_interruptible(o2hb_steady_queue, atomic_read(®->hr_steady_iterations) == 0 || reg->hr_node_deleted); + mutex_lock(®->hr_dev_mutex); if (ret) { atomic_set(®->hr_steady_iterations, 0); reg->hr_aborted_start = 1; @@ -2041,6 +2054,20 @@ static ssize_t o2hb_region_dev_store(struct config_item *item, return ret; } +static ssize_t o2hb_region_dev_store(struct config_item *item, + const char *page, + size_t count) +{ + struct o2hb_region *reg = to_o2hb_region(item); + ssize_t ret; + + mutex_lock(®->hr_dev_mutex); + ret = __o2hb_region_dev_store(reg, page, count); + mutex_unlock(®->hr_dev_mutex); + + return ret; +} + static ssize_t o2hb_region_pid_show(struct config_item *item, char *page) { struct o2hb_region *reg = to_o2hb_region(item); @@ -2143,6 +2170,7 @@ static struct config_item *o2hb_heartbeat_group_make_item(struct config_group *g INIT_LIST_HEAD(®->hr_all_item); mutex_init(®->hr_arming_mutex); + mutex_init(®->hr_dev_mutex); reg->hr_stopping = true; INIT_DELAYED_WORK(®->hr_write_timeout_work, o2hb_write_timeout); INIT_DELAYED_WORK(®->hr_nego_timeout_work, o2hb_nego_timeout); From f48151da595e74efdc23d83d8d6d142207b8c731 Mon Sep 17 00:00:00 2001 From: Joseph Qi Date: Sat, 24 Oct 2026 23:10:07 +0800 Subject: [PATCH 04/22] ocfs2: read the xattr entry hash before defragmenting the bucket Patch series "ocfs2: fix stale xattr entry pointers and empty header reads", v1. Five defects in the ocfs2 extended attribute code, plus one robustness measure that belongs with them, all on the paths that move entries between a block, a bucket and an index tree. They fall into two groups. Stale entry pointers (patches 1 to 3) ocfs2_xa_set() writes through a pointer into xh_entries[] that two of its callers invalidate first. Defragmenting a bucket ends in a sort() on xe_name_hash, and lib/sort.c is an unstable heapsort, so entries that share a hash can trade slots. Copying a block into a bucket changes the order outright, since a block holds its entries in insertion order while a bucket holds them by hash, yet ocfs2_xattr_update_xattr_search() relocates the search by the index the entry had in the block. Neither path compares the entry's name with the one being set, so setxattr() can replace a neighbouring attribute's value and still report success, or trip ocfs2_xa_reuse_entry()'s BUG_ON. Patch 1 takes the hash before the sort; patches 2 and 3 look the entry up again afterwards. Reads of an empty entry array (patches 4 to 6) A block and a bucket may both hold no entries: ocfs2_xa_remove_entry() keeps the last slot when it removes the only one, so an empty bucket still carries the hash it is keyed by. Three sites index xh_entries[] without checking xh_count and so read xh_entries[-1], which lands on the header and comes back as xh_count | xh_free_start << 16. Nothing faults, but the value is acted on: the collision check reports no collision for an empty bucket, the divide commits a sentinel hash that hides part of the key space from ocfs2_xattr_bucket_find(), and the block to bucket copy claims storage the bucket does not hold. Patches 4 to 6 guard each site and keep the one read of xh_entries[0] that ocfs2_xa_remove_entry() documents as intentional. This patch (of 6): ocfs2_xattr_set_entry_index_block() takes the hash a bucket split has to work with out of xs->here, but only after ocfs2_xattr_set_entry_bucket() has tried and failed to fit the entry. That call defragments the bucket when it runs out of room, and ocfs2_defrag_xattr_bucket() finishes by sorting the entry array by name hash. lib/sort.c is not stable, so two entries sharing a hash may trade slots and xs->here is left naming an entry other than the one the search found. The hash value itself survives the permutation -- the array is already sorted, so a slot can only exchange with an equal-hash neighbour -- but there is no reason to depend on that. Pick the hash up before the set, where xs->here is unambiguously the entry the search landed on. No functional change intended. Link: https://lore.kernel.org/20261024151012.1059683-1-joseph.qi@linux.alibaba.com Link: https://lore.kernel.org/20261024151012.1059683-2-joseph.qi@linux.alibaba.com Fixes: e39baff18b85 ("ocfs2: use the stored hash when checking xattr bucket collision") Signed-off-by: Joseph Qi Signed-off-by: Andrew Morton Cc: Heming Zhao Cc: Mark Fasheh Cc: Joel Becker --- fs/ocfs2/xattr.c | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/fs/ocfs2/xattr.c b/fs/ocfs2/xattr.c index b2bd06083a14..bb5b12924e1b 100644 --- a/fs/ocfs2/xattr.c +++ b/fs/ocfs2/xattr.c @@ -6100,6 +6100,22 @@ static int ocfs2_xattr_set_entry_index_block(struct inode *inode, trace_ocfs2_xattr_set_entry_index_block(xi->xi_name); + /* + * An existing entry keeps the hash it was stored under, and that is + * the hash a split has to work with. A new entry is stored under the + * unsigned one, which is what ocfs2_xa_add_entry() will write. + * + * Read it before the set below. Filling the bucket may defragment it, + * and ocfs2_defrag_xattr_bucket() re-sorts the entry array, so + * afterwards xs->here only denotes whichever entry the sort left in + * that slot. + */ + if (xs->not_found) + name_hash = ocfs2_xattr_name_hash(inode, xi->xi_name, + xi->xi_name_len); + else + name_hash = le32_to_cpu(xs->here->xe_name_hash); + ret = ocfs2_xattr_set_entry_bucket(inode, xi, xs, ctxt); if (!ret) goto out; @@ -6115,17 +6131,7 @@ static int ocfs2_xattr_set_entry_index_block(struct inode *inode, * the maximum number of collisions we will allow for then is * one bucket's worth, so check it here whether we need to * add a new bucket for the insert. - * - * An existing entry keeps the hash it was stored under, and that is - * the hash a split has to work with. A new entry is stored under the - * unsigned one, which is what ocfs2_xa_add_entry() will write. */ - if (xs->not_found) - name_hash = ocfs2_xattr_name_hash(inode, xi->xi_name, - xi->xi_name_len); - else - name_hash = le32_to_cpu(xs->here->xe_name_hash); - ret = ocfs2_check_xattr_bucket_collision(xs->bucket, name_hash); if (ret) { mlog_errno(ret); From 9a40c050f58344615a0604701b5d299519833163 Mon Sep 17 00:00:00 2001 From: Joseph Qi Date: Sat, 24 Oct 2026 23:10:08 +0800 Subject: [PATCH 05/22] ocfs2: find the xattr entry again after defragmenting a bucket ocfs2_xattr_set_entry_bucket() hands loc.xl_entry to ocfs2_xa_set() twice: once for the bucket as it stands, and, if that reports -ENOSPC, once more after ocfs2_defrag_xattr_bucket() has packed the name/value pairs down. Defragmenting ends by sorting the entry array on xe_name_hash. Buckets are kept sorted, so the sort only permutes entries that share a hash, but lib/sort.c is an unstable heapsort and it does permute them. loc.xl_entry is an address inside xh_entries[], so afterwards it denotes whichever entry landed in that slot rather than the one the search found. The second ocfs2_xa_set() then works on the wrong entry. Nothing on that path compares the entry's name with xi->xi_name, so what happens depends on how the two entries compare in size: ocfs2_xa_reuse_entry() trips its BUG_ON() when the padded name sizes differ, and otherwise the new value is written into the neighbouring attribute's value slot while the attribute being set is left untouched. setxattr() reports success either way. Remember the entry's hash before defragmenting and look the entry up again afterwards, the way ocfs2_xattr_bucket_find() does. An entry that cannot be found again means the bucket no longer holds what the search found, so fail with -EFSCORRUPTED rather than writing through the stale pointer. Link: https://lore.kernel.org/20261024151012.1059683-3-joseph.qi@linux.alibaba.com Fixes: 012255961c9e ("ocfs2: Enable xattr set in index btree") Signed-off-by: Joseph Qi Signed-off-by: Andrew Morton Cc: Heming Zhao Cc: Mark Fasheh Cc: Joel Becker Cc: --- fs/ocfs2/xattr.c | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/fs/ocfs2/xattr.c b/fs/ocfs2/xattr.c index bb5b12924e1b..d6d07116ce97 100644 --- a/fs/ocfs2/xattr.c +++ b/fs/ocfs2/xattr.c @@ -6052,13 +6052,18 @@ static int ocfs2_xattr_set_entry_bucket(struct inode *inode, struct ocfs2_xattr_search *xs, struct ocfs2_xattr_set_ctxt *ctxt) { - int ret; + int ret, found = 0; + u16 xe_index; + u32 name_hash; struct ocfs2_xa_loc loc; trace_ocfs2_xattr_set_entry_bucket(xi->xi_name); ocfs2_init_xattr_bucket_xa_loc(&loc, xs->bucket, xs->not_found ? NULL : xs->here); + /* Defragmenting below may move this entry, so remember its hash. */ + name_hash = loc.xl_entry ? le32_to_cpu(loc.xl_entry->xe_name_hash) : 0; + ret = ocfs2_xa_set(&loc, xi, ctxt); if (!ret) { xs->here = loc.xl_entry; @@ -6077,6 +6082,29 @@ static int ocfs2_xattr_set_entry_bucket(struct inode *inode, goto out; } + /* + * ocfs2_defrag_xattr_bucket() sorts the entries by name hash and + * lib/sort.c is not stable, so entries sharing a hash may have traded + * slots. Look the entry up again before writing through it. + */ + if (loc.xl_entry) { + ret = ocfs2_find_xe_in_bucket(inode, xs->bucket, + xi->xi_name_index, xi->xi_name, + name_hash, &xe_index, &found); + if (ret) { + mlog_errno(ret); + goto out; + } + if (!found) { + mlog(ML_ERROR, "xattr entry not found in bucket %llu after defrag\n", + (unsigned long long)bucket_blkno(xs->bucket)); + ret = -EFSCORRUPTED; + goto out; + } + loc.xl_entry = &bucket_xh(xs->bucket)->xh_entries[xe_index]; + xs->here = loc.xl_entry; + } + ret = ocfs2_xa_set(&loc, xi, ctxt); if (!ret) { xs->here = loc.xl_entry; From 9c191a7a44882487e291e09f095d2ae4d7cca146 Mon Sep 17 00:00:00 2001 From: Joseph Qi Date: Sat, 24 Oct 2026 23:10:09 +0800 Subject: [PATCH 06/22] ocfs2: find the xattr entry again after converting a block to a bucket ocfs2_xattr_create_index_block() turns a non-indexed xattr block into a tree by copying it into a bucket, and the copy sorts the entries: a block holds them in insertion order, a bucket by name hash. ocfs2_xattr_update_xattr_search() nevertheless relocates the caller's search by the index the entry had in the block, so unless the two orders agreed it points at an unrelated entry, and nothing between there and the write compares that entry's name with the one being set. Setting an xattr whose new value no longer fits the block arrives here with xs->not_found clear. The block gave up because ocfs2_xa_block_can_reuse() wants an exact namevalue size match, while ocfs2_xa_bucket_can_reuse() only wants an entry at least as large as the new one, so the bucket accepts a pair the block rejected and the update lands on whichever entry the sort left at that index. Equal padded name lengths get that other attribute's value replaced in place and its name left alone, so setxattr returns success while the attribute asked for keeps its old value. A longer one trips ocfs2_xa_reuse_entry()'s BUG_ON, and a shorter one is refused by the bucket as well, so ocfs2_xa_prepare_entry() wipes the entry and writes the new name over it, leaving xe_name_hash describing a name that is no longer there. Find the entry again by the name being set, matching the type as well. The copy carries every entry and every name over unchanged, and (type, name) pairs are unique within a block, so the match is exact. The type matters because the handlers strip their prefix before calling ocfs2_xattr_set(), so user.foo and trusted.foo share a stored name, a stored length and a hash, and both can sit in the same block. The name has to come from xi rather than from xs->here: a failed set can remove the entry xs->here names, and only the "growing" caller sets ctxt->set_abort, which is what stops the conversion. Scan every entry rather than narrowing on the hash the way ocfs2_find_xe_in_bucket() does, since an existing entry keeps the hash it was stored under, which a kernel that sign-extended the name bytes made different from the one computed now. An empty result means the entry did not survive the failed set and this has become an insert; leaving xs->here alone would keep it pointing into the block, which is rewritten as a tree root before the caller reads it again. Link: https://lore.kernel.org/20261024151012.1059683-4-joseph.qi@linux.alibaba.com Fixes: 012255961c9e ("ocfs2: Enable xattr set in index btree") Signed-off-by: Joseph Qi Signed-off-by: Andrew Morton Cc: Heming Zhao Cc: Mark Fasheh Cc: Joel Becker Cc: --- fs/ocfs2/xattr.c | 54 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/fs/ocfs2/xattr.c b/fs/ocfs2/xattr.c index d6d07116ce97..2c069029cef7 100644 --- a/fs/ocfs2/xattr.c +++ b/fs/ocfs2/xattr.c @@ -275,6 +275,7 @@ static int ocfs2_xattr_tree_list_index_block(struct inode *inode, size_t buffer_size); static int ocfs2_xattr_create_index_block(struct inode *inode, + struct ocfs2_xattr_info *xi, struct ocfs2_xattr_search *xs, struct ocfs2_xattr_set_ctxt *ctxt); @@ -3375,7 +3376,7 @@ static int ocfs2_xattr_block_set(struct inode *inode, else if ((ret != -ENOSPC) || ctxt->set_abort) goto end; else { - ret = ocfs2_xattr_create_index_block(inode, xs, ctxt); + ret = ocfs2_xattr_create_index_block(inode, xi, xs, ctxt); if (ret) goto end; } @@ -4749,13 +4750,10 @@ static void ocfs2_cp_xattr_block_to_bucket(struct inode *inode, * real place of the xattr. */ static void ocfs2_xattr_update_xattr_search(struct inode *inode, - struct ocfs2_xattr_search *xs, - struct buffer_head *old_bh) + struct ocfs2_xattr_info *xi, + struct ocfs2_xattr_search *xs) { - char *buf = old_bh->b_data; - struct ocfs2_xattr_block *old_xb = (struct ocfs2_xattr_block *)buf; - struct ocfs2_xattr_header *old_xh = &old_xb->xb_attrs.xb_header; - int i; + int i, count; xs->header = bucket_xh(xs->bucket); xs->base = bucket_block(xs->bucket, 0); @@ -4764,8 +4762,43 @@ static void ocfs2_xattr_update_xattr_search(struct inode *inode, if (xs->not_found) return; - i = xs->here - old_xh->xh_entries; - xs->here = &xs->header->xh_entries[i]; + /* + * A block holds its entries in insertion order while a bucket holds + * them by name hash, so ocfs2_cp_xattr_block_to_bucket() sorted the + * copies and the index this entry had in the block no longer says + * where it is. It copied every entry and every name unchanged, and + * (type, name) pairs are unique within a block, so find it again by + * name. + * + * Search for the name being set, not for the name xs->here carries. + * A failed set may have removed that entry, and ocfs2_xa_remove_entry() + * then memmoves its successor into the slot. Comparing every entry + * rather than narrowing on the hash also finds an entry stored under + * the legacy signed hash, which ocfs2_find_xe_in_bucket() would skip. + */ + count = le16_to_cpu(xs->header->xh_count); + for (i = 0; i < count; i++) { + struct ocfs2_xattr_entry *xe = &xs->header->xh_entries[i]; + + if (ocfs2_xattr_get_type(xe) != xi->xi_name_index) + continue; + if (xe->xe_name_len != xi->xi_name_len) + continue; + if (!memcmp(xs->base + le16_to_cpu(xe->xe_name_offset), + xi->xi_name, xi->xi_name_len)) { + xs->here = xe; + return; + } + } + + /* + * The entry did not survive the failed set, so this has become an + * insert. Point xs->here back inside the bucket: it still refers to + * the block being converted, which is about to be rewritten as a tree + * root. + */ + xs->not_found = -ENODATA; + xs->here = xs->header->xh_entries; } /* @@ -4773,6 +4806,7 @@ static void ocfs2_xattr_update_xattr_search(struct inode *inode, * is allocated and the xattr block header is rewritten. */ static int ocfs2_xattr_create_index_block(struct inode *inode, + struct ocfs2_xattr_info *xi, struct ocfs2_xattr_search *xs, struct ocfs2_xattr_set_ctxt *ctxt) { @@ -4834,7 +4868,7 @@ static int ocfs2_xattr_create_index_block(struct inode *inode, ocfs2_cp_xattr_block_to_bucket(inode, xb_bh, xs->bucket); ocfs2_xattr_bucket_journal_dirty(handle, xs->bucket); - ocfs2_xattr_update_xattr_search(inode, xs, xb_bh); + ocfs2_xattr_update_xattr_search(inode, xi, xs); /* Change from ocfs2_xattr_header to ocfs2_xattr_tree_root */ memset(&xb->xb_attrs, 0, inode->i_sb->s_blocksize - From e8de099f2958c84aa23186c5481e69f7ffd39355 Mon Sep 17 00:00:00 2001 From: Joseph Qi Date: Sat, 24 Oct 2026 23:10:10 +0800 Subject: [PATCH 07/22] ocfs2: do not read the entry array of an empty xattr bucket ocfs2_check_xattr_bucket_collision() decides whether splitting a bucket can make room by comparing the hash being inserted against the first and the last entry. xh_count comes off disk and is never checked, and a bucket is allowed to hold no entries: ocfs2_xa_remove_entry() keeps the last slot when it removes the only one, so that an empty bucket still carries the hash it is keyed by. xh_count then reads back as 0 while xh_entries[0] is still meaningful, and the second comparison indexes xh_entries[-1]. That is outside the __counted_by_le(xh_count) bounds of xh_entries[] and lands on the header. An entry is 16 bytes and xe_name_hash is its first field, so the value read is xh_count | xh_free_start << 16. It stays inside the block, but the answer is arbitrary, and for an empty bucket it says there is no collision, so a new bucket gets added on the strength of it. An empty bucket holds no entries to collide with. Return before reading the array. One way to reach the test with an empty bucket is a set that removes the bucket's only entry during cleanup and then fails for space again, since it is that second -ENOSPC which calls this. Growing a value past OCFS2_XATTR_INLINE_SIZE is the easy route: the cluster allocation fails, the "!orig_clusters" arm of ocfs2_xa_cleanup_value_truncate() removes the entry, and although the "growing" caller does set ctxt->set_abort, only ocfs2_xattr_block_set() and __ocfs2_xattr_set_handle() look at it, so ocfs2_xattr_set_entry_index_block() goes straight on to the collision check. This sits on commit a9ea8b397864 ("ocfs2: use the stored hash when checking xattr bucket collision"), which gave the collision check its two argument form and is marked for 6.2 and later. The read fixed here is present in the three argument version too, but a tree without that commit needs the hunk adapted by hand. Link: https://lore.kernel.org/20261024151012.1059683-5-joseph.qi@linux.alibaba.com Fixes: 012255961c9e ("ocfs2: Enable xattr set in index btree") Signed-off-by: Joseph Qi Signed-off-by: Andrew Morton Cc: Heming Zhao Cc: Mark Fasheh Cc: Joel Becker Cc: --- fs/ocfs2/xattr.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/fs/ocfs2/xattr.c b/fs/ocfs2/xattr.c index 2c069029cef7..3a16b4a36d9a 100644 --- a/fs/ocfs2/xattr.c +++ b/fs/ocfs2/xattr.c @@ -6061,11 +6061,20 @@ static int ocfs2_check_xattr_bucket_collision(struct ocfs2_xattr_bucket *bucket, u32 name_hash) { struct ocfs2_xattr_header *xh = bucket_xh(bucket); + u16 count = le16_to_cpu(xh->xh_count); + + /* + * An empty bucket keeps the hash of the entry that was removed in + * xh_entries[0] so that it can still be found, but it holds no + * entries to collide with. + */ + if (!count) + return 0; if (name_hash != le32_to_cpu(xh->xh_entries[0].xe_name_hash)) return 0; - if (xh->xh_entries[le16_to_cpu(xh->xh_count) - 1].xe_name_hash == + if (xh->xh_entries[count - 1].xe_name_hash == xh->xh_entries[0].xe_name_hash) { mlog(ML_ERROR, "Too much hash collision in xattr bucket %llu, " "hash = %u\n", From 7133a0d0954c109f1764ebf8ab32d7e757a6c587 Mon Sep 17 00:00:00 2001 From: Joseph Qi Date: Sat, 24 Oct 2026 23:10:11 +0800 Subject: [PATCH 08/22] ocfs2: do not index before the entry array when dividing an empty bucket ocfs2_divide_xattr_bucket() appends an empty bucket carrying a sentinel hash when the source bucket has no divide position, taking that hash from xh_entries[start - 1]. ocfs2_xattr_find_divide_pos() returns count when every entry shares one hash, and its loop does not run at all for count < 2, so an empty bucket comes back with start == count == 0 and the read is xh_entries[-1]. That is outside the __counted_by_le(xh_count) bounds of xh_entries[] and lands on the header, so the value picked up is xh_count | xh_free_start << 16. One above it is written into the new bucket's xh_entries[0] and committed, which leaves a bucket whose first hash does not follow the bucket before it. ocfs2_xattr_bucket_find() binary searches on xh_entries[0], so the broken key hides the range the sentinel was meant to cover: getxattr() answers -ENODATA for an attribute that is stored, and an insert can go to the wrong bucket and leave two copies of a name. The cross cluster divide hands the same value back as a tree record's e_cpos, which then claims a hash range it does not cover. Read xh_entries[0] instead. The branch is only reached when every entry shares one hash, so for a non-empty bucket it holds the same value, and for an empty one it is the slot that carries the bucket key. An empty bucket reaches the divide from ocfs2_check_xattr_bucket_collision(), which reports no collision for one, through ocfs2_add_new_xattr_bucket() and ocfs2_extend_xattr_bucket(). It does not take a failed set to get there: a divide that finds no position is itself what leaves an empty sentinel bucket behind, and a later extend can pick it as the target. Link: https://lore.kernel.org/20261024151012.1059683-6-joseph.qi@linux.alibaba.com Fixes: 80bcaf3469b8 ("ocfs2/xattr: Proper hash collision handle in bucket division") Signed-off-by: Joseph Qi Signed-off-by: Andrew Morton Cc: Heming Zhao Cc: Mark Fasheh Cc: Joel Becker Cc: --- fs/ocfs2/xattr.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/fs/ocfs2/xattr.c b/fs/ocfs2/xattr.c index 3a16b4a36d9a..822b1e9c3ba2 100644 --- a/fs/ocfs2/xattr.c +++ b/fs/ocfs2/xattr.c @@ -5228,7 +5228,15 @@ static int ocfs2_divide_xattr_bucket(struct inode *inode, start = ocfs2_xattr_find_divide_pos(xh); if (start == count) { - xe = &xh->xh_entries[start-1]; + /* + * start == count means every entry shares one hash, so the + * first of them carries it too. An empty bucket has no + * entries[start - 1], but its entries[0] still carries the + * bucket key: either the hash ocfs2_xa_remove_entry() + * deliberately leaves behind, or the sentinel a previous + * divide wrote. + */ + xe = &xh->xh_entries[0]; /* * initialized a new empty bucket here. From cd7c3563a98fe8c984463d83772e518cb7e68356 Mon Sep 17 00:00:00 2001 From: Joseph Qi Date: Sat, 24 Oct 2026 23:10:12 +0800 Subject: [PATCH 09/22] ocfs2: do not read the entry array of an empty xattr block ocfs2_cp_xattr_block_to_bucket() sizes the name and value area it has to carry over from the block's last entry, so an empty block makes it read xh_entries[-1]. That lands on the header, where xe_name_offset overlaps xh_name_value_len, and block storage never maintains that field, so it reads as 0 and the copy runs from the header to the end of the block. The bucket that comes out has xh_count 0 but a name_value_len of blocksize less the header, and a matching xh_free_start that claims those bytes are in use -- nearly the whole bucket with the common 4K block size -- while the last block of the bucket holds a copy of the old block's bytes. Nothing but a trace event reads xh_name_value_len, the copied bytes came from the same inode and were wiped before the entry was removed, and ocfs2_defrag_xattr_bucket() recomputes xh_free_start, so this corrupts the header rather than leaking anything. Leave the bucket empty instead, carrying over the hash the block was keyed by, since ocfs2_add_new_xattr_bucket() and ocfs2_xattr_bucket_find() both read xh_entries[0] of an empty bucket. That read is the exception ocfs2_xa_remove_entry() documents rather than a new out of bounds access. A block reaches the conversion empty when truncating an external value back fails partway through: the "reusing" and "overwriting" callers of ocfs2_xa_cleanup_value_truncate() remove the entry without setting ctxt->set_abort, so ocfs2_xattr_block_set() sees a plain -ENOSPC and converts. The "growing" caller does set it, and the block form makes a single attempt with no defragment and retry in between, so that is the only route here. Link: https://lore.kernel.org/20261024151012.1059683-7-joseph.qi@linux.alibaba.com Fixes: 012255961c9e ("ocfs2: Enable xattr set in index btree") Signed-off-by: Joseph Qi Signed-off-by: Andrew Morton Cc: Heming Zhao Cc: Mark Fasheh Cc: Joel Becker Cc: --- fs/ocfs2/xattr.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/fs/ocfs2/xattr.c b/fs/ocfs2/xattr.c index 822b1e9c3ba2..ac41653e59f5 100644 --- a/fs/ocfs2/xattr.c +++ b/fs/ocfs2/xattr.c @@ -4704,6 +4704,21 @@ static void ocfs2_cp_xattr_block_to_bucket(struct inode *inode, for (i = 0; i < blks; i++) memset(bucket_block(bucket, i), 0, blocksize); + /* + * An empty block has no names or values to carry over, and the + * xh_entries[count - 1] below would be xh_entries[-1]. Leave the + * bucket empty, but carry over the hash ocfs2_xa_remove_entry() left + * in the block's xh_entries[0] so that the bucket still holds the key + * it is looked up by. + */ + if (!count) { + xh->xh_num_buckets = cpu_to_le16(1); + xh->xh_free_start = cpu_to_le16(OCFS2_XATTR_BUCKET_SIZE); + xh->xh_entries[0].xe_name_hash = + xb_xh->xh_entries[0].xe_name_hash; + return; + } + /* * Since the xe_name_offset is based on ocfs2_xattr_header, * there is a offset change corresponding to the change of From 4bdfe8dd2342f9c2bdc3b788699164ebe21d9126 Mon Sep 17 00:00:00 2001 From: Matheus Alves de Almeida Date: Fri, 23 Oct 2026 22:30:20 -0300 Subject: [PATCH 10/22] fat: preallocate space before expanding in fat_cont_expand() The fat_cont_expand() implementation leaves allocation of new clusters to generic_cont_expand_simple(). This path does many small cluster allocations, harming performance. Preallocate cluster space in fat_cont_expand(). Move preallocation to a new fat_preallocate() helper used by both fat_cont_expand() and fat_fallocate(FALLOC_FL_KEEP_SIZE). Use a FAT-specific fat_cont_expand_simple() instead of generic_cont_expand_simple() to avoid rechecking the new size after clusters have been allocated. Benchmarking was done under QEMU/KVM with a virtio-blk device using cache=none. Each result is the arithmetic mean of the listed samples. start = now_ns(); for (unsigned i = 0; i < count; i++) { if (truncate_mode) ftruncate(fds[i], bytes); else fallocate(fds[i], 0, 0, bytes); } elapsed = now_ns() - start; 16 files, 512-byte clusters: length mode sync? before after improvement samples 256MiB ftruncate no 9.576s 5.345s 1.79x 10 256MiB fallocate no 9.634s 5.640s 1.71x 10 1MiB ftruncate yes 58.66s 0.362s 162.15x 5 1MiB fallocate yes 61.36s 0.390s 157.15x 5 128 files, 512-byte clusters: length mode sync? before after improvement samples 512KiB ftruncate no 0.433s 0.352s 1.23x 10 512KiB fallocate no 0.446s 0.393s 1.13x 10 64KiB ftruncate yes 31.32s 0.429s 73.00x 10 64KiB fallocate yes 29.59s 0.410s 72.42x 10 16 files, 4096-byte clusters: length mode sync? before after improvement samples 256MiB ftruncate no 5.181s 4.417s 1.17x 10 256MiB fallocate no 5.166s 4.442s 1.16x 10 1MiB ftruncate yes 9.294s 0.111s 84.06x 10 1MiB fallocate yes 8.710s 0.110s 79.36x 10 The biggest improvement is on filesystems with a small cluster size and on synchronous mounts. Link: https://lore.kernel.org/20261024013021.251898-1-matheus.aalmeida@inf.ufrgs.br Signed-off-by: Matheus Alves de Almeida Signed-off-by: Andrew Morton Acked-by: OGAWA Hirofumi --- fs/fat/file.c | 70 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/fs/fat/file.c b/fs/fat/file.c index d4f1b360b974..70583e791bdc 100644 --- a/fs/fat/file.c +++ b/fs/fat/file.c @@ -217,13 +217,62 @@ const struct file_operations fat_file_operations = { .setlease = generic_setlease, }; +static int fat_preallocate(struct inode *inode, loff_t new_size) +{ + int nr_cluster; /* Number of clusters to be allocated */ + loff_t mm_bytes; /* Number of bytes to be allocated for file */ + loff_t ondisksize; /* block aligned on-disk size in bytes*/ + struct super_block *sb = inode->i_sb; + struct msdos_sb_info *sbi = MSDOS_SB(sb); + + ondisksize = inode->i_blocks << 9; + if (new_size <= ondisksize) + return 0; + + /* First compute the number of clusters to be allocated */ + mm_bytes = new_size - ondisksize; + nr_cluster = (mm_bytes + (sbi->cluster_size - 1)) >> + sbi->cluster_bits; + + /* Allocate the clusters. We are not zeroing them out */ + return fat_add_clusters(inode, nr_cluster); +} + +static int fat_cont_expand_simple(struct inode *inode, loff_t size) +{ + struct address_space *mapping = inode->i_mapping; + const struct address_space_operations *aops = mapping->a_ops; + struct folio *folio; + void *fsdata = NULL; + int err; + + err = aops->write_begin(NULL, mapping, size, 0, &folio, &fsdata); + if (err) + goto out; + + err = aops->write_end(NULL, mapping, size, 0, 0, folio, fsdata); + +out: + return err; +} + static int fat_cont_expand(struct inode *inode, loff_t size) { struct address_space *mapping = inode->i_mapping; loff_t start = inode->i_size, count = size - inode->i_size; int err; - err = generic_cont_expand_simple(inode, size); + /* + * Preallocating space here is more efficient than during + * fat_cont_expand_simple() because of batching. + */ + err = inode_newsize_ok(inode, size); + if (err) + goto out; + err = fat_preallocate(inode, size); + if (err) + goto out; + err = fat_cont_expand_simple(inode, size); if (err) goto out; @@ -263,12 +312,7 @@ static int fat_cont_expand(struct inode *inode, loff_t size) static long fat_fallocate(struct file *file, int mode, loff_t offset, loff_t len) { - int nr_cluster; /* Number of clusters to be allocated */ - loff_t mm_bytes; /* Number of bytes to be allocated for file */ - loff_t ondisksize; /* block aligned on-disk size in bytes*/ struct inode *inode = file->f_mapping->host; - struct super_block *sb = inode->i_sb; - struct msdos_sb_info *sbi = MSDOS_SB(sb); int err = 0; /* No support for hole punch or other fallocate flags. */ @@ -281,19 +325,7 @@ static long fat_fallocate(struct file *file, int mode, inode_lock(inode); if (mode & FALLOC_FL_KEEP_SIZE) { - ondisksize = inode->i_blocks << 9; - if ((offset + len) <= ondisksize) - goto error; - - /* First compute the number of clusters to be allocated */ - mm_bytes = offset + len - ondisksize; - nr_cluster = (mm_bytes + (sbi->cluster_size - 1)) >> - sbi->cluster_bits; - - /* Allocate the clusters. We are not zeroing them out */ - err = fat_add_clusters(inode, nr_cluster); - if (err) - goto error; + err = fat_preallocate(inode, offset + len); } else { if ((offset + len) <= i_size_read(inode)) goto error; From 7b90009546f5d3f3ac9428040cd7ffbdfd60f2eb Mon Sep 17 00:00:00 2001 From: Matheus Alves de Almeida Date: Wed, 21 Oct 2026 13:21:04 -0300 Subject: [PATCH 11/22] fat: hold truncate_lock in fat_write_failed() The current fat_write_failed() implementation calls into fat_truncate_blocks() without holding truncate_lock. A failed or short extending write racing with FIBMAP can spuriously make a FAT filesystem read-only. FIBMAP bounds its lookup by i_blocks, so it can walk allocated clusters beyond i_size while fat_write_failed() is removing them. If the chain is shortened during the walk, the mapper can report a bogus "request beyond EOF" error, triggering the default read-only error policy. Hold truncate_lock for write around the block cleanup to serialize it with _fat_bmap(). For comparison, fat_setattr() correctly holds truncate_lock during truncation: down_write(&MSDOS_I(inode)->truncate_lock); truncate_setsize(inode, attr->ia_size); fat_truncate_blocks(inode, attr->ia_size); up_write(&MSDOS_I(inode)->truncate_lock); Make it so ->truncate_lock is held exclusive while truncating. Link: https://lore.kernel.org/20261021162105.62525-1-matheus.aalmeida@inf.ufrgs.br Fixes: 16fab2015099 ("fat: permit to return phy block number by fibmap in fallocated region") Fixes: 459f6ed3b8c7 ("fat: convert to use the new truncate convention.") Signed-off-by: Matheus Alves de Almeida Signed-off-by: Andrew Morton Acked-by: OGAWA Hirofumi Cc: Pedro Falcato Cc: --- fs/fat/inode.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fs/fat/inode.c b/fs/fat/inode.c index d85c8fbdcca4..353c91f0a843 100644 --- a/fs/fat/inode.c +++ b/fs/fat/inode.c @@ -217,7 +217,9 @@ static void fat_write_failed(struct address_space *mapping, loff_t to) if (to > inode->i_size) { truncate_pagecache(inode, inode->i_size); + down_write(&MSDOS_I(inode)->truncate_lock); fat_truncate_blocks(inode, inode->i_size); + up_write(&MSDOS_I(inode)->truncate_lock); } } From 84db6619cb702c37cf848b450c21b9c9706b0775 Mon Sep 17 00:00:00 2001 From: Cen Zhang Date: Mon, 26 Oct 2026 19:04:00 +0800 Subject: [PATCH 12/22] ocfs2: validate move extents against a stable EOF The move-extents range must be bounded by the inode size used for extent movement. ocfs2_ioctl_move_extents() checks and clamps the request before taking the local inode and cluster locks in ocfs2_move_extents(). It also uses separate i_size_read() calls for the range check and subtraction. A concurrent truncate can invalidate the first check and make the later unsigned subtraction underflow: Move-extents ioctl Truncate ------------------ -------- check me_start <= i_size take inode and cluster locks shrink EOF below me_start release locks compare me_start + me_len with EOF set me_len = EOF - me_start [unsigned underflow] acquire inode and cluster locks select extents using the bad range A truncate after clamping can also leave the range extending beyond EOF. Conversely, the cached size may be smaller than the size refreshed when the cluster inode lock is acquired. Early validation can then reject a valid start or permanently shorten the requested length and threshold. The me_start + me_len comparison can also wrap for a large input length. Validate and adjust the original request once, after inode_lock(), the exclusive rw lock and ocfs2_inode_lock() have stabilized and refreshed the size. Read EOF once, reject a start beyond it, and bound the length by EOF minus start. Normalize the threshold using the same EOF snapshot. This keeps zero as the default-threshold sentinel until validation uses the size that will govern extent movement. Move goal validation after range adjustment as well. Its allocation-group capacity check uses me_len and must see the EOF-clamped length. Both validation failures use the existing inode-lock unwind path before any extent movement or transaction starts. Range errors now reach the ioctl's normal copyout path, as goal errors already did. Unlike the old early rejection, a failed copyout can therefore override -EINVAL with -EFAULT. Link: https://lore.kernel.org/pm-ocfs2-move-extents-isize-rrw-v6-395bdee84ea81149068e@gmail.com Fixes: 53069d4e7695 ("Ocfs2/move_extents: move/defrag extents within a certain range.") Signed-off-by: Cen Zhang Signed-off-by: Andrew Morton Suggested-by: Joseph Qi Reviewed-by: Joseph Qi Assisted-by: LLM Cc: Mark Fasheh Cc: Joel Becker Cc: Heming Zhao Cc: Jia-Ju Bai Cc: Cc: --- fs/ocfs2/move_extents.c | 70 +++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/fs/ocfs2/move_extents.c b/fs/ocfs2/move_extents.c index ad1678ee7cc4..93b45531bf7d 100644 --- a/fs/ocfs2/move_extents.c +++ b/fs/ocfs2/move_extents.c @@ -907,6 +907,30 @@ static int __ocfs2_move_extents_range(struct buffer_head *di_bh, return ret; } +static int ocfs2_validate_and_adjust_move_range(struct inode *inode, + struct ocfs2_move_extents *range) +{ + u64 i_size = i_size_read(inode); + + if (range->me_start > i_size) + return -EINVAL; + + if (range->me_len > i_size - range->me_start) + range->me_len = i_size - range->me_start; + + /* + * The default threshold for defragmentation is 1 MiB, which + * matches the maximum OCFS2 cluster size. + */ + if (!range->me_threshold) + range->me_threshold = 1024 * 1024; + + if (range->me_threshold > i_size) + range->me_threshold = i_size; + + return 0; +} + static int ocfs2_move_extents(struct ocfs2_move_extents_context *context) { int status; @@ -936,6 +960,21 @@ static int ocfs2_move_extents(struct ocfs2_move_extents_context *context) goto out_rw_unlock; } + status = ocfs2_validate_and_adjust_move_range(inode, context->range); + if (status) { + mlog_errno(status); + goto out_inode_unlock; + } + + /* The goal check depends on the adjusted range length. */ + if (!context->auto_defrag) { + status = ocfs2_validate_and_adjust_move_goal(inode, context->range); + if (status) { + mlog_errno(status); + goto out_inode_unlock; + } + } + /* * remember ip_xattr_sem also needs to be held if necessary */ @@ -1028,27 +1067,8 @@ int ocfs2_ioctl_move_extents(struct file *filp, void __user *argp) goto out_free; } - if (range.me_start > i_size_read(inode)) { - status = -EINVAL; - goto out_free; - } - - if (range.me_start + range.me_len > i_size_read(inode)) - range.me_len = i_size_read(inode) - range.me_start; - context->range = ⦥ - /* - * ok, the default threshold for the defragmentation - * is 1M, since our maximum clustersize was 1M also. - * any thought? - */ - if (!range.me_threshold) - range.me_threshold = 1024 * 1024; - - if (range.me_threshold > i_size_read(inode)) - range.me_threshold = i_size_read(inode); - if (range.me_flags & ~(OCFS2_MOVE_EXT_FL_AUTO_DEFRAG | OCFS2_MOVE_EXT_FL_PART_DEFRAG)) { status = -EINVAL; @@ -1060,23 +1080,11 @@ int ocfs2_ioctl_move_extents(struct file *filp, void __user *argp) if (range.me_flags & OCFS2_MOVE_EXT_FL_PART_DEFRAG) context->partial = 1; - } else { - /* - * first best-effort attempt to validate and adjust the goal - * (physical address in block), while it can't guarantee later - * operation can succeed all the time since global_bitmap may - * change a bit over time. - */ - - status = ocfs2_validate_and_adjust_move_goal(inode, &range); - if (status) - goto out_copy; } status = ocfs2_move_extents(context); if (status) mlog_errno(status); -out_copy: /* * movement/defragmentation may end up being partially completed, * that's the reason why we need to return userspace the finished From fe626e8c60e1330d55f173a6ce34f010fe64eccf Mon Sep 17 00:00:00 2001 From: Andrew Morton Date: Tue, 27 Oct 2026 21:09:52 -0700 Subject: [PATCH 13/22] MAINTAINERS: add an IPC entry Add an entry covering ipc/ and its IPC and POSIX message queue selftests, with Andrew Morton as maintainer and Manfred Spraul and Davidlohr Bueso as reviewers. Signed-off-by: Andrew Morton Acked-by: Davidlohr Bueso Acked-by: Manfred Spraul --- MAINTAINERS | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 93abbb315065..f28e50ede11e 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -14070,6 +14070,16 @@ S: Maintained F: io_uring/bpf-ops.* F: io_uring/loop.* +IPC +M: Andrew Morton +R: Manfred Spraul +R: Davidlohr Bueso +L: linux-kernel@vger.kernel.org +S: Maintained +F: ipc/ +F: tools/testing/selftests/ipc/ +F: tools/testing/selftests/mqueue/ + IPMI SUBSYSTEM M: Corey Minyard L: openipmi-developer@lists.sourceforge.net (moderated for non-subscribers) From 7e7022849e1c3d0a2a8659d01d23390d630e780a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20L=C3=A9ger?= Date: Thu, 29 Oct 2026 10:51:06 +0100 Subject: [PATCH 14/22] =?UTF-8?q?MAINTAINERS:=20mailmap:=20use=20Cl=C3=A9m?= =?UTF-8?q?ent=20L=C3=A9ger's=20linux.dev=20mail=20address?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I recently requested a @linux.dev address so I can keep a consistent one throughout my contributions and use it for the upcoming ones. Map my previously used email addresses to this new one. Link: https://lore.kernel.org/20261029095107.24985-1-clement.leger@linux.dev Signed-off-by: Clément Léger Signed-off-by: Andrew Morton --- .mailmap | 4 ++++ MAINTAINERS | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.mailmap b/.mailmap index 0d05be949eff..5ab2cf568d3d 100644 --- a/.mailmap +++ b/.mailmap @@ -233,6 +233,10 @@ Chuck Lever Chuck Lever Chuck Lever Claudiu Beznea +Clément Léger +Clément Léger +Clément Léger +Clément Léger Coiby Xu Colin Ian King Corey Minyard diff --git a/MAINTAINERS b/MAINTAINERS index f28e50ede11e..84b32d6e643b 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -20934,7 +20934,7 @@ S: Maintained F: drivers/char/hw_random/optee-rng.c OP-TEE RTC DRIVER -M: Clément Léger +M: Clément Léger L: linux-rtc@vger.kernel.org S: Maintained F: drivers/rtc/rtc-optee.c @@ -23833,7 +23833,7 @@ F: Documentation/devicetree/bindings/rtc/renesas,rz-rtca3.yaml F: drivers/rtc/rtc-renesas-rtca3.c RENESAS RZ/N1 A5PSW SWITCH DRIVER -M: Clément Léger +M: Clément Léger L: linux-renesas-soc@vger.kernel.org L: netdev@vger.kernel.org S: Maintained From 5bdef466851e39ba752634cbea68e5ea24ccfb24 Mon Sep 17 00:00:00 2001 From: Cen Zhang Date: Tue, 27 Oct 2026 12:31:44 +0800 Subject: [PATCH 15/22] ocfs2: keep locking_state within the cluster connection lifetime The locking_state reader calls ocfs2_dlm_lvb() through the selected cluster stack. Its debug-object reference keeps the tracking list alive, but does not keep that stack selected without a cluster connection. DLM initialization publishes the file before establishing its connection, and DLM shutdown drops the connection while the file still exists. Both the mount error unwind in ocfs2_mount_volume() and ocfs2_dismount_volume() remove the debugfs subtree only after ocfs2_dlm_shutdown() has returned. Whenever the disconnect releases the last stack reference, which is the case for a failed mount and for any unmount without a pending hangup, a concurrent reader dereferences a NULL active_stack: Mount error unwind Debugfs reader ------------------ -------------- ocfs2_mount_volume(): out_dlm ocfs2_dlm_shutdown() disconnect last cluster clear active_stack read locking_state ocfs2_dlm_seq_show() ocfs2_dlm_lvb() dereference active_stack fill_super(): out_debugfs remove debugfs subtree Publish the debug files only after the connection and the mount lock resources are initialized, so a failed DLM initialization has no debug reference to drop. Remove them again in the common DLM shutdown helper before the connection is dropped: debugfs removal waits for readers that are inside the file operations, and the parent directory is still present at that point in every caller. This covers the mount error unwind, the ocfs2_fill_super() failures after a successful mount that dismount with mnt_err set, and unmounts whose disconnect releases the stack directly. Only the relevant frames of the report are kept: Oops: general protection fault, probably for non-canonical address 0xdffffc0000000001: 0000 [#1] SMP KASAN NOPTI KASAN: null-ptr-deref in range [0x0000000000000008-0x000000000000000f] RIP: 0010:ocfs2_dlm_lvb+0x38/0xf0 Call Trace: ocfs2_dlm_seq_show+0x36a/0xb70 seq_read_iter+0x480/0x1110 seq_read+0x362/0x5a0 full_proxy_read+0x11f/0x180 vfs_read+0x1a0/0xc20 ksys_read+0x111/0x200 Link: https://lore.kernel.org/pm-ocfs2-objects-candidate-0011-v7-61e3ef8b1ad44ac2e817@gmail.com Fixes: 286eaa95c5c5 ("ocfs2: Break out stackglue into modules.") Signed-off-by: Cen Zhang Signed-off-by: Andrew Morton Reviewed-by: Joseph Qi Assisted-by: LLM Cc: Mark Fasheh Cc: Joel Becker Cc: Heming Zhao Cc: Jia-Ju Bai Cc: Cc: --- fs/ocfs2/dlmglue.c | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/fs/ocfs2/dlmglue.c b/fs/ocfs2/dlmglue.c index 88d946466083..412ab0808c59 100644 --- a/fs/ocfs2/dlmglue.c +++ b/fs/ocfs2/dlmglue.c @@ -3356,6 +3356,14 @@ static void ocfs2_dlm_shutdown_debug(struct ocfs2_super *osb) { struct ocfs2_dlm_debug *dlm_debug = osb->osb_dlm_debug; + /* + * Drain the readers before the cluster stack goes away: the + * locking_state reader resolves ocfs2_dlm_lvb() through the + * stack. Every caller removes the parent directory later. + */ + debugfs_lookup_and_remove("locking_state", osb->osb_debug_root); + debugfs_lookup_and_remove("locking_filter", osb->osb_debug_root); + if (dlm_debug) ocfs2_put_dlm_debug(dlm_debug); } @@ -3370,8 +3378,6 @@ int ocfs2_dlm_init(struct ocfs2_super *osb) goto local; } - ocfs2_dlm_init_debug(osb); - /* launch downconvert thread */ osb->dc_task = kthread_run(ocfs2_downconvert_thread, osb, "ocfs2dc-%s", osb->uuid_str); @@ -3411,9 +3417,10 @@ int ocfs2_dlm_init(struct ocfs2_super *osb) ocfs2_orphan_scan_lock_res_init(&osb->osb_orphan_scan.os_lockres, osb); osb->cconn = conn; + if (conn) + ocfs2_dlm_init_debug(osb); bail: if (status < 0) { - ocfs2_dlm_shutdown_debug(osb); if (osb->dc_task) kthread_stop(osb->dc_task); } @@ -3443,10 +3450,11 @@ void ocfs2_dlm_shutdown(struct ocfs2_super *osb, ocfs2_lock_res_free(&osb->osb_orphan_scan.os_lockres); if (osb->cconn) { + /* The disconnect below may release the selected stack. */ + ocfs2_dlm_shutdown_debug(osb); + ocfs2_cluster_disconnect(osb->cconn, hangup_pending); osb->cconn = NULL; - - ocfs2_dlm_shutdown_debug(osb); } } From 723982e7052e65c3b31e8e8c815ffaf2ecfeaa8f Mon Sep 17 00:00:00 2001 From: Nickolai Zeldovich Date: Fri, 23 Oct 2026 22:01:52 -0400 Subject: [PATCH 16/22] panic: zero the dynamically allocated taint buffer A %s read of print_tainted()'s string can run past the end of the taint buffer while another CPU rebuilds a longer string in it, reading slab memory out of bounds into the kernel log (a KASAN slab-out-of-bounds report at taint_buf + size). A warning or an unhandled-signal dump on another CPU reaches print_tainted() through dump_stack_print_info(). alloc_taint_buf() sizes the buffer for the longest taint string and allocates it with kmalloc(), so the bytes past that string are never written and the object holds no permanent NUL terminator. _print_tainted() rebuilds the string in place without locking and writes the terminator last, so a reader that overtakes a writer extending the string past the previous high-water mark finds no terminator inside the object and walks off its end. print_tainted()'s kernel-doc promises the string is "always NULL terminated", a guarantee the previous zero-filled static buffer kept. Allocate the buffer with kzalloc() so its trailing bytes stay NUL and every reader stops inside the object. Reproduced on QEMU riscv64 under KASAN by growing the taint set while processes took unhandled-signal dumps on several CPUs: the unpatched kernel reports a slab-out-of-bounds read at taint_buf + size, the patched one is clean. Link: https://lore.kernel.org/20261024020153.830459-1-nickolai@csail.mit.edu Fixes: a9dff0d0d11c ("kernel/panic: allocate taint string buffer dynamically") Signed-off-by: Nickolai Zeldovich Signed-off-by: Andrew Morton Reviewed-by: Bradley Morgan Reviewed-by: Petr Mladek Cc: Rio Cc: --- kernel/panic.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/panic.c b/kernel/panic.c index 574ee5a12f92..91e48d33b7db 100644 --- a/kernel/panic.c +++ b/kernel/panic.c @@ -884,7 +884,7 @@ static __init int alloc_taint_buf(void) size += 1; /* For NULL terminator */ - buf = kmalloc(size, GFP_KERNEL); + buf = kzalloc(size, GFP_KERNEL); if (!buf) { panic("Failed to allocate taint string buffer"); From 61469249aab03799b0d3294b7a0cd25dd303017a Mon Sep 17 00:00:00 2001 From: Cen Zhang Date: Tue, 27 Oct 2026 18:14:49 +0800 Subject: [PATCH 17/22] ocfs2/dlm: serialize migration MLE cleanup on failure A migration MLE has a base reference for the mastering answer and an in-use reference held by dlm_migrate_lockres(). The finishing assertion consumes the base reference, but migration failure drops it again without checking whether the assertion already consumed it. The target queues its migration worker before it acknowledges the ALL_DONE packet, so its finishing assertion can be handled while the sender still waits for the status. If the connection fails before the status arrives, dlm_send_one_lockres() returns an error for a migration that the target has already completed: Old-master sender Finishing assertion ----------------- ------------------- publish MLE; take in-use reference send ALL_DONE; wait for status take lookup reference unlink MLE put lookup and base references send returns a connection error put base reference again; free MLE put in-use reference; access freed MLE dlm_assert_master_handler() holds dlm->spinlock from the MLE lookup to the final puts, and it takes dlm->master_lock around the unlink and the puts. Decide under the same locks: if the MLE is already unhashed, the assertion consumed the base reference, so drop only the sender's in-use reference. Otherwise unlink it before consuming the base reference so a later assertion no longer finds it. An assertion that got there first has also made the target the owner and cleared DLM_LOCK_RES_MIGRATING. Treating the attempt as failed at that point would leave the lockres owned by the target while this node still holds the other nodes' locks and refmap bits, so it could neither be purged nor migrated again. Finish our side as a completed migration does instead: drop those locks under res->spinlock if the target still owns the lockres, and recalculate its usage. Use the same cleanup for the pre-send failure, the send failure and the target-death failure paths. The sender's in-use reference keeps the MLE alive until its final put outside the nested locks. Successful migration retains its existing in-use-only put. A target that has died cannot assert after the sender has seen its death. The o2net node-down callback runs before the DLM one; it disconnects the node and flushes the workqueue that runs the message handlers, so an assertion already received has been handled by the time the DLM removes the node from the domain map. A node that leaves the domain cleanly sends its exit message only after its assertions have been answered. Two windows of the hand-over itself are left as they are: the handler switches the owner and the sender drops the other nodes' locks moments later, and an assertion from a live target that arrives after the sender has given up still meets the owner check in the handler. Waiting for the target before giving up would only stretch the first window into the time in which recovery of the target can observe it. KASAN report, only the relevant frames are kept: BUG: KASAN: slab-use-after-free in dlm_put_mle_inuse+0xce/0xe0 Read of size 8 at addr ffff8881190e3c20 by task umount/593 Call Trace: dlm_put_mle_inuse+0xce/0xe0 dlm_migrate_lockres+0x16af/0x20d0 dlm_empty_lockres+0x60a/0xbd0 dlm_unregister_domain+0x53b/0x11d0 o2cb_cluster_disconnect+0xa5/0x110 ocfs2_cluster_disconnect+0x73/0xc0 ocfs2_dlm_shutdown+0x311/0x440 ocfs2_dismount_volume+0x696/0x940 generic_shutdown_super+0x15f/0x490 kill_block_super+0x40/0xa0 deactivate_locked_super+0xae/0x120 deactivate_super+0xa2/0xc0 cleanup_mnt+0x217/0x450 task_work_run+0x144/0x230 Allocated by task 593: kmem_cache_alloc_noprof+0x20b/0x5c0 dlm_migrate_lockres+0x20e/0x20d0 dlm_empty_lockres+0x60a/0xbd0 dlm_unregister_domain+0x53b/0x11d0 o2cb_cluster_disconnect+0xa5/0x110 ocfs2_cluster_disconnect+0x73/0xc0 ocfs2_dlm_shutdown+0x311/0x440 ocfs2_dismount_volume+0x696/0x940 Freed by task 593: kmem_cache_free+0x242/0x580 __dlm_put_mle+0x4b2/0x660 dlm_migrate_lockres+0x1690/0x20d0 dlm_empty_lockres+0x60a/0xbd0 dlm_unregister_domain+0x53b/0x11d0 o2cb_cluster_disconnect+0xa5/0x110 ocfs2_cluster_disconnect+0x73/0xc0 ocfs2_dlm_shutdown+0x311/0x440 The buggy address belongs to the object at ffff8881190e3c00 which belongs to the cache o2dlm_mle of size 480 The buggy address is located 32 bytes inside of freed 480-byte region [ffff8881190e3c00, ffff8881190e3de0) Link: https://lore.kernel.org/pm-ocfs2-objects-candidate-0038-v9-659f247ea980d5f3fbeb@gmail.com Fixes: 6714d8e86bf4 ("[PATCH] OCFS2: The Second Oracle Cluster Filesystem") Signed-off-by: Cen Zhang Signed-off-by: Andrew Morton Reviewed-by: Joseph Qi Assisted-by: LLM Cc: Mark Fasheh Cc: Joel Becker Cc: Heming Zhao Cc: Jia-Ju Bai Cc: Cc: --- fs/ocfs2/dlm/dlmmaster.c | 70 ++++++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/fs/ocfs2/dlm/dlmmaster.c b/fs/ocfs2/dlm/dlmmaster.c index 612969867ff9..ea48c013f7fb 100644 --- a/fs/ocfs2/dlm/dlmmaster.c +++ b/fs/ocfs2/dlm/dlmmaster.c @@ -2525,6 +2525,52 @@ static int dlm_is_lockres_migratable(struct dlm_ctxt *dlm, */ +/* + * Drop the sender's references on a migration MLE after the attempt has + * failed. The finishing assert from the target may already have unlinked + * the MLE and consumed its base reference; decide under the locks the + * assert handler holds, and only drop the base reference while the MLE is + * still hashed. Returns true when the assert got there first: the target + * has become the master, so the caller must complete the migration. + */ +static bool dlm_put_migration_mle(struct dlm_ctxt *dlm, + struct dlm_master_list_entry *mle) +{ + bool asserted; + + spin_lock(&dlm->spinlock); + spin_lock(&dlm->master_lock); + __dlm_mle_detach_hb_events(dlm, mle); + asserted = hlist_unhashed(&mle->master_hash_node); + if (!asserted) { + __dlm_unlink_mle(dlm, mle); + __dlm_put_mle(mle); + } + spin_unlock(&dlm->master_lock); + spin_unlock(&dlm->spinlock); + + dlm_put_mle_inuse(mle); + return asserted; +} + +/* + * The finishing assert got there first: it switched the owner to the + * target and cleared DLM_LOCK_RES_MIGRATING. Drop the other nodes' locks + * as a completed migration does, unless recovery has taken the lockres + * over since, and let the usage be recalculated. + */ +static void dlm_migrate_lockres_asserted(struct dlm_ctxt *dlm, + struct dlm_lock_resource *res, + u8 target) +{ + spin_lock(&res->spinlock); + if (res->owner == target) + dlm_remove_nonlocal_locks(dlm, res); + spin_unlock(&res->spinlock); + + dlm_lockres_calc_usage(dlm, res); +} + static int dlm_migrate_lockres(struct dlm_ctxt *dlm, struct dlm_lock_resource *res, u8 target) { @@ -2609,9 +2655,10 @@ static int dlm_migrate_lockres(struct dlm_ctxt *dlm, if (ret < 0) { if (mle_added) { - dlm_mle_detach_hb_events(dlm, mle); - dlm_put_mle(mle); - dlm_put_mle_inuse(mle); + if (dlm_put_migration_mle(dlm, mle)) { + dlm_migrate_lockres_asserted(dlm, res, target); + ret = 0; + } } else if (mle) { kmem_cache_free(dlm_mle_cache, mle); mle = NULL; @@ -2640,9 +2687,11 @@ static int dlm_migrate_lockres(struct dlm_ctxt *dlm, mlog(0, "migration to node %u failed with %d\n", target, ret); /* migration failed, detach and clean up mle */ - dlm_mle_detach_hb_events(dlm, mle); - dlm_put_mle(mle); - dlm_put_mle_inuse(mle); + if (dlm_put_migration_mle(dlm, mle)) { + dlm_migrate_lockres_asserted(dlm, res, target); + ret = 0; + goto leave; + } spin_lock(&res->spinlock); res->state &= ~DLM_LOCK_RES_MIGRATING; wake = 1; @@ -2686,9 +2735,12 @@ static int dlm_migrate_lockres(struct dlm_ctxt *dlm, res->lockname.name, target); ret = -EINVAL; /* migration failed, detach and clean up mle */ - dlm_mle_detach_hb_events(dlm, mle); - dlm_put_mle(mle); - dlm_put_mle_inuse(mle); + if (dlm_put_migration_mle(dlm, mle)) { + dlm_migrate_lockres_asserted(dlm, res, + target); + ret = 0; + goto leave; + } spin_lock(&res->spinlock); res->state &= ~DLM_LOCK_RES_MIGRATING; wake = 1; From 126429e2f503ade63502978beb0c519066442250 Mon Sep 17 00:00:00 2001 From: Cen Zhang Date: Thu, 29 Oct 2026 14:57:35 +0800 Subject: [PATCH 18/22] ocfs2: stop recovery before shutting down local allocators Recovery completion needs the local allocators until it has finished reclaiming a failed node's allocation window and truncate log. During unmount, quota recovery is stopped first, but that leaves recovery_state at OCFS2_REC_QUOTA_DISABLED. New non-quota recovery can still start. ocfs2_truncate_log_shutdown() drains existing completion work and releases osb_tl_bh before ocfs2_recovery_exit() closes that publication path: Unmount Recovery ------- -------- stop quota recovery; disable quotas truncate_log_shutdown() flush existing completion work recover another failed node queue completion flush local log; release buffer complete_truncate_log_recovery() use released osb_tl_bh recovery_exit(): disable and drain An extra buffer reference alone would still let completion append new records after the final truncate-log flush. Split stopping and draining recovery from freeing its state. Stop all recovery producers and drain completion work before shutting down either local allocator. Keep recovery-state release at its existing later point, so its lifetime relative to other teardown users stays unchanged. The second stop at recovery_exit() is harmless once recovery is disabled. KASAN report as below: BUG: KASAN: slab-use-after-free in ocfs2_truncate_log_needs_flush+0x23d/0x360 Read of size 8 at addr ffff8881093711e8 by task kworker/u16:3/53 CPU: 0 UID: 0 PID: 53 Comm: kworker/u16:3 Not tainted 7.3.0-rc4-next-20260921-pmb-ocfs2-functional-v1+ #1 PREEMPT(lazy) 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 Workqueue: ocfs2_wq ocfs2_complete_recovery Call Trace: dump_stack_lvl+0x93/0xd0 print_report+0xce/0x630 ? ocfs2_truncate_log_needs_flush+0x23d/0x360 ? srso_alias_return_thunk+0x5/0xfbef5 ? __virt_addr_valid+0x20e/0x420 ? ocfs2_truncate_log_needs_flush+0x23d/0x360 kasan_report+0xe0/0x110 ? ocfs2_truncate_log_needs_flush+0x23d/0x360 ocfs2_truncate_log_needs_flush+0x23d/0x360 ? __pfx_ocfs2_truncate_log_needs_flush+0x10/0x10 ? __pfx_down_write+0x10/0x10 ocfs2_complete_truncate_log_recovery+0x315/0x780 ? trace_hardirqs_on+0x18/0x160 ? __pfx_ocfs2_complete_truncate_log_recovery+0x10/0x10 ? ocfs2_complete_recovery+0x666/0x11b0 ? srso_alias_return_thunk+0x5/0xfbef5 ? kfree+0x308/0x580 ocfs2_complete_recovery+0x676/0x11b0 ? __pfx_ocfs2_complete_recovery+0x10/0x10 ? reacquire_held_locks+0xdd/0x200 ? process_one_work+0x7c8/0x1b40 ? srso_alias_return_thunk+0x5/0xfbef5 ? lock_acquire+0x190/0x300 ? __pfx_autoremove_wake_function+0x10/0x10 ? process_one_work+0x834/0x1b40 ? srso_alias_return_thunk+0x5/0xfbef5 ? lock_release+0xc8/0x290 ? srso_alias_return_thunk+0x5/0xfbef5 process_one_work+0x9a8/0x1b40 ? __pfx_process_one_work+0x10/0x10 ? lock_acquire+0x190/0x300 ? lock_is_held_type+0x8f/0x100 ? srso_alias_return_thunk+0x5/0xfbef5 worker_thread+0x65c/0xe40 ? srso_alias_return_thunk+0x5/0xfbef5 ? __kthread_parkme+0x177/0x220 ? srso_alias_return_thunk+0x5/0xfbef5 ? __pfx_worker_thread+0x10/0x10 kthread+0x351/0x460 ? srso_alias_return_thunk+0x5/0xfbef5 ? __pfx_kthread+0x10/0x10 ret_from_fork+0x659/0x940 ? __pfx_ret_from_fork+0x10/0x10 ? srso_alias_return_thunk+0x5/0xfbef5 ? __switch_to+0x74f/0xf70 ? __pfx_kthread+0x10/0x10 ret_from_fork_asm+0x1a/0x30 Allocated by task 538: kasan_save_stack+0x33/0x60 kasan_save_track+0x14/0x30 __kasan_slab_alloc+0x89/0x90 kmem_cache_alloc_noprof+0x20b/0x5c0 alloc_buffer_head+0x26/0x120 folio_alloc_buffers+0x122/0x1d0 bdev_getblk+0x299/0x4f0 ocfs2_read_blocks_sync+0x513/0xbc0 ocfs2_iget+0x9cd/0x1a00 ocfs2_get_system_file_inode+0x25e/0x7a0 ocfs2_fill_super+0x1b05/0x3930 get_tree_bdev_flags+0x35d/0x5f0 vfs_get_tree+0x93/0x320 fc_mount+0x1d/0x200 path_mount+0x592/0x1e00 __x64_sys_mount+0x24b/0x2d0 do_syscall_64+0x114/0x620 entry_SYSCALL_64_after_hwframe+0x77/0x7f Freed by task 553: kasan_save_stack+0x33/0x60 kasan_save_track+0x14/0x30 kasan_save_free_info+0x3b/0x60 __kasan_slab_free+0x5f/0x80 kmem_cache_free+0x242/0x580 free_buffer_head+0x4b/0xc0 try_to_free_buffers+0x1dc/0x330 ocfs2_truncate_log_shutdown+0x789/0x1290 ocfs2_dismount_volume+0x1da/0x940 generic_shutdown_super+0x15f/0x490 kill_block_super+0x40/0xa0 deactivate_locked_super+0xae/0x120 deactivate_super+0xa2/0xc0 cleanup_mnt+0x217/0x450 task_work_run+0x144/0x230 exit_to_user_mode_loop+0x17d/0x570 do_syscall_64+0x4d1/0x620 entry_SYSCALL_64_after_hwframe+0x77/0x7f The buggy address belongs to the object at ffff8881093711c0 which belongs to the cache buffer_head of size 160 The buggy address is located 40 bytes inside of freed 160-byte region [ffff8881093711c0, ffff888109371260) The buggy address belongs to the physical page: page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x109371 flags: 0x200000000000000(node=0|zone=2) page_type: f5(slab) raw: 0200000000000000 ffff8881008883c0 dead000000000100 dead000000000122 raw: 0000000000000000 0000000000120012 00000000f5000000 0000000000000000 page dumped because: kasan: bad access detected Memory state around the buggy address: ffff888109371080: 00 00 00 00 fc fc fc fc fc fc fc fc 00 00 00 00 ffff888109371100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >ffff888109371180: fc fc fc fc fc fc fc fc fa fb fb fb fb fb fb fb ^ ffff888109371200: fb fb fb fb fb fb fb fb fb fb fb fb fc fc fc fc ffff888109371280: fc fc fc fc 00 00 00 00 00 00 00 00 00 00 00 00 ================================================================== Link: https://lore.kernel.org/pm-ocfs2-objects-candidate-0019-v8-574dd69fd3f47f56348b@gmail.com Fixes: ccd979bdbce9 ("[PATCH] OCFS2: The Second Oracle Cluster Filesystem") Signed-off-by: Cen Zhang Signed-off-by: Andrew Morton Suggested-by: Joseph Qi Reviewed-by: Joseph Qi Assisted-by: LLM Cc: Mark Fasheh Cc: Joel Becker Cc: Heming Zhao Cc: Jia-Ju Bai Cc: Cc: --- fs/ocfs2/journal.c | 15 ++++++++++++--- fs/ocfs2/journal.h | 1 + fs/ocfs2/super.c | 3 +++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/fs/ocfs2/journal.c b/fs/ocfs2/journal.c index cc60db6b9706..cbb44e280d4b 100644 --- a/fs/ocfs2/journal.c +++ b/fs/ocfs2/journal.c @@ -227,13 +227,22 @@ void ocfs2_recovery_disable_quota(struct ocfs2_super *osb) ocfs2_recovery_disable(osb, OCFS2_REC_QUOTA_WANT_DISABLE); } +/* Stop producers and drain completion work without freeing recovery state. */ +void ocfs2_recovery_stop(struct ocfs2_super *osb) +{ + ocfs2_recovery_disable(osb, OCFS2_REC_WANT_DISABLE); +} + void ocfs2_recovery_exit(struct ocfs2_super *osb) { struct ocfs2_recovery_map *rm; - /* disable any new recovery threads and wait for any currently - * running ones to exit. Do this before setting the vol_state. */ - ocfs2_recovery_disable(osb, OCFS2_REC_WANT_DISABLE); + /* + * Disable producers and drain work before freeing recovery state. + * Do this before setting vol_state to VOLUME_DISMOUNTED: completion + * work may still be waiting in __ocfs2_wait_on_mount(). + */ + ocfs2_recovery_stop(osb); /* * Now that recovery is shut down, and the osb is about to be diff --git a/fs/ocfs2/journal.h b/fs/ocfs2/journal.h index 19fc920d26b1..4b52bf7b0938 100644 --- a/fs/ocfs2/journal.h +++ b/fs/ocfs2/journal.h @@ -147,6 +147,7 @@ void ocfs2_complete_recovery(struct work_struct *work); void ocfs2_wait_for_recovery(struct ocfs2_super *osb); int ocfs2_recovery_init(struct ocfs2_super *osb); +void ocfs2_recovery_stop(struct ocfs2_super *osb); void ocfs2_recovery_exit(struct ocfs2_super *osb); void ocfs2_recovery_disable_quota(struct ocfs2_super *osb); diff --git a/fs/ocfs2/super.c b/fs/ocfs2/super.c index 0af2245ae303..3072b0386db8 100644 --- a/fs/ocfs2/super.c +++ b/fs/ocfs2/super.c @@ -1831,6 +1831,9 @@ static void ocfs2_dismount_volume(struct super_block *sb, int mnt_err) /* Wait for worker to be done with the work structure in osb */ cancel_work_sync(&osb->dquot_drop_work); + /* Stop recovery producers before releasing their local allocators. */ + ocfs2_recovery_stop(osb); + ocfs2_shutdown_local_alloc(osb); ocfs2_truncate_log_shutdown(osb); From 0988f0c0c7c908f0ff77fd8b90ce9918a1f41479 Mon Sep 17 00:00:00 2001 From: Cen Zhang Date: Tue, 13 Oct 2026 23:34:46 +0800 Subject: [PATCH 19/22] ocfs2/cluster: unlink socket containers before releasing resources A socket container's node and socket must remain alive while the container is on sock_containers. The debugfs formatter reads sc_node and sc_sock under o2net_debug_lock without taking their references, but sc_kref_release() releases both before taking that lock to unlink the container. With CONFIG_DEBUG_FS enabled, a synchronous sender can retain its last socket-container reference after o2net_disconnect_node() has detached the connection and flushed o2net_wq. The caller runs outside that workqueue. After the sender removes its configfs dependency, node removal can proceed without waiting for the sender's final sc_put(): Sender (final sc_put) Configfs rmdir Debugfs reader -------------------- -------------- -------------- o2nm_undepend_item() Drop node item refs o2nm_node_put() [node can be freed] sc_seq_show() Lock debug lock Read sc_node->nd_name Unlock debug lock sc_node = NULL o2net_debug_del_sc() The earlier socket release has a similar window. The container is still selectable while its socket has been released and sc_sock has not yet been cleared: Sender (final sc_put) Debugfs reader -------------------- -------------- sock_release(sc->sc_sock) [socket can be freed] sc_seq_show() Lock debug lock Read sc_sock->sk fields Unlock debug lock sc_sock = NULL ... o2net_debug_del_sc() Move o2net_debug_del_sc() before releasing either resource. Unlinking under the existing debug lock waits for an active formatter to finish and prevents later readers from selecting the container. This covers both socket fields and node fields, including nd_num in the stats formatter. KASAN report as below: BUG: KASAN: slab-use-after-free in string+0x42b/0x470 Read of size 1 at addr ffff88810f56fc90 by task python3/506 CPU: 0 UID: 0 PID: 506 Comm: python3 Not tainted 7.3.0-rc4-next-20260921-pmb-ocfs2-functional-v1+ #1 PREEMPT(lazy) 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+0x93/0xd0 print_report+0xce/0x630 kasan_report+0xe0/0x110 string+0x42b/0x470 vsnprintf+0x330/0x1110 seq_printf+0x1a8/0x260 sc_seq_show+0x604/0x9f0 seq_read_iter+0x480/0x1110 seq_read+0x362/0x5a0 full_proxy_read+0x11f/0x180 vfs_read+0x1a0/0xc20 ksys_read+0x111/0x200 do_syscall_64+0x114/0x620 entry_SYSCALL_64_after_hwframe+0x77/0x7f (User register dump omitted.) Allocated by task 497: kasan_save_stack+0x33/0x60 kasan_save_track+0x14/0x30 __kasan_kmalloc+0xaa/0xb0 __kmalloc_cache_noprof+0x28d/0x610 o2nm_node_group_make_item+0xea/0x1a0 configfs_mkdir+0x762/0xca0 vfs_mkdir+0x399/0x7e0 filename_mkdirat+0x129/0x450 __x64_sys_mkdir+0x70/0xa0 do_syscall_64+0x114/0x620 entry_SYSCALL_64_after_hwframe+0x77/0x7f Freed by task 505: kasan_save_stack+0x33/0x60 kasan_save_track+0x14/0x30 kasan_save_free_info+0x3b/0x60 __kasan_slab_free+0x5f/0x80 kfree+0x308/0x580 config_item_cleanup+0x14b/0x230 config_item_put+0x79/0x90 sc_put+0x454/0x6e0 o2net_send_message_vec+0x63d/0x1560 o2net_send_message+0xa6/0xe0 validation_send_write+0x17f/0x1d0 full_proxy_write+0x11f/0x180 vfs_write+0x25a/0x1010 ksys_write+0x111/0x200 do_syscall_64+0x114/0x620 entry_SYSCALL_64_after_hwframe+0x77/0x7f The buggy address belongs to the object at ffff88810f56fc00 which belongs to the cache kmalloc-512 of size 512 The buggy address is located 144 bytes inside of freed 512-byte region [ffff88810f56fc00, ffff88810f56fe00) (Page and shadow-memory dumps omitted.) Link: https://lore.kernel.org/pm-ocfs2-objects-candidate-0065-v6-3090153fd7cacb810214@gmail.com Fixes: 2309e9e040fe ("ocfs2/net: Add debug interface to o2net") Signed-off-by: Cen Zhang Signed-off-by: Andrew Morton Reviewed-by: Joseph Qi Cc: Mark Fasheh Cc: Joel Becker Cc: Heming Zhao Cc: Kees Cook Cc: Jakub Kicinski Cc: Sunil Mushran Cc: Jia-Ju Bai Cc: Cc: --- fs/ocfs2/cluster/tcp.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/ocfs2/cluster/tcp.c b/fs/ocfs2/cluster/tcp.c index 8b3830b601ba..0eaf2c4a6989 100644 --- a/fs/ocfs2/cluster/tcp.c +++ b/fs/ocfs2/cluster/tcp.c @@ -393,6 +393,9 @@ static void sc_kref_release(struct kref *kref) sclog(sc, "releasing\n"); + /* Unlink before freeing anything the debugfs readers dereference. */ + o2net_debug_del_sc(sc); + if (sc->sc_sock) { sock_release(sc->sc_sock); sc->sc_sock = NULL; @@ -402,8 +405,6 @@ static void sc_kref_release(struct kref *kref) o2nm_node_put(sc->sc_node); sc->sc_node = NULL; - o2net_debug_del_sc(sc); - if (sc->sc_page) __free_page(sc->sc_page); kfree(sc); From 6cd4c2736c659397bd6c6d9e3cffd8211afcb60b Mon Sep 17 00:00:00 2001 From: Hungyu Lin Date: Fri, 30 Oct 2026 06:00:11 +0000 Subject: [PATCH 20/22] fat: fix parent link count when replacing a sibling directory vfat_rename() and do_msdos_rename() adjust parent link counts only when updating the dotdot entry for a move between different parents. Replacing an existing empty directory in the same parent also removes a subdirectory, but leaves the parent's link count unchanged. For example, renaming one of two sibling directories over the other leaves the parent with four links instead of three. stat and ls report the stale count, and find -links 3 does not match the parent. Drop the parent's link count for this case after the source entry has been removed successfully. Leave cross-parent renames, regular files and the existing error paths unchanged. Share the parent link count check with rmdir through fat_dir_drop_nlink(). Only decrement counts of at least three and report a filesystem error otherwise, rather than reducing a corrupted directory's count below two. Export the helper for the VFAT and MSDOS modules. Link: https://lore.kernel.org/20261030060011.70237-1-dennylin0707@gmail.com Signed-off-by: Hungyu Lin Signed-off-by: Andrew Morton Acked-by: OGAWA Hirofumi Cc: Christian Brauner --- fs/fat/dir.c | 10 ++++++++++ fs/fat/fat.h | 1 + fs/fat/namei_msdos.c | 9 +++------ fs/fat/namei_vfat.c | 9 +++------ 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/fs/fat/dir.c b/fs/fat/dir.c index 35bdb62944a2..0cebcaf82426 100644 --- a/fs/fat/dir.c +++ b/fs/fat/dir.c @@ -963,6 +963,16 @@ int fat_dir_empty(struct inode *dir) } EXPORT_SYMBOL_GPL(fat_dir_empty); +void fat_dir_drop_nlink(struct inode *dir) +{ + if (dir->i_nlink >= 3) + drop_nlink(dir); + else + fat_fs_error(dir->i_sb, "parent dir link count too low (%u)", + dir->i_nlink); +} +EXPORT_SYMBOL_GPL(fat_dir_drop_nlink); + /* * fat_subdirs counts the number of sub-directories of dir. It can be run * on directories being created. diff --git a/fs/fat/fat.h b/fs/fat/fat.h index 76978a268410..a8a3ca4f465b 100644 --- a/fs/fat/fat.h +++ b/fs/fat/fat.h @@ -332,6 +332,7 @@ extern const struct file_operations fat_dir_operations; extern int fat_search_long(struct inode *inode, const unsigned char *name, int name_len, struct fat_slot_info *sinfo); extern int fat_dir_empty(struct inode *dir); +void fat_dir_drop_nlink(struct inode *dir); extern int fat_subdirs(struct inode *dir); extern int fat_scan(struct inode *dir, const unsigned char *name, struct fat_slot_info *sinfo); diff --git a/fs/fat/namei_msdos.c b/fs/fat/namei_msdos.c index c7500c2859e0..5a43d042eb01 100644 --- a/fs/fat/namei_msdos.c +++ b/fs/fat/namei_msdos.c @@ -333,12 +333,7 @@ static int msdos_rmdir(struct inode *dir, struct dentry *dentry) err = fat_remove_entries(dir, &sinfo); /* and releases bh */ if (err) goto out; - if (dir->i_nlink >= 3) - drop_nlink(dir); - else { - fat_fs_error(sb, "parent dir link count too low (%u)", - dir->i_nlink); - } + fat_dir_drop_nlink(dir); clear_nlink(inode); fat_detach(inode); @@ -565,6 +560,8 @@ static int do_msdos_rename(struct inode *old_dir, unsigned char *old_name, (void)sync_inode_metadata(old_dir, 1); if (new_inode) { + if (is_dir && old_dir == new_dir) + fat_dir_drop_nlink(old_dir); drop_nlink(new_inode); if (is_dir) drop_nlink(new_inode); diff --git a/fs/fat/namei_vfat.c b/fs/fat/namei_vfat.c index ff7652bf0410..791f2d7b8509 100644 --- a/fs/fat/namei_vfat.c +++ b/fs/fat/namei_vfat.c @@ -809,12 +809,7 @@ static int vfat_rmdir(struct inode *dir, struct dentry *dentry) err = fat_remove_entries(dir, &sinfo); /* and releases bh */ if (err) goto out; - if (dir->i_nlink >= 3) - drop_nlink(dir); - else { - fat_fs_error(sb, "parent dir link count too low (%u)", - dir->i_nlink); - } + fat_dir_drop_nlink(dir); clear_nlink(inode); fat_truncate_time(inode, NULL, FAT_UPDATE_ATIME | FAT_UPDATE_CMTIME); @@ -1011,6 +1006,8 @@ static int vfat_rename(struct inode *old_dir, struct dentry *old_dentry, vfat_update_dir_metadata(old_dir, &ts); if (new_inode) { + if (is_dir && old_dir == new_dir) + fat_dir_drop_nlink(old_dir); drop_nlink(new_inode); if (is_dir) drop_nlink(new_inode); From a25c02af29c3e95128e4343fceb4bdace8e480a0 Mon Sep 17 00:00:00 2001 From: Cen Zhang Date: Sun, 25 Oct 2026 00:18:52 +0800 Subject: [PATCH 21/22] ocfs2: wait for filecheck kobject release after a failed add The filecheck kobject and its release completion are embedded in ocfs2_super, so the osb must outlive the kobject. On a failed kobject_init_and_add(), creation clears fs_fcheck and calls kobject_put(), but returns without waiting for the release callback. With CONFIG_DEBUG_KOBJECT_RELEASE, the last put queues delayed cleanup. Mount failure can then free the containing osb before the embedded release timer expires: Mount failure Delayed cleanup ------------- --------------- ocfs2_filecheck_create_sysfs() kobject_init_and_add() fails fs_fcheck = NULL kobject_put() queue delayed release return to mount error cleanup ocfs2_dismount_volume() remove_sysfs() skips the wait kfree(osb) expire embedded release timer access freed osb The normal removal path waits for fs_kobj_unregister, but it returns early when fs_fcheck is NULL. The failed-add path therefore leaves the delayed work and its completion in freed storage. Wait for the existing release completion after the failed-add kobject_put(), before returning to mount error cleanup. This keeps the enclosing osb alive until the kobject has finished releasing, matching the normal teardown path. KASAN report as below: BUG: KASAN: slab-use-after-free in __run_timers+0xa6d/0xa90 Write of size 8 at addr ffff8881178a5658 by task swapper/0/0 CPU: 0 UID: 0 PID: 0 Comm: swapper/0 Not tainted 7.3.0-rc4-next-20260921-pmb-ocfs2-functional-v1+ #1 PREEMPT(lazy) 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+0x93/0xd0 print_report+0xce/0x630 ? __run_timers+0xa6d/0xa90 ? srso_alias_return_thunk+0x5/0xfbef5 ? __virt_addr_valid+0x20e/0x420 ? __run_timers+0xa6d/0xa90 kasan_report+0xe0/0x110 ? __run_timers+0xa6d/0xa90 __run_timers+0xa6d/0xa90 ? srso_alias_return_thunk+0x5/0xfbef5 ? __pfx___run_timers+0x10/0x10 ? __lock_acquire+0x5a3/0x2140 ? __pfx_do_raw_spin_lock+0x10/0x10 ? srso_alias_return_thunk+0x5/0xfbef5 timer_expire_remote+0xfa/0x160 ? __pfx_timer_expire_remote+0x10/0x10 ? srso_alias_return_thunk+0x5/0xfbef5 ? srso_alias_return_thunk+0x5/0xfbef5 ? trace_hardirqs_on+0x18/0x160 tmigr_handle_remote_up+0x4ba/0x9d0 ? __pfx_tmigr_handle_remote_up+0x10/0x10 ? tmigr_handle_remote+0x21a/0x340 ? srso_alias_return_thunk+0x5/0xfbef5 ? lock_release+0xc8/0x290 ? __pfx_tmigr_handle_remote_up+0x10/0x10 __walk_groups_from+0x5a/0x160 tmigr_handle_remote+0x24f/0x340 ? __pfx_tmigr_handle_remote+0x10/0x10 ? lock_acquire+0x190/0x300 ? srso_alias_return_thunk+0x5/0xfbef5 ? lock_release+0xc8/0x290 run_timer_softirq+0x222/0x270 ? __pfx_run_timer_softirq+0x10/0x10 ? srso_alias_return_thunk+0x5/0xfbef5 ? srso_alias_return_thunk+0x5/0xfbef5 ? lockdep_hardirqs_on_prepare+0xb8/0x1a0 ? srso_alias_return_thunk+0x5/0xfbef5 handle_softirqs+0x1fa/0x8d0 ? __pfx_handle_softirqs+0x10/0x10 ? srso_alias_return_thunk+0x5/0xfbef5 ? _raw_spin_unlock+0x23/0x40 ? srso_alias_return_thunk+0x5/0xfbef5 ? __hrtimer_rearm_deferred+0x1fb/0x5d0 __irq_exit_rcu+0x113/0x1e0 irq_exit_rcu+0xe/0x20 sysvec_apic_timer_interrupt+0x6c/0x80 asm_sysvec_apic_timer_interrupt+0x1a/0x20 RIP: 0010:pv_native_safe_halt+0xf/0x20 Code: fa 18 01 e9 ee 37 ff fa 0f 1f 00 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 f3 0f 1e fa eb 07 0f 00 2d 23 c7 14 00 fb f4 c7 37 ff fa 66 2e 0f 1f 84 00 00 00 00 00 66 90 90 90 90 90 90 RSP: 0018:ffffffff87c07cf8 EFLAGS: 00000206 RAX: 0000000000045d0f RBX: ffffffff87c2d4c0 RCX: ffffffff86111e95 RDX: 0000000000000000 RSI: 0000000000000000 RDI: ffffffff815b508e RBP: fffffbfff0f85a98 R08: 0000000000000001 R09: ffffed10310c68ba R10: ffff8881886345d3 R11: 0000000000000000 R12: 0000000000000001 R13: 0000000000000000 R14: 1ffffffff0f80fa4 R15: dffffc0000000000 ? ct_kernel_exit.constprop.0+0xc5/0xf0 ? do_idle+0x3ae/0x650 default_idle+0x9/0x10 default_idle_call+0x6c/0xb0 do_idle+0x3ae/0x650 ? __pfx___schedule+0x10/0x10 ? __pfx_do_idle+0x10/0x10 ? rest_init+0x1cd/0x330 ? srso_alias_return_thunk+0x5/0xfbef5 cpu_startup_entry+0x54/0x60 rest_init+0x1e1/0x330 ? __pfx_acpi_read_bit_register+0x10/0x10 ? __pfx_rest_init+0x10/0x10 ? acpi_os_write_port+0x23/0xb0 ? __pfx_kernel_init+0x10/0x10 ? srso_alias_return_thunk+0x5/0xfbef5 ? acpi_ut_status_exit+0xd6/0x180 ? srso_alias_return_thunk+0x5/0xfbef5 ? acpi_ut_status_exit+0xd6/0x180 ? srso_alias_return_thunk+0x5/0xfbef5 ? acpi_enable_subsystem+0x2b0/0x4e0 ? __pfx_x86_late_time_init+0x10/0x10 start_kernel+0x417/0x4b0 x86_64_start_reservations+0x18/0x30 x86_64_start_kernel+0x111/0x120 common_startup_64+0x13e/0x158 Allocated by task 494: kasan_save_stack+0x33/0x60 kasan_save_track+0x14/0x30 __kasan_kmalloc+0xaa/0xb0 __kmalloc_cache_noprof+0x28d/0x610 ocfs2_initialize_super.isra.0+0xab/0x3070 ocfs2_fill_super+0x801/0x3920 get_tree_bdev_flags+0x35d/0x5f0 vfs_get_tree+0x93/0x320 fc_mount+0x1d/0x200 path_mount+0x592/0x1e00 __x64_sys_mount+0x24b/0x2d0 do_syscall_64+0x114/0x620 entry_SYSCALL_64_after_hwframe+0x77/0x7f Freed by task 494: kasan_save_stack+0x33/0x60 kasan_save_track+0x14/0x30 kasan_save_free_info+0x3b/0x60 __kasan_slab_free+0x5f/0x80 kfree+0x308/0x580 ocfs2_dismount_volume+0x43c/0x9b0 ocfs2_fill_super+0x258f/0x3920 get_tree_bdev_flags+0x35d/0x5f0 vfs_get_tree+0x93/0x320 fc_mount+0x1d/0x200 path_mount+0x592/0x1e00 __x64_sys_mount+0x24b/0x2d0 do_syscall_64+0x114/0x620 entry_SYSCALL_64_after_hwframe+0x77/0x7f The buggy address belongs to the object at ffff8881178a4000 which belongs to the cache kmalloc-8k of size 8192 The buggy address is located 5720 bytes inside of freed 8192-byte region [ffff8881178a4000, ffff8881178a6000) The buggy address belongs to the physical page: page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x1178a0 head: order:3 mapcount:0 entire_mapcount:0 nr_pages_mapped:0 pincount:0 flags: 0x200000000000040(head|node=0|zone=2) page_type: f5(slab) raw: 0200000000000040 ffff888100043880 dead000000000122 0000000000000000 raw: 0000000000000000 0000000000020002 00000000f5000000 0000000000000000 head: 0200000000000040 ffff888100043880 dead000000000122 0000000000000000 head: 0000000000000000 0000000000020002 00000000f5000000 0000000000000000 head: 0200000000000003 fffffffffffffe01 00000000ffffffff 00000000ffffffff head: 0000000000000000 0000000000000000 00000000ffffffff 0000000000000000 page dumped because: kasan: bad access detected Memory state around the buggy address: ffff8881178a5500: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb ffff8881178a5580: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb >ffff8881178a5600: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb ^ ffff8881178a5680: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb ffff8881178a5700: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb ================================================================== Link: https://lore.kernel.org/pm-ocfs2-objects-candidate-0087-v6-78f3c3d6d70887d6e0cc@gmail.com Fixes: b9fba67b3806 ("ocfs2: fix error path kobject memory leak") Signed-off-by: Cen Zhang Signed-off-by: Andrew Morton Reviewed-by: Joseph Qi Assisted-by: LLM Cc: Mark Fasheh Cc: Joel Becker Cc: Heming Zhao Cc: Jia-Ju Bai Cc: Cc: --- fs/ocfs2/filecheck.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fs/ocfs2/filecheck.c b/fs/ocfs2/filecheck.c index 75334e4bd082..e3b79bc749f5 100644 --- a/fs/ocfs2/filecheck.c +++ b/fs/ocfs2/filecheck.c @@ -189,6 +189,8 @@ int ocfs2_filecheck_create_sysfs(struct ocfs2_super *osb) /* Mount failure still calls ocfs2_filecheck_remove_sysfs(). */ entry->fs_fcheck = NULL; kobject_put(&entry->fs_kobj); + /* Keep ocfs2_super alive until the kobject is released. */ + wait_for_completion(&entry->fs_kobj_unregister); kfree(fcheck); return ret; } From 5b4696e0bfc358619adaeab25ad35b4979af8331 Mon Sep 17 00:00:00 2001 From: Cen Zhang Date: Tue, 20 Oct 2026 22:25:53 +0800 Subject: [PATCH 22/22] ocfs2: cache the mount context for dentry lock callbacks ocfs2_drop_dentry_lock() drops the inode reference before releasing the DENTRY lock. This lets inode eviction release the local inode and open locks before a remote unlink can proceed past the DENTRY lock. A DLM callback can still arrive before the DENTRY lock has been dropped. ocfs2_get_dentry_osb() obtains the mount context through dl_inode->i_sb, which can access a freed inode when the downconvert thread drops the last inode reference in its post-unlock callback: Downconvert thread DLM callback thread ------------------ ------------------- ocfs2_process_blocked_lock() ocfs2_dentry_post_unlock() ocfs2_dentry_lock_put() ocfs2_drop_dentry_lock() iput(dl->dl_inode) [inode reclaimed after RCU] ocfs2_blocking_ast() ocfs2_get_dentry_osb() dl->dl_inode->i_sb ocfs2_simple_drop_lockres() Store the mount context in the DENTRY lock resource's l_priv field and use the default mount-context lookup. Recover the containing dentry lock with container_of(), as already done for refcount locks, and remove ocfs2_get_dentry_osb(). ocfs2_dismount_volume() shuts down the DLM before freeing the mount context, so callbacks can use it without accessing the inode. Preserve the existing iput-before-unlock order to avoid delaying local inode and open lock release until after the DENTRY lock is released. Link: https://lore.kernel.org/pm-ocfs2-objects-candidate-0012-v9-eeb9084fb29577971229@gmail.com Fixes: 8ed6b23709b3 ("ocfs2: revert iput deferring code in ocfs2_drop_dentry_lock") Signed-off-by: Cen Zhang Signed-off-by: Andrew Morton Suggested-by: Joseph Qi Reviewed-by: Joseph Qi Assisted-by: LLM Cc: Mark Fasheh Cc: Joel Becker Cc: Heming Zhao Cc: Jia-Ju Bai Cc: Cc: --- fs/ocfs2/dcache.h | 5 ++--- fs/ocfs2/dlmglue.c | 16 ++++------------ 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/fs/ocfs2/dcache.h b/fs/ocfs2/dcache.h index 7f246c5692d8..046ab9a80769 100644 --- a/fs/ocfs2/dcache.h +++ b/fs/ocfs2/dcache.h @@ -17,9 +17,8 @@ struct ocfs2_dentry_lock { u64 dl_parent_blkno; /* - * The ocfs2_dentry_lock keeps an inode reference until - * dl_lockres has been destroyed. This is usually done in - * ->d_iput() anyway, so there should be minimal impact. + * The inode reference is dropped before dl_lockres is unlocked. + * Callbacks must not use it to find the mount context during teardown. */ struct inode *dl_inode; struct ocfs2_lock_res dl_lockres; diff --git a/fs/ocfs2/dlmglue.c b/fs/ocfs2/dlmglue.c index 412ab0808c59..c281902e3bec 100644 --- a/fs/ocfs2/dlmglue.c +++ b/fs/ocfs2/dlmglue.c @@ -58,7 +58,6 @@ struct ocfs2_mask_waiter { #endif }; -static struct ocfs2_super *ocfs2_get_dentry_osb(struct ocfs2_lock_res *lockres); static struct ocfs2_super *ocfs2_get_inode_osb(struct ocfs2_lock_res *lockres); static struct ocfs2_super *ocfs2_get_file_osb(struct ocfs2_lock_res *lockres); static struct ocfs2_super *ocfs2_get_qinfo_osb(struct ocfs2_lock_res *lockres); @@ -258,7 +257,6 @@ static const struct ocfs2_lock_res_ops ocfs2_orphan_scan_lops = { }; static const struct ocfs2_lock_res_ops ocfs2_dentry_lops = { - .get_osb = ocfs2_get_dentry_osb, .post_unlock = ocfs2_dentry_post_unlock, .downconvert_worker = ocfs2_dentry_convert_worker, .flags = 0, @@ -309,7 +307,7 @@ static inline struct ocfs2_dentry_lock *ocfs2_lock_res_dl(struct ocfs2_lock_res { BUG_ON(lockres->l_type != OCFS2_LOCK_TYPE_DENTRY); - return (struct ocfs2_dentry_lock *)lockres->l_priv; + return container_of(lockres, struct ocfs2_dentry_lock, dl_lockres); } static inline struct ocfs2_mem_dqinfo *ocfs2_lock_res_qinfo(struct ocfs2_lock_res *lockres) @@ -610,13 +608,6 @@ static __u64 ocfs2_get_dentry_lock_ino(struct ocfs2_lock_res *lockres) return be64_to_cpu(inode_blkno_be); } -static struct ocfs2_super *ocfs2_get_dentry_osb(struct ocfs2_lock_res *lockres) -{ - struct ocfs2_dentry_lock *dl = lockres->l_priv; - - return OCFS2_SB(dl->dl_inode->i_sb); -} - void ocfs2_dentry_lock_res_init(struct ocfs2_dentry_lock *dl, u64 parent, struct inode *inode) { @@ -624,6 +615,7 @@ void ocfs2_dentry_lock_res_init(struct ocfs2_dentry_lock *dl, u64 inode_blkno = OCFS2_I(inode)->ip_blkno; __be64 inode_blkno_be = cpu_to_be64(inode_blkno); struct ocfs2_lock_res *lockres = &dl->dl_lockres; + struct ocfs2_super *osb = OCFS2_SB(inode->i_sb); ocfs2_lock_res_init_once(lockres); @@ -652,9 +644,9 @@ void ocfs2_dentry_lock_res_init(struct ocfs2_dentry_lock *dl, memcpy(&lockres->l_name[OCFS2_DENTRY_LOCK_INO_START], &inode_blkno_be, sizeof(__be64)); - ocfs2_lock_res_init_common(OCFS2_SB(inode->i_sb), lockres, + ocfs2_lock_res_init_common(osb, lockres, OCFS2_LOCK_TYPE_DENTRY, &ocfs2_dentry_lops, - dl); + osb); } static void ocfs2_super_lock_res_init(struct ocfs2_lock_res *res,