![Thadeu Lima de Souza Cascardo](/assets/img/avatar_default.png)
[ Upstream commit 1c82587cb57687de3f18ab4b98a8850c789bedcf ] Devices block sizes may change. One of these cases is a loop device by using ioctl LOOP_SET_BLOCK_SIZE. While this may cause other issues like IO being rejected, in the case of hfsplus, it will allocate a block by using that size and potentially write out-of-bounds when hfsplus_read_wrapper calls hfsplus_submit_bio and the latter function reads a different io_size. Using a new min_io_size initally set to sb_min_blocksize works for the purposes of the original fix, since it will be set to the max between HFSPLUS_SECTOR_SIZE and the first seen logical block size. We still use the max between HFSPLUS_SECTOR_SIZE and min_io_size in case the latter is not initialized. Tested by mounting an hfsplus filesystem with loop block sizes 512, 1024 and 4096. The produced KASAN report before the fix looks like this: [ 419.944641] ================================================================== [ 419.945655] BUG: KASAN: slab-use-after-free in hfsplus_read_wrapper+0x659/0xa0a [ 419.946703] Read of size 2 at addr ffff88800721fc00 by task repro/10678 [ 419.947612] [ 419.947846] CPU: 0 UID: 0 PID: 10678 Comm: repro Not tainted 6.12.0-rc5-00008-gdf56e0f2f3ca #84 [ 419.949007] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.15.0-1 04/01/2014 [ 419.950035] Call Trace: [ 419.950384] <TASK> [ 419.950676] dump_stack_lvl+0x57/0x78 [ 419.951212] ? hfsplus_read_wrapper+0x659/0xa0a [ 419.951830] print_report+0x14c/0x49e [ 419.952361] ? __virt_addr_valid+0x267/0x278 [ 419.952979] ? kmem_cache_debug_flags+0xc/0x1d [ 419.953561] ? hfsplus_read_wrapper+0x659/0xa0a [ 419.954231] kasan_report+0x89/0xb0 [ 419.954748] ? hfsplus_read_wrapper+0x659/0xa0a [ 419.955367] hfsplus_read_wrapper+0x659/0xa0a [ 419.955948] ? __pfx_hfsplus_read_wrapper+0x10/0x10 [ 419.956618] ? do_raw_spin_unlock+0x59/0x1a9 [ 419.957214] ? _raw_spin_unlock+0x1a/0x2e [ 419.957772] hfsplus_fill_super+0x348/0x1590 [ 419.958355] ? hlock_class+0x4c/0x109 [ 419.958867] ? __pfx_hfsplus_fill_super+0x10/0x10 [ 419.959499] ? __pfx_string+0x10/0x10 [ 419.960006] ? lock_acquire+0x3e2/0x454 [ 419.960532] ? bdev_name.constprop.0+0xce/0x243 [ 419.961129] ? __pfx_bdev_name.constprop.0+0x10/0x10 [ 419.961799] ? pointer+0x3f0/0x62f [ 419.962277] ? __pfx_pointer+0x10/0x10 [ 419.962761] ? vsnprintf+0x6c4/0xfba [ 419.963178] ? __pfx_vsnprintf+0x10/0x10 [ 419.963621] ? setup_bdev_super+0x376/0x3b3 [ 419.964029] ? snprintf+0x9d/0xd2 [ 419.964344] ? __pfx_snprintf+0x10/0x10 [ 419.964675] ? lock_acquired+0x45c/0x5e9 [ 419.965016] ? set_blocksize+0x139/0x1c1 [ 419.965381] ? sb_set_blocksize+0x6d/0xae [ 419.965742] ? __pfx_hfsplus_fill_super+0x10/0x10 [ 419.966179] mount_bdev+0x12f/0x1bf [ 419.966512] ? __pfx_mount_bdev+0x10/0x10 [ 419.966886] ? vfs_parse_fs_string+0xce/0x111 [ 419.967293] ? __pfx_vfs_parse_fs_string+0x10/0x10 [ 419.967702] ? __pfx_hfsplus_mount+0x10/0x10 [ 419.968073] legacy_get_tree+0x104/0x178 [ 419.968414] vfs_get_tree+0x86/0x296 [ 419.968751] path_mount+0xba3/0xd0b [ 419.969157] ? __pfx_path_mount+0x10/0x10 [ 419.969594] ? kmem_cache_free+0x1e2/0x260 [ 419.970311] do_mount+0x99/0xe0 [ 419.970630] ? __pfx_do_mount+0x10/0x10 [ 419.971008] __do_sys_mount+0x199/0x1c9 [ 419.971397] do_syscall_64+0xd0/0x135 [ 419.971761] entry_SYSCALL_64_after_hwframe+0x76/0x7e [ 419.972233] RIP: 0033:0x7c3cb812972e [ 419.972564] Code: 48 8b 0d f5 46 0d 00 f7 d8 64 89 01 48 83 c8 ff c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 49 89 ca b8 a5 00 00 00 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 8b 0d c2 46 0d 00 f7 d8 64 89 01 48 [ 419.974371] RSP: 002b:00007ffe30632548 EFLAGS: 00000286 ORIG_RAX: 00000000000000a5 [ 419.975048] RAX: ffffffffffffffda RBX: 00007ffe306328d8 RCX: 00007c3cb812972e [ 419.975701] RDX: 0000000020000000 RSI: 0000000020000c80 RDI: 00007ffe306325d0 [ 419.976363] RBP: 00007ffe30632720 R08: 00007ffe30632610 R09: 0000000000000000 [ 419.977034] R10: 0000000000200008 R11: 0000000000000286 R12: 0000000000000000 [ 419.977713] R13: 00007ffe306328e8 R14: 00005a0eb298bc68 R15: 00007c3cb8356000 [ 419.978375] </TASK> [ 419.978589] Fixes: 6596528e391a ("hfsplus: ensure bio requests are not smaller than the hardware sectors") Signed-off-by: Thadeu Lima de Souza Cascardo <cascardo@igalia.com> Link: https://lore.kernel.org/r/20241107114109.839253-1-cascardo@igalia.com Signed-off-by: Christian Brauner <brauner@kernel.org> Signed-off-by: Sasha Levin <sashal@kernel.org>
270 lines
7 KiB
C
Executable file
270 lines
7 KiB
C
Executable file
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* linux/fs/hfsplus/wrapper.c
|
|
*
|
|
* Copyright (C) 2001
|
|
* Brad Boyer (flar@allandria.com)
|
|
* (C) 2003 Ardis Technologies <roman@ardistech.com>
|
|
*
|
|
* Handling of HFS wrappers around HFS+ volumes
|
|
*/
|
|
|
|
#include <linux/fs.h>
|
|
#include <linux/blkdev.h>
|
|
#include <linux/cdrom.h>
|
|
#include <linux/genhd.h>
|
|
#include <asm/unaligned.h>
|
|
|
|
#include "hfsplus_fs.h"
|
|
#include "hfsplus_raw.h"
|
|
|
|
struct hfsplus_wd {
|
|
u32 ablk_size;
|
|
u16 ablk_start;
|
|
u16 embed_start;
|
|
u16 embed_count;
|
|
};
|
|
|
|
/**
|
|
* hfsplus_submit_bio - Perform block I/O
|
|
* @sb: super block of volume for I/O
|
|
* @sector: block to read or write, for blocks of HFSPLUS_SECTOR_SIZE bytes
|
|
* @buf: buffer for I/O
|
|
* @data: output pointer for location of requested data
|
|
* @op: direction of I/O
|
|
* @op_flags: request op flags
|
|
*
|
|
* The unit of I/O is hfsplus_min_io_size(sb), which may be bigger than
|
|
* HFSPLUS_SECTOR_SIZE, and @buf must be sized accordingly. On reads
|
|
* @data will return a pointer to the start of the requested sector,
|
|
* which may not be the same location as @buf.
|
|
*
|
|
* If @sector is not aligned to the bdev logical block size it will
|
|
* be rounded down. For writes this means that @buf should contain data
|
|
* that starts at the rounded-down address. As long as the data was
|
|
* read using hfsplus_submit_bio() and the same buffer is used things
|
|
* will work correctly.
|
|
*/
|
|
int hfsplus_submit_bio(struct super_block *sb, sector_t sector,
|
|
void *buf, void **data, int op, int op_flags)
|
|
{
|
|
struct bio *bio;
|
|
int ret = 0;
|
|
u64 io_size;
|
|
loff_t start;
|
|
int offset;
|
|
|
|
/*
|
|
* Align sector to hardware sector size and find offset. We
|
|
* assume that io_size is a power of two, which _should_
|
|
* be true.
|
|
*/
|
|
io_size = hfsplus_min_io_size(sb);
|
|
start = (loff_t)sector << HFSPLUS_SECTOR_SHIFT;
|
|
offset = start & (io_size - 1);
|
|
sector &= ~((io_size >> HFSPLUS_SECTOR_SHIFT) - 1);
|
|
|
|
bio = bio_alloc(GFP_NOIO, 1);
|
|
bio->bi_iter.bi_sector = sector;
|
|
bio_set_dev(bio, sb->s_bdev);
|
|
bio_set_op_attrs(bio, op, op_flags);
|
|
|
|
if (op != WRITE && data)
|
|
*data = (u8 *)buf + offset;
|
|
|
|
while (io_size > 0) {
|
|
unsigned int page_offset = offset_in_page(buf);
|
|
unsigned int len = min_t(unsigned int, PAGE_SIZE - page_offset,
|
|
io_size);
|
|
|
|
ret = bio_add_page(bio, virt_to_page(buf), len, page_offset);
|
|
if (ret != len) {
|
|
ret = -EIO;
|
|
goto out;
|
|
}
|
|
io_size -= len;
|
|
buf = (u8 *)buf + len;
|
|
}
|
|
|
|
ret = submit_bio_wait(bio);
|
|
out:
|
|
bio_put(bio);
|
|
return ret < 0 ? ret : 0;
|
|
}
|
|
|
|
static int hfsplus_read_mdb(void *bufptr, struct hfsplus_wd *wd)
|
|
{
|
|
u32 extent;
|
|
u16 attrib;
|
|
__be16 sig;
|
|
|
|
sig = *(__be16 *)(bufptr + HFSP_WRAPOFF_EMBEDSIG);
|
|
if (sig != cpu_to_be16(HFSPLUS_VOLHEAD_SIG) &&
|
|
sig != cpu_to_be16(HFSPLUS_VOLHEAD_SIGX))
|
|
return 0;
|
|
|
|
attrib = be16_to_cpu(*(__be16 *)(bufptr + HFSP_WRAPOFF_ATTRIB));
|
|
if (!(attrib & HFSP_WRAP_ATTRIB_SLOCK) ||
|
|
!(attrib & HFSP_WRAP_ATTRIB_SPARED))
|
|
return 0;
|
|
|
|
wd->ablk_size =
|
|
be32_to_cpu(*(__be32 *)(bufptr + HFSP_WRAPOFF_ABLKSIZE));
|
|
if (wd->ablk_size < HFSPLUS_SECTOR_SIZE)
|
|
return 0;
|
|
if (wd->ablk_size % HFSPLUS_SECTOR_SIZE)
|
|
return 0;
|
|
wd->ablk_start =
|
|
be16_to_cpu(*(__be16 *)(bufptr + HFSP_WRAPOFF_ABLKSTART));
|
|
|
|
extent = get_unaligned_be32(bufptr + HFSP_WRAPOFF_EMBEDEXT);
|
|
wd->embed_start = (extent >> 16) & 0xFFFF;
|
|
wd->embed_count = extent & 0xFFFF;
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int hfsplus_get_last_session(struct super_block *sb,
|
|
sector_t *start, sector_t *size)
|
|
{
|
|
struct cdrom_device_info *cdi = disk_to_cdi(sb->s_bdev->bd_disk);
|
|
|
|
/* default values */
|
|
*start = 0;
|
|
*size = i_size_read(sb->s_bdev->bd_inode) >> 9;
|
|
|
|
if (HFSPLUS_SB(sb)->session >= 0) {
|
|
struct cdrom_tocentry te;
|
|
|
|
if (!cdi)
|
|
return -EINVAL;
|
|
|
|
te.cdte_track = HFSPLUS_SB(sb)->session;
|
|
te.cdte_format = CDROM_LBA;
|
|
if (cdrom_read_tocentry(cdi, &te) ||
|
|
(te.cdte_ctrl & CDROM_DATA_TRACK) != 4) {
|
|
pr_err("invalid session number or type of track\n");
|
|
return -EINVAL;
|
|
}
|
|
*start = (sector_t)te.cdte_addr.lba << 2;
|
|
} else if (cdi) {
|
|
struct cdrom_multisession ms_info;
|
|
|
|
ms_info.addr_format = CDROM_LBA;
|
|
if (cdrom_multisession(cdi, &ms_info) == 0 && ms_info.xa_flag)
|
|
*start = (sector_t)ms_info.addr.lba << 2;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Find the volume header and fill in some minimum bits in superblock */
|
|
/* Takes in super block, returns true if good data read */
|
|
int hfsplus_read_wrapper(struct super_block *sb)
|
|
{
|
|
struct hfsplus_sb_info *sbi = HFSPLUS_SB(sb);
|
|
struct hfsplus_wd wd;
|
|
sector_t part_start, part_size;
|
|
u32 blocksize;
|
|
int error = 0;
|
|
|
|
error = -EINVAL;
|
|
blocksize = sb_min_blocksize(sb, HFSPLUS_SECTOR_SIZE);
|
|
if (!blocksize)
|
|
goto out;
|
|
|
|
sbi->min_io_size = blocksize;
|
|
|
|
if (hfsplus_get_last_session(sb, &part_start, &part_size))
|
|
goto out;
|
|
|
|
error = -ENOMEM;
|
|
sbi->s_vhdr_buf = kmalloc(hfsplus_min_io_size(sb), GFP_KERNEL);
|
|
if (!sbi->s_vhdr_buf)
|
|
goto out;
|
|
sbi->s_backup_vhdr_buf = kmalloc(hfsplus_min_io_size(sb), GFP_KERNEL);
|
|
if (!sbi->s_backup_vhdr_buf)
|
|
goto out_free_vhdr;
|
|
|
|
reread:
|
|
error = hfsplus_submit_bio(sb, part_start + HFSPLUS_VOLHEAD_SECTOR,
|
|
sbi->s_vhdr_buf, (void **)&sbi->s_vhdr,
|
|
REQ_OP_READ, 0);
|
|
if (error)
|
|
goto out_free_backup_vhdr;
|
|
|
|
error = -EINVAL;
|
|
switch (sbi->s_vhdr->signature) {
|
|
case cpu_to_be16(HFSPLUS_VOLHEAD_SIGX):
|
|
set_bit(HFSPLUS_SB_HFSX, &sbi->flags);
|
|
fallthrough;
|
|
case cpu_to_be16(HFSPLUS_VOLHEAD_SIG):
|
|
break;
|
|
case cpu_to_be16(HFSP_WRAP_MAGIC):
|
|
if (!hfsplus_read_mdb(sbi->s_vhdr, &wd))
|
|
goto out_free_backup_vhdr;
|
|
wd.ablk_size >>= HFSPLUS_SECTOR_SHIFT;
|
|
part_start += (sector_t)wd.ablk_start +
|
|
(sector_t)wd.embed_start * wd.ablk_size;
|
|
part_size = (sector_t)wd.embed_count * wd.ablk_size;
|
|
goto reread;
|
|
default:
|
|
/*
|
|
* Check for a partition block.
|
|
*
|
|
* (should do this only for cdrom/loop though)
|
|
*/
|
|
if (hfs_part_find(sb, &part_start, &part_size))
|
|
goto out_free_backup_vhdr;
|
|
goto reread;
|
|
}
|
|
|
|
error = hfsplus_submit_bio(sb, part_start + part_size - 2,
|
|
sbi->s_backup_vhdr_buf,
|
|
(void **)&sbi->s_backup_vhdr, REQ_OP_READ,
|
|
0);
|
|
if (error)
|
|
goto out_free_backup_vhdr;
|
|
|
|
error = -EINVAL;
|
|
if (sbi->s_backup_vhdr->signature != sbi->s_vhdr->signature) {
|
|
pr_warn("invalid secondary volume header\n");
|
|
goto out_free_backup_vhdr;
|
|
}
|
|
|
|
blocksize = be32_to_cpu(sbi->s_vhdr->blocksize);
|
|
|
|
/*
|
|
* Block size must be at least as large as a sector and a multiple of 2.
|
|
*/
|
|
if (blocksize < HFSPLUS_SECTOR_SIZE || ((blocksize - 1) & blocksize))
|
|
goto out_free_backup_vhdr;
|
|
sbi->alloc_blksz = blocksize;
|
|
sbi->alloc_blksz_shift = ilog2(blocksize);
|
|
blocksize = min_t(u32, sbi->alloc_blksz, PAGE_SIZE);
|
|
|
|
/*
|
|
* Align block size to block offset.
|
|
*/
|
|
while (part_start & ((blocksize >> HFSPLUS_SECTOR_SHIFT) - 1))
|
|
blocksize >>= 1;
|
|
|
|
if (sb_set_blocksize(sb, blocksize) != blocksize) {
|
|
pr_err("unable to set blocksize to %u!\n", blocksize);
|
|
goto out_free_backup_vhdr;
|
|
}
|
|
|
|
sbi->blockoffset =
|
|
part_start >> (sb->s_blocksize_bits - HFSPLUS_SECTOR_SHIFT);
|
|
sbi->part_start = part_start;
|
|
sbi->sect_count = part_size;
|
|
sbi->fs_shift = sbi->alloc_blksz_shift - sb->s_blocksize_bits;
|
|
return 0;
|
|
|
|
out_free_backup_vhdr:
|
|
kfree(sbi->s_backup_vhdr_buf);
|
|
out_free_vhdr:
|
|
kfree(sbi->s_vhdr_buf);
|
|
out:
|
|
return error;
|
|
}
|