summaryrefslogtreecommitdiffstats
path: root/fs/exfat/iomap.c
diff options
context:
space:
mode:
authorKumar Kartikeya Dwivedi <memxor@gmail.com>2026-09-04 10:43:19 +0200
committerAlexei Starovoitov <ast@kernel.org>2026-09-04 07:58:36 -0700
commit6668ed271eaefaa63e686bdfbedaeb7b8e492722 (patch)
tree58e8288fead484f6755ad0815fc74c3742663761 /fs/exfat/iomap.c
downloadlinux-stable-6668ed271eaefaa63e686bdfbedaeb7b8e492722.tar.gz
linux-stable-6668ed271eaefaa63e686bdfbedaeb7b8e492722.zip
selftests/bpf: Reject graph kptr use after RCU unlockgrafted
Add a sleepable verifier test that loads a graph-node local kptr in an explicit RCU read-side critical section, then passes its node to bpf_rbtree_remove() after the section ends. Before the verifier fix, the stale NON_OWN_REF flag makes the node look like a live borrowed reference and the program is accepted. After the fix, the pointer is demoted without NON_OWN_REF and the graph kfunc argument is rejected. Also exercise a graph kptr loaded while a spin lock provides implicit RCU protection. The pointer must be invalidated when the lock is released, which guards the required ordering between non-owning-reference invalidation and RCU demotion. Update the existing fault-protected load test state description. The post-unlock pointer no longer carries NON_OWN_REF, but remains readable because the load is rewritten to use BPF_PROBE_MEM. Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com> Link: https://lore.kernel.org/r/20260904084325.52250-7-memxor@gmail.com Signed-off-by: Alexei Starovoitov <ast@kernel.org>
Diffstat (limited to 'fs/exfat/iomap.c')
-rw-r--r--fs/exfat/iomap.c272
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);
+}