* [PATCH v2] ntfs: trim the sizes after a short write
@ 2026-11-12 3:08 Zhan Xusheng
0 siblings, 0 replies; only message in thread
From: Zhan Xusheng @ 2026-11-12 3:08 UTC (permalink / raw)
To: linkinjeon, hyc.lee
Cc: liubaolin12138, ntfs, linux-kernel, zhanxusheng, stable
From: Zhan Xusheng <zhanxusheng@xiaomi.com>
ntfs_file_write_iter() moves the data size and, for a non-resident
uncompressed file, the initialized size to pos + count before the copy.
__ntfs_extend_initialized_size() only zeroes [old initialized size, pos),
so the range [pos, pos + count) is initialized only by the copy itself.
When iomap_file_buffered_write() copies less than count, it returns a
positive short count and the error rollback at out: does not run. The
file keeps the size pos + count, and [pos + ret, pos + count) reads back
whatever the freed clusters held before, also after a remount. A local
user can read the old contents of freed clusters this way.
Run the existing rollback for a short write as well, and roll the two
sizes back to the end of what was written instead of to the old sizes.
A short copy that ends inside a block can also leave the rest of that
block cached as read from disk, so clear the page cache beyond the new
initialized size before shrinking it.
Fixes: b041ca562526 ("ntfs: update iomap and address space operations")
Cc: stable@vger.kernel.org
Signed-off-by: Zhan Xusheng <zhanxusheng@xiaomi.com>
---
v2: also clear the cached tail of the last block, which v1 left in
place when the write stayed inside the old data size (Baolin).
v1: https://lore.kernel.org/all/20261111093459.660484-1-zhanxusheng1024@gmail.com/
Tested in a VM on ntfs-next b025f526c5d0, with all free space filled
with a known pattern first. Short buffered writes of 4M, old data
readable after drop_caches and after remount, 3 runs each:
unfixed v1 v2
fallocated, ends block-aligned 3137536 0 0
fallocated, ends inside a block 3141332 212 0
fallocated, unaligned start 3137536 412 0
append 3137536 0 0
A short write 1M past EOF, O_DIRECT, resident and compressed files and
full writes give the same results with v2 as with v1.
---
fs/ntfs/file.c | 33 ++++++++++++++++++++++++++-------
1 file changed, 26 insertions(+), 7 deletions(-)
diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
index 3213a0c9f96b..1779110a0c4f 100644
--- a/fs/ntfs/file.c
+++ b/fs/ntfs/file.c
@@ -928,10 +928,21 @@ ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
ret = iomap_file_buffered_write(iocb, from, &ntfs_write_iomap_ops,
&ntfs_iomap_folio_ops, NULL);
out:
- if (ret < 0 && ret != -EIOCBQUEUED) {
+ /*
+ * Both sizes were moved to end before the copy, so a short write,
+ * like a failed one, must take back what it did not write.
+ */
+ if (ret < count && ret != -EIOCBQUEUED) {
+ loff_t new_init_size = old_init_size;
+ loff_t new_data_size = old_data_size;
int init_err = 0;
int data_err = 0;
+ if (ret > 0) {
+ new_init_size = max(old_init_size, pos + ret);
+ new_data_size = max(old_data_size, pos + ret);
+ }
+
/*
* Like ntfs_setattr_size(): __ntfs_attr_truncate_vfs()
* expects the invalidate lock held across the size
@@ -940,13 +951,21 @@ ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
filemap_invalidate_lock(vi->i_mapping);
/* ntfs_attr_set_initialized_size() rejects a resident one. */
- if (ni->initialized_size != old_init_size &&
+ if (ni->initialized_size != new_init_size &&
NInoNonResident(ni)) {
struct ntfs_inode *mrec_ni = ntfs_base_inode(ni);
+ /*
+ * A short copy can leave the rest of the last block
+ * cached as read from disk. Clear what is no longer
+ * initialized before shrinking the initialized size.
+ */
+ if (ni->initialized_size > new_init_size)
+ truncate_pagecache_range(vi, new_init_size,
+ ni->initialized_size - 1);
mutex_lock(&mrec_ni->mrec_lock);
init_err = ntfs_attr_set_initialized_size(ni,
- old_init_size);
+ new_init_size);
mutex_unlock(&mrec_ni->mrec_lock);
}
/*
@@ -955,13 +974,13 @@ ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
* the uninitialized tail, so skip it unless the rollback
* above restored that size.
*/
- if (ni->data_size != old_data_size && !init_err) {
+ if (ni->data_size != new_data_size && !init_err) {
struct ntfs_inode *mrec_ni = ntfs_base_inode(ni);
- truncate_setsize(vi, old_data_size);
+ truncate_setsize(vi, new_data_size);
mutex_lock(&mrec_ni->mrec_lock);
- data_err = __ntfs_attr_truncate_vfs(ni, old_data_size,
- old_data_size);
+ data_err = __ntfs_attr_truncate_vfs(ni, new_data_size,
+ new_data_size);
mutex_unlock(&mrec_ni->mrec_lock);
/* Like ntfs_setattr_size(): stay on the durable size. */
if (data_err)
base-commit: b025f526c5d04929212c9d5fb7ffc96608f3eacc
--
2.43.0
^ permalink raw reply [flat|nested] only message in thread
only message in thread, other threads:[~2026-11-12 3:08 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-12 3:08 [PATCH v2] ntfs: trim the sizes after a short write Zhan Xusheng
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®