ocfs2: fix UBSAN array-index-out-of-bounds in ocfs2_sum_rightmost_rec
PATCH v4 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 embedded 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 embedded 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 4:
- Update commit message to use "inline" instead of "embedded"
v3: https://lore.kernel.org/all/aibMhhAH-swS38i0@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
Quoted 19 lines · Ian Bridges
[BUG]
On-disk corruption settingl_next_free_recto 0 in an inode's embedded 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. 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.[FIX]
Validate the inode's embedded 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.
Quoted 4 lines
Reported-by [email protected]Signed-off-by Ian Bridges <[email protected]>
Looks fine.
Quoted 3 lines
---
Changes in 4:
- Update commit message to use "inline" instead of "embedded"
Typo here? I think you mean 'embedded' instead of 'inline'.
Quoted 49 lines
v3: https://lore.kernel.org/all/aibMhhAH-swS38i0@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:
Quoted 27 lines · Joseph Qi
Quoted 19 lines · Ian Bridges
[BUG]
On-disk corruption settingl_next_free_recto 0 in an inode's embedded 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. 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.[FIX]
Validate the inode's embedded 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.Quoted 4 lines
Reported-by [email protected]Signed-off-by Ian Bridges <[email protected]>
Quoted 9 lines
Looks fine.
Reviewed-by Joseph Qi <[email protected]>Quoted 3 lines
---
Changes in 4:
- Update commit message to use "inline" instead of "embedded"Typo here? I think you mean 'embedded' instead of 'inline'.
Yes, the placement of the "inline" and "embedded" should have been swapped.
Ian
Quoted 50 lines
Quoted 49 lines
v3: https://lore.kernel.org/all/aibMhhAH-swS38i0@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: