diff --git a/MAINTAINERS b/MAINTAINERS index 335d20584754..c31a32a2c748 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -9774,7 +9774,7 @@ EXFAT FILE SYSTEM M: Namjae Jeon M: Sungjong Seo R: Yuezhang Mo -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 #include #include +#include #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; }