* [PATCH v2 0/7] block device fixes for large block sizes, IOCB_NOWAIT, and direct I/O
@ 2026-08-28 13:49 Tal Zussman
2026-08-28 13:49 ` [PATCH v2 1/7] block: use iomap_dirty_folio for block devices Tal Zussman
` (7 more replies)
0 siblings, 8 replies; 10+ messages in thread
From: Tal Zussman @ 2026-08-28 13:49 UTC (permalink / raw)
To: Jens Axboe, Christoph Hellwig, Johannes Thumshirn,
Luis Chamberlain, Hannes Reinecke, Matthew Wilcox (Oracle),
John Garry, Christian Brauner, Darrick J. Wong, Keith Busch,
Martin K. Petersen
Cc: linux-block, linux-kernel, Sashiko, Tal Zussman
A set of independent fixes for the block device file operations. The
first two were found by Sashiko while reviewing the RWF_DONTCACHE series
[1]. The fourth and fifth were found by Sashiko's review of v1 of this
series, and the rest came from asking an LLM to find any similar or
related issues. Each issue has been reproduced, with the fixes resolving
the issues.
Patch 1 fixes silently lost mmap writes with CONFIG_BUFFER_HEAD=n.
Patches 2 and 3 take i_rwsem around the direct I/O write fallback and
the splice read path, which race set_blocksize() changing the mapping's
minimum folio order.
Patch 4 makes the buffered read path honor IOCB_NOWAIT instead of
blocking on i_rwsem.
Patch 5 makes IOCB_ATOMIC writes fail instead of tearing and falling
back to buffered I/O. Block devices can reject both paths into the
fallback before submitting any I/O, so we fail rather than issue a
WARN() like ext4 does.
Patch 6 fixes leaked page pins in bio_iov_iter_align_down(), and patch
7 removes dead metadata handling in the async direct I/O path.
These issues are currently unlikely to be hit in practice due to the
specific configurations required to trigger them.
The reproducer for patch 1 has been submitted to blktests [2].
[1] https://lore.kernel.org/all/20260730-blk-dontcache-v7-0-3e8e6850068d@columbia.edu/
[2] https://github.com/linux-blktests/blktests/pull/258
---
Changes in v2:
- Rebase on current master.
- 1/7: Add Christoph's Reviewed-by and submit the reproducer to
blktests.
- 2/7: Skip the buffered fallback for IOCB_NOWAIT direct writes, per
Sashiko.
- 2/7: Change the Fixes: commit from c0e473a0d226 to 3c20917120ce
- 3/7 to 7/7: New patches.
- Link to v1: https://lore.kernel.org/r/20260802-blkdev-fixes-v1-0-a82fc549fd74@columbia.edu
---
Tal Zussman (7):
block: use iomap_dirty_folio for block devices
block: take i_rwsem for the direct I/O write fallback
block: take i_rwsem for the splice read path
block: honor IOCB_NOWAIT in the block device buffered read path
block: fail atomic writes instead of falling back to buffered I/O
block: unpin all pages of a bvec in bio_iov_iter_align_down()
block: remove dead metadata handling from the async direct I/O path
block/bio.c | 18 ++++++++++++-
block/fops.c | 84 ++++++++++++++++++++++++++++++++++++++++++++++++------------
2 files changed, 84 insertions(+), 18 deletions(-)
---
base-commit: 1b78070aaef63512688aebfbc82365ef9d6660f1
change-id: 20260801-blkdev-fixes-771b1c314ebb
Best regards,
--
Tal Zussman <tz2294@columbia.edu>
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH v2 1/7] block: use iomap_dirty_folio for block devices
2026-08-28 13:49 [PATCH v2 0/7] block device fixes for large block sizes, IOCB_NOWAIT, and direct I/O Tal Zussman
@ 2026-08-28 13:49 ` Tal Zussman
2026-08-28 13:49 ` [PATCH v2 2/7] block: take i_rwsem for the direct I/O write fallback Tal Zussman
` (6 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Tal Zussman @ 2026-08-28 13:49 UTC (permalink / raw)
To: Jens Axboe, Christoph Hellwig, Johannes Thumshirn,
Luis Chamberlain, Hannes Reinecke, Matthew Wilcox (Oracle),
John Garry, Christian Brauner, Darrick J. Wong, Keith Busch,
Martin K. Petersen
Cc: linux-block, linux-kernel, Sashiko, Tal Zussman
With CONFIG_BUFFER_HEAD=n, block devices are written back through iomap,
but def_blk_aops uses filemap_dirty_folio, which only sets PG_dirty. It
does not set the per-block dirty bits in the folio's iomap_folio_state,
so iomap_writeback_folio() finds no dirty range, submits no I/O and
clears PG_dirty, resulting in data loss.
Other iomap users set .dirty_folio to iomap_dirty_folio, which marks the
folio's blocks dirty before calling filemap_dirty_folio().
This is only observable with block size < folio size. With a single
block there is no iomap_folio_state to get out of sync and
iomap_writeback_folio() marks the whole folio dirty itself. For a
page-aligned device, this may require using the BLKBSZSET ioctl to set
the block size, which requires CAP_SYS_ADMIN. A device whose size is not
page aligned already gets a sub-page block size from
set_init_blocksize(), so no ioctl and no privilege is needed.
To reproduce, on a device with a sub-page block size, write a known
pattern with O_DIRECT, mmap the same range, store to it, msync() and
fsync(), then read it back with O_DIRECT. A reproducer is available at
[1].
[1] https://gist.github.com/tzussman/18ab05cba4b3fdc79cce0a69d1fd05b4
Fixes: 925c86a19bac ("fs: add CONFIG_BUFFER_HEAD")
Reported-by: Sashiko <sashiko-bot@kernel.org>
Link: https://sashiko.dev/#/patchset/20260730-blk-dontcache-v7-0-3e8e6850068d%40columbia.edu?part=5
Reviewed-by: Christoph Hellwig <hch@lst.de>
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
block/fops.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/block/fops.c b/block/fops.c
index 2ce7c6c4714e..c57784773fe1 100644
--- a/block/fops.c
+++ b/block/fops.c
@@ -560,7 +560,7 @@ static int blkdev_writepages(struct address_space *mapping,
}
const struct address_space_operations def_blk_aops = {
- .dirty_folio = filemap_dirty_folio,
+ .dirty_folio = iomap_dirty_folio,
.release_folio = iomap_release_folio,
.invalidate_folio = iomap_invalidate_folio,
.read_folio = blkdev_read_folio,
--
2.39.5
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH v2 2/7] block: take i_rwsem for the direct I/O write fallback
2026-08-28 13:49 [PATCH v2 0/7] block device fixes for large block sizes, IOCB_NOWAIT, and direct I/O Tal Zussman
2026-08-28 13:49 ` [PATCH v2 1/7] block: use iomap_dirty_folio for block devices Tal Zussman
@ 2026-08-28 13:49 ` Tal Zussman
2026-08-28 13:49 ` [PATCH v2 3/7] block: take i_rwsem for the splice read path Tal Zussman
` (5 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Tal Zussman @ 2026-08-28 13:49 UTC (permalink / raw)
To: Jens Axboe, Christoph Hellwig, Johannes Thumshirn,
Luis Chamberlain, Hannes Reinecke, Matthew Wilcox (Oracle),
John Garry, Christian Brauner, Darrick J. Wong, Keith Busch,
Martin K. Petersen
Cc: linux-block, linux-kernel, Sashiko, Tal Zussman
Commit c0e473a0d226 ("block: fix race between set_blocksize and read
paths") closed a race between set_blocksize() and block device I/O: with
large sector size support, set_blocksize() can change i_blkbits and the
mapping's minimum folio order while a concurrent reader still holds a
folio of the old, smaller order, leading to crashes. In particular, it
made blkdev_write_iter() wrap buffered writes in inode_lock_shared().
However, the direct I/O fallback path was missed in that conversion.
blkdev_write_iter() passes blkdev_buffered_write() as an argument to
direct_write_fallback() with no lock held. A direct write that completes
only partially then finishes as a buffered write with no protection.
This can cause a BUG by racing partial direct writes against
ioctl(BLKBSZSET). Writer threads issue O_DIRECT pwritev() with a
two-segment iovec whose second segment is an unreadable PROT_NONE
mapping. The direct path then writes the first segment, fails to pin the
second, and returns short, entering the fallback. A second thread keeps
toggling the second segment's protection so that some fallbacks get past
fault_in_iov_iter_readable() and reach the page cache, a third thread
populates the page cache with folios of the current block size via
pread() and readahead(), and a fourth thread toggles the block size
between 512 bytes and 64K with BLKBSZSET. The minimum folio order only
moves with block sizes above PAGE_SIZE, i.e. with
CONFIG_TRANSPARENT_HUGEPAGE raising BLK_MAX_BLOCK_SIZE to 64K.
On a CONFIG_DEBUG_VM kernel this yields the following BUG:
page dumped because: VM_BUG_ON_FOLIO(folio_order(folio) < mapping_min_folio_order(mapping))
kernel BUG at mm/filemap.c:858!
Oops: invalid opcode: 0000 [#1] SMP KASAN NOPTI
RIP: 0010:__filemap_add_folio+0x860/0x8d0
Call Trace:
filemap_add_folio+0xc9/0x1f0
__filemap_get_folio_mpol+0x240/0x660
iomap_write_begin+0xa87/0xd70
iomap_file_buffered_write+0x304/0x6a0
blkdev_write_iter+0x255/0x510
do_iter_readv_writev+0x23d/0x3c0
vfs_writev+0x211/0x7d0
do_pwritev+0x121/0x190
do_syscall_64+0x121/0x630
entry_SYSCALL_64_after_hwframe+0x77/0x7f
The same workload also trips WARN_ON_ONCE(pos >= folio_pos(folio) +
fsize) in iomap_trim_folio_range().
Fix this by calling blkdev_buffered_write() in the fallback path under
inode_lock_shared(), matching the plain buffered-write branch. With the
fix the same workload runs clean.
A short IOCB_NOWAIT direct write reaches the same fallback. Taking
i_rwsem there can now block behind set_blocksize(), and the fallback
already blocks on writeback of the data it copied in
direct_write_fallback(). blkdev_write_iter() already rejects a purely
buffered IOCB_NOWAIT write with -EOPNOTSUPP, so do not enter the
fallback for IOCB_NOWAIT at all: return the bytes the direct path
already wrote, or -EAGAIN if none, and let the caller retry.
The reproducer used was written by an LLM, and is available at [1].
[1] https://gist.github.com/tzussman/69d06bc57d42a42989eb038b1b5aeb74
Fixes: 3c20917120ce ("block/bdev: enable large folio support for large logical block sizes")
Reported-by: Sashiko <sashiko-bot@kernel.org>
Link: https://sashiko.dev/#/patchset/20260730-blk-dontcache-v7-0-3e8e6850068d%40columbia.edu?part=5
Assisted-by: Claude:claude-fable-5
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
block/fops.c | 23 ++++++++++++++++++++---
1 file changed, 20 insertions(+), 3 deletions(-)
diff --git a/block/fops.c b/block/fops.c
index c57784773fe1..d5f569333f46 100644
--- a/block/fops.c
+++ b/block/fops.c
@@ -765,9 +765,26 @@ static ssize_t blkdev_write_iter(struct kiocb *iocb, struct iov_iter *from)
if (iocb->ki_flags & IOCB_DIRECT) {
ret = blkdev_direct_write(iocb, from);
- if (ret >= 0 && iov_iter_count(from))
- ret = direct_write_fallback(iocb, from, ret,
- blkdev_buffered_write(iocb, from));
+ if (ret >= 0 && iov_iter_count(from)) {
+ if (iocb->ki_flags & IOCB_NOWAIT) {
+ /*
+ * The buffered fallback blocks on i_rwsem and
+ * on writeback of the data it copied: return
+ * the short direct write instead and let the
+ * caller retry.
+ */
+ if (!ret)
+ ret = -EAGAIN;
+ } else {
+ ssize_t ret2;
+
+ inode_lock_shared(bd_inode);
+ ret2 = blkdev_buffered_write(iocb, from);
+ inode_unlock_shared(bd_inode);
+ ret = direct_write_fallback(iocb, from, ret,
+ ret2);
+ }
+ }
} else {
/*
* Take i_rwsem and invalidate_lock to avoid racing with
--
2.39.5
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH v2 3/7] block: take i_rwsem for the splice read path
2026-08-28 13:49 [PATCH v2 0/7] block device fixes for large block sizes, IOCB_NOWAIT, and direct I/O Tal Zussman
2026-08-28 13:49 ` [PATCH v2 1/7] block: use iomap_dirty_folio for block devices Tal Zussman
2026-08-28 13:49 ` [PATCH v2 2/7] block: take i_rwsem for the direct I/O write fallback Tal Zussman
@ 2026-08-28 13:49 ` Tal Zussman
2026-08-28 13:49 ` [PATCH v2 4/7] block: honor IOCB_NOWAIT in the block device buffered " Tal Zussman
` (4 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Tal Zussman @ 2026-08-28 13:49 UTC (permalink / raw)
To: Jens Axboe, Christoph Hellwig, Johannes Thumshirn,
Luis Chamberlain, Hannes Reinecke, Matthew Wilcox (Oracle),
John Garry, Christian Brauner, Darrick J. Wong, Keith Busch,
Martin K. Petersen
Cc: linux-block, linux-kernel, Tal Zussman
def_blk_fops wires ->splice_read directly to filemap_splice_read(),
which allocates folios based on mapping_min_folio_order() without any
lock against set_blocksize(). A splice from a block device can race
set_blocksize() raising the minimum folio order and insert a folio that
is too small for the mapping. blkdev_read_iter() wraps filemap_read()
in inode_lock_shared() for this reason, but the splice path was missed.
Splicing from a block device while toggling the block size between 512
bytes and 64K with BLKBSZSET hits this within seconds on a
CONFIG_DEBUG_VM kernel:
page dumped because: VM_BUG_ON_FOLIO(folio_order(folio) < mapping_min_folio_order(mapping))
kernel BUG at mm/filemap.c:858!
Oops: invalid opcode: 0000 [#1] SMP NOPTI
RIP: 0010:__filemap_add_folio+0x51c/0x570
Call Trace:
filemap_add_folio+0x64/0x140
page_cache_ra_order+0x1dd/0x3d0
filemap_get_pages+0x153/0x760
filemap_splice_read+0x13f/0x300
splice_file_to_pipe+0xc0/0xd0
do_splice+0x6a8/0x890
__do_splice+0xb0/0x210
__x64_sys_splice+0x80/0x100
do_syscall_64+0x10e/0x520
entry_SYSCALL_64_after_hwframe+0x77/0x7f
Take inode_lock_shared() around filemap_splice_read(), like the read
path does.
Fixes: 3c20917120ce ("block/bdev: enable large folio support for large logical block sizes")
Assisted-by: Claude:claude-fable-5
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
block/fops.c | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/block/fops.c b/block/fops.c
index d5f569333f46..a51814821100 100644
--- a/block/fops.c
+++ b/block/fops.c
@@ -855,6 +855,22 @@ static ssize_t blkdev_read_iter(struct kiocb *iocb, struct iov_iter *to)
return ret;
}
+/*
+ * Take i_rwsem to avoid racing with set_blocksize changing i_blkbits/folio
+ * order and punching out the pagecache.
+ */
+static ssize_t blkdev_splice_read(struct file *in, loff_t *ppos,
+ struct pipe_inode_info *pipe, size_t len, unsigned int flags)
+{
+ struct inode *bd_inode = bdev_file_inode(in);
+ ssize_t ret;
+
+ inode_lock_shared(bd_inode);
+ ret = filemap_splice_read(in, ppos, pipe, len, flags);
+ inode_unlock_shared(bd_inode);
+ return ret;
+}
+
#define BLKDEV_FALLOC_FL_SUPPORTED \
(FALLOC_FL_KEEP_SIZE | FALLOC_FL_PUNCH_HOLE | \
FALLOC_FL_ZERO_RANGE | FALLOC_FL_WRITE_ZEROES)
@@ -956,7 +972,7 @@ const struct file_operations def_blk_fops = {
#ifdef CONFIG_COMPAT
.compat_ioctl = compat_blkdev_ioctl,
#endif
- .splice_read = filemap_splice_read,
+ .splice_read = blkdev_splice_read,
.splice_write = iter_file_splice_write,
.fallocate = blkdev_fallocate,
.uring_cmd = blkdev_uring_cmd,
--
2.39.5
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH v2 4/7] block: honor IOCB_NOWAIT in the block device buffered read path
2026-08-28 13:49 [PATCH v2 0/7] block device fixes for large block sizes, IOCB_NOWAIT, and direct I/O Tal Zussman
` (2 preceding siblings ...)
2026-08-28 13:49 ` [PATCH v2 3/7] block: take i_rwsem for the splice read path Tal Zussman
@ 2026-08-28 13:49 ` Tal Zussman
2026-08-28 13:49 ` [PATCH v2 5/7] block: fail atomic writes instead of falling back to buffered I/O Tal Zussman
` (3 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Tal Zussman @ 2026-08-28 13:49 UTC (permalink / raw)
To: Jens Axboe, Christoph Hellwig, Johannes Thumshirn,
Luis Chamberlain, Hannes Reinecke, Matthew Wilcox (Oracle),
John Garry, Christian Brauner, Darrick J. Wong, Keith Busch,
Martin K. Petersen
Cc: linux-block, linux-kernel, Sashiko, Tal Zussman
blkdev_read_iter() takes inode_lock_shared() unconditionally around
filemap_read(). Unlike blkdev_write_iter(), it does not reject
IOCB_NOWAIT for buffered I/O, so a non-blocking read, or the buffered
tail of a short IOCB_NOWAIT direct read, blocks behind set_blocksize()
holding i_rwsem across sync_blockdev().
A preadv2(RWF_NOWAIT) issued while another thread changes the block
size with a dirty page cache blocks for as long as sync_blockdev()
takes, 4 to 6 seconds on a scsi_debug device with delay=5.
Use inode_trylock_shared() for IOCB_NOWAIT and return the bytes the
direct path already read, or -EAGAIN if none, when the lock is
contended, preserving NOWAIT semantics.
Fixes: c0e473a0d226 ("block: fix race between set_blocksize and read paths")
Reported-by: Sashiko <sashiko-bot@kernel.org>
Link: https://sashiko.dev/#/patchset/20260802-blkdev-fixes-v1-0-a82fc549fd74%40columbia.edu?part=2
Assisted-by: Claude:claude-fable-5
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
block/fops.c | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/block/fops.c b/block/fops.c
index a51814821100..a3a709697b40 100644
--- a/block/fops.c
+++ b/block/fops.c
@@ -845,7 +845,15 @@ static ssize_t blkdev_read_iter(struct kiocb *iocb, struct iov_iter *to)
* Take i_rwsem and invalidate_lock to avoid racing with set_blocksize
* changing i_blkbits/folio order and punching out the pagecache.
*/
- inode_lock_shared(bd_inode);
+ if (iocb->ki_flags & IOCB_NOWAIT) {
+ if (!inode_trylock_shared(bd_inode)) {
+ if (!ret)
+ ret = -EAGAIN;
+ goto reexpand;
+ }
+ } else {
+ inode_lock_shared(bd_inode);
+ }
ret = filemap_read(iocb, to, ret);
inode_unlock_shared(bd_inode);
--
2.39.5
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH v2 5/7] block: fail atomic writes instead of falling back to buffered I/O
2026-08-28 13:49 [PATCH v2 0/7] block device fixes for large block sizes, IOCB_NOWAIT, and direct I/O Tal Zussman
` (3 preceding siblings ...)
2026-08-28 13:49 ` [PATCH v2 4/7] block: honor IOCB_NOWAIT in the block device buffered " Tal Zussman
@ 2026-08-28 13:49 ` Tal Zussman
2026-08-28 13:49 ` [PATCH v2 6/7] block: unpin all pages of a bvec in bio_iov_iter_align_down() Tal Zussman
` (2 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Tal Zussman @ 2026-08-28 13:49 UTC (permalink / raw)
To: Jens Axboe, Christoph Hellwig, Johannes Thumshirn,
Luis Chamberlain, Hannes Reinecke, Matthew Wilcox (Oracle),
John Garry, Christian Brauner, Darrick J. Wong, Keith Busch,
Martin K. Petersen
Cc: linux-block, linux-kernel, Sashiko, Tal Zussman
An IOCB_ATOMIC direct write to a block device can silently lose its
torn-write guarantee in two ways:
1. blkdev_direct_write() turns an -EBUSY from page cache invalidation
into a 0 return, so the whole write is retried through
blkdev_buffered_write(), with no atomicity guarantee.
2. On a partial page pin, __blkdev_direct_IO_simple() and
__blkdev_direct_IO_async() submit what was pinned with REQ_ATOMIC
set and leave the rest to the buffered fallback.
The second case can be triggered deterministically. A 16K
pwritev2(RWF_ATOMIC) whose last page is PROT_NONE, on a scsi_debug
device with atomic_wr=1, completes short with only three of the four
pages written, violating RWF_ATOMIC semantics.
Fail the I/O instead. Return -EAGAIN when page cache invalidation fails
for IOCB_ATOMIC rather than retrying through the page cache, matching
__iomap_dio_rw(), which treats the failure as transient and lets the
caller retry. Release a short atomic pin and return -EFAULT before
submission, which is what a direct write already returns when none of
the buffer can be pinned. A sync atomic write can then never return
short with a remainder, so the buffered fallback is never reached.
ext4 has the same fallback and only warns in it. For block devices both
ways in can be detected before any I/O is submitted, so fail early instead.
Fixes: caf336f81b3a ("block: Add fops atomic write support")
Reported-by: Sashiko <sashiko-bot@kernel.org>
Link: https://sashiko.dev/#/patchset/20260802-blkdev-fixes-v1-0-a82fc549fd74%40columbia.edu?part=2
Assisted-by: Claude:claude-fable-5
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
block/fops.c | 21 ++++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/block/fops.c b/block/fops.c
index a3a709697b40..8769bb13df1c 100644
--- a/block/fops.c
+++ b/block/fops.c
@@ -87,6 +87,12 @@ static ssize_t __blkdev_direct_IO_simple(struct kiocb *iocb,
ret = blkdev_iov_iter_get_pages(&bio, iter, bdev);
if (unlikely(ret))
goto out;
+ if ((iocb->ki_flags & IOCB_ATOMIC) && iov_iter_count(iter)) {
+ /* a short atomic write would be torn by definition */
+ bio_release_pages(&bio, false);
+ ret = -EFAULT;
+ goto out;
+ }
ret = bio.bi_iter.bi_size;
if (iov_iter_rw(iter) == WRITE)
@@ -352,6 +358,12 @@ static ssize_t __blkdev_direct_IO_async(struct kiocb *iocb,
ret = blkdev_iov_iter_get_pages(bio, iter, bdev);
if (unlikely(ret))
goto out_bio_put;
+ if ((iocb->ki_flags & IOCB_ATOMIC) && iov_iter_count(iter)) {
+ /* a short atomic write would be torn by definition */
+ bio_release_pages(bio, false);
+ ret = -EFAULT;
+ goto out_bio_put;
+ }
}
dio->size = bio->bi_iter.bi_size;
@@ -691,8 +703,15 @@ blkdev_direct_write(struct kiocb *iocb, struct iov_iter *from)
written = kiocb_invalidate_pages(iocb, count);
if (written) {
- if (written == -EBUSY)
+ /*
+ * The buffered write fallback cannot provide torn-write
+ * protection, so atomic writes must fail instead.
+ */
+ if (written == -EBUSY) {
+ if (iocb->ki_flags & IOCB_ATOMIC)
+ return -EAGAIN;
return 0;
+ }
return written;
}
--
2.39.5
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH v2 6/7] block: unpin all pages of a bvec in bio_iov_iter_align_down()
2026-08-28 13:49 [PATCH v2 0/7] block device fixes for large block sizes, IOCB_NOWAIT, and direct I/O Tal Zussman
` (4 preceding siblings ...)
2026-08-28 13:49 ` [PATCH v2 5/7] block: fail atomic writes instead of falling back to buffered I/O Tal Zussman
@ 2026-08-28 13:49 ` Tal Zussman
2026-08-28 14:36 ` Tal Zussman
2026-08-28 13:49 ` [PATCH v2 7/7] block: remove dead metadata handling from the async direct I/O path Tal Zussman
2026-08-28 15:32 ` [PATCH v2 0/7] block device fixes for large block sizes, IOCB_NOWAIT, and direct I/O Tal Zussman
7 siblings, 1 reply; 10+ messages in thread
From: Tal Zussman @ 2026-08-28 13:49 UTC (permalink / raw)
To: Jens Axboe, Christoph Hellwig, Johannes Thumshirn,
Luis Chamberlain, Hannes Reinecke, Matthew Wilcox (Oracle),
John Garry, Christian Brauner, Darrick J. Wong, Keith Busch,
Martin K. Petersen
Cc: linux-block, linux-kernel, Tal Zussman
bio_iov_iter_align_down() drops trailing bvecs with unpin_user_page(),
but a bvec built by iov_iter_extract_bvecs() can span several pages of
one folio, each with its own pin. All but the first pin leak.
The partially trimmed bvec has the same problem. Shrinking bv_len does
not release the pins for the pages cut off by the trim, and
__bio_release_pages() only unpins the pages bv_len still covers at
completion.
Both issues occur only with a logical block size above PAGE_SIZE and a
large folio backing the user buffer. On a device with a 64K logical
block size, an O_DIRECT pwritev() from a hugetlb mapping that ends 16K
past a block boundary leaks one huge page per call, whether the
remainder is its own bvec or the tail of a larger one.
Unpin all pages of a dropped bvec with unpin_user_folio(), as
__bio_release_pages() does, and unpin the pages trimmed off the last
bvec as well.
Fixes: 20a0e6276edb ("block: align the bio after building it")
Assisted-by: Claude:claude-fable-5
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
block/bio.c | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/block/bio.c b/block/bio.c
index 898b2f5ef8c8..48fa6b9a6dba 100644
--- a/block/bio.c
+++ b/block/bio.c
@@ -1196,6 +1196,11 @@ bool bio_iov_iter_set(struct bio *bio, const struct iov_iter *iter)
return true;
}
+static unsigned int bvec_nr_pages(const struct bio_vec *bv)
+{
+ return DIV_ROUND_UP(bv->bv_offset + bv->bv_len, PAGE_SIZE);
+}
+
/*
* Aligns the bio size to the len_align_mask, releasing excessive bio vecs that
* __bio_iov_iter_get_pages may have inserted, and reverts the trimmed length
@@ -1205,6 +1210,7 @@ static int bio_iov_iter_align_down(struct bio *bio, struct iov_iter *iter,
struct bio_vec *bv, unsigned len_align_mask)
{
size_t nbytes = bio->bi_iter.bi_size & len_align_mask;
+ unsigned int npages;
if (!nbytes)
return 0;
@@ -1213,14 +1219,24 @@ static int bio_iov_iter_align_down(struct bio *bio, struct iov_iter *iter,
bio->bi_iter.bi_size -= nbytes;
while (nbytes >= bv->bv_len) {
if (bio_flagged(bio, BIO_PAGE_PINNED))
- unpin_user_page(bv->bv_page);
+ unpin_user_folio(bvec_folio(bv),
+ bvec_nr_pages(bv));
if (!--bio->bi_vcnt)
return -EFAULT;
nbytes -= bv->bv_len;
bv--;
}
+
+ /*
+ * __bio_release_pages() only unpins the pages still covered by
+ * bv_len, so drop the pins for the pages trimmed off here.
+ */
+ npages = bvec_nr_pages(bv);
bv->bv_len -= nbytes;
+ npages -= bvec_nr_pages(bv);
+ if (npages && bio_flagged(bio, BIO_PAGE_PINNED))
+ unpin_user_folio(bvec_folio(bv), npages);
return 0;
}
--
2.39.5
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH v2 7/7] block: remove dead metadata handling from the async direct I/O path
2026-08-28 13:49 [PATCH v2 0/7] block device fixes for large block sizes, IOCB_NOWAIT, and direct I/O Tal Zussman
` (5 preceding siblings ...)
2026-08-28 13:49 ` [PATCH v2 6/7] block: unpin all pages of a bvec in bio_iov_iter_align_down() Tal Zussman
@ 2026-08-28 13:49 ` Tal Zussman
2026-08-28 15:32 ` [PATCH v2 0/7] block device fixes for large block sizes, IOCB_NOWAIT, and direct I/O Tal Zussman
7 siblings, 0 replies; 10+ messages in thread
From: Tal Zussman @ 2026-08-28 13:49 UTC (permalink / raw)
To: Jens Axboe, Christoph Hellwig, Johannes Thumshirn,
Luis Chamberlain, Hannes Reinecke, Matthew Wilcox (Oracle),
John Garry, Christian Brauner, Darrick J. Wong, Keith Busch,
Martin K. Petersen
Cc: linux-block, linux-kernel, Tal Zussman
Since commit 2729a60bbfb9 ("block: don't silently ignore metadata for
sync read/write"), blkdev_direct_IO() sends every IOCB_HAS_METADATA
request to __blkdev_direct_IO(). Remove the now unreachable metadata
mapping in __blkdev_direct_IO_async() and the unmap in its completion
handler.
No functional change.
Assisted-by: Claude:claude-fable-5
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
block/fops.c | 10 ----------
1 file changed, 10 deletions(-)
diff --git a/block/fops.c b/block/fops.c
index 8769bb13df1c..1964cb113842 100644
--- a/block/fops.c
+++ b/block/fops.c
@@ -312,9 +312,6 @@ static void blkdev_bio_end_io_async(struct bio *bio)
ret = blk_status_to_errno(bio->bi_status);
}
- if (bio_integrity(bio))
- bio_integrity_unmap_user(bio);
-
iocb->ki_complete(iocb, ret);
if (dio->flags & DIO_SHOULD_DIRTY) {
@@ -376,13 +373,6 @@ static ssize_t __blkdev_direct_IO_async(struct kiocb *iocb,
task_io_account_write(bio->bi_iter.bi_size);
}
- if (iocb->ki_flags & IOCB_HAS_METADATA) {
- ret = bio_integrity_map_iter(bio, iocb->private);
- WRITE_ONCE(iocb->private, NULL);
- if (unlikely(ret))
- goto out_bio_put;
- }
-
if (iocb->ki_flags & IOCB_ATOMIC)
bio->bi_opf |= REQ_ATOMIC;
--
2.39.5
^ permalink raw reply related [flat|nested] 10+ messages in thread
* Re: [PATCH v2 6/7] block: unpin all pages of a bvec in bio_iov_iter_align_down()
2026-08-28 13:49 ` [PATCH v2 6/7] block: unpin all pages of a bvec in bio_iov_iter_align_down() Tal Zussman
@ 2026-08-28 14:36 ` Tal Zussman
0 siblings, 0 replies; 10+ messages in thread
From: Tal Zussman @ 2026-08-28 14:36 UTC (permalink / raw)
To: Jens Axboe, Christoph Hellwig, Johannes Thumshirn,
Luis Chamberlain, Hannes Reinecke, Matthew Wilcox (Oracle),
John Garry, Christian Brauner, Darrick J. Wong, Keith Busch,
Martin K. Petersen
Cc: linux-block, linux-kernel
On 8/28/26 4:49 PM, Tal Zussman wrote:
> bio_iov_iter_align_down() drops trailing bvecs with unpin_user_page(),
> but a bvec built by iov_iter_extract_bvecs() can span several pages of
> one folio, each with its own pin. All but the first pin leak.
>
> The partially trimmed bvec has the same problem. Shrinking bv_len does
> not release the pins for the pages cut off by the trim, and
> __bio_release_pages() only unpins the pages bv_len still covers at
> completion.
>
> Both issues occur only with a logical block size above PAGE_SIZE and a
> large folio backing the user buffer. On a device with a 64K logical
> block size, an O_DIRECT pwritev() from a hugetlb mapping that ends 16K
> past a block boundary leaks one huge page per call, whether the
> remainder is its own bvec or the tail of a larger one.
>
> Unpin all pages of a dropped bvec with unpin_user_folio(), as
> __bio_release_pages() does, and unpin the pages trimmed off the last
> bvec as well.
>
> Fixes: 20a0e6276edb ("block: align the bio after building it")
> Assisted-by: Claude:claude-fable-5
> Signed-off-by: Tal Zussman <tz2294@columbia.edu>
> ---
> block/bio.c | 18 +++++++++++++++++-
> 1 file changed, 17 insertions(+), 1 deletion(-)
>
> diff --git a/block/bio.c b/block/bio.c
> index 898b2f5ef8c8..48fa6b9a6dba 100644
> --- a/block/bio.c
> +++ b/block/bio.c
> @@ -1196,6 +1196,11 @@ bool bio_iov_iter_set(struct bio *bio, const struct iov_iter *iter)
> return true;
> }
>
> +static unsigned int bvec_nr_pages(const struct bio_vec *bv)
> +{
> + return DIV_ROUND_UP(bv->bv_offset + bv->bv_len, PAGE_SIZE);
> +}
> +
> /*
> * Aligns the bio size to the len_align_mask, releasing excessive bio vecs that
> * __bio_iov_iter_get_pages may have inserted, and reverts the trimmed length
> @@ -1205,6 +1210,7 @@ static int bio_iov_iter_align_down(struct bio *bio, struct iov_iter *iter,
> struct bio_vec *bv, unsigned len_align_mask)
> {
> size_t nbytes = bio->bi_iter.bi_size & len_align_mask;
> + unsigned int npages;
>
> if (!nbytes)
> return 0;
> @@ -1213,14 +1219,24 @@ static int bio_iov_iter_align_down(struct bio *bio, struct iov_iter *iter,
> bio->bi_iter.bi_size -= nbytes;
> while (nbytes >= bv->bv_len) {
> if (bio_flagged(bio, BIO_PAGE_PINNED))
> - unpin_user_page(bv->bv_page);
> + unpin_user_folio(bvec_folio(bv),
> + bvec_nr_pages(bv));
And right after sending this, I noticed bvec_unpin()...
I think a cleaner solution is to move bvec_unpin() up, make bvec_nr_pages()
handle bv_offset > PAGE_SIZE, use bvec_nr_pages() in bvec_unpin(), and then
use bvec_unpin() here.
> if (!--bio->bi_vcnt)
> return -EFAULT;
> nbytes -= bv->bv_len;
> bv--;
> }
> +
> + /*
> + * __bio_release_pages() only unpins the pages still covered by
> + * bv_len, so drop the pins for the pages trimmed off here.
> + */
> + npages = bvec_nr_pages(bv);
> bv->bv_len -= nbytes;
> + npages -= bvec_nr_pages(bv);
> + if (npages && bio_flagged(bio, BIO_PAGE_PINNED))
> + unpin_user_folio(bvec_folio(bv), npages);
> return 0;
> }
>
>
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v2 0/7] block device fixes for large block sizes, IOCB_NOWAIT, and direct I/O
2026-08-28 13:49 [PATCH v2 0/7] block device fixes for large block sizes, IOCB_NOWAIT, and direct I/O Tal Zussman
` (6 preceding siblings ...)
2026-08-28 13:49 ` [PATCH v2 7/7] block: remove dead metadata handling from the async direct I/O path Tal Zussman
@ 2026-08-28 15:32 ` Tal Zussman
7 siblings, 0 replies; 10+ messages in thread
From: Tal Zussman @ 2026-08-28 15:32 UTC (permalink / raw)
To: Jens Axboe, Christoph Hellwig, Johannes Thumshirn,
Luis Chamberlain, Hannes Reinecke, Matthew Wilcox (Oracle),
John Garry, Christian Brauner, Darrick J. Wong, Keith Busch,
Martin K. Petersen
Cc: linux-block, linux-kernel, Sashiko
On 8/28/26 4:49 PM, Tal Zussman wrote:
> A set of independent fixes for the block device file operations. The
> first two were found by Sashiko while reviewing the RWF_DONTCACHE series
> [1]. The fourth and fifth were found by Sashiko's review of v1 of this
> series, and the rest came from asking an LLM to find any similar or
> related issues. Each issue has been reproduced, with the fixes resolving
> the issues.
>
A couple more pre-existing issues from Sashiko, some of which seem valid:
https://sashiko.dev/#/patchset/20260828-blkdev-fixes-v2-0-32f3f40cebed%40columbia.edu
^ permalink raw reply [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-08-28 15:32 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-28 13:49 [PATCH v2 0/7] block device fixes for large block sizes, IOCB_NOWAIT, and direct I/O Tal Zussman
2026-08-28 13:49 ` [PATCH v2 1/7] block: use iomap_dirty_folio for block devices Tal Zussman
2026-08-28 13:49 ` [PATCH v2 2/7] block: take i_rwsem for the direct I/O write fallback Tal Zussman
2026-08-28 13:49 ` [PATCH v2 3/7] block: take i_rwsem for the splice read path Tal Zussman
2026-08-28 13:49 ` [PATCH v2 4/7] block: honor IOCB_NOWAIT in the block device buffered " Tal Zussman
2026-08-28 13:49 ` [PATCH v2 5/7] block: fail atomic writes instead of falling back to buffered I/O Tal Zussman
2026-08-28 13:49 ` [PATCH v2 6/7] block: unpin all pages of a bvec in bio_iov_iter_align_down() Tal Zussman
2026-08-28 14:36 ` Tal Zussman
2026-08-28 13:49 ` [PATCH v2 7/7] block: remove dead metadata handling from the async direct I/O path Tal Zussman
2026-08-28 15:32 ` [PATCH v2 0/7] block device fixes for large block sizes, IOCB_NOWAIT, and direct I/O Tal Zussman
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox