* [PATCH v2 1/2] xfs: refuse a pNFS LAYOUTCOMMIT of a range without blocks
2026-10-14 3:42 [PATCH v2 0/2] xfs: refuse pNFS LAYOUTCOMMITs of ranges without blocks Daejun Park via B4 Relay
@ 2026-10-14 3:42 ` Daejun Park via B4 Relay
2026-10-14 13:08 ` Christoph Hellwig
2026-10-14 3:42 ` [PATCH v2 2/2] xfs: clamp a pNFS LAYOUTCOMMIT extent before computing its end Daejun Park via B4 Relay
2026-10-14 13:05 ` [PATCH v2 0/2] xfs: refuse pNFS LAYOUTCOMMITs of ranges without blocks Christoph Hellwig
2 siblings, 1 reply; 6+ messages in thread
From: Daejun Park via B4 Relay @ 2026-10-14 3:42 UTC (permalink / raw)
To: Carlos Maiolino, linux-xfs
Cc: Christoph Hellwig, Darrick J. Wong, Dave Chinner, Chuck Lever,
Jeff Layton, Dai Ngo, Sergey Bashirov, linux-nfs,
Andrey Albershteyn, linux-kernel, stable, Daejun Park
From: Daejun Park <daejun7.park@samsung.com>
xfs_fs_commit_blocks() passes every range that a pNFS client commits to
xfs_iomap_write_unwritten(), whatever is mapped there, and the range can
be a hole. nfsd does not check the extents of a block or SCSI layout
LAYOUTCOMMIT against the layouts it handed out, and for a reclaim during
the grace period it has no layouts to check against, so a client can
commit a range that it never had a layout for.
For a hole, xfs_bmapi_write() with XFS_BMAPI_CONVERT allocates new
blocks as written extents, while the transaction only reserved blocks
for bmbt splits. A hole larger than that reservation shuts XFS down:
XFS (nvme0n1): Corruption of in-memory data (0x8) detected at
xfs_trans_mod_sb+0x817/0x960 (fs/xfs/xfs_trans.c:351).
Shutting down filesystem.
A smaller hole gets blocks that nobody wrote mapped into the file as
data, so readers see whatever those blocks held, and the LAYOUTCOMMIT
succeeds.
With pynfs as the client and an RW layout for the first 64 KiB of a
2 MiB sparse file, a LAYOUTCOMMIT of 64 KiB at offset 1 MiB, which no
layout covered, got NFS4ERR_IO and shut XFS down. A LAYOUTCOMMIT of
4 KiB at 1 MiB got NFS4_OK, and the file got a written block there. The
client wrote nothing to the device in either case.
A client may only write to blocks that a layout handed out, and
xfs_fs_map_blocks() allocates blocks for any hole in a write layout, so
a committed range without blocks holds nothing that can be committed.
Add XFS_BMAPI_CONVERT_ONLY, which makes xfs_bmapi_write() fail with -EIO
instead of allocating blocks for a hole or a delalloc extent, and pass
it from xfs_fs_commit_blocks() through xfs_iomap_write_unwritten(). Each
xfs_bmapi_write() call finds the extent and converts it in one
transaction under the ILOCK, so nothing can change it between the check
and the conversion. -EIO is what xfs_pnfs_validate_isize() returns when
the new size falls into a hole.
Ranges before the hole have been converted by the time the commit
fails, as they would have been had the client committed them in a
LAYOUTCOMMIT of their own. Later ranges are left alone, and the
timestamps and the size are not updated.
With this, both LAYOUTCOMMITs get NFS4ERR_IO, XFS keeps running and no
block is allocated.
Suggested-by: Christoph Hellwig <hch@lst.de>
Fixes: 527851124d10 ("xfs: implement pNFS export operations")
Cc: stable@vger.kernel.org
Signed-off-by: Daejun Park <daejun7.park@samsung.com>
---
fs/xfs/libxfs/xfs_bmap.c | 14 ++++++++++++++
fs/xfs/libxfs/xfs_bmap.h | 10 +++++++++-
fs/xfs/xfs_file.c | 2 +-
fs/xfs/xfs_ioend.c | 2 +-
fs/xfs/xfs_iomap.c | 7 ++++---
fs/xfs/xfs_iomap.h | 3 ++-
fs/xfs/xfs_pnfs.c | 9 ++++++++-
7 files changed, 39 insertions(+), 8 deletions(-)
diff --git a/fs/xfs/libxfs/xfs_bmap.c b/fs/xfs/libxfs/xfs_bmap.c
index 9bffde1484..fbfbfaf2b3 100644
--- a/fs/xfs/libxfs/xfs_bmap.c
+++ b/fs/xfs/libxfs/xfs_bmap.c
@@ -4130,6 +4130,8 @@ xfs_bmapi_finish(
* Returns a negative error code on failure, including -ENOSPC when it could not
* allocate any blocks and -ENOSR when it did allocate blocks to convert a
* delalloc range, but those blocks were before the passed in range.
+ * With XFS_BMAPI_CONVERT_ONLY, returns -EIO instead of allocating blocks when
+ * the range starts with a hole or a delalloc extent.
*/
int
xfs_bmapi_write(
@@ -4237,6 +4239,18 @@ xfs_bmapi_write(
* that we found, if any.
*/
if (need_alloc || wasdelay) {
+ if (flags & XFS_BMAPI_CONVERT_ONLY) {
+ /*
+ * Earlier mappings may have dirtied the
+ * transaction, so return them and fail on the
+ * next call.
+ */
+ if (n)
+ break;
+ error = -EIO;
+ goto error0;
+ }
+
bma.eof = eof;
bma.conv = !!(flags & XFS_BMAPI_CONVERT);
bma.wasdel = wasdelay;
diff --git a/fs/xfs/libxfs/xfs_bmap.h b/fs/xfs/libxfs/xfs_bmap.h
index 60f6df4ce0..a0bc9cd412 100644
--- a/fs/xfs/libxfs/xfs_bmap.h
+++ b/fs/xfs/libxfs/xfs_bmap.h
@@ -90,6 +90,13 @@ struct xfs_bmalloca {
/* Try to align allocations to the extent size hint */
#define XFS_BMAPI_EXTSZALIGN (1u << 11)
+/*
+ * Use with XFS_BMAPI_CONVERT to convert unwritten extents without allocating
+ * blocks. Stop at a hole or a delalloc extent: return the mappings found so
+ * far, or fail with -EIO if there are none.
+ */
+#define XFS_BMAPI_CONVERT_ONLY (1u << 12)
+
#define XFS_BMAPI_FLAGS \
{ XFS_BMAPI_ENTIRE, "ENTIRE" }, \
{ XFS_BMAPI_METADATA, "METADATA" }, \
@@ -102,7 +109,8 @@ struct xfs_bmalloca {
{ XFS_BMAPI_COWFORK, "COWFORK" }, \
{ XFS_BMAPI_NODISCARD, "NODISCARD" }, \
{ XFS_BMAPI_NORMAP, "NORMAP" },\
- { XFS_BMAPI_EXTSZALIGN, "EXTSZALIGN" }
+ { XFS_BMAPI_EXTSZALIGN, "EXTSZALIGN" }, \
+ { XFS_BMAPI_CONVERT_ONLY, "CONVERT_ONLY" }
static inline int xfs_bmapi_aflag(int w)
diff --git a/fs/xfs/xfs_file.c b/fs/xfs/xfs_file.c
index 7d0c8a1975..720f4a9b40 100644
--- a/fs/xfs/xfs_file.c
+++ b/fs/xfs/xfs_file.c
@@ -666,7 +666,7 @@ xfs_dio_write_end_io(
* they are converted.
*/
if (flags & IOMAP_DIO_UNWRITTEN) {
- error = xfs_iomap_write_unwritten(ip, offset, size, true);
+ error = xfs_iomap_write_unwritten(ip, offset, size, true, 0);
goto out;
}
diff --git a/fs/xfs/xfs_ioend.c b/fs/xfs/xfs_ioend.c
index f0e01ac34d..6d4d084c71 100644
--- a/fs/xfs/xfs_ioend.c
+++ b/fs/xfs/xfs_ioend.c
@@ -237,7 +237,7 @@ xfs_end_ioend_write(
if (ioend->io_flags & IOMAP_IOEND_SHARED)
error = xfs_reflink_end_cow(ip, offset, size);
else if (ioend->io_flags & IOMAP_IOEND_UNWRITTEN)
- error = xfs_iomap_write_unwritten(ip, offset, size, false);
+ error = xfs_iomap_write_unwritten(ip, offset, size, false, 0);
if (!error &&
!(ioend->io_flags & IOMAP_IOEND_DIRECT) &&
diff --git a/fs/xfs/xfs_iomap.c b/fs/xfs/xfs_iomap.c
index 329c745e31..7c3630be65 100644
--- a/fs/xfs/xfs_iomap.c
+++ b/fs/xfs/xfs_iomap.c
@@ -614,7 +614,8 @@ xfs_iomap_write_unwritten(
xfs_inode_t *ip,
xfs_off_t offset,
xfs_off_t count,
- bool update_isize)
+ bool update_isize,
+ uint32_t bmapi_flags)
{
xfs_mount_t *mp = ip->i_mount;
xfs_fileoff_t offset_fsb;
@@ -676,8 +677,8 @@ xfs_iomap_write_unwritten(
*/
nimaps = 1;
error = xfs_bmapi_write(tp, ip, offset_fsb, count_fsb,
- XFS_BMAPI_CONVERT, resblks, &imap,
- &nimaps);
+ XFS_BMAPI_CONVERT | bmapi_flags,
+ resblks, &imap, &nimaps);
if (error)
goto error_on_bmapi_transaction;
diff --git a/fs/xfs/xfs_iomap.h b/fs/xfs/xfs_iomap.h
index f2520a9b3a..8fc0c6fc8b 100644
--- a/fs/xfs/xfs_iomap.h
+++ b/fs/xfs/xfs_iomap.h
@@ -15,7 +15,8 @@ struct xfs_zone_alloc_ctx;
int xfs_iomap_write_direct(struct xfs_inode *ip, xfs_fileoff_t offset_fsb,
xfs_fileoff_t count_fsb, unsigned int flags,
struct xfs_bmbt_irec *imap, u64 *sequence);
-int xfs_iomap_write_unwritten(struct xfs_inode *, xfs_off_t, xfs_off_t, bool);
+int xfs_iomap_write_unwritten(struct xfs_inode *ip, xfs_off_t offset,
+ xfs_off_t count, bool update_isize, uint32_t bmapi_flags);
xfs_fileoff_t xfs_iomap_eof_align_last_fsb(struct xfs_inode *ip,
xfs_fileoff_t end_fsb);
diff --git a/fs/xfs/xfs_pnfs.c b/fs/xfs/xfs_pnfs.c
index ab38561700..05c9660aea 100644
--- a/fs/xfs/xfs_pnfs.c
+++ b/fs/xfs/xfs_pnfs.c
@@ -328,7 +328,14 @@ xfs_fs_commit_blocks(
(end - 1) >> PAGE_SHIFT);
WARN_ON_ONCE(error);
- error = xfs_iomap_write_unwritten(ip, start, length, false);
+ /*
+ * A client may only write to blocks that a layout handed out,
+ * and xfs_fs_map_blocks() allocates blocks for any hole in a
+ * write layout, so refuse to allocate blocks for a hole or a
+ * delalloc extent in the committed range.
+ */
+ error = xfs_iomap_write_unwritten(ip, start, length, false,
+ XFS_BMAPI_CONVERT_ONLY);
if (error)
goto out_drop_iolock;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 6+ messages in thread* [PATCH v2 2/2] xfs: clamp a pNFS LAYOUTCOMMIT extent before computing its end
2026-10-14 3:42 [PATCH v2 0/2] xfs: refuse pNFS LAYOUTCOMMITs of ranges without blocks Daejun Park via B4 Relay
2026-10-14 3:42 ` [PATCH v2 1/2] xfs: refuse a pNFS LAYOUTCOMMIT of a range " Daejun Park via B4 Relay
@ 2026-10-14 3:42 ` Daejun Park via B4 Relay
2026-10-14 13:09 ` Christoph Hellwig
2026-10-14 13:05 ` [PATCH v2 0/2] xfs: refuse pNFS LAYOUTCOMMITs of ranges without blocks Christoph Hellwig
2 siblings, 1 reply; 6+ messages in thread
From: Daejun Park via B4 Relay @ 2026-10-14 3:42 UTC (permalink / raw)
To: Carlos Maiolino, linux-xfs
Cc: Christoph Hellwig, Darrick J. Wong, Dave Chinner, Chuck Lever,
Jeff Layton, Dai Ngo, Sergey Bashirov, linux-nfs,
Andrey Albershteyn, linux-kernel, stable, Daejun Park
From: Daejun Park <daejun7.park@samsung.com>
xfs_fs_commit_blocks() clamps the end of each committed extent to the
file size, but computes the end as offset + length first. A length
from the client can make that sum wrap around, and the loop then
passes the client's length, unclamped, to xfs_iomap_write_unwritten().
xfs_bmapi_write() then fails an assertion on XFS_DEBUG kernels, which
is a BUG with XFS_ASSERT_FATAL, and otherwise returns -ENOSR, which
nfsd warns about as a non-standard errno.
With pynfs as the client and an RW layout for the first 64 KiB of a
2 MiB sparse file, a LAYOUTCOMMIT of 2^64 - 4 KiB at 4 KiB got
NFS4ERR_IO, "nfsd: non-standard errno: -63" and a WARNING, or the
assertion on XFS_DEBUG.
Clamp the length to what is left below the size before adding it to
the offset. With this and "xfs: refuse a pNFS LAYOUTCOMMIT of a range
without blocks", the extent ends at the size and gets NFS4ERR_IO at the
hole after the laid-out range, without a warning or an assertion.
Fixes: 527851124d10 ("xfs: implement pNFS export operations")
Cc: stable@vger.kernel.org
Signed-off-by: Daejun Park <daejun7.park@samsung.com>
---
fs/xfs/xfs_pnfs.c | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/fs/xfs/xfs_pnfs.c b/fs/xfs/xfs_pnfs.c
index 05c9660aea..d76873115c 100644
--- a/fs/xfs/xfs_pnfs.c
+++ b/fs/xfs/xfs_pnfs.c
@@ -312,9 +312,7 @@ xfs_fs_commit_blocks(
if (start > size)
continue;
- end = start + maps[i].length;
- if (end > size)
- end = size;
+ end = start + min_t(u64, maps[i].length, size - start);
length = end - start;
if (!length)
--
2.43.0
^ permalink raw reply related [flat|nested] 6+ messages in thread