Re: [PATCH v2] ocfs2: validate inline dir size during inode reads

From: Heming Zhao

Date: Mon Apr 13 2026 - 03:19:53 EST


On Fri, Apr 10, 2026 at 07:32:29PM +0800, ZhengYuan Huang wrote:
> [BUG]
> A crafted inline-data directory can store i_size larger than id_count.
> Once such a dinode is instantiated, readdir walks past data->id_data
> and KASAN reports:
>
> BUG: KASAN: use-after-free in ocfs2_check_dir_entry.isra.0+0x31f/0x370 fs/ocfs2/dir.c:305
> Read of size 2 at addr ffff8880088f0008 by task syz.0.1936/4656
> Call Trace:
> ...
> ocfs2_check_dir_entry.isra.0+0x31f/0x370 fs/ocfs2/dir.c:305
> ocfs2_dir_foreach_blk_id+0x203/0xa70 fs/ocfs2/dir.c:1805
> ocfs2_dir_foreach_blk fs/ocfs2/dir.c:1933 [inline]
> ocfs2_readdir+0x4ba/0x520 fs/ocfs2/dir.c:1977
> wrap_directory_iterator+0x9c/0xe0 fs/readdir.c:65
> shared_ocfs2_readdir+0x29/0x40 fs/ocfs2/file.c:2822
> iterate_dir+0x276/0x9e0 fs/readdir.c:108
> __do_sys_getdents64 fs/readdir.c:410 [inline]
> __se_sys_getdents64 fs/readdir.c:396 [inline]
> __x64_sys_getdents64+0x143/0x2a0 fs/readdir.c:396
> ...
>
> [CAUSE]
> The inline-dir invariant i_size <= id_count is never validated when a
> dinode is read. ocfs2_validate_inode_block() accepts the corrupted
> metadata, then ocfs2_populate_inode() or ocfs2_refresh_inode() copies
> that unchecked on-disk i_size into inode->i_size.
>
> JBD2-managed buffers can also bypass ocfs2_validate_inode_block(), so
> the same unchecked size can still reach ocfs2_populate_inode() and
> ocfs2_refresh_inode() through those read paths.
>
> [FIX]
> Introduce a shared helper that validates inline directory i_size
> against id_count. Call it from ocfs2_validate_inode_block() so corrupt
> inline-dir dinodes are rejected in the cold metadata-read path, and add
> matching guards in ocfs2_read_locked_inode() and ocfs2_inode_lock_update()
> for the JBD2-managed buffer paths that skip the validator.
>
> This blocks the corrupted metadata before it reaches VFS inode state
> and keeps the hot readdir path unchanged.
>
> Fixes: 23193e513d1c ("ocfs2: Read support for directories with inline data")
> Signed-off-by: ZhengYuan Huang <gality369@xxxxxxxxx>
> ---
> v2:
> - Move the validation from ocfs2_dir_foreach_blk_id() to inode read paths
> - Add JBD2-managed buffer guards in ocfs2_read_locked_inode() and ocfs2_inode_lock_update()
> - Reword the changelog to describe unchecked on-disk i_size rather than corrupted in-memory state

ocfs2_validate_inode_block() already has the logic to check inline-data size.
You can find it by searching for:
"if (le16_to_cpu(di->i_dyn_features) & OCFS2_INLINE_DATA_FL) {"

- Heming

