]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
fs/ntfs3: implement llseek SEEK_DATA/SEEK_HOLE by scanning data runs
authorKonstantin Komarov <almaz.alexandrovich@paragon-software.com>
Fri, 26 Dec 2025 12:59:47 +0000 (15:59 +0300)
committerKonstantin Komarov <almaz.alexandrovich@paragon-software.com>
Mon, 29 Dec 2025 13:33:31 +0000 (13:33 +0000)
The generic llseek implementation does not understand ntfs data runs,
sparse regions, or compression semantics, and therefore cannot correctly
locate data or holes in files.

Add a filesystem-specific llseek handler that scans attribute data runs
to find the next data or hole starting at the given offset. Handle
resident attributes, sparse runs, compressed holes, and the implicit hole
at end-of-file.

Signed-off-by: Konstantin Komarov <almaz.alexandrovich@paragon-software.com>
fs/ntfs3/attrib.c
fs/ntfs3/file.c
fs/ntfs3/frecord.c
fs/ntfs3/ntfs.h
fs/ntfs3/ntfs_fs.h

index 0cd15a0983fee3d49a49c01f217324cc575ad4c3..3e188d6c229f6332ffba4ef8c0079fc566135313 100644 (file)
@@ -940,7 +940,7 @@ int attr_data_get_block(struct ntfs_inode *ni, CLST vcn, CLST clen, CLST *lcn,
 
        if (!attr_b->non_res) {
                *lcn = RESIDENT_LCN;
-               *len = 1;
+               *len = le32_to_cpu(attr_b->res.data_size);
                goto out;
        }
 
@@ -950,7 +950,7 @@ int attr_data_get_block(struct ntfs_inode *ni, CLST vcn, CLST clen, CLST *lcn,
                        err = -EINVAL;
                } else {
                        *len = 1;
-                       *lcn = SPARSE_LCN;
+                       *lcn = EOF_LCN;
                }
                goto out;
        }
index a88045ab549f20c18dba3931cda2b9a9c635b705..c89b1e7e734c1efb8cbbf3f5cc3c514c08981f3c 100644 (file)
@@ -1474,6 +1474,31 @@ int ntfs_file_fsync(struct file *file, loff_t start, loff_t end, int datasync)
        return ret;
 }
 
