[PATCH] fs/ntfs3: do not leave the file extended after a short write
From: Hengyu Liang
Date: Thu Oct 15 2026 - 21:44:34 EST
ntfs_file_write_iter() sets the file size to the end of the request
with ntfs_set_size() before it copies the data.
However, when fewer bytes are written, the function returns without
putting the size back. As of now, a write() that runs out of space on a
sparse file, or that stops at an unreadable part of the buffer, leaves
the file as long as the whole request. The part that was not written
reads as zeros.
The issue can be reproduced on a volume mounted with the sparse option:
truncate -s 16M img
mkntfs -F -q img
mount -t ntfs3 -o loop,sparse img /mnt
dd if=/dev/zero of=/mnt/hole bs=512k count=1
dd if=/dev/zero of=/mnt/fill bs=4k
sync
rm /mnt/hole
sync
dd if=/dev/zero of=/mnt/f bs=1M count=1
ls -l /mnt/f
The last dd command fails with "No space left on device". Before this
patch, /mnt/f is then 1048576 bytes long, although only 524288 bytes
were written. After this patch, /mnt/f is 524288 bytes long.
For non-compressed files, trim back to the end of the written data without
shrinking below the old size, and restore the old size if no data was
written. Reuse ntfs_setattr()'s shrink sequence and i_size rollback,
recording cleanup errors for fsync while preserving the write result.
Fixes: 4342306f0f0d ("fs/ntfs3: Add file operations and implementation")
Signed-off-by: Hengyu Liang <hengyul@xxxxxxxxxx>
---
Tested in KVM guests with KASAN enabled:
- Short/failed writes and ENOSPC, including unmount/remount checks.
- 78 focused cases across default, sparse, delalloc and prealloc=0
combinations, covering buffered/direct I/O, append and O_DSYNC.
- 15 model runs (21,600 operations), with readback after remount.
- 32 ENOMEM/EIO cases each with and without the new error handling,
using a test-only injection annotation at ntfs_set_size() entry.
Checked size rollback, data preservation and fsync/O_DSYNC errors.
Compressed files and alternate data streams were not tested.
fs/ntfs3/file.c | 42 +++++++++++++++++++++++++++++++++++++-----
1 file changed, 37 insertions(+), 5 deletions(-)
diff --git a/fs/ntfs3/file.c b/fs/ntfs3/file.c
index 95dfe6878860..bf9b39d00458 100644
--- a/fs/ntfs3/file.c
+++ b/fs/ntfs3/file.c
@@ -1236,6 +1236,33 @@ static int check_write_restriction(struct inode *inode)
return 0;
}
+/*
+ * ntfs_write_trim - Cut the file back after a write that was not completed.
+ *
+ * ntfs_file_write_iter() sets the file size to the end of the request before
+ * the data is copied. Keep what was written, or the old size if nothing was.
+ */
+static void ntfs_write_trim(struct kiocb *iocb, loff_t pos, loff_t oldsize)
+{
+ struct inode *inode = file_inode(iocb->ki_filp);
+ loff_t isize = i_size_read(inode);
+ loff_t newsize = oldsize;
+ int err;
+
+ if (iocb->ki_pos > pos)
+ newsize = max(newsize, iocb->ki_pos);
+
+ if (newsize < isize) {
+ truncate_setsize(inode, newsize);
+ err = ntfs_set_size(inode, newsize);
+ if (err) {
+ i_size_write(inode, isize);
+ /* Preserve the write result; report the error on fsync. */
+ mapping_set_error(inode->i_mapping, err);
+ }
+ }
+}
+
/*
* ntfs_file_write_iter - file_operations::write_iter
*/
@@ -1244,7 +1271,7 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
struct file *file = iocb->ki_filp;
struct inode *inode = file_inode(file);
struct ntfs_inode *ni = ntfs_i(inode);
- loff_t vbo, endbyte;
+ loff_t vbo, endbyte, pos, oldsize;
ssize_t ret, err;
if (!inode_trylock(inode)) {
@@ -1281,6 +1308,8 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
vbo = iocb->ki_pos;
endbyte = vbo + ret;
+ pos = vbo;
+ oldsize = inode->i_size;
if (endbyte > inode->i_size) {
/*
@@ -1300,7 +1329,7 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
if (vbo > ni->i_valid) {
ret = ntfs_extend_initialized_size(file, ni, vbo);
if (ret)
- goto out;
+ goto trim;
}
/* Fallback to buffered I/O if the inode does not support direct I/O. */
@@ -1310,6 +1339,7 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
ret = iomap_file_buffered_write(iocb, from, &ntfs_iomap_ops,
&ntfs_iomap_folio_ops, NULL);
+ ntfs_write_trim(iocb, pos, oldsize);
inode_unlock(inode);
if (likely(ret > 0))
@@ -1322,7 +1352,7 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
/* Direct I/O is not compatible with delalloc. */
ret = ni_allocate_da_blocks(ni);
if (ret)
- goto out;
+ goto trim;
}
ret = iomap_dio_rw(iocb, from, &ntfs_iomap_ops, NULL,
@@ -1342,7 +1372,7 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
&ntfs_iomap_folio_ops, NULL);
if (err < 0) {
ret = err;
- goto out;
+ goto trim;
}
/*
@@ -1358,13 +1388,15 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
endbyte);
if (err) {
ret = err;
- goto out;
+ goto trim;
}
invalidate_mapping_pages(inode->i_mapping, vbo >> PAGE_SHIFT,
endbyte >> PAGE_SHIFT);
}
+trim:
+ ntfs_write_trim(iocb, pos, oldsize);
out:
inode_unlock(inode);
--
2.53.0