mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
synced 2026-05-26 11:40:24 +02:00
f3c8cd8a63
This is not a normal data I/O hot path. The single in-tree caller is
the $LogFile emptying path used during read-write mount/remount, and
the bug only becomes visible on NTFS volumes whose cluster_size is
strictly smaller than the kernel's PAGE_SIZE (typically 4 KiB on
x86_64). Per Microsoft's format command documentation, NTFS supports
allocation unit sizes starting at 512 bytes, so 512 B, 1 KiB and 2 KiB
clusters are uncommon but valid on-disk configurations. When
cluster_size >= PAGE_SIZE every "start" passed in is page-aligned and
the buggy "from != 0" path is never taken.
ntfs_bdev_write() splits the write across one or more block-device
folios. Inside the loop, "to" is computed as the *end byte offset*
within the current page (0..PAGE_SIZE), and "from" is the start byte
offset within the page (reset to 0 from the second iteration onward).
The copy length should therefore be "to - from", but the current code
uses "to" directly:
to = min_t(u32, end - offset, PAGE_SIZE);
memcpy_to_folio(folio, from, buf + buf_off, to);
buf_off += to;
When "from != 0" (i.e. "start" is not page-aligned) memcpy_to_folio()
copies "from" extra bytes:
- it reads "from" bytes past the source buffer into kernel heap;
- it writes "from" bytes past the requested range into the next part
of the block-device page (or, if "from + to > PAGE_SIZE", past the
folio boundary entirely, which trips the VM_BUG_ON inside
memcpy_to_folio() on CONFIG_DEBUG_VM=y kernels).
"buf_off" is then advanced by the wrong amount, so every subsequent
iteration also reads the source buffer at the wrong offset and writes
the wrong content to disk.
ntfs_empty_logfile() calls
ntfs_bdev_write(sb, empty_buf, NTFS_CLU_TO_B(vol, lcn),
vol->cluster_size);
with empty_buf sized to vol->cluster_size. On a sub-PAGE_SIZE-cluster
volume, any $LogFile run whose LCN is not aligned to
PAGE_SIZE / cluster_size reaches the non-page-aligned path. The
over-copy can read beyond empty_buf and overwrite the sectors following
the requested cluster in the block-device page with unrelated kernel
heap contents while $LogFile is being emptied.
A userspace reducer of the same arithmetic and copy loop confirms the
bug under AddressSanitizer: ASan reports a heap-buffer-overflow read
past the source buffer for the buggy length, and the fixed version is
ASan-clean.
Compute the copy length as "to - from" and advance buf_off by the same
amount.
Fixes: 5218cd102a ("ntfs: update misc operations")
Link: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/format
Signed-off-by: DaeMyung Kang <charsyam@gmail.com>
Reviewed-by: Hyunchul Lee <hyc.lee@gmail.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
121 lines
3.1 KiB
C
121 lines
3.1 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* NTFS block device I/O.
|
|
*
|
|
* Copyright (c) 2026 LG Electronics Co., Ltd.
|
|
*/
|
|
|
|
#include <linux/blkdev.h>
|
|
|
|
#include "ntfs.h"
|
|
|
|
/*
|
|
* ntfs_bdev_read - Read data directly from block device using bio
|
|
* @bdev: block device to read from
|
|
* @data: destination buffer
|
|
* @start: starting byte offset on the block device
|
|
* @size: number of bytes to read
|
|
*
|
|
* Reads @size bytes starting from byte offset @start directly from the block
|
|
* device using one or more BIOs. This function bypasses the page cache
|
|
* completely and performs synchronous I/O with REQ_META | REQ_SYNC flags set.
|
|
*
|
|
* The @start offset must be sector-aligned (512 bytes). If it is not aligned,
|
|
* the function will return -EINVAL.
|
|
*
|
|
* If the destination buffer @data is not a vmalloc address, it falls back
|
|
* to the more efficient bdev_rw_virt() helper.
|
|
*
|
|
* Return: 0 on success, negative error code on failure.
|
|
*/
|
|
int ntfs_bdev_read(struct block_device *bdev, char *data, loff_t start, size_t size)
|
|
{
|
|
unsigned int done = 0, added;
|
|
int error;
|
|
struct bio *bio;
|
|
enum req_op op;
|
|
sector_t sector = start >> SECTOR_SHIFT;
|
|
|
|
if (start & (SECTOR_SIZE - 1))
|
|
return -EINVAL;
|
|
|
|
op = REQ_OP_READ | REQ_META | REQ_SYNC;
|
|
if (!is_vmalloc_addr(data))
|
|
return bdev_rw_virt(bdev, sector, data, size, op);
|
|
|
|
bio = bio_alloc(bdev,
|
|
bio_max_segs(DIV_ROUND_UP(size, PAGE_SIZE)),
|
|
op, GFP_KERNEL);
|
|
bio->bi_iter.bi_sector = sector;
|
|
|
|
do {
|
|
added = bio_add_vmalloc_chunk(bio, data + done, size - done);
|
|
if (!added) {
|
|
struct bio *prev = bio;
|
|
|
|
bio = bio_alloc(prev->bi_bdev,
|
|
bio_max_segs(DIV_ROUND_UP(size - done, PAGE_SIZE)),
|
|
prev->bi_opf, GFP_KERNEL);
|
|
bio->bi_iter.bi_sector = bio_end_sector(prev);
|
|
bio_chain(prev, bio);
|
|
submit_bio(prev);
|
|
}
|
|
done += added;
|
|
} while (done < size);
|
|
|
|
error = submit_bio_wait(bio);
|
|
bio_put(bio);
|
|
|
|
if (op == REQ_OP_READ)
|
|
invalidate_kernel_vmap_range(data, size);
|
|
return error;
|
|
}
|
|
|
|
/*
|
|
* ntfs_bdev_write - Update block device contents via page cache
|
|
* @sb: super block of the mounted NTFS filesystem
|
|
* @buf: source buffer containing data to write
|
|
* @start: starting byte offset on the block device
|
|
* @size: number of bytes to write
|
|
*
|
|
* Writes @size bytes from @buf to the block device (sb->s_bdev) starting
|
|
* at byte offset @start. The write is performed entirely through the page
|
|
* cache of the block device's address space.
|
|
*/
|
|
int ntfs_bdev_write(struct super_block *sb, void *buf, loff_t start, size_t size)
|
|
{
|
|
pgoff_t idx, idx_end;
|
|
loff_t offset, end = start + size;
|
|
u32 from, to, buf_off = 0;
|
|
struct folio *folio;
|
|
|
|
idx = start >> PAGE_SHIFT;
|
|
idx_end = end >> PAGE_SHIFT;
|
|
from = start & ~PAGE_MASK;
|
|
|
|
if (idx == idx_end)
|
|
idx_end++;
|
|
|
|
for (; idx < idx_end; idx++, from = 0) {
|
|
u32 len;
|
|
|
|
folio = read_mapping_folio(sb->s_bdev->bd_mapping, idx, NULL);
|
|
if (IS_ERR(folio)) {
|
|
ntfs_error(sb, "Unable to read %ld page", idx);
|
|
return PTR_ERR(folio);
|
|
}
|
|
|
|
offset = (loff_t)idx << PAGE_SHIFT;
|
|
to = min_t(u32, end - offset, PAGE_SIZE);
|
|
len = to - from;
|
|
|
|
memcpy_to_folio(folio, from, buf + buf_off, len);
|
|
buf_off += len;
|
|
folio_mark_uptodate(folio);
|
|
folio_mark_dirty(folio);
|
|
folio_put(folio);
|
|
}
|
|
|
|
return 0;
|
|
}
|