ocfs2: validate rl_used against rl_count in refcount block validator
Subsystems (2)
PATCH 1/1 ocfs2: validate rl_used against rl_count in refcount block validator
ocfs2_find_refcount_rec_in_rl() walks the on-disk refcount record array with:
for (; i < le16_to_cpu(rb->rf_records.rl_used); i++) {
rec = &rb->rf_records.rl_recs[i];
...rl_recs[] lives in a single metadata block (4096 bytes on the common configuration), so its real capacity is fixed by ocfs2_refcount_recs_per_rb(sb) (247 records for a 4K block with the 16-byte ocfs2_refcount_rec). rl_used and rl_count are both read directly off disk by ocfs2_validate_refcount_block() and are never checked against that capacity, nor against each other, before any refcount/reflink/CoW operation walks the array.
A crafted (or corrupted) refcount block with rl_used == 0xffff makes the loop above walk far past the end of the block, dereferencing rl_recs[i] for i up to 65534. The resulting index is then handed to the sibling ocfs2_insert_refcount_rec(), whose insert-shift does:
if (index < le16_to_cpu(rf_list->rl_used)) memmove(&rf_list->rl_recs[index + 1], &rf_list->rl_recs[index], (le16_to_cpu(rf_list->rl_used) - index) * sizeof(struct ocfs2_refcount_rec));
i.e. a memmove() of up to (0xffff - index) * 16 bytes (~1 MiB) from an offset already past the block. This is reachable from an ordinary reflink (FICLONE) against a crafted/corrupted ocfs2 image: attaching an extent whose cpos sorts past every real record in the leaf forces the lookup to run off the end instead of returning early on a match. The attacker model is local: CAP_SYS_ADMIN mounting a crafted or corrupted ocfs2 image, or a raw write to the block device backing an already-mounted ocfs2 filesystem.
ocfs2_validate_refcount_block() already validates the block's ECC, signature, rf_blkno and rf_fs_generation, but never rl_count/rl_used against the block's actual on-disk capacity. This is the same class of gap that ocfs2_validate_extent_block() (fs/ocfs2/alloc.c) already closes for the sibling extent-list header, which checks both the record capacity and the "used" bound before any code walks h_list.l_recs[]:
if (le16_to_cpu(eb->h_list.l_count) != ocfs2_extent_recs_per_eb(sb)) {
rc = ocfs2_error(...);
goto bail;
}
if (le16_to_cpu(eb->h_list.l_next_free_rec) >
le16_to_cpu(eb->h_list.l_count)) {
rc = ocfs2_error(...);
goto bail;
}Add the equivalent pair of checks to ocfs2_validate_refcount_block():
reject a refcount block whose rl_count does not match the fixed per-block capacity returned by ocfs2_refcount_recs_per_rb(), and reject rl_used > rl_count. Both checks are skipped when OCFS2_REFCOUNT_TREE_FL is set, because in that case the same union bytes hold an ocfs2_extent_list (rf_list), not the refcount record list (rf_records) -- that layout is already validated separately by ocfs2_validate_extent_block() when the referenced extent block is read. This mirrors the existing
"!(rb->rf_flags & OCFS2_REFCOUNT_TREE_FL)" guard used elsewhere in this file (e.g. ocfs2_get_refcount_rec()) to decide whether rf_records or rf_list is the live member of the union.
With this in place, a forged rl_used/rl_count is caught at block validation time (ocfs2_error()), consistent with every other corruption check in this function, instead of driving an out-of-bounds read in ocfs2_find_refcount_rec_in_rl() and a subsequent out-of-bounds memmove() in ocfs2_insert_refcount_rec().
Verified against a crafted image on a v6.19 KASAN (KASAN_GENERIC)
build: replaying the same reflink (FICLONE) reliably hit a KASAN report in __ocfs2_increase_refcount()/ocfs2_insert_refcount_rec() before this patch, and triggers no report once ocfs2_validate_refcount_block() rejects the forged rl_used/rl_count.
--- fs/ocfs2/refcounttree.c | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+)
fs/ocfs2/refcounttree.c +27 -0
@@ -116,6 +116,33 @@ static int ocfs2_validate_refcount_block(struct super_block *sb, le32_to_cpu(rb->rf_fs_generation)); goto out; } + +/* +* rf_records (rl_count/rl_used/rl_recs[]) is only meaningful when +* this block is not an interior tree block (OCFS2_REFCOUNT_TREE_FL); +* in that case the same union bytes hold an extent list (rf_list) +* instead, which is validated by ocfs2_validate_extent_block(). +*/ +if (!(le32_to_cpu (rb->rf_flags) &OCFS2_REFCOUNT_TREE_FL )) { +if (le16_to_cpu (rb->rf_records.rl_count) != +ocfs2_refcount_recs_per_rb (sb)) { + rc =ocfs2_error (sb, +"Refcount block #%llu has an invalid rl_count of %u\n" , + (unsigned long long )bh->b_blocknr, +le16_to_cpu (rb->rf_records.rl_count)); + goto out; + } + +if (le16_to_cpu (rb->rf_records.rl_used) > +le16_to_cpu (rb->rf_records.rl_count)) { + rc =ocfs2_error (sb, +"Refcount block #%llu has an invalid rl_used of %u (rl_count %u)\n" , + (unsigned long long )bh->b_blocknr, +le16_to_cpu (rb->rf_records.rl_used), +le16_to_cpu (rb->rf_records.rl_count)); + goto out; + } + } out:return rc; }
Signature · 1 line
2.50.1 (Apple Git-155)
Quoted 79 lines · Ibrahim Hashimov
ocfs2_find_refcount_rec_in_rl()walks the on-disk refcount record array with:for (; i < le16_to_cpu(rb->rf_records.rl_used); i++) { rec = &rb->rf_records.rl_recs[i]; ...rl_recs[] lives in a single metadata block (4096 bytes on the common configuration), so its real capacity is fixed by
ocfs2_refcount_recs_per_rb(sb)(247 records for a 4K block with the 16-byteocfs2_refcount_rec).rl_usedandrl_countare both read directly off disk byocfs2_validate_refcount_block()and are never checked against that capacity, nor against each other, before any refcount/reflink/CoW operation walks the array.A crafted (or corrupted) refcount block with
rl_used==0xffffmakes the loop above walk far past the end of the block, dereferencing rl_recs[i] for i up to 65534. The resulting index is then handed to the siblingocfs2_insert_refcount_rec(), whose insert-shift does:if (index < le16_to_cpu(rf_list->rl_used)) memmove(&rf_list->rl_recs[index + 1], &rf_list->rl_recs[index], (le16_to_cpu(rf_list->rl_used) - index) * sizeof(struct ocfs2_refcount_rec));i.e. a
memmove()of up to (0xffff- index) * 16 bytes (~1 MiB) from an offset already past the block. This is reachable from an ordinary reflink (FICLONE) against a crafted/corrupted ocfs2 image: attaching an extent whose cpos sorts past every real record in the leaf forces the lookup to run off the end instead of returning early on a match. The attacker model is local:CAP_SYS_ADMINmounting a crafted or corrupted ocfs2 image, or a raw write to the block device backing an already-mounted ocfs2 filesystem.
ocfs2_validate_refcount_block()already validates the block's ECC, signature,rf_blknoandrf_fs_generation, but never rl_count/rl_used against the block's actual on-disk capacity. This is the same class of gap thatocfs2_validate_extent_block()(fs/ocfs2/alloc.c) already closes for the sibling extent-list header, which checks both the record capacity and the "used" bound before any code walks h_list.l_recs[]:if (le16_to_cpu(eb->h_list.l_count) != ocfs2_extent_recs_per_eb(sb)) { rc = ocfs2_error(...); goto bail; } if (le16_to_cpu(eb->h_list.l_next_free_rec) > le16_to_cpu(eb->h_list.l_count)) { rc = ocfs2_error(...); goto bail; }Add the equivalent pair of checks to
ocfs2_validate_refcount_block():
reject a refcount block whoserl_countdoes not match the fixed per-block capacity returned byocfs2_refcount_recs_per_rb(), and rejectrl_used>rl_count. Both checks are skipped whenOCFS2_REFCOUNT_TREE_FLis set, because in that case the same union bytes hold anocfs2_extent_list(rf_list), not the refcount record list (rf_records) -- that layout is already validated separately byocfs2_validate_extent_block()when the referenced extent block is read. This mirrors the existing
"!(rb->rf_flags &OCFS2_REFCOUNT_TREE_FL)" guard used elsewhere in this file (e.g.ocfs2_get_refcount_rec()) to decide whetherrf_recordsorrf_listis the live member of the union.With this in place, a forged rl_used/rl_count is caught at block validation time (
ocfs2_error()), consistent with every other corruption check in this function, instead of driving an out-of-bounds read inocfs2_find_refcount_rec_in_rl()and a subsequent out-of-boundsmemmove()inocfs2_insert_refcount_rec().Verified against a crafted image on a v6.19 KASAN (
KASAN_GENERIC)
build: replaying the same reflink (FICLONE) reliably hit a KASAN report in __ocfs2_increase_refcount()/ocfs2_insert_refcount_rec() before this patch, and triggers no report onceocfs2_validate_refcount_block()rejects the forged rl_used/rl_count.
Quoted 4 lines
Fixes f2c870e3b12e ("ocfs2: Add ocfs2_read_refcount_block.")Signed-off-by Ibrahim Hashimov <[email protected]>Assisted-by AuditCode-AI:2026.07
Looks good.
Quoted 42 lines
--- fs/ocfs2/refcounttree.c | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+)fs/ocfs2/refcounttree.c +27
-0@@ -116,6 +116,33 @@ static int ocfs2_validate_refcount_block(struct super_block *sb, le32_to_cpu(rb->rf_fs_generation)); goto out; } + +/* +* rf_records (rl_count/rl_used/rl_recs[]) is only meaningful when +* this block is not an interior tree block (OCFS2_REFCOUNT_TREE_FL); +* in that case the same union bytes hold an extent list (rf_list) +* instead, which is validated by ocfs2_validate_extent_block(). +*/ +if (!(le32_to_cpu (rb->rf_flags) &OCFS2_REFCOUNT_TREE_FL )) { +if (le16_to_cpu (rb->rf_records.rl_count) != +ocfs2_refcount_recs_per_rb (sb)) { + rc =ocfs2_error (sb, +"Refcount block #%llu has an invalid rl_count of %u\n" , + (unsigned long long )bh->b_blocknr, +le16_to_cpu (rb->rf_records.rl_count)); + goto out; + } + +if (le16_to_cpu (rb->rf_records.rl_used) > +le16_to_cpu (rb->rf_records.rl_count)) { + rc =ocfs2_error (sb, +"Refcount block #%llu has an invalid rl_used of %u (rl_count %u)\n" , + (unsigned long long )bh->b_blocknr, +le16_to_cpu (rb->rf_records.rl_used), +le16_to_cpu (rb->rf_records.rl_count)); + goto out; + } + } out:return rc; }
Quoted 79 lines · Ibrahim Hashimov
ocfs2_find_refcount_rec_in_rl()walks the on-disk refcount record array with:for (; i < le16_to_cpu(rb->rf_records.rl_used); i++) { rec = &rb->rf_records.rl_recs[i]; ...rl_recs[] lives in a single metadata block (4096 bytes on the common configuration), so its real capacity is fixed by
ocfs2_refcount_recs_per_rb(sb)(247 records for a 4K block with the 16-byteocfs2_refcount_rec).rl_usedandrl_countare both read directly off disk byocfs2_validate_refcount_block()and are never checked against that capacity, nor against each other, before any refcount/reflink/CoW operation walks the array.A crafted (or corrupted) refcount block with
rl_used==0xffffmakes the loop above walk far past the end of the block, dereferencing rl_recs[i] for i up to 65534. The resulting index is then handed to the siblingocfs2_insert_refcount_rec(), whose insert-shift does:if (index < le16_to_cpu(rf_list->rl_used)) memmove(&rf_list->rl_recs[index + 1], &rf_list->rl_recs[index], (le16_to_cpu(rf_list->rl_used) - index) * sizeof(struct ocfs2_refcount_rec));i.e. a
memmove()of up to (0xffff- index) * 16 bytes (~1 MiB) from an offset already past the block. This is reachable from an ordinary reflink (FICLONE) against a crafted/corrupted ocfs2 image: attaching an extent whose cpos sorts past every real record in the leaf forces the lookup to run off the end instead of returning early on a match. The attacker model is local:CAP_SYS_ADMINmounting a crafted or corrupted ocfs2 image, or a raw write to the block device backing an already-mounted ocfs2 filesystem.
ocfs2_validate_refcount_block()already validates the block's ECC, signature,rf_blknoandrf_fs_generation, but never rl_count/rl_used against the block's actual on-disk capacity. This is the same class of gap thatocfs2_validate_extent_block()(fs/ocfs2/alloc.c) already closes for the sibling extent-list header, which checks both the record capacity and the "used" bound before any code walks h_list.l_recs[]:if (le16_to_cpu(eb->h_list.l_count) != ocfs2_extent_recs_per_eb(sb)) { rc = ocfs2_error(...); goto bail; } if (le16_to_cpu(eb->h_list.l_next_free_rec) > le16_to_cpu(eb->h_list.l_count)) { rc = ocfs2_error(...); goto bail; }Add the equivalent pair of checks to
ocfs2_validate_refcount_block():
reject a refcount block whoserl_countdoes not match the fixed per-block capacity returned byocfs2_refcount_recs_per_rb(), and rejectrl_used>rl_count. Both checks are skipped whenOCFS2_REFCOUNT_TREE_FLis set, because in that case the same union bytes hold anocfs2_extent_list(rf_list), not the refcount record list (rf_records) -- that layout is already validated separately byocfs2_validate_extent_block()when the referenced extent block is read. This mirrors the existing
"!(rb->rf_flags &OCFS2_REFCOUNT_TREE_FL)" guard used elsewhere in this file (e.g.ocfs2_get_refcount_rec()) to decide whetherrf_recordsorrf_listis the live member of the union.With this in place, a forged rl_used/rl_count is caught at block validation time (
ocfs2_error()), consistent with every other corruption check in this function, instead of driving an out-of-bounds read inocfs2_find_refcount_rec_in_rl()and a subsequent out-of-boundsmemmove()inocfs2_insert_refcount_rec().Verified against a crafted image on a v6.19 KASAN (
KASAN_GENERIC)
build: replaying the same reflink (FICLONE) reliably hit a KASAN report in __ocfs2_increase_refcount()/ocfs2_insert_refcount_rec() before this patch, and triggers no report onceocfs2_validate_refcount_block()rejects the forged rl_used/rl_count.
Quoted 4 lines
Fixes f2c870e3b12e ("ocfs2: Add ocfs2_read_refcount_block.")Signed-off-by Ibrahim Hashimov <[email protected]>Assisted-by AuditCode-AI:2026.07
Quoted 42 lines
--- fs/ocfs2/refcounttree.c | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+)fs/ocfs2/refcounttree.c +27
-0@@ -116,6 +116,33 @@ static int ocfs2_validate_refcount_block(struct super_block *sb, le32_to_cpu(rb->rf_fs_generation)); goto out; } + +/* +* rf_records (rl_count/rl_used/rl_recs[]) is only meaningful when +* this block is not an interior tree block (OCFS2_REFCOUNT_TREE_FL); +* in that case the same union bytes hold an extent list (rf_list) +* instead, which is validated by ocfs2_validate_extent_block(). +*/ +if (!(le32_to_cpu (rb->rf_flags) &OCFS2_REFCOUNT_TREE_FL )) { +if (le16_to_cpu (rb->rf_records.rl_count) != +ocfs2_refcount_recs_per_rb (sb)) { + rc =ocfs2_error (sb, +"Refcount block #%llu has an invalid rl_count of %u\n" , + (unsigned long long )bh->b_blocknr, +le16_to_cpu (rb->rf_records.rl_count)); + goto out; + } + +if (le16_to_cpu (rb->rf_records.rl_used) > +le16_to_cpu (rb->rf_records.rl_count)) { + rc =ocfs2_error (sb, +"Refcount block #%llu has an invalid rl_used of %u (rl_count %u)\n" , + (unsigned long long )bh->b_blocknr, +le16_to_cpu (rb->rf_records.rl_used), +le16_to_cpu (rb->rf_records.rl_count)); + goto out; + } + } out:return rc; }
Quoted 15 lines · On Thu, 9 Jul 2026 15:26:09 +0200 Ibrahim Hashimov <[email protected]>…
ocfs2_find_refcount_rec_in_rl()walks the on-disk refcount record array with:for (; i < le16_to_cpu(rb->rf_records.rl_used); i++) { rec = &rb->rf_records.rl_recs[i]; ...rl_recs[] lives in a single metadata block (4096 bytes on the common configuration), so its real capacity is fixed by
ocfs2_refcount_recs_per_rb(sb)(247 records for a 4K block with the 16-byteocfs2_refcount_rec).rl_usedandrl_countare both read directly off disk byocfs2_validate_refcount_block()and are never checked against that capacity, nor against each other, before any refcount/reflink/CoW operation walks the array.
Quoted 9 lines
A crafted (or corrupted) refcount block with
rl_used==0xffffmakes the loop above walk far past the end of the block, dereferencing rl_recs[i] for i up to 65534. The resulting index is then handed to the siblingocfs2_insert_refcount_rec(), whose insert-shift does:...
Add the equivalent pair of checks to
ocfs2_validate_refcount_block():
Thanks. AI review might have found another bug in there:
https://sashiko.dev/#/patchset/[email protected]