From 7a41fd2b32e5908f19a68732008d581c167279dd Mon Sep 17 00:00:00 2001 From: Edward Adam Davis Date: Thu, 16 Apr 2026 11:37:53 +0800 Subject: [PATCH 01/10] hfsplus: Remove the duplicate attr inode dirty marking action Syzbot reported a null-ptr-deref in [1]. If the attributes file is not loaded during system mount, a trigger occurs [1] when setxattr is executed in userspace. Remove the first mark attr inode dirty operation. [1] KASAN: null-ptr-deref in range [0x0000000000000008-0x000000000000000f] Call Trace: hfsplus_setxattr+0x124/0x340 fs/hfsplus/xattr.c:555 hfsplus_trusted_setxattr+0x40/0x60 fs/hfsplus/xattr_trusted.c:30 __vfs_setxattr+0x43c/0x480 fs/xattr.c:218 __vfs_setxattr_noperm+0x12d/0x660 fs/xattr.c:252 vfs_setxattr+0x163/0x360 fs/xattr.c:339 do_setxattr fs/xattr.c:654 [inline] Reported-by: syzbot+bc70a12e438dadba4fb4@syzkaller.appspotmail.com Fixes: ee8422d00b7c ("hfsplus: fix potential Allocation File corruption after fsync") Closes: https://syzkaller.appspot.com/bug?extid=bc70a12e438dadba4fb4 Signed-off-by: Edward Adam Davis Reviewed-by: Viacheslav Dubeyko Signed-off-by: Viacheslav Dubeyko Link: https://lore.kernel.org/r/tencent_A8D47429765566CC3C8B378496D036664A09@qq.com Signed-off-by: Viacheslav Dubeyko --- fs/hfsplus/xattr.c | 1 - 1 file changed, 1 deletion(-) diff --git a/fs/hfsplus/xattr.c b/fs/hfsplus/xattr.c index 452a1f9becb2..21a1c196c71f 100644 --- a/fs/hfsplus/xattr.c +++ b/fs/hfsplus/xattr.c @@ -317,7 +317,6 @@ static int hfsplus_create_attributes_file(struct super_block *sb) next_node++; } - hfsplus_mark_inode_dirty(HFSPLUS_ATTR_TREE_I(sb), HFSPLUS_I_ATTR_DIRTY); hfsplus_mark_inode_dirty(attr_file, HFSPLUS_I_ATTR_DIRTY); sbi->attr_tree = hfs_btree_open(sb, HFSPLUS_ATTR_CNID); From ca724ed2cb9b4223fbbc7fb250b876b6679f9a1e Mon Sep 17 00:00:00 2001 From: Rosen Penev Date: Fri, 24 Apr 2026 18:44:41 -0700 Subject: [PATCH 02/10] hfs/hfxplus: use kzalloc_flex() Instead of doing the math manually, use kzalloc_flex() which does it in a cleaner way. Also clarifies which is the flexible array member. Signed-off-by: Rosen Penev Reviewed-by: Viacheslav Dubeyko Signed-off-by: Viacheslav Dubeyko Link: https://lore.kernel.org/r/20260425014441.441252-1-rosenp@gmail.com Signed-off-by: Viacheslav Dubeyko --- fs/hfs/bnode.c | 6 ++---- fs/hfsplus/bnode.c | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/fs/hfs/bnode.c b/fs/hfs/bnode.c index 13d58c51fc46..b0165de7640d 100644 --- a/fs/hfs/bnode.c +++ b/fs/hfs/bnode.c @@ -344,7 +344,7 @@ static struct hfs_bnode *__hfs_bnode_create(struct hfs_btree *tree, u32 cnid) struct hfs_bnode *node, *node2; struct address_space *mapping; struct page *page; - int size, block, i, hash; + int block, i, hash; loff_t off; if (cnid >= tree->node_count) { @@ -352,9 +352,7 @@ static struct hfs_bnode *__hfs_bnode_create(struct hfs_btree *tree, u32 cnid) return NULL; } - size = sizeof(struct hfs_bnode) + tree->pages_per_bnode * - sizeof(struct page *); - node = kzalloc(size, GFP_KERNEL); + node = kzalloc_flex(*node, page, tree->pages_per_bnode, GFP_KERNEL); if (!node) return NULL; node->tree = tree; diff --git a/fs/hfsplus/bnode.c b/fs/hfsplus/bnode.c index f8b5a8ae58ff..a717fa008ddd 100644 --- a/fs/hfsplus/bnode.c +++ b/fs/hfsplus/bnode.c @@ -455,7 +455,7 @@ static struct hfs_bnode *__hfs_bnode_create(struct hfs_btree *tree, u32 cnid) struct hfs_bnode *node, *node2; struct address_space *mapping; struct page *page; - int size, block, i, hash; + int block, i, hash; loff_t off; if (cnid >= tree->node_count) { @@ -464,9 +464,7 @@ static struct hfs_bnode *__hfs_bnode_create(struct hfs_btree *tree, u32 cnid) return NULL; } - size = sizeof(struct hfs_bnode) + tree->pages_per_bnode * - sizeof(struct page *); - node = kzalloc(size, GFP_KERNEL); + node = kzalloc_flex(*node, page, tree->pages_per_bnode); if (!node) return NULL; node->tree = tree; From 5f63ac80aef2ee6bb58eab62e98c264774872da6 Mon Sep 17 00:00:00 2001 From: Viacheslav Dubeyko Date: Fri, 17 Apr 2026 14:49:41 -0700 Subject: [PATCH 03/10] hfsplus: fix issue of direct writes beyond end-of-file The xfstests' test-case generic/729 fails with error: sudo ./check generic/729 FSTYP -- hfsplus PLATFORM -- Linux/x86_64 hfsplus-testing-0001 7.0.0-rc1+ #36 SMP PREEMPT_DYNAMIC Fri Apr 17 12:40:51 PDT 2026 MKFS_OPTIONS -- /dev/loop51 MOUNT_OPTIONS -- /dev/loop51 /mnt/scratch generic/729 23s ... [failed, exit status 1]- output mismatch mmap-rw-fault: /mnt/test/mmap-rw-fault.tmp: Input/output error The hfsplus_get_block() only allows creating the next sequential block. It returns -EIO for direct writes beyond EOF. This patch waits for any in-flight DIO on the inode to finish. Then, it extends the file by calling generic_cont_expand_simple() with the goal to guarantee that blockdev_direct_IO() finds all needed blocks already reachable sequentially. And, finally, it flushes and invalidates the DIO range again so the page cache is clean before the direct write begins. sudo ./check generic/729 FSTYP -- hfsplus PLATFORM -- Linux/x86_64 hfsplus-testing-0001 7.0.0-rc1+ #40 SMP PREEMPT_DYNAMIC Thu Apr 16 15:41:03 PDT 2026 MKFS_OPTIONS -- /dev/loop51 MOUNT_OPTIONS -- /dev/loop51 /mnt/scratch generic/729 23s ... 32s Ran: generic/729 Passed all 1 tests Closes: https://github.com/hfs-linux-kernel/hfs-linux-kernel/issues/210 cc: John Paul Adrian Glaubitz cc: Yangtao Li cc: linux-fsdevel@vger.kernel.org Signed-off-by: Viacheslav Dubeyko Link: https://lore.kernel.org/r/20260417214940.2735557-2-slava@dubeyko.com Signed-off-by: Viacheslav Dubeyko --- fs/hfsplus/inode.c | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/fs/hfsplus/inode.c b/fs/hfsplus/inode.c index d05891ec492e..036ca516d851 100644 --- a/fs/hfsplus/inode.c +++ b/fs/hfsplus/inode.c @@ -125,9 +125,44 @@ static ssize_t hfsplus_direct_IO(struct kiocb *iocb, struct iov_iter *iter) struct file *file = iocb->ki_filp; struct address_space *mapping = file->f_mapping; struct inode *inode = mapping->host; + loff_t isize; size_t count = iov_iter_count(iter); + loff_t end = iocb->ki_pos + count; ssize_t ret; + /* + * The hfsplus_get_block() only allows creating the next sequential block. + * For direct writes beyond EOF, expand the file first. + */ + if (iov_iter_rw(iter) == WRITE && iocb->ki_pos > i_size_read(inode)) { + loff_t start_off, end_off; + loff_t start_page, end_page; + + isize = i_size_read(inode); + + /* + * Wait for any in-flight DIO on this inode to finish before + * calling generic_cont_expand_simple(). + */ + inode_dio_wait(inode); + + ret = generic_cont_expand_simple(inode, iocb->ki_pos); + if (ret) + return ret; + + start_off = isize; + end_off = (end > 0) ? end - 1 : end; + + ret = filemap_write_and_wait_range(mapping, start_off, end_off); + if (ret) + return ret; + + start_page = start_off >> PAGE_SHIFT; + end_page = end_off >> PAGE_SHIFT; + + invalidate_inode_pages2_range(mapping, start_page, end_page); + } + ret = blockdev_direct_IO(iocb, inode, iter, hfsplus_get_block); /* @@ -135,8 +170,7 @@ static ssize_t hfsplus_direct_IO(struct kiocb *iocb, struct iov_iter *iter) * blocks outside i_size. Trim these off again. */ if (unlikely(iov_iter_rw(iter) == WRITE && ret < 0)) { - loff_t isize = i_size_read(inode); - loff_t end = iocb->ki_pos + count; + isize = i_size_read(inode); if (end > isize) hfsplus_write_failed(mapping, end); From 3f95e2661574ff13f099dd13456751933c280628 Mon Sep 17 00:00:00 2001 From: Edward Adam Davis Date: Fri, 17 Apr 2026 07:44:02 +0800 Subject: [PATCH 04/10] hfsplus: Add a sanity check for btree node size Syzbot reported an uninit-value bug in [1] with a corrupted HFS+ image, during the file system mounting process, specifically while loading the catalog, a corrupted node_size value of 1 caused the rec_off argument passed to hfs_bnode_read_u16() (within hfs_bnode_find()) to be excessively large. Consequently, the function failed to return a valid value to initialize the off variable, triggering the bug [1]. Every node starts from BTree node descriptor: struct hfs_bnode_desc. So, the size of node cannot be lesser than that. However, technical specification declares that: "The node size (which is expressed in bytes) must be power of two, from 512 through 32,768, inclusive." Add a check for btree node size base on technical specification. [1] BUG: KMSAN: uninit-value in hfsplus_bnode_find+0x141c/0x1600 fs/hfsplus/bnode.c:584 hfsplus_bnode_find+0x141c/0x1600 fs/hfsplus/bnode.c:584 hfsplus_btree_open+0x169a/0x1e40 fs/hfsplus/btree.c:382 hfsplus_fill_super+0x111f/0x2770 fs/hfsplus/super.c:553 get_tree_bdev_flags+0x6e6/0x920 fs/super.c:1694 get_tree_bdev+0x38/0x50 fs/super.c:1717 hfsplus_get_tree+0x35/0x40 fs/hfsplus/super.c:709 vfs_get_tree+0xb3/0x5d0 fs/super.c:1754 fc_mount fs/namespace.c:1193 [inline] Fixes: 8ad2c6a36ac4 ("hfsplus: validate b-tree node 0 bitmap at mount time") Reported-by: syzbot+217eb327242d08197efb@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=217eb327242d08197efb Signed-off-by: Edward Adam Davis Reviewed-by: Viacheslav Dubeyko Signed-off-by: Viacheslav Dubeyko Link: https://lore.kernel.org/r/tencent_5ED373437A697F83A4A446B771577626CD05@qq.com Signed-off-by: Viacheslav Dubeyko --- fs/hfsplus/btree.c | 2 ++ include/linux/hfs_common.h | 1 + 2 files changed, 3 insertions(+) diff --git a/fs/hfsplus/btree.c b/fs/hfsplus/btree.c index 761c74ccd653..394542a47e60 100644 --- a/fs/hfsplus/btree.c +++ b/fs/hfsplus/btree.c @@ -365,6 +365,8 @@ struct hfs_btree *hfs_btree_open(struct super_block *sb, u32 id) } size = tree->node_size; + if (size < HFSPLUS_NODE_MINSZ || size > HFSPLUS_NODE_MXSZ) + goto fail_page; if (!is_power_of_2(size)) goto fail_page; if (!tree->node_count) diff --git a/include/linux/hfs_common.h b/include/linux/hfs_common.h index 07dfc39630ab..45fb4c9ff9f5 100644 --- a/include/linux/hfs_common.h +++ b/include/linux/hfs_common.h @@ -513,6 +513,7 @@ struct hfs_btree_header_rec { /* HFS+ BTree misc info */ #define HFSPLUS_TREE_HEAD 0 #define HFSPLUS_NODE_MXSZ 32768 +#define HFSPLUS_NODE_MINSZ 512 #define HFSPLUS_ATTR_TREE_NODE_SIZE 8192 #define HFSPLUS_BTREE_HDR_NODE_RECS_COUNT 3 #define HFSPLUS_BTREE_HDR_MAP_REC_INDEX 2 /* Map (bitmap) record in Header node */ From 966cb76fb2857a4242cab6ea2ea17acf818a3da7 Mon Sep 17 00:00:00 2001 From: Tristan Madani Date: Tue, 5 May 2026 11:12:58 +0000 Subject: [PATCH 05/10] hfs/hfsplus: fix u32 overflow in check_and_correct_requested_length check_and_correct_requested_length() compares (off + len) against node_size using u32 arithmetic. When the caller passes a large len value (e.g. from an underflowed subtraction in hfs_brec_remove()), off + len can wrap past 2^32 and produce a small result, causing the bounds check to pass when it should fail. For example, with off=14 and len=0xFFFFFFF2 (underflowed from data_off - keyoffset - size in hfs_brec_remove), off + len wraps to 6, which is less than a typical node_size of 512, so the check passes and the subsequent memmove reads ~4GB past the node buffer. Fix this by widening the addition to u64 before comparing against node_size. This prevents the u32 wrap while keeping the logic straightforward. Reported-by: syzbot+6df204b70bf3261691c5@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=6df204b70bf3261691c5 Tested-by: syzbot+6df204b70bf3261691c5@syzkaller.appspotmail.com Reported-by: syzbot+e76bf3d19b85350571ac@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=e76bf3d19b85350571ac Tested-by: syzbot+e76bf3d19b85350571ac@syzkaller.appspotmail.com Fixes: a431930c9bac ("hfs: fix slab-out-of-bounds in hfs_bnode_read()") Cc: stable@vger.kernel.org Signed-off-by: Tristan Madani Reviewed-by: Viacheslav Dubeyko Signed-off-by: Viacheslav Dubeyko Link: https://lore.kernel.org/r/20260505111300.3592757-2-tristmd@gmail.com Signed-off-by: Viacheslav Dubeyko --- fs/hfs/bnode.c | 2 +- fs/hfsplus/hfsplus_fs.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/hfs/bnode.c b/fs/hfs/bnode.c index b0165de7640d..3a8a3878d7b0 100644 --- a/fs/hfs/bnode.c +++ b/fs/hfs/bnode.c @@ -41,7 +41,7 @@ u32 check_and_correct_requested_length(struct hfs_bnode *node, u32 off, u32 len) node_size = node->tree->node_size; - if ((off + len) > node_size) { + if ((u64)off + len > node_size) { u32 new_len = node_size - off; pr_err("requested length has been corrected: " diff --git a/fs/hfsplus/hfsplus_fs.h b/fs/hfsplus/hfsplus_fs.h index 3545b8dbf11c..0e4268de9e60 100644 --- a/fs/hfsplus/hfsplus_fs.h +++ b/fs/hfsplus/hfsplus_fs.h @@ -600,7 +600,7 @@ u32 check_and_correct_requested_length(struct hfs_bnode *node, u32 off, u32 len) node_size = node->tree->node_size; - if ((off + len) > node_size) { + if ((u64)off + len > node_size) { u32 new_len = node_size - off; pr_err("requested length has been corrected: " From d67aadee19ffdf3cc8520c5a4f4d5b2916d30baf Mon Sep 17 00:00:00 2001 From: Tristan Madani Date: Tue, 5 May 2026 11:12:59 +0000 Subject: [PATCH 06/10] hfs/hfsplus: zero-initialize buffer in hfs_bnode_read hfs_bnode_read() can return early without writing to the output buffer when is_bnode_offset_valid() fails or when check_and_correct_requested_ length() corrects the length to zero. Callers such as hfs_bnode_read_ u16() and hfs_bnode_read_u8() pass stack-allocated buffers and use the result unconditionally, leading to KMSAN uninit-value reports. Rather than initializing at each individual call site, zero the buffer at the start of hfs_bnode_read() before any validation checks. This ensures all callers in both hfs and hfsplus get a deterministic zero value regardless of which early-return path is taken. Reported-by: syzbot+217eb327242d08197efb@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=217eb327242d08197efb Tested-by: syzbot+217eb327242d08197efb@syzkaller.appspotmail.com Fixes: a431930c9bac ("hfs: fix slab-out-of-bounds in hfs_bnode_read()") Cc: stable@vger.kernel.org Signed-off-by: Tristan Madani Reviewed-by: Viacheslav Dubeyko Signed-off-by: Viacheslav Dubeyko Link: https://lore.kernel.org/r/20260505111300.3592757-3-tristmd@gmail.com Signed-off-by: Viacheslav Dubeyko --- fs/hfs/bnode.c | 2 ++ fs/hfsplus/bnode.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/fs/hfs/bnode.c b/fs/hfs/bnode.c index 3a8a3878d7b0..da8e5342c91c 100644 --- a/fs/hfs/bnode.c +++ b/fs/hfs/bnode.c @@ -64,6 +64,8 @@ void hfs_bnode_read(struct hfs_bnode *node, void *buf, u32 off, u32 len) u32 bytes_read; u32 bytes_to_read; + memset(buf, 0, len); + if (!is_bnode_offset_valid(node, off)) return; diff --git a/fs/hfsplus/bnode.c b/fs/hfsplus/bnode.c index a717fa008ddd..d088fb7eb0df 100644 --- a/fs/hfsplus/bnode.c +++ b/fs/hfsplus/bnode.c @@ -25,6 +25,8 @@ void hfs_bnode_read(struct hfs_bnode *node, void *buf, u32 off, u32 len) struct page **pagep; u32 l; + memset(buf, 0, len); + if (!is_bnode_offset_valid(node, off)) return; From 4b0496432844628ad05a5b1efce329a3340174d2 Mon Sep 17 00:00:00 2001 From: Viacheslav Dubeyko Date: Tue, 5 May 2026 15:00:52 -0700 Subject: [PATCH 07/10] hfsplus: rework hfsplus_readdir() logic The xfstests' test-case generic/637 fails with error: FSTYP -- hfsplus PLATFORM -- Linux/x86_64 hfsplus-testing-0001 6.15.0-rc4+ #8 SMP PREEMPT_DYNAMIC Thu May 1 16:43:22 PDT 2025 MKFS_OPTIONS -- /dev/loop51 MOUNT_OPTIONS -- /dev/loop51 /mnt/scratch QA output created by 637 entries 7 and 8 have duplicate d_off 8 Found unlinked files in open dir (see xfstests-dev/results//generic/637.full for details) Debugging of the hfsplus_readdir() logic showed this: hfsplus: hfsplus_readdir(): 163 ctx->pos 0 hfsplus: hfsplus_readdir(): 189 ctx->pos 1 hfsplus: hfsplus_readdir(): 264 ctx->pos 2, ino 18 hfsplus: hfsplus_readdir(): 264 ctx->pos 3, ino 19 hfsplus: hfsplus_readdir(): 264 ctx->pos 4, ino 28 hfsplus: hfsplus_readdir(): 264 ctx->pos 5, ino 118 hfsplus: hfsplus_readdir(): 264 ctx->pos 6, ino 29 hfsplus: hfsplus_readdir(): 264 ctx->pos 7, ino 30 hfsplus: hfsplus_readdir(): 264 ctx->pos 8, ino 31 hfsplus: hfsplus_readdir(): 304 ctx->pos 8 hfsplus: hfsplus_unlink():420 dir->i_ino 17, inode->i_ino 28 hfsplus: hfsplus_readdir(): 141 ctx->pos 7 hfsplus: hfsplus_readdir(): 264 ctx->pos 7, ino 31 hfsplus: hfsplus_readdir(): 264 ctx->pos 8, ino 32 hfsplus: hfsplus_readdir(): 264 ctx->pos 9, ino 33 It means that hfsplus_readdir() stopped the processing of folder's items on ctx->pos 8, then, item with ino 28 has been deleted and hfsplus_readdir() re-started the logic from ctx->pos 7. As a result, previous and new sets of folder's items have overlapping values for the case of d_off 8. Currently, HFS+ has very complicated and fragile logic of rd->file->f_pos correction in hfsplus_delete_cat(). This patch removes this logic and it stores the current pos into hfsplus_readdir_data. Finally, if rd->pos == ctx->pos then hfsplus_readdir() tries to find the position in b-tree's node by means of hfsplus_cat_key. This position is used to re-start the folder's content traversal. sudo ./check generic/637 FSTYP -- hfsplus PLATFORM -- Linux/x86_64 hfsplus-testing-0001 7.1.0-rc1+ #44 SMP PREEMPT_DYNAMIC Mon May 4 15:58:45 PDT 2026 MKFS_OPTIONS -- /dev/loop51 MOUNT_OPTIONS -- /dev/loop51 /mnt/scratch generic/637 22s ... 22s Ran: generic/637 Passed all 1 tests Closes: https://github.com/hfs-linux-kernel/hfs-linux-kernel/issues/198 cc: John Paul Adrian Glaubitz cc: Yangtao Li cc: linux-fsdevel@vger.kernel.org Signed-off-by: Viacheslav Dubeyko Link: https://lore.kernel.org/r/20260505220051.2854696-2-slava@dubeyko.com Signed-off-by: Viacheslav Dubeyko --- fs/hfsplus/catalog.c | 11 ----------- fs/hfsplus/dir.c | 28 +++++++++++----------------- fs/hfsplus/hfsplus_fs.h | 5 +---- fs/hfsplus/inode.c | 2 -- fs/hfsplus/super.c | 2 -- 5 files changed, 12 insertions(+), 36 deletions(-) diff --git a/fs/hfsplus/catalog.c b/fs/hfsplus/catalog.c index 9bc8a6c48fd3..776ce36cf076 100644 --- a/fs/hfsplus/catalog.c +++ b/fs/hfsplus/catalog.c @@ -333,7 +333,6 @@ int hfsplus_delete_cat(u32 cnid, struct inode *dir, const struct qstr *str) struct super_block *sb = dir->i_sb; struct hfs_find_data fd; struct hfsplus_fork_raw fork; - struct list_head *pos; int err, off; u16 type; @@ -392,16 +391,6 @@ int hfsplus_delete_cat(u32 cnid, struct inode *dir, const struct qstr *str) hfsplus_free_fork(sb, cnid, &fork, HFSPLUS_TYPE_RSRC); } - /* we only need to take spinlock for exclusion with ->release() */ - spin_lock(&HFSPLUS_I(dir)->open_dir_lock); - list_for_each(pos, &HFSPLUS_I(dir)->open_dir_list) { - struct hfsplus_readdir_data *rd = - list_entry(pos, struct hfsplus_readdir_data, list); - if (fd.tree->keycmp(fd.search_key, (void *)&rd->key) < 0) - rd->file->f_pos--; - } - spin_unlock(&HFSPLUS_I(dir)->open_dir_lock); - err = hfs_brec_remove(&fd); if (err) goto out; diff --git a/fs/hfsplus/dir.c b/fs/hfsplus/dir.c index 47194370c2c5..8bf6c7cdd9a8 100644 --- a/fs/hfsplus/dir.c +++ b/fs/hfsplus/dir.c @@ -185,7 +185,15 @@ static int hfsplus_readdir(struct file *file, struct dir_context *ctx) } if (ctx->pos >= inode->i_size) goto out; - err = hfs_brec_goto(&fd, ctx->pos - 1); + rd = file->private_data; + if (rd && rd->pos == ctx->pos) { + memcpy(fd.search_key, &rd->key, sizeof(struct hfsplus_cat_key)); + err = hfs_brec_find(&fd, hfs_find_rec_by_key); + if (err == -ENOENT) + err = hfs_brec_goto(&fd, 1); + } else { + err = hfs_brec_goto(&fd, ctx->pos - 1); + } if (err) goto out; for (;;) { @@ -261,7 +269,6 @@ static int hfsplus_readdir(struct file *file, struct dir_context *ctx) if (err) goto out; } - rd = file->private_data; if (!rd) { rd = kmalloc_obj(struct hfsplus_readdir_data); if (!rd) { @@ -269,15 +276,8 @@ static int hfsplus_readdir(struct file *file, struct dir_context *ctx) goto out; } file->private_data = rd; - rd->file = file; - spin_lock(&HFSPLUS_I(inode)->open_dir_lock); - list_add(&rd->list, &HFSPLUS_I(inode)->open_dir_list); - spin_unlock(&HFSPLUS_I(inode)->open_dir_lock); } - /* - * Can be done after the list insertion; exclusion with - * hfsplus_delete_cat() is provided by directory lock. - */ + rd->pos = ctx->pos; memcpy(&rd->key, fd.key, sizeof(struct hfsplus_cat_key)); out: kfree(strbuf); @@ -287,13 +287,7 @@ static int hfsplus_readdir(struct file *file, struct dir_context *ctx) static int hfsplus_dir_release(struct inode *inode, struct file *file) { - struct hfsplus_readdir_data *rd = file->private_data; - if (rd) { - spin_lock(&HFSPLUS_I(inode)->open_dir_lock); - list_del(&rd->list); - spin_unlock(&HFSPLUS_I(inode)->open_dir_lock); - kfree(rd); - } + kfree(file->private_data); return 0; } diff --git a/fs/hfsplus/hfsplus_fs.h b/fs/hfsplus/hfsplus_fs.h index 0e4268de9e60..ec04b82ad927 100644 --- a/fs/hfsplus/hfsplus_fs.h +++ b/fs/hfsplus/hfsplus_fs.h @@ -214,8 +214,6 @@ struct hfsplus_inode_info { sector_t fs_blocks; u8 userflags; /* BSD user file flags */ u32 subfolders; /* Subfolder count (HFSX only) */ - struct list_head open_dir_list; - spinlock_t open_dir_lock; loff_t phys_size; struct inode vfs_inode; @@ -272,8 +270,7 @@ struct hfs_find_data { }; struct hfsplus_readdir_data { - struct list_head list; - struct file *file; + loff_t pos; struct hfsplus_cat_key key; }; diff --git a/fs/hfsplus/inode.c b/fs/hfsplus/inode.c index 036ca516d851..1de9cc97b42e 100644 --- a/fs/hfsplus/inode.c +++ b/fs/hfsplus/inode.c @@ -483,8 +483,6 @@ struct inode *hfsplus_new_inode(struct super_block *sb, struct inode *dir, simple_inode_init_ts(inode); hip = HFSPLUS_I(inode); - INIT_LIST_HEAD(&hip->open_dir_list); - spin_lock_init(&hip->open_dir_lock); mutex_init(&hip->extents_lock); atomic_set(&hip->opencnt, 0); hip->extent_state = 0; diff --git a/fs/hfsplus/super.c b/fs/hfsplus/super.c index 40a0feda716b..5777e31de45a 100644 --- a/fs/hfsplus/super.c +++ b/fs/hfsplus/super.c @@ -91,8 +91,6 @@ struct inode *hfsplus_iget(struct super_block *sb, unsigned long ino) HFSPLUS_I(inode)->fs_blocks = 0; HFSPLUS_I(inode)->userflags = 0; HFSPLUS_I(inode)->subfolders = 0; - INIT_LIST_HEAD(&HFSPLUS_I(inode)->open_dir_list); - spin_lock_init(&HFSPLUS_I(inode)->open_dir_lock); HFSPLUS_I(inode)->phys_size = 0; if (inode->i_ino >= HFSPLUS_FIRSTUSER_CNID || From 6592287869bffee91f59363e51de5f971ec2bd9d Mon Sep 17 00:00:00 2001 From: Viacheslav Dubeyko Date: Thu, 14 May 2026 12:55:19 -0700 Subject: [PATCH 08/10] hfs: fix incorrect inode ID assignment in hfs_new_inode() The xfstests' test-case generic/003 reveals the HFS volume corruption: sudo ./check generic/003 FSTYP -- hfs PLATFORM -- Linux/x86_64 hfsplus-testing-0001 7.0.0-rc1+ #18 SMP PREEMPT_DYNAMIC Fri Mar 13 17:54:19 PDT 2026 MKFS_OPTIONS -- /dev/loop51 MOUNT_OPTIONS -- /dev/loop51 /mnt/scratch generic/003 51s ... _check_generic_filesystem: filesystem on /dev/loop51 is inconsistent sudo fsck.hfs -d /dev/loop51 ** /dev/loop51 Using cacheBlockSize=32K cacheTotalBlock=1024 cacheSize=32768K. Executing fsck_hfs (version 540.1-Linux). ** Checking HFS volume. The volume name is untitled ** Checking extents overflow file. ** Checking catalog file. ** Checking catalog hierarchy. ** Checking volume bitmap. ** Checking volume information. invalid MDB drNxtCNID Master Directory Block needs minor repair (1, 0) Verify Status: VIStat = 0x8000, ABTStat = 0x0000 EBTStat = 0x0000 CBTStat = 0x0000 CatStat = 0x00000000 ** Repairing volume. ** Rechecking volume. ** Checking HFS volume. The volume name is untitled ** Checking extents overflow file. ** Checking catalog file. ** Checking catalog hierarchy. ** Checking volume bitmap. ** Checking volume information. ** The volume untitled was repaired successfully. The reason of corruption is incorrect value of drNxtCNID (next CNID) in the MDB or superblock. The generic/003 test-case creates several new inodes: kernel: run fstests generic/003 hfs: hfs_mdb_get():179 next_id 16 hfs: hfs_mdb_get():179 next_id 16 hfs: hfs_new_inode():208 next_id 17 hfs: hfs_new_inode():208 next_id 18 hfs: hfs_mdb_commit():307 next_id 18 hfs: hfs_mdb_get():179 next_id 18 hfs: hfs_new_inode():208 next_id 19 hfs: hfs_new_inode():208 next_id 20 hfs_mdb_commit():307 next_id 20 hfs: hfs_mdb_get():179 next_id 20 hfs: hfs_new_inode():208 next_id 21 hfs: hfs_mdb_commit():307 next_id 21 hfs: hfs_mdb_get():179 next_id 21 The final assigned CNID was 21 but fsck correct it on 22. It is possible to see that the reason of the issue is incrementing the next_id value at first and assigning already incremented value to the inode->i_ino: struct inode *hfs_new_inode(...) { next_id = atomic64_inc_return(&HFS_SB(sb)->next_id); inode->i_ino = (u32)next_id; } This patch fixes the issue by assigning the decremented value to inode->i_ino. Fixes: a06ec283e125 ("hfs: add logic of correcting a next unused CNID") cc: John Paul Adrian Glaubitz cc: Yangtao Li cc: linux-fsdevel@vger.kernel.org Signed-off-by: Viacheslav Dubeyko Link: https://lore.kernel.org/r/20260514195518.354108-2-slava@dubeyko.com Signed-off-by: Viacheslav Dubeyko --- fs/hfs/inode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/hfs/inode.c b/fs/hfs/inode.c index 89b33a9d46d5..1cbba7345038 100644 --- a/fs/hfs/inode.c +++ b/fs/hfs/inode.c @@ -204,7 +204,7 @@ struct inode *hfs_new_inode(struct inode *dir, const struct qstr *name, umode_t pr_err("cannot create new inode: next CNID exceeds limit\n"); goto out_discard; } - inode->i_ino = (u32)next_id; + inode->i_ino = (u32)next_id - 1; inode->i_mode = mode; inode->i_uid = current_fsuid(); inode->i_gid = current_fsgid(); From f14ccd7baf7793020c9d1bc34ae7a8e048546f2c Mon Sep 17 00:00:00 2001 From: Viacheslav Dubeyko Date: Thu, 14 May 2026 12:56:31 -0700 Subject: [PATCH 09/10] hfs: disable the updating of file access times (atime) The xfstests' test-case generic/003 fails with errors: sudo ./check generic/003 FSTYP -- hfs PLATFORM -- Linux/x86_64 hfsplus-testing-0001 7.0.0-rc1+ #18 SMP PREEMPT_DYNAMIC Fri Mar 13 17:54:19 PDT 2026 MKFS_OPTIONS -- /dev/loop51 MOUNT_OPTIONS -- /dev/loop51 /mnt/scratch output mismatch QA output created by 003 ERROR: access time has changed for file1 after remount ERROR: access time has changed after modifying file1 ERROR: change time has not been updated after changing file1 ERROR: access time has changed for file in read-only filesystem Silence is golden This patch fixes the issue with change time by adding inode_set_ctime_current() and mark_inode_dirty() in hfs_rename(). Also, it reworks hfs_inode_setattr() by changing simple_inode_init_ts() on inode_set_mtime_to_ts() and inode_set_ctime_current() calls. HFS hasn't any field in on-disk layout that can keep the file/folder access times (atime). It was added setting of SB_NOATIME in SB_NOATIME. Finally, we have only atime related errors in generic/003 output: sudo ./check generic/003 FSTYP -- hfs PLATFORM -- Linux/x86_64 hfsplus-testing-0001 7.1.0-rc1+ #52 SMP PREEMPT_DYNAMIC Wed May 13 15:04:37 PDT 2026 MKFS_OPTIONS -- /dev/loop51 MOUNT_OPTIONS -- /dev/loop51 /mnt/scratch QA output created by 003 ERROR: access time has not been updated after accessing file1 first time ERROR: access time has not been updated after accessing file2 ERROR: access time has not been updated after accessing file3 second time ERROR: access time has not been updated after accessing file3 third time Silence is golden The generic/003 test-case needs to be disabled for HFS case because it cannot support the file/folder access times (atime). Closes: https://github.com/hfs-linux-kernel/hfs-linux-kernel/issues/3 cc: John Paul Adrian Glaubitz cc: Yangtao Li cc: linux-fsdevel@vger.kernel.org Signed-off-by: Viacheslav Dubeyko Link: https://lore.kernel.org/r/20260514195630.354206-2-slava@dubeyko.com Signed-off-by: Viacheslav Dubeyko --- fs/hfs/dir.c | 9 +++++++-- fs/hfs/inode.c | 15 ++++++++++----- fs/hfs/super.c | 2 +- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/fs/hfs/dir.c b/fs/hfs/dir.c index f5e7efe924e7..702c16ea954b 100644 --- a/fs/hfs/dir.c +++ b/fs/hfs/dir.c @@ -306,10 +306,15 @@ static int hfs_rename(struct mnt_idmap *idmap, struct inode *old_dir, res = hfs_cat_move(d_inode(old_dentry)->i_ino, old_dir, &old_dentry->d_name, new_dir, &new_dentry->d_name); - if (!res) + if (!res) { + struct inode *inode = d_inode(old_dentry); + hfs_cat_build_key(old_dir->i_sb, - (btree_key *)&HFS_I(d_inode(old_dentry))->cat_key, + (btree_key *)&HFS_I(inode)->cat_key, new_dir->i_ino, &new_dentry->d_name); + inode_set_ctime_current(inode); + mark_inode_dirty(inode); + } return res; } diff --git a/fs/hfs/inode.c b/fs/hfs/inode.c index 1cbba7345038..b2329de151ec 100644 --- a/fs/hfs/inode.c +++ b/fs/hfs/inode.c @@ -348,6 +348,7 @@ static int hfs_read_inode(struct inode *inode, void *data) struct hfs_iget_data *idata = data; struct hfs_sb_info *hsb = HFS_SB(inode->i_sb); hfs_cat_rec *rec; + struct timespec64 mtime; HFS_I(inode)->flags = 0; HFS_I(inode)->rsrc_inode = NULL; @@ -383,8 +384,10 @@ static int hfs_read_inode(struct inode *inode, void *data) inode->i_mode |= S_IWUGO; inode->i_mode &= ~hsb->s_file_umask; inode->i_mode |= S_IFREG; - inode_set_mtime_to_ts(inode, - inode_set_atime_to_ts(inode, inode_set_ctime_to_ts(inode, hfs_m_to_utime(rec->file.MdDat)))); + mtime = hfs_m_to_utime(rec->file.MdDat); + inode_set_ctime_to_ts(inode, mtime); + inode_set_atime_to_ts(inode, mtime); + inode_set_mtime_to_ts(inode, mtime); inode->i_op = &hfs_file_inode_operations; inode->i_fop = &hfs_file_operations; inode->i_mapping->a_ops = &hfs_aops; @@ -394,8 +397,10 @@ static int hfs_read_inode(struct inode *inode, void *data) inode->i_size = be16_to_cpu(rec->dir.Val) + 2; HFS_I(inode)->fs_blocks = 0; inode->i_mode = S_IFDIR | (S_IRWXUGO & ~hsb->s_dir_umask); - inode_set_mtime_to_ts(inode, - inode_set_atime_to_ts(inode, inode_set_ctime_to_ts(inode, hfs_m_to_utime(rec->dir.MdDat)))); + mtime = hfs_m_to_utime(rec->dir.MdDat); + inode_set_ctime_to_ts(inode, mtime); + inode_set_atime_to_ts(inode, mtime); + inode_set_mtime_to_ts(inode, mtime); inode->i_op = &hfs_dir_inode_operations; inode->i_fop = &hfs_dir_operations; break; @@ -665,7 +670,7 @@ int hfs_inode_setattr(struct mnt_idmap *idmap, struct dentry *dentry, truncate_setsize(inode, attr->ia_size); hfs_file_truncate(inode); - simple_inode_init_ts(inode); + inode_set_mtime_to_ts(inode, inode_set_ctime_current(inode)); } setattr_copy(&nop_mnt_idmap, inode, attr); diff --git a/fs/hfs/super.c b/fs/hfs/super.c index a4f2a2bfa6d3..a466c401f6bb 100644 --- a/fs/hfs/super.c +++ b/fs/hfs/super.c @@ -338,7 +338,7 @@ static int hfs_fill_super(struct super_block *sb, struct fs_context *fc) sbi->sb = sb; sb->s_op = &hfs_super_operations; sb->s_xattr = hfs_xattr_handlers; - sb->s_flags |= SB_NODIRATIME; + sb->s_flags |= SB_NOATIME | SB_NODIRATIME; mutex_init(&sbi->bitmap_lock); res = hfs_mdb_get(sb); From 7fde7e806657fbe0d33f489521b488eed94f9b39 Mon Sep 17 00:00:00 2001 From: Viacheslav Dubeyko Date: Tue, 19 May 2026 15:28:12 -0700 Subject: [PATCH 10/10] hfs: rework hfsplus_readdir() logic The xfstests' test-case generic/637 fails with error: FSTYP -- hfs PLATFORM -- Linux/x86_64 kvm-xfstests 6.15.0-rc4-xfstests-g00b827f0cffa #1 SMP PREEMPT_DYNAMIC Fri May 25 MKFS_OPTIONS -- /dev/vdc MOUNT_OPTIONS -- /dev/vdc /vdc QA output created by 637 entries 7 and 8 have duplicate d_off 8 Found unlinked files in open dir (see xfstests-dev/results//generic/637.full for details) Likewise HFS+, currently, HFS has very complicated and fragile logic of rd->file->f_pos correction in hfs_delete_cat(). This patch removes this logic and it stores the current pos into hfs_readdir_data. Finally, if rd->pos == ctx->pos then hfs_readdir() tries to find the position in b-tree's node by means of hfs_cat_key. This position is used to re-start the folder's content traversal. sudo ./check generic/637 FSTYP -- hfs PLATFORM -- Linux/x86_64 hfsplus-testing-0001 7.1.0-rc1+ #55 SMP PREEMPT_DYNAMIC Tue May 19 15:18:02 PDT 2026 MKFS_OPTIONS -- /dev/loop51 MOUNT_OPTIONS -- /dev/loop51 /mnt/scratch generic/637 32s ... 31s Ran: generic/637 Passed all 1 tests Closes: https://github.com/hfs-linux-kernel/hfs-linux-kernel/issues/65 cc: John Paul Adrian Glaubitz cc: Yangtao Li cc: linux-fsdevel@vger.kernel.org Signed-off-by: Viacheslav Dubeyko Link: https://lore.kernel.org/r/20260519222811.1311071-2-slava@dubeyko.com Signed-off-by: Viacheslav Dubeyko --- fs/hfs/catalog.c | 9 --------- fs/hfs/dir.c | 28 +++++++++++----------------- fs/hfs/hfs.h | 3 +-- fs/hfs/hfs_fs.h | 2 -- fs/hfs/inode.c | 4 ---- 5 files changed, 12 insertions(+), 34 deletions(-) diff --git a/fs/hfs/catalog.c b/fs/hfs/catalog.c index 7f5339ee57c1..1bfa36d71e24 100644 --- a/fs/hfs/catalog.c +++ b/fs/hfs/catalog.c @@ -340,7 +340,6 @@ int hfs_cat_delete(u32 cnid, struct inode *dir, const struct qstr *str) { struct super_block *sb; struct hfs_find_data fd; - struct hfs_readdir_data *rd; int res, type; hfs_dbg("name %s, cnid %u\n", str ? str->name : NULL, cnid); @@ -366,14 +365,6 @@ int hfs_cat_delete(u32 cnid, struct inode *dir, const struct qstr *str) } } - /* we only need to take spinlock for exclusion with ->release() */ - spin_lock(&HFS_I(dir)->open_dir_lock); - list_for_each_entry(rd, &HFS_I(dir)->open_dir_list, list) { - if (fd.tree->keycmp(fd.search_key, (void *)&rd->key) < 0) - rd->file->f_pos--; - } - spin_unlock(&HFS_I(dir)->open_dir_lock); - res = hfs_brec_remove(&fd); if (res) goto out; diff --git a/fs/hfs/dir.c b/fs/hfs/dir.c index 702c16ea954b..509c77a99625 100644 --- a/fs/hfs/dir.c +++ b/fs/hfs/dir.c @@ -97,7 +97,15 @@ static int hfs_readdir(struct file *file, struct dir_context *ctx) } if (ctx->pos >= inode->i_size) goto out; - err = hfs_brec_goto(&fd, ctx->pos - 1); + rd = file->private_data; + if (rd && rd->pos == ctx->pos) { + memcpy(fd.search_key, &rd->key, sizeof(struct hfs_cat_key)); + err = hfs_brec_find(&fd); + if (err == -ENOENT) + err = hfs_brec_goto(&fd, 1); + } else { + err = hfs_brec_goto(&fd, ctx->pos - 1); + } if (err) goto out; @@ -146,7 +154,6 @@ static int hfs_readdir(struct file *file, struct dir_context *ctx) if (err) goto out; } - rd = file->private_data; if (!rd) { rd = kmalloc_obj(struct hfs_readdir_data); if (!rd) { @@ -154,15 +161,8 @@ static int hfs_readdir(struct file *file, struct dir_context *ctx) goto out; } file->private_data = rd; - rd->file = file; - spin_lock(&HFS_I(inode)->open_dir_lock); - list_add(&rd->list, &HFS_I(inode)->open_dir_list); - spin_unlock(&HFS_I(inode)->open_dir_lock); } - /* - * Can be done after the list insertion; exclusion with - * hfs_delete_cat() is provided by directory lock. - */ + rd->pos = ctx->pos; memcpy(&rd->key, &fd.key->cat, sizeof(struct hfs_cat_key)); out: hfs_find_exit(&fd); @@ -171,13 +171,7 @@ static int hfs_readdir(struct file *file, struct dir_context *ctx) static int hfs_dir_release(struct inode *inode, struct file *file) { - struct hfs_readdir_data *rd = file->private_data; - if (rd) { - spin_lock(&HFS_I(inode)->open_dir_lock); - list_del(&rd->list); - spin_unlock(&HFS_I(inode)->open_dir_lock); - kfree(rd); - } + kfree(file->private_data); return 0; } diff --git a/fs/hfs/hfs.h b/fs/hfs/hfs.h index 3f2293ff6fdd..2e958c46bc0a 100644 --- a/fs/hfs/hfs.h +++ b/fs/hfs/hfs.h @@ -14,8 +14,7 @@ /*======== Data structures kept in memory ========*/ struct hfs_readdir_data { - struct list_head list; - struct file *file; + loff_t pos; struct hfs_cat_key key; }; diff --git a/fs/hfs/hfs_fs.h b/fs/hfs/hfs_fs.h index ac0e83f77a0f..97e8d1f96d6d 100644 --- a/fs/hfs/hfs_fs.h +++ b/fs/hfs/hfs_fs.h @@ -36,8 +36,6 @@ struct hfs_inode_info { struct hfs_cat_key cat_key; - struct list_head open_dir_list; - spinlock_t open_dir_lock; struct inode *rsrc_inode; struct mutex extents_lock; diff --git a/fs/hfs/inode.c b/fs/hfs/inode.c index b2329de151ec..ab52ad2d2ad8 100644 --- a/fs/hfs/inode.c +++ b/fs/hfs/inode.c @@ -195,8 +195,6 @@ struct inode *hfs_new_inode(struct inode *dir, const struct qstr *name, umode_t err = -ERANGE; mutex_init(&HFS_I(inode)->extents_lock); - INIT_LIST_HEAD(&HFS_I(inode)->open_dir_list); - spin_lock_init(&HFS_I(inode)->open_dir_lock); hfs_cat_build_key(sb, (btree_key *)&HFS_I(inode)->cat_key, dir->i_ino, name); next_id = atomic64_inc_return(&HFS_SB(sb)->next_id); if (next_id > U32_MAX) { @@ -353,8 +351,6 @@ static int hfs_read_inode(struct inode *inode, void *data) HFS_I(inode)->flags = 0; HFS_I(inode)->rsrc_inode = NULL; mutex_init(&HFS_I(inode)->extents_lock); - INIT_LIST_HEAD(&HFS_I(inode)->open_dir_list); - spin_lock_init(&HFS_I(inode)->open_dir_lock); /* Initialize the inode */ inode->i_uid = hsb->s_uid;