Every ocfs2 node keeps a local quota file made of chunks. A chunk is a header block, whose bitmap marks the entries in use, followed by up to ol_chunk_blocks() blocks of entries, and only the last chunk may be partly filled. The file header records the number of chunks in dqi_chunks and the number of blocks in dqi_blocks. When no chunk has a free entry, ocfs2_extend_local_quota_file() works out from dqi_blocks how many entry blocks the last chunk already has, and puts the new entry right after them: chunk_blocks = oinfo->dqi_blocks - ol_quota_chunk_block(sb, chunk->qc_num) - 1; ... *offset = chunk_blocks * epb; olq_alloc_dquot() then sets that bit in the chunk's bitmap, which fits in a single block. This is only safe while chunk_blocks is at most ol_chunk_blocks(). ocfs2_local_read_info() reads dqi_chunks and dqi_blocks from disk at mount time and never checks that they agree. With one chunk and a dqi_blocks of a few thousand, the last chunk appears to hold thousands of entry blocks, and the bit lands kilobytes past the end of the bitmap. A dqi_blocks below the last chunk's header block makes chunk_blocks, an unsigned int, wrap around to the same effect. The stray write corrupts unrelated kernel memory. KASAN calls it a use-after-free only because the page it hits happens to be free; nothing in ocfs2 was freed. Reaching it takes mounting a crafted image with quotas enabled, which needs CAP_SYS_ADMIN, and creating a file. The issue was found by fuzzing and not seen in production, the reproducer can be found in the linked report. Check in ocfs2_local_read_info() that the last chunk has its header block and at most ol_chunk_blocks() entry blocks, and that a file without chunks holds only its info block, since the first new chunk is placed at block dqi_blocks. Refuse the file with ocfs2_error() otherwise, as ocfs2_read_quota_block() does for a corrupted quota file. The header block of the last chunk is computed in 64 bits because a crafted dqi_chunks can wrap ol_quota_chunk_block(). Fixes: 9e33d69f553a ("ocfs2: Implementation of local and global quota file handling") Closes: https://lore.kernel.org/lkml/CANypQFbjcxXjiigMCCyErN=DSEh3ZG3NMWRpq=Hic8KJ76cwew@mail.gmail.com/ Cc: stable@vger.kernel.org Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Jiaming Zhang --- fs/ocfs2/quota_local.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/fs/ocfs2/quota_local.c b/fs/ocfs2/quota_local.c index f55810c59b1b..bad913d8bc4f 100644 --- a/fs/ocfs2/quota_local.c +++ b/fs/ocfs2/quota_local.c @@ -678,6 +678,30 @@ int ocfs2_finish_quota_recovery(struct ocfs2_super *osb, return status; } +/* Check that dqi_blocks agrees with dqi_chunks in local quota file info */ +static int ocfs2_local_check_info(struct super_block *sb, struct inode *lqinode, + struct ocfs2_mem_dqinfo *oinfo) +{ + u64 hdr_blk; + + if (!oinfo->dqi_chunks) { + if (oinfo->dqi_blocks == ol_quota_chunk_block(sb, 0)) + return 0; + } else { + /* Same as ol_quota_chunk_block(), but in 64 bits */ + hdr_blk = 1 + (u64)(ol_chunk_blocks(sb) + 1) * + (oinfo->dqi_chunks - 1); + if (oinfo->dqi_blocks > hdr_blk && + oinfo->dqi_blocks - hdr_blk - 1 <= ol_chunk_blocks(sb)) + return 0; + } + + return ocfs2_error(sb, + "corrupted local quota file %llu (chunks=%u blocks=%u)\n", + (u64)OCFS2_I(lqinode)->ip_blkno, + oinfo->dqi_chunks, oinfo->dqi_blocks); +} + /* Read information header from quota file */ static int ocfs2_local_read_info(struct super_block *sb, int type) { @@ -733,6 +757,10 @@ static int ocfs2_local_read_info(struct super_block *sb, int type) oinfo->dqi_blocks = le32_to_cpu(ldinfo->dqi_blocks); oinfo->dqi_libh = bh; + status = ocfs2_local_check_info(sb, lqinode, oinfo); + if (status) + goto out_err; + /* We crashed when using local quota file? */ if (!(oinfo->dqi_flags & OLQF_CLEAN)) { rec = OCFS2_SB(sb)->quota_rec;