summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-08-22 08:57:28 -0700
committerLinus Torvalds <torvalds@linux-foundation.org>2026-08-22 08:57:28 -0700
commit728785f8fb07ae9c295b1be6d7d672b9ebfc3c05 (patch)
tree58663c6ddc557f42f82b61a3aba27533225947c5
parent473f6c8f437b049f8ec015d57cd59bb983b1d85c (diff)
parent490529f668016c401d3da688104d15d3fc2c4fca (diff)
Merge tag 'exfat-for-7.3-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/linkinjeon/exfat
Pull exfat updates from Namjae Jeon: "A set of exfat fixes covering shared writable mappings, entry error handling, allocation cleanup, FITRIM bounds, and locking: - Fix valid_size extension over shared writable mappings by lazily zeroing page-cache gaps and ensuring racing stores cannot be overwritten or extend beyond valid_size - Clean up partially written directory entries on add-entry failure and safely free new directory clusters - Free a newly allocated directory cluster when zeroing it fails - Write the destination entry before removing the source entry during rename, preserving the source if the destination write fails - Keep FITRIM within the requested range, even when the free-space search wraps around the allocation bitmap - Fix truncated FS_IOC_GETFSLABEL results by passing the destination buffer size in bytes to the UTF-16-to-NLS conversion - Fix overflow in the cluster-to-dentry calculation, which could cause readdir to stop early on large volumes - Replace the per-inode truncate_lock with inode_lock, using shared locking in bmap to serialize against concurrent truncation - Update the exfat mailing list address in MAINTAINERS" * tag 'exfat-for-7.3-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/linkinjeon/exfat: exfat: replace truncate_lock with inode_lock exfat: keep FITRIM within the requested range exfat: fix truncated volume labels returned by FS_IOC_GETFSLABEL exfat: fix overflow in cluster-to-dentry conversion exfat: clean up new entry on add entry failure exfat: free new directory cluster if zeroing fails exfat: write moved entry before removing source MAINTAINERS: update mailing list address for exfat exfat: fix valid_size extension over a shared writable mapping
-rw-r--r--MAINTAINERS2
-rw-r--r--fs/exfat/balloc.c31
-rw-r--r--fs/exfat/dir.c10
-rw-r--r--fs/exfat/exfat_fs.h8
-rw-r--r--fs/exfat/file.c184
-rw-r--r--fs/exfat/inode.c5
-rw-r--r--fs/exfat/iomap.c11
-rw-r--r--fs/exfat/namei.c27
-rw-r--r--fs/exfat/super.c1
9 files changed, 207 insertions, 72 deletions
diff --git a/MAINTAINERS b/MAINTAINERS
index 335d20584754..c31a32a2c748 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -9774,7 +9774,7 @@ EXFAT FILE SYSTEM
M: Namjae Jeon <linkinjeon@kernel.org>
M: Sungjong Seo <sj1557.seo@samsung.com>
R: Yuezhang Mo <yuezhang.mo@sony.com>
-L: linux-fsdevel@vger.kernel.org
+L: exfat@lists.linux.dev
S: Maintained
T: git git://git.kernel.org/pub/scm/linux/kernel/git/linkinjeon/exfat.git
F: fs/exfat/
diff --git a/fs/exfat/balloc.c b/fs/exfat/balloc.c
index e66ebf899778..c0ddd522c1e1 100644
--- a/fs/exfat/balloc.c
+++ b/fs/exfat/balloc.c
@@ -340,14 +340,27 @@ int exfat_trim_fs(struct inode *inode, struct fstrim_range *range)
mutex_lock(&sbi->bitmap_lock);
trim_begin = trim_end = exfat_find_free_bitmap(sb, clu_start);
- if (trim_begin == EXFAT_EOF_CLUSTER)
+ /*
+ * exfat_find_free_bitmap() may wrap around to the beginning of
+ * the bitmap. Reject a cluster outside the requested range.
+ */
+ if (trim_begin == EXFAT_EOF_CLUSTER ||
+ trim_begin < clu_start || trim_begin > clu_end)
goto unlock;
- next_free_clu = exfat_find_free_bitmap(sb, trim_end + 1);
- if (next_free_clu == EXFAT_EOF_CLUSTER)
- goto unlock;
+ for (;;) {
+ if (trim_end >= clu_end)
+ break;
+
+ next_free_clu = exfat_find_free_bitmap(sb, trim_end + 1);
+ /*
+ * Stop if the search wrapped around or moved beyond the requested
+ * FITRIM range.
+ */
+ if (next_free_clu == EXFAT_EOF_CLUSTER ||
+ next_free_clu <= trim_end || next_free_clu > clu_end)
+ break;
- do {
if (next_free_clu == trim_end + 1) {
/* extend trim range for continuous free cluster */
trim_end++;
@@ -368,17 +381,11 @@ int exfat_trim_fs(struct inode *inode, struct fstrim_range *range)
trim_begin = trim_end = next_free_clu;
}
- if (next_free_clu >= clu_end)
- break;
-
if (fatal_signal_pending(current)) {
err = -ERESTARTSYS;
goto unlock;
}
-
- next_free_clu = exfat_find_free_bitmap(sb, next_free_clu + 1);
- } while (next_free_clu != EXFAT_EOF_CLUSTER &&
- next_free_clu > trim_end);
+ }
/* try to trim remainder */
count = trim_end - trim_begin + 1;
diff --git a/fs/exfat/dir.c b/fs/exfat/dir.c
index 4fd4ec52b8c0..fe73b1380c5d 100644
--- a/fs/exfat/dir.c
+++ b/fs/exfat/dir.c
@@ -87,7 +87,7 @@ static int exfat_readdir(struct inode *inode, loff_t *cpos, struct exfat_dir_ent
exfat_bytes_to_cluster(sbi, i_size_read(inode)), ei->flags);
dentries_per_clu = sbi->dentries_per_clu;
- max_dentries = min(MAX_EXFAT_DENTRIES,
+ max_dentries = (unsigned int)min_t(u64, MAX_EXFAT_DENTRIES,
exfat_cluster_to_dentries(sbi, sbi->num_clusters));
clu_offset = exfat_dentries_to_cluster(sbi, dentry);
@@ -299,7 +299,13 @@ int exfat_alloc_new_dir(struct inode *inode, struct exfat_chain *clu)
if (ret)
return ret;
- return exfat_zeroed_cluster(inode, clu->dir);
+ ret = exfat_zeroed_cluster(inode, clu->dir);
+ if (ret) {
+ exfat_free_cluster(inode, clu);
+ return ret;
+ }
+
+ return 0;
}
int exfat_calc_num_entries(struct exfat_uni_name *p_uniname)
diff --git a/fs/exfat/exfat_fs.h b/fs/exfat/exfat_fs.h
index 9be50949ce34..a9131fe03302 100644
--- a/fs/exfat/exfat_fs.h
+++ b/fs/exfat/exfat_fs.h
@@ -294,12 +294,10 @@ struct exfat_inode_info {
/* on-disk position of directory entry or 0 */
loff_t i_pos;
loff_t valid_size;
- /* page-aligned size that has been zeroed out for mmap */
+ /* block-aligned size zeroed in the page cache (>= valid_size) */
loff_t zeroed_size;
/* hash by i_location */
struct hlist_node i_hash_fat;
- /* protect bmap against truncate */
- struct rw_semaphore truncate_lock;
struct inode vfs_inode;
/* File creation time */
struct timespec64 i_crtime;
@@ -487,10 +485,10 @@ static inline u32 exfat_dentries_to_bytes(u32 dentry)
/*
* helpers for cluster size to dentry size conversion.
*/
-static inline u32 exfat_cluster_to_dentries(struct exfat_sb_info *sbi,
+static inline u64 exfat_cluster_to_dentries(struct exfat_sb_info *sbi,
u32 nr_clusters)
{
- return nr_clusters << (sbi->cluster_size_bits - DENTRY_SIZE_BITS);
+ return (u64)nr_clusters << (sbi->cluster_size_bits - DENTRY_SIZE_BITS);
}
static inline u32 exfat_dentries_to_cluster(struct exfat_sb_info *sbi,
diff --git a/fs/exfat/file.c b/fs/exfat/file.c
index 5fc13378d35f..a2a9ee1a2004 100644
--- a/fs/exfat/file.c
+++ b/fs/exfat/file.c
@@ -16,6 +16,7 @@
#include <linux/falloc.h>
#include <linux/fileattr.h>
#include <linux/iomap.h>
+#include <linux/pagemap.h>
#include "exfat_raw.h"
#include "exfat_fs.h"
@@ -412,7 +413,6 @@ int exfat_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
* about to be freed.
*/
inode_dio_wait(inode);
- down_write(&EXFAT_I(inode)->truncate_lock);
truncate_setsize(inode, attr->ia_size);
/*
@@ -420,7 +420,6 @@ int exfat_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
* is already written by it, so mark_inode_dirty() is unneeded.
*/
exfat_truncate(inode);
- up_write(&EXFAT_I(inode)->truncate_lock);
} else
mark_inode_dirty(inode);
@@ -565,7 +564,7 @@ static int exfat_ioctl_get_volume_label(struct super_block *sb, unsigned long ar
if (ret < 0)
return ret;
- ret = exfat_utf16_to_nls(sb, &uniname, label, uniname.name_len);
+ ret = exfat_utf16_to_nls(sb, &uniname, label, sizeof(label));
if (ret < 0)
return ret;
@@ -654,6 +653,104 @@ int exfat_file_fsync(struct file *filp, loff_t start, loff_t end, int datasync)
return blkdev_issue_flush(inode->i_sb->s_bdev);
}
+/*
+ * exfat_zero_new_range - zero [start, end) without overwriting uptodate blocks
+ *
+ * Uptodate blocks may contain data written through a shared mapping beyond
+ * valid_size.
+ */
+static int exfat_zero_new_range(struct inode *inode, loff_t start, loff_t end)
+{
+ struct address_space *mapping = inode->i_mapping;
+ unsigned int blocksize = i_blocksize(inode);
+ loff_t pos = start;
+ int err;
+
+ while (pos < end) {
+ loff_t next = min_t(loff_t,
+ round_down(pos, PAGE_SIZE) + PAGE_SIZE, end);
+ struct folio *folio;
+ loff_t bpos;
+
+ folio = filemap_get_folio(mapping, pos >> PAGE_SHIFT);
+ if (IS_ERR(folio)) {
+ err = iomap_zero_range(inode, pos, next - pos, NULL,
+ &exfat_iomap_ops, NULL, NULL);
+ if (err < 0)
+ return err;
+ pos = next;
+ continue;
+ }
+
+ if (folio_test_uptodate(folio)) {
+ folio_lock(folio);
+ if (folio->mapping == mapping)
+ folio_mark_dirty(folio);
+ folio_unlock(folio);
+ folio_put(folio);
+ pos = next;
+ continue;
+ }
+
+ /*
+ * Zero not-uptodate block runs. iomap_zero_range() requires an
+ * unlocked folio, so recheck ->mapping after each call.
+ */
+ folio_lock(folio);
+ bpos = pos;
+ while (bpos < next) {
+ loff_t rstart, rend;
+
+ if (folio->mapping != mapping) {
+ folio_unlock(folio);
+ err = iomap_zero_range(inode, bpos, next - bpos,
+ NULL, &exfat_iomap_ops, NULL, NULL);
+ if (err < 0) {
+ folio_put(folio);
+ return err;
+ }
+ folio_lock(folio);
+ break;
+ }
+
+ if (iomap_is_partially_uptodate(folio,
+ offset_in_folio(folio, bpos), blocksize)) {
+ bpos += blocksize;
+ continue;
+ }
+
+ rstart = bpos;
+ rend = min_t(loff_t, bpos + blocksize, next);
+ while (rend < next &&
+ !iomap_is_partially_uptodate(folio,
+ offset_in_folio(folio, rend), blocksize))
+ rend = min_t(loff_t, rend + blocksize, next);
+
+ folio_unlock(folio);
+ err = iomap_zero_range(inode, rstart, rend - rstart,
+ NULL, &exfat_iomap_ops, NULL, NULL);
+ if (err < 0) {
+ folio_put(folio);
+ return err;
+ }
+ folio_lock(folio);
+ bpos = rend;
+ }
+
+ /*
+ * Dirty only a fully uptodate folio. Dirtying a partial folio could
+ * write uninitialised cache contents over valid on-disk blocks.
+ */
+ if (folio->mapping == mapping && folio_test_uptodate(folio))
+ folio_mark_dirty(folio);
+ folio_unlock(folio);
+ folio_put(folio);
+ pos = next;
+ }
+
+ return 0;
+}
+
static int exfat_extend_valid_size(struct inode *inode, loff_t new_valid_size)
{
struct exfat_inode_info *ei = EXFAT_I(inode);
@@ -661,18 +758,41 @@ static int exfat_extend_valid_size(struct inode *inode, loff_t new_valid_size)
int ret = 0;
if (old_valid_size < new_valid_size) {
+ /* Do not re-zero blocks already covered by zeroed_size. */
+ loff_t gap_start = max(old_valid_size, ei->zeroed_size);
+
if (i_size_read(inode) < new_valid_size) {
- i_size_write(inode, new_valid_size);
- mark_inode_dirty(inode);
+ /*
+ * Allocate clusters before increasing i_size. The gap
+ * may already be zeroed, so the subsequent zeroing
+ * can be skipped.
+ */
+ ret = exfat_cont_expand(inode, new_valid_size);
+ if (ret)
+ return ret;
}
- ret = iomap_zero_range(inode, old_valid_size,
- new_valid_size - old_valid_size, NULL,
- &exfat_write_iomap_ops, NULL, NULL);
+ /*
+ * Revoke writable PTEs while zeroing the gap. A racing mmap
+ * store re-faults through exfat_page_mkwrite() after valid_size
+ * is updated.
+ */
+ filemap_invalidate_lock(inode->i_mapping);
+ if (gap_start < new_valid_size)
+ unmap_mapping_range(inode->i_mapping, gap_start,
+ new_valid_size - gap_start, 0);
+ ret = exfat_zero_new_range(inode, gap_start, new_valid_size);
+ filemap_invalidate_unlock(inode->i_mapping);
if (ret) {
truncate_setsize(inode, old_valid_size);
exfat_truncate(inode);
+ return ret;
}
+
+ ei->valid_size = new_valid_size;
+ if (ei->zeroed_size < round_up(new_valid_size, i_blocksize(inode)))
+ ei->zeroed_size = round_up(new_valid_size, i_blocksize(inode));
+ mark_inode_dirty(inode);
}
return ret;
@@ -825,39 +945,39 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf)
struct inode *inode = file_inode(vmf->vma->vm_file);
struct exfat_inode_info *ei = EXFAT_I(inode);
vm_fault_t ret;
- loff_t new_valid_size, mmap_valid_size;
+ loff_t new_valid_size, mmap_valid_size, fault_page_start;
if (!inode_trylock(inode))
return VM_FAULT_RETRY;
mmap_valid_size = ((loff_t)vmf->pgoff + 1) << PAGE_SHIFT;
+ fault_page_start = ((loff_t)vmf->pgoff) << PAGE_SHIFT;
new_valid_size = min(mmap_valid_size, i_size_read(inode));
if (ei->valid_size < new_valid_size) {
- if (ei->zeroed_size < mmap_valid_size) {
+ if (ei->zeroed_size < fault_page_start) {
int err;
/*
- * Only zero the range that hasn't been zeroed yet for
- * this mmap write path. zeroed_size tracks the largest
- * page-aligned offset that has already been zeroed.
- *
- * This prevents unnecessarily zeroing out the entire
- * tail page on every page fault when userspace writes
- * data byte-by-byte through mmap (after a small
- * fallocate). It fixes data corruption in the tail page
- * while preserving the existing valid_size semantics.
+ * Zero only the gap below the faulting page. The read
+ * fault populated its folio and iomap_page_mkwrite()
+ * will dirty it.
*/
- err = iomap_zero_range(inode, ei->zeroed_size,
- mmap_valid_size - ei->zeroed_size, NULL,
- &exfat_iomap_ops, NULL, NULL);
+ err = exfat_zero_new_range(inode, ei->zeroed_size,
+ fault_page_start);
if (err < 0) {
inode_unlock(inode);
return vmf_fs_error(err);
}
- ei->zeroed_size = mmap_valid_size;
}
+ /*
+ * Track zeroed_size by block, not page, because writeback stops
+ * at i_size recording blocks wholly beyond it could skip a
+ * later required zeroing.
+ */
+ if (ei->zeroed_size < round_up(new_valid_size, i_blocksize(inode)))
+ ei->zeroed_size = round_up(new_valid_size, i_blocksize(inode));
ei->valid_size = new_valid_size;
mark_inode_dirty(inode);
}
@@ -866,7 +986,7 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf)
file_update_time(vmf->vma->vm_file);
filemap_invalidate_lock_shared(inode->i_mapping);
- ret = iomap_page_mkwrite(vmf, &exfat_write_iomap_ops, NULL);
+ ret = iomap_page_mkwrite(vmf, &exfat_iomap_ops, NULL);
filemap_invalidate_unlock_shared(inode->i_mapping);
sb_end_pagefault(inode->i_sb);
inode_unlock(inode);
@@ -876,7 +996,6 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf)
static const struct vm_operations_struct exfat_file_vm_ops = {
.fault = filemap_fault,
- .map_pages = filemap_map_pages,
.page_mkwrite = exfat_page_mkwrite,
};
@@ -887,21 +1006,6 @@ static int exfat_file_mmap_prepare(struct vm_area_desc *desc)
if (unlikely(exfat_forced_shutdown(file_inode(desc->file)->i_sb)))
return -EIO;
- if (vma_desc_test_all(desc, VMA_SHARED_BIT, VMA_MAYWRITE_BIT)) {
- struct inode *inode = file_inode(file);
- loff_t from, to;
- int err;
-
- from = ((loff_t)desc->pgoff << PAGE_SHIFT);
- to = min_t(loff_t, i_size_read(inode),
- from + vma_desc_size(desc));
- if (EXFAT_I(inode)->valid_size < to) {
- err = exfat_extend_valid_size(inode, to);
- if (err)
- return err;
- }
- }
-
file_accessed(file);
desc->vm_ops = &exfat_file_vm_ops;
return 0;
diff --git a/fs/exfat/inode.c b/fs/exfat/inode.c
index 89826aea5e1e..ccd13630187e 100644
--- a/fs/exfat/inode.c
+++ b/fs/exfat/inode.c
@@ -291,10 +291,9 @@ static sector_t exfat_aop_bmap(struct address_space *mapping, sector_t block)
{
sector_t blocknr;
- /* exfat_get_cluster() assumes the requested blocknr isn't truncated. */
- down_read(&EXFAT_I(mapping->host)->truncate_lock);
+ inode_lock_shared(mapping->host);
blocknr = iomap_bmap(mapping, block, &exfat_iomap_ops);
- up_read(&EXFAT_I(mapping->host)->truncate_lock);
+ inode_unlock_shared(mapping->host);
return blocknr;
}
diff --git a/fs/exfat/iomap.c b/fs/exfat/iomap.c
index 4b9207bc4ab0..8911aa84a730 100644
--- a/fs/exfat/iomap.c
+++ b/fs/exfat/iomap.c
@@ -177,11 +177,18 @@ static int exfat_write_iomap_end(struct inode *inode, loff_t pos, loff_t length,
if (ei->valid_size < end) {
ei->valid_size = end;
- if (ei->zeroed_size < end)
- ei->zeroed_size = end;
dirtied = true;
}
+ /*
+ * IOMAP_F_ZERO_TAIL zeroes the remainder of the last block. Track that
+ * block as zeroed so later valid_size extensions do not zero it again.
+ */
+ if (iomap->flags & IOMAP_F_ZERO_TAIL)
+ end = round_up(end, i_blocksize(inode));
+ if (ei->zeroed_size < end)
+ ei->zeroed_size = end;
+
if (dirtied || iomap->flags & IOMAP_F_SIZE_CHANGED)
mark_inode_dirty(inode);
diff --git a/fs/exfat/namei.c b/fs/exfat/namei.c
index cd9c9eca58f8..a4dc83b5949c 100644
--- a/fs/exfat/namei.c
+++ b/fs/exfat/namei.c
@@ -471,6 +471,7 @@ static int exfat_add_entry(struct inode *inode, const char *path,
struct exfat_entry_set_cache es;
int clu_size = 0;
unsigned int start_clu = EXFAT_FREE_CLUSTER;
+ bool dir_allocated = false;
ret = exfat_resolve_path(inode, path, &uniname);
if (ret)
@@ -497,6 +498,7 @@ static int exfat_add_entry(struct inode *inode, const char *path,
}
start_clu = clu.dir;
clu_size = sbi->cluster_size;
+ dir_allocated = true;
}
/* update the directory entry */
@@ -507,8 +509,21 @@ static int exfat_add_entry(struct inode *inode, const char *path,
exfat_init_ext_entry(&es, num_entries, &uniname, NULL, 0);
ret = exfat_put_dentry_set(&es, IS_DIRSYNC(inode));
- if (ret)
+ if (ret) {
+ int cleanup_ret;
+
+ cleanup_ret = exfat_get_dentry_set(&es, sb, &info->dir,
+ dentry, ES_ALL_ENTRIES);
+ if (!cleanup_ret) {
+ exfat_remove_entries(inode, &es, ES_IDX_FILE, false);
+ cleanup_ret = exfat_put_dentry_set(&es,
+ IS_DIRSYNC(inode));
+ }
+
+ if (!cleanup_ret && dir_allocated)
+ exfat_free_cluster(inode, &clu);
goto out;
+ }
info->entry = dentry;
info->flags = ALLOC_NO_FAT_CHAIN;
@@ -1116,11 +1131,6 @@ static int exfat_move_file(struct inode *parent_inode,
exfat_init_ext_entry(&new_es, num_new_entries, p_uniname,
&mov_es, num_extra_entries);
- exfat_remove_entries(parent_inode, &mov_es, ES_IDX_FILE, false);
-
- ei->dir = newdir;
- ei->entry = newentry;
-
ret = exfat_put_dentry_set(&new_es, IS_DIRSYNC(parent_inode));
if (ret) {
/* Best-effort delete to avoid duplicate entries */
@@ -1134,6 +1144,11 @@ static int exfat_move_file(struct inode *parent_inode,
goto put_mov_es;
}
+ exfat_remove_entries(parent_inode, &mov_es, ES_IDX_FILE, false);
+
+ ei->dir = newdir;
+ ei->entry = newentry;
+
return exfat_put_dentry_set(&mov_es, IS_DIRSYNC(parent_inode));
put_mov_es:
diff --git a/fs/exfat/super.c b/fs/exfat/super.c
index 388db271c6bf..a9ea36ba2693 100644
--- a/fs/exfat/super.c
+++ b/fs/exfat/super.c
@@ -195,7 +195,6 @@ static struct inode *exfat_alloc_inode(struct super_block *sb)
if (!ei)
return NULL;
- init_rwsem(&ei->truncate_lock);
return &ei->vfs_inode;
}