Skip to contentSkip to conversation

hfs: don't let internal B-tree inodes leak into the VFS open path

PATCH In review 1sh1ro 2 participants 15 messages Sep 16 – Oct 19 2 messages not in the archive

Subsystems (3)
lore
Share & tools
lore

A message that is not in the archive (sent off-list or not yet received): 20260916080000.0-hfs-fix@localhost

1sh1ro

author
#1 lore raw

PATCH 1/1 hfs: don't let internal B-tree inodes leak into the VFS open path

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")
---
 fs/hfs/btree.c | 8 ++++++++
 fs/hfs/inode.c | 11 +++++++++++
 2 files changed, 19 insertions(+)
fs/hfs/btree.c +8 -0
index 2eb37a2f64e8..bd69ace8f309 100644
@@ -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;
fs/hfs/inode.c +11 -0
index ac4a9055c5c0..bb759345f1fc 100644
@@ -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

#2 lore raw
Quoted 59 lines · 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")

---
 fs/hfs/btree.c | 8 ++++++++
 fs/hfs/inode.c | 11 +++++++++++

 2 files changed, 19 insertions(+)
fs/hfs/btree.c +0 -0
index 2eb37a2f64e8..bd69ace8f309 100644
@@ -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)); +

Quoted 8 lines

+   /* 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.

Quoted 24 lines

    {
    struct hfs_mdb *mdb = HFS_SB(sb)->mdb;
    HFS_I(tree->inode)->flags = 0;

fs/hfs/inode.c +0 -0
index ac4a9055c5c0..bb759345f1fc 100644
@@ -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)

Quoted 8 lines

+      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

author
#3 lore raw

v2 cover hfs: keep the internal B-tree inodes out of the VFS open path

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(-)

A message that is not in the archive (sent off-list or not yet received): [email protected]

author
#5 lore raw

PATCH v2 1/2 hfs: create the internal B-tree inodes through the generic iget path

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.

---
  fs/hfs/btree.c  |  25 +++++--------------------
 fs/hfs/hfs_fs.h |   1 +
 fs/hfs/inode.c  |  74 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 80 insertions(+), 20 deletions(-)
fs/hfs/btree.c +5 -20
index 41b4e8f..7486707 100644
@@ -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);
fs/hfs/hfs_fs.h +1 -0
index e250f87..26505f7 100644
@@ -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);
 
fs/hfs/inode.c +74 -0

80 lines not on this page · raw message

Signature · 1 line
2.39.5
#6 lore raw

Re: [PATCH v2 1/2] hfs: create the internal B-tree inodes through the generic iget path

Quoted 46 lines · 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.

Quoted 6 lines

Link:
https://lore.kernel.org/linux-fsdevel/[email protected]/

Fixes 1da177e4c3f4 ("Linux-2.6.12-rc2")

Please, provide the real name.

Quoted 34 lines

---
  fs/hfs/btree.c  |  25 +++++-------------------- fs/hfs/hfs_fs.h |   1 +
 fs/hfs/inode.c  |  74 ++++++++++++++++++++++++++++++++++++++++

 3 files changed, 80 insertions(+), 20 deletions(-)
fs/hfs/btree.c +0 -0
index 41b4e8f..7486707 100644
@@ -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;

fs/hfs/btree.c +2 -2
-	tree->inode = iget_locked(sb, id);
-	if (!tree->inode)
+	tree->inode = hfs_system_iget(sb, id);
+	if (IS_ERR(tree->inode))

  goto free_tree;

fs/hfs/btree.c +1 -5
-	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:

fs/hfs/btree.c +0 -1
-		hfs_inode_read_fork(tree->inode, mdb->drXTExtRec,

mdb->drXTFlSize,

fs/hfs/btree.c +0 -1
-				    mdb->drXTFlSize,

be32_to_cpu(mdb->drXTClpSiz));
  if (HFS_I(tree->inode)->alloc_blocks > HFS_I(tree->inode)-

Quoted 2 lines
Quoted 1 line

first_blocks) {

  pr_err("invalid btree extent records\n");

This check should be in hfs_system_read_inode() now.

Quoted 10 lines
fs/hfs/btree.c +0 -1
-			unlock_new_inode(tree->inode);

  goto free_inode;
  }

fs/hfs/btree.c +0 -2
-
-		tree->inode->i_mapping->a_ops = &hfs_btree_aops;

  break;
  case HFS_CAT_CNID:

fs/hfs/btree.c +0 -1
-		hfs_inode_read_fork(tree->inode, mdb->drCTExtRec,

mdb->drCTFlSize,

fs/hfs/btree.c +0 -1
-				    mdb->drCTFlSize,
Quoted 5 lines

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.

Quoted 39 lines
fs/hfs/btree.c +0 -1
-			unlock_new_inode(tree->inode);

  goto free_inode;
  }

fs/hfs/btree.c +0 -2
-
-		tree->inode->i_mapping->a_ops = &hfs_btree_aops;

  break;
  default:
  BUG();
  }

fs/hfs/btree.c +2 -2
-	}
-	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);

fs/hfs/hfs_fs.h +0 -0
index e250f87..26505f7 100644
@@ -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);

fs/hfs/inode.c +0 -0
index 2aef3c3..4316d75 100644
@@ -421,6 +421,80 @@ static int hfs_read_inode(struct inode *inode,
 void *data)
  	return 0;
  }
Quoted 3 lines


+/*
+ * 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.

Quoted 6 lines

+ *
+ * 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.

Quoted 39 lines

+ */

