[PATCH v6] hfs: initialize B-tree inodes and validate catalog CNIDs

From: yangguikun

Date: Fri Oct 16 2026 - 13:49:17 EST


hfs_btree_open() leaves the internal B-tree inodes with i_mode == 0.
A crafted catalog record naming CNID 3 or 4 can return one of these
cached inodes through hfs_iget(), triggering VFS_BUG_ON_INODE() in
may_open() with CONFIG_DEBUG_VFS=y.

Initialize the B-tree inodes with S_IFREG and their address-space
operations before publishing them. Reject attempts to reuse an
already-cached B-tree inode.

Validate catalog CNIDs and record types before the inode-cache lookup,
since cached inodes bypass hfs_read_inode() validation. This also
prevents a crafted file record from aliasing the root directory inode.

Found by syzkaller-style fuzzing of v7.2-rc7.

Suggested-by: Viacheslav Dubeyko <slava@xxxxxxxxxxx>
Link: https://lore.kernel.org/linux-fsdevel/tencent_24D928451FEFA8A35482EFFA9132A7305307@xxxxxx/
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: yangguikun <yangguikun19@xxxxxx>
---
Changes in v6:
- Drop redundant set_nlink(inode, 1); the link count remains 1.
- Drop tz_secondswest initialization for B-tree inodes.
- Use WARN_ON_ONCE() for an already-cached B-tree inode.
- Defer hfs_iget() error propagation to a separate patch.

diff --git a/fs/hfs/btree.c b/fs/hfs/btree.c
index 41b4e8f..1891ad5 100644
--- a/fs/hfs/btree.c
+++ b/fs/hfs/btree.c
@@ -155,44 +155,9 @@ 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);

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..8d52170 100644
--- a/fs/hfs/hfs_fs.h
+++ b/fs/hfs/hfs_fs.h
@@ -163,6 +163,11 @@ extern int hfs_cat_move(u32 cnid, struct inode *src_dir,
extern void hfs_cat_build_key(struct super_block *sb, btree_key *key,
u32 parent, const struct qstr *name);

+static inline bool hfs_is_system_reserved_cnid(u32 cnid)
+{
+ return cnid > HFS_ROOT_CNID && cnid < HFS_FIRSTUSER_CNID;
+}
+
/*
* Validate the CNID of a catalog record.
*/
@@ -219,6 +224,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..a66de7b 100644
--- a/fs/hfs/inode.c
+++ b/fs/hfs/inode.c
@@ -421,6 +421,69 @@ static int hfs_read_inode(struct inode *inode, void *data)
return 0;
}

+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);
+
+ inode->i_uid = hsb->s_uid;
+ inode->i_gid = hsb->s_gid;
+
+ inode->i_mode = S_IFREG;
+ inode->i_mapping->a_ops = &hfs_btree_aops;
+
+ 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;
+ }
+
+ return 0;
+}
+
+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 (WARN_ON_ONCE(!(inode_state_read_once(inode) & I_NEW))) {
+ iput(inode);
+ return ERR_PTR(-EEXIST);
+ }
+
+ 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 +509,11 @@ struct inode *hfs_iget(struct super_block *sb, struct hfs_cat_key *key, hfs_cat_
default:
return NULL;
}
+
+ if (!hfs_is_valid_cnid(cnid, rec->type) ||
+ hfs_is_system_reserved_cnid(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.53.0.windows.1