* [PATCH 1/1] hfs: don't let internal B-tree inodes leak into the VFS open path [not found] <20260916080000.0-hfs-fix@localhost> @ 2026-09-16 9:09 ` 1sh1ro 2026-09-16 18:33 ` Viacheslav Dubeyko 0 siblings, 1 reply; 9+ messages in thread From: 1sh1ro @ 2026-09-16 9:09 UTC (permalink / raw) To: Viacheslav Dubeyko, John Paul Adrian Glaubitz, Yangtao Li Cc: 1sh1ro, linux-fsdevel, linux-kernel, Mateusz Guzik, Christian Brauner, stable hfs_btree_open() creates the extents and catalog B-tree inodes with iget_locked() and never assigns them a file type, so both stay cached with i_mode == 0 after mount. A crafted HFS image whose catalog contains a record whose DirID/FlNum aliases HFS_EXT_CNID (3) or HFS_CAT_CNID (4) makes hfs_lookup() -> hfs_iget() -> iget5_locked() return that already-cached internal inode (hfs_test_inode() only compares i_ino), without running hfs_read_inode(). The typeless inode then reaches the generic open path and, with CONFIG_DEBUG_VFS=y, deterministically panics: VFS_BUG_ON_INODE(!IS_ANON_FILE(inode)): inode:... fs:hfs mode:0 opflags:0xc flags:0x0 state:0x0 count:2 ------------[ cut here ]------------ kernel BUG at fs/namei.c:4271! RIP: 0010:may_open+0x2ba/0x480 fs/namei.c:4271 Call Trace: do_open fs/namei.c:4698 [inline] path_openat+0x1600/0x3de0 fs/namei.c:4863 __x64_sys_openat+0x13f/0x1f0 fs/open.c:1385 Fix it in two steps: - give the B-tree inodes an S_IFREG type in hfs_btree_open(), so the "every cached inode has a valid S_IFMT type" VFS invariant holds regardless of what is on disk; - refuse, in hfs_iget(), catalog records that address the reserved B-tree CNIDs so a malformed image cannot alias internal objects into dentries at all. Returning NULL follows the existing convention for unaddressable records: hfs_fill_super() turns it into a mount failure and hfs_lookup() into -EACCES. Without CONFIG_DEBUG_VFS the assertion compiles away and the same crafted image silently makes a typeless inode openable, so this is not merely a debug-config nuisance. Found by syzkaller-style fuzzing of v7.2-rc7; still reproducible on current mainline. Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Signed-off-by: 1sh1ro <yangguikun19@qq.com> --- fs/hfs/btree.c | 8 ++++++++ fs/hfs/inode.c | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/fs/hfs/btree.c b/fs/hfs/btree.c index 2eb37a2f64e8..bd69ace8f309 100644 --- a/fs/hfs/btree.c +++ b/fs/hfs/btree.c @@ -43,6 +43,14 @@ struct hfs_btree *hfs_btree_open(struct super_block *sb, u32 id, btree_keycmp ke if (!tree->inode) goto free_tree; BUG_ON(!(inode_state_read_once(tree->inode) & I_NEW)); + + /* B-tree inodes are internal objects that must never be exposed + * through directory entries. Give them a REG type so the VFS + * invariant "every cached inode has a valid S_IFMT type" holds even + * when a corrupted image contains catalog records aliasing these + * reserved CNIDs. + */ + tree->inode->i_mode = S_IFREG; { struct hfs_mdb *mdb = HFS_SB(sb)->mdb; HFS_I(tree->inode)->flags = 0; diff --git a/fs/hfs/inode.c b/fs/hfs/inode.c index ac4a9055c5c0..bb759345f1fc 100644 --- a/fs/hfs/inode.c +++ b/fs/hfs/inode.c @@ -432,6 +432,17 @@ struct inode *hfs_iget(struct super_block *sb, struct hfs_cat_key *key, hfs_cat_ default: return NULL; } + + /* Refuse catalog records that address the internal B-tree objects: + * a crafted image can otherwise make hfs_lookup() return the cached + * typeless B-tree inode, which then reaches may_open() with + * i_mode == 0 and trips VFS_BUG_ON_INODE() there. NULL follows the + * existing convention for unaddressable records (mount failure / + * -EACCES). + */ + if (cnid == HFS_EXT_CNID || cnid == HFS_CAT_CNID) + return NULL; + inode = iget5_locked(sb, cnid, hfs_test_inode, hfs_read_inode, &data); if (inode && (inode_state_read_once(inode) & I_NEW)) unlock_new_inode(inode); -- 2.39.5 ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH 1/1] hfs: don't let internal B-tree inodes leak into the VFS open path 2026-09-16 9:09 ` [PATCH 1/1] hfs: don't let internal B-tree inodes leak into the VFS open path 1sh1ro @ 2026-09-16 18:33 ` Viacheslav Dubeyko 2026-10-12 7:44 ` [PATCH v2 0/2] hfs: keep the internal B-tree inodes out of " 1sh1ro [not found] ` <20261012154438.6396-1-yangguikun19@qq.com> 0 siblings, 2 replies; 9+ messages in thread From: Viacheslav Dubeyko @ 2026-09-16 18:33 UTC (permalink / raw) To: 1sh1ro, John Paul Adrian Glaubitz, Yangtao Li Cc: linux-fsdevel, linux-kernel, Mateusz Guzik, Christian Brauner, stable On Wed, 2026-09-16 at 17:09 +0800, 1sh1ro wrote: > hfs_btree_open() creates the extents and catalog B-tree inodes with > iget_locked() and never assigns them a file type, so both stay cached > with i_mode == 0 after mount. > > A crafted HFS image whose catalog contains a record whose DirID/FlNum > aliases HFS_EXT_CNID (3) or HFS_CAT_CNID (4) makes hfs_lookup() -> > hfs_iget() -> iget5_locked() return that already-cached internal > inode > (hfs_test_inode() only compares i_ino), without running > hfs_read_inode(). The typeless inode then reaches the generic open > path and, with CONFIG_DEBUG_VFS=y, deterministically panics: > > VFS_BUG_ON_INODE(!IS_ANON_FILE(inode)): inode:... fs:hfs mode:0 > opflags:0xc flags:0x0 state:0x0 count:2 > ------------[ cut here ]------------ > kernel BUG at fs/namei.c:4271! > RIP: 0010:may_open+0x2ba/0x480 fs/namei.c:4271 > Call Trace: > do_open fs/namei.c:4698 [inline] > path_openat+0x1600/0x3de0 fs/namei.c:4863 > __x64_sys_openat+0x13f/0x1f0 fs/open.c:1385 > > Fix it in two steps: > > - give the B-tree inodes an S_IFREG type in hfs_btree_open(), so the > "every cached inode has a valid S_IFMT type" VFS invariant holds > regardless of what is on disk; > > - refuse, in hfs_iget(), catalog records that address the reserved > B-tree CNIDs so a malformed image cannot alias internal objects > into dentries at all. Returning NULL follows the existing > convention for unaddressable records: hfs_fill_super() turns it > into a mount failure and hfs_lookup() into -EACCES. > > Without CONFIG_DEBUG_VFS the assertion compiles away and the same > crafted image silently makes a typeless inode openable, so this is > not > merely a debug-config nuisance. > > Found by syzkaller-style fuzzing of v7.2-rc7; still reproducible on > current mainline. > > Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") > Signed-off-by: 1sh1ro <yangguikun19@qq.com> > --- > fs/hfs/btree.c | 8 ++++++++ > fs/hfs/inode.c | 11 +++++++++++ > 2 files changed, 19 insertions(+) > > diff --git a/fs/hfs/btree.c b/fs/hfs/btree.c > index 2eb37a2f64e8..bd69ace8f309 100644 > --- a/fs/hfs/btree.c > +++ b/fs/hfs/btree.c > @@ -43,6 +43,14 @@ struct hfs_btree *hfs_btree_open(struct > super_block *sb, u32 id, btree_keycmp ke > if (!tree->inode) > goto free_tree; > BUG_ON(!(inode_state_read_once(tree->inode) & I_NEW)); > + > + /* B-tree inodes are internal objects that must never be exposed > + * through directory entries. Give them a REG type so the VFS > + * invariant "every cached inode has a valid S_IFMT type" holds > even > + * when a corrupted image contains catalog records aliasing these > + * reserved CNIDs. > + */ > + tree->inode->i_mode = S_IFREG; In HFS+ we call hfsplus_iget() in hfs_btree_open() [1] and it assign the S_IFREG in hfsplus_system_read_inode() [2]. But in HFS we call iget_locked() instead. I think we need to call hfs_iget() there and to have something like hfs_system_read_inode() that will assign S_IFREG to the i_mode. But current fix doesn't look like right solution now. Could you please explore this direction for the fix? Thanks, Slava. > { > struct hfs_mdb *mdb = HFS_SB(sb)->mdb; > HFS_I(tree->inode)->flags = 0; > diff --git a/fs/hfs/inode.c b/fs/hfs/inode.c > index ac4a9055c5c0..bb759345f1fc 100644 > --- a/fs/hfs/inode.c > +++ b/fs/hfs/inode.c > @@ -432,6 +432,17 @@ struct inode *hfs_iget(struct super_block *sb, > struct hfs_cat_key *key, hfs_cat_ > default: > return NULL; > } > + > + /* Refuse catalog records that address the internal B-tree > objects: > + * a crafted image can otherwise make hfs_lookup() return the > cached > + * typeless B-tree inode, which then reaches may_open() with > + * i_mode == 0 and trips VFS_BUG_ON_INODE() there. NULL follows > the > + * existing convention for unaddressable records (mount failure / > + * -EACCES). > + */ > + if (cnid == HFS_EXT_CNID || cnid == HFS_CAT_CNID) > + return NULL; > + > inode = iget5_locked(sb, cnid, hfs_test_inode, hfs_read_inode, > &data); > if (inode && (inode_state_read_once(inode) & I_NEW)) > unlock_new_inode(inode); > -- > 2.39.5 [1] https://elixir.bootlin.com/linux/v7.3-rc3/source/fs/hfsplus/btree.c#L286 [2] https://elixir.bootlin.com/linux/v7.3-rc3/source/fs/hfsplus/super.c#L60 ^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v2 0/2] hfs: keep the internal B-tree inodes out of the VFS open path 2026-09-16 18:33 ` Viacheslav Dubeyko @ 2026-10-12 7:44 ` 1sh1ro [not found] ` <20261012154438.6396-1-yangguikun19@qq.com> 1 sibling, 0 replies; 9+ messages in thread From: 1sh1ro @ 2026-10-12 7:44 UTC (permalink / raw) To: Viacheslav Dubeyko, John Paul Adrian Glaubitz, Yangtao Li Cc: linux-fsdevel, linux-kernel Hi Viacheslav, v2, following your suggestion. hfs_btree_open() no longer patches i_mode up by hand: there is now an hfs_system_iget()/hfs_system_read_inode() pair, the HFS counterpart of hfsplus_iget()/hfsplus_system_read_inode(). The B-tree inodes are created through the generic inode-cache path and get their S_IFREG there, and the extent-record validation still runs before hfs_btree_open() succeeds. hfs_iget() itself is keyed on a catalog record (key + rec) and the B-tree inodes have no catalog record, so hfs_system_iget() is a sibling entry point sharing the same inode-cache path. Folding it into hfs_iget() is a small change if you prefer that shape. Patch 2 keeps the alias guard from v1 - a catalog record must not address CNID 3/4 - as hardening on top of patch 1. Drop it if you consider hfs_is_valid_cnid()'s acceptance of HFS_CDR_FIL records for those CNIDs intentional. The series applies to v7.3-rc5 and fs/hfs builds clean with clang. Thanks, 1sh1ro 1sh1ro (2): hfs: create the internal B-tree inodes through the generic iget path hfs: refuse catalog records that address the reserved B-tree CNIDs fs/hfs/btree.c | 25 +++++-------------------- fs/hfs/hfs_fs.h | 1 + fs/hfs/inode.c | 85 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 20 deletions(-) ^ permalink raw reply [flat|nested] 9+ messages in thread
[parent not found: <20261012154438.6396-1-yangguikun19@qq.com>]
* [PATCH v2 2/2] hfs: refuse catalog records that address the reserved B-tree CNIDs [not found] ` <20261012154438.6396-1-yangguikun19@qq.com> @ 2026-10-12 7:44 ` 1sh1ro 2026-10-12 21:30 ` Viacheslav Dubeyko 2026-10-12 7:44 ` [PATCH v2 1/2] hfs: create the internal B-tree inodes through the generic iget path 1sh1ro 1 sibling, 1 reply; 9+ messages in thread From: 1sh1ro @ 2026-10-12 7:44 UTC (permalink / raw) To: Viacheslav Dubeyko, John Paul Adrian Glaubitz, Yangtao Li Cc: linux-fsdevel, linux-kernel The extent and catalog B-tree inodes are described by the MDB, not by catalog records, but they are hashed by CNID like any other inode and hfs_test_inode() compares i_ino only. A crafted image with a catalog record whose FlNum (or DirID) is HFS_EXT_CNID or HFS_CAT_CNID therefore makes hfs_lookup() hand out the internal B-tree inode as a regular dentry, bypassing the CNID validation that hfs_read_inode() performs: a read exposes the B-tree file's contents, and a write would go through hfs_write_inode(), which tries to update a catalog record that does not exist. Refuse such records in hfs_iget() by returning NULL for the reserved B-tree CNIDs. hfs_lookup() reports that as -EACCES and hfs_fill_super() as a mount failure, the same as for any other unaddressable record. This is hardening on top of the file-type fix, not required for the panic; it can be dropped if hfs_is_valid_cnid()'s acceptance of HFS_CDR_FIL records for CNID 3/4 is intentional. Signed-off-by: 1sh1ro <yangguikun19@qq.com> --- fs/hfs/inode.c | 11 +++++++++++ 1 file changed, 11 insertions(+), 0 deletions(-) diff --git a/fs/hfs/inode.c b/fs/hfs/inode.c index 4316d75..5190856 100644 --- a/fs/hfs/inode.c +++ b/fs/hfs/inode.c @@ -520,6 +520,17 @@ struct inode *hfs_iget(struct super_block *sb, struct hfs_cat_key *key, hfs_cat_ default: return NULL; } + + /* + * Never let a catalog record address one of the internal B-tree + * objects. hfs_test_inode() matches on i_ino alone, so such a + * record would make hfs_lookup() hand out the cached B-tree inode as + * a regular dentry, bypassing the CNID validation done by + * hfs_read_inode(). + */ + if (cnid == HFS_EXT_CNID || cnid == HFS_CAT_CNID) + return NULL; + inode = iget5_locked(sb, cnid, hfs_test_inode, hfs_read_inode, &data); if (inode && (inode_state_read_once(inode) & I_NEW)) unlock_new_inode(inode); -- 2.39.5 ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 2/2] hfs: refuse catalog records that address the reserved B-tree CNIDs 2026-10-12 7:44 ` [PATCH v2 2/2] hfs: refuse catalog records that address the reserved B-tree CNIDs 1sh1ro @ 2026-10-12 21:30 ` Viacheslav Dubeyko 2026-10-13 6:44 ` [PATCH v3] hfs: give the internal B-tree inodes a type and keep catalog records off them yangguikun 0 siblings, 1 reply; 9+ messages in thread From: Viacheslav Dubeyko @ 2026-10-12 21:30 UTC (permalink / raw) To: 1sh1ro, John Paul Adrian Glaubitz, Yangtao Li; +Cc: linux-fsdevel, linux-kernel On Mon, 2026-10-12 at 15:44 +0800, 1sh1ro wrote: > The extent and catalog B-tree inodes are described by the MDB, not by > catalog records, but they are hashed by CNID like any other inode and > hfs_test_inode() compares i_ino only. A crafted image with a catalog > record whose FlNum (or DirID) is HFS_EXT_CNID or HFS_CAT_CNID > therefore > makes hfs_lookup() hand out the internal B-tree inode as a regular > dentry, > bypassing the CNID validation that hfs_read_inode() performs: a read > exposes the B-tree file's contents, and a write would go through > hfs_write_inode(), which tries to update a catalog record that does > not > exist. > > Refuse such records in hfs_iget() by returning NULL for the reserved > B-tree CNIDs. hfs_lookup() reports that as -EACCES and > hfs_fill_super() as a mount failure, the same as for any other > unaddressable record. > > This is hardening on top of the file-type fix, not required for the > panic; it can be dropped if hfs_is_valid_cnid()'s acceptance of > HFS_CDR_FIL records for CNID 3/4 is intentional. Please, combine both patches into one. First patch doesn't fix the issue alone. > > Signed-off-by: 1sh1ro <yangguikun19@qq.com> Please, provide the real name. > --- > fs/hfs/inode.c | 11 +++++++++++ > 1 file changed, 11 insertions(+), 0 deletions(-) > > diff --git a/fs/hfs/inode.c b/fs/hfs/inode.c > index 4316d75..5190856 100644 > --- a/fs/hfs/inode.c > +++ b/fs/hfs/inode.c > @@ -520,6 +520,17 @@ struct inode *hfs_iget(struct super_block *sb, > struct hfs_cat_key *key, hfs_cat_ > default: > return NULL; > } > + > + /* > + * Never let a catalog record address one of the internal B- > tree > + * objects. hfs_test_inode() matches on i_ino alone, so > such a > + * record would make hfs_lookup() hand out the cached B-tree > inode as > + * a regular dentry, bypassing the CNID validation done by > + * hfs_read_inode(). > + */ Please, rework or remove the comment. Currently, it's long and not informative. > + if (cnid == HFS_EXT_CNID || cnid == HFS_CAT_CNID) I think we can reject all cnid that lower than HFS_FIRSTUSER_CNID. Thanks, Slava. > + return NULL; > + > inode = iget5_locked(sb, cnid, hfs_test_inode, > hfs_read_inode, &data); > if (inode && (inode_state_read_once(inode) & I_NEW)) > unlock_new_inode(inode); ^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v3] hfs: give the internal B-tree inodes a type and keep catalog records off them 2026-10-12 21:30 ` Viacheslav Dubeyko @ 2026-10-13 6:44 ` yangguikun 2026-10-13 21:38 ` Viacheslav Dubeyko 0 siblings, 1 reply; 9+ messages in thread From: yangguikun @ 2026-10-13 6:44 UTC (permalink / raw) To: Viacheslav Dubeyko, John Paul Adrian Glaubitz, Yangtao Li Cc: linux-fsdevel, linux-kernel hfs_btree_open() builds the extents and catalog B-tree inodes with a raw iget_locked() and never assigns them a file type, so both stay cached with i_mode == 0 after every mount. Two problems follow from that, and both are reachable from a crafted image. First, a catalog record that names CNID 3 or 4 makes hfs_lookup() -> hfs_iget() return the cached B-tree inode - hfs_test_inode() matches on i_ino alone - and the typeless inode then reaches the generic open path, where it trips VFS_BUG_ON_INODE() in may_open() with CONFIG_DEBUG_VFS=y: VFS_BUG_ON_INODE(!IS_ANON_FILE(inode)): inode:... fs:hfs mode:0 opflags:0xc ------------[ cut here ]------------ kernel BUG at fs/namei.c:4271! RIP: 0010:may_open+0x2ba/0x480 fs/namei.c:4271 Call Trace: do_open fs/namei.c:4698 [inline] path_openat+0x1600/0x3de0 fs/namei.c:4863 __x64_sys_openat+0x13f/0x1f0 fs/open.c:1385 Without CONFIG_DEBUG_VFS the assertion compiles away and the same image silently yields an openable typeless inode, so this is not merely a debug-config nuisance. Second, once the inode has a type, such a record still hands the internal B-tree file out as a regular dentry: it can be opened and read, which exposes the B-tree, and from then on the VFS treats an inode that has no catalog record behind it as an ordinary file. hfs_is_valid_cnid() does not close this path: it runs from hfs_read_inode(), and a lookup that hits the already cached B-tree inode never reaches that function. Fix both. Add hfs_system_read_inode(), which reads the fork from the MDB and assigns S_IFREG - the counterpart of hfsplus_system_read_inode() - and hfs_system_iget(), which looks the inode up through the normal inode-cache path and can fail before the inode is published. hfs_btree_open() now uses hfs_system_iget(), and the extent-record validation moves into the new read helper. hfs_iget() refuses catalog records that name a reserved CNID below HFS_FIRSTUSER_CNID; the root directory, the one reserved CNID that legitimately comes through there, is exempt. Found by syzkaller-style fuzzing of v7.2-rc7; still reproducible on current mainline. Suggested-by: Viacheslav Dubeyko <slava@dubeyko.com> Link: https://lore.kernel.org/linux-fsdevel/tencent_24D928451FEFA8A35482EFFA9132A7305307@qq.com/ Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Cc: stable@vger.kernel.org Signed-off-by: yangguikun <yangguikun19@qq.com> --- fs/hfs/btree.c | 39 +++------------------------ fs/hfs/hfs_fs.h | 1 + fs/hfs/inode.c | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 36 deletions(-) diff --git a/fs/hfs/btree.c b/fs/hfs/btree.c index 41b4e8f..b5892de 100644 --- a/fs/hfs/btree.c +++ b/fs/hfs/btree.c @@ -155,44 +155,11 @@ struct hfs_btree *hfs_btree_open(struct super_block *sb, u32 id, btree_keycmp ke tree->cnid = id; tree->keycmp = keycmp; - tree->inode = iget_locked(sb, id); - if (!tree->inode) + tree->inode = hfs_system_iget(sb, id); + if (IS_ERR(tree->inode)) goto free_tree; - BUG_ON(!(inode_state_read_once(tree->inode) & I_NEW)); - { - struct hfs_mdb *mdb = HFS_SB(sb)->mdb; - HFS_I(tree->inode)->flags = 0; - mutex_init(&HFS_I(tree->inode)->extents_lock); - switch (id) { - case HFS_EXT_CNID: - hfs_inode_read_fork(tree->inode, mdb->drXTExtRec, mdb->drXTFlSize, - mdb->drXTFlSize, be32_to_cpu(mdb->drXTClpSiz)); - if (HFS_I(tree->inode)->alloc_blocks > - HFS_I(tree->inode)->first_blocks) { - pr_err("invalid btree extent records\n"); - unlock_new_inode(tree->inode); - goto free_inode; - } - tree->inode->i_mapping->a_ops = &hfs_btree_aops; - break; - case HFS_CAT_CNID: - hfs_inode_read_fork(tree->inode, mdb->drCTExtRec, mdb->drCTFlSize, - mdb->drCTFlSize, be32_to_cpu(mdb->drCTClpSiz)); - - if (!HFS_I(tree->inode)->first_blocks) { - pr_err("invalid btree extent records (0 size)\n"); - unlock_new_inode(tree->inode); - goto free_inode; - } - - tree->inode->i_mapping->a_ops = &hfs_btree_aops; - break; - default: - BUG(); - } - } - unlock_new_inode(tree->inode); + tree->inode->i_mapping->a_ops = &hfs_btree_aops; mapping = tree->inode->i_mapping; folio = filemap_grab_folio(mapping, 0); diff --git a/fs/hfs/hfs_fs.h b/fs/hfs/hfs_fs.h index e250f87..26505f7 100644 --- a/fs/hfs/hfs_fs.h +++ b/fs/hfs/hfs_fs.h @@ -219,6 +219,7 @@ extern void hfs_inode_read_fork(struct inode *inode, struct hfs_extent *ext, u32 clump_size); extern struct inode *hfs_iget(struct super_block *sb, struct hfs_cat_key *key, hfs_cat_rec *rec); +extern struct inode *hfs_system_iget(struct super_block *sb, u32 cnid); extern void hfs_evict_inode(struct inode *inode); extern void hfs_delete_inode(struct inode *inode); diff --git a/fs/hfs/inode.c b/fs/hfs/inode.c index 2aef3c3..5c00383 100644 --- a/fs/hfs/inode.c +++ b/fs/hfs/inode.c @@ -421,6 +421,87 @@ static int hfs_read_inode(struct inode *inode, void *data) return 0; } +/** + * hfs_system_read_inode - read an internal B-tree inode + * @inode: inode to initialize, hashed by its CNID + * + * The extent and catalog B-trees are described by the MDB instead of by + * catalog records, so they do not go through hfs_read_inode(). + * + * Return: 0 on success, -EIO if the MDB does not describe this CNID. + */ +static int hfs_system_read_inode(struct inode *inode) +{ + struct hfs_sb_info *hsb = HFS_SB(inode->i_sb); + struct hfs_mdb *mdb = hsb->mdb; + + HFS_I(inode)->flags = 0; + HFS_I(inode)->rsrc_inode = NULL; + mutex_init(&HFS_I(inode)->extents_lock); + HFS_I(inode)->tz_secondswest = sys_tz.tz_minuteswest * 60; + + inode->i_uid = hsb->s_uid; + inode->i_gid = hsb->s_gid; + set_nlink(inode, 1); + + switch (inode->i_ino) { + case HFS_EXT_CNID: + hfs_inode_read_fork(inode, mdb->drXTExtRec, mdb->drXTFlSize, + mdb->drXTFlSize, be32_to_cpu(mdb->drXTClpSiz)); + if (HFS_I(inode)->alloc_blocks > HFS_I(inode)->first_blocks) { + pr_err("invalid btree extent records\n"); + return -EIO; + } + break; + case HFS_CAT_CNID: + hfs_inode_read_fork(inode, mdb->drCTExtRec, mdb->drCTFlSize, + mdb->drCTFlSize, be32_to_cpu(mdb->drCTClpSiz)); + if (!HFS_I(inode)->first_blocks) { + pr_err("invalid btree extent records (0 size)\n"); + return -EIO; + } + break; + default: + return -EIO; + } + + /* + * may_open() requires a file type, and a crafted catalog record can + * reach this inode by its CNID. + */ + inode->i_mode = S_IFREG; + + return 0; +} + +/** + * hfs_system_iget - look up an internal B-tree inode by CNID + * @sb: super block + * @cnid: HFS_EXT_CNID or HFS_CAT_CNID + * + * Return: the inode, or an ERR_PTR() if it cannot be initialized. + */ +struct inode *hfs_system_iget(struct super_block *sb, u32 cnid) +{ + struct inode *inode; + int err; + + inode = iget_locked(sb, cnid); + if (!inode) + return ERR_PTR(-ENOMEM); + if (!(inode_state_read_once(inode) & I_NEW)) + return inode; + + err = hfs_system_read_inode(inode); + if (err) { + iget_failed(inode); + return ERR_PTR(err); + } + + unlock_new_inode(inode); + return inode; +} + /* * __hfs_iget() * @@ -446,6 +527,15 @@ struct inode *hfs_iget(struct super_block *sb, struct hfs_cat_key *key, hfs_cat_ default: return NULL; } + + /* + * hfs_test_inode() matches on i_ino only, so a catalog record naming + * a reserved CNID would hand out a B-tree inode as a regular dentry. + * The root directory is the one reserved CNID that comes through here. + */ + if (cnid != HFS_ROOT_CNID && cnid < HFS_FIRSTUSER_CNID) + return NULL; + inode = iget5_locked(sb, cnid, hfs_test_inode, hfs_read_inode, &data); if (inode && (inode_state_read_once(inode) & I_NEW)) unlock_new_inode(inode); -- 2.39.5 ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v3] hfs: give the internal B-tree inodes a type and keep catalog records off them 2026-10-13 6:44 ` [PATCH v3] hfs: give the internal B-tree inodes a type and keep catalog records off them yangguikun @ 2026-10-13 21:38 ` Viacheslav Dubeyko 0 siblings, 0 replies; 9+ messages in thread From: Viacheslav Dubeyko @ 2026-10-13 21:38 UTC (permalink / raw) To: yangguikun, John Paul Adrian Glaubitz, Yangtao Li Cc: linux-fsdevel, linux-kernel On Tue, 2026-10-13 at 14:44 +0800, yangguikun wrote: > hfs_btree_open() builds the extents and catalog B-tree inodes with a > raw > iget_locked() and never assigns them a file type, so both stay cached > with > i_mode == 0 after every mount. Two problems follow from that, and > both are > reachable from a crafted image. > > First, a catalog record that names CNID 3 or 4 makes hfs_lookup() -> > hfs_iget() > return the cached B-tree inode - hfs_test_inode() matches on i_ino > alone - and > the typeless inode then reaches the generic open path, where it trips > VFS_BUG_ON_INODE() in may_open() with CONFIG_DEBUG_VFS=y: > > VFS_BUG_ON_INODE(!IS_ANON_FILE(inode)): inode:... fs:hfs mode:0 > opflags:0xc > ------------[ cut here ]------------ > kernel BUG at fs/namei.c:4271! > RIP: 0010:may_open+0x2ba/0x480 fs/namei.c:4271 > Call Trace: > do_open fs/namei.c:4698 [inline] > path_openat+0x1600/0x3de0 fs/namei.c:4863 > __x64_sys_openat+0x13f/0x1f0 fs/open.c:1385 > > Without CONFIG_DEBUG_VFS the assertion compiles away and the same > image > silently yields an openable typeless inode, so this is not merely a > debug-config nuisance. > > Second, once the inode has a type, such a record still hands the > internal > B-tree file out as a regular dentry: it can be opened and read, which > exposes > the B-tree, and from then on the VFS treats an inode that has no > catalog > record behind it as an ordinary file. hfs_is_valid_cnid() does not > close this > path: it runs from hfs_read_inode(), and a lookup that hits the > already cached > B-tree inode never reaches that function. > > Fix both. Add hfs_system_read_inode(), which reads the fork from the > MDB and > assigns S_IFREG - the counterpart of hfsplus_system_read_inode() - > and > hfs_system_iget(), which looks the inode up through the normal inode- > cache path > and can fail before the inode is published. hfs_btree_open() now > uses > hfs_system_iget(), and the extent-record validation moves into the > new read > helper. hfs_iget() refuses catalog records that name a reserved CNID > below > HFS_FIRSTUSER_CNID; the root directory, the one reserved CNID that > legitimately > comes through there, is exempt. > > Found by syzkaller-style fuzzing of v7.2-rc7; still reproducible on > current > mainline. > > Suggested-by: Viacheslav Dubeyko <slava@dubeyko.com> > Link: > https://lore.kernel.org/linux-fsdevel/tencent_24D928451FEFA8A35482EFFA9132A7305307@qq.com/ > Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") > Cc: stable@vger.kernel.org > Signed-off-by: yangguikun <yangguikun19@qq.com> > --- > fs/hfs/btree.c | 39 +++------------------------ > fs/hfs/hfs_fs.h | 1 + > fs/hfs/inode.c | 82 > +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ > 3 files changed, 86 insertions(+), 36 deletions(-) > > diff --git a/fs/hfs/btree.c b/fs/hfs/btree.c > index 41b4e8f..b5892de 100644 > --- a/fs/hfs/btree.c > +++ b/fs/hfs/btree.c > @@ -155,44 +155,11 @@ struct hfs_btree *hfs_btree_open(struct > super_block *sb, u32 id, btree_keycmp ke > tree->cnid = id; > tree->keycmp = keycmp; > > - tree->inode = iget_locked(sb, id); > - if (!tree->inode) > + tree->inode = hfs_system_iget(sb, id); > + if (IS_ERR(tree->inode)) > goto free_tree; I worried that we jump to free_tree now. Before, we cannot get inode but now method can or cannot get inode. We need somehow distinguish these cases now and to jump to free_tree or to free_inode. Or we need to manage it in more smart way. > - BUG_ON(!(inode_state_read_once(tree->inode) & I_NEW)); I am not sure that it's good idea to drop this. If hfs_system_iget() quietly returns an existing inode. In hfs_btree_open() that would mean the same tree was opened twice. > - { > - struct hfs_mdb *mdb = HFS_SB(sb)->mdb; > - HFS_I(tree->inode)->flags = 0; > - mutex_init(&HFS_I(tree->inode)->extents_lock); > - switch (id) { > - case HFS_EXT_CNID: > - hfs_inode_read_fork(tree->inode, mdb->drXTExtRec, > mdb->drXTFlSize, > - mdb->drXTFlSize, > be32_to_cpu(mdb->drXTClpSiz)); > - if (HFS_I(tree->inode)->alloc_blocks > > - HFS_I(tree->inode)- > >first_blocks) { > - pr_err("invalid btree extent records\n"); > - unlock_new_inode(tree->inode); > - goto free_inode; > - } > > - tree->inode->i_mapping->a_ops = &hfs_btree_aops; > - break; > - case HFS_CAT_CNID: > - hfs_inode_read_fork(tree->inode, mdb->drCTExtRec, > mdb->drCTFlSize, > - mdb->drCTFlSize, > be32_to_cpu(mdb->drCTClpSiz)); > - > - if (!HFS_I(tree->inode)->first_blocks) { > - pr_err("invalid btree extent records (0 > size)\n"); > - unlock_new_inode(tree->inode); > - goto free_inode; > - } > - > - tree->inode->i_mapping->a_ops = &hfs_btree_aops; > - break; > - default: > - BUG(); > - } > - } > - unlock_new_inode(tree->inode); > + tree->inode->i_mapping->a_ops = &hfs_btree_aops; Set a_ops inside hfs_system_read_inode(). Right now tree->inode- >i_mapping->a_ops = &hfs_btree_aops; runs in hfs_btree_open() after unlock_new_inode(), so the inode is visible in the hash briefly without its aops. > > mapping = tree->inode->i_mapping; > folio = filemap_grab_folio(mapping, 0); > diff --git a/fs/hfs/hfs_fs.h b/fs/hfs/hfs_fs.h > index e250f87..26505f7 100644 > --- a/fs/hfs/hfs_fs.h > +++ b/fs/hfs/hfs_fs.h > @@ -219,6 +219,7 @@ extern void hfs_inode_read_fork(struct inode > *inode, struct hfs_extent *ext, > u32 clump_size); > extern struct inode *hfs_iget(struct super_block *sb, struct > hfs_cat_key *key, > hfs_cat_rec *rec); > +extern struct inode *hfs_system_iget(struct super_block *sb, u32 > cnid); > extern void hfs_evict_inode(struct inode *inode); > extern void hfs_delete_inode(struct inode *inode); > > diff --git a/fs/hfs/inode.c b/fs/hfs/inode.c > index 2aef3c3..5c00383 100644 > --- a/fs/hfs/inode.c > +++ b/fs/hfs/inode.c > @@ -421,6 +421,87 @@ static int hfs_read_inode(struct inode *inode, > void *data) > return 0; > } > > +/** > + * hfs_system_read_inode - read an internal B-tree inode > + * @inode: inode to initialize, hashed by its CNID > + * > + * The extent and catalog B-trees are described by the MDB instead > of by > + * catalog records, so they do not go through hfs_read_inode(). > + * > + * Return: 0 on success, -EIO if the MDB does not describe this > CNID. > + */ > +static int hfs_system_read_inode(struct inode *inode) > +{ > + struct hfs_sb_info *hsb = HFS_SB(inode->i_sb); > + struct hfs_mdb *mdb = hsb->mdb; > + > + HFS_I(inode)->flags = 0; > + HFS_I(inode)->rsrc_inode = NULL; > + mutex_init(&HFS_I(inode)->extents_lock); > + HFS_I(inode)->tz_secondswest = sys_tz.tz_minuteswest * 60; > + > + inode->i_uid = hsb->s_uid; > + inode->i_gid = hsb->s_gid; > + set_nlink(inode, 1); > + > + switch (inode->i_ino) { > + case HFS_EXT_CNID: > + hfs_inode_read_fork(inode, mdb->drXTExtRec, mdb- > >drXTFlSize, > + mdb->drXTFlSize, > be32_to_cpu(mdb->drXTClpSiz)); > + if (HFS_I(inode)->alloc_blocks > HFS_I(inode)- > >first_blocks) { > + pr_err("invalid btree extent records\n"); > + return -EIO; > + } > + break; > + case HFS_CAT_CNID: > + hfs_inode_read_fork(inode, mdb->drCTExtRec, mdb- > >drCTFlSize, > + mdb->drCTFlSize, > be32_to_cpu(mdb->drCTClpSiz)); > + if (!HFS_I(inode)->first_blocks) { > + pr_err("invalid btree extent records (0 > size)\n"); > + return -EIO; > + } > + break; > + default: > + return -EIO; > + } > + > + /* > + * may_open() requires a file type, and a crafted catalog > record can > + * reach this inode by its CNID. > + */ > + inode->i_mode = S_IFREG; > + > + return 0; > +} > + > +/** > + * hfs_system_iget - look up an internal B-tree inode by CNID > + * @sb: super block > + * @cnid: HFS_EXT_CNID or HFS_CAT_CNID > + * > + * Return: the inode, or an ERR_PTR() if it cannot be initialized. > + */ > +struct inode *hfs_system_iget(struct super_block *sb, u32 cnid) > +{ > + struct inode *inode; > + int err; > + > + inode = iget_locked(sb, cnid); > + if (!inode) > + return ERR_PTR(-ENOMEM); > + if (!(inode_state_read_once(inode) & I_NEW)) > + return inode; > + > + err = hfs_system_read_inode(inode); > + if (err) { > + iget_failed(inode); > + return ERR_PTR(err); > + } > + > + unlock_new_inode(inode); > + return inode; > +} > + > /* > * __hfs_iget() > * > @@ -446,6 +527,15 @@ struct inode *hfs_iget(struct super_block *sb, > struct hfs_cat_key *key, hfs_cat_ > default: > return NULL; > } > + > + /* > + * hfs_test_inode() matches on i_ino only, so a catalog > record naming > + * a reserved CNID would hand out a B-tree inode as a > regular dentry. > + * The root directory is the one reserved CNID that comes > through here. > + */ > + if (cnid != HFS_ROOT_CNID && cnid < HFS_FIRSTUSER_CNID) I am guessing maybe we need to introduce a special method: static inline bool hfs_is_system_reserved_cnid(u32 cnid) { if (likely(cnid > HFS_ROOT_CNID && cnid < HFS_FIRSTUSER_CNID)) return true; else return false; } What do you think? Thanks, Slava. > + return NULL; > + > inode = iget5_locked(sb, cnid, hfs_test_inode, > hfs_read_inode, &data); > if (inode && (inode_state_read_once(inode) & I_NEW)) > unlock_new_inode(inode); ^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v2 1/2] hfs: create the internal B-tree inodes through the generic iget path [not found] ` <20261012154438.6396-1-yangguikun19@qq.com> 2026-10-12 7:44 ` [PATCH v2 2/2] hfs: refuse catalog records that address the reserved B-tree CNIDs 1sh1ro @ 2026-10-12 7:44 ` 1sh1ro 2026-10-12 21:26 ` Viacheslav Dubeyko 1 sibling, 1 reply; 9+ messages in thread From: 1sh1ro @ 2026-10-12 7:44 UTC (permalink / raw) To: Viacheslav Dubeyko, John Paul Adrian Glaubitz, Yangtao Li Cc: linux-fsdevel, linux-kernel hfs_btree_open() builds the extents and catalog B-tree inodes with a raw iget_locked() and never assigns them a file type, so both stay cached with i_mode == 0 after every mount. A crafted HFS image whose catalog contains a record addressing one of the reserved B-tree CNIDs (3 or 4) makes hfs_lookup() -> hfs_iget() return that cached inode - hfs_test_inode() matches on i_ino alone - and the typeless inode then reaches the generic open path, where it trips VFS_BUG_ON_INODE() in may_open() with CONFIG_DEBUG_VFS=y: VFS_BUG_ON_INODE(!IS_ANON_FILE(inode)): inode:... fs:hfs mode:0 opflags:0xc ------------[ cut here ]------------ kernel BUG at fs/namei.c:4271! RIP: 0010:may_open+0x2ba/0x480 fs/namei.c:4271 Call Trace: do_open fs/namei.c:4698 [inline] path_openat+0x1600/0x3de0 fs/namei.c:4863 __x64_sys_openat+0x13f/0x1f0 fs/open.c:1385 Without CONFIG_DEBUG_VFS the assertion compiles away and the same image silently yields an openable typeless inode, so this is not merely a debug-config nuisance. Fix it the way HFS+ handles its system inodes: add hfs_system_read_inode(), which reads the fork from the MDB (the counterpart of hfsplus_system_read_inode()), and hfs_system_iget(), which looks the inode up through the normal inode-cache path and can fail before the inode is published. hfs_btree_open() now uses hfs_system_iget(), so the B-tree inodes carry a valid S_IFREG type like every other inode. The extent-record validation is unchanged; it just runs after the inode has been created. Found by syzkaller-style fuzzing of v7.2-rc7; still reproducible on current mainline. Suggested-by: Viacheslav Dubeyko <slava@dubeyko.com> Link: https://lore.kernel.org/linux-fsdevel/tencent_24D928451FEFA8A35482EFFA9132A7305307@qq.com/ Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Cc: stable@vger.kernel.org Signed-off-by: 1sh1ro <yangguikun19@qq.com> --- fs/hfs/btree.c | 25 +++++-------------------- fs/hfs/hfs_fs.h | 1 + fs/hfs/inode.c | 74 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 20 deletions(-) diff --git a/fs/hfs/btree.c b/fs/hfs/btree.c index 41b4e8f..7486707 100644 --- a/fs/hfs/btree.c +++ b/fs/hfs/btree.c @@ -155,44 +155,29 @@ struct hfs_btree *hfs_btree_open(struct super_block *sb, u32 id, btree_keycmp ke tree->cnid = id; tree->keycmp = keycmp; - tree->inode = iget_locked(sb, id); - if (!tree->inode) + tree->inode = hfs_system_iget(sb, id); + if (IS_ERR(tree->inode)) goto free_tree; - BUG_ON(!(inode_state_read_once(tree->inode) & I_NEW)); - { - struct hfs_mdb *mdb = HFS_SB(sb)->mdb; - HFS_I(tree->inode)->flags = 0; - mutex_init(&HFS_I(tree->inode)->extents_lock); + switch (id) { case HFS_EXT_CNID: - hfs_inode_read_fork(tree->inode, mdb->drXTExtRec, mdb->drXTFlSize, - mdb->drXTFlSize, be32_to_cpu(mdb->drXTClpSiz)); if (HFS_I(tree->inode)->alloc_blocks > HFS_I(tree->inode)->first_blocks) { pr_err("invalid btree extent records\n"); - unlock_new_inode(tree->inode); goto free_inode; } - - tree->inode->i_mapping->a_ops = &hfs_btree_aops; break; case HFS_CAT_CNID: - hfs_inode_read_fork(tree->inode, mdb->drCTExtRec, mdb->drCTFlSize, - mdb->drCTFlSize, be32_to_cpu(mdb->drCTClpSiz)); - if (!HFS_I(tree->inode)->first_blocks) { pr_err("invalid btree extent records (0 size)\n"); - unlock_new_inode(tree->inode); goto free_inode; } - - tree->inode->i_mapping->a_ops = &hfs_btree_aops; break; default: BUG(); } - } - unlock_new_inode(tree->inode); + + tree->inode->i_mapping->a_ops = &hfs_btree_aops; mapping = tree->inode->i_mapping; folio = filemap_grab_folio(mapping, 0); diff --git a/fs/hfs/hfs_fs.h b/fs/hfs/hfs_fs.h index e250f87..26505f7 100644 --- a/fs/hfs/hfs_fs.h +++ b/fs/hfs/hfs_fs.h @@ -219,6 +219,7 @@ extern void hfs_inode_read_fork(struct inode *inode, struct hfs_extent *ext, u32 clump_size); extern struct inode *hfs_iget(struct super_block *sb, struct hfs_cat_key *key, hfs_cat_rec *rec); +extern struct inode *hfs_system_iget(struct super_block *sb, u32 cnid); extern void hfs_evict_inode(struct inode *inode); extern void hfs_delete_inode(struct inode *inode); diff --git a/fs/hfs/inode.c b/fs/hfs/inode.c index 2aef3c3..4316d75 100644 --- a/fs/hfs/inode.c +++ b/fs/hfs/inode.c @@ -421,6 +421,80 @@ static int hfs_read_inode(struct inode *inode, void *data) return 0; } +/* + * hfs_system_read_inode + * + * Initialize one of the internal B-tree inodes. These are described by the + * MDB rather than by catalog records, so they need their own read helper - + * the HFS counterpart of hfsplus_system_read_inode(). + */ +static int hfs_system_read_inode(struct inode *inode) +{ + struct hfs_sb_info *hsb = HFS_SB(inode->i_sb); + struct hfs_mdb *mdb = hsb->mdb; + + HFS_I(inode)->flags = 0; + HFS_I(inode)->rsrc_inode = NULL; + mutex_init(&HFS_I(inode)->extents_lock); + HFS_I(inode)->tz_secondswest = sys_tz.tz_minuteswest * 60; + + inode->i_uid = hsb->s_uid; + inode->i_gid = hsb->s_gid; + set_nlink(inode, 1); + + switch (inode->i_ino) { + case HFS_EXT_CNID: + hfs_inode_read_fork(inode, mdb->drXTExtRec, mdb->drXTFlSize, + mdb->drXTFlSize, be32_to_cpu(mdb->drXTClpSiz)); + break; + case HFS_CAT_CNID: + hfs_inode_read_fork(inode, mdb->drCTExtRec, mdb->drCTFlSize, + mdb->drCTFlSize, be32_to_cpu(mdb->drCTClpSiz)); + break; + default: + return -EIO; + } + + /* + * Assign a dummy file type, for may_open() requires that an inode has + * a valid file type. These inodes are hashed by CNID like any other + * inode, so without a type a catalog record of a crafted image that + * aliases CNID 3 or 4 would reach the VFS with i_mode == 0. + */ + inode->i_mode = S_IFREG; + + return 0; +} + +/* + * hfs_system_iget + * + * Look up (or create) one of the internal B-tree inodes. hfs_btree_open() + * uses this instead of a raw iget_locked() so that the inode is completely + * initialized before it becomes visible in the inode cache, in the same way + * hfsplus_iget() handles the system CNIDs for HFS+. + */ +struct inode *hfs_system_iget(struct super_block *sb, u32 cnid) +{ + struct inode *inode; + int err; + + inode = iget_locked(sb, cnid); + if (!inode) + return ERR_PTR(-ENOMEM); + if (!(inode_state_read_once(inode) & I_NEW)) + return inode; + + err = hfs_system_read_inode(inode); + if (err) { + iget_failed(inode); + return ERR_PTR(err); + } + + unlock_new_inode(inode); + return inode; +} + /* * __hfs_iget() * -- 2.39.5 ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 1/2] hfs: create the internal B-tree inodes through the generic iget path 2026-10-12 7:44 ` [PATCH v2 1/2] hfs: create the internal B-tree inodes through the generic iget path 1sh1ro @ 2026-10-12 21:26 ` Viacheslav Dubeyko 0 siblings, 0 replies; 9+ messages in thread From: Viacheslav Dubeyko @ 2026-10-12 21:26 UTC (permalink / raw) To: 1sh1ro, John Paul Adrian Glaubitz, Yangtao Li; +Cc: linux-fsdevel, linux-kernel On Mon, 2026-10-12 at 15:44 +0800, 1sh1ro wrote: > hfs_btree_open() builds the extents and catalog B-tree inodes with a > raw > iget_locked() and never assigns them a file type, so both stay cached > with > i_mode == 0 after every mount. A crafted HFS image whose catalog > contains > a record addressing one of the reserved B-tree CNIDs (3 or 4) makes > hfs_lookup() -> hfs_iget() return that cached inode - > hfs_test_inode() > matches on i_ino alone - and the typeless inode then reaches the > generic > open path, where it trips VFS_BUG_ON_INODE() in may_open() with > CONFIG_DEBUG_VFS=y: > > VFS_BUG_ON_INODE(!IS_ANON_FILE(inode)): inode:... fs:hfs mode:0 > opflags:0xc > ------------[ cut here ]------------ > kernel BUG at fs/namei.c:4271! > RIP: 0010:may_open+0x2ba/0x480 fs/namei.c:4271 > Call Trace: > do_open fs/namei.c:4698 [inline] > path_openat+0x1600/0x3de0 fs/namei.c:4863 > __x64_sys_openat+0x13f/0x1f0 fs/open.c:1385 > > Without CONFIG_DEBUG_VFS the assertion compiles away and the same > image > silently yields an openable typeless inode, so this is not merely a > debug-config nuisance. > > Fix it the way HFS+ handles its system inodes: add > hfs_system_read_inode(), > which reads the fork from the MDB (the counterpart of > hfsplus_system_read_inode()), and hfs_system_iget(), which looks the > inode > up through the normal inode-cache path and can fail before the inode > is > published. hfs_btree_open() now uses hfs_system_iget(), so the B- > tree > inodes carry a valid S_IFREG type like every other inode. The > extent-record validation is unchanged; it just runs after the inode > has > been created. > > Found by syzkaller-style fuzzing of v7.2-rc7; still reproducible on > current mainline. > > Suggested-by: Viacheslav Dubeyko <slava@dubeyko.com> > Link: > https://lore.kernel.org/linux-fsdevel/tencent_24D928451FEFA8A35482EFFA9132A7305307@qq.com/ > Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") > Cc: stable@vger.kernel.org > Signed-off-by: 1sh1ro <yangguikun19@qq.com> Please, provide the real name. > --- > fs/hfs/btree.c | 25 +++++-------------------- > fs/hfs/hfs_fs.h | 1 + > fs/hfs/inode.c | 74 ++++++++++++++++++++++++++++++++++++++++ > 3 files changed, 80 insertions(+), 20 deletions(-) > > diff --git a/fs/hfs/btree.c b/fs/hfs/btree.c > index 41b4e8f..7486707 100644 > --- a/fs/hfs/btree.c > +++ b/fs/hfs/btree.c > @@ -155,44 +155,29 @@ struct hfs_btree *hfs_btree_open(struct > super_block *sb, u32 id, btree_keycmp ke > tree->cnid = id; > tree->keycmp = keycmp; > > - tree->inode = iget_locked(sb, id); > - if (!tree->inode) > + tree->inode = hfs_system_iget(sb, id); > + if (IS_ERR(tree->inode)) > goto free_tree; > - BUG_ON(!(inode_state_read_once(tree->inode) & I_NEW)); > - { > - struct hfs_mdb *mdb = HFS_SB(sb)->mdb; > - HFS_I(tree->inode)->flags = 0; > - mutex_init(&HFS_I(tree->inode)->extents_lock); > + > switch (id) { > case HFS_EXT_CNID: > - hfs_inode_read_fork(tree->inode, mdb->drXTExtRec, > mdb->drXTFlSize, > - mdb->drXTFlSize, > be32_to_cpu(mdb->drXTClpSiz)); > if (HFS_I(tree->inode)->alloc_blocks > > HFS_I(tree->inode)- > >first_blocks) { > pr_err("invalid btree extent records\n"); This check should be in hfs_system_read_inode() now. > - unlock_new_inode(tree->inode); > goto free_inode; > } > - > - tree->inode->i_mapping->a_ops = &hfs_btree_aops; > break; > case HFS_CAT_CNID: > - hfs_inode_read_fork(tree->inode, mdb->drCTExtRec, > mdb->drCTFlSize, > - mdb->drCTFlSize, > be32_to_cpu(mdb->drCTClpSiz)); > - > if (!HFS_I(tree->inode)->first_blocks) { > pr_err("invalid btree extent records (0 > size)\n"); Ditto. This check should be in hfs_system_read_inode() now. > - unlock_new_inode(tree->inode); > goto free_inode; > } > - > - tree->inode->i_mapping->a_ops = &hfs_btree_aops; > break; > default: > BUG(); > } > - } > - unlock_new_inode(tree->inode); > + > + tree->inode->i_mapping->a_ops = &hfs_btree_aops; > > mapping = tree->inode->i_mapping; > folio = filemap_grab_folio(mapping, 0); > diff --git a/fs/hfs/hfs_fs.h b/fs/hfs/hfs_fs.h > index e250f87..26505f7 100644 > --- a/fs/hfs/hfs_fs.h > +++ b/fs/hfs/hfs_fs.h > @@ -219,6 +219,7 @@ extern void hfs_inode_read_fork(struct inode > *inode, struct hfs_extent *ext, > u32 clump_size); > extern struct inode *hfs_iget(struct super_block *sb, struct > hfs_cat_key *key, > hfs_cat_rec *rec); > +extern struct inode *hfs_system_iget(struct super_block *sb, u32 > cnid); > extern void hfs_evict_inode(struct inode *inode); > extern void hfs_delete_inode(struct inode *inode); > > diff --git a/fs/hfs/inode.c b/fs/hfs/inode.c > index 2aef3c3..4316d75 100644 > --- a/fs/hfs/inode.c > +++ b/fs/hfs/inode.c > @@ -421,6 +421,80 @@ static int hfs_read_inode(struct inode *inode, > void *data) > return 0; > } > > +/* > + * hfs_system_read_inode You need to place short abstract of the function: hfs_system_read_inode() - <short explanation> or simply remove it because there is no useful info here. > + * > + * Initialize one of the internal B-tree inodes. These are > described by the > + * MDB rather than by catalog records, so they need their own read > helper - > + * the HFS counterpart of hfsplus_system_read_inode(). Could you please rework comment? It is not useful now. > + */ > +static int hfs_system_read_inode(struct inode *inode) > +{ > + struct hfs_sb_info *hsb = HFS_SB(inode->i_sb); > + struct hfs_mdb *mdb = hsb->mdb; > + > + HFS_I(inode)->flags = 0; > + HFS_I(inode)->rsrc_inode = NULL; > + mutex_init(&HFS_I(inode)->extents_lock); > + HFS_I(inode)->tz_secondswest = sys_tz.tz_minuteswest * 60; > + > + inode->i_uid = hsb->s_uid; > + inode->i_gid = hsb->s_gid; > + set_nlink(inode, 1); > + > + switch (inode->i_ino) { > + case HFS_EXT_CNID: > + hfs_inode_read_fork(inode, mdb->drXTExtRec, mdb- > >drXTFlSize, > + mdb->drXTFlSize, > be32_to_cpu(mdb->drXTClpSiz)); > + break; > + case HFS_CAT_CNID: > + hfs_inode_read_fork(inode, mdb->drCTExtRec, mdb- > >drCTFlSize, > + mdb->drCTFlSize, > be32_to_cpu(mdb->drCTClpSiz)); > + break; > + default: > + return -EIO; > + } > + > + /* > + * Assign a dummy file type, for may_open() requires that an > inode has > + * a valid file type. These inodes are hashed by CNID like > any other > + * inode, so without a type a catalog record of a crafted > image that > + * aliases CNID 3 or 4 would reach the VFS with i_mode == 0. > + */ Could it be short and clear comment? > + inode->i_mode = S_IFREG; > + > + return 0; > +} > + > +/* > + * hfs_system_iget Ditto. > + * > + * Look up (or create) one of the internal B-tree inodes. > hfs_btree_open() > + * uses this instead of a raw iget_locked() so that the inode is > completely > + * initialized before it becomes visible in the inode cache, in the > same way > + * hfsplus_iget() handles the system CNIDs for HFS+. Could you please rework the AI generated comment? Currently, it is not useful. Thanks, Slava. > + */ > +struct inode *hfs_system_iget(struct super_block *sb, u32 cnid) > +{ > + struct inode *inode; > + int err; > + > + inode = iget_locked(sb, cnid); > + if (!inode) > + return ERR_PTR(-ENOMEM); > + if (!(inode_state_read_once(inode) & I_NEW)) > + return inode; > + > + err = hfs_system_read_inode(inode); > + if (err) { > + iget_failed(inode); > + return ERR_PTR(err); > + } > + > + unlock_new_inode(inode); > + return inode; > +} > + > /* > * __hfs_iget() > * ^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-10-13 21:38 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
[not found] <20260916080000.0-hfs-fix@localhost>
2026-09-16 9:09 ` [PATCH 1/1] hfs: don't let internal B-tree inodes leak into the VFS open path 1sh1ro
2026-09-16 18:33 ` Viacheslav Dubeyko
2026-10-12 7:44 ` [PATCH v2 0/2] hfs: keep the internal B-tree inodes out of " 1sh1ro
[not found] ` <20261012154438.6396-1-yangguikun19@qq.com>
2026-10-12 7:44 ` [PATCH v2 2/2] hfs: refuse catalog records that address the reserved B-tree CNIDs 1sh1ro
2026-10-12 21:30 ` Viacheslav Dubeyko
2026-10-13 6:44 ` [PATCH v3] hfs: give the internal B-tree inodes a type and keep catalog records off them yangguikun
2026-10-13 21:38 ` Viacheslav Dubeyko
2026-10-12 7:44 ` [PATCH v2 1/2] hfs: create the internal B-tree inodes through the generic iget path 1sh1ro
2026-10-12 21:26 ` Viacheslav Dubeyko
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®