> ---
> fs/ocfs2/dlmglue.c | 5 +++++
> fs/ocfs2/inode.c | 29 +++++++++++++++++++++++++++++
> fs/ocfs2/inode.h | 2 ++
> 3 files changed, 36 insertions(+)
>
> diff --git a/fs/ocfs2/dlmglue.c b/fs/ocfs2/dlmglue.c
> index 92a6149da9c1..69aaceeb76bc 100644
> --- a/fs/ocfs2/dlmglue.c
> +++ b/fs/ocfs2/dlmglue.c
> @@ -2363,6 +2363,11 @@ static int ocfs2_inode_lock_update(struct inode *inode,
> goto bail_refresh;
> }
>
> + /* JBD2-managed buffers can bypass ocfs2_validate_inode_block(). */
> + status = ocfs2_validate_inline_dir(inode->i_sb, oi->ip_blkno, fe);
> + if (status)
> + goto bail_refresh;
> +
> /* This is a good chance to make sure we're not
> * locking an invalid object. ocfs2_read_inode_block()
> * already checked that the inode block is sane.
> diff --git a/fs/ocfs2/inode.c b/fs/ocfs2/inode.c
> index fcc89856ab95..3c8a9b592d25 100644
> --- a/fs/ocfs2/inode.c
> +++ b/fs/ocfs2/inode.c
> @@ -66,6 +66,26 @@ static int ocfs2_filecheck_validate_inode_block(struct super_block *sb,
> static int ocfs2_filecheck_repair_inode_block(struct super_block *sb,
> struct buffer_head *bh);
>
> +/* Inline directories must never advertise more data than id_count can hold. */
> +int ocfs2_validate_inline_dir(struct super_block *sb, u64 blkno,
> + struct ocfs2_dinode *di)
> +{
> + struct ocfs2_inline_data *data = &di->id2.i_data;
> +
> + if (!S_ISDIR(le16_to_cpu(di->i_mode)) ||
> + !(le16_to_cpu(di->i_dyn_features) & OCFS2_INLINE_DATA_FL))
> + return 0;
> +
> + if (le64_to_cpu(di->i_size) > le16_to_cpu(data->id_count))
> + return ocfs2_error(sb,
> + "Invalid dinode #%llu: inline dir i_size %llu exceeds id_count %u\n",
> + (unsigned long long)blkno,
> + (unsigned long long)le64_to_cpu(di->i_size),
> + le16_to_cpu(data->id_count));
> +
> + return 0;
> +}
> +
> void ocfs2_set_inode_flags(struct inode *inode)
> {
> unsigned int flags = OCFS2_I(inode)->ip_attr;
> @@ -611,6 +631,11 @@ static int ocfs2_read_locked_inode(struct inode *inode,
> "Inode %llu: system file state is ambiguous\n",
> (unsigned long long)args->fi_blkno);
>
> + /* JBD2-managed buffers can bypass ocfs2_validate_inode_block(). */
> + status = ocfs2_validate_inline_dir(inode->i_sb, args->fi_blkno, fe);
> + if (status)
> + goto bail;
> +
> if (S_ISCHR(le16_to_cpu(fe->i_mode)) ||
> S_ISBLK(le16_to_cpu(fe->i_mode)))
> inode->i_rdev = huge_decode_dev(le64_to_cpu(fe->id1.dev1.i_rdev));
> @@ -1503,6 +1528,10 @@ int ocfs2_validate_inode_block(struct super_block *sb,
> goto bail;
> }
>
> + rc = ocfs2_validate_inline_dir(sb, bh->b_blocknr, di);
> + if (rc)
> + goto bail;
> +
> rc = 0;
>
> bail:
> diff --git a/fs/ocfs2/inode.h b/fs/ocfs2/inode.h
> index accf03d4765e..1d71648c1294 100644
> --- a/fs/ocfs2/inode.h
> +++ b/fs/ocfs2/inode.h
> @@ -139,6 +139,8 @@ int ocfs2_mark_inode_dirty(handle_t *handle,
>
> void ocfs2_set_inode_flags(struct inode *inode);
> void ocfs2_get_inode_flags(struct ocfs2_inode_info *oi);
> +int ocfs2_validate_inline_dir(struct super_block *sb, u64 blkno,
> + struct ocfs2_dinode *di);
>
> static inline blkcnt_t ocfs2_inode_sector_count(struct inode *inode)
> {
> --
> 2.49.0
>