[PATCH 0/2] ocfs2: validate xattr metadata bounds
Zhang Cen <[email protected]> Tue, 26 May 2026 11:04:08 +0800
| Newsgroups | dev.linux.lists.ocfs2-devel |
|---|---|
| Message-ID | <[email protected]> |
Hi, This small series rejects corrupted OCFS2 xattr metadata before the getxattr and listxattr paths walk filesystem-controlled inline, external block, or indexed bucket storage. Patch 1 validates the inline xattr header placement and entry count before ocfs2_xattr_ibody_get() parses it. Patch 2 builds on that helper and validates listxattr entry counts and name ranges against the storage that backs each walk. Both issues were reproduced with crafted OCFS2 images under KASAN. Zhang Cen (2): ocfs2: validate inline xattr metadata in ocfs2_xattr_ibody_get ocfs2: validate listxattr entry bounds fs/ocfs2/xattr.c | 122 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 101 insertions(+), 21 deletions(-) -- 2.43.0 From 89ea62caaac9486558bf9cb933975d276a3e67c8 Mon Sep 17 00:00:00 2001 From: Zhang Cen <[email protected]> Date: Sun, 24 May 2026 14:34:49 +0800 Subject: [PATCH 1/2] ocfs2: validate inline xattr metadata in ocfs2_xattr_ibody_get ocfs2_xattr_ibody_get() derives xs->header from di->i_xattr_inline_size and then immediately hands that header to ocfs2_xattr_find_entry(). If a corrupted inode advertises a zero, too-small, or too-large inline xattr size, the get path can place the header at the end of the inode block or outside it before reading xh_count and walking entries. Reuse the inline xattr size and xh_count validation that the list path already relies on before parsing the in-inode header on the get path. Reject corrupted inline metadata with -EFSCORRUPTED instead of walking past the inline area. A crafted image can otherwise trigger: BUG: KASAN: use-after-free in ocfs2_xattr_find_entry+0x5a/0x170 Fixes: cf1d6c763fbc ("ocfs2: Add extended attribute support") Assisted-by: Codex:gpt-5.5 Signed-off-by: Zhang Cen <[email protected]> --- fs/ocfs2/xattr.c | 51 ++++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/fs/ocfs2/xattr.c b/fs/ocfs2/xattr.c index 86cfd4c2adf9..eabfaa02d8b8 100644 --- a/fs/ocfs2/xattr.c +++ b/fs/ocfs2/xattr.c @@ -967,24 +967,17 @@ int ocfs2_has_inline_xattr_value_outside(struct inode *inode, return 0; } -static int ocfs2_xattr_ibody_list(struct inode *inode, - struct ocfs2_dinode *di, - char *buffer, - size_t buffer_size) +static int ocfs2_xattr_check_inline_xh(struct inode *inode, + struct ocfs2_dinode *di, + struct ocfs2_xattr_header **header) { - struct ocfs2_xattr_header *header = NULL; - struct ocfs2_inode_info *oi = OCFS2_I(inode); - int ret = 0; - u16 xattr_count; + struct ocfs2_xattr_header *xh; size_t max_entries; u16 inline_size; - - if (!(oi->ip_dyn_features & OCFS2_INLINE_XATTR_FL)) - return ret; + u16 xattr_count; inline_size = le16_to_cpu(di->i_xattr_inline_size); - /* Validate inline size is reasonable */ if (inline_size > inode->i_sb->s_blocksize || inline_size < sizeof(struct ocfs2_xattr_header)) { ocfs2_error(inode->i_sb, @@ -994,12 +987,11 @@ static int ocfs2_xattr_ibody_list(struct inode *inode, return -EFSCORRUPTED; } - header = (struct ocfs2_xattr_header *) - ((void *)di + inode->i_sb->s_blocksize - inline_size); - - xattr_count = le16_to_cpu(header->xh_count); + xh = (struct ocfs2_xattr_header *) + ((void *)di + inode->i_sb->s_blocksize - inline_size); + xattr_count = le16_to_cpu(xh->xh_count); max_entries = (inline_size - sizeof(struct ocfs2_xattr_header)) / - sizeof(struct ocfs2_xattr_entry); + sizeof(struct ocfs2_xattr_entry); if (xattr_count > max_entries) { ocfs2_error(inode->i_sb, @@ -1009,6 +1001,26 @@ static int ocfs2_xattr_ibody_list(struct inode *inode, return -EFSCORRUPTED; } + *header = xh; + return 0; +} + +static int ocfs2_xattr_ibody_list(struct inode *inode, + struct ocfs2_dinode *di, + char *buffer, + size_t buffer_size) +{ + struct ocfs2_xattr_header *header = NULL; + struct ocfs2_inode_info *oi = OCFS2_I(inode); + int ret = 0; + + if (!(oi->ip_dyn_features & OCFS2_INLINE_XATTR_FL)) + return ret; + + ret = ocfs2_xattr_check_inline_xh(inode, di, &header); + if (ret) + return ret; + ret = ocfs2_xattr_list_entries(inode, header, buffer, buffer_size); return ret; @@ -1200,8 +1212,9 @@ static int ocfs2_xattr_ibody_get(struct inode *inode, return -ENODATA; xs->end = (void *)di + inode->i_sb->s_blocksize; - xs->header = (struct ocfs2_xattr_header *) - (xs->end - le16_to_cpu(di->i_xattr_inline_size)); + ret = ocfs2_xattr_check_inline_xh(inode, di, &xs->header); + if (ret) + return ret; xs->base = (void *)xs->header; xs->here = xs->header->xh_entries; -- 2.43.0 From 1ff5a1fde2b2ed5cfacf6c2205bb2f139795aa68 Mon Sep 17 00:00:00 2001 From: Zhang Cen <[email protected]> Date: Tue, 26 May 2026 10:56:57 +0800 Subject: [PATCH 2/2] ocfs2: validate listxattr entry bounds listxattr() trusts xh_count and the on-disk name offsets in the xattr storage it walks. A corrupted inline xattr area, non-indexed xattr block or indexed bucket can therefore push the walk past the entry array or make it read name bytes from outside the local xattr storage. Validate the entry count against the storage that actually backs the walk before iterating it, and reject names that extend past that local storage. For non-indexed external xattr blocks, use the bytes from xb_attrs.xb_header to the end of the block, so the count check matches the real xattr storage. For indexed buckets, validate xh_count against the full 4K bucket storage and reject names that select an invalid bucket block or cross the end of that block before bucket_block() is used. This makes listxattr() fail with -EFSCORRUPTED instead of walking corrupted xattr metadata. Validation reproduced this kernel report: KASAN use-after-free in ocfs2_xattr_list_entries+0xd7/0x190 RIP: 0033:0x42086b Read of size 1 Call trace: dump_stack_lvl+0x66/0xa0 (?:?) print_report+0xce/0x630 (?:?) ocfs2_xattr_list_entries+0xd7/0x190 (fs/ocfs2/xattr.c:937) srso_alias_return_thunk+0x5/0xfbef5 (?:?) __virt_addr_valid+0x19f/0x330 (?:?) kasan_report+0xe0/0x110 (?:?) ocfs2_listxattr+0x3f6/0x610 (fs/ocfs2/xattr.c:1050) vfs_listxattr+0x4c/0xa0 (?:?) listxattr+0x90/0xe0 (?:?) path_listxattrat+0xed/0x220 (?:?) do_user_addr_fault+0x65a/0x890 (?:?) do_syscall_64+0x115/0x6a0 (arch/x86/entry/syscall_64.c:87) entry_SYSCALL_64_after_hwframe+0x77/0x7f (?:?) Fixes: cf1d6c763fbc ("ocfs2: Add extended attribute support") Fixes: 0c044f0b24b9 ("ocfs2: Add xattr bucket iteration for large numbers of EAs") Assisted-by: Codex:gpt-5.5 Signed-off-by: Zhang Cen <[email protected]> --- fs/ocfs2/xattr.c | 71 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/fs/ocfs2/xattr.c b/fs/ocfs2/xattr.c index eabfaa02d8b8..66f5f9da2b3c 100644 --- a/fs/ocfs2/xattr.c +++ b/fs/ocfs2/xattr.c @@ -925,14 +925,54 @@ static int ocfs2_xattr_list_entry(struct super_block *sb, return 0; } +static int ocfs2_xattr_list_corrupted(struct inode *inode) +{ + ocfs2_error(inode->i_sb, "corrupted xattr entries in inode %llu", + (unsigned long long)OCFS2_I(inode)->ip_blkno); + return -EFSCORRUPTED; +} + +static int ocfs2_validate_xattr_list_entries(struct inode *inode, + struct ocfs2_xattr_header *header, + size_t storage_size) +{ + u16 xattr_count = le16_to_cpu(header->xh_count); + size_t max_entries; + int i; + + if (storage_size < sizeof(*header)) + return ocfs2_xattr_list_corrupted(inode); + + max_entries = (storage_size - sizeof(*header)) / + sizeof(struct ocfs2_xattr_entry); + if (xattr_count > max_entries) + return ocfs2_xattr_list_corrupted(inode); + + for (i = 0; i < xattr_count; i++) { + struct ocfs2_xattr_entry *entry = &header->xh_entries[i]; + size_t name_offset = le16_to_cpu(entry->xe_name_offset); + + if (name_offset > storage_size || + entry->xe_name_len > storage_size - name_offset) + return ocfs2_xattr_list_corrupted(inode); + } + + return 0; +} + static int ocfs2_xattr_list_entries(struct inode *inode, struct ocfs2_xattr_header *header, + size_t storage_size, char *buffer, size_t buffer_size) { size_t result = 0; int i, type, ret; const char *name; + ret = ocfs2_validate_xattr_list_entries(inode, header, storage_size); + if (ret) + return ret; + for (i = 0 ; i < le16_to_cpu(header->xh_count); i++) { struct ocfs2_xattr_entry *entry = &header->xh_entries[i]; type = ocfs2_xattr_get_type(entry); @@ -1013,15 +1053,18 @@ static int ocfs2_xattr_ibody_list(struct inode *inode, struct ocfs2_xattr_header *header = NULL; struct ocfs2_inode_info *oi = OCFS2_I(inode); int ret = 0; + u16 inline_size; if (!(oi->ip_dyn_features & OCFS2_INLINE_XATTR_FL)) return ret; + inline_size = le16_to_cpu(di->i_xattr_inline_size); ret = ocfs2_xattr_check_inline_xh(inode, di, &header); if (ret) return ret; - ret = ocfs2_xattr_list_entries(inode, header, buffer, buffer_size); + ret = ocfs2_xattr_list_entries(inode, header, inline_size, + buffer, buffer_size); return ret; } @@ -1033,6 +1076,7 @@ static int ocfs2_xattr_block_list(struct inode *inode, { struct buffer_head *blk_bh = NULL; struct ocfs2_xattr_block *xb; + size_t storage_size; int ret = 0; if (!di->i_xattr_loc) @@ -1048,7 +1092,10 @@ static int ocfs2_xattr_block_list(struct inode *inode, xb = (struct ocfs2_xattr_block *)blk_bh->b_data; if (!(le16_to_cpu(xb->xb_flags) & OCFS2_XATTR_INDEXED)) { struct ocfs2_xattr_header *header = &xb->xb_attrs.xb_header; + storage_size = blk_bh->b_size - + offsetof(struct ocfs2_xattr_block, xb_attrs.xb_header); ret = ocfs2_xattr_list_entries(inode, header, + storage_size, buffer, buffer_size); } else ret = ocfs2_xattr_tree_list_index_block(inode, blk_bh, @@ -4096,8 +4143,28 @@ static int ocfs2_list_xattr_bucket(struct inode *inode, struct ocfs2_xattr_tree_list *xl = (struct ocfs2_xattr_tree_list *)para; int i, block_off, new_offset; const char *name; + size_t blocksize = inode->i_sb->s_blocksize; + u16 xattr_count = le16_to_cpu(bucket_xh(bucket)->xh_count); + size_t max_entries; + + max_entries = (OCFS2_XATTR_BUCKET_SIZE - + sizeof(struct ocfs2_xattr_header)) / + sizeof(struct ocfs2_xattr_entry); + if (xattr_count > max_entries) + return ocfs2_xattr_list_corrupted(inode); + + for (i = 0; i < xattr_count; i++) { + struct ocfs2_xattr_entry *entry = &bucket_xh(bucket)->xh_entries[i]; + size_t name_offset = le16_to_cpu(entry->xe_name_offset); + + block_off = name_offset >> inode->i_sb->s_blocksize_bits; + new_offset = name_offset % blocksize; + if (block_off >= bucket->bu_blocks || + entry->xe_name_len > blocksize - new_offset) + return ocfs2_xattr_list_corrupted(inode); + } - for (i = 0 ; i < le16_to_cpu(bucket_xh(bucket)->xh_count); i++) { + for (i = 0 ; i < xattr_count; i++) { struct ocfs2_xattr_entry *entry = &bucket_xh(bucket)->xh_entries[i]; type = ocfs2_xattr_get_type(entry); -- 2.43.0