[PATCH] exfat: free clusters beyond EOF when a write fails or is short
From: Hengyu Liang
Date: Mon Oct 12 2026 - 05:41:55 EST
Commit 82a81a7352bc ("exfat: add iomap buffered I/O support") and commit
867b9c96dc83 ("exfat: add iomap direct I/O support") made a write
allocate the clusters for its whole range in one step. They also dropped
exfat_write_failed(), which freed the clusters beyond the end of the
file after a failed or short write.
However, a write can stop early, for example when the process is killed
or when a part of the buffer cannot be read. As of now, the clusters for
the part that was not written stay allocated beyond the end of the file.
exfat_cont_expand() does not expect them, so a later write or truncate
that extends the file fails with ENOSPC. The directory entry records
only the size of the file, so the space can be lost for good once the
volume is unmounted.
The issue can be reproduced on a 64M volume made by mkfs.exfat and
mounted on /mnt, with a write of 1M from a buffer that has only its
first 4K mapped:
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
int main(void)
{
char *p = mmap(NULL, 8192, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
int fd = open("/mnt/f", O_CREAT | O_WRONLY, 0644);
struct stat st;
munmap(p + 4096, 4096);
printf("write: %zd\n", write(fd, p, 1 << 20));
fstat(fd, &st);
printf("blocks: %ld\n", (long)st.st_blocks);
printf("ftruncate: %d\n", ftruncate(fd, 1 << 16));
return 0;
}
Before commit 82a81a7352bc ("exfat: add iomap buffered I/O support"),
the result is:
write: 4096
blocks: 8
ftruncate: 0
After that commit, the result is:
write: 4096
blocks: 2048
ftruncate: -1
This patch will bring back exfat_write_failed() and call it when a write
ends before the end of its range, so that the clusters beyond the end of
the file are freed again. It waits for direct I/O in flight first, as
exfat_setattr() does before it frees clusters.
Fixes: 82a81a7352bc ("exfat: add iomap buffered I/O support")
Fixes: 867b9c96dc83 ("exfat: add iomap direct I/O support")
Cc: stable@xxxxxxxxxxxxxxx
Assisted-by: LLM
Signed-off-by: Hengyu Liang <hengyul@xxxxxxxxxx>
---
Based on the exfat dev branch (1cae92a58966).
Tested on v7.3-rc6 with fs/exfat taken from that branch (KASAN, and a
second build with lockdep), on volumes with 512-byte, 4K and 32K
clusters: the program above, the same write with O_DIRECT, a process
killed in the middle of one 48M write(), a fragmented volume, and
asynchronous direct writes in flight while writes on the same file
fail. With the patch no clusters are left beyond EOF and all the space
is free again after the file is deleted.
Without the patch, the killed 48M write leaves a 1.5M file with 48M
allocated; after a remount and deleting the file, only 15M of the 62M
volume are free.
The inode_dio_wait() is needed. In a test build without it, a direct
write that fails while asynchronous direct writes are extending the
file frees their clusters, and fsck.exfat reports "less clusters are
allocated".
fs/exfat/file.c | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/fs/exfat/file.c b/fs/exfat/file.c
index f106db945d8d..dc57d2c2f0f0 100644
--- a/fs/exfat/file.c
+++ b/fs/exfat/file.c
@@ -804,6 +804,19 @@ static int exfat_extend_valid_size(struct inode *inode, loff_t new_valid_size)
return ret;
}
+/*
+ * The clusters for a write are allocated before the data is copied. Free the
+ * ones that a failed or short write has left beyond the end of the file.
+ */
+static void exfat_write_failed(struct inode *inode)
+{
+ inode_dio_wait(inode);
+ filemap_invalidate_lock(inode->i_mapping);
+ truncate_pagecache(inode, i_size_read(inode));
+ exfat_truncate(inode);
+ filemap_invalidate_unlock(inode->i_mapping);
+}
+
static ssize_t exfat_fallback_buffered_write(struct kiocb *iocb,
struct iov_iter *from)
{
@@ -862,7 +875,7 @@ static ssize_t exfat_file_write_iter(struct kiocb *iocb, struct iov_iter *iter)
struct file *file = iocb->ki_filp;
struct inode *inode = file_inode(file);
struct exfat_inode_info *ei = EXFAT_I(inode);
- loff_t pos, valid_size;
+ loff_t pos, valid_size, end;
int err;
if (unlikely(exfat_forced_shutdown(inode->i_sb)))
@@ -881,6 +894,7 @@ static ssize_t exfat_file_write_iter(struct kiocb *iocb, struct iov_iter *iter)
}
pos = iocb->ki_pos;
+ end = pos + iov_iter_count(iter);
if (pos > i_size_read(inode))
truncate_pagecache(inode, i_size_read(inode));
@@ -909,6 +923,8 @@ static ssize_t exfat_file_write_iter(struct kiocb *iocb, struct iov_iter *iter)
ret = iomap_file_buffered_write(iocb, iter,
&exfat_write_iomap_ops, NULL, NULL);
}
+ if (ret != -EIOCBQUEUED && end > i_size_read(inode))
+ exfat_write_failed(inode);
if (ret < 0)
goto unlock;
--
2.53.0