ocfs2: fix UBSAN array-index-out-of-bounds in ocfs2_sum_rightmost_rec
PATCH v2 1/1 ocfs2: fix UBSAN array-index-out-of-bounds in ocfs2_sum_rightmost_rec
[BUG]
On-disk corruption setting l_next_free_rec to 0 in an inode's inline extent list triggers a UBSAN panic on the next write to that file.
[CAUSE]ocfs2_sum_rightmost_rec() computes
i = le16_to_cpu(el->l_next_free_rec) - 1
and accesses el->l_recs[i] without validating i. When l_next_free_rec is 0, i becomes -1; when l_next_free_rec exceeds l_count, i falls past the end of the array. Either case violates the __counted_by_le(l_count) annotation on l_recs[] and triggers UBSAN.
[FIX]
Validate the inode's inline extent list when the inode is read, in ocfs2_validate_inode_block(): l_count must be non-zero and no larger than the inode block can hold, and l_next_free_rec must not exceed l_count. A corrupt list is rejected at read time, before the b-tree code can index l_recs[] out of bounds.
---
Changes in v2:
- Reject the corrupt inode at read time by validating its inline extent list (l_count, l_next_free_rec) in ocfs2_validate_inode_block().
v1: https://lore.kernel.org/all/ah2ljwKRw-Xsi4Ga@dev/
fs/ocfs2/inode.c | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+)
fs/ocfs2/inode.c +32 -0
@@ -1559,6 +1559,38 @@ int ocfs2_validate_inode_block(struct super_block *sb, goto bail; } +if (ocfs2_dinode_has_extents (di)) { +struct ocfs2_extent_list *el = &di->id2.i_list; +u16 count =le16_to_cpu (el->l_count); +u16 next_free =le16_to_cpu (el->l_next_free_rec); + +if (count ==0 ) { + rc =ocfs2_error (sb, +"Invalid dinode %llu: extent list l_count is zero\n" , + (unsigned long long )bh->b_blocknr); + goto bail; + } +/* +* The exact capacity depends on i_xattr_inline_size, another +* unvalidated on-disk field. Inline xattrs only shrink the +* list, so the no-xattr maximum is a safe upper bound that a +* valid l_count never exceeds. +*/ +if (count >ocfs2_extent_recs_per_inode (sb)) { + rc =ocfs2_error (sb, +"Invalid dinode %llu: extent list l_count %u exceeds max %u\n" , + (unsigned long long )bh->b_blocknr, count, +ocfs2_extent_recs_per_inode (sb)); + goto bail; + } +if (next_free > count) { + rc =ocfs2_error (sb, +"Invalid dinode %llu: extent list l_next_free_rec %u exceeds l_count %u\n" , + (unsigned long long )bh->b_blocknr, next_free, count); + goto bail; + } + } + rc =0 ; bail:
Signature · 1 line
2.47.3
Joseph Qi
maintainerQuoted 3 lines · Ian Bridges
[BUG]
On-disk corruption settingl_next_free_recto 0 in an inode's inline extent list triggers a UBSAN panic on the next write to that file.
Replace 'inline extent list' here to 'embedded extent list'. Since 'inline' always means inline data and may cause confuse.
Quoted 9 lines
[CAUSE]
ocfs2_sum_rightmost_rec()computes
i =le16_to_cpu(el->l_next_free_rec)- 1
and accesses el->l_recs[i] without validating i. Whenl_next_free_recis 0, i becomes -1; whenl_next_free_recexceedsl_count, i falls past the end of the array. Either case violates the__counted_by_le(l_count)annotation on l_recs[] and triggers UBSAN.
Quoted 8 lines
[FIX]
Validate the inode's inline extent list when the inode is read, inocfs2_validate_inode_block():l_countmust be non-zero and no larger than the inode block can hold, andl_next_free_recmust not exceedl_count. A corrupt list is rejected at read time, before the b-tree code can index l_recs[] out of bounds.Fixes 2f26f58df041 ("ocfs2: annotate flexible array members with __counted_by_le()")
IMO, commit 2f26f58df041 just expose it as UBSAN. Before it, accessing l_recs[-1] was silent undefined behavior.
So I don't think it is the right blame commit. Since the logic was introduced long time ago, so I'd rather just mark it as "Cc: [email protected]" with no "Fixes" tag.
Quoted 56 lines
Reported-by [email protected]Signed-off-by Ian Bridges <[email protected]>---
Changes in v2:
- Reject the corrupt inode at read time by validating its inline extent list (l_count,l_next_free_rec) inocfs2_validate_inode_block().v1: https://lore.kernel.org/all/ah2ljwKRw-Xsi4Ga@dev/
fs/ocfs2/inode.c | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+)fs/ocfs2/inode.c +32
-0@@ -1559,6 +1559,38 @@ int ocfs2_validate_inode_block(struct super_block *sb, goto bail; } +if (ocfs2_dinode_has_extents (di)) { +struct ocfs2_extent_list *el = &di->id2.i_list; +u16 count =le16_to_cpu (el->l_count); +u16 next_free =le16_to_cpu (el->l_next_free_rec); + +if (count ==0 ) { + rc =ocfs2_error (sb, +"Invalid dinode %llu: extent list l_count is zero\n" , + (unsigned long long )bh->b_blocknr); + goto bail; + } +/* +* The exact capacity depends on i_xattr_inline_size, another +* unvalidated on-disk field. Inline xattrs only shrink the +* list, so the no-xattr maximum is a safe upper bound that a +* valid l_count never exceeds. +*/ +if (count >ocfs2_extent_recs_per_inode (sb)) { + rc =ocfs2_error (sb, +"Invalid dinode %llu: extent list l_count %u exceeds max %u\n" , + (unsigned long long )bh->b_blocknr, count, +ocfs2_extent_recs_per_inode (sb)); + goto bail; + } +if (next_free > count) { + rc =ocfs2_error (sb, +"Invalid dinode %llu: extent list l_next_free_rec %u exceeds l_count %u\n" , + (unsigned long long )bh->b_blocknr, next_free, count); + goto bail; + } + } + rc =0 ; bail: