]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
exfat: add support for SEEK_HOLE and SEEK_DATA in llseek
authorNamjae Jeon <linkinjeon@kernel.org>
Sat, 23 May 2026 04:59:28 +0000 (13:59 +0900)
committerNamjae Jeon <linkinjeon@kernel.org>
Mon, 15 Jun 2026 11:00:44 +0000 (20:00 +0900)
Adds exfat_file_llseek() that implements these whence values via
the iomap layer (iomap_seek_hole() and iomap_seek_data()) using the
existing exfat_read_iomap_ops.
Unlike many other modern filesystems, exFAT does not support sparse files
with unallocated clusters (holes). In exFAT, clusters are always fully
allocated once they are written or preallocated. In addition, exFAT
maintains a separate "Valid Data Length" (valid_size) that is distinct
from the logical file size. This affects how holes are reported during
seeking. In exfat_iomap_begin(), ranges where the offset is greater
than or equal to ei->valid_size are mapped as IOMAP_UNWRITTEN, while ranges
below valid_size are mapped as IOMAP_MAPPED. This mapping behavior is used
by the iomap seek functions to correctly report SEEK_HOLE and SEEK_DATA
positions.

   - Ranges with offset >= ei->valid_size are mapped as IOMAP_HOLE.
   - Ranges with offset < ei->valid_size are mapped as IOMAP_MAPPED.

Reviewed-by: Christoph Hellwig <hch@lst.de>
Acked-by: Christoph Hellwig <hch@lst.de>
Acked-by: "Darrick J. Wong" <djwong@kernel.org>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
fs/exfat/file.c
fs/exfat/iomap.c

index 9cd34149a1886bac92936e519c6349936f77e172..c5ff2a97a465804f2669b1ceffdecfe63a23e1ca 100644 (file)
@@ -926,9 +926,32 @@ static int exfat_file_open(struct inode *inode, struct file *filp)
        return 0;
 }
 
+static loff_t exfat_file_llseek(struct file *file, loff_t offset, int whence)
+{
+       struct inode *inode = file->f_mapping->host;
+
+       switch (whence) {
+       case SEEK_HOLE:
+               inode_lock_shared(inode);
+               offset = iomap_seek_hole(inode, offset, &exfat_iomap_ops);
+               inode_unlock_shared(inode);
+               break;
+       case SEEK_DATA:
+               inode_lock_shared(inode);
+               offset = iomap_seek_data(inode, offset, &exfat_iomap_ops);
+               inode_unlock_shared(inode);
+               break;
+       default:
+               return generic_file_llseek(file, offset, whence);
+       }
+       if (offset < 0)
+               return offset;
+       return vfs_setpos(file, offset, inode->i_sb->s_maxbytes);
+}
+
 const struct file_operations exfat_file_operations = {
        .open           = exfat_file_open,
-       .llseek         = generic_file_llseek,
+       .llseek         = exfat_file_llseek,
        .read_iter      = exfat_file_read_iter,
        .write_iter     = exfat_file_write_iter,
        .unlocked_ioctl = exfat_ioctl,
index 7ad94d5806d9b89ff410aa3aef1b6d7ba3bc3830..3ac1eebe997f531ec952616946c0768b8c1cf8c7 100644 (file)
@@ -100,12 +100,36 @@ static int __exfat_iomap_begin(struct inode *inode, loff_t offset, loff_t length
                        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 = IOMAP_UNWRITTEN;
+                       iomap->type = flags & IOMAP_REPORT ?
+                               IOMAP_HOLE : IOMAP_UNWRITTEN;
                } else if (offset + iomap->length > ei->valid_size) {
-                       iomap->length = round_up(ei->valid_size,
-                                                i_blocksize(inode)) -
-                                                       iomap->offset;
+                       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;
                }
        }