fs/hfs/inode.c +17 -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);
+	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-
Quoted 1 line

drXTFlSize,

fs/hfs/inode.c +1 -0
+				    mdb->drXTFlSize,

be32_to_cpu(mdb->drXTClpSiz));

fs/hfs/inode.c +3 -0
+		break;
+	case HFS_CAT_CNID:
+		hfs_inode_read_fork(inode, mdb->drCTExtRec, mdb-
Quoted 1 line

drCTFlSize,

fs/hfs/inode.c +1 -0
+				    mdb->drCTFlSize,

be32_to_cpu(mdb->drCTClpSiz));

fs/hfs/inode.c +7 -0
+		break;
+	default:
+		return -EIO;
+	}
+
+	/*
+	 * Assign a dummy file type, for may_open() requires that an

inode has

fs/hfs/inode.c +1 -0
+	 * a valid file type.  These inodes are hashed by CNID like

any other

fs/hfs/inode.c +1 -0
+	 * inode, so without a type a catalog record of a crafted

image that

fs/hfs/inode.c

Quoted 2 lines
fs/hfs/inode.c +2 -0
+	 * aliases CNID 3 or 4 would reach the VFS with i_mode == 0.
+	 */

Could it be short and clear comment?

fs/hfs/inode.c static int hfs_read_inode(struct inode *inode, void *data) · line 470

Quoted 7 lines
fs/hfs/inode.c +7 -0
+	inode->i_mode = S_IFREG;
+
+	return 0;
+}
+
+/*
+ * hfs_system_iget

Ditto.

Quoted 8 lines

+ *
+ * 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.

Quoted 25 lines

+ */

fs/hfs/inode.c +21 -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 (!(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()
  *

author
#7 lore raw

PATCH v3 1/1 hfs: give the internal B-tree inodes a type and keep catalog records off them

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.

---
 fs/hfs/btree.c  | 39 +++------------------------
 fs/hfs/hfs_fs.h |  1 +
 fs/hfs/inode.c  | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 86 insertions(+), 36 deletions(-)
fs/hfs/btree.c +3 -36
index 41b4e8f..b5892de 100644
@@ -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);
fs/hfs/hfs_fs.h +1 -0
index e250f87..26505f7 100644
@@ -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);
 
fs/hfs/inode.c +90 -0

102 lines not on this page · raw message

Signature · 1 line
2.39.5
#8 lore raw

Re: [PATCH v3] hfs: give the internal B-tree inodes a type and keep catalog records off them

Quoted 88 lines · yangguikun

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.

Link:
https://lore.kernel.org/linux-fsdevel/[email protected]/

Fixes 1da177e4c3f4 ("Linux-2.6.12-rc2")

---
 fs/hfs/btree.c  | 39 +++------------------------
 fs/hfs/hfs_fs.h |  1 +
 fs/hfs/inode.c  | 82
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++

 3 files changed, 86 insertions(+), 36 deletions(-)
fs/hfs/btree.c +0 -0
index 41b4e8f..b5892de 100644
@@ -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;

fs/hfs/btree.c +2 -2
-	tree->inode = iget_locked(sb, id);
-	if (!tree->inode)
+	tree->inode = hfs_system_iget(sb, id);
+	if (IS_ERR(tree->inode))
Quoted 1 line

  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.

fs/hfs/btree.c struct hfs_btree *hfs_btree_open(struct super_block *sb, u32 id, btree_keycmp ke · line 161

Quoted 1 line
fs/hfs/btree.c +0 -1
-	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.

Quoted 31 lines
fs/hfs/btree.c +0 -7
-	{
-	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,

fs/hfs/btree.c +0 -1
-				    mdb->drXTFlSize,

be32_to_cpu(mdb->drXTClpSiz));

fs/hfs/btree.c +0 -2
-		if (HFS_I(tree->inode)->alloc_blocks >
-					HFS_I(tree->inode)-
Quoted 1 line

first_blocks) {

fs/hfs/btree.c +0 -4
-			pr_err("invalid btree extent records\n");
-			unlock_new_inode(tree->inode);
-			goto free_inode;
-		}

fs/hfs/btree.c +0 -4
-		tree->inode->i_mapping->a_ops = &hfs_btree_aops;
-		break;
-	case HFS_CAT_CNID:
-		hfs_inode_read_fork(tree->inode, mdb->drCTExtRec,

mdb->drCTFlSize,

fs/hfs/btree.c +0 -1
-				    mdb->drCTFlSize,

be32_to_cpu(mdb->drCTClpSiz));

fs/hfs/btree.c +0 -3
-
-		if (!HFS_I(tree->inode)->first_blocks) {
-			pr_err("invalid btree extent records (0

size)\n");

fs/hfs/btree.c +0 -2
-			unlock_new_inode(tree->inode);
-			goto free_inode;

fs/hfs/btree.c struct hfs_btree *hfs_btree_open(struct super_block *sb, u32 id, btree_keycmp ke · line 162

Quoted 10 lines
fs/hfs/btree.c +1 -9
-		}
-
-		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-

Quoted 1 line

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.

Quoted 133 lines


  mapping = tree->inode->i_mapping;
  folio = filemap_grab_folio(mapping, 0);

fs/hfs/hfs_fs.h +0 -0
index e250f87..26505f7 100644
@@ -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);

fs/hfs/inode.c +0 -0
index 2aef3c3..5c00383 100644
@@ -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.
+ */

fs/hfs/inode.c +17 -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);
+	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-
Quoted 1 line

