[PATCH v3] ntfs: handle a failed size rollback in the write error path

From: Hongling Zeng

Date: Mon Oct 19 2026 - 22:54:17 EST


ntfs_file_write_iter() extends the data and initialized sizes before
it copies the user data and restores them when the write then fails.
The restoration is best-effort: both results are discarded, even
though the extension is already recorded in the dirty MFT record.
When the restoration fails too, the extended sizes reach the disk
while the VFS inode keeps reporting the old ones: after a remount the
file appears grown again, with an uninitialized tail where the failed
write never put any data, and nothing records the failure.

Keep both undo results: roll the data size back through the same
VFS-facing truncate ntfs_setattr_size() uses, which also supports
compressed attributes, and on failure keep the VFS size on the
durable data size. On an undo failure, set NVolSetErrors(), which
the next persistence point persists as VOLUME_IS_DIRTY, and log a
message for the errors= policy.

Fixes: 9c87959601e8 ("ntfs: update file operations")
Suggested-by: Hyunchul Lee <hyc.lee@xxxxxxxxx>
Signed-off-by: Hongling Zeng <zenghongling@xxxxxxxxxx>
---
Changes in v3:
- skip the initialized size rollback on a resident attribute;
- roll the data size back through __ntfs_attr_truncate_vfs(), which
ntfs_setattr_size() uses, so compressed attributes roll back too;
- keep the VFS size on the durable data size if the rollback fails.
---
fs/ntfs/file.c | 34 +++++++++++++++++++++++++++-------
1 file changed, 27 insertions(+), 7 deletions(-)

diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
index d810ca5904bd..e66814d08f59 100644
--- a/fs/ntfs/file.c
+++ b/fs/ntfs/file.c
@@ -934,22 +934,42 @@ ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
&ntfs_iomap_folio_ops, NULL);
out:
if (ret < 0 && ret != -EIOCBQUEUED) {
- if (ni->initialized_size != old_init_size) {
- struct ntfs_inode *mrec_ni =
- ntfs_base_inode(ni);
+ int init_err = 0;
+ int data_err = 0;
+
+ /* ntfs_attr_set_initialized_size() rejects a resident one. */
+ if (ni->initialized_size != old_init_size &&
+ NInoNonResident(ni)) {
+ struct ntfs_inode *mrec_ni = ntfs_base_inode(ni);

mutex_lock(&mrec_ni->mrec_lock);
- ntfs_attr_set_initialized_size(ni, old_init_size);
+ init_err = ntfs_attr_set_initialized_size(ni,
+ old_init_size);
mutex_unlock(&mrec_ni->mrec_lock);
}
if (ni->data_size != old_data_size) {
- struct ntfs_inode *mrec_ni =
- ntfs_base_inode(ni);
+ struct ntfs_inode *mrec_ni = ntfs_base_inode(ni);

truncate_setsize(vi, old_data_size);
+ /*
+ * ntfs_attr_truncate() would reject a compressed
+ * attribute; the shrink path also clamps the
+ * initialized size down.
+ */
mutex_lock(&mrec_ni->mrec_lock);
- ntfs_attr_truncate(ni, old_data_size);
+ data_err = __ntfs_attr_truncate_vfs(ni, old_data_size,
+ old_data_size);
mutex_unlock(&mrec_ni->mrec_lock);
+ /* Like ntfs_setattr_size(): stay on the durable size. */
+ if (data_err)
+ i_size_write(vi, ni->data_size);
+ }
+ if (init_err || data_err) {
+ /* The failed sizes stay in the dirty MFT record. */
+ NVolSetErrors(vol);
+ ntfs_error(vi->i_sb,
+ "Failed to roll back the size update: %d",
+ init_err ?: data_err);
}
}
out_lock:
--
2.25.1