diff options
Diffstat (limited to 'fs/exfat/iomap.c')
| -rw-r--r-- | fs/exfat/iomap.c | 272 |
1 files changed, 272 insertions, 0 deletions
diff --git a/fs/exfat/iomap.c b/fs/exfat/iomap.c new file mode 100644 index 000000000..4b9207bc4 --- /dev/null +++ b/fs/exfat/iomap.c @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * iomap callack functions + * + * Copyright (C) 2026 Namjae Jeon <linkinjeon@kernel.org> + */ + +#include <linux/iomap.h> +#include <linux/pagemap.h> + +#include "exfat_raw.h" +#include "exfat_fs.h" +#include "iomap.h" + +/* + * exfat_file_write_dio_end_io - Direct I/O write completion handler + * + * Updates i_size if the write extended the file. Called from the dio layer + * after I/O completion. + */ +static int exfat_file_write_dio_end_io(struct kiocb *iocb, ssize_t size, + int error, unsigned int flags) +{ + struct inode *inode = file_inode(iocb->ki_filp); + + if (error) + return error; + + if (size && i_size_read(inode) < iocb->ki_pos + size) { + i_size_write(inode, iocb->ki_pos + size); + mark_inode_dirty(inode); + } + + return 0; +} + +const struct iomap_dio_ops exfat_write_dio_ops = { + .end_io = exfat_file_write_dio_end_io, +}; + +static int __exfat_iomap_begin(struct inode *inode, loff_t offset, loff_t length, + unsigned int flags, struct iomap *iomap, bool may_alloc) +{ + struct super_block *sb = inode->i_sb; + struct exfat_sb_info *sbi = EXFAT_SB(sb); + struct exfat_inode_info *ei = EXFAT_I(inode); + unsigned int cluster, num_clusters; + loff_t cluster_offset, cluster_length; + int err; + bool balloc = false; + + if (!may_alloc) { + /* Completely beyond EOF. Treat as hole */ + if (i_size_read(inode) <= offset) { + iomap->type = IOMAP_HOLE; + iomap->addr = IOMAP_NULL_ADDR; + iomap->offset = offset; + iomap->length = length; + return 0; + } + + /* Clamp length if the requested range goes beyond i_size */ + if (offset + length > i_size_read(inode)) + length = round_up(i_size_read(inode), + i_blocksize(inode)) - offset; + } + + num_clusters = exfat_bytes_to_cluster_round_up(sbi, + offset + length) - exfat_bytes_to_cluster(sbi, offset); + + mutex_lock(&sbi->s_lock); + iomap->bdev = inode->i_sb->s_bdev; + iomap->offset = offset; + + err = exfat_map_cluster(inode, exfat_bytes_to_cluster(sbi, offset), + &cluster, &num_clusters, may_alloc, &balloc); + if (err) + goto out; + + cluster_offset = exfat_cluster_offset(sbi, offset); + cluster_length = exfat_cluster_to_bytes(sbi, num_clusters); + + iomap->length = min_t(loff_t, length, cluster_length - cluster_offset); + iomap->addr = exfat_cluster_to_phys_bytes(sbi, cluster) + cluster_offset; + iomap->type = IOMAP_MAPPED; + iomap->flags = IOMAP_F_MERGED; + + if (may_alloc || flags & IOMAP_ZERO) { + if (balloc) + iomap->flags |= IOMAP_F_NEW; + else if (iomap->offset + iomap->length >= ei->valid_size) { + /* + * This is a write that starts at or extends beyond + * the current valid_size. The region between the old + * valid_size and the end of this write needs to be + * zeroed in the page cache to prevent stale data + * exposure (see IOMAP_F_ZERO_TAIL handling in + * __iomap_write_begin()). + */ + iomap->flags |= IOMAP_F_ZERO_TAIL; + } + } else { + /* + * valid_size is tracked in byte granularity and + * marks the exact boundary between valid data and + * holes (or unwritten space). + * + * When IOMAP_REPORT is set (used by lseek(SEEK_HOLE) + * and SEEK_DATA), we return IOMAP_HOLE. This allows + * iomap_seek_hole_iter() to directly return the + * precise byte position. + * + * For normal I/O paths (without IOMAP_REPORT) we + * return IOMAP_UNWRITTEN so the write path can + * distinguish it from a real hole. + */ + if (offset >= ei->valid_size) { + iomap->type = flags & IOMAP_REPORT ? + IOMAP_HOLE : IOMAP_UNWRITTEN; + } else if (offset + iomap->length > ei->valid_size) { + if (flags & IOMAP_REPORT) { + /* + * For SEEK_HOLE/SEEK_DATA, clip the length + * to the exact byte boundary (valid_size). + * This ensures the caller gets the precise + * hole position in byte units. + */ + iomap->length = ei->valid_size - iomap->offset; + } else + iomap->length = round_up(ei->valid_size, + i_blocksize(inode)) - + iomap->offset; + } + } + + iomap->flags |= IOMAP_F_MERGED; +out: + mutex_unlock(&sbi->s_lock); + return err; +} + +static int exfat_iomap_begin(struct inode *inode, loff_t offset, loff_t length, + unsigned int flags, struct iomap *iomap, struct iomap *srcmap) +{ + return __exfat_iomap_begin(inode, offset, length, flags, iomap, false); +} + +static int exfat_write_iomap_begin(struct inode *inode, loff_t offset, loff_t length, + unsigned int flags, struct iomap *iomap, struct iomap *srcmap) +{ + return __exfat_iomap_begin(inode, offset, length, flags, iomap, true); +} + +static DEFINE_IOMAP_ITER_NEXT(exfat_iomap_next, exfat_iomap_begin); + +const struct iomap_ops exfat_iomap_ops = { + .iomap_next = exfat_iomap_next, +}; + +/* + * exfat_write_iomap_end - Update the state after write + * + * Extends ->valid_size to cover the newly written range. + * Marks the inode dirty if metadata was changed. + */ +static int exfat_write_iomap_end(struct inode *inode, loff_t pos, loff_t length, + ssize_t written, unsigned int flags, struct iomap *iomap) +{ + struct exfat_inode_info *ei = EXFAT_I(inode); + bool dirtied = false; + loff_t end; + + if (!written) + return 0; + + end = pos + written; + + if (ei->valid_size < end) { + ei->valid_size = end; + if (ei->zeroed_size < end) + ei->zeroed_size = end; + dirtied = true; + } + + if (dirtied || iomap->flags & IOMAP_F_SIZE_CHANGED) + mark_inode_dirty(inode); + + return written; +} + +static DEFINE_IOMAP_ITER_NEXT_END(exfat_write_iomap_next, + exfat_write_iomap_begin, exfat_write_iomap_end); + +const struct iomap_ops exfat_write_iomap_ops = { + .iomap_next = exfat_write_iomap_next, +}; + +/* + * exfat_writeback_range - Map folio during writeback + * + * Called for each folio during writeback. If the folio falls outside the + * current iomap, remaps by calling read_iomap_begin. + */ +static ssize_t exfat_writeback_range(struct iomap_writepage_ctx *wpc, + struct folio *folio, u64 offset, unsigned int len, u64 end_pos) +{ + if (offset < wpc->iomap.offset || + offset >= wpc->iomap.offset + wpc->iomap.length) { + int error; + + error = __exfat_iomap_begin(wpc->inode, offset, len, + 0, &wpc->iomap, false); + if (error) + return error; + } + + return iomap_add_to_ioend(wpc, folio, offset, end_pos, len); +} + +const struct iomap_writeback_ops exfat_writeback_ops = { + .writeback_range = exfat_writeback_range, + .writeback_submit = iomap_ioend_writeback_submit, +}; + +/** + * exfat_iomap_read_end_io - iomap read bio completion handler for exFAT + * @bio: bio that has completed reading + * + * exfat_iomap_begin() rounds up MAPPED extents to the block boundary of + * valid_size. This ensures that any subsequent blocks are treated as + * IOMAP_UNWRITTEN, but it also causes the "straddle block" containing + * valid_size to be read from disk. The disk data beyond valid_size in + * this block is stale and must be zeroed to prevent data leakage. + */ +static void exfat_iomap_read_end_io(struct bio *bio) +{ + int error = blk_status_to_errno(bio->bi_status); + struct folio_iter iter; + + bio_for_each_folio_all(iter, bio) { + struct folio *folio = iter.folio; + struct exfat_inode_info *ei = EXFAT_I(folio->mapping->host); + s64 valid_size; + loff_t pos = folio_pos(folio); + + valid_size = ei->valid_size; + if (pos + iter.offset < valid_size && + pos + iter.offset + iter.length > valid_size) + folio_zero_segment(folio, offset_in_folio(folio, valid_size), + iter.offset + iter.length); + + iomap_finish_folio_read(folio, iter.offset, iter.length, error); + } + bio_put(bio); +} + +static void exfat_iomap_bio_submit_read(const struct iomap_iter *iter, + struct iomap_read_folio_ctx *ctx) +{ + iomap_bio_submit_read_endio(iter, ctx, exfat_iomap_read_end_io); +} + +const struct iomap_read_ops exfat_iomap_bio_read_ops = { + .read_folio_range = iomap_bio_read_folio_range, + .submit_read = exfat_iomap_bio_submit_read, +}; + +int exfat_iomap_swap_activate(struct swap_info_struct *sis, + struct file *file, sector_t *span) +{ + return iomap_swapfile_activate(sis, file, span, &exfat_iomap_ops); +} |
