From 21668258995bdc99b48f372c2d7d52539c2f2567 Mon Sep 17 00:00:00 2001 From: Davy Felipe Date: Wed, 16 Sep 2026 19:40:16 -0300 Subject: [PATCH 1/6] hfs: return -EPERM for unsupported setattr changes hfs_inode_setattr() rejects unsupported UID, GID and mode changes, but returns the value stored in error when the filesystem is not mounted with the quiet option. At this point error is zero, as execution has already passed a successful setattr_prepare() call. As a result, unsupported attribute changes are incorrectly reported to userspace as successful even though the inode remains unchanged. Return -EPERM for these operations while preserving the existing behavior for quiet mounts. A chown test on an HFS image reproduces the problem. Without the fix, chown 1234:1234 returns success while ownership remains 0:0. With the fix, the same operation fails with EPERM and ownership remains unchanged. Signed-off-by: Davy Felipe Reviewed-by: Viacheslav Dubeyko Signed-off-by: Viacheslav Dubeyko Link: https://lore.kernel.org/r/20260916224016.266800-1-davyfelipe34@gmail.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 2aef3c36a150..cce3a7bb95e9 100644 --- a/fs/hfs/inode.c +++ b/fs/hfs/inode.c @@ -663,7 +663,7 @@ int hfs_inode_setattr(struct mnt_idmap *idmap, struct dentry *dentry, ((S_ISDIR(inode->i_mode) && (attr->ia_mode != inode->i_mode)) || (attr->ia_mode & ~HFS_VALID_MODE_BITS)))) { - return hsb->s_quiet ? 0 : error; + return hsb->s_quiet ? 0 : -EPERM; } /* map file permissions to the closest allowable permissions in HFS */ From dce0e0248205bb3dc5b4c6793cd2ecb453152259 Mon Sep 17 00:00:00 2001 From: Bruno Produit Date: Mon, 21 Sep 2026 17:37:29 +0200 Subject: [PATCH 2/6] hfs/hfsplus: serialize B-tree close against folio release B-tree nodes with a zero reference count remain in the node hash until folio reclaim or tree teardown frees them. The folio release callbacks remove nodes while holding hash_lock, but hfs_btree_close() walks and frees the same hash without that lock. Reclaim can therefore unhash and free a node after close has loaded its pointer, causing a use-after-free or double-free. The following syzkaller crashes seem to be the same UAF in {hfs,hfsplus}_btree_close() (or {hfs,hfsplus}_bnode_unhash()) and follow the same pattern, but do not contain a reproducer to confirm. Detach each node with hfs_bnode_unhash() while holding hash_lock before inspecting and freeing it. Drop the lock before hfs_bnode_free() so a large tree is not freed while holding a spinlock. Apply the same fix to the matching HFS+ implementation. Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Reported-by: syzbot+d729df28d933979e017a@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=d729df28d933979e017a Reported-by: syzbot+2eac7d175baf21e6a5d5@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=2eac7d175baf21e6a5d5 Reported-by: syzbot+7155b2fe09e033c91381@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=7155b2fe09e033c91381 Reported-by: syzbot+adeb387cede15eb11607@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=adeb387cede15eb11607 Reported-by: syzbot+ae7f2423f3648100506d@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=ae7f2423f3648100506d Assisted-by: Codex:gpt-5.6-sol Reported-by: Kyle Zeng Signed-off-by: Kyle Zeng Signed-off-by: Bruno Produit Reviewed-by: Viacheslav Dubeyko Tested-by: Viacheslav Dubeyko Link: https://lore.kernel.org/r/20260921153729.600313-1-bruno.produit@trailofbits.com Signed-off-by: Viacheslav Dubeyko --- fs/hfs/btree.c | 7 +++++-- fs/hfsplus/btree.c | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/fs/hfs/btree.c b/fs/hfs/btree.c index 41b4e8fc9463..4f0ddc76e8c5 100644 --- a/fs/hfs/btree.c +++ b/fs/hfs/btree.c @@ -310,15 +310,18 @@ void hfs_btree_close(struct hfs_btree *tree) return; for (i = 0; i < NODE_HASH_SIZE; i++) { + spin_lock(&tree->hash_lock); while ((node = tree->node_hash[i])) { - tree->node_hash[i] = node->next_hash; + hfs_bnode_unhash(node); + spin_unlock(&tree->hash_lock); if (atomic_read(&node->refcnt)) pr_err("node %d:%d still has %d user(s)!\n", node->tree->cnid, node->this, atomic_read(&node->refcnt)); hfs_bnode_free(node); - tree->node_hash_cnt--; + spin_lock(&tree->hash_lock); } + spin_unlock(&tree->hash_lock); } iput(tree->inode); kfree(tree); diff --git a/fs/hfsplus/btree.c b/fs/hfsplus/btree.c index 2ea8cd5658e1..bd4dbdbc8f98 100644 --- a/fs/hfsplus/btree.c +++ b/fs/hfsplus/btree.c @@ -417,16 +417,19 @@ void hfs_btree_close(struct hfs_btree *tree) return; for (i = 0; i < NODE_HASH_SIZE; i++) { + spin_lock(&tree->hash_lock); while ((node = tree->node_hash[i])) { - tree->node_hash[i] = node->next_hash; + hfs_bnode_unhash(node); + spin_unlock(&tree->hash_lock); if (atomic_read(&node->refcnt)) pr_crit("node %d:%d " "still has %d user(s)!\n", node->tree->cnid, node->this, atomic_read(&node->refcnt)); hfs_bnode_free(node); - tree->node_hash_cnt--; + spin_lock(&tree->hash_lock); } + spin_unlock(&tree->hash_lock); } iput(tree->inode); kfree(tree); From 7f33d2e5b0a6ab249807ba9a08e46793b0166ad8 Mon Sep 17 00:00:00 2001 From: Viacheslav Dubeyko Date: Fri, 18 Sep 2026 17:16:05 -0700 Subject: [PATCH 3/6] hfsplus: add Unicode combining-class and legacy decomposition data macOS's own fsck_hfs rejects a catalog name as "Illegal name" in two cases this driver's decomposition tables don't currently account for: - a decomposed name whose combining marks aren't in Unicode canonical combining-class order (fsck_hfs's FixDecomps(), CatalogCheck.c); - a small, fixed set of characters whose canonical decomposition either wasn't yet defined, or was later corrected, since the driver's existing hfsplus_decompose_table (Apple Technote #1150) predates those corrections, which macOS has applied since Mac OS X 10.2 ("Jaguar", 2002). This is purely a data patch and adds no new behavior on its own: - hfsplus_ccc_table: canonical combining class per BMP code point, generated from the Unicode Character Database version 12.1.0 (https://www.unicode.org/Public/12.1.0/ucd/extracted/ DerivedCombiningClass.txt), the same UCD version fs/unicode's README.utf8data documents. - hfsplus_legacy_decomp_table and hfsplus_legacy_seq_fixups: the corrected decompositions and sequence replacements fsck_hfs itself requires, decoded from hfsprogs 540.1's fsck_hfs.tproj/dfalib/DecompData.h (upstream: Apple's diskdev_cmds, https://opensource.apple.com/source/diskdev_cmds/). The three new struct types and the extern declarations for all of tables.c's tables (existing and new) move into a new fs/hfsplus/ unicode.h, replacing the ad hoc externs that used to live in hfsplus_fs.h. 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/20260919001607.2777138-2-slava@dubeyko.com Signed-off-by: Viacheslav Dubeyko --- fs/hfsplus/hfsplus_fs.h | 6 +- fs/hfsplus/tables.c | 397 ++++++++++++++++++++++++++++++++++++++ fs/hfsplus/unicode.c | 1 + fs/hfsplus/unicode.h | 59 ++++++ fs/hfsplus/unicode_test.c | 1 + 5 files changed, 459 insertions(+), 5 deletions(-) create mode 100644 fs/hfsplus/unicode.h diff --git a/fs/hfsplus/hfsplus_fs.h b/fs/hfsplus/hfsplus_fs.h index 1e5b58e6a13f..916e6552e3f0 100644 --- a/fs/hfsplus/hfsplus_fs.h +++ b/fs/hfsplus/hfsplus_fs.h @@ -17,6 +17,7 @@ #include #include #include "hfsplus_raw.h" +#include "unicode.h" /* Runtime config options */ #define HFSPLUS_DEF_CR_TYPE 0x3F3F3F3F /* '????' */ @@ -486,11 +487,6 @@ void hfsplus_mark_mdb_dirty(struct super_block *sb); void hfsplus_prepare_volume_header_for_commit(struct hfsplus_vh *vhdr); int hfsplus_commit_superblock(struct super_block *sb); -/* tables.c */ -extern u16 hfsplus_case_fold_table[]; -extern u16 hfsplus_decompose_table[]; -extern u16 hfsplus_compose_table[]; - /* unicode.c */ int hfsplus_strcasecmp(const struct hfsplus_unistr *s1, const struct hfsplus_unistr *s2); diff --git a/fs/hfsplus/tables.c b/fs/hfsplus/tables.c index a5fb8ee7d019..3bbdc83debb5 100644 --- a/fs/hfsplus/tables.c +++ b/fs/hfsplus/tables.c @@ -3244,3 +3244,400 @@ u16 hfsplus_compose_table[] = { /* 0x0342 0x0314 0x03c9 0x0345 */ 0x1fa7, 0x0000, }; + +/* + * Unicode canonical combining class (ccc) table, restricted to the Basic + * Multilingual Plane: HFS+ names are stored as UTF-16 and this kernel's + * wchar_t (see MAX_WCHAR_T in include/linux/nls.h) tops out at U+FFFF, so + * no HFS+ catalog name can contain a code point above the BMP anyway. + * + * Generated from the Unicode Character Database, version 12.1.0 + * (https://www.unicode.org/Public/12.1.0/ucd/extracted/DerivedCombiningClass.txt), + * the same UCD version fs/unicode/README.utf8data documents. Class 0 + * ("Not_Reordered") is the default for every code point not listed here + * and needs no entry. + * + * Used by hfsplus_canonical_reorder() to apply the Unicode Canonical + * Ordering Algorithm to a decomposed name, matching what macOS's own + * fsck_hfs (FixDecomps()) requires of a stored catalog name. + */ +struct hfsplus_ccc_range hfsplus_ccc_table[] = { + { 0x0300, 0x0314, 230 }, + { 0x0315, 0x0315, 232 }, + { 0x0316, 0x0319, 220 }, + { 0x031a, 0x031a, 232 }, + { 0x031b, 0x031b, 216 }, + { 0x031c, 0x0320, 220 }, + { 0x0321, 0x0322, 202 }, + { 0x0323, 0x0326, 220 }, + { 0x0327, 0x0328, 202 }, + { 0x0329, 0x0333, 220 }, + { 0x0334, 0x0338, 1 }, + { 0x0339, 0x033c, 220 }, + { 0x033d, 0x0344, 230 }, + { 0x0345, 0x0345, 240 }, + { 0x0346, 0x0346, 230 }, + { 0x0347, 0x0349, 220 }, + { 0x034a, 0x034c, 230 }, + { 0x034d, 0x034e, 220 }, + { 0x0350, 0x0352, 230 }, + { 0x0353, 0x0356, 220 }, + { 0x0357, 0x0357, 230 }, + { 0x0358, 0x0358, 232 }, + { 0x0359, 0x035a, 220 }, + { 0x035b, 0x035b, 230 }, + { 0x035c, 0x035c, 233 }, + { 0x035d, 0x035e, 234 }, + { 0x035f, 0x035f, 233 }, + { 0x0360, 0x0361, 234 }, + { 0x0362, 0x0362, 233 }, + { 0x0363, 0x036f, 230 }, + { 0x0483, 0x0487, 230 }, + { 0x0591, 0x0591, 220 }, + { 0x0592, 0x0595, 230 }, + { 0x0596, 0x0596, 220 }, + { 0x0597, 0x0599, 230 }, + { 0x059a, 0x059a, 222 }, + { 0x059b, 0x059b, 220 }, + { 0x059c, 0x05a1, 230 }, + { 0x05a2, 0x05a7, 220 }, + { 0x05a8, 0x05a9, 230 }, + { 0x05aa, 0x05aa, 220 }, + { 0x05ab, 0x05ac, 230 }, + { 0x05ad, 0x05ad, 222 }, + { 0x05ae, 0x05ae, 228 }, + { 0x05af, 0x05af, 230 }, + { 0x05b0, 0x05b0, 10 }, + { 0x05b1, 0x05b1, 11 }, + { 0x05b2, 0x05b2, 12 }, + { 0x05b3, 0x05b3, 13 }, + { 0x05b4, 0x05b4, 14 }, + { 0x05b5, 0x05b5, 15 }, + { 0x05b6, 0x05b6, 16 }, + { 0x05b7, 0x05b7, 17 }, + { 0x05b8, 0x05b8, 18 }, + { 0x05b9, 0x05ba, 19 }, + { 0x05bb, 0x05bb, 20 }, + { 0x05bc, 0x05bc, 21 }, + { 0x05bd, 0x05bd, 22 }, + { 0x05bf, 0x05bf, 23 }, + { 0x05c1, 0x05c1, 24 }, + { 0x05c2, 0x05c2, 25 }, + { 0x05c4, 0x05c4, 230 }, + { 0x05c5, 0x05c5, 220 }, + { 0x05c7, 0x05c7, 18 }, + { 0x0610, 0x0617, 230 }, + { 0x0618, 0x0618, 30 }, + { 0x0619, 0x0619, 31 }, + { 0x061a, 0x061a, 32 }, + { 0x064b, 0x064b, 27 }, + { 0x064c, 0x064c, 28 }, + { 0x064d, 0x064d, 29 }, + { 0x064e, 0x064e, 30 }, + { 0x064f, 0x064f, 31 }, + { 0x0650, 0x0650, 32 }, + { 0x0651, 0x0651, 33 }, + { 0x0652, 0x0652, 34 }, + { 0x0653, 0x0654, 230 }, + { 0x0655, 0x0656, 220 }, + { 0x0657, 0x065b, 230 }, + { 0x065c, 0x065c, 220 }, + { 0x065d, 0x065e, 230 }, + { 0x065f, 0x065f, 220 }, + { 0x0670, 0x0670, 35 }, + { 0x06d6, 0x06dc, 230 }, + { 0x06df, 0x06e2, 230 }, + { 0x06e3, 0x06e3, 220 }, + { 0x06e4, 0x06e4, 230 }, + { 0x06e7, 0x06e8, 230 }, + { 0x06ea, 0x06ea, 220 }, + { 0x06eb, 0x06ec, 230 }, + { 0x06ed, 0x06ed, 220 }, + { 0x0711, 0x0711, 36 }, + { 0x0730, 0x0730, 230 }, + { 0x0731, 0x0731, 220 }, + { 0x0732, 0x0733, 230 }, + { 0x0734, 0x0734, 220 }, + { 0x0735, 0x0736, 230 }, + { 0x0737, 0x0739, 220 }, + { 0x073a, 0x073a, 230 }, + { 0x073b, 0x073c, 220 }, + { 0x073d, 0x073d, 230 }, + { 0x073e, 0x073e, 220 }, + { 0x073f, 0x0741, 230 }, + { 0x0742, 0x0742, 220 }, + { 0x0743, 0x0743, 230 }, + { 0x0744, 0x0744, 220 }, + { 0x0745, 0x0745, 230 }, + { 0x0746, 0x0746, 220 }, + { 0x0747, 0x0747, 230 }, + { 0x0748, 0x0748, 220 }, + { 0x0749, 0x074a, 230 }, + { 0x07eb, 0x07f1, 230 }, + { 0x07f2, 0x07f2, 220 }, + { 0x07f3, 0x07f3, 230 }, + { 0x07fd, 0x07fd, 220 }, + { 0x0816, 0x0819, 230 }, + { 0x081b, 0x0823, 230 }, + { 0x0825, 0x0827, 230 }, + { 0x0829, 0x082d, 230 }, + { 0x0859, 0x085b, 220 }, + { 0x08d3, 0x08d3, 220 }, + { 0x08d4, 0x08e1, 230 }, + { 0x08e3, 0x08e3, 220 }, + { 0x08e4, 0x08e5, 230 }, + { 0x08e6, 0x08e6, 220 }, + { 0x08e7, 0x08e8, 230 }, + { 0x08e9, 0x08e9, 220 }, + { 0x08ea, 0x08ec, 230 }, + { 0x08ed, 0x08ef, 220 }, + { 0x08f0, 0x08f0, 27 }, + { 0x08f1, 0x08f1, 28 }, + { 0x08f2, 0x08f2, 29 }, + { 0x08f3, 0x08f5, 230 }, + { 0x08f6, 0x08f6, 220 }, + { 0x08f7, 0x08f8, 230 }, + { 0x08f9, 0x08fa, 220 }, + { 0x08fb, 0x08ff, 230 }, + { 0x093c, 0x093c, 7 }, + { 0x094d, 0x094d, 9 }, + { 0x0951, 0x0951, 230 }, + { 0x0952, 0x0952, 220 }, + { 0x0953, 0x0954, 230 }, + { 0x09bc, 0x09bc, 7 }, + { 0x09cd, 0x09cd, 9 }, + { 0x09fe, 0x09fe, 230 }, + { 0x0a3c, 0x0a3c, 7 }, + { 0x0a4d, 0x0a4d, 9 }, + { 0x0abc, 0x0abc, 7 }, + { 0x0acd, 0x0acd, 9 }, + { 0x0b3c, 0x0b3c, 7 }, + { 0x0b4d, 0x0b4d, 9 }, + { 0x0bcd, 0x0bcd, 9 }, + { 0x0c4d, 0x0c4d, 9 }, + { 0x0c55, 0x0c55, 84 }, + { 0x0c56, 0x0c56, 91 }, + { 0x0cbc, 0x0cbc, 7 }, + { 0x0ccd, 0x0ccd, 9 }, + { 0x0d3b, 0x0d3c, 9 }, + { 0x0d4d, 0x0d4d, 9 }, + { 0x0dca, 0x0dca, 9 }, + { 0x0e38, 0x0e39, 103 }, + { 0x0e3a, 0x0e3a, 9 }, + { 0x0e48, 0x0e4b, 107 }, + { 0x0eb8, 0x0eb9, 118 }, + { 0x0eba, 0x0eba, 9 }, + { 0x0ec8, 0x0ecb, 122 }, + { 0x0f18, 0x0f19, 220 }, + { 0x0f35, 0x0f35, 220 }, + { 0x0f37, 0x0f37, 220 }, + { 0x0f39, 0x0f39, 216 }, + { 0x0f71, 0x0f71, 129 }, + { 0x0f72, 0x0f72, 130 }, + { 0x0f74, 0x0f74, 132 }, + { 0x0f7a, 0x0f7d, 130 }, + { 0x0f80, 0x0f80, 130 }, + { 0x0f82, 0x0f83, 230 }, + { 0x0f84, 0x0f84, 9 }, + { 0x0f86, 0x0f87, 230 }, + { 0x0fc6, 0x0fc6, 220 }, + { 0x1037, 0x1037, 7 }, + { 0x1039, 0x103a, 9 }, + { 0x108d, 0x108d, 220 }, + { 0x135d, 0x135f, 230 }, + { 0x1714, 0x1714, 9 }, + { 0x1734, 0x1734, 9 }, + { 0x17d2, 0x17d2, 9 }, + { 0x17dd, 0x17dd, 230 }, + { 0x18a9, 0x18a9, 228 }, + { 0x1939, 0x1939, 222 }, + { 0x193a, 0x193a, 230 }, + { 0x193b, 0x193b, 220 }, + { 0x1a17, 0x1a17, 230 }, + { 0x1a18, 0x1a18, 220 }, + { 0x1a60, 0x1a60, 9 }, + { 0x1a75, 0x1a7c, 230 }, + { 0x1a7f, 0x1a7f, 220 }, + { 0x1ab0, 0x1ab4, 230 }, + { 0x1ab5, 0x1aba, 220 }, + { 0x1abb, 0x1abc, 230 }, + { 0x1abd, 0x1abd, 220 }, + { 0x1b34, 0x1b34, 7 }, + { 0x1b44, 0x1b44, 9 }, + { 0x1b6b, 0x1b6b, 230 }, + { 0x1b6c, 0x1b6c, 220 }, + { 0x1b6d, 0x1b73, 230 }, + { 0x1baa, 0x1baa, 9 }, + { 0x1bab, 0x1bab, 9 }, + { 0x1be6, 0x1be6, 7 }, + { 0x1bf2, 0x1bf3, 9 }, + { 0x1c37, 0x1c37, 7 }, + { 0x1cd0, 0x1cd2, 230 }, + { 0x1cd4, 0x1cd4, 1 }, + { 0x1cd5, 0x1cd9, 220 }, + { 0x1cda, 0x1cdb, 230 }, + { 0x1cdc, 0x1cdf, 220 }, + { 0x1ce0, 0x1ce0, 230 }, + { 0x1ce2, 0x1ce8, 1 }, + { 0x1ced, 0x1ced, 220 }, + { 0x1cf4, 0x1cf4, 230 }, + { 0x1cf8, 0x1cf9, 230 }, + { 0x1dc0, 0x1dc1, 230 }, + { 0x1dc2, 0x1dc2, 220 }, + { 0x1dc3, 0x1dc9, 230 }, + { 0x1dca, 0x1dca, 220 }, + { 0x1dcb, 0x1dcc, 230 }, + { 0x1dcd, 0x1dcd, 234 }, + { 0x1dce, 0x1dce, 214 }, + { 0x1dcf, 0x1dcf, 220 }, + { 0x1dd0, 0x1dd0, 202 }, + { 0x1dd1, 0x1df5, 230 }, + { 0x1df6, 0x1df6, 232 }, + { 0x1df7, 0x1df8, 228 }, + { 0x1df9, 0x1df9, 220 }, + { 0x1dfb, 0x1dfb, 230 }, + { 0x1dfc, 0x1dfc, 233 }, + { 0x1dfd, 0x1dfd, 220 }, + { 0x1dfe, 0x1dfe, 230 }, + { 0x1dff, 0x1dff, 220 }, + { 0x20d0, 0x20d1, 230 }, + { 0x20d2, 0x20d3, 1 }, + { 0x20d4, 0x20d7, 230 }, + { 0x20d8, 0x20da, 1 }, + { 0x20db, 0x20dc, 230 }, + { 0x20e1, 0x20e1, 230 }, + { 0x20e5, 0x20e6, 1 }, + { 0x20e7, 0x20e7, 230 }, + { 0x20e8, 0x20e8, 220 }, + { 0x20e9, 0x20e9, 230 }, + { 0x20ea, 0x20eb, 1 }, + { 0x20ec, 0x20ef, 220 }, + { 0x20f0, 0x20f0, 230 }, + { 0x2cef, 0x2cf1, 230 }, + { 0x2d7f, 0x2d7f, 9 }, + { 0x2de0, 0x2dff, 230 }, + { 0x302a, 0x302a, 218 }, + { 0x302b, 0x302b, 228 }, + { 0x302c, 0x302c, 232 }, + { 0x302d, 0x302d, 222 }, + { 0x302e, 0x302f, 224 }, + { 0x3099, 0x309a, 8 }, + { 0xa66f, 0xa66f, 230 }, + { 0xa674, 0xa67d, 230 }, + { 0xa69e, 0xa69f, 230 }, + { 0xa6f0, 0xa6f1, 230 }, + { 0xa806, 0xa806, 9 }, + { 0xa8c4, 0xa8c4, 9 }, + { 0xa8e0, 0xa8f1, 230 }, + { 0xa92b, 0xa92d, 220 }, + { 0xa953, 0xa953, 9 }, + { 0xa9b3, 0xa9b3, 7 }, + { 0xa9c0, 0xa9c0, 9 }, + { 0xaab0, 0xaab0, 230 }, + { 0xaab2, 0xaab3, 230 }, + { 0xaab4, 0xaab4, 220 }, + { 0xaab7, 0xaab8, 230 }, + { 0xaabe, 0xaabf, 230 }, + { 0xaac1, 0xaac1, 230 }, + { 0xaaf6, 0xaaf6, 9 }, + { 0xabed, 0xabed, 9 }, + { 0xfb1e, 0xfb1e, 26 }, + { 0xfe20, 0xfe26, 230 }, + { 0xfe27, 0xfe2d, 220 }, + { 0xfe2e, 0xfe2f, 230 }, +}; + +/* + * Some HFS Plus catalog names contain characters whose canonical + * decomposition either was not yet defined, or was later corrected, in + * the version of Unicode this file's tables above were generated from + * (Apple Technote #1150, itself derived from pre-Unicode-3.0/pre-"Jaguar" + * (Mac OS X 10.2) data). macOS's own fsck_hfs has carried a permanent + * "FixDecomps" fixup for exactly this since 2002 (see + * fsck_hfs.tproj/dfalib/CatalogCheck.c and DecompData.h in the hfsprogs / + * diskdev_cmds sources) and flags any catalog name it finds still using + * the old form as "Illegal name". The two tables and functions below are + * a port of that same fixup data, decoded from hfsprogs 540.1's + * DecompData.h, so this driver stores names the same way macOS has since + * 10.2 rather than the way the original HFS Plus format documentation + * described in 2001. + */ + +/* Category A: single code point -> missing canonical decomposition */ +/* 44 entries */ +struct hfsplus_legacy_decomp hfsplus_legacy_decomp_table[] = { + { 0x01f8, 2, { 0x004e, 0x0300 } }, + { 0x01f9, 2, { 0x006e, 0x0300 } }, + { 0x0218, 2, { 0x0053, 0x0326 } }, + { 0x0219, 2, { 0x0073, 0x0326 } }, + { 0x021a, 2, { 0x0054, 0x0326 } }, + { 0x021b, 2, { 0x0074, 0x0326 } }, + { 0x021e, 2, { 0x0048, 0x030c } }, + { 0x021f, 2, { 0x0068, 0x030c } }, + { 0x0226, 2, { 0x0041, 0x0307 } }, + { 0x0227, 2, { 0x0061, 0x0307 } }, + { 0x0228, 2, { 0x0045, 0x0327 } }, + { 0x0229, 2, { 0x0065, 0x0327 } }, + { 0x022a, 3, { 0x004f, 0x0308, 0x0304 } }, + { 0x022b, 3, { 0x006f, 0x0308, 0x0304 } }, + { 0x022c, 3, { 0x004f, 0x0303, 0x0304 } }, + { 0x022d, 3, { 0x006f, 0x0303, 0x0304 } }, + { 0x022e, 2, { 0x004f, 0x0307 } }, + { 0x022f, 2, { 0x006f, 0x0307 } }, + { 0x0230, 3, { 0x004f, 0x0307, 0x0304 } }, + { 0x0231, 3, { 0x006f, 0x0307, 0x0304 } }, + { 0x0232, 2, { 0x0059, 0x0304 } }, + { 0x0233, 2, { 0x0079, 0x0304 } }, + { 0x0400, 2, { 0x0415, 0x0300 } }, + { 0x040d, 2, { 0x0418, 0x0300 } }, + { 0x0450, 2, { 0x0435, 0x0300 } }, + { 0x045d, 2, { 0x0438, 0x0300 } }, + { 0x04ec, 2, { 0x042d, 0x0308 } }, + { 0x04ed, 2, { 0x044d, 0x0308 } }, + { 0x0622, 2, { 0x0627, 0x0653 } }, + { 0x0623, 2, { 0x0627, 0x0654 } }, + { 0x0624, 2, { 0x0648, 0x0654 } }, + { 0x0625, 2, { 0x0627, 0x0655 } }, + { 0x0626, 2, { 0x064a, 0x0654 } }, + { 0x06c0, 2, { 0x06d5, 0x0654 } }, + { 0x06c2, 2, { 0x06c1, 0x0654 } }, + { 0x06d3, 2, { 0x06d2, 0x0654 } }, + { 0x0a33, 2, { 0x0a32, 0x0a3c } }, + { 0x0a36, 2, { 0x0a38, 0x0a3c } }, + { 0x0dda, 2, { 0x0dd9, 0x0dca } }, + { 0x0ddc, 2, { 0x0dd9, 0x0dcf } }, + { 0x0ddd, 3, { 0x0dd9, 0x0dcf, 0x0dca } }, + { 0x0dde, 2, { 0x0dd9, 0x0ddf } }, + { 0x1026, 2, { 0x1025, 0x102e } }, + { 0xfb1d, 2, { 0x05d9, 0x05b4 } }, +}; + +/* Category B: 2-3 code point sequence -> corrected replacement */ +/* 25 entries */ +struct hfsplus_legacy_seq_fixup hfsplus_legacy_seq_fixups[] = { + { 2, { 0x00a8, 0x030d }, 2, { 0x00a8, 0x0301 } }, + { 2, { 0x0306, 0x0307 }, 1, { 0x0310 } }, + { 2, { 0x0308, 0x030d }, 2, { 0x0308, 0x0301 } }, + { 2, { 0x0391, 0x030d }, 2, { 0x0391, 0x0301 } }, + { 2, { 0x0395, 0x030d }, 2, { 0x0395, 0x0301 } }, + { 2, { 0x0397, 0x030d }, 2, { 0x0397, 0x0301 } }, + { 2, { 0x0399, 0x030d }, 2, { 0x0399, 0x0301 } }, + { 2, { 0x039f, 0x030d }, 2, { 0x039f, 0x0301 } }, + { 2, { 0x03a5, 0x030d }, 2, { 0x03a5, 0x0301 } }, + { 2, { 0x03a9, 0x030d }, 2, { 0x03a9, 0x0301 } }, + { 2, { 0x03b1, 0x030d }, 2, { 0x03b1, 0x0301 } }, + { 2, { 0x03b5, 0x030d }, 2, { 0x03b5, 0x0301 } }, + { 2, { 0x03b7, 0x030d }, 2, { 0x03b7, 0x0301 } }, + { 2, { 0x03b9, 0x030d }, 2, { 0x03b9, 0x0301 } }, + { 2, { 0x03bf, 0x030d }, 2, { 0x03bf, 0x0301 } }, + { 2, { 0x03c5, 0x030d }, 2, { 0x03c5, 0x0301 } }, + { 2, { 0x03c9, 0x030d }, 2, { 0x03c9, 0x0301 } }, + { 2, { 0x03d2, 0x030d }, 2, { 0x03d2, 0x0301 } }, + { 2, { 0x09ac, 0x09bc }, 1, { 0x09b0 } }, + { 2, { 0x0a21, 0x0a3c }, 1, { 0x0a5c } }, + { 2, { 0x0b2f, 0x0b3c }, 1, { 0x0b5f } }, + { 2, { 0x0e4d, 0x0e32 }, 1, { 0x0e33 } }, + { 2, { 0x0ecd, 0x0eb2 }, 1, { 0x0eb3 } }, + { 3, { 0x0fb2, 0x0f80, 0x0f71 }, 1, { 0x0f77 } }, + { 3, { 0x0fb3, 0x0f80, 0x0f71 }, 1, { 0x0f79 } }, +}; diff --git a/fs/hfsplus/unicode.c b/fs/hfsplus/unicode.c index 008fec186382..93d39481e477 100644 --- a/fs/hfsplus/unicode.c +++ b/fs/hfsplus/unicode.c @@ -16,6 +16,7 @@ #include "hfsplus_fs.h" #include "hfsplus_raw.h" +#include "unicode.h" /* Fold the case of a unicode char, given the 16 bit value */ /* Returns folded char, or 0 if ignorable */ diff --git a/fs/hfsplus/unicode.h b/fs/hfsplus/unicode.h new file mode 100644 index 000000000000..3957ebccc232 --- /dev/null +++ b/fs/hfsplus/unicode.h @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Unicode related declarations + */ + +#ifndef _LINUX_HFSPLUS_UNICODE_H +#define _LINUX_HFSPLUS_UNICODE_H + +/* + * struct hfsplus_ccc_range - one run of consecutive BMP code points that + * share the same Unicode canonical combining class + * @first: the first UTF-16 code point in the run + * @last: the last code point in the run (inclusive) + * @combining_class: canonical combining class (ccc) shared by @first..@last + */ +struct hfsplus_ccc_range { + u16 first; + u16 last; + u8 combining_class; +}; + +/* + * struct hfsplus_legacy_decomp - corrected canonical decomposition + * @uc: the code point (a UTF-16 code unit) to look up + * @len: number of valid code units in @repl + * @repl: corrected decomposition sequence (base letter + combining marks) + */ +struct hfsplus_legacy_decomp { + u16 uc; + u8 len; +#define HFSPLUS_LEGACY_DECOMP_MAX_LEN (3) + u16 repl[HFSPLUS_LEGACY_DECOMP_MAX_LEN]; +}; + +/* + * struct hfsplus_legacy_seq_fixup - corrected replacement for a short + * sequence of already-decomposed code units + * @match_len: number of valid code units in @match + * @match: decomposed sequence to look for in the buffer + * @repl_len: number of valid code units in @repl + * @repl: corrected sequence to substitute in place of @match + */ +struct hfsplus_legacy_seq_fixup { + u8 match_len; +#define HFSPLUS_LEGACY_SEQ_MAX_MATCH (3) + u16 match[HFSPLUS_LEGACY_SEQ_MAX_MATCH]; + u8 repl_len; +#define HFSPLUS_LEGACY_SEQ_MAX_REPL (2) + u16 repl[HFSPLUS_LEGACY_SEQ_MAX_REPL]; +}; + +extern u16 hfsplus_case_fold_table[]; +extern u16 hfsplus_decompose_table[]; +extern u16 hfsplus_compose_table[]; +extern struct hfsplus_ccc_range hfsplus_ccc_table[]; +extern struct hfsplus_legacy_decomp hfsplus_legacy_decomp_table[]; +extern struct hfsplus_legacy_seq_fixup hfsplus_legacy_seq_fixups[]; + +#endif /* _LINUX_HFSPLUS_UNICODE_H */ diff --git a/fs/hfsplus/unicode_test.c b/fs/hfsplus/unicode_test.c index 7f6b2a3c69d9..32a64029d77b 100644 --- a/fs/hfsplus/unicode_test.c +++ b/fs/hfsplus/unicode_test.c @@ -10,6 +10,7 @@ #include #include #include "hfsplus_fs.h" +#include "unicode.h" struct test_mock_string_env { struct hfsplus_unistr str1; From bbe5ebcbc006ac3c7353759fc593fb7cb7de76a1 Mon Sep 17 00:00:00 2001 From: Viacheslav Dubeyko Date: Fri, 18 Sep 2026 17:16:06 -0700 Subject: [PATCH 4/6] hfsplus: canonicalize decomposed catalog names like fsck_hfs expects hfsplus_decompose_table (Apple Technote #1150) decomposes each source character entirely on its own. It never reorders the result, and it predates a handful of later corrections to the decomposition standard. Two concrete consequences, both of which macOS's own fsck_hfs treats as "Illegal name" (CatalogCheck.c: CheckCatalogName(), FixDecomps()): - When combining marks contributed by two different source characters end up adjacent, this driver stores them in input order rather than ascending Unicode combining-class order. - A small, fixed set of characters either aren't decomposed at all, or are decomposed into a sequence macOS corrected back in Mac OS X 10.2 ("Jaguar"). This is reliably reproducible: xfstests generic/339 exercises dirhash collisions by creating many files with randomized Unicode names, and the resulting HFS+ volume fails _check_generic_filesystem afterward with a long run of "Illegal name" reports from fsck.hfsplus. Fix hfsplus_asc2uni() to store names the way current macOS does, using the Unicode tables: - hfsplus_canonical_reorder() applies the Unicode Canonical Ordering Algorithm: each maximal run of nonzero-combining-class code units is stable-sorted into ascending class order. - decompose_unichar() falls back to hfsplus_legacy_decompose() for the characters missing from Apple Technote #1150's table. - hfsplus_fixup_legacy_sequences() substitutes any of fsck_hfs's known bad sequences with their corrected form. hfsplus_decompose_str() ties these together and is now shared by hfsplus_asc2uni(), hfsplus_hash_dentry() and hfsplus_compare_dentry(), so hashing and comparison always agree with what actually gets stored - otherwise two byte-for-byte different but canonically-equivalent spellings of a name could hash differently or fail to compare equal against the catalog entry a create() of either would produce. 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/20260919001607.2777138-3-slava@dubeyko.com Signed-off-by: Viacheslav Dubeyko --- fs/hfsplus/tables.c | 99 +++++++++++ fs/hfsplus/unicode.c | 401 ++++++++++++++++++++++++++++--------------- fs/hfsplus/unicode.h | 4 + 3 files changed, 366 insertions(+), 138 deletions(-) diff --git a/fs/hfsplus/tables.c b/fs/hfsplus/tables.c index 3bbdc83debb5..2101201ab973 100644 --- a/fs/hfsplus/tables.c +++ b/fs/hfsplus/tables.c @@ -3641,3 +3641,102 @@ struct hfsplus_legacy_seq_fixup hfsplus_legacy_seq_fixups[] = { { 3, { 0x0fb2, 0x0f80, 0x0f71 }, 1, { 0x0f77 } }, { 3, { 0x0fb3, 0x0f80, 0x0f71 }, 1, { 0x0f79 } }, }; + +/* + * Look up the Unicode canonical combining class of a BMP code point. + * Returns 0 ("Not_Reordered") for any code point not listed in the table, + * which includes every ordinary base character. + */ +u8 hfsplus_combining_class(u16 c) +{ + int lo = 0, hi = ARRAY_SIZE(hfsplus_ccc_table) - 1; + + while (lo <= hi) { + int mid = (lo + hi) / 2; + const struct hfsplus_ccc_range *r = &hfsplus_ccc_table[mid]; + + if (c < r->first) + hi = mid - 1; + else if (c > r->last) + lo = mid + 1; + else + return r->combining_class; + } + + return 0; +} + +/* + * Look up the corrected canonical decomposition for a single BMP code + * point that Apple Technote #1150's own decomposition table (above) + * lacks. Returns NULL (and leaves *size alone) if @uc isn't one of these. + */ +const u16 *hfsplus_legacy_decompose(u16 uc, int *size) +{ + int lo = 0, hi = ARRAY_SIZE(hfsplus_legacy_decomp_table) - 1; + + while (lo <= hi) { + int mid = (lo + hi) / 2; + const struct hfsplus_legacy_decomp *e = + &hfsplus_legacy_decomp_table[mid]; + + if (uc < e->uc) + hi = mid - 1; + else if (uc > e->uc) + lo = mid + 1; + else { + *size = e->len; + return e->repl; + } + } + + return NULL; +} + +/* + * Scan an already decomposed and canonically-reordered code unit buffer + * for any of the short legacy sequences above and replace them in place + * with their corrected form. @len is updated to the buffer's new length. + */ +void hfsplus_fixup_legacy_sequences(u16 *buf, int *len) +{ + int i = 0; + + while (i < *len) { + unsigned int j; + bool matched = false; + + for (j = 0; j < ARRAY_SIZE(hfsplus_legacy_seq_fixups); j++) { + struct hfsplus_legacy_seq_fixup *f = + &hfsplus_legacy_seq_fixups[j]; + u16 *src, *dst; + size_t copy_len; + int k; + + if (i + f->match_len > *len) + continue; + for (k = 0; k < f->match_len; k++) + if (buf[i + k] != f->match[k]) + break; + if (k != f->match_len) + continue; + + dst = &buf[i + f->repl_len]; + src = &buf[i + f->match_len]; + copy_len = (*len - i - f->match_len) * sizeof(*buf); + memmove(dst, src, copy_len); + + dst = &buf[i]; + src = f->repl; + copy_len = f->repl_len * sizeof(*buf); + memcpy(dst, src, copy_len); + + *len += f->repl_len - f->match_len; + matched = true; + break; + } + + if (!matched) + i++; + } +} diff --git a/fs/hfsplus/unicode.c b/fs/hfsplus/unicode.c index 93d39481e477..59b8788a69a9 100644 --- a/fs/hfsplus/unicode.c +++ b/fs/hfsplus/unicode.c @@ -442,45 +442,264 @@ static u16 *decompose_unichar(wchar_t uc, int *size, u16 *hangul_buffer) *size = hfsplus_try_decompose_hangul(uc, result); if (*size == 0) result = hfsplus_decompose_nonhangul(uc, size); + if (!result) { + /* + * Not every character with a canonical decomposition is in + * Apple Technote #1150's own table above; a small, fixed + * set was only added to the decomposition standard (or had + * its decomposition corrected) after that table was + * generated. hfsplus_legacy_decompose() covers those. + */ + const u16 *legacy = hfsplus_legacy_decompose(uc, size); + + if (legacy) + result = memcpy(hangul_buffer, legacy, + *size * sizeof(*legacy)); + } return result; } +/* + * Apply the Unicode Canonical Ordering Algorithm to a decomposed name held + * as plain host-order code units: within each maximal run of characters + * that have a nonzero combining class, stable-sort the run into ascending + * combining-class order. A character with combining class 0 always starts + * a new run and is never itself reordered. + * + * hfsplus_decompose_table decomposes each source character on its own; it + * says nothing about how the decompositions of two different source + * characters should be ordered relative to each other when both produce + * combining marks that end up adjacent. Without this pass, such a + * sequence can be stored in an order that macOS's own fsck_hfs + * (FixDecomps() in CatalogCheck.c) considers illegal, even though every + * individual character was decomposed correctly. + */ +static void hfsplus_canonical_reorder(u16 *ustr, int len) +{ + int i; + + for (i = 1; i < len; i++) { + u8 cls = hfsplus_combining_class(ustr[i]); + int j = i; + + if (!cls) + continue; + + while (j > 0) { + u8 prev_cls = hfsplus_combining_class(ustr[j - 1]); + u16 tmp; + + if (!prev_cls || prev_cls <= cls) + break; + + tmp = ustr[j]; + ustr[j] = ustr[j - 1]; + ustr[j - 1] = tmp; + j--; + } + } +} + +#define HFSPLUS_HANGUL_MAX_JAMO (3) /* L + V + optional T */ + +static_assert(HFSPLUS_HANGUL_MAX_JAMO >= HFSPLUS_LEGACY_DECOMP_MAX_LEN, + "hangul_buffer must fit the longest legacy decomposition too"); + +/* + * Decompose and canonically reorder an entire Linux name, as plain + * host-order code units. hfsplus_asc2uni(), hfsplus_hash_dentry() and + * hfsplus_compare_dentry() all go through this so that storage, hashing + * and comparison always agree on what a given name canonicalizes to. + * + * @out must hold at least @max_len entries, which must not exceed + * HFSPLUS_MAX_STRLEN. Returns the number of code units written. If + * @consumed is non-NULL, it is set to the number of input bytes actually + * consumed, which is less than @len when @out fills up first. + */ +static int hfsplus_decompose_str(struct super_block *sb, const char *astr, + int len, int max_len, int name_type, + u16 *out, int *consumed) +{ + int decompose = !test_bit(HFSPLUS_SB_NODECOMPOSE, &HFSPLUS_SB(sb)->flags); + const char *start = astr; + int outlen = 0; + + while (outlen < max_len && len > 0) { + u16 *dstr; + u16 dhangul[HFSPLUS_HANGUL_MAX_JAMO]; + int dsize, size; + wchar_t c; + + size = asc2unichar(sb, astr, len, &c, name_type); + + dstr = decompose ? decompose_unichar(c, &dsize, dhangul) : NULL; + if (dstr) { + if (outlen + dsize > max_len) + break; + do { + out[outlen++] = *dstr++; + } while (--dsize > 0); + } else { + out[outlen++] = c; + } + + astr += size; + len -= size; + } + + hfsplus_canonical_reorder(out, outlen); + hfsplus_fixup_legacy_sequences(out, &outlen); + if (consumed) + *consumed = astr - start; + return outlen; +} + int hfsplus_asc2uni(struct super_block *sb, struct hfsplus_unistr *ustr, int max_unistr_len, const char *astr, int len, int name_type) { - int size, dsize, decompose; - u16 *dstr, outlen = 0; - wchar_t c; - u16 dhangul[3]; + u16 buf[HFSPLUS_MAX_STRLEN]; + int outlen, i, consumed; - decompose = !test_bit(HFSPLUS_SB_NODECOMPOSE, &HFSPLUS_SB(sb)->flags); - while (outlen < max_unistr_len && len > 0) { - size = asc2unichar(sb, astr, len, &c, name_type); + if (max_unistr_len > HFSPLUS_MAX_STRLEN) + max_unistr_len = HFSPLUS_MAX_STRLEN; - if (decompose) - dstr = decompose_unichar(c, &dsize, dhangul); - else - dstr = NULL; - if (dstr) { - if (outlen + dsize > max_unistr_len) - break; - do { - ustr->unicode[outlen++] = cpu_to_be16(*dstr++); - } while (--dsize > 0); - } else - ustr->unicode[outlen++] = cpu_to_be16(c); - - astr += size; - len -= size; - } + outlen = hfsplus_decompose_str(sb, astr, len, max_unistr_len, + name_type, buf, &consumed); + for (i = 0; i < outlen; i++) + ustr->unicode[i] = cpu_to_be16(buf[i]); ustr->length = cpu_to_be16(outlen); - if (len > 0) + + if (consumed < len) return -ENAMETOOLONG; return 0; } EXPORT_SYMBOL_IF_KUNIT(hfsplus_asc2uni); +/* + * Maximum length of a single maximal run of nonzero-combining-class code + * units that hfsplus_decompose_iter_next() below will canonically + * reorder. Every code unit with combining class 0 starts a new run, so + * this only bounds how many *consecutive* combining marks between two + * base characters get sorted - real text, and even deliberately + * adversarial "Zalgo" text, essentially never approaches this. Keeping + * it small means hfsplus_hash_dentry() and hfsplus_compare_dentry() only + * ever need a tiny amount of lookahead state, rather than buffering an + * entire (up to 255-unit) name. + */ +#define HFSPLUS_CCC_RUN_MAX 32 + +/* + * Iterator that produces the canonically-ordered, decomposed code units + * of a Linux name one at a time, without materializing the whole name. + * hfsplus_hash_dentry() and hfsplus_compare_dentry() use this so that + * hashing and comparison always agree with what hfsplus_asc2uni() would + * actually store, while still comparing lazily (stopping at the first + * difference) the way this code did before canonical reordering existed. + */ +struct hfsplus_decompose_iter { + struct super_block *sb; + const char *astr; + int len; + int name_type; + int decompose; + + u16 run[HFSPLUS_CCC_RUN_MAX]; + int run_len; + int run_pos; +}; + +static void hfsplus_decompose_iter_init(struct hfsplus_decompose_iter *it, + struct super_block *sb, + const char *astr, int len, + int name_type) +{ + it->sb = sb; + it->astr = astr; + it->len = len; + it->name_type = name_type; + it->decompose = !test_bit(HFSPLUS_SB_NODECOMPOSE, &HFSPLUS_SB(sb)->flags); + it->run_len = 0; + it->run_pos = 0; +} + +/* + * Gather the next maximal run of nonzero-combining-class code units + * (starting with whatever character comes next, base or not) and + * canonically reorder just that run. A character is only included once + * we know the class of the first unit it produces, so decoding it is + * speculative until that's decided. + */ +static bool hfsplus_decompose_iter_refill(struct hfsplus_decompose_iter *it) +{ + it->run_pos = 0; + it->run_len = 0; + + while (it->len > 0) { + u16 *dstr; + u16 dhangul[HFSPLUS_HANGUL_MAX_JAMO]; + int dsize, size; + wchar_t c; + + size = asc2unichar(it->sb, it->astr, it->len, &c, + it->name_type); + + dstr = it->decompose ? + decompose_unichar(c, &dsize, dhangul) : NULL; + if (!dstr) { + dhangul[0] = c; + dstr = dhangul; + dsize = 1; + } + + if (it->run_len > 0 && !hfsplus_combining_class(dstr[0])) + break; + + if (it->run_len + dsize > HFSPLUS_CCC_RUN_MAX) + break; + + it->astr += size; + it->len -= size; + + do { + it->run[it->run_len++] = *dstr++; + } while (--dsize > 0); + } + + hfsplus_canonical_reorder(it->run, it->run_len); + hfsplus_fixup_legacy_sequences(it->run, &it->run_len); + return it->run_len > 0; +} + +/* Returns the next code unit, or a negative value once @it is exhausted. */ +static int hfsplus_decompose_iter_next(struct hfsplus_decompose_iter *it) +{ + if (it->run_pos >= it->run_len && !hfsplus_decompose_iter_refill(it)) + return -1; + return it->run[it->run_pos++]; +} + +/* + * Returns the next code unit that matters for comparison/hashing: folded + * if @casefold, with any character folding to 0 ("ignorable") skipped + * entirely. Returns a negative value once @it is exhausted. + */ +static int hfsplus_decompose_iter_next_folded(struct hfsplus_decompose_iter *it, + int casefold) +{ + int c; + + do { + c = hfsplus_decompose_iter_next(it); + if (c < 0) + return -1; + if (casefold) + c = case_fold(c); + } while (casefold && !c); + + return c; +} + /* * Hash a string to an integer as appropriate for the HFS+ filesystem. * Composed unicode characters are decomposed and case-folding is performed @@ -489,45 +708,17 @@ EXPORT_SYMBOL_IF_KUNIT(hfsplus_asc2uni); int hfsplus_hash_dentry(const struct dentry *dentry, struct qstr *str) { struct super_block *sb = dentry->d_sb; - const char *astr; - const u16 *dstr; - int casefold, decompose, size, len; + int casefold = test_bit(HFSPLUS_SB_CASEFOLD, &HFSPLUS_SB(sb)->flags); + struct hfsplus_decompose_iter it; unsigned long hash; - wchar_t c; - u16 c2; - u16 dhangul[3]; + int c; + + hfsplus_decompose_iter_init(&it, sb, str->name, str->len, + HFS_REGULAR_NAME); - casefold = test_bit(HFSPLUS_SB_CASEFOLD, &HFSPLUS_SB(sb)->flags); - decompose = !test_bit(HFSPLUS_SB_NODECOMPOSE, &HFSPLUS_SB(sb)->flags); hash = init_name_hash(dentry); - astr = str->name; - len = str->len; - while (len > 0) { - int dsize; - size = asc2unichar(sb, astr, len, &c, HFS_REGULAR_NAME); - astr += size; - len -= size; - - if (decompose) - dstr = decompose_unichar(c, &dsize, dhangul); - else - dstr = NULL; - if (dstr) { - do { - c2 = *dstr++; - if (casefold) - c2 = case_fold(c2); - if (!casefold || c2) - hash = partial_name_hash(c2, hash); - } while (--dsize > 0); - } else { - c2 = c; - if (casefold) - c2 = case_fold(c2); - if (!casefold || c2) - hash = partial_name_hash(c2, hash); - } - } + while ((c = hfsplus_decompose_iter_next_folded(&it, casefold)) >= 0) + hash = partial_name_hash(c, hash); str->hash = end_name_hash(hash); return 0; @@ -543,87 +734,21 @@ int hfsplus_compare_dentry(const struct dentry *dentry, unsigned int len, const char *str, const struct qstr *name) { struct super_block *sb = dentry->d_sb; - int casefold, decompose, size; - int dsize1, dsize2, len1, len2; - const u16 *dstr1, *dstr2; - const char *astr1, *astr2; - u16 c1, c2; - wchar_t c; - u16 dhangul_1[3], dhangul_2[3]; + int casefold = test_bit(HFSPLUS_SB_CASEFOLD, &HFSPLUS_SB(sb)->flags); + struct hfsplus_decompose_iter it1, it2; - casefold = test_bit(HFSPLUS_SB_CASEFOLD, &HFSPLUS_SB(sb)->flags); - decompose = !test_bit(HFSPLUS_SB_NODECOMPOSE, &HFSPLUS_SB(sb)->flags); - astr1 = str; - len1 = len; - astr2 = name->name; - len2 = name->len; - dsize1 = dsize2 = 0; - dstr1 = dstr2 = NULL; + hfsplus_decompose_iter_init(&it1, sb, str, len, HFS_REGULAR_NAME); + hfsplus_decompose_iter_init(&it2, sb, name->name, name->len, + HFS_REGULAR_NAME); - while (len1 > 0 && len2 > 0) { - if (!dsize1) { - size = asc2unichar(sb, astr1, len1, &c, - HFS_REGULAR_NAME); - astr1 += size; - len1 -= size; + while (1) { + int c1 = hfsplus_decompose_iter_next_folded(&it1, casefold); + int c2 = hfsplus_decompose_iter_next_folded(&it2, casefold); - if (decompose) - dstr1 = decompose_unichar(c, &dsize1, - dhangul_1); - if (!decompose || !dstr1) { - c1 = c; - dstr1 = &c1; - dsize1 = 1; - } - } - - if (!dsize2) { - size = asc2unichar(sb, astr2, len2, &c, - HFS_REGULAR_NAME); - astr2 += size; - len2 -= size; - - if (decompose) - dstr2 = decompose_unichar(c, &dsize2, - dhangul_2); - if (!decompose || !dstr2) { - c2 = c; - dstr2 = &c2; - dsize2 = 1; - } - } - - c1 = *dstr1; - c2 = *dstr2; - if (casefold) { - c1 = case_fold(c1); - if (!c1) { - dstr1++; - dsize1--; - continue; - } - c2 = case_fold(c2); - if (!c2) { - dstr2++; - dsize2--; - continue; - } - } - if (c1 < c2) - return -1; - else if (c1 > c2) - return 1; - - dstr1++; - dsize1--; - dstr2++; - dsize2--; + if (c1 < 0 || c2 < 0) + return c1 == c2 ? 0 : (c1 < 0 ? -1 : 1); + if (c1 != c2) + return c1 < c2 ? -1 : 1; } - - if (len1 < len2) - return -1; - if (len1 > len2) - return 1; - return 0; } EXPORT_SYMBOL_IF_KUNIT(hfsplus_compare_dentry); diff --git a/fs/hfsplus/unicode.h b/fs/hfsplus/unicode.h index 3957ebccc232..31cc7691c916 100644 --- a/fs/hfsplus/unicode.h +++ b/fs/hfsplus/unicode.h @@ -56,4 +56,8 @@ extern struct hfsplus_ccc_range hfsplus_ccc_table[]; extern struct hfsplus_legacy_decomp hfsplus_legacy_decomp_table[]; extern struct hfsplus_legacy_seq_fixup hfsplus_legacy_seq_fixups[]; +u8 hfsplus_combining_class(u16 c); +const u16 *hfsplus_legacy_decompose(u16 uc, int *size); +void hfsplus_fixup_legacy_sequences(u16 *buf, int *len); + #endif /* _LINUX_HFSPLUS_UNICODE_H */ From 28d4963409507b6b1ebaa9aa1a2ab007bc745958 Mon Sep 17 00:00:00 2001 From: Viacheslav Dubeyko Date: Fri, 18 Sep 2026 17:16:07 -0700 Subject: [PATCH 5/6] hfsplus: add KUnit coverage for canonical reordering and legacy fixups Add regression tests for checking that decomposed catalog names get canonically reordered and legacy-decomposition corrected before storage, matching what macOS's own fsck_hfs requires and previously flagged as "Illegal name" (reproducible via xfstests generic/339). Two small helpers, test_char2uni_utf8()/test_uni2char_utf8(), wrap the real utf8_to_utf32()/utf32_to_utf8() codec: the existing test_char2uni()/test_uni2char() stand-ins only handle one raw byte per character and can't represent the multi-byte combining marks (U+0301, U+0323, ...) these tests need. New cases: - hfsplus_asc2uni_combining_reorder_test: two independently-typed combining marks (U+0301 class 230, U+0323 class 220) end up stored in ascending combining-class order regardless of typed order, are left alone when already canonical, and keep their relative order when their classes are equal (the sort must be stable). - hfsplus_unicode_combining_reorder_roundtrip_test: a name stored with marks reordered still reads back as the same, valid UTF-8. - hfsplus_asc2uni_legacy_decomp_test: U+01F8 decomposes to "N" + U+0300 even though Apple Technote #1150's table has no entry for it. - hfsplus_asc2uni_legacy_seq_fixup_test: a Greek letter + U+030D becomes that letter + U+0301, and Bengali BA + NUKTA collapses to the single letter RA WITH MIDDLE DIAGONAL. - hfsplus_hash_dentry_combining_reorder_test: two names differing only in typed mark order hash identically, since both canonicalize to the same stored form. - hfsplus_compare_dentry_combining_reorder_test and hfsplus_compare_dentry_legacy_decomp_test: likewise, both compare equal - a precomposed U+01F8 and its already-decomposed "N" + U+0300 spelling must be found as the same catalog entry. All 34 cases in the hfsplus_unicode KUnit suite (27 existing + 7 new) pass under tools/testing/kunit/kunit.py. Signed-off-by: Viacheslav Dubeyko Link: https://lore.kernel.org/r/20260919001607.2777138-4-slava@dubeyko.com Signed-off-by: Viacheslav Dubeyko --- fs/hfsplus/unicode_test.c | 308 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) diff --git a/fs/hfsplus/unicode_test.c b/fs/hfsplus/unicode_test.c index 32a64029d77b..a683c6d5fc22 100644 --- a/fs/hfsplus/unicode_test.c +++ b/fs/hfsplus/unicode_test.c @@ -948,6 +948,223 @@ static void hfsplus_asc2uni_decompose_test(struct kunit *test) free_mock_sb(mock_sb); } +/* + * Real UTF-8 <-> wchar_t conversion, mirroring what the "utf8" NLS table + * actually does (see char2uni()/uni2char() in fs/nls/nls_utf8.c). The + * test_char2uni()/test_uni2char() stand-ins above only handle one raw + * byte per character, which cannot represent the combining marks (e.g. + * U+0301, U+0323) the canonical-reordering tests below need. + */ +static int test_char2uni_utf8(const unsigned char *rawstring, int boundlen, + wchar_t *uni) +{ + unicode_t u; + int n = utf8_to_utf32(rawstring, boundlen, &u); + + if (n < 0 || u > MAX_WCHAR_T) { + *uni = 0x3f; /* ? */ + return -EINVAL; + } + *uni = (wchar_t)u; + return n; +} + +static int test_uni2char_utf8(wchar_t uni, unsigned char *out, int boundlen) +{ + int n = utf32_to_utf8(uni, out, boundlen); + + if (n < 0) { + *out = '?'; + return -EINVAL; + } + return n; +} + +/* + * Test that hfsplus_asc2uni() brings combining marks contributed by + * different source characters into Unicode canonical (combining-class) + * order, instead of just storing them in whatever order they were typed. + * + * U+0301 (COMBINING ACUTE ACCENT) has combining class 230; U+0323 + * (COMBINING DOT BELOW) has combining class 220. Since 220 < 230, + * canonical order requires the dot-below before the acute. A name + * created with them typed in the "wrong" order must still end up stored + * in canonical order - this is exactly what macOS's own fsck_hfs + * (FixDecomps() in CatalogCheck.c) requires and flags as "Illegal name" + * when it isn't true. + */ +static void hfsplus_asc2uni_combining_reorder_test(struct kunit *test) +{ + struct test_mock_sb *mock_sb; + struct hfsplus_unistr ustr; + /* "e" + COMBINING ACUTE ACCENT (U+0301) + COMBINING DOT BELOW (U+0323) */ + static const char wrong_order[] = "e\xcc\x81\xcc\xa3"; + /* "e" + COMBINING DOT BELOW (U+0323) + COMBINING ACUTE ACCENT (U+0301) */ + static const char canonical_order[] = "e\xcc\xa3\xcc\x81"; + /* Two class-230 marks: GRAVE (U+0300) then ACUTE (U+0301) */ + static const char same_class[] = "e\xcc\x80\xcc\x81"; + int result; + + mock_sb = setup_mock_sb(); + KUNIT_ASSERT_NOT_NULL(test, mock_sb); + + mock_sb->nls.char2uni = test_char2uni_utf8; + + /* Typed out of canonical order: must be reordered on storage. */ + result = hfsplus_asc2uni(&mock_sb->sb, &ustr, HFSPLUS_MAX_STRLEN, + wrong_order, strlen(wrong_order), + HFS_REGULAR_NAME); + + KUNIT_EXPECT_EQ(test, 0, result); + KUNIT_EXPECT_EQ(test, 3, be16_to_cpu(ustr.length)); + KUNIT_EXPECT_EQ(test, 'e', be16_to_cpu(ustr.unicode[0])); + KUNIT_EXPECT_EQ(test, 0x0323, be16_to_cpu(ustr.unicode[1])); + KUNIT_EXPECT_EQ(test, 0x0301, be16_to_cpu(ustr.unicode[2])); + + /* Already in canonical order: must come out unchanged. */ + result = hfsplus_asc2uni(&mock_sb->sb, &ustr, HFSPLUS_MAX_STRLEN, + canonical_order, strlen(canonical_order), + HFS_REGULAR_NAME); + + KUNIT_EXPECT_EQ(test, 0, result); + KUNIT_EXPECT_EQ(test, 3, be16_to_cpu(ustr.length)); + KUNIT_EXPECT_EQ(test, 'e', be16_to_cpu(ustr.unicode[0])); + KUNIT_EXPECT_EQ(test, 0x0323, be16_to_cpu(ustr.unicode[1])); + KUNIT_EXPECT_EQ(test, 0x0301, be16_to_cpu(ustr.unicode[2])); + + /* Two marks of equal combining class must keep their relative + * (input) order - the sort must be stable, not just "sorted". + */ + result = hfsplus_asc2uni(&mock_sb->sb, &ustr, HFSPLUS_MAX_STRLEN, + same_class, strlen(same_class), + HFS_REGULAR_NAME); + + KUNIT_EXPECT_EQ(test, 0, result); + KUNIT_EXPECT_EQ(test, 3, be16_to_cpu(ustr.length)); + KUNIT_EXPECT_EQ(test, 'e', be16_to_cpu(ustr.unicode[0])); + KUNIT_EXPECT_EQ(test, 0x0300, be16_to_cpu(ustr.unicode[1])); + KUNIT_EXPECT_EQ(test, 0x0301, be16_to_cpu(ustr.unicode[2])); + + free_mock_sb(mock_sb); +} + +/* + * Test that a name written with combining marks in non-canonical order + * still reads back as valid, correct UTF-8 once reordered - i.e. that + * hfsplus_asc2uni() and hfsplus_uni2asc_str() stay consistent with each + * other across the reordering. + * + * NODECOMPOSE is set so hfsplus_uni2asc_str() doesn't also recompose + * "e" + COMBINING DOT BELOW back into the precomposed U+1EB9 ("e"): that + * composition behavior is real (and already covered elsewhere), but it + * would obscure what this test is actually checking. + */ +static void hfsplus_unicode_combining_reorder_roundtrip_test(struct kunit *test) +{ + struct test_mock_sb *mock_sb; + struct hfsplus_unistr ustr; + static const char wrong_order[] = "e\xcc\x81\xcc\xa3"; + static const char expected[] = "e\xcc\xa3\xcc\x81"; /* canonical order */ + char buf[32]; + int len = sizeof(buf); + int result; + + mock_sb = setup_mock_sb(); + KUNIT_ASSERT_NOT_NULL(test, mock_sb); + + set_bit(HFSPLUS_SB_NODECOMPOSE, &mock_sb->sb_info.flags); + mock_sb->nls.char2uni = test_char2uni_utf8; + mock_sb->nls.uni2char = test_uni2char_utf8; + + result = hfsplus_asc2uni(&mock_sb->sb, &ustr, HFSPLUS_MAX_STRLEN, + wrong_order, strlen(wrong_order), + HFS_REGULAR_NAME); + KUNIT_EXPECT_EQ(test, 0, result); + + result = hfsplus_uni2asc_str(&mock_sb->sb, &ustr, buf, &len); + + KUNIT_EXPECT_EQ(test, 0, result); + KUNIT_EXPECT_EQ(test, (int)strlen(expected), len); + KUNIT_EXPECT_MEMEQ(test, expected, buf, len); + + free_mock_sb(mock_sb); +} + +/* + * Test that hfsplus_asc2uni() decomposes a character using the corrected + * (post-2002/"Jaguar") canonical decomposition even when Apple Technote + * #1150's own decomposition table doesn't have an entry for it. + * + * U+01F8 (LATIN CAPITAL LETTER N WITH GRAVE) is exactly one of the + * characters macOS's own fsck_hfs (FixDecomps() in CatalogCheck.c) has + * flagged "Illegal name" for since 2002 when found stored undecomposed. + */ +static void hfsplus_asc2uni_legacy_decomp_test(struct kunit *test) +{ + struct test_mock_sb *mock_sb; + struct hfsplus_unistr ustr; + static const char input[] = "\xc7\xb8"; /* U+01F8 */ + int result; + + mock_sb = setup_mock_sb(); + KUNIT_ASSERT_NOT_NULL(test, mock_sb); + + mock_sb->nls.char2uni = test_char2uni_utf8; + + result = hfsplus_asc2uni(&mock_sb->sb, &ustr, HFSPLUS_MAX_STRLEN, + input, strlen(input), HFS_REGULAR_NAME); + + KUNIT_EXPECT_EQ(test, 0, result); + KUNIT_EXPECT_EQ(test, 2, be16_to_cpu(ustr.length)); + KUNIT_EXPECT_EQ(test, 'N', be16_to_cpu(ustr.unicode[0])); + KUNIT_EXPECT_EQ(test, 0x0300, be16_to_cpu(ustr.unicode[1])); + + free_mock_sb(mock_sb); +} + +/* + * Test that hfsplus_asc2uni() corrects two more of fsck_hfs's known + * legacy decomposition sequences once combining marks from independently + * typed characters end up adjacent: + * + * - GREEK SMALL LETTER ALPHA (U+03B1) + COMBINING VERTICAL LINE ABOVE + * (U+030D) must become U+03B1 + COMBINING ACUTE ACCENT (U+0301). + * - BENGALI LETTER BA (U+09AC) + BENGALI SIGN NUKTA (U+09BC) must become + * the single character BENGALI LETTER RA WITH MIDDLE DIAGONAL (U+09B0). + */ +static void hfsplus_asc2uni_legacy_seq_fixup_test(struct kunit *test) +{ + struct test_mock_sb *mock_sb; + struct hfsplus_unistr ustr; + static const char greek_input[] = "\xce\xb1\xcc\x8d"; /* U+03B1 U+030D */ + static const char bengali_input[] = "\xe0\xa6\xac\xe0\xa6\xbc"; /* U+09AC U+09BC */ + int result; + + mock_sb = setup_mock_sb(); + KUNIT_ASSERT_NOT_NULL(test, mock_sb); + + mock_sb->nls.char2uni = test_char2uni_utf8; + + result = hfsplus_asc2uni(&mock_sb->sb, &ustr, HFSPLUS_MAX_STRLEN, + greek_input, strlen(greek_input), + HFS_REGULAR_NAME); + + KUNIT_EXPECT_EQ(test, 0, result); + KUNIT_EXPECT_EQ(test, 2, be16_to_cpu(ustr.length)); + KUNIT_EXPECT_EQ(test, 0x03b1, be16_to_cpu(ustr.unicode[0])); + KUNIT_EXPECT_EQ(test, 0x0301, be16_to_cpu(ustr.unicode[1])); + + result = hfsplus_asc2uni(&mock_sb->sb, &ustr, HFSPLUS_MAX_STRLEN, + bengali_input, strlen(bengali_input), + HFS_REGULAR_NAME); + + KUNIT_EXPECT_EQ(test, 0, result); + KUNIT_EXPECT_EQ(test, 1, be16_to_cpu(ustr.length)); + KUNIT_EXPECT_EQ(test, 0x09b0, be16_to_cpu(ustr.unicode[0])); + + free_mock_sb(mock_sb); +} + /* Mock dentry for testing hfsplus_hash_dentry */ static struct dentry test_dentry; @@ -1231,6 +1448,37 @@ static void hfsplus_hash_dentry_edge_cases_test(struct kunit *test) free_mock_sb(mock_sb); } +/* + * Test that hfsplus_hash_dentry() hashes two names identically when they + * differ only in the (non-canonical) typed order of the same combining + * marks - both must canonicalize to the same stored form, so they must + * hash the same or dcache lookups would spuriously miss. + */ +static void hfsplus_hash_dentry_combining_reorder_test(struct kunit *test) +{ + struct test_mock_sb *mock_sb; + struct qstr str1, str2; + int result; + + mock_sb = setup_mock_sb(); + KUNIT_ASSERT_NOT_NULL(test, mock_sb); + + setup_mock_dentry(&mock_sb->sb); + mock_sb->nls.char2uni = test_char2uni_utf8; + + create_qstr(&str1, "e\xcc\x81\xcc\xa3"); /* acute, then dot-below */ + result = hfsplus_hash_dentry(&test_dentry, &str1); + KUNIT_EXPECT_EQ(test, 0, result); + + create_qstr(&str2, "e\xcc\xa3\xcc\x81"); /* dot-below, then acute */ + result = hfsplus_hash_dentry(&test_dentry, &str2); + KUNIT_EXPECT_EQ(test, 0, result); + + KUNIT_EXPECT_EQ(test, str1.hash, str2.hash); + + free_mock_sb(mock_sb); +} + /* Test hfsplus_compare_dentry basic functionality */ static void hfsplus_compare_dentry_basic_test(struct kunit *test) { @@ -1553,6 +1801,59 @@ static void hfsplus_compare_dentry_combined_flags_test(struct kunit *test) free_mock_sb(mock_sb); } +/* + * Test that hfsplus_compare_dentry() treats two names as equal when they + * differ only in the (non-canonical) typed order of the same combining + * marks, since both refer to the same canonically-ordered catalog entry. + */ +static void hfsplus_compare_dentry_combining_reorder_test(struct kunit *test) +{ + struct test_mock_sb *mock_sb; + struct qstr name; + int result; + + mock_sb = setup_mock_sb(); + KUNIT_ASSERT_NOT_NULL(test, mock_sb); + + setup_mock_dentry(&mock_sb->sb); + mock_sb->nls.char2uni = test_char2uni_utf8; + + create_qstr(&name, "e\xcc\xa3\xcc\x81"); /* dot-below, then acute */ + result = hfsplus_compare_dentry(&test_dentry, 5, "e\xcc\x81\xcc\xa3", + &name); /* acute, then dot-below */ + KUNIT_EXPECT_EQ(test, 0, result); + + free_mock_sb(mock_sb); +} + +/* + * Test that a precomposed character using one of fsck_hfs's known legacy + * decompositions compares equal to the already-decomposed form of the + * same character - i.e. that hfsplus_compare_dentry() applies the same + * legacy-decomposition correction as hfsplus_asc2uni() does on storage, + * so a lookup finds the entry regardless of which form was typed. + */ +static void hfsplus_compare_dentry_legacy_decomp_test(struct kunit *test) +{ + struct test_mock_sb *mock_sb; + struct qstr name; + int result; + + mock_sb = setup_mock_sb(); + KUNIT_ASSERT_NOT_NULL(test, mock_sb); + + setup_mock_dentry(&mock_sb->sb); + mock_sb->nls.char2uni = test_char2uni_utf8; + + /* "N" + COMBINING GRAVE ACCENT (U+0300), already decomposed */ + create_qstr(&name, "N\xcc\x80"); + /* U+01F8, precomposed */ + result = hfsplus_compare_dentry(&test_dentry, 2, "\xc7\xb8", &name); + KUNIT_EXPECT_EQ(test, 0, result); + + free_mock_sb(mock_sb); +} + static struct kunit_case hfsplus_unicode_test_cases[] = { KUNIT_CASE(hfsplus_strcasecmp_test), KUNIT_CASE(hfsplus_strcmp_test), @@ -1568,12 +1869,17 @@ static struct kunit_case hfsplus_unicode_test_cases[] = { KUNIT_CASE(hfsplus_asc2uni_buffer_limits_test), KUNIT_CASE(hfsplus_asc2uni_edge_cases_test), KUNIT_CASE(hfsplus_asc2uni_decompose_test), + KUNIT_CASE(hfsplus_asc2uni_combining_reorder_test), + KUNIT_CASE(hfsplus_unicode_combining_reorder_roundtrip_test), + KUNIT_CASE(hfsplus_asc2uni_legacy_decomp_test), + KUNIT_CASE(hfsplus_asc2uni_legacy_seq_fixup_test), KUNIT_CASE(hfsplus_hash_dentry_basic_test), KUNIT_CASE(hfsplus_hash_dentry_casefold_test), KUNIT_CASE(hfsplus_hash_dentry_special_chars_test), KUNIT_CASE(hfsplus_hash_dentry_decompose_test), KUNIT_CASE(hfsplus_hash_dentry_consistency_test), KUNIT_CASE(hfsplus_hash_dentry_edge_cases_test), + KUNIT_CASE(hfsplus_hash_dentry_combining_reorder_test), KUNIT_CASE(hfsplus_compare_dentry_basic_test), KUNIT_CASE(hfsplus_compare_dentry_casefold_test), KUNIT_CASE(hfsplus_compare_dentry_special_chars_test), @@ -1581,6 +1887,8 @@ static struct kunit_case hfsplus_unicode_test_cases[] = { KUNIT_CASE(hfsplus_compare_dentry_decompose_test), KUNIT_CASE(hfsplus_compare_dentry_edge_cases_test), KUNIT_CASE(hfsplus_compare_dentry_combined_flags_test), + KUNIT_CASE(hfsplus_compare_dentry_combining_reorder_test), + KUNIT_CASE(hfsplus_compare_dentry_legacy_decomp_test), {} }; From f278d04be3acfd3a9eccf045b9eb180007cceccc Mon Sep 17 00:00:00 2001 From: Davy Felipe Date: Mon, 28 Sep 2026 23:03:29 -0300 Subject: [PATCH 6/6] hfs: handle extent B-tree write errors hfs_brec_insert() may fail while inserting a new extent record, but __hfs_ext_write_extent() currently ignores its return value and clears HFS_FLG_EXT_DIRTY and HFS_FLG_EXT_NEW as if the insertion had succeeded. Propagate errors returned by hfs_brec_insert() and only clear the extent flags after a successful insertion. When updating an existing extent record, hfs_bnode_write() returns void. Validate the extent write parameters before calling it so an invalid update is reported as -ERANGE instead of being treated as successful. Use a reusable B-tree node range helper that takes the find data and the expected record size. The helper validates the bnode and tree pointers, entry offset and entry length, and ensures that the write range fits within the node. Negative-path testing in QEMU confirmed that an insertion error is propagated to the caller. Testing the existing-record path also confirmed that invalid write parameters are rejected before HFS_FLG_EXT_DIRTY is cleared. Changes in v6: - Rename hfs_bnode_is_valid_range() to is_hfs_bnode_range_valid(). - Use size_t for the expected record size. - Drop the redundant expected_len validity check. - Return -ERANGE for invalid find-data/range parameters. - Incorporate the final review feedback from Viacheslav Dubeyko. Signed-off-by: Davy Felipe Reviewed-by: Viacheslav Dubeyko Link: https://lore.kernel.org/r/20260929020329.32911-1-davyfelipe34@gmail.com Signed-off-by: Viacheslav Dubeyko --- fs/hfs/btree.h | 19 +++++++++++++++++++ fs/hfs/extent.c | 10 ++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/fs/hfs/btree.h b/fs/hfs/btree.h index b4c3f2a31471..1f703f2c53d8 100644 --- a/fs/hfs/btree.h +++ b/fs/hfs/btree.h @@ -84,6 +84,25 @@ struct hfs_find_data { int entryoffset, entrylength; }; +static inline bool is_hfs_bnode_range_valid(struct hfs_find_data *fd, + size_t expected_len) +{ + struct hfs_bnode *node; + + if (!fd) + return false; + + node = fd->bnode; + if (!node || !node->tree) + return false; + + if (fd->entryoffset < 0 || fd->entrylength != expected_len) + return false; + + return (u64)fd->entryoffset + fd->entrylength <= + node->tree->node_size; +} + /* btree.c */ extern struct hfs_btree *hfs_btree_open(struct super_block *sb, u32 id, diff --git a/fs/hfs/extent.c b/fs/hfs/extent.c index f066a99a863b..26357086517f 100644 --- a/fs/hfs/extent.c +++ b/fs/hfs/extent.c @@ -121,12 +121,18 @@ static int __hfs_ext_write_extent(struct inode *inode, struct hfs_find_data *fd) res = hfs_bmap_reserve(fd->tree, fd->tree->depth + 1); if (res) return res; - hfs_brec_insert(fd, HFS_I(inode)->cached_extents, sizeof(hfs_extent_rec)); + res = hfs_brec_insert(fd, HFS_I(inode)->cached_extents, + sizeof(hfs_extent_rec)); + if (res) + return res; HFS_I(inode)->flags &= ~(HFS_FLG_EXT_DIRTY|HFS_FLG_EXT_NEW); } else { if (res) return res; - hfs_bnode_write(fd->bnode, HFS_I(inode)->cached_extents, fd->entryoffset, fd->entrylength); + if (!is_hfs_bnode_range_valid(fd, sizeof(hfs_extent_rec))) + return -ERANGE; + hfs_bnode_write(fd->bnode, HFS_I(inode)->cached_extents, + fd->entryoffset, fd->entrylength); HFS_I(inode)->flags &= ~HFS_FLG_EXT_DIRTY; } return 0;