drXTFlSize,

fs/hfs/inode.c +1 -0
+				    mdb->drXTFlSize,

be32_to_cpu(mdb->drXTClpSiz));

fs/hfs/inode.c +1 -0
+		if (HFS_I(inode)->alloc_blocks > HFS_I(inode)-
Quoted 1 line

first_blocks) {

fs/hfs/inode.c +6 -0
+			pr_err("invalid btree extent records\n");
+			return -EIO;
+		}
+		break;
+	case HFS_CAT_CNID:
+		hfs_inode_read_fork(inode, mdb->drCTExtRec, mdb-
Quoted 1 line

drCTFlSize,

fs/hfs/inode.c +1 -0
+				    mdb->drCTFlSize,

be32_to_cpu(mdb->drCTClpSiz));

fs/hfs/inode.c +2 -0
+		if (!HFS_I(inode)->first_blocks) {
+			pr_err("invalid btree extent records (0

size)\n");

fs/hfs/inode.c +9 -0
+			return -EIO;
+		}
+		break;
+	default:
+		return -EIO;
+	}
+
+	/*
+	 * may_open() requires a file type, and a crafted catalog

record can

fs/hfs/inode.c +35 -0
+	 * 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()
  *

fs/hfs/inode.c +0 -0
@@ -446,6 +527,15 @@ struct inode *hfs_iget(struct super_block *sb,

struct hfs_cat_key *key, hfs_cat_
  default:
  return NULL;
  }

fs/hfs/inode.c +3 -0
+
+	/*
+	 * hfs_test_inode() matches on i_ino only, so a catalog

record naming

fs/hfs/inode.c +1 -0
+	 * a reserved CNID would hand out a B-tree inode as a

regular dentry.

fs/hfs/inode.c +1 -0
+	 * The root directory is the one reserved CNID that comes

through here.

fs/hfs/inode.c struct inode *hfs_iget(struct super_block *sb, struct hfs_cat_key *key, hfs_cat_ · line 536

Quoted 2 lines
fs/hfs/inode.c +2 -0
+	 */
+	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.

Quoted 6 lines
fs/hfs/inode.c +2 -0
+		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);

author
#9 lore raw

PATCH v4 1/1 hfs: initialize B-tree inodes and validate catalog CNIDs

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.

---
Changes in v4:
- set a_ops in hfs_system_read_inode(), before the inode is published.
- hfs_system_iget(): reject an already-cached inode (the original BUG_ON()).
- document why the free_tree error path is correct.
- add hfs_is_system_reserved_cnid(), as suggested.
- hfs_iget(): also reject records that hfs_read_inode() would reject (CNID 2 typed as a file, for example); an already-cached inode skips that check.

 fs/hfs/btree.c  |  43 ++++-------------------
 fs/hfs/hfs_fs.h |  13 +++++++
 fs/hfs/inode.c  | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 122 insertions(+), 37 deletions(-)
fs/hfs/btree.c +6 -37
index 41b4e8f..454caa1 100644
@@ -155,44 +155,13 @@ 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)
+	/*
+	 * hfs_system_iget() releases the inode itself when it cannot
+	 * initialize it, so there is nothing to iput here.
+	 */
+	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);
fs/hfs/hfs_fs.h +13 -0
index e250f87..ecdcf81 100644
@@ -163,6 +163,18 @@ 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);
 
+/*
+ * A CNID below HFS_FIRSTUSER_CNID is reserved for the system files; the
+ * root directory is the one reserved CNID that catalog records address.
+ */
+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;
+}
+
 /*
  * Validate the CNID of a catalog record.
  */
@@ -219,6 +231,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);
 
fs/hfs/inode.c +103 -0

115 lines not on this page · raw message

Signature · 1 line
2.39.5
#10 lore raw

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

Quoted 56 lines · yangguikun

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.

Link:
https://lore.kernel.org/linux-fsdevel/[email protected]/

Fixes 1da177e4c3f4 ("Linux-2.6.12-rc2")

---
Changes in v4:
 - set a_ops in hfs_system_read_inode(), before the inode is published.
 - hfs_system_iget(): reject an already-cached inode (the original BUG_ON()).
 - document why the free_tree error path is correct.
 - add hfs_is_system_reserved_cnid(), as suggested.
 - hfs_iget(): also reject records that hfs_read_inode() would reject (CNID 2 typed as a file, for example); an already-cached inode skips
   that check.

 fs/hfs/btree.c  |  43 ++++-------------------
 fs/hfs/hfs_fs.h |  13 +++++++
 fs/hfs/inode.c  | 103
++++++++++++++++++++++++++++++++++++++++++++++++++++++++

 3 files changed, 122 insertions(+), 37 deletions(-)
fs/hfs/btree.c +0 -0
index 41b4e8f..454caa1 100644
@@ -155,44 +155,13 @@ struct hfs_btree *hfs_btree_open(struct

super_block *sb, u32 id, btree_keycmp ke
  tree->cnid = id;
  tree->keycmp = keycmp;

fs/hfs/btree.c +2 -2
-	tree->inode = iget_locked(sb, id);
-	if (!tree->inode)
+	/*
+	 * hfs_system_iget() releases the inode itself when it

cannot

fs/hfs/btree.c struct hfs_btree *hfs_btree_open(struct super_block *sb, u32 id, btree_keycmp ke · line 161

Quoted 2 lines
fs/hfs/btree.c +2 -0
+	 * initialize it, so there is nothing to iput here.
+	 */

Please, remove this comment.

Quoted 55 lines
fs/hfs/btree.c +2 -0
+	tree->inode = hfs_system_iget(sb, id);
+	if (IS_ERR(tree->inode))

  goto free_tree;

fs/hfs/btree.c +0 -8
-	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,

fs/hfs/btree.c +0 -1
-				    mdb->drXTFlSize,

be32_to_cpu(mdb->drXTClpSiz));