+/*
+ * ntfs_llseek - file_operations::llseek
+ */
+static loff_t ntfs_llseek(struct file *file, loff_t offset, int whence)
+{
+       struct inode *inode = file->f_mapping->host;
+       struct ntfs_inode *ni = ntfs_i(inode);
+       loff_t maxbytes = ntfs_get_maxbytes(ni);
+       loff_t ret;
+
+       if (whence == SEEK_DATA || whence == SEEK_HOLE) {
+               inode_lock_shared(inode);
+               /* Scan fragments for hole or data. */
+               ret = ni_seek_data_or_hole(ni, offset, whence == SEEK_DATA);
+               inode_unlock_shared(inode);
+
+               if (ret >= 0)
+                       ret = vfs_setpos(file, ret, maxbytes);
+       } else {
+               ret = generic_file_llseek_size(file, offset, whence, maxbytes,
+                                              i_size_read(inode));
+       }
+       return ret;
+}
+
 // clang-format off
 const struct inode_operations ntfs_file_inode_operations = {
        .getattr        = ntfs_getattr,
@@ -1485,7 +1510,7 @@ const struct inode_operations ntfs_file_inode_operations = {
 };
 
 const struct file_operations ntfs_file_operations = {
-       .llseek         = generic_file_llseek,
+       .llseek         = ntfs_llseek,
        .read_iter      = ntfs_file_read_iter,
        .write_iter     = ntfs_file_write_iter,
        .unlocked_ioctl = ntfs_ioctl,
index a123e3f0acdeb44042ca777bfb5723118831c812..03dcb66b5f6c31dec7cbde7339a9c3b3495746df 100644 (file)
@@ -3001,6 +3001,82 @@ bool ni_is_dirty(struct inode *inode)
        return false;
 }
 
+/*
+ * ni_seek_data_or_hole
+ *
+ * Helper function for ntfs_llseek( SEEK_DATA/SEEK_HOLE )
+ */
+loff_t ni_seek_data_or_hole(struct ntfs_inode *ni, loff_t offset, bool data)
+{
+       int err;
+       u8 cluster_bits = ni->mi.sbi->cluster_bits;
+       CLST vcn, lcn, clen;
+       loff_t vbo;
+
+       /* Enumerate all fragments. */
+       for (vcn = offset >> cluster_bits;; vcn += clen) {
+               err = attr_data_get_block(ni, vcn, 1, &lcn, &clen, NULL, false);
+               if (err) {
+                       return err;
+               }
+
+               if (lcn == RESIDENT_LCN) {
+                       /* clen - resident size in bytes. clen == ni->vfs_inode.i_size */
+                       if (offset >= clen) {
+                               /* check eof. */
+                               return -ENXIO;
+                       }
+
+                       if (data) {
+                               return offset;
+                       }
+
+                       return clen;
+               }
+
+               if (lcn == EOF_LCN) {
+                       if (data) {
+                               return -ENXIO;
+                       }
+
+                       /* implicit hole at the end of file. */
+                       return ni->vfs_inode.i_size;
+               }
+
+               if (data) {
+                       /*
+                        * Adjust the file offset to the next location in the file greater than
+                        * or equal to offset containing data. If offset points to data, then
+                        * the file offset is set to offset.
+                        */
+                       if (lcn != SPARSE_LCN) {
+                               vbo = (u64)vcn << cluster_bits;
+                               return max(vbo, offset);
+                       }
+               } else {
+                       /*
+                        * Adjust the file offset to the next hole in the file greater than or 
+                        * equal to offset. If offset points into the middle of a hole, then the
+                        * file offset is set to offset. If there is no hole past offset, then the 
+                        * file offset is adjusted to the end of the file
+                        * (i.e., there is an implicit hole at the end of any file).
+                        */
+                       if (lcn == SPARSE_LCN &&
+                           /* native compression hole begins at aligned vcn. */
+                           (!(ni->std_fa & FILE_ATTRIBUTE_COMPRESSED) ||
+                            !(vcn & (NTFS_LZNT_CLUSTERS - 1)))) {
+                               vbo = (u64)vcn << cluster_bits;
+                               return max(vbo, offset);
+                       }
+               }
+
+               if (!clen) {
+                       /* Corrupted file. */
+                       return -EINVAL;
+               }
+       }
+}
+
 /*
  * ni_write_parents
  *
index 552b97905813bc5d47440b084c78ee61e83a348b..ae0a6ba102c02b17a1586b3605c5638af8dbfa45 100644 (file)
@@ -81,6 +81,7 @@ typedef u32 CLST;
 #define SPARSE_LCN     ((CLST)-1)
 #define RESIDENT_LCN   ((CLST)-2)
 #define COMPRESSED_LCN ((CLST)-3)
+#define EOF_LCN       ((CLST)-4)
 
 enum RECORD_NUM {
        MFT_REC_MFT             = 0,
index 482722438bd998bc94575375baec7ea58cffd1f0..32823e1428a7640fde2a4baf38693e432aa001ec 100644 (file)
@@ -591,6 +591,7 @@ int ni_rename(struct ntfs_inode *dir_ni, struct ntfs_inode *new_dir_ni,
              struct NTFS_DE *new_de);
 
 bool ni_is_dirty(struct inode *inode);
+loff_t ni_seek_data_or_hole(struct ntfs_inode *ni, loff_t offset, bool data);
 int ni_write_parents(struct ntfs_inode *ni, int sync);
 
 /* Globals from fslog.c */
@@ -1107,6 +1108,13 @@ static inline int is_resident(struct ntfs_inode *ni)
        return ni->ni_flags & NI_FLAG_RESIDENT;
 }
 
+static inline loff_t ntfs_get_maxbytes(struct ntfs_inode *ni)
+{
+       struct ntfs_sb_info *sbi = ni->mi.sbi;
+       return is_sparsed(ni) || is_compressed(ni) ? sbi->maxbytes_sparse :
+                                                    sbi->maxbytes;
+}
+
 static inline void le16_sub_cpu(__le16 *var, u16 val)
 {
        *var = cpu_to_le16(le16_to_cpu(*var) - val);