[PATCH] ntfs: include the final partial cluster in sync writes

From: Karl Mehltretter

Date: Tue Sep 22 2026 - 00:36:14 EST


ntfs_attrlist_repack() synchronously writes the replacement attribute
list before updating its mapping pairs and freeing the old run. With
512-byte clusters, a 544-byte list submits a 512-byte bio, but the
write returns 544 and repack accepts it as complete. If the later
buffered update does not reach disk, the replacement list lacks its
final 32 bytes.

__ntfs_inode_non_resident_attr_pwrite() converts attr_len to clusters
with ntfs_bytes_to_cluster(), which rounds down. Before repack, its
synchronous callers used either cluster-aligned lengths or lengths
smaller than one cluster, where max_t() selects one.

Round attr_len up so repack writes the final partial cluster before
publishing the new mapping pairs.

Fixes: b1d732e62a5b ("ntfs: repack $MFT/$ATTRIBUTE LIST")
Cc: stable@xxxxxxxxxxxxxxx
Assisted-by: LLM
Signed-off-by: Karl Mehltretter <kmehltretter@xxxxxxxxx>
---
Built fs/ntfs/ at 444c7dc82415 with W=1 and NTFS_FS=m for x86_64 and
i386 using GCC 15.2.0. Both builds completed without warnings.

Runtime-tested with QEMU 10.2.1, x86_64 TCG and virtio-blk cache=none
on 4904082812d5, a v7.2.7 stable queue containing b1d732e62a5b. Test
hooks called the normal unlocked update, forced the optional expansion
to return -ENOSPC and used a 64 KiB reserve on a 16 MiB image.

With 512-byte clusters and a 544-byte list, both kernels paused after
repack and before the buffered update. The old code submitted 512 bytes,
returned 544 and left a marker in the second cluster unchanged. The
fixed code submitted 1024 bytes and wrote the exact 32-byte tail. A
1024-byte list wrote exactly two clusters and left a marker in the
third cluster unchanged.

A test hook returned -EIO instead of submitting the 1024-byte BIO. The
rollback restored the original mapping and all 544 bytes, and freed the
entire replacement run. The image cleanly unmounted and remounted
with the unmodified queue kernel.

Runtime coverage was limited to the repack caller, which writes at
offset zero.

The base's scripts/checkpatch.pl --strict reports no findings.

fs/ntfs/inode.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index cadf623d54d48..d8d3a7b7b8a9a 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -3761,7 +3761,9 @@ static s64 __ntfs_inode_non_resident_attr_pwrite(struct inode *vi,
struct runlist_element *rl;
int bio_err;

- lcn_count = max_t(s64, 1, ntfs_bytes_to_cluster(vol, attr_len));
+ lcn_count = max_t(s64, 1,
+ ntfs_bytes_to_cluster(vol, attr_len +
+ vol->cluster_size - 1));
vcn = ntfs_pidx_to_cluster(vol, folio->index);

do {

base-commit: 444c7dc82415a353b57e43189f276124cc5fd785
--
2.53.0