From 49f771d14578733a350c45e34e1bc6ad14f56952 Mon Sep 17 00:00:00 2001 From: Hui Peng Date: Wed, 21 Oct 2026 13:46:18 +0200 Subject: [PATCH 01/16] udf: validate vat20->lengthHeader and VAT inode size in udf_load_vat() In udf_load_vat() (fs/udf/super.c), validate that vat20->lengthHeader is at least sizeof(struct virtualAllocationTable20) and does not exceed the mapped buffer or VAT inode size so computing the VAT table pointer and entry count cannot underflow or read out of bounds. Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Link: https://patch.msgid.link/20260919222600.3793513-1-benquike@gmail.com Signed-off-by: Hui Peng Signed-off-by: Jan Kara --- fs/udf/super.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/fs/udf/super.c b/fs/udf/super.c index 5351755aca3e..2335c31cde31 100644 --- a/fs/udf/super.c +++ b/fs/udf/super.c @@ -1252,6 +1252,12 @@ static int udf_load_vat(struct super_block *sb, int p_index, int type1_index) (sbi->s_vat_inode->i_size - 36) >> 2; } else if (map->s_partition_type == UDF_VIRTUAL_MAP20) { vati = UDF_I(sbi->s_vat_inode); + if (sbi->s_vat_inode->i_size < + sizeof(struct virtualAllocationTable20)) { + udf_err(sb, "Too short VAT inode size %lld\n", + sbi->s_vat_inode->i_size); + return -EFSCORRUPTED; + } if (vati->i_alloc_type != ICBTAG_FLAG_AD_IN_ICB) { int err = 0; @@ -1269,8 +1275,12 @@ static int udf_load_vat(struct super_block *sb, int p_index, int type1_index) map->s_type_specific.s_virtual.s_start_offset = le16_to_cpu(vat20->lengthHeader); - if (map->s_type_specific.s_virtual.s_start_offset - > sbi->s_vat_inode->i_size) { + if (map->s_type_specific.s_virtual.s_start_offset < + sizeof(struct virtualAllocationTable20) || + map->s_type_specific.s_virtual.s_start_offset > + sb->s_blocksize || + map->s_type_specific.s_virtual.s_start_offset > + sbi->s_vat_inode->i_size) { udf_err(sb, "Corrupted VAT header length %u (VAT inode size %lld)\n", map->s_type_specific.s_virtual.s_start_offset, sbi->s_vat_inode->i_size); From e21a6f39aa6d0336ffbf5f34e5fa20ff8de36773 Mon Sep 17 00:00:00 2001 From: YouYou Yu Date: Wed, 21 Oct 2026 18:12:17 +0000 Subject: [PATCH 02/16] quota: initialize dquot cache before mounting rootfs On some systems, a tmpfs backed rootfs is extensively used throughout system's lifecycle. tmpfs quotas are needed to limit per user rootfs usage. However, booting with CONFIG_TMPFS_QUOTA=y, an initramfs, no root= and "initramfs_options=usrquota" panics at boot: BUG: kernel NULL pointer dereference, address: 0000000000000000 RIP: 0010:dqget+0x1ac/0x490 Call Trace: __dquot_initialize dquot_initialize shmem_get_inode shmem_fill_super ... mnt_init vfs_caches_init start_kernel The rootfs is mounted from mnt_init(), but the dquot hash table and cache are only set up later by dquot_init(), an fs_initcall. Creating the root inode of a quota enabled rootfs thus dereferences the NULL dquot_hash in dqget(). Rename dquot_init() to dquot_cache_init() and call it from vfs_caches_init() before mnt_init(). The slab, percpu and early workqueue code it relies on is available there, and the workqueue rescuer and shrinker debugfs entries are set up later by their own subsystems. Keep the sysctl registration in an fs_initcall, as done for the dcache, inode and mount sysctls. This only moves existing allocations earlier in boot. Fixes: 278033a225e1 ("fs: Add 'initramfs_options' to set initramfs mount options") Cc: stable@vger.kernel.org Assisted-by: LLM Co-developed-by: Dylan Johnson Signed-off-by: Dylan Johnson Signed-off-by: YouYou Yu Reviewed-by: Gunnar Kudrjavets Link: https://patch.msgid.link/20261021181217.20171-1-youyouyu@amazon.com Signed-off-by: Jan Kara --- fs/dcache.c | 2 ++ fs/quota/dquot.c | 10 ++++++---- include/linux/quotaops.h | 5 +++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/fs/dcache.c b/fs/dcache.c index 37d021d22a7d..a9a919bc9197 100644 --- a/fs/dcache.c +++ b/fs/dcache.c @@ -33,6 +33,7 @@ #include #include #include +#include #include "internal.h" #include "mount.h" @@ -3683,6 +3684,7 @@ void __init vfs_caches_init(void) inode_init(); files_init(); files_maxfiles_init(); + dquot_cache_init(); mnt_init(); bdev_cache_init(); chrdev_init(); diff --git a/fs/quota/dquot.c b/fs/quota/dquot.c index 3ad19d7e472f..a3d503591871 100644 --- a/fs/quota/dquot.c +++ b/fs/quota/dquot.c @@ -2958,7 +2958,7 @@ static const struct ctl_table fs_dqstats_table[] = { }, }; -static int __init dquot_init(void) +void __init dquot_cache_init(void) { int i, ret; unsigned long nr_hash; @@ -2966,8 +2966,6 @@ static int __init dquot_init(void) printk(KERN_NOTICE "VFS: Disk quotas %s\n", __DQUOT_VERSION__); - register_sysctl_init("fs/quota", fs_dqstats_table); - dquot_cachep = kmem_cache_create("dquot", sizeof(struct dquot), sizeof(unsigned long) * 4, (SLAB_HWCACHE_ALIGN|SLAB_RECLAIM_ACCOUNT| @@ -3008,7 +3006,11 @@ static int __init dquot_init(void) WQ_UNBOUND | WQ_MEM_RECLAIM, WQ_MAX_ACTIVE); if (!quota_unbound_wq) panic("Cannot create quota_unbound_wq\n"); +} +static int __init init_fs_dquot_sysctls(void) +{ + register_sysctl_init("fs/quota", fs_dqstats_table); return 0; } -fs_initcall(dquot_init); +fs_initcall(init_fs_dquot_sysctls); diff --git a/include/linux/quotaops.h b/include/linux/quotaops.h index 0c64ca674e77..7346efc85d43 100644 --- a/include/linux/quotaops.h +++ b/include/linux/quotaops.h @@ -40,6 +40,7 @@ void __quota_error(struct super_block *sb, const char *func, /* * declaration of quota_function calls in kernel. */ +void dquot_cache_init(void); int dquot_initialize(struct inode *inode); bool dquot_initialize_needed(struct inode *inode); void dquot_drop(struct inode *inode); @@ -206,6 +207,10 @@ static inline int sb_has_quota_active(struct super_block *sb, int type) return 0; } +static inline void dquot_cache_init(void) +{ +} + static inline int dquot_initialize(struct inode *inode) { return 0; From 0415c2624f8be6eac60de2b298de195dd18802d8 Mon Sep 17 00:00:00 2001 From: Jan Kara Date: Fri, 23 Oct 2026 10:41:21 +0200 Subject: [PATCH 03/16] udf: Validate extended attribute space is large enough When there is extended attributes space in ICB, there must be at least enough space for the header of this space. Otherwise the fs is corrupted and other offset validity checks could underflow. Reported-by: Lennart Finke Fixes: 127b0d87649f ("udf: Avoid confusion of missing and corrupted extended attributes") Signed-off-by: Jan Kara --- fs/udf/inode.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fs/udf/inode.c b/fs/udf/inode.c index bacf2a86161e..88b1c7d351e1 100644 --- a/fs/udf/inode.c +++ b/fs/udf/inode.c @@ -1607,6 +1607,11 @@ static int udf_read_inode(struct inode *inode, bool hidden_inode) */ if (iinfo->i_lenEAttr > bs || iinfo->i_lenAlloc > bs) goto out; + /* We need to have space at least for generic descriptor */ + if (iinfo->i_lenEAttr && iinfo->i_lenEAttr < + sizeof(struct extendedAttrHeaderDesc)) + goto out; + /* Now do exact checks */ if (udf_file_entry_alloc_offset(inode) + iinfo->i_lenAlloc > bs) goto out; From afb28ca1f1ebe5279ffedd2fb24b92b8ee968351 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Fri, 23 Oct 2026 14:25:02 -0700 Subject: [PATCH 04/16] ext2: Make sbi an proper alias for EXT2_SB(sb) in ext2_fill_super() A recent LLVM change to fix parsing and validating __guarded_by on structure members [1] reveals a couple of warnings in ext2_fill_super() from the annotations added by commit 023051c2d0f1 ("ext2: mark s_mount_state as guarded by s_lock") and commit c23746524faf ("ext2: annotate s_rsv_window_root as requiring s_rsv_window_lock"): fs/ext2/super.c:1040:7: error: writing variable 's_mount_state' requires holding spinlock '_res->s_lock' exclusively [-Werror,-Wthread-safety-precise] 1040 | sbi->s_mount_state = le16_to_cpu(es->s_state); | ^ fs/ext2/super.c:1040:7: note: found near match 'EXT2_SB(sb).s_lock' fs/ext2/super.c:1138:8: error: writing variable 's_rsv_window_root' requires holding spinlock '_res->s_rsv_window_lock' exclusively [-Werror,-Wthread-safety-precise] 1138 | sbi->s_rsv_window_root = RB_ROOT; | ^ fs/ext2/super.c:1138:8: note: found near match 'EXT2_SB(sb).s_rsv_window_lock' Context analysis does very minimal alias detection [2], so it is not able to figure out that sbi == sb->s_fs_info == EXT2_SB(sb). The only reason there were no warnings before the above LLVM change is because the annotation was not parsed and verified correctly. Rework the allocation and assignment of sbi to have a direct assignment of EXT2_SB(sb) to sbi so that the analysis knows that sbi and EXT2_SB(sb) refer to the same object, clearing up the warnings. Fixes: 9708538ba204 ("ext2: enable context analysis support for ext2 filesystem") Link: https://github.com/llvm/llvm-project/commit/217e229a1f32785fccdd3b3f51429f0d309dea45 [1] Link: https://clang.llvm.org/docs/ThreadSafetyAnalysis.html#no-alias-analysis [2] Signed-off-by: Nathan Chancellor Reviewed-by: Marco Elver Link: https://patch.msgid.link/20261023-ext2-fix-context-analysis-clang-24-v2-1-a33961453899@kernel.org Signed-off-by: Jan Kara --- fs/ext2/super.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/fs/ext2/super.c b/fs/ext2/super.c index fb217ef2d69f..bde5b433776c 100644 --- a/fs/ext2/super.c +++ b/fs/ext2/super.c @@ -883,17 +883,18 @@ static int ext2_fill_super(struct super_block *sb, struct fs_context *fc) __le32 features; int err; - sbi = kzalloc_obj(*sbi); - if (!sbi) + sb->s_fs_info = kzalloc_obj(*sbi); + if (!sb->s_fs_info) return -ENOMEM; + sbi = EXT2_SB(sb); sbi->s_blockgroup_lock = kzalloc_obj(struct blockgroup_lock); if (!sbi->s_blockgroup_lock) { - kfree(sbi); + kfree(sb->s_fs_info); + sb->s_fs_info = NULL; return -ENOMEM; } - sb->s_fs_info = sbi; sbi->s_sb_block = sb_block; guard(spinlock_init)(&EXT2_SB(sb)->s_lock); From 5406ffab820ebad6637cca8737192449dcd680d5 Mon Sep 17 00:00:00 2001 From: Pedro Falcato Date: Sat, 24 Oct 2026 23:27:26 +0100 Subject: [PATCH 05/16] ext2: take invalidate_lock in ext2_write_failed() Currently, ext2_write_failed() takes care of removing extra data (folios & blocks) from the page cache by first truncating the page cache, then truncating the actual inode's blocks. Understanding that this is correct - namely, that a concurrent page cache lookup can't create stale pages and stale block mappings between truncate_pagecache() and ext2_truncate_blocks() - is non trivial. This is correct for two non-obvious reasons: 1) every caller of ext2_write_failed() holds some sort of exclusion against truncation. In most cases, callers hold (in some way) the inode lock. ext2_write_failed() is only reachable from buffered/direct IO write paths, which hold it. page_symlink() can also call write_begin _without the inode lock_, but at that point the inode is I_NEW, so there are no other users. 2) the page cache is generally extremely careful at never instantiating pages beyond EOF. This is all quite confusing. Holding the invalidate lock across both operations is largely simpler, and mirrors other bits of other filesystems (like ext4_truncate_failed_write). Do that. This has the side effect of making it so ext2_evict_inode() doesn't take the invalidate_lock when truncating blocks on inode deletion. This is safe as the inode should not have any users at that point. Signed-off-by: Pedro Falcato Link: https://patch.msgid.link/20261024222726.889520-1-pfalcato@suse.de Signed-off-by: Jan Kara --- fs/ext2/inode.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index 12ac4cfe1500..723b31dc7dac 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -58,8 +58,10 @@ void ext2_write_failed(struct address_space *mapping, loff_t to) struct inode *inode = mapping->host; if (to > inode->i_size) { + filemap_invalidate_lock(mapping); truncate_pagecache(inode, inode->i_size); ext2_truncate_blocks(inode, inode->i_size); + filemap_invalidate_unlock(mapping); } } @@ -1236,9 +1238,7 @@ static void ext2_truncate_blocks(struct inode *inode, loff_t offset) if (ext2_inode_is_fast_symlink(inode)) return; - filemap_invalidate_lock(inode->i_mapping); __ext2_truncate_blocks(inode, offset); - filemap_invalidate_unlock(inode->i_mapping); } static int ext2_setsize(struct inode *inode, loff_t newsize) From cbf04dde8b3c5a7e043e91cefad68b8980ea0d18 Mon Sep 17 00:00:00 2001 From: Hungyu Lin Date: Mon, 2 Nov 2026 06:15:55 +0000 Subject: [PATCH 06/16] ext2: preserve completed DIO bytes when buffered fallback fails ext2_dio_write_iter() can complete part of a direct write before falling back to buffered I/O for a hole inside i_size. If generic_perform_write() then fails, its error overwrites the completed direct-I/O byte count. For example, on a full filesystem, an 8 KiB write to a file with an allocated 4 KiB prefix and a 4 KiB hole overwrites the prefix but returns ENOSPC and leaves the file position unchanged. GNU dd with oflag=direct consequently reports zero bytes written. Preserve a positive direct-I/O result and return the buffered error when no bytes were direct-written. This restores the fallback policy of generic_file_write_iter(), which ext2 used before its iomap conversion. Fixes: fb5de4358e1a ("ext2: Move direct-io to use iomap") Cc: stable@vger.kernel.org Signed-off-by: Hungyu Lin Link: https://patch.msgid.link/20261102061555.55183-1-dennylin0707@gmail.com Signed-off-by: Jan Kara --- fs/ext2/file.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/ext2/file.c b/fs/ext2/file.c index 67fe423c3828..8ddc149d4884 100644 --- a/fs/ext2/file.c +++ b/fs/ext2/file.c @@ -145,7 +145,8 @@ static ssize_t ext2_dio_write_iter(struct kiocb *iocb, struct iov_iter *from) pos = iocb->ki_pos; status = generic_perform_write(iocb, from); if (unlikely(status < 0)) { - ret = status; + if (!ret) + ret = status; goto out_unlock; } From 3f57fc2c49ed05ccfc56e4dc9eda41be95925bbd Mon Sep 17 00:00:00 2001 From: Josef Bacik Date: Tue, 27 Oct 2026 18:43:14 +0000 Subject: [PATCH 07/16] ext2: only flush DIRSYNC directories on every change Every change of an ext2 directory is written to disk before the system call returns, whether the directory is DIRSYNC or not: its pages with filemap_write_and_wait() and its inode with sync_inode_metadata(). Writes which reach the device for 2000 files, and 200 directories, in a directory without DIRSYNC, with the flusher turned off: create rename unlink mkdir before 4000 8000 4000 800 after 0 0 0 0 In a directory with chattr +D the numbers are those of "before", before and after. commit bc943f4872a7 ("ext2: Don't flush page immediately for DIRSYNC directories") moved the flush from ext2_commit_chunk() to the new ext2_handle_dirsync(). The IS_DIRSYNC() check which was around it didn't move along. Fix by putting the check into ext2_handle_dirsync(). Fixes: bc943f4872a7 ("ext2: Don't flush page immediately for DIRSYNC directories") Signed-off-by: Josef Bacik Link: https://patch.msgid.link/20261027-b4-ext2-dirsync-v1-1-3dcd012d518d@toxicpanda.com Signed-off-by: Jan Kara --- fs/ext2/dir.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fs/ext2/dir.c b/fs/ext2/dir.c index e17bbc7598c1..5de32c221a83 100644 --- a/fs/ext2/dir.c +++ b/fs/ext2/dir.c @@ -442,6 +442,9 @@ static int ext2_handle_dirsync(struct inode *dir) { int err; + if (!IS_DIRSYNC(dir)) + return 0; + err = filemap_write_and_wait(dir->i_mapping); if (!err) err = sync_inode_metadata(dir, 1); From 0cbfc16fe49629f5c1fc3628405661c2b938ec61 Mon Sep 17 00:00:00 2001 From: Hungyu Lin Date: Thu, 29 Oct 2026 00:49:02 +0000 Subject: [PATCH 08/16] isofs: handle Rock Ridge long-form dates before 1900 iso_date() stores the year relative to 1900 and rejects negative values. Rock Ridge LONG_FORM timestamps encode calendar years from 1 to 9999, so valid years before 1900 are returned as seconds since the Unix epoch equal to zero instead of their recorded dates. For example, an 1899 timestamp on an image produced by xorriso is reported as 1970 by stat, and cp -p preserves that incorrect time. Use the calendar year for both timestamp formats and pass it directly to mktime64(). Keep the epoch fallback for year zero, while allowing valid long-form years before 1900. Short-form dates are unchanged. Fixes: 5ea45f54c8d6 ("isofs: fix Y2038 and Y2156 issues in Rock Ridge TF entry") Signed-off-by: Hungyu Lin Link: https://patch.msgid.link/20261029004902.76944-1-dennylin0707@gmail.com Signed-off-by: Jan Kara --- fs/isofs/util.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fs/isofs/util.c b/fs/isofs/util.c index 42f479da0b28..6f9a90e58a89 100644 --- a/fs/isofs/util.c +++ b/fs/isofs/util.c @@ -25,7 +25,7 @@ struct timespec64 iso_date(u8 *p, int flags) year = (p[0] - '0') * 1000 + (p[1] - '0') * 100 + (p[2] - '0') * 10 + - (p[3] - '0') - 1900; + (p[3] - '0'); month = ((p[4] - '0') * 10 + (p[5] - '0')); day = ((p[6] - '0') * 10 + (p[7] - '0')); hour = ((p[8] - '0') * 10 + (p[9] - '0')); @@ -34,7 +34,7 @@ struct timespec64 iso_date(u8 *p, int flags) ts.tv_nsec = ((p[14] - '0') * 10 + (p[15] - '0')) * 10000000; tz = p[16]; } else { - year = p[0]; + year = p[0] + 1900; month = p[1]; day = p[2]; hour = p[3]; @@ -45,10 +45,10 @@ struct timespec64 iso_date(u8 *p, int flags) tz = flags & ISO_DATE_HIGH_SIERRA ? 0 : p[6]; } - if (year < 0) { + if (year < 1) { ts.tv_sec = 0; } else { - ts.tv_sec = mktime64(year+1900, month, day, hour, minute, second); + ts.tv_sec = mktime64(year, month, day, hour, minute, second); /* sign extend */ if (tz & 0x80) From d4a6efef3e21b3b9cbcc48c34f270ff95d21a248 Mon Sep 17 00:00:00 2001 From: Jan Kara Date: Thu, 5 Nov 2026 09:30:43 +0100 Subject: [PATCH 09/16] udf, ext2, quota, jbd2: Unify email addresses in MAINTAINERS In MAINTAINERS file I was using two different email address. With the advent of various tools which use address in MAINTAINERS as an identifier (like Sashiko's pre-existing issues list) it is a bit annoying to remember which entry has what. Make all entries use the same address. Signed-off-by: Jan Kara --- MAINTAINERS | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MAINTAINERS b/MAINTAINERS index 1e3f9f2751fe..807bbaaa6fc4 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -7772,7 +7772,7 @@ W: http://www.win.tue.nl/~aeb/linux/zip/zip-1.html W: http://www.win.tue.nl/~aeb/partitions/partition_types-1.html DISKQUOTA -M: Jan Kara +M: Jan Kara S: Maintained F: Documentation/filesystems/quota.rst F: fs/quota/ @@ -10042,7 +10042,7 @@ F: drivers/leds/leds-expresswire.c F: include/linux/leds-expresswire.h EXT2 FILE SYSTEM -M: Jan Kara +M: Jan Kara L: linux-ext4@vger.kernel.org S: Maintained F: Documentation/filesystems/ext2.rst @@ -14236,7 +14236,7 @@ F: include/uapi/linux/jffs2.h JOURNALLING LAYER FOR BLOCK DEVICES (JBD2) M: "Theodore Ts'o" -M: Jan Kara +M: Jan Kara L: linux-ext4@vger.kernel.org S: Maintained F: fs/jbd2/ @@ -28512,7 +28512,7 @@ F: arch/m68k/coldfire/ F: arch/m68k/include/asm/*_no.* UDF FILESYSTEM -M: Jan Kara +M: Jan Kara S: Maintained F: Documentation/filesystems/udf.rst F: fs/udf/ From 4f91b042af212ca0b610e43f9a975ddc7f0d0923 Mon Sep 17 00:00:00 2001 From: Hungyu Lin Date: Thu, 29 Oct 2026 01:36:16 +0000 Subject: [PATCH 10/16] ext2: preserve IOMAP_F_DIRTY for extending direct writes ext2_iomap_begin() sets IOMAP_F_DIRTY for writes that may extend i_size, but clears the flag after ext2_get_blocks(). When the write uses an existing mapped block, iomap can then use FUA and skip generic_write_sync(), even though ext2_dio_write_end_io() updates the inode size. An O_DSYNC write can therefore return without persisting the new size. For example, on a FUA-capable device, a 512-byte O_DIRECT|O_DSYNC write at offset 512 into a fsynced one-byte file returns successfully and extends the in-memory size to 1024, while an immediate read of the on-disk inode still shows size 1. An explicit fsync persists the new size. Move setting of IOMAP_F_DIRTY after the initialization. Fixes: fb5de4358e1a ("ext2: Move direct-io to use iomap") Cc: stable@vger.kernel.org Signed-off-by: Hungyu Lin Link: https://patch.msgid.link/20261029013616.97194-1-dennylin0707@gmail.com Signed-off-by: Jan Kara --- fs/ext2/inode.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index 723b31dc7dac..1989258aef8a 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -807,6 +807,12 @@ static int ext2_iomap_begin(struct inode *inode, loff_t offset, loff_t length, (first_block << blkbits) < i_size_read(inode)) create = 0; + ret = ext2_get_blocks(inode, first_block, max_blocks, + &bno, &new, &boundary, create); + if (ret < 0) + return ret; + + iomap->flags = 0; /* * Writes that span EOF might trigger an IO size update on completion, * so consider them to be dirty for the purposes of O_DSYNC even if @@ -814,13 +820,6 @@ static int ext2_iomap_begin(struct inode *inode, loff_t offset, loff_t length, */ if ((flags & IOMAP_WRITE) && offset + length > i_size_read(inode)) iomap->flags |= IOMAP_F_DIRTY; - - ret = ext2_get_blocks(inode, first_block, max_blocks, - &bno, &new, &boundary, create); - if (ret < 0) - return ret; - - iomap->flags = 0; iomap->offset = (u64)first_block << blkbits; iomap->bdev = inode->i_sb->s_bdev; From da582d84fcf5f2f0c1d7760e1a899ca7cedb80fc Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Fri, 30 Oct 2026 22:05:34 +0800 Subject: [PATCH 11/16] udf: don't let the extent type hide an exhausted free-space table extent udf_table_new_block() takes the first block of the free-space table extent closest to the goal. It keeps that extent's type and length together in goal_elen, subtracts one block, and deletes the extent only if goal_elen then reaches zero. UDF 2.60 section 2.3.7.1 requires the free-space extents of an Unallocated Space Entry to be of type 1 (allocated but not recorded), and mkudffs writes them that way. For such an extent the type bits keep goal_elen non-zero, so taking its last block leaves a type-1 extent of length zero behind, starting at the block after the extent, which is in use. The next allocation that picks this empty extent returns that in-use block. Subtracting a block from 0x40000000 then borrows from the type bits, and the extent becomes type 0 with a length of 2^30 - blocksize: about a gigabyte of "free" space overlapping live metadata, the other table extents and whatever lies behind the partition. From then on blocks are handed out twice, or past the end of the partition. Extents that udf_table_free_blocks() adds are type 0, where the check works, so only tables written by mkudffs or by another implementation are affected. Nothing more than filling such a filesystem is needed: on a fresh 1 MiB "mkudffs --space=unalloctable" image, creating empty files until the partition is full writes file entries over the reserve volume descriptor sequence and the backup anchor, and df then reports a negative amount of used space. On syzbot's images the same double allocation is what the two reports below trip over. In the first, ftruncate() extends a new file with enough hole extents to need a chain of allocation extent descriptors; one AED is placed on another inode's file entry, and a later one is placed on the block of the AED currently being filled, which udf_setup_indirect_aext() zeroes, so __udf_add_aext() finds lengthAllocDescs out of step with its cursor. In the second, an AED is placed past the end of the device, sb_getblk() fails, and the error path of udf_do_extend_file() calls udf_truncate_extents() on an extent list that no longer covers i_size. Keep the type separately from the length, as udf_table_prealloc_blocks() already does. Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Cc: stable@vger.kernel.org Reported-by: syzbot+799a0e744ac47f928024@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=799a0e744ac47f928024 Reported-by: syzbot+43fc5ba6dcb33e3261ca@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=43fc5ba6dcb33e3261ca Signed-off-by: Matthias Goergens Link: https://patch.msgid.link/6b51c38d2d069544053c542470950e2a448b0b8d.1793349907.git.matthias.goergens@gmail.com Signed-off-by: Jan Kara --- fs/udf/balloc.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/fs/udf/balloc.c b/fs/udf/balloc.c index 30cec5600149..2ec577b4321c 100644 --- a/fs/udf/balloc.c +++ b/fs/udf/balloc.c @@ -572,7 +572,7 @@ static udf_pblk_t udf_table_new_block(struct super_block *sb, uint32_t elen, goal_elen = 0; struct kernel_lb_addr eloc, goal_eloc; struct extent_position epos, goal_epos; - int8_t etype; + int8_t etype, goal_etype = 0; struct udf_inode_info *iinfo = UDF_I(table); int ret = 0; @@ -623,7 +623,8 @@ static udf_pblk_t udf_table_new_block(struct super_block *sb, goal_epos.block = epos.block; goal_epos.offset = epos.offset - adsize; goal_eloc = eloc; - goal_elen = (etype << 30) | elen; + goal_elen = elen; + goal_etype = etype; } } @@ -647,7 +648,8 @@ static udf_pblk_t udf_table_new_block(struct super_block *sb, goal_elen -= sb->s_blocksize; if (goal_elen) - udf_write_aext(table, &goal_epos, &goal_eloc, goal_elen, 1); + udf_write_aext(table, &goal_epos, &goal_eloc, + (goal_etype << 30) | goal_elen, 1); else udf_delete_aext(table, goal_epos, &freed); brelse(goal_epos.bh); From e614c3e35a6699ed691b4d17e1ec4230e34aa3eb Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Fri, 30 Oct 2026 22:05:35 +0800 Subject: [PATCH 12/16] udf: check the unallocated space table when it is loaded udf_table_new_block() hands out the first block of the extent closest to its goal and trusts that the extent covers at least one whole block inside the partition. Nothing checks that. A zero-length extent makes it return the block the extent points at, which need not be free, and the subtraction of one block then underflows the length into the type bits, turning the entry into a bogus continuation. An extent running past the end of the partition makes it return blocks behind the partition. Such tables are not only found on crafted images: until the previous commit, udf_table_new_block() itself left zero-length extents behind in tables written by mkudffs, and then converted them into extents running past the partition. Check every extent once when the table is loaded, and refuse read-write access if one is empty, is not a whole number of blocks, or does not lie inside the partition, in the same way as for other allocation information the kernel cannot use. Check first that the table is an Unallocated Space Entry at all: the walk starts at the offset of the allocation descriptors in one, and for a file entry that offset lies before the inode's in-memory copy of the descriptors. Treat a chain of allocation extent descriptors that leads back to itself the same way: the walk would otherwise return the same extents forever, and the mount would never finish. Let a fatal signal interrupt the walk and fail the mount. Refusing read-write access to an empty extent is deliberate. It shows that an old kernel took the last block but left the entry behind. If consumed again, the entry returns an in-use block and turns into a huge bogus extent. Refusing the table before that happens avoids this second allocation; the filesystem can still be mounted read-only. The check cannot detect every overlap that may already exist in a damaged table. This patch should be backported together with the previous one: that patch stops creating new empty extents, and this one rejects existing ones before they are consumed again. Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Cc: stable@vger.kernel.org Signed-off-by: Matthias Goergens Link: https://patch.msgid.link/81233decd91f4a48bf4d74e3d15b547c7711e39f.1793349907.git.matthias.goergens@gmail.com Signed-off-by: Jan Kara --- fs/udf/super.c | 80 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/fs/udf/super.c b/fs/udf/super.c index 2335c31cde31..a81a8509524a 100644 --- a/fs/udf/super.c +++ b/fs/udf/super.c @@ -1106,6 +1106,73 @@ static int check_partition_desc(struct super_block *sb, return 0; } +/* + * Check the Unallocated Space Table once when it is loaded: the allocator + * hands out blocks from the start of each extent and trusts that every + * extent covers at least one whole block inside the partition. + */ +static int udf_check_unalloc_table(struct super_block *sb, + struct inode *table, u32 partition_len) +{ + struct extent_position epos = { + .block = UDF_I(table)->i_location, + .offset = sizeof(struct unallocSpaceEntry), + }; + struct kernel_lb_addr eloc; + uint32_t elen, blocks; + struct kernel_lb_addr seen_block = {}; + uint32_t seen_offset = 0; + u64 steps = 0, period = 1; + int8_t etype; + int ret; + + /* The walk below assumes the layout of an Unallocated Space Entry */ + if (!UDF_I(table)->i_use) { + udf_err(sb, "unallocated space table is not an unallocated space entry\n"); + return -EFSCORRUPTED; + } + + while ((ret = udf_next_aext(table, &epos, &eloc, &elen, &etype, 1)) > 0) { + blocks = elen >> sb->s_blocksize_bits; + if (!blocks || (elen & (sb->s_blocksize - 1)) || + eloc.logicalBlockNum >= partition_len || + blocks > partition_len - eloc.logicalBlockNum) { + udf_err(sb, "invalid unallocated space table extent (block %u, length %u)\n", + eloc.logicalBlockNum, elen); + ret = -EFSCORRUPTED; + break; + } + /* + * A chain of allocation extents can lead back to itself, and + * then the walk returns the same extents forever. Remember a + * position at doubling intervals; a walk that comes back to it + * is in a loop. + */ + if (epos.block.logicalBlockNum == seen_block.logicalBlockNum && + epos.block.partitionReferenceNum == + seen_block.partitionReferenceNum && + epos.offset == seen_offset) { + udf_err(sb, "unallocated space table loops back on itself\n"); + ret = -EFSCORRUPTED; + break; + } + if (++steps == period) { + seen_block = epos.block; + seen_offset = epos.offset; + steps = 0; + period <<= 1; + } + if (fatal_signal_pending(current)) { + udf_err(sb, "interrupted while checking the unallocated space table\n"); + ret = -EINTR; + break; + } + cond_resched(); + } + brelse(epos.bh); + return ret; +} + static int udf_fill_partdesc_info(struct super_block *sb, struct partitionDesc *p, int p_index) { @@ -1166,6 +1233,16 @@ static int udf_fill_partdesc_info(struct super_block *sb, p_index); return PTR_ERR(inode); } + err = udf_check_unalloc_table(sb, inode, map->s_partition_len); + if (err) { + iput(inode); + if (err == -EINTR) + return err; + if (!sb_rdonly(sb)) + return -EACCES; + UDF_SET_FLAG(sb, UDF_FLAG_RW_INCOMPAT); + return 0; + } map->s_uspace.s_table = inode; map->s_partition_flags |= UDF_PART_FLAG_UNALLOC_TABLE; udf_debug("unallocSpaceTable (part %d) @ %llu\n", @@ -2243,8 +2320,9 @@ static int udf_fill_super(struct super_block *sb, struct fs_context *fc) /* * EACCES is special - we want to propagate to * upper layers that we cannot handle RW mount. + * EINTR means that a fatal signal is pending. */ - if (ret == -EACCES) + if (ret == -EACCES || ret == -EINTR) break; } else break; From 0c52750caaaccee8b0e929450fe6538ec4985722 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Fri, 30 Oct 2026 22:05:36 +0800 Subject: [PATCH 13/16] udf: fix preallocation discarding before an empty allocation extent udf_discard_prealloc() finds the last extent of a file by peeking at the next one with udf_next_aext() and stopping when that returns 0. The peek reads into eloc and elen, and the code then uses them as the last extent. But when udf_next_aext() returns 0 they hold no defined value: if the extent list ends in a continuation descriptor that leads to an empty allocation extent, they describe that continuation, i.e. the block of the empty allocation extent, one block long. The kernel creates such lists itself: when udf_delete_aext() removes the only descriptor of the last allocation extent in a list, it leaves that allocation extent empty and the continuation to it in place. If the last extent is a preallocation, udf_discard_prealloc() deletes it with udf_delete_aext(), which finds the following allocation extent empty, so it also removes the continuation and frees that allocation extent's block. udf_discard_prealloc() then frees eloc and elen: that block a second time, instead of the preallocated blocks. On a space bitmap the preallocated blocks leak and the free block count drifts. On an unallocated space table the second free adds a second free extent for the same block, which is later handed out twice: fsx as run by generic/091 and generic/263 ends up with two parts of its test file in one block and reads back bad data. Peek into separate variables, so that eloc and elen keep the last extent that udf_next_aext() returned with 1. Fixes: cfe4c1b25dd6 ("udf: Fix preallocation discarding at indirect extent boundary") Cc: stable@vger.kernel.org Suggested-by: Jan Kara Signed-off-by: Matthias Goergens Link: https://patch.msgid.link/480eccd1264a4cea5226fc36ed108931b2c403fb.1793349907.git.matthias.goergens@gmail.com Signed-off-by: Jan Kara --- fs/udf/truncate.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/fs/udf/truncate.c b/fs/udf/truncate.c index 0990f94b8551..75151a59f0da 100644 --- a/fs/udf/truncate.c +++ b/fs/udf/truncate.c @@ -124,8 +124,8 @@ void udf_discard_prealloc(struct inode *inode) { struct extent_position epos = {}; struct extent_position prev_epos = {}; - struct kernel_lb_addr eloc; - uint32_t elen; + struct kernel_lb_addr eloc, tmpeloc; + uint32_t elen, tmpelen; uint64_t lbcount = 0; int8_t etype = -1; struct udf_inode_info *iinfo = UDF_I(inode); @@ -141,7 +141,8 @@ void udf_discard_prealloc(struct inode *inode) /* Find the last extent in the file */ while (1) { - ret = udf_next_aext(inode, &epos, &eloc, &elen, &tmpetype, 0); + ret = udf_next_aext(inode, &epos, &tmpeloc, &tmpelen, + &tmpetype, 0); if (ret < 0) goto out; if (ret == 0) From 94401199df90e1c2a50c6e4b73618ac5a4ba83ba Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Fri, 30 Oct 2026 22:05:37 +0800 Subject: [PATCH 14/16] udf: don't use eloc after the end of the unallocated space table When udf_next_aext() returns 0, its eloc, elen and etype outputs hold no defined value. Two walks of the unallocated space table still use eloc after the walk has reached the end of the table. udf_table_prealloc_blocks() looks for a free extent starting at first_block and decides whether it found one by comparing eloc with first_block after the walk. If the table's list ends in a continuation descriptor that leads to an empty allocation extent, eloc is then the block of that allocation extent, which is in use, and if that is first_block, the block is handed out as preallocation. Once udf_discard_prealloc() no longer frees the wrong block, this is what still makes fsx in generic/091 and generic/263 read back bad data on a table, in about one run in four. Only take the extent if the walk stopped at it. When udf_table_free_blocks() cannot merge the freed blocks into an existing extent, it appends a new one and sets only its block number, so the partition reference comes from whatever eloc held after the walk, and with long allocation descriptors it is written to disk. For a table without extents, eloc was never set. Take the partition reference from the freed blocks instead. Short allocation descriptors do not store the partition reference, which is why this half remains a latent long-descriptor fix rather than a reproduced failure. Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Cc: stable@vger.kernel.org Signed-off-by: Matthias Goergens Link: https://patch.msgid.link/fd9ca4c4ba98c00c012ec861f6c0bc08a3b1d07b.1793349907.git.matthias.goergens@gmail.com Signed-off-by: Jan Kara --- fs/udf/balloc.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/udf/balloc.c b/fs/udf/balloc.c index 2ec577b4321c..75124188e745 100644 --- a/fs/udf/balloc.c +++ b/fs/udf/balloc.c @@ -457,6 +457,7 @@ static void udf_table_free_blocks(struct super_block *sb, int adsize; + eloc.partitionReferenceNum = bloc->partitionReferenceNum; eloc.logicalBlockNum = start; elen = EXT_RECORDED_ALLOCATED | (count << sb->s_blocksize_bits); @@ -532,7 +533,7 @@ static int udf_table_prealloc_blocks(struct super_block *sb, eloc.logicalBlockNum, elen, first_block); } - if (first_block == eloc.logicalBlockNum) { + if (ret > 0 && first_block == eloc.logicalBlockNum) { epos.offset -= adsize; alloc_count = (elen >> sb->s_blocksize_bits); From a3b98efdb320e62ec0f89caaa67993a2d1c178be Mon Sep 17 00:00:00 2001 From: Jan Kara Date: Thu, 5 Nov 2026 15:09:19 +0100 Subject: [PATCH 15/16] udf: Simplify udf_table_prealloc_blocks() Simplify handling when the specified extent is not found in the table of free blocks. Signed-off-by: Jan Kara --- fs/udf/balloc.c | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/fs/udf/balloc.c b/fs/udf/balloc.c index 75124188e745..b6551cc7c6cf 100644 --- a/fs/udf/balloc.c +++ b/fs/udf/balloc.c @@ -525,30 +525,22 @@ static int udf_table_prealloc_blocks(struct super_block *sb, while (first_block != eloc.logicalBlockNum) { ret = udf_next_aext(table, &epos, &eloc, &elen, &etype, 1); - if (ret < 0) + if (ret <= 0) goto err_out; - if (ret == 0) - break; udf_debug("eloc=%u, elen=%u, first_block=%u\n", eloc.logicalBlockNum, elen, first_block); } - if (ret > 0 && first_block == eloc.logicalBlockNum) { - epos.offset -= adsize; - - alloc_count = (elen >> sb->s_blocksize_bits); - if (alloc_count > block_count) { - alloc_count = block_count; - eloc.logicalBlockNum += alloc_count; - elen -= (alloc_count << sb->s_blocksize_bits); - udf_write_aext(table, &epos, &eloc, - (etype << 30) | elen, 1); - } else - udf_delete_aext(table, epos, &freed); - } else { - alloc_count = 0; - } + epos.offset -= adsize; + alloc_count = (elen >> sb->s_blocksize_bits); + if (alloc_count > block_count) { + alloc_count = block_count; + eloc.logicalBlockNum += alloc_count; + elen -= (alloc_count << sb->s_blocksize_bits); + udf_write_aext(table, &epos, &eloc, (etype << 30) | elen, 1); + } else + udf_delete_aext(table, epos, &freed); err_out: brelse(epos.bh); From e5801273f93eb7513850b1c38ea14d69f2de1592 Mon Sep 17 00:00:00 2001 From: Jan Kara Date: Thu, 5 Nov 2026 15:11:44 +0100 Subject: [PATCH 16/16] udf: Fix incorrect error code in udf_current_aext() There are two places in udf_current_aext() that return -1 instead of proper error code. Fix them. Signed-off-by: Jan Kara --- fs/udf/inode.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/udf/inode.c b/fs/udf/inode.c index 88b1c7d351e1..c425096a22ba 100644 --- a/fs/udf/inode.c +++ b/fs/udf/inode.c @@ -2330,10 +2330,10 @@ int udf_current_aext(struct inode *inode, struct extent_position *epos, ptr = epos->bh->b_data + epos->offset; if (check_add_overflow(sizeof(struct allocExtDesc), le32_to_cpu(header->lengthAllocDescs), &alen)) - return -1; + return -EFSCORRUPTED; if (alen > epos->bh->b_size) - return -1; + return -EFSCORRUPTED; } switch (iinfo->i_alloc_type) {