Re: [PATCH v6] hfsplus: validate b-tree fork extents at mount and inode read

From: Viacheslav Dubeyko

Date: Mon Sep 21 2026 - 17:14:29 EST


On Fri, 2026-09-18 at 15:51 +0700, Nguyen Ngoc Thang wrote:
> 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.

This patch looks like the small portion of the whole fix. As far as I
can see, this patch has lost necessary portion of fixes that we had
before.

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

I think we need to place this declaration into hfsplus_fs.h.

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

This logic doesn't look like the fork check.

struct hfsplus_fork_raw {
__be64 total_size;
__be32 clump_size;
__be32 total_blocks;
hfsplus_extent_rec extents;
} __packed;

We need to be sure that total_size, total_blocks are consistent with
the extents state. Also, we can check the clump_size that it is
reasonable one.

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

This patch hasn't any logic of checking the error code of
hfsplus_inode_read_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);

We need to check the whole fork but not only extents.

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

NO, we cannot set SB_RDONLY in this method. It can be done in
hfsplus_fill_super() or hfsplus_reconfigure().

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

Looks like a mess and improper formatting of the code. What's happen
here?

Thanks,
Slava.

> +    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,