lists.openwall.net   lists  /  announce  owl-users  owl-dev  john-users  john-dev  passwdqc-users  yescrypt  popa3d-users  /  oss-security  kernel-hardening  musl  sabotage  tlsify  passwords  /  crypt-dev  xvendor  /  Bugtraq  Full-Disclosure  linux-kernel  linux-netdev  linux-ext4  linux-hardening  linux-cve-announce  PHC 
Open Source and information security mailing list archives
 
Hash Suite: Windows password security audit tool. GUI, reports in PDF.
[<prev] [next>] [day] [month] [year] [list]
Message-ID: <20251226174358.15936-1-almaz.alexandrovich@paragon-software.com>
Date: Fri, 26 Dec 2025 18:43:58 +0100
From: Konstantin Komarov <almaz.alexandrovich@...agon-software.com>
To: <ntfs3@...ts.linux.dev>
CC: <linux-kernel@...r.kernel.org>, <linux-fsdevel@...r.kernel.org>,
	Konstantin Komarov <almaz.alexandrovich@...agon-software.com>
Subject: [PATCH] fs/ntfs3: implement llseek SEEK_DATA/SEEK_HOLE by scanning data runs

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@...agon-software.com>
---
 fs/ntfs3/attrib.c  |  4 +--
 fs/ntfs3/file.c    | 27 +++++++++++++++-
 fs/ntfs3/frecord.c | 76 ++++++++++++++++++++++++++++++++++++++++++++++
 fs/ntfs3/ntfs.h    |  1 +
 fs/ntfs3/ntfs_fs.h |  8 +++++
 5 files changed, 113 insertions(+), 3 deletions(-)

diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c
index 0cd15a0983fe..3e188d6c229f 100644
--- a/fs/ntfs3/attrib.c
+++ b/fs/ntfs3/attrib.c
@@ -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;
 	}
diff --git a/fs/ntfs3/file.c b/fs/ntfs3/file.c
index a88045ab549f..c89b1e7e734c 100644
--- a/fs/ntfs3/file.c
+++ b/fs/ntfs3/file.c
@@ -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,
diff --git a/fs/ntfs3/frecord.c b/fs/ntfs3/frecord.c
index a123e3f0acde..03dcb66b5f6c 100644
--- a/fs/ntfs3/frecord.c
+++ b/fs/ntfs3/frecord.c
@@ -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
  *
diff --git a/fs/ntfs3/ntfs.h b/fs/ntfs3/ntfs.h
index 552b97905813..ae0a6ba102c0 100644
--- a/fs/ntfs3/ntfs.h
+++ b/fs/ntfs3/ntfs.h
@@ -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,
diff --git a/fs/ntfs3/ntfs_fs.h b/fs/ntfs3/ntfs_fs.h
index 482722438bd9..32823e1428a7 100644
--- a/fs/ntfs3/ntfs_fs.h
+++ b/fs/ntfs3/ntfs_fs.h
@@ -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);
-- 
2.43.0


Powered by blists - more mailing lists

Powered by Openwall GNU/*/Linux Powered by OpenVZ