[PATCH] xfs: validate btree block level on read

From: David Lee

Date: Fri Jul 31 2026 - 08:11:48 EST


Btree buffer verifiers validate record counts against the level stored in
the block itself. Callers of xfs_btree_read_buf_block(), however, use
the returned block according to the level they are traversing. A
corrupted sibling pointer can therefore return a verifier-valid block
from another level and cause out-of-bounds accesses when layout-specific
entries are moved.

Pass the expected level into xfs_btree_read_buf_block() and reject
blocks whose on-disk level differs before returning them. This
centralizes the check for traversal and sibling reads.

Fixes: 3d3e6f64e22c ("xfs: verify btree blocks as they are read from disk")
Bug found and triaged by OpenAI Security Research and
validated by Trail of Bits.

Assisted-by: Codex:gpt-5.6-sol gpt-5.5-cyber
Signed-off-by: Kyle Zeng <kylebot@xxxxxxxxxx>
---
Trail of Bits has a reproducer for this bug that triggers ㅁ
KASAN use-after-free and can share if needed.

fs/xfs/libxfs/xfs_btree.c | 48 +++++++++++++++++++------------
fs/xfs/libxfs/xfs_btree.h | 2 +-
fs/xfs/libxfs/xfs_btree_staging.c | 9 +++---
3 files changed, 36 insertions(+), 23 deletions(-)

diff --git a/fs/xfs/libxfs/xfs_btree.c b/fs/xfs/libxfs/xfs_btree.c
index 60ef7f08b..b2db143bb 100644
--- a/fs/xfs/libxfs/xfs_btree.c
+++ b/fs/xfs/libxfs/xfs_btree.c
@@ -1378,13 +1378,14 @@ xfs_btree_get_buf_block(
}

/*
- * Read in the buffer at the given ptr and return the buffer and
- * the block pointer within the buffer.
+ * Read in the buffer at the given ptr and return the buffer and the block
+ * pointer within the buffer. The block must be at the expected level.
*/
int
xfs_btree_read_buf_block(
struct xfs_btree_cur *cur,
const union xfs_btree_ptr *ptr,
+ int level,
int flags,
struct xfs_btree_block **block,
struct xfs_buf **bpp)
@@ -1409,6 +1410,14 @@ xfs_btree_read_buf_block(

xfs_btree_set_refs(cur, *bpp);
*block = XFS_BUF_TO_BLOCK(*bpp);
+ if (xfs_btree_get_level(*block) != level) {
+ xfs_buf_mark_corrupt(*bpp);
+ xfs_trans_brelse(cur->bc_tp, *bpp);
+ xfs_btree_mark_sick(cur);
+ *block = NULL;
+ *bpp = NULL;
+ return -EFSCORRUPTED;
+ }
return 0;
}

