[PATCH] exfat: zero invalid block tails in direct reads
From: Hengyu Liang
Date: Tue Oct 13 2026 - 01:17:35 EST
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@xxxxxxxxxxxxxxx
Signed-off-by: Hengyu Liang <hengyul@xxxxxxxxxx>
---
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 <linkinjeon@xxxxxxxxxx>
*/
+#include <linux/bio.h>
+#include <linux/blk-crypto.h>
#include <linux/iomap.h>
+#include <linux/slab.h>
#include <linux/pagemap.h>
#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