Commit 867b9c96dc83 ("exfat: add iomap direct I/O support") made exfat_file_read_iter() use iomap_dio_rw() for direct reads, removing the zeroing done by exfat_direct_IO(). However, the read mapping still includes the whole block containing valid_size. As of now, O_DIRECT reads return old data from the invalid tail of that block, while buffered reads return zeros. The issue can be reproduced on an exfat filesystem with 512-byte blocks mounted at /mnt: head -c 4096 /dev/zero | tr '\000' A > /mnt/f sync /mnt/f truncate -s 100 /mnt/f truncate -s 8192 /mnt/f dd if=/mnt/f of=/tmp/readout bs=4096 count=1 iflag=direct status=none od -An -tu1 -j100 -N1 /tmp/readout Before commit 867b9c96dc83 ("exfat: add iomap direct I/O support"), od prints 0. After that commit, it prints 65 ('A') instead. This patch will zero the part of that block beyond valid_size when the read bio completes, as exfat_iomap_read_end_io() does for buffered reads. Fixes: 867b9c96dc83 ("exfat: add iomap direct I/O support") Cc: stable@vger.kernel.org Signed-off-by: Hengyu Liang --- Based on the exfat dev branch (1cae92a58966). It also applies on top of "[PATCH v2 0/7] exfat: fix valid_size update races". Tested on v7.3-rc6 with fs/exfat taken from that branch (KASAN and lockdep), on volumes with 512-byte and 4K blocks: the commands above, and direct reads with pread(), preadv(), native AIO and io_uring (READ, READV and READ_FIXED) at aligned and unaligned valid sizes, after drop_caches and after a remount, including short reads at EOF and misaligned requests. Without the patch 88 of the 280 pread/AIO reads and 180 of the 384 io_uring reads get old data in their buffer; with the patch none does. The results are the same with the series above applied. The pages of the bio are zeroed, not the user addresses as exfat_direct_IO() did. This also works for asynchronous reads, and it does not depend on the buffer still being mapped writable when the read completes. The allocation in exfat_dio_read_submit_io() was failed with fault injection: the read returns ENOMEM. fs/exfat/file.c | 4 ++-- fs/exfat/iomap.c | 60 ++++++++++++++++++++++++++++++++++++++++++++++++ fs/exfat/iomap.h | 1 + 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/fs/exfat/file.c b/fs/exfat/file.c index f106db945d8d..0aa99238c0ea 100644 --- a/fs/exfat/file.c +++ b/fs/exfat/file.c @@ -941,8 +941,8 @@ static ssize_t exfat_file_read_iter(struct kiocb *iocb, struct iov_iter *iter) if (iocb->ki_flags & IOCB_DIRECT) { file_accessed(iocb->ki_filp); - ret = iomap_dio_rw(iocb, iter, &exfat_iomap_ops, NULL, 0, - NULL, 0); + ret = iomap_dio_rw(iocb, iter, &exfat_iomap_ops, + &exfat_read_dio_ops, 0, NULL, 0); } else { ret = generic_file_read_iter(iocb, iter); } diff --git a/fs/exfat/iomap.c b/fs/exfat/iomap.c index e50ecbfbfbe0..ec152776c7ff 100644 --- a/fs/exfat/iomap.c +++ b/fs/exfat/iomap.c @@ -5,13 +5,73 @@ * Copyright (C) 2026 Namjae Jeon */ +#include +#include #include +#include #include #include "exfat_raw.h" #include "exfat_fs.h" #include "iomap.h" +struct exfat_dio_read { + struct bvec_iter zero; + void *private; +}; + +/* + * exfat_dio_read_end_io - Direct I/O read bio completion handler + * + * exfat_iomap_begin() maps the whole block containing valid_size, so a + * direct read of that block also reads the stale data beyond valid_size. + * Zero it before iomap releases the pages, as exfat_iomap_read_end_io() + * does for buffered reads. + */ +static void exfat_dio_read_end_io(struct bio *bio) +{ + struct exfat_dio_read *read = bio->bi_private; + struct bvec_iter iter; + struct bio_vec bv; + + __bio_for_each_segment(bv, bio, iter, read->zero) + memzero_bvec(&bv); + + bio->bi_private = read->private; + kfree(read); + iomap_dio_bio_end_io(bio); +} + +static void exfat_dio_read_submit_io(const struct iomap_iter *iter, + struct bio *bio, loff_t pos) +{ + loff_t valid_size = exfat_get_valid_size(EXFAT_I(iter->inode)); + struct exfat_dio_read *read; + + if (pos + bio->bi_iter.bi_size > valid_size) { + read = kmalloc_obj(*read, GFP_NOFS); + if (!read) { + bio->bi_status = BLK_STS_RESOURCE; + bio_endio(bio); + return; + } + + /* bio->bi_iter may have advanced by the time the bio completes. */ + read->zero = bio->bi_iter; + if (pos < valid_size) + bio_advance_iter(bio, &read->zero, valid_size - pos); + read->private = bio->bi_private; + bio->bi_private = read; + bio->bi_end_io = exfat_dio_read_end_io; + } + + blk_crypto_submit_bio(bio); +} + +const struct iomap_dio_ops exfat_read_dio_ops = { + .submit_io = exfat_dio_read_submit_io, +}; + /* * exfat_file_write_dio_end_io - Direct I/O write completion handler * diff --git a/fs/exfat/iomap.h b/fs/exfat/iomap.h index fd8a913f7794..99853c78ce55 100644 --- a/fs/exfat/iomap.h +++ b/fs/exfat/iomap.h @@ -6,6 +6,7 @@ #ifndef _LINUX_EXFAT_IOMAP_H #define _LINUX_EXFAT_IOMAP_H +extern const struct iomap_dio_ops exfat_read_dio_ops; extern const struct iomap_dio_ops exfat_write_dio_ops; extern const struct iomap_ops exfat_iomap_ops; extern const struct iomap_ops exfat_write_iomap_ops; base-commit: 1cae92a58966f19d3e43fe34e9c4f315d4e9b822 -- 2.53.0