@@ -1733,7 +1742,8 @@ xfs_btree_increment(

ptrp = xfs_btree_ptr_addr(cur, cur->bc_levels[lev].ptr, block);
--lev;
- error = xfs_btree_read_buf_block(cur, ptrp, 0, &block, &bp);
+ error = xfs_btree_read_buf_block(cur, ptrp, lev, 0, &block,
+ &bp);
if (error)
goto error0;

@@ -1827,7 +1837,8 @@ xfs_btree_decrement(

ptrp = xfs_btree_ptr_addr(cur, cur->bc_levels[lev].ptr, block);
--lev;
- error = xfs_btree_read_buf_block(cur, ptrp, 0, &block, &bp);
+ error = xfs_btree_read_buf_block(cur, ptrp, lev, 0, &block,
+ &bp);
if (error)
goto error0;
xfs_btree_setbuf(cur, lev, bp);
@@ -1904,7 +1915,7 @@ xfs_btree_lookup_get_block(
return 0;
}

- error = xfs_btree_read_buf_block(cur, pp, 0, blkp, &bp);
+ error = xfs_btree_read_buf_block(cur, pp, level, 0, blkp, &bp);
if (error)
return error;

@@ -1912,10 +1923,6 @@ xfs_btree_lookup_get_block(
if (xfs_btree_check_block_owner(cur, *blkp) != NULL)
goto out_bad;

- /* Did we get the level we were looking for? */
- if (be16_to_cpu((*blkp)->bb_level) != level)
- goto out_bad;
-
/* Check that internal nodes have at least one record. */
if (level != 0 && be16_to_cpu((*blkp)->bb_numrecs) == 0)
goto out_bad;
@@ -2457,7 +2464,7 @@ xfs_btree_lshift(
goto out0;

/* Set up the left neighbor as "left". */
- error = xfs_btree_read_buf_block(cur, &lptr, 0, &left, &lbp);
+ error = xfs_btree_read_buf_block(cur, &lptr, level, 0, &left, &lbp);
if (error)
goto error0;

@@ -2653,7 +2660,7 @@ xfs_btree_rshift(
goto out0;

/* Set up the right neighbor as "right". */
- error = xfs_btree_read_buf_block(cur, &rptr, 0, &right, &rbp);
+ error = xfs_btree_read_buf_block(cur, &rptr, level, 0, &right, &rbp);
if (error)
goto error0;

@@ -2940,8 +2947,8 @@ __xfs_btree_split(
* point back to right instead of to left.
*/
if (!xfs_btree_ptr_is_null(cur, &rrptr)) {
- error = xfs_btree_read_buf_block(cur, &rrptr,
- 0, &rrblock, &rrbp);
+ error = xfs_btree_read_buf_block(cur, &rrptr, level, 0,
+ &rrblock, &rrbp);
if (error)
goto error0;
xfs_btree_set_sibling(cur, rrblock, &rptr, XFS_BB_LEFTSIB);
@@ -3365,7 +3372,8 @@ xfs_btree_new_root(
lbp = bp;
xfs_btree_buf_to_ptr(cur, lbp, &lptr);
left = block;
- error = xfs_btree_read_buf_block(cur, &rptr, 0, &right, &rbp);
+ error = xfs_btree_read_buf_block(cur, &rptr,
+ cur->bc_nlevels - 1, 0, &right, &rbp);
if (error)
goto error0;
bp = rbp;
@@ -3376,7 +3384,8 @@ xfs_btree_new_root(
xfs_btree_buf_to_ptr(cur, rbp, &rptr);
right = block;
xfs_btree_get_sibling(cur, right, &lptr, XFS_BB_LEFTSIB);
- error = xfs_btree_read_buf_block(cur, &lptr, 0, &left, &lbp);
+ error = xfs_btree_read_buf_block(cur, &lptr,
+ cur->bc_nlevels - 1, 0, &left, &lbp);
if (error)
goto error0;
bp = lbp;
@@ -4394,7 +4403,8 @@ xfs_btree_delrec(
rptr = cptr;
right = block;
rbp = bp;
- error = xfs_btree_read_buf_block(cur, &lptr, 0, &left, &lbp);
+ error = xfs_btree_read_buf_block(cur, &lptr, level, 0, &left,
+ &lbp);
if (error)
goto error0;

@@ -4411,7 +4421,8 @@ xfs_btree_delrec(
lptr = cptr;
left = block;
lbp = bp;
- error = xfs_btree_read_buf_block(cur, &rptr, 0, &right, &rbp);
+ error = xfs_btree_read_buf_block(cur, &rptr, level, 0, &right,
+ &rbp);
if (error)
goto error0;

@@ -4483,7 +4494,8 @@ xfs_btree_delrec(
/* If there is a right sibling, point it to the remaining block. */
xfs_btree_get_sibling(cur, left, &cptr, XFS_BB_RIGHTSIB);
if (!xfs_btree_ptr_is_null(cur, &cptr)) {
- error = xfs_btree_read_buf_block(cur, &cptr, 0, &rrblock, &rrbp);
+ error = xfs_btree_read_buf_block(cur, &cptr, level, 0,
+ &rrblock, &rrbp);
if (error)
goto error0;
xfs_btree_set_sibling(cur, rrblock, &lptr, XFS_BB_LEFTSIB);
diff --git a/fs/xfs/libxfs/xfs_btree.h b/fs/xfs/libxfs/xfs_btree.h
index 60e78572e..2153ee08d 100644
--- a/fs/xfs/libxfs/xfs_btree.h
+++ b/fs/xfs/libxfs/xfs_btree.h
@@ -650,7 +650,7 @@ int xfs_btree_get_buf_block(struct xfs_btree_cur *cur,
const union xfs_btree_ptr *ptr, struct xfs_btree_block **block,
struct xfs_buf **bpp);
int xfs_btree_read_buf_block(struct xfs_btree_cur *cur,
- const union xfs_btree_ptr *ptr, int flags,
+ const union xfs_btree_ptr *ptr, int level, int flags,
struct xfs_btree_block **block, struct xfs_buf **bpp);
void xfs_btree_set_sibling(struct xfs_btree_cur *cur,
struct xfs_btree_block *block, const union xfs_btree_ptr *ptr,
diff --git a/fs/xfs/libxfs/xfs_btree_staging.c b/fs/xfs/libxfs/xfs_btree_staging.c
index c3c7ea548..a8af4f834 100644
--- a/fs/xfs/libxfs/xfs_btree_staging.c
+++ b/fs/xfs/libxfs/xfs_btree_staging.c
@@ -385,6 +385,7 @@ xfs_btree_bload_leaf(
STATIC int
xfs_btree_bload_node(
struct xfs_btree_cur *cur,
+ unsigned int level,
unsigned int recs_this_block,
union xfs_btree_ptr *child_ptr,
struct xfs_btree_block *block)
@@ -407,8 +408,8 @@ xfs_btree_bload_node(
* been reclaimed. LRU refs will be set on the block, which is
* desirable if the new btree commits.
*/
- ret = xfs_btree_read_buf_block(cur, child_ptr, 0, &child_block,
- &child_bp);
+ ret = xfs_btree_read_buf_block(cur, child_ptr, level - 1, 0,
+ &child_block, &child_bp);
if (ret)
return ret;

@@ -767,8 +768,8 @@ xfs_btree_bload(
trace_xfs_btree_bload_block(cur, level, i, blocks,
&ptr, nr_this_block);

- ret = xfs_btree_bload_node(cur, nr_this_block,
- &child_ptr, block);
+ ret = xfs_btree_bload_node(cur, level, nr_this_block,
+ &child_ptr, block);
if (ret)
goto out;