From 54c8f8e59040411ef4e72a599a6019fc83752497 Mon Sep 17 00:00:00 2001 From: Zhihao Cheng Date: Thu, 22 Oct 2026 21:26:03 +0800 Subject: [PATCH] udf: Fix zero-length extent handling in udf_table_new_block() udf_table_new_block() keeps the remaining extent length together with the extent type in goal_elen and then uses the whole value to decide whether the extent is empty: goal_elen = (etype << 30) | elen; ... goal_elen -= sb->s_blocksize; if (goal_elen) udf_write_aext(...); else udf_delete_aext(...); On a table-managed partition, free extents may have non-zero type bits. If such an extent has exactly one block left, subtracting the block leaves only the type bits (e.g. 0x40000000 for type 1). The test is still true, so instead of deleting the extent the function writes a zero-length extent at the next block back into the unallocated space table. Consider a free extent with type 1 and 9 blocks: lb=55, len=9 blocks (55..63) A new file is allocated from it at lb=55, so the extent becomes "lb=56, len=8 blocks". A huge write then extends the file with a hole. After the in-ICB allocation descriptor area fills, AED blocks 56..63 are allocated from this extent. When lb=63 is allocated, goal_elen is 0x40000200; after subtracting one block it becomes 0x40000000, so the extent is rewritten as lb=64, len=0, type=1 instead of being deleted. The next allocation with goal=63 prefers this zero-length extent (distance 1) over the real next extent "lb=65, len=31 blocks" (distance 2) and allocates lb=64 from it. Subtracting a block from the zero-length value borrows from the type field: 0x40000000 - 0x200 = 0x3FFFFE00 and writes back a bogus ~1GB extent: lb=65, len=0x3FFFFE00 which now overlaps the real free extent "lb=65, len=31 blocks". The next allocation with goal=64 picks the bogus extent first (both start at 65, the bogus one is encountered first and the search keeps the first minimum), so lb=65 is handed out and the bogus extent advances to lb=66. lb=65 is now the file's current AED. After 60 short_ad descriptors are appended to it, epos->offset is 504 and aed->lengthAllocDescs is 480. The next append enters udf_setup_indirect_aext(). The allocation goal is 65, and now the real extent "lb=65, len=31 blocks" wins: the goal lies inside it (distance 0), while the bogus extent has advanced to lb=66 (distance 1). udf_new_block() therefore returns 65 again, i.e. the block that is already the current AED. udf_setup_indirect_aext() then does: sb_getblk() returns the buffer of the current AED memset(bh->b_data, 0, sb->s_blocksize) -> clears aed->lengthAllocDescs Because epos->offset is still 504, the block is not considered full (504 + 8 <= 512) and the else branch calls __udf_add_aext(), which hits WARN_ON(aed->lengthAllocDescs != epos->offset - sizeof(struct allocExtDesc)) with lengthAllocDescs == 0 and offset == 504, i.e. 0 != 480. The AED contents are destroyed and the filesystem is corrupted. This is reachable by mounting a table-based UDF image with adinicb,shortad and writing at a very large offset: the write creates a huge hole, fills the in-ICB allocation descriptor area and then allocates many AED blocks from the unallocated space table. Reproducer is https://syzkaller.appspot.com/x/repro.c?x=16562e75180000. Fix it by testing only the extent length field. Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Reported-by: syzbot+799a0e744ac47f928024@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=799a0e744ac47f928024 Signed-off-by: Zhihao Cheng --- fs/udf/balloc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/udf/balloc.c b/fs/udf/balloc.c index 30cec56001491..621656be65d2f 100644 --- a/fs/udf/balloc.c +++ b/fs/udf/balloc.c @@ -646,7 +646,7 @@ static udf_pblk_t udf_table_new_block(struct super_block *sb, goal_eloc.logicalBlockNum++; goal_elen -= sb->s_blocksize; - if (goal_elen) + if (goal_elen & UDF_EXTENT_LENGTH_MASK) udf_write_aext(table, &goal_epos, &goal_eloc, goal_elen, 1); else udf_delete_aext(table, goal_epos, &freed);