[PATCH v6] hfsplus: validate b-tree fork extents at mount and inode read
From: Nguyen Ngoc Thang
Date: Fri Sep 18 2026 - 04:41:09 EST
Validate fork extents during inode reading and mount time to catch
on-disk corruptions early, returning appropriate errors and marking
the tree as corrupted.
v6:
- Move fork validation logic into inode read fork function.
- Refactor extent validation helpers, use volume_blocks, count == 0,
and introduce HFSPLUS_EXTENT_LAST_IDX named constant.
- Return error from hfsplus_inode_read_fork() to allow hfsplus_iget()
to catch on-disk corruption and propagate error correctly.
Signed-off-by: Nguyen Ngoc Thang <ngocthang2710.1999@xxxxxxxxx>
---
fs/hfsplus/extents.c | 41 ++++++++++++++++++++++++++++
fs/hfsplus/inode.c | 65 ++++++++++++++++++++++++++------------------
2 files changed, 79 insertions(+), 27 deletions(-)
diff --git a/fs/hfsplus/extents.c b/fs/hfsplus/extents.c
index eb7c11524d18..737cf113f215 100644
--- a/fs/hfsplus/extents.c
+++ b/fs/hfsplus/extents.c
@@ -16,6 +16,47 @@
#include "hfsplus_fs.h"
#include "hfsplus_raw.h"
+/* Index of the last extent in the fork */
+#define HFSPLUS_EXTENT_LAST_IDX 7
+
+static inline bool is_extents_btree(struct inode *inode)
+{
+ return inode->i_ino == HFSPLUS_EXT_CNID;
+}
+
+static bool hfsplus_extent_valid(struct hfsplus_extent *ext, u32 volume_blocks)
+{
+ u32 start = be32_to_cpu(ext->start_block);
+ u32 count = be32_to_cpu(ext->block_count);
+
+ if (count == 0)
+ return start == 0;
+
+ return start + count <= volume_blocks;
+}
+
+/*
+ * Returns 0 if fork extents are consistent, -EUCLEAN if extents
+ * past the first are corrupt, or -EIO if the first extent is corrupt.
+ */
+int hfsplus_check_fork(struct super_block *sb, struct hfsplus_extent *ext, u32 volume_blocks)
+{
+ bool non_zero_seen = false;
+ int i;
+
+ for (i = 0; i <= HFSPLUS_EXTENT_LAST_IDX; i++, ext++) {
+ u32 count = be32_to_cpu(ext->block_count);
+
+ if (!hfsplus_extent_valid(ext, volume_blocks) || (non_zero_seen && count == 0))
+ return i ? -EUCLEAN : -EIO;
+
+ if (count > 0)
+ non_zero_seen = true;
+ }
+
+ return 0;
+}
+
/* Compare two extents keys, returns 0 on same, pos/neg for difference */
int hfsplus_ext_cmp_key(const hfsplus_btree_key *k1,
const hfsplus_btree_key *k2)
diff --git a/fs/hfsplus/inode.c b/fs/hfsplus/inode.c
index 2ce6de574fa6..57fedd0e80fb 100644
--- a/fs/hfsplus/inode.c
+++ b/fs/hfsplus/inode.c
@@ -559,34 +559,45 @@ void hfsplus_delete_inode(struct inode *inode)
hfsplus_mark_mdb_dirty(sb);
}
-void hfsplus_inode_read_fork(struct inode *inode, struct hfsplus_fork_raw *fork)
+int hfsplus_inode_read_fork(struct inode *inode, struct hfsplus_fork_raw *fork)
{
- struct super_block *sb = inode->i_sb;
- struct hfsplus_sb_info *sbi = HFSPLUS_SB(sb);
- struct hfsplus_inode_info *hip = HFSPLUS_I(inode);
- u32 count;
- int i;
-
- memcpy(&hip->first_extents, &fork->extents, sizeof(hfsplus_extent_rec));
- for (count = 0, i = 0; i < 8; i++)
- count += be32_to_cpu(fork->extents[i].block_count);
- hip->first_blocks = count;
- memset(hip->cached_extents, 0, sizeof(hfsplus_extent_rec));
- hip->cached_start = 0;
- hip->cached_blocks = 0;
-
- hip->alloc_blocks = be32_to_cpu(fork->total_blocks);
- hip->phys_size = inode->i_size = be64_to_cpu(fork->total_size);
- hip->fs_blocks =
- (inode->i_size + sb->s_blocksize - 1) >> sb->s_blocksize_bits;
- inode_set_bytes(inode, hip->fs_blocks << sb->s_blocksize_bits);
- hip->clump_blocks =
- be32_to_cpu(fork->clump_size) >> sbi->alloc_blksz_shift;
- if (!hip->clump_blocks) {
- hip->clump_blocks = HFSPLUS_IS_RSRC(inode) ?
- sbi->rsrc_clump_blocks :
- sbi->data_clump_blocks;
- }
+ struct super_block *sb = inode->i_sb;
+ struct hfsplus_sb_info *sbi = HFSPLUS_SB(sb);
+ struct hfsplus_inode_info *hip = HFSPLUS_I(inode);
+ u32 count;
+ int i, ret;
+
+ /* Validate fork extents to catch on-disk corruption early */
+ ret = hfsplus_check_fork(sb, fork->extents, sbi->total_blocks);
+ if (ret) {
+ pr_err("hfsplus: fork check failed for inode %lu (err=%d)\n", inode->i_ino, ret);
+ set_bit(HFSPLUS_I_CORRUPT_TREE, &hip->flags);
+ sb->s_flags |= SB_RDONLY;
+ return ret; /* Return error directly to the caller */
+ }
+
+ memcpy(&hip->first_extents, &fork->extents, sizeof(hfsplus_extent_rec));
+ for (count = 0, i = 0; i <= HFSPLUS_EXTENT_LAST_IDX; i++)
+ count += be32_to_cpu(fork->extents[i].block_count);
+ hip->first_blocks = count;
+ memset(hip->cached_extents, 0, sizeof(hfsplus_extent_rec));
+ hip->cached_start = 0;
+ hip->cached_blocks = 0;
+
+ hip->alloc_blocks = be32_to_cpu(fork->total_blocks);
+ hip->phys_size = inode->i_size = be64_to_cpu(fork->total_size);
+ hip->fs_blocks =
+ (inode->i_size + sb->s_blocksize - 1) >> sb->s_blocksize_bits;
+ inode_set_bytes(inode, hip->fs_blocks << sb->s_blocksize_bits);
+ hip->clump_blocks =
+ be32_to_cpu(fork->clump_size) >> sbi->alloc_blksz_shift;
+ if (!hip->clump_blocks) {
+ hip->clump_blocks = HFSPLUS_IS_RSRC(inode) ?
+ sbi->rsrc_clump_blocks :
+ sbi->data_clump_blocks;
+ }
+
+ return 0;
}
void hfsplus_inode_write_fork(struct inode *inode,
--
2.43.0