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

ZhengYuan Huang posted 1 patch 2 months ago
fs/ocfs2/dlmglue.c |  5 +++++
fs/ocfs2/inode.c   | 29 +++++++++++++++++++++++++++++
fs/ocfs2/inode.h   |  2 ++
3 files changed, 36 insertions(+)
[PATCH v2] ocfs2: validate inline dir size during inode reads
Posted by ZhengYuan Huang 2 months ago
[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@gmail.com>
---
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
---
 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
Re: [PATCH v2] ocfs2: validate inline dir size during inode reads
Posted by Heming Zhao 2 months ago
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@gmail.com>
> ---
> 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
>
Re: [PATCH v2] ocfs2: validate inline dir size during inode reads
Posted by ZhengYuan Huang 2 months ago
On Mon, Apr 13, 2026 at 3:19 PM Heming Zhao <heming.zhao@suse.com> wrote:
>
> 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@gmail.com>
> > ---
> > 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

My mistake.

I reran my reproducer on the latest upstream kernel and could not
reproduce the issue there, so based on the current repro results, it
appears that the bug has already been fixed.

Although code inspection still suggests that JBD-managed buffers may
bypass that validation path, my current reproducer has not been able
to trigger the bug on the latest kernel. We will continue testing to
determine whether there is still a reachable path here or whether the
issue has in fact already been fixed by another change.

Thanks,
ZhengYuan Huang