fs/hfs/btree.c +0 -2
-		if (HFS_I(tree->inode)->alloc_blocks >
-					HFS_I(tree->inode)-
Quoted 1 line

first_blocks) {

fs/hfs/btree.c +0 -9
-			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,

fs/hfs/btree.c +0 -1
-				    mdb->drCTFlSize,

be32_to_cpu(mdb->drCTClpSiz));

fs/hfs/btree.c +0 -3
-
-		if (!HFS_I(tree->inode)->first_blocks) {
-			pr_err("invalid btree extent records (0

size)\n");

fs/hfs/btree.c +0 -11
-			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);

fs/hfs/hfs_fs.h +0 -0
index e250f87..ecdcf81 100644
@@ -163,6 +163,18 @@ extern int hfs_cat_move(u32 cnid, struct inode
 *src_dir,

 extern void hfs_cat_build_key(struct super_block *sb, btree_key *key,

Quoted 8 lines

        u32 parent, const struct qstr *name);
+/*
+ * A CNID below HFS_FIRSTUSER_CNID is reserved for the system files; the
+ * root directory is the one reserved CNID that catalog records address.
+ */

The method name is informative enough. Please, remove the comment.

fs/hfs/hfs_fs.h extern int hfs_cat_move(u32 cnid, struct inode *src_dir, · line 175

Quoted 7 lines
fs/hfs/hfs_fs.h +3 -0
+static inline bool hfs_is_system_reserved_cnid(u32 cnid)
+{
+	if (likely(cnid > HFS_ROOT_CNID && cnid <

HFS_FIRSTUSER_CNID))

fs/hfs/hfs_fs.h +3 -0
+		return true;
+	else
+		return false;
       return cnid > HFS_ROOT_CNID && cnid < HFS_FIRSTUSER_CNID;
Quoted 1 line

+}

We don't need anything more here.

Quoted 27 lines

+
 /*
  * Validate the CNID of a catalog record.
  */

fs/hfs/hfs_fs.h +0 -0
@@ -219,6 +231,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);

fs/hfs/inode.c +0 -0
index 2aef3c3..986a6f9 100644
@@ -421,6 +421,97 @@ 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

Quoted 8 lines

+ *
+ * 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.
+ */

I am not sure that we need this comment at all. The function name is good enough.

fs/hfs/inode.c static int hfs_read_inode(struct inode *inode, void *data) · line 445

Quoted 13 lines
fs/hfs/inode.c +13 -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);
+	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);

Maybe, we need to place it here?

+ inode->i_mode = S_IFREG;
+ inode->i_mapping->a_ops = &hfs_btree_aops;

Quoted 31 lines
fs/hfs/inode.c +4 -0
+
+	switch (inode->i_ino) {
+	case HFS_EXT_CNID:
+		hfs_inode_read_fork(inode, mdb->drXTExtRec, mdb-
Quoted 1 line

drXTFlSize,

fs/hfs/inode.c +1 -0
+				    mdb->drXTFlSize,

be32_to_cpu(mdb->drXTClpSiz));

fs/hfs/inode.c +1 -0
+		if (HFS_I(inode)->alloc_blocks > HFS_I(inode)-
Quoted 1 line

first_blocks) {

fs/hfs/inode.c +6 -0
+			pr_err("invalid btree extent records\n");
+			return -EIO;
+		}
+		break;
+	case HFS_CAT_CNID:
+		hfs_inode_read_fork(inode, mdb->drCTExtRec, mdb-
Quoted 1 line

drCTFlSize,

fs/hfs/inode.c +1 -0
+				    mdb->drCTFlSize,

be32_to_cpu(mdb->drCTClpSiz));

fs/hfs/inode.c +2 -0
+		if (!HFS_I(inode)->first_blocks) {
+			pr_err("invalid btree extent records (0

size)\n");

fs/hfs/inode.c +9 -0
+			return -EIO;
+		}
+		break;
+	default:
+		return -EIO;
+	}
+
+	/*
+	 * may_open() requires a file type, and a crafted catalog

record can

fs/hfs/inode.c

Quoted 2 lines
fs/hfs/inode.c +2 -0
+	 * reach this inode by its CNID.
+	 */

/* may_open() requires a file type */

fs/hfs/inode.c static int hfs_read_inode(struct inode *inode, void *data) · line 474

Quoted 3 lines
fs/hfs/inode.c +3 -0
+	inode->i_mode = S_IFREG;
+
+	/* Set before hfs_system_iget() publishes the inode. */

Please, remove the comment.

fs/hfs/inode.c static int hfs_read_inode(struct inode *inode, void *data) · line 486

Quoted 12 lines
fs/hfs/inode.c +12 -0
+	inode->i_mapping->a_ops = &hfs_btree_aops;
+
+	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.
+ */

Ditto.

Quoted 14 lines
fs/hfs/inode.c +11 -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);
+
+	/*
+	 * The B-tree inodes are created once per mount, so finding

one

fs/hfs/inode.c +1 -0
+	 * already in the cache means the same tree was opened

twice.

fs/hfs/inode.c

Quoted 1 line
fs/hfs/inode.c +1 -0
+	 */

Please, delete the comment.

Quoted 33 lines
fs/hfs/inode.c +15 -0
+	if (!(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()
  *

fs/hfs/inode.c +0 -0
@@ -446,6 +537,18 @@ struct inode *hfs_iget(struct super_block *sb,

struct hfs_cat_key *key, hfs_cat_
  default:
  return NULL;
  }

fs/hfs/inode.c +3 -0
+
+	/*
+	 * hfs_test_inode() matches on i_ino alone, so a crafted

record can

fs/hfs/inode.c +1 -0
+	 * alias any cached inode.  hfs_read_inode() rejects an

invalid CNID,

fs/hfs/inode.c +1 -0
+	 * but it does not run when the inode is already cached, and

a record

fs/hfs/inode.c +1 -0
+	 * naming a reserved CNID would hand out a B-tree inode as a

regular

fs/hfs/inode.c struct inode *hfs_iget(struct super_block *sb, struct hfs_cat_key *key, hfs_cat_ · line 547

Quoted 2 lines
fs/hfs/inode.c +2 -0
+	 * dentry.
+	 */

Please, delete the comment. Code is informative enough.

fs/hfs/inode.c struct inode *hfs_iget(struct super_block *sb, struct hfs_cat_key *key, hfs_cat_ · line 550

Quoted 3 lines
fs/hfs/inode.c +3 -0
+	if (!hfs_is_valid_cnid(cnid, rec->type) ||
+	    hfs_is_system_reserved_cnid(cnid))
+		return NULL;

A NULL return becomes -EACCES in hfs_lookup() (fs/hfs/dir.c:38). For a corrupted catalog, -EIO/-EUCLEAN fits better. What do you think? Am I right here?

Thanks,
Slava.

Quoted 5 lines

+
  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);

author
#11 lore raw

PATCH v5 1/1 hfs: initialize B-tree inodes and validate catalog CNIDs

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.

---
Changes in v5:
- Remove unnecessary comments.
- Simplify hfs_is_system_reserved_cnid().
- Move i_mode and a_ops initialization after set_nlink().
- Use -EIO instead of -EACCES.

 fs/hfs/btree.c  | 39 ++-----------------------------
 fs/hfs/dir.c    |  2 +-
 fs/hfs/hfs_fs.h |  6 +++++
 fs/hfs/inode.c  | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 80 insertions(+), 38 deletions(-)
fs/hfs/btree.c +2 -37
index 41b4e8f..1891ad5 100644
@@ -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);
fs/hfs/dir.c +1 -1
index e1f1fb3..0a31304 100644
@@ -36,7 +36,7 @@ static struct dentry *hfs_lookup(struct inode *dir, struct dentry *dentry,
 	} else {
 		inode = hfs_iget(dir->i_sb, &fd.search_key->cat, &rec);
 		if (!inode)
-			inode = ERR_PTR(-EACCES);
+			inode = ERR_PTR(-EIO);
 	}
 	hfs_find_exit(&fd);
 	return d_splice_alias(inode, dentry);
fs/hfs/hfs_fs.h +6 -0
index e250f87..8d52170 100644
@@ -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);
 
fs/hfs/inode.c +71 -0

83 lines not on this page · raw message

#12 lore raw

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

Quoted 100 lines · yangguikun

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.

Link:
https://lore.kernel.org/linux-fsdevel/[email protected]/

Fixes 1da177e4c3f4 ("Linux-2.6.12-rc2")

---
Changes in v5:
 - Remove unnecessary comments.
 - Simplify hfs_is_system_reserved_cnid().
 - Move i_mode and a_ops initialization after set_nlink().
 - Use -EIO instead of -EACCES.

 fs/hfs/btree.c  | 39 ++----------------------------- fs/hfs/dir.c    |  2 +-
 fs/hfs/hfs_fs.h |  6 +++++
 fs/hfs/inode.c  | 71
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++

 4 files changed, 80 insertions(+), 38 deletions(-)
fs/hfs/btree.c +0 -0
index 41b4e8f..1891ad5 100644
@@ -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;

fs/hfs/btree.c +2 -2
-	tree->inode = iget_locked(sb, id);
-	if (!tree->inode)
+	tree->inode = hfs_system_iget(sb, id);
+	if (IS_ERR(tree->inode))

  goto free_tree;

fs/hfs/btree.c +0 -8
-	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,

fs/hfs/btree.c +0 -1
-				    mdb->drXTFlSize,

be32_to_cpu(mdb->drXTClpSiz));

fs/hfs/btree.c +0 -2
-		if (HFS_I(tree->inode)->alloc_blocks >
-					HFS_I(tree->inode)-
Quoted 1 line

first_blocks) {

fs/hfs/btree.c +0 -9
-			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,

fs/hfs/btree.c +0 -1
-				    mdb->drCTFlSize,

be32_to_cpu(mdb->drCTClpSiz));

fs/hfs/btree.c +0 -3
-
-		if (!HFS_I(tree->inode)->first_blocks) {
-			pr_err("invalid btree extent records (0

size)\n");

fs/hfs/btree.c +0 -11
-			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);

fs/hfs/dir.c +0 -0
index e1f1fb3..0a31304 100644

fs/hfs/dir.c static struct dentry *hfs_lookup(struct inode *dir, · line 41

Quoted 7 lines
fs/hfs/dir.c +1 -1
@@ -36,7 +36,7 @@ static struct dentry *hfs_lookup(struct inode *dir,
 struct dentry *dentry,
  	} else {
  		inode = hfs_iget(dir->i_sb, &fd.search_key->cat,
 &rec);
  		if (!inode)
-			inode = ERR_PTR(-EACCES);
+			inode = ERR_PTR(-EIO);

With the patch applied, hfs_iget() can return NULL in these cases:

1. Unknown record type. The default: branch of its own switch (rec-

Quoted 1 line

type) returns NULL. That's corruption.

2. The new CNID check (!hfs_is_valid_cnid() || hfs_is_system_reserved_cnid()). Also corruption.
3. iget5_locked() returns NULL. In fs/inode.c that happens when:
- alloc_inode(sb) fails, meaning the inode slab allocation ran out
of memory. This is not corruption; it's -ENOMEM.
- The set() callback fails. Here that's hfs_read_inode() returning -
EIO for a bad CNID, which is corruption again.

If we want it done properly, ten, maybe, we need to make hfs_iget() return ERR_PTR() instead of NULL:
- ERR_PTR(-ENOMEM) when the allocation fails
- ERR_PTR(-EIO) for the corruption cases

What do you think?

And, maybe, we need to consider it as dedicated patch.

Quoted 41 lines

  }
  hfs_find_exit(&fd);
  return d_splice_alias(inode, dentry);

fs/hfs/hfs_fs.h +0 -0
index e250f87..8d52170 100644
@@ -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);

fs/hfs/hfs_fs.h +5 -0
+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.
  */

fs/hfs/hfs_fs.h +0 -0
@@ -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);

fs/hfs/inode.c +0 -0
index 2aef3c3..5a03fca 100644
@@ -421,6 +421,72 @@ static int hfs_read_inode(struct inode *inode,
 void *data)
  	return 0;
  }

fs/hfs/inode.c static int hfs_read_inode(struct inode *inode, void *data) · line 432

Quoted 9 lines
fs/hfs/inode.c +9 -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);
+	HFS_I(inode)->tz_secondswest = sys_tz.tz_minuteswest * 60;

I am still not sure that we need it for b-trees' inodes. Do we need it?

fs/hfs/inode.c static int hfs_read_inode(struct inode *inode, void *data) · line 436

Quoted 4 lines
fs/hfs/inode.c +4 -0
+
+	inode->i_uid = hsb->s_uid;
+	inode->i_gid = hsb->s_gid;
+	set_nlink(inode, 1);

I assume you copied it from hfs_read_inode(). But this method is for b- trees' inodes and I suspect that we have trouble here.

Before this patch, B-tree inodes had i_nlink == 0, so inode_generic_drop() returned true and the final iput() evicted them without writeback. With nlink = 1, the final iput() in hfs_btree_close() runs during put_super, when SB_ACTIVE is already clear. iput_final() then takes the !drop branch and calls write_inode_now(inode, 1) (fs/inode.c, iput_final). If a tree inode is still dirty at that point:

- Extents tree (closed first): hfs_write_inode() → hfs_btree_write(ext_tree) re-reads node 0 into a hash that hfs_btree_close() has just emptied. Then kfree(tree) leaks that node.
- Catalog tree (closed second): hfs_write_inode() → hfs_ext_write_extent() can touch HFS_SB(sb)->ext_tree, which has already been freed. That's a use-after-free if the catalog's extent record is dirty.

Are you sure that we safe here with set_nlink(inode, 1)?

Quoted 38 lines
fs/hfs/inode.c +8 -0
+
+	/* may_open() requires a file type */
+	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-
Quoted 1 line

drXTFlSize,

fs/hfs/inode.c +1 -0
+				    mdb->drXTFlSize,

be32_to_cpu(mdb->drXTClpSiz));

fs/hfs/inode.c +1 -0
+		if (HFS_I(inode)->alloc_blocks > HFS_I(inode)-
Quoted 1 line

first_blocks) {

fs/hfs/inode.c +6 -0
+			pr_err("invalid btree extent records\n");
+			return -EIO;
+		}
+		break;
+	case HFS_CAT_CNID:
+		hfs_inode_read_fork(inode, mdb->drCTExtRec, mdb-
Quoted 1 line

drCTFlSize,

fs/hfs/inode.c +1 -0
+				    mdb->drCTFlSize,

be32_to_cpu(mdb->drCTClpSiz));

fs/hfs/inode.c +2 -0
+		if (!HFS_I(inode)->first_blocks) {
+			pr_err("invalid btree extent records (0

size)\n");

fs/hfs/inode.c +13 -0
+			return -EIO;
+		}
+		break;
+	default:
+		return -EIO;
+	}
+
+	return 0;
+}
+
+struct inode *hfs_system_iget(struct super_block *sb, u32 cnid)
+{
+	struct inode *inode;

fs/hfs/inode.c static int hfs_read_inode(struct inode *inode, void *data) · line 478

Quoted 10 lines
fs/hfs/inode.c +10 -0
+	int err;
+
+	inode = iget_locked(sb, cnid);
+	if (!inode)
+		return ERR_PTR(-ENOMEM);
+
+	if (!(inode_state_read_once(inode) & I_NEW)) {
+		iput(inode);
+		return ERR_PTR(-EEXIST);
+	}

iget_locked() returns a fresh inode with I_NEW set if no inode with that number exists yet in this superblock's inode cache. If one already exists, it returns that cached inode without I_NEW. So the -EEXIST branch fires only if an inode numbered 3 or 4 is already cached for this superblock before hfs_btree_open() creates it. That can't happen I suppose.

What's about this then?

        /* B-trees are opened before any catalog lookup can run */
        if (WARN_ON_ONCE(!(inode_state_read_once(inode) & I_NEW))) {
                iput(inode);
                return ERR_PTR(-EEXIST);
        }

Thanks,
Slava.

Quoted 28 lines
fs/hfs/inode.c +11 -0
+
+	err = hfs_system_read_inode(inode);
+	if (err) {
+		iget_failed(inode);
+		return ERR_PTR(err);
+	}
+
+	unlock_new_inode(inode);
+	return inode;
+}
+

 /*
  * __hfs_iget()
  *

fs/hfs/inode.c +0 -0
@@ -446,6 +512,11 @@ struct inode *hfs_iget(struct super_block *sb,

struct hfs_cat_key *key, hfs_cat_
  default:
  return NULL;
  }

fs/hfs/inode.c +5 -0
+
+	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);

author
#13 lore raw

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

Hi Slava,

Thanks for the detailed review.

On set_nlink(): I removed the explicit call, but please note that it was a no-op: inode_init_always_gfp() sets __i_nlink = 1 (fs/inode.c:238) for every new inode, and the original iget_locked() path in hfs_btree_open() never cleared it. So the B-tree inodes have had i_nlink == 1 since before this series, inode_generic_drop() returns false for them, and the final iput() in hfs_btree_close() already took the !drop branch with writeback. I could not construct the still-dirty-at-put_super scenario outside of an error in the umount sync; if you have a concrete case in mind, I am happy to test it. The redundant set_nlink() is still worth removing for clarity.

tz_secondswest is only consumed by hfs_revalidate_dentry(), which runs from dentries; the B-tree inodes never get one, so I removed that initialization as well.

I will use the WARN_ON_ONCE() form for the already-cached inode case.

On hfs_iget(): agreed on both counts. Returning ERR_PTR() lets the corruption cases (-EIO) and allocation failure (-ENOMEM) stay distinguishable, and both callers currently guess: hfs_lookup() translates NULL to -EACCES and hfs_fill_super() reports -EINVAL. minix_iget() already works this way. In v5 I only did what the v4 review suggested (-EIO), but the complete fix is the ERR_PTR() conversion, so I reverted the dir.c change and will send it as a separate patch with the caller updates.

Patch v6 follows.

Thanks,
yangguikun

From 014ad11799ffc5fb857c23337e26121ee571f76a Mon Sep 17 00:00:00 2001
From: yangguikun <[email protected]>
Date: Sat, 17 Oct 2026 01:49:17 +0800
Subject: [PATCH v6] hfs: initialize B-tree inodes and validate catalog CNIDs

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.

---
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.

fs/hfs/btree.c +2 -37
index 41b4e8f..1891ad5 100644
@@ -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);
fs/hfs/hfs_fs.h +6 -0
index e250f87..8d52170 100644
@@ -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);
 
fs/hfs/inode.c +68 -0

80 lines not on this page · raw message

Signature · 1 line
2.53.0.windows.1
#14 lore raw

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

Hi Yangguikun,

Could you please resend the v6 patch with proper subject? Otherwise, it looks like a mess. I see it as Re: [PATCH v5] hfs: initialize B-tree inodes and validate catalog CNIDs (but not as v6 patch).

The patch looks good, by the way. I think I would like to run the xfstests for the v6 patch after receiving it as proper patch. ;) And I will need to have the second patch too for the check.

Thanks,
Slava.

Quoted 257 lines · yangguikun

Hi Slava,

Thanks for the detailed review.

On set_nlink(): I removed the explicit call, but please note that it was a no-op: inode_init_always_gfp() sets __i_nlink = 1 (fs/inode.c:238)
for every new inode, and the original iget_locked() path in hfs_btree_open() never cleared it. So the B-tree inodes have had i_nlink == 1 since before this series, inode_generic_drop() returns false for them, and the final iput() in hfs_btree_close() already took
the !drop branch with writeback. I could not construct the still-dirty-at-put_super scenario outside of an error in the umount sync; if you have a concrete case in mind, I am happy to test it. The redundant set_nlink() is still worth removing for clarity.

tz_secondswest is only consumed by hfs_revalidate_dentry(), which runs
from dentries; the B-tree inodes never get one, so I removed that initialization as well.

I will use the WARN_ON_ONCE() form for the already-cached inode case.

On hfs_iget(): agreed on both counts. Returning ERR_PTR() lets the corruption cases (-EIO) and allocation failure (-ENOMEM) stay distinguishable, and both callers currently guess: hfs_lookup() translates NULL to -EACCES and hfs_fill_super() reports -EINVAL. minix_iget() already works this way. In v5 I only did what the v4 review suggested (-EIO), but the complete fix is the ERR_PTR() conversion, so I reverted the dir.c change and will send it as a separate patch with the caller updates.

Patch v6 follows.

Thanks,
yangguikun

From 014ad11799ffc5fb857c23337e26121ee571f76a Mon Sep 17 00:00:00 2001
From: yangguikun <[email protected]>
Date: Sat, 17 Oct 2026 01:49:17 +0800
Subject: [PATCH v6] hfs: initialize B-tree inodes and validate catalog CNIDs

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.

Link:
https://lore.kernel.org/linux-fsdevel/[email protected]/

Fixes 1da177e4c3f4 ("Linux-2.6.12-rc2")

---
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.

fs/hfs/btree.c +0 -0
index 41b4e8f..1891ad5 100644
@@ -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;

fs/hfs/btree.c +2 -2
-	tree->inode = iget_locked(sb, id);
-	if (!tree->inode)
+	tree->inode = hfs_system_iget(sb, id);
+	if (IS_ERR(tree->inode))

  goto free_tree;

fs/hfs/btree.c +0 -8
-	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,

fs/hfs/btree.c +0 -1
-				    mdb->drXTFlSize,

be32_to_cpu(mdb->drXTClpSiz));

fs/hfs/btree.c +0 -2
-		if (HFS_I(tree->inode)->alloc_blocks >
-					HFS_I(tree->inode)-
Quoted 1 line

first_blocks) {

fs/hfs/btree.c +0 -9
-			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,

fs/hfs/btree.c +0 -1
-				    mdb->drCTFlSize,

be32_to_cpu(mdb->drCTClpSiz));

fs/hfs/btree.c +0 -3
-
-		if (!HFS_I(tree->inode)->first_blocks) {
-			pr_err("invalid btree extent records (0

size)\n");

fs/hfs/btree.c +0 -11
-			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);

fs/hfs/hfs_fs.h +0 -0
index e250f87..8d52170 100644
@@ -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);

fs/hfs/hfs_fs.h +5 -0
+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.
  */

fs/hfs/hfs_fs.h +0 -0
@@ -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);

fs/hfs/inode.c +0 -0
index 2aef3c3..a66de7b 100644
@@ -421,6 +421,69 @@ static int hfs_read_inode(struct inode *inode,
 void *data)
  	return 0;
  }

fs/hfs/inode.c +18 -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-
Quoted 1 line

drXTFlSize,

fs/hfs/inode.c +1 -0
+				    mdb->drXTFlSize,

be32_to_cpu(mdb->drXTClpSiz));

fs/hfs/inode.c +1 -0
+		if (HFS_I(inode)->alloc_blocks > HFS_I(inode)-
Quoted 1 line

first_blocks) {

fs/hfs/inode.c +6 -0
+			pr_err("invalid btree extent records\n");
+			return -EIO;
+		}
+		break;
+	case HFS_CAT_CNID:
+		hfs_inode_read_fork(inode, mdb->drCTExtRec, mdb-
Quoted 1 line

drCTFlSize,

fs/hfs/inode.c +1 -0
+				    mdb->drCTFlSize,

be32_to_cpu(mdb->drCTClpSiz));

fs/hfs/inode.c +2 -0
+		if (!HFS_I(inode)->first_blocks) {
+			pr_err("invalid btree extent records (0

size)\n");

fs/hfs/inode.c +34 -0
+			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()
  *

fs/hfs/inode.c +0 -0
@@ -446,6 +509,11 @@ struct inode *hfs_iget(struct super_block *sb,

struct hfs_cat_key *key, hfs_cat_
  default:
  return NULL;
  }

fs/hfs/inode.c +5 -0
+
+	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);

author
#15 lore raw

PATCH v2 2/2 hfs: refuse catalog records that address the reserved B-tree CNIDs

In reply to a message not in the archive

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.

---
  fs/hfs/inode.c |  11 +++++++++++
 1 file changed, 11 insertions(+), 0 deletions(-)
fs/hfs/inode.c +11 -0
index 4316d75..5190856 100644
@@ -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);
Signature · 1 line
2.39.5
#16 lore raw

Re: [PATCH v2 2/2] hfs: refuse catalog records that address the reserved B-tree CNIDs

Quoted 13 lines · 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.

Quoted 8 lines

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.

Quoted 2 lines

Please, provide the real name.

Quoted 21 lines

---
  fs/hfs/inode.c |  11 +++++++++++

 1 file changed, 11 insertions(+), 0 deletions(-)
fs/hfs/inode.c +0 -0
index 4316d75..5190856 100644
@@ -520,6 +520,17 @@ struct inode *hfs_iget(struct super_block *sb,

struct hfs_cat_key *key, hfs_cat_
  default:
  return NULL;
  }

fs/hfs/inode.c +3 -0
+
+	/*
+	 * Never let a catalog record address one of the internal B-

tree

fs/hfs/inode.c +1 -0
+	 * objects.  hfs_test_inode() matches on i_ino alone, so

such a

fs/hfs/inode.c +1 -0
+	 * record would make hfs_lookup() hand out the cached B-tree

inode as

fs/hfs/inode.c struct inode *hfs_iget(struct super_block *sb, struct hfs_cat_key *key, hfs_cat_ · line 530

Quoted 3 lines
fs/hfs/inode.c +3 -0
+	 * 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.

fs/hfs/inode.c struct inode *hfs_iget(struct super_block *sb, struct hfs_cat_key *key, hfs_cat_ · line 531

Quoted 1 line
fs/hfs/inode.c +1 -0
+	if (cnid == HFS_EXT_CNID || cnid == HFS_CAT_CNID)

I think we can reject all cnid that lower than HFS_FIRSTUSER_CNID.

Thanks,
Slava.

Quoted 6 lines
fs/hfs/inode.c +2 -0
+		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);