Re: [PATCH] hfs: validate catalog CNIDs before instantiating inodes
From: Viacheslav Dubeyko
Date: Thu Jul 02 2026 - 19:59:55 EST
On Thu, 2026-07-02 at 18:38 +0000, David Maximiliano Hermitte wrote:
> hfs_cat_find_brec() first resolves a catalog thread record by CNID
> and
> then looks up the corresponding catalog record by parent/name. On a
> corrupted filesystem image, the second lookup may find a record whose
> CNID does not match the CNID that was requested.
>
> Validate the catalog record found by the second lookup before
> returning
> it to callers. Inspect the already-found record with
> hfs_bnode_read(),
> not hfs_brec_read(), and reject records whose CNID is invalid for
> their
> record type or does not match the requested CNID.
>
> Also validate CNIDs in hfs_read_inode() before the inode is
> populated,
> and propagate hfs_read_inode() errors from the resource-fork lookup
> path.
> For the root inode path, require the root catalog record to be a
> directory
> with DirID == HFS_ROOT_CNID, and drop the root inode reference if it
> was
> instantiated as a bad inode.
>
> This keeps hfs_write_inode() unchanged and prevents corrupted catalog
> records from reaching the existing reserved-CNID BUG() path during
> writeback.
>
> Reported-by: syzbot+97e301b4b82ae803d21b@xxxxxxxxxxxxxxxxxxxxxxxxx
> Closes: https://syzkaller.appspot.com/bug?extid=97e301b4b82ae803d21b
> Cc: George Anthony Vernon <contact@xxxxxxxxxxx>
> Cc: Tetsuo Handa <penguin-kernel@xxxxxxxxxxxxxxxxxxx>
> Signed-off-by: David Maximiliano Hermitte <davemadmaxxx@xxxxxxxxx>
> ---
> fs/hfs/catalog.c | 48
> +++++++++++++++++++++++++++++++++++++++++++++++-
> fs/hfs/hfs_fs.h | 19 +++++++++++++++++++
> fs/hfs/inode.c | 9 ++++++++-
> fs/hfs/super.c | 8 +++++++-
> 4 files changed, 81 insertions(+), 3 deletions(-)
>
> diff --git a/fs/hfs/catalog.c b/fs/hfs/catalog.c
> index 1bfa36d71e24..fa2a0a5975e3 100644
> --- a/fs/hfs/catalog.c
> +++ b/fs/hfs/catalog.c
> @@ -182,6 +182,47 @@ int hfs_cat_keycmp(const btree_key *key1, const
> btree_key *key2)
> key2->cat.CName.name, key2-
> >cat.CName.len);
> }
>
> +static int hfs_cat_validate_found_cnid(struct hfs_find_data *fd, u32
> cnid)
> +{
> + hfs_cat_rec rec;
> + u32 found_cnid;
> + unsigned int rec_len;
I prefer to see the size_t here.
> + size_t cnid_off;
> +
> + if (fd->entrylength <= 0)
> + return -EIO;
> +
> + if ((unsigned int)fd->entrylength > sizeof(rec))
The hfs_cat_rec union is big enough to keep any type of record in
Catalog File. If fd->entrylength is bigger one, then it sounds like a
incorrect behavior or corruption. We cannot silently process such
situation.
> + rec_len = sizeof(rec);
> + else
> + rec_len = fd->entrylength;
> +
> + memset(&rec, 0, sizeof(rec));
> + hfs_bnode_read(fd->bnode, &rec, fd->entryoffset, rec_len);
> +
> + switch (rec.type) {
> + case HFS_CDR_FIL:
> + cnid_off = offsetof(struct hfs_cat_file, FlNum);
> + if ((size_t)rec_len < cnid_off +
> sizeof(rec.file.FlNum))
> + return -EIO;
If you would like to check that size of the record, then you don't need
to do this weird logic.
typedef union hfs_cat_rec {
s8 type; /* The type of entry */
struct hfs_cat_file file;
struct hfs_cat_dir dir;
struct hfs_cat_thread thread;
} hfs_cat_rec;
You can simply compare with particular structure size.
> + found_cnid = be32_to_cpu(rec.file.FlNum);
> + break;
> + case HFS_CDR_DIR:
> + cnid_off = offsetof(struct hfs_cat_dir, DirID);
> + if ((size_t)rec_len < cnid_off +
> sizeof(rec.dir.DirID))
> + return -EIO;
Ditto.
> + found_cnid = be32_to_cpu(rec.dir.DirID);
> + break;
> + default:
> + return -EIO;
Are we sure that thread record cannot be checked by this logic?
> + }
> +
> + if (!hfs_is_valid_cnid(found_cnid, rec.type) || found_cnid
> != cnid)
We don't need to call hfs_is_valid_cnid() because found_cnid != cnid is
completely enough.
> + return -EIO;
> +
> + return 0;
> +}
> +
> /* Try to get a catalog entry for given catalog id */
> // move to read_super???
> int hfs_cat_find_brec(struct super_block *sb, u32 cnid,
> @@ -208,7 +249,12 @@ int hfs_cat_find_brec(struct super_block *sb,
> u32 cnid,
> return -EIO;
> }
> memcpy(fd->search_key->cat.CName.name,
> rec.thread.CName.name, len);
> - return hfs_brec_find(fd);
> +
> + res = hfs_brec_find(fd);
> + if (res)
> + return res;
> +
> + return hfs_cat_validate_found_cnid(fd, cnid);
> }
>
> static inline
> diff --git a/fs/hfs/hfs_fs.h b/fs/hfs/hfs_fs.h
> index f3624514fcb0..670638f17438 100644
> --- a/fs/hfs/hfs_fs.h
> +++ b/fs/hfs/hfs_fs.h
> @@ -155,6 +155,25 @@ 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);
>
> +/*
> + * Validate the CNID of a catalog record.
> + */
> +static inline bool hfs_is_valid_cnid(u32 cnid, u8 type)
> +{
> + if (likely(cnid >= HFS_FIRSTUSER_CNID))
> + return true;
> +
> + switch (cnid) {
> + case HFS_ROOT_CNID:
> + return type == HFS_CDR_DIR;
> + case HFS_EXT_CNID:
> + case HFS_CAT_CNID:
> + return type == HFS_CDR_FIL;
> + default:
> + return false;
> + }
> +}
> +
> /* dir.c */
> extern const struct file_operations hfs_dir_operations;
> extern const struct inode_operations hfs_dir_inode_operations;
> diff --git a/fs/hfs/inode.c b/fs/hfs/inode.c
> index ac4a9055c5c0..c685f1bb7009 100644
> --- a/fs/hfs/inode.c
> +++ b/fs/hfs/inode.c
> @@ -367,6 +367,9 @@ static int hfs_read_inode(struct inode *inode,
> void *data)
> rec = idata->rec;
> switch (rec->type) {
> case HFS_CDR_FIL:
> + if (!hfs_is_valid_cnid(be32_to_cpu(rec->file.FlNum),
> rec->type))
> + return -EIO;
> +
> if (!HFS_IS_RSRC(inode)) {
> hfs_inode_read_fork(inode, rec->file.ExtRec,
> rec->file.LgLen,
> rec->file.PyLen,
> be16_to_cpu(rec->file.ClpSize));
> @@ -390,6 +393,9 @@ static int hfs_read_inode(struct inode *inode,
> void *data)
> inode->i_mapping->a_ops = &hfs_aops;
> break;
> case HFS_CDR_DIR:
> + if (!hfs_is_valid_cnid(be32_to_cpu(rec->dir.DirID),
> rec->type))
> + return -EIO;
> +
> inode->i_ino = be32_to_cpu(rec->dir.DirID);
> inode->i_size = be16_to_cpu(rec->dir.Val) + 2;
> HFS_I(inode)->fs_blocks = 0;
> @@ -571,7 +577,8 @@ static struct dentry *hfs_file_lookup(struct
> inode *dir, struct dentry *dentry,
> res = hfs_brec_read(&fd, &rec, sizeof(rec));
> if (!res) {
> struct hfs_iget_data idata = { NULL, &rec };
> - hfs_read_inode(inode, &idata);
> +
Do we really need empty line here?
> + res = hfs_read_inode(inode, &idata);
> }
> hfs_find_exit(&fd);
I believe we need something like this here:
if (res || is_bad_inode(inode)) {
iput(inode);
return ERR_PTR(-EIO);
}
> if (res) {
> diff --git a/fs/hfs/super.c b/fs/hfs/super.c
> index a466c401f6bb..5275936304c7 100644
> --- a/fs/hfs/super.c
> +++ b/fs/hfs/super.c
> @@ -361,7 +361,8 @@ static int hfs_fill_super(struct super_block *sb,
> struct fs_context *fc)
> goto bail_hfs_find;
> }
> hfs_bnode_read(fd.bnode, &rec, fd.entryoffset,
> fd.entrylength);
> - if (rec.type != HFS_CDR_DIR)
> + if (rec.type != HFS_CDR_DIR ||
> + be32_to_cpu(rec.dir.DirID) != HFS_ROOT_CNID)
This check is completely not necessary here because
hfs_cat_validate_found_cnid() will execute the CNID check.
Thanks,
Slava.
> res = -EIO;
> }
> if (res)
> @@ -372,6 +373,11 @@ static int hfs_fill_super(struct super_block
> *sb, struct fs_context *fc)
> if (!root_inode)
> goto bail_no_root;
>
> + if (is_bad_inode(root_inode)) {
> + iput(root_inode);
> + goto bail_no_root;
> + }
> +
> set_default_d_op(sb, &hfs_dentry_operations);
> res = -ENOMEM;
> sb->s_root = d_make_root(root_inode);