From 5f0419457f89dce1a3f1c8e62a3adf2f39ab8168 Mon Sep 17 00:00:00 2001 From: Michael Bommarito Date: Fri, 15 May 2026 10:23:27 -0400 Subject: [PATCH 1/9] udf: validate free block extents against the partition length udf_free_blocks() checks the logical block number and count against the partition length, but drops the extent offset from that final bound. A crafted extent can pass the guard while logicalBlockNum + offset + count points past the partition, which later indexes past the space bitmap array. A single ftruncate(2) on a file backed by such an extent reliably panics the kernel. This is a local availability issue. On desktop systems where UDisks/polkit allows the active user to mount removable UDF media without CAP_SYS_ADMIN, an unprivileged local user can supply the crafted filesystem and trigger the panic by truncating a writable file on it. Systems that require root or CAP_SYS_ADMIN to mount the image have a higher prerequisite. No confidentiality or integrity impact is claimed: the reproduced primitive is an out-of-bounds read of a bitmap pointer slot followed by a kernel panic. Use the already computed logicalBlockNum + offset + count value for the partition length check. Also make load_block_bitmap() reject an out-of-range block group before indexing s_block_bitmap[], so corrupted callers cannot walk past the flexible array. Fixes: 56e69e59751d ("udf: prevent integer overflow in udf_bitmap_free_blocks()") Cc: stable@vger.kernel.org Assisted-by: Claude:claude-opus-4-7 Signed-off-by: Michael Bommarito Link: https://patch.msgid.link/20260515142327.1120767-1-michael.bommarito@gmail.com Signed-off-by: Jan Kara --- fs/udf/balloc.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/udf/balloc.c b/fs/udf/balloc.c index 807c493ed0cd..cc6dc6e1d84d 100644 --- a/fs/udf/balloc.c +++ b/fs/udf/balloc.c @@ -82,8 +82,9 @@ static int load_block_bitmap(struct super_block *sb, int nr_groups = bitmap->s_nr_groups; if (block_group >= nr_groups) { - udf_debug("block_group (%u) > nr_groups (%d)\n", + udf_debug("block_group (%u) >= nr_groups (%d)\n", block_group, nr_groups); + return -EFSCORRUPTED; } if (bitmap->s_block_bitmap[block_group]) { @@ -662,7 +663,7 @@ void udf_free_blocks(struct super_block *sb, struct inode *inode, if (check_add_overflow(bloc->logicalBlockNum, offset, &blk) || check_add_overflow(blk, count, &blk) || - bloc->logicalBlockNum + count > map->s_partition_len) { + blk > map->s_partition_len) { udf_debug("Invalid request to free blocks: (%d, %u), off %u, " "len %u, partition len %u\n", partition, bloc->logicalBlockNum, offset, count, From 6d06b04c2b8dc0e02920336c9eb4312585fb1492 Mon Sep 17 00:00:00 2001 From: "Mike Rapoport (Microsoft)" Date: Sat, 23 May 2026 20:54:13 +0300 Subject: [PATCH 2/9] quota: allocate dquot_hash with kmalloc() dquot_init() allocates a single page for dquot_hash with __get_free_pages(). kmalloc() is a better API for such use and it also provides better scalability and more debugging possibilities. Replace use of __get_free_pages() with kmalloc() and get rid of the order variable that remained 0 for more than 20 years. Signed-off-by: Mike Rapoport (Microsoft) Link: https://patch.msgid.link/20260523-b4-fs-v1-1-275e36a83f0e@kernel.org Signed-off-by: Jan Kara --- fs/quota/dquot.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/fs/quota/dquot.c b/fs/quota/dquot.c index 64cf42721496..9850de3955d3 100644 --- a/fs/quota/dquot.c +++ b/fs/quota/dquot.c @@ -3022,7 +3022,7 @@ static const struct ctl_table fs_dqstats_table[] = { static int __init dquot_init(void) { int i, ret; - unsigned long nr_hash, order; + unsigned long nr_hash; struct shrinker *dqcache_shrinker; printk(KERN_NOTICE "VFS: Disk quotas %s\n", __DQUOT_VERSION__); @@ -3035,8 +3035,7 @@ static int __init dquot_init(void) SLAB_PANIC), NULL); - order = 0; - dquot_hash = (struct hlist_head *)__get_free_pages(GFP_KERNEL, order); + dquot_hash = kmalloc(PAGE_SIZE, GFP_KERNEL); if (!dquot_hash) panic("Cannot create dquot hash table"); @@ -3046,7 +3045,7 @@ static int __init dquot_init(void) panic("Cannot create dquot stat counters"); /* Find power-of-two hlist_heads which can fit into allocation */ - nr_hash = (1UL << order) * PAGE_SIZE / sizeof(struct hlist_head); + nr_hash = PAGE_SIZE / sizeof(struct hlist_head); dq_hash_bits = ilog2(nr_hash); nr_hash = 1UL << dq_hash_bits; @@ -3054,8 +3053,8 @@ static int __init dquot_init(void) for (i = 0; i < nr_hash; i++) INIT_HLIST_HEAD(dquot_hash + i); - pr_info("VFS: Dquot-cache hash table entries: %ld (order %ld," - " %ld bytes)\n", nr_hash, order, (PAGE_SIZE << order)); + pr_info("VFS: Dquot-cache hash table entries: %ld (%ld bytes)\n", + nr_hash, PAGE_SIZE); dqcache_shrinker = shrinker_alloc(0, "dquota-cache"); if (!dqcache_shrinker) From 9ddaf06cb08b4ea0eec8b9307d5d822fbb111461 Mon Sep 17 00:00:00 2001 From: "Mike Rapoport (Microsoft)" Date: Sat, 23 May 2026 20:54:23 +0300 Subject: [PATCH 3/9] isofs: replace __get_free_page() with kmalloc() isofs_readdir() allocates a temporary buffer with __get_free_page(). kmalloc() is a better API for such use and it also provides better scalability and more debugging possibilities. Replace use of __get_free_page() with kmalloc(). Signed-off-by: Mike Rapoport (Microsoft) Link: https://patch.msgid.link/20260523-b4-fs-v1-11-275e36a83f0e@kernel.org Signed-off-by: Jan Kara --- fs/isofs/dir.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/isofs/dir.c b/fs/isofs/dir.c index 2fd9948d606e..6d220eab531e 100644 --- a/fs/isofs/dir.c +++ b/fs/isofs/dir.c @@ -13,6 +13,7 @@ */ #include #include +#include #include "isofs.h" int isofs_name_translate(struct iso_directory_record *de, char *new, struct inode *inode) @@ -255,7 +256,7 @@ static int isofs_readdir(struct file *file, struct dir_context *ctx) struct iso_directory_record *tmpde; struct inode *inode = file_inode(file); - tmpname = (char *)__get_free_page(GFP_KERNEL); + tmpname = kmalloc(PAGE_SIZE, GFP_KERNEL); if (tmpname == NULL) return -ENOMEM; @@ -263,7 +264,7 @@ static int isofs_readdir(struct file *file, struct dir_context *ctx) result = do_isofs_readdir(inode, file, ctx, tmpname, tmpde); - free_page((unsigned long) tmpname); + kfree(tmpname); return result; } From f6fce9f17d9cfde8a88beda1527600d6daacc49b Mon Sep 17 00:00:00 2001 From: Ashwin Gundarapu Date: Sun, 24 May 2026 21:05:27 +0530 Subject: [PATCH 4/9] ext2: Remove deprecated DAX support DAX support in ext2 was deprecated in commit d5a2693f93e4 ("ext2: Deprecate DAX") with a removal deadline of end of 2025. Remove all DAX code from ext2 as scheduled. This removes the DAX mount option, IOMAP DAX support, DAX file operations, DAX address_space_operations, and the DAX fault handler. [JK: Fixup some whitespace damage] Signed-off-by: Ashwin Gundarapu Link: https://patch.msgid.link/19e5aa07c9b.3a2e576d130187.5289857983023045470@zohomail.in Signed-off-by: Jan Kara --- fs/ext2/ext2.h | 4 -- fs/ext2/file.c | 116 +----------------------------------------------- fs/ext2/inode.c | 60 ++----------------------- fs/ext2/super.c | 39 ++-------------- 4 files changed, 8 insertions(+), 211 deletions(-) diff --git a/fs/ext2/ext2.h b/fs/ext2/ext2.h index 3eb1f342645c..79f7b395258c 100644 --- a/fs/ext2/ext2.h +++ b/fs/ext2/ext2.h @@ -114,8 +114,6 @@ struct ext2_sb_info { */ spinlock_t s_lock; struct mb_cache *s_ea_block_cache; - struct dax_device *s_daxdev; - u64 s_dax_part_off; }; static inline spinlock_t * @@ -373,11 +371,9 @@ struct ext2_inode { #define EXT2_MOUNT_NO_UID32 0x000200 /* Disable 32-bit UIDs */ #define EXT2_MOUNT_XATTR_USER 0x004000 /* Extended user attributes */ #define EXT2_MOUNT_POSIX_ACL 0x008000 /* POSIX Access Control Lists */ -#define EXT2_MOUNT_XIP 0x010000 /* Obsolete, use DAX */ #define EXT2_MOUNT_USRQUOTA 0x020000 /* user quota */ #define EXT2_MOUNT_GRPQUOTA 0x040000 /* group quota */ #define EXT2_MOUNT_RESERVATION 0x080000 /* Preallocation */ -#define EXT2_MOUNT_DAX 0x100000 /* Direct Access */ #define clear_opt(o, opt) o &= ~EXT2_MOUNT_##opt diff --git a/fs/ext2/file.c b/fs/ext2/file.c index d9b1eb34694a..fa0a9750cbd8 100644 --- a/fs/ext2/file.c +++ b/fs/ext2/file.c @@ -21,7 +21,6 @@ #include #include -#include #include #include #include @@ -32,111 +31,6 @@ #include "acl.h" #include "trace.h" -#ifdef CONFIG_FS_DAX -static ssize_t ext2_dax_read_iter(struct kiocb *iocb, struct iov_iter *to) -{ - struct inode *inode = iocb->ki_filp->f_mapping->host; - ssize_t ret; - - if (!iov_iter_count(to)) - return 0; /* skip atime */ - - inode_lock_shared(inode); - ret = dax_iomap_rw(iocb, to, &ext2_iomap_ops); - inode_unlock_shared(inode); - - file_accessed(iocb->ki_filp); - return ret; -} - -static ssize_t ext2_dax_write_iter(struct kiocb *iocb, struct iov_iter *from) -{ - struct file *file = iocb->ki_filp; - struct inode *inode = file->f_mapping->host; - ssize_t ret; - - inode_lock(inode); - ret = generic_write_checks(iocb, from); - if (ret <= 0) - goto out_unlock; - ret = file_remove_privs(file); - if (ret) - goto out_unlock; - ret = file_update_time(file); - if (ret) - goto out_unlock; - - ret = dax_iomap_rw(iocb, from, &ext2_iomap_ops); - if (ret > 0 && iocb->ki_pos > i_size_read(inode)) { - i_size_write(inode, iocb->ki_pos); - mark_inode_dirty(inode); - } - -out_unlock: - inode_unlock(inode); - if (ret > 0) - ret = generic_write_sync(iocb, ret); - return ret; -} - -/* - * The lock ordering for ext2 DAX fault paths is: - * - * mmap_lock (MM) - * sb_start_pagefault (vfs, freeze) - * address_space->invalidate_lock - * address_space->i_mmap_rwsem or page_lock (mutually exclusive in DAX) - * ext2_inode_info->truncate_mutex - * - * The default page_lock and i_size verification done by non-DAX fault paths - * is sufficient because ext2 doesn't support hole punching. - */ -static vm_fault_t ext2_dax_fault(struct vm_fault *vmf) -{ - struct inode *inode = file_inode(vmf->vma->vm_file); - vm_fault_t ret; - bool write = (vmf->flags & FAULT_FLAG_WRITE) && - (vmf->vma->vm_flags & VM_SHARED); - - if (write) { - sb_start_pagefault(inode->i_sb); - file_update_time(vmf->vma->vm_file); - } - filemap_invalidate_lock_shared(inode->i_mapping); - - ret = dax_iomap_fault(vmf, 0, NULL, NULL, &ext2_iomap_ops); - - filemap_invalidate_unlock_shared(inode->i_mapping); - if (write) - sb_end_pagefault(inode->i_sb); - return ret; -} - -static const struct vm_operations_struct ext2_dax_vm_ops = { - .fault = ext2_dax_fault, - /* - * .huge_fault is not supported for DAX because allocation in ext2 - * cannot be reliably aligned to huge page sizes and so pmd faults - * will always fail and fail back to regular faults. - */ - .page_mkwrite = ext2_dax_fault, - .pfn_mkwrite = ext2_dax_fault, -}; - -static int ext2_file_mmap_prepare(struct vm_area_desc *desc) -{ - struct file *file = desc->file; - - if (!IS_DAX(file_inode(file))) - return generic_file_mmap_prepare(desc); - - file_accessed(file); - desc->vm_ops = &ext2_dax_vm_ops; - return 0; -} -#else -#define ext2_file_mmap_prepare generic_file_mmap_prepare -#endif /* * Called when filp is released. This happens when all file descriptors @@ -285,10 +179,6 @@ static ssize_t ext2_dio_write_iter(struct kiocb *iocb, struct iov_iter *from) static ssize_t ext2_file_read_iter(struct kiocb *iocb, struct iov_iter *to) { -#ifdef CONFIG_FS_DAX - if (IS_DAX(iocb->ki_filp->f_mapping->host)) - return ext2_dax_read_iter(iocb, to); -#endif if (iocb->ki_flags & IOCB_DIRECT) return ext2_dio_read_iter(iocb, to); @@ -297,10 +187,6 @@ static ssize_t ext2_file_read_iter(struct kiocb *iocb, struct iov_iter *to) static ssize_t ext2_file_write_iter(struct kiocb *iocb, struct iov_iter *from) { -#ifdef CONFIG_FS_DAX - if (IS_DAX(iocb->ki_filp->f_mapping->host)) - return ext2_dax_write_iter(iocb, from); -#endif if (iocb->ki_flags & IOCB_DIRECT) return ext2_dio_write_iter(iocb, from); @@ -321,7 +207,7 @@ const struct file_operations ext2_file_operations = { #ifdef CONFIG_COMPAT .compat_ioctl = ext2_compat_ioctl, #endif - .mmap_prepare = ext2_file_mmap_prepare, + .mmap_prepare = generic_file_mmap_prepare, .open = ext2_file_open, .release = ext2_release_file, .fsync = ext2_fsync, diff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index 74aca5eb572d..29808629cce5 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -26,7 +26,6 @@ #include #include #include -#include #include #include #include @@ -741,27 +740,6 @@ static int ext2_get_blocks(struct inode *inode, goto cleanup; } - if (IS_DAX(inode)) { - /* - * We must unmap blocks before zeroing so that writeback cannot - * overwrite zeros with stale data from block device page cache. - */ - clean_bdev_aliases(inode->i_sb->s_bdev, - le32_to_cpu(chain[depth-1].key), - count); - /* - * block must be initialised before we put it in the tree - * so that it's not found by another thread before it's - * initialised - */ - err = sb_issue_zeroout(inode->i_sb, - le32_to_cpu(chain[depth-1].key), count, - GFP_KERNEL); - if (err) { - mutex_unlock(&ei->truncate_mutex); - goto cleanup; - } - } *new = true; ext2_splice_branch(inode, iblock, partial, indirect_blks, count); @@ -811,7 +789,6 @@ static int ext2_iomap_begin(struct inode *inode, loff_t offset, loff_t length, unsigned int blkbits = inode->i_blkbits; unsigned long first_block = offset >> blkbits; unsigned long max_blocks = (length + (1 << blkbits) - 1) >> blkbits; - struct ext2_sb_info *sbi = EXT2_SB(inode->i_sb); bool new = false, boundary = false; u32 bno; int ret; @@ -841,10 +818,7 @@ static int ext2_iomap_begin(struct inode *inode, loff_t offset, loff_t length, iomap->flags = 0; iomap->offset = (u64)first_block << blkbits; - if (flags & IOMAP_DAX) - iomap->dax_dev = sbi->s_daxdev; - else - iomap->bdev = inode->i_sb->s_bdev; + iomap->bdev = inode->i_sb->s_bdev; if (ret == 0) { /* @@ -859,8 +833,6 @@ static int ext2_iomap_begin(struct inode *inode, loff_t offset, loff_t length, } else { iomap->type = IOMAP_MAPPED; iomap->addr = (u64)bno << blkbits; - if (flags & IOMAP_DAX) - iomap->addr += sbi->s_dax_part_off; iomap->length = (u64)ret << blkbits; iomap->flags |= IOMAP_F_MERGED; } @@ -962,13 +934,6 @@ ext2_writepages(struct address_space *mapping, struct writeback_control *wbc) return mpage_writepages(mapping, wbc, ext2_get_block); } -static int -ext2_dax_writepages(struct address_space *mapping, struct writeback_control *wbc) -{ - struct ext2_sb_info *sbi = EXT2_SB(mapping->host->i_sb); - - return dax_writeback_mapping_range(mapping, sbi->s_daxdev, wbc); -} const struct address_space_operations ext2_aops = { .dirty_folio = block_dirty_folio, @@ -984,10 +949,6 @@ const struct address_space_operations ext2_aops = { .error_remove_folio = generic_error_remove_folio, }; -static const struct address_space_operations ext2_dax_aops = { - .writepages = ext2_dax_writepages, - .dirty_folio = noop_dirty_folio, -}; /* * Probably it should be a library function... search for first non-zero word @@ -1186,9 +1147,6 @@ static void __ext2_truncate_blocks(struct inode *inode, loff_t offset) blocksize = inode->i_sb->s_blocksize; iblock = (offset + blocksize-1) >> EXT2_BLOCK_SIZE_BITS(inode->i_sb); -#ifdef CONFIG_FS_DAX - WARN_ON(!rwsem_is_locked(&inode->i_mapping->invalidate_lock)); -#endif n = ext2_block_to_path(inode, iblock, offsets, NULL); if (n == 0) @@ -1290,12 +1248,7 @@ static int ext2_setsize(struct inode *inode, loff_t newsize) inode_dio_wait(inode); - if (IS_DAX(inode)) - error = dax_truncate_page(inode, newsize, NULL, - &ext2_iomap_ops); - else - error = block_truncate_page(inode->i_mapping, - newsize, ext2_get_block); + error = block_truncate_page(inode->i_mapping, newsize, ext2_get_block); if (error) return error; @@ -1363,7 +1316,7 @@ void ext2_set_inode_flags(struct inode *inode) unsigned int flags = EXT2_I(inode)->i_flags; inode->i_flags &= ~(S_SYNC | S_APPEND | S_IMMUTABLE | S_NOATIME | - S_DIRSYNC | S_DAX); + S_DIRSYNC); if (flags & EXT2_SYNC_FL) inode->i_flags |= S_SYNC; if (flags & EXT2_APPEND_FL) @@ -1374,18 +1327,13 @@ void ext2_set_inode_flags(struct inode *inode) inode->i_flags |= S_NOATIME; if (flags & EXT2_DIRSYNC_FL) inode->i_flags |= S_DIRSYNC; - if (test_opt(inode->i_sb, DAX) && S_ISREG(inode->i_mode)) - inode->i_flags |= S_DAX; } void ext2_set_file_ops(struct inode *inode) { inode->i_op = &ext2_file_inode_operations; inode->i_fop = &ext2_file_operations; - if (IS_DAX(inode)) - inode->i_mapping->a_ops = &ext2_dax_aops; - else - inode->i_mapping->a_ops = &ext2_aops; + inode->i_mapping->a_ops = &ext2_aops; } struct inode *ext2_iget (struct super_block *sb, unsigned long ino) diff --git a/fs/ext2/super.c b/fs/ext2/super.c index 19f76d8cb473..3999f8f3b156 100644 --- a/fs/ext2/super.c +++ b/fs/ext2/super.c @@ -34,7 +34,6 @@ #include #include #include -#include #include #include "ext2.h" #include "xattr.h" @@ -198,7 +197,6 @@ static void ext2_put_super (struct super_block * sb) brelse (sbi->s_sbh); sb->s_fs_info = NULL; kfree(sbi->s_blockgroup_lock); - fs_put_dax(sbi->s_daxdev, NULL); kfree(sbi); } @@ -329,11 +327,7 @@ static int ext2_show_options(struct seq_file *seq, struct dentry *root) if (test_opt(sb, GRPQUOTA)) seq_puts(seq, ",grpquota"); - if (test_opt(sb, XIP)) - seq_puts(seq, ",xip"); - if (test_opt(sb, DAX)) - seq_puts(seq, ",dax"); if (!test_opt(sb, RESERVATION)) seq_puts(seq, ",noreservation"); @@ -596,18 +590,10 @@ static int ext2_parse_param(struct fs_context *fc, struct fs_parameter *param) break; #endif case Opt_xip: - ext2_msg_fc(fc, KERN_INFO, "use dax instead of xip"); - ctx_set_mount_opt(ctx, EXT2_MOUNT_XIP); - fallthrough; + ext2_msg_fc(fc, KERN_WARNING, "DAX support has been removed. xip option ignored."); + break; case Opt_dax: -#ifdef CONFIG_FS_DAX - ext2_msg_fc(fc, KERN_WARNING, - "DAX enabled. Warning: DAX support in ext2 driver is deprecated" - " and will be removed at the end of 2025. Please use ext4 driver instead."); - ctx_set_mount_opt(ctx, EXT2_MOUNT_DAX); -#else - ext2_msg_fc(fc, KERN_INFO, "dax option not supported"); -#endif + ext2_msg_fc(fc, KERN_WARNING, "DAX support has been removed. dax option ignored."); break; #if defined(CONFIG_QUOTA) @@ -906,8 +892,6 @@ static int ext2_fill_super(struct super_block *sb, struct fs_context *fc) } sb->s_fs_info = sbi; sbi->s_sb_block = sb_block; - sbi->s_daxdev = fs_dax_get_by_bdev(sb->s_bdev, &sbi->s_dax_part_off, - NULL, NULL); spin_lock_init(&sbi->s_lock); ret = -EINVAL; @@ -992,17 +976,6 @@ static int ext2_fill_super(struct super_block *sb, struct fs_context *fc) } blocksize = BLOCK_SIZE << le32_to_cpu(sbi->s_es->s_log_block_size); - if (test_opt(sb, DAX)) { - if (!sbi->s_daxdev) { - ext2_msg(sb, KERN_ERR, - "DAX unsupported by block device. Turning off DAX."); - clear_opt(sbi->s_mount_opt, DAX); - } else if (blocksize != PAGE_SIZE) { - ext2_msg(sb, KERN_ERR, "unsupported blocksize for DAX\n"); - clear_opt(sbi->s_mount_opt, DAX); - } - } - /* If the blocksize doesn't match, re-read the thing.. */ if (sb->s_blocksize != blocksize) { brelse(bh); @@ -1252,7 +1225,6 @@ static int ext2_fill_super(struct super_block *sb, struct fs_context *fc) failed_mount: brelse(bh); failed_sbi: - fs_put_dax(sbi->s_daxdev, NULL); sb->s_fs_info = NULL; kfree(sbi->s_blockgroup_lock); kfree(sbi); @@ -1379,11 +1351,6 @@ static int ext2_reconfigure(struct fs_context *fc) spin_lock(&sbi->s_lock); es = sbi->s_es; - if ((sbi->s_mount_opt ^ new_opts.s_mount_opt) & EXT2_MOUNT_DAX) { - ext2_msg(sb, KERN_WARNING, "warning: refusing change of " - "dax flag with busy inodes while remounting"); - new_opts.s_mount_opt ^= EXT2_MOUNT_DAX; - } if ((bool)(flags & SB_RDONLY) == sb_rdonly(sb)) goto out_set; if (flags & SB_RDONLY) { From a4659be0bc7cb1856ffb15b67f903229ae8891ec Mon Sep 17 00:00:00 2001 From: Danila Chernetsov Date: Sat, 30 May 2026 12:23:11 +0000 Subject: [PATCH 5/9] ext2: fix ignored return value of generic_write_sync() Fix ext2_dio_write_iter() to propagate the error returned by generic_write_sync() instead of silently discarding it, which could cause write(2) to return success to userspace on O_SYNC/O_DSYNC files even when the sync failed. The correct pattern, already used in ext2_dax_write_iter() in the same file and in ext4, xfs, f2fs among others, is: if (ret > 0) ret = generic_write_sync(iocb, ret); Found by Linux Verification Center (linuxtesting.org) with SVACE. [JK: Reflect also filemap_write_and_wait() return value] Fixes: fb5de4358e1a ("ext2: Move direct-io to use iomap") Signed-off-by: Danila Chernetsov Link: https://patch.msgid.link/20260530122311.136803-1-listdansp@mail.ru Signed-off-by: Jan Kara --- fs/ext2/file.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/fs/ext2/file.c b/fs/ext2/file.c index fa0a9750cbd8..8dca9ec4cacd 100644 --- a/fs/ext2/file.c +++ b/fs/ext2/file.c @@ -161,12 +161,15 @@ static ssize_t ext2_dio_write_iter(struct kiocb *iocb, struct iov_iter *from) endbyte = pos + status - 1; ret2 = filemap_write_and_wait_range(inode->i_mapping, pos, endbyte); - if (!ret2) + if (!ret2) { invalidate_mapping_pages(inode->i_mapping, pos >> PAGE_SHIFT, endbyte >> PAGE_SHIFT); - if (ret > 0) - generic_write_sync(iocb, ret); + if (ret > 0) + ret = generic_write_sync(iocb, ret); + } else { + ret = ret2; + } } out_unlock: From 5fa1d6a5ec2356d2107dead614437c66fa7138b1 Mon Sep 17 00:00:00 2001 From: Bryam Vargas Date: Sun, 7 Jun 2026 01:18:27 +0000 Subject: [PATCH 6/9] isofs: bound Rock Ridge symlink components to the SL record get_symlink_chunk() and the SL handling in parse_rock_ridge_inode_internal() walk the variable-length components of a Rock Ridge "SL" (symbolic link) record. Each component is a two-byte header (flags, len) followed by len bytes of text, so it occupies slp->len + 2 bytes. Both loops read slp->len and advance to the next component, and get_symlink_chunk() additionally does memcpy(rpnt, slp->text, slp->len), but neither checks that the component lies within the SL record before dereferencing it. A crafted SL record whose component declares a len that runs past the record (rr->len) therefore triggers an out-of-bounds read of up to 255 bytes. When the record sits at the tail of its backing buffer - for example a small kmalloc()ed continuation block reached through a CE record - the read crosses the allocation; get_symlink_chunk() then copies the out-of-bounds bytes into the symlink body returned to user space by readlink(), disclosing adjacent kernel memory. ISO 9660 images are routinely mounted from untrusted removable media - desktop environments auto-mount them (e.g. via udisks2) without CAP_SYS_ADMIN - so the record contents are attacker-controlled. Reject any component that does not fit in the remaining record bytes before using it. In get_symlink_chunk() return NULL, like the existing output-buffer (plimit) checks, so a malformed record makes readlink() fail with -EIO rather than silently returning a truncated target; in parse_rock_ridge_inode_internal() stop the inode-size walk. Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Cc: stable@vger.kernel.org Suggested-by: Michael Bommarito Signed-off-by: Bryam Vargas Link: https://patch.msgid.link/20260607011823.217748-1-hexlabsecurity@proton.me Signed-off-by: Jan Kara --- fs/isofs/rock.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/fs/isofs/rock.c b/fs/isofs/rock.c index 1232fab59a4e..2628f31bd3a5 100644 --- a/fs/isofs/rock.c +++ b/fs/isofs/rock.c @@ -466,6 +466,9 @@ parse_rock_ridge_inode_internal(struct iso_directory_record *de, inode->i_size = symlink_len; while (slen > 1) { rootflag = 0; + /* keep the component within the SL record */ + if (slp->len + 2 > slen) + goto eio; switch (slp->flags & ~1) { case 0: inode->i_size += @@ -621,6 +624,14 @@ static char *get_symlink_chunk(char *rpnt, struct rock_ridge *rr, char *plimit) slp = &rr->u.SL.link; while (slen > 1) { rootflag = 0; + /* + * A component is slp->len + 2 bytes (a two-byte header plus + * len bytes of text). If it does not fit in the bytes left in + * the SL record the record is malformed: fail like the plimit + * checks below so readlink() returns -EIO, not a truncated path. + */ + if (slp->len + 2 > slen) + return NULL; switch (slp->flags & ~1) { case 0: if (slp->len > plimit - rpnt) From 3ec997bd5508e9b25210b5bbec89031629cdb093 Mon Sep 17 00:00:00 2001 From: Bryam Vargas Date: Fri, 12 Jun 2026 01:40:01 -0500 Subject: [PATCH 7/9] udf: validate sparing table length as an entry count, not a byte count udf_load_sparable_map() accepts a sparing table when sizeof(*st) + le16_to_cpu(st->reallocationTableLen) > sb->s_blocksize is false, i.e. it treats reallocationTableLen as a number of BYTES that must fit in the block. But the table is walked as an array of 8-byte sparingEntry elements: for (i = 0; i < le16_to_cpu(st->reallocationTableLen); i++) { struct sparingEntry *entry = &st->mapEntry[i]; ... entry->origLocation ... } in udf_get_pblock_spar15() and udf_relocate_blocks(). A reallocationTableLen of N therefore passes the check whenever sizeof(*st) + N <= blocksize, yet the consumers index sizeof(*st) + N * sizeof(struct sparingEntry) bytes -- up to ~8x the block. On a crafted UDF image this is an out-of-bounds read in udf_get_pblock_spar15(); udf_relocate_blocks() additionally feeds the same length to udf_update_tag(), whose crc_itu_t() reads far past the block, and its memmove() through st->mapEntry[] is an out-of-bounds write. Validate reallocationTableLen as the entry count it is, with struct_size(). Fixes: 1df2ae31c724 ("udf: Fortify loading of sparing table") Cc: stable@vger.kernel.org Signed-off-by: Bryam Vargas Link: https://patch.msgid.link/20260612-b4-disp-91780c4e-v1-1-f15112ff6882@proton.me Signed-off-by: Jan Kara --- fs/udf/super.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/udf/super.c b/fs/udf/super.c index cad2e15d633b..d75bfc733975 100644 --- a/fs/udf/super.c +++ b/fs/udf/super.c @@ -1418,7 +1418,8 @@ static int udf_load_sparable_map(struct super_block *sb, if (ident != 0 || strncmp(st->sparingIdent.ident, UDF_ID_SPARING, strlen(UDF_ID_SPARING)) || - sizeof(*st) + le16_to_cpu(st->reallocationTableLen) > + struct_size(st, mapEntry, + le16_to_cpu(st->reallocationTableLen)) > sb->s_blocksize) { brelse(bh); continue; From d8202786b3d75125c84ebc4de6d946f92fde0ee8 Mon Sep 17 00:00:00 2001 From: Bryam Vargas Date: Fri, 12 Jun 2026 02:53:31 -0500 Subject: [PATCH 8/9] udf: validate VAT header length against the VAT inode size udf_load_vat() takes the virtual partition's start offset straight from the on-disk VAT 2.0 header without checking it against the VAT inode size: map->s_type_specific.s_virtual.s_start_offset = le16_to_cpu(vat20->lengthHeader); map->s_type_specific.s_virtual.s_num_entries = (sbi->s_vat_inode->i_size - map->s_type_specific.s_virtual.s_start_offset) >> 2; lengthHeader is a fully attacker-controlled 16-bit value. If it exceeds the VAT inode size, the s_num_entries subtraction underflows to a huge count, which defeats the "block > s_num_entries" bound in udf_get_pblock_virt15(); and on the ICB-inline path that function reads ((__le32 *)(iinfo->i_data + s_start_offset))[block] so a large s_start_offset indexes past the inode's in-ICB data. Mounting a crafted UDF image with a virtual (VAT) partition then triggers an out-of-bounds read. Reject a VAT whose header length does not leave room for at least one entry within the VAT inode. Fixes: fa5e08156335 ("udf: Handle VAT packed inside inode properly") Cc: stable@vger.kernel.org Signed-off-by: Bryam Vargas Link: https://patch.msgid.link/20260612-b4-disp-9a2317ee-v1-1-fefef5736154@proton.me Signed-off-by: Jan Kara --- fs/udf/super.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fs/udf/super.c b/fs/udf/super.c index d75bfc733975..1b5282790de6 100644 --- a/fs/udf/super.c +++ b/fs/udf/super.c @@ -1263,6 +1263,14 @@ 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) { + 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); + brelse(bh); + return -EFSCORRUPTED; + } map->s_type_specific.s_virtual.s_num_entries = (sbi->s_vat_inode->i_size - map->s_type_specific.s_virtual. From 5163e6ee1ea744d412fe516235bfd9cab15141dc Mon Sep 17 00:00:00 2001 From: Jan Kara Date: Fri, 12 Jun 2026 13:50:04 +0200 Subject: [PATCH 9/9] udf: validate VAT inode size for old VAT format Validate VAT inode is large enough to contain at least the header for pre-2.00 UDF media format. Signed-off-by: Jan Kara --- fs/udf/super.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fs/udf/super.c b/fs/udf/super.c index 1b5282790de6..c7110d26d6fe 100644 --- a/fs/udf/super.c +++ b/fs/udf/super.c @@ -1242,6 +1242,11 @@ static int udf_load_vat(struct super_block *sb, int p_index, int type1_index) if (map->s_partition_type == UDF_VIRTUAL_MAP15) { map->s_type_specific.s_virtual.s_start_offset = 0; + if (sbi->s_vat_inode->i_size < 36) { + udf_err(sb, "Too short VAT inode size %lld\n", + sbi->s_vat_inode->i_size); + return -EFSCORRUPTED; + } map->s_type_specific.s_virtual.s_num_entries = (sbi->s_vat_inode->i_size - 36) >> 2; } else if (map->s_partition_type == UDF_VIRTUAL_MAP20) {