[PATCH v2 2/2] fs/ntfs3: bound page_lcns[] index by the log record

Xiang Mei <[email protected]> Wed, 17 Jun 2026 16:13:43 -0700
Newsgroups dev.linux.lists.ntfs3,org.kernel.vger.linux-kernel,org.kernel.vger.stable
Message-ID <[email protected]>
The copy_lcns loop and the redo shorten loop index page_lcns[] at j + i,
where i runs up to the log record's lcns_follow. That count is checked only
against the record's own length, not the target entry, so check_dp_table()
(which validates the entry's lcns_follow) does not cover it: the copy_lcns
entry may even be freshly allocated after that check, and find_dp() bounds j
but not i. A crafted record thus overflows page_lcns[] of an otherwise valid
entry.

Add dp_range_ok() and reject, before each loop, any record whose run does
not fit the entry. These are the only two page_lcns[] accesses indexed by
the record rather than the entry, so together with the entry validation
every access is now bounded.

Fixes: b46acd6a6a62 ("fs/ntfs3: Add NTFS journal")
Cc: [email protected]
Reported-by: Weiming Shi <[email protected]>
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Xiang Mei <[email protected]>
---
v2: resend to public mailing list

 fs/ntfs3/fslog.c | 25 +++++++++++++++++++++----
 1 file changed, 21 insertions(+), 4 deletions(-)

diff --git a/fs/ntfs3/fslog.c b/fs/ntfs3/fslog.c
index b1ca84d83de5..1cc7402fdc2c 100644
--- a/fs/ntfs3/fslog.c
+++ b/fs/ntfs3/fslog.c
@@ -647,6 +647,14 @@ static inline void *enum_rstbl(struct RESTART_TABLE *t, void *c)
 	return NULL;
 }
 
+/*
+ * dp_range_ok - true if [j, j + count) fits in a page_lcns[cap] array.
+ */
+static inline bool dp_range_ok(size_t j, u32 count, u32 cap)
+{
+	return j < cap && count <= cap - j;
+}
+
 /*
  * find_dp - Search for a @vcn in Dirty Page Table.
  */
@@ -3801,6 +3809,7 @@ int log_replay(struct ntfs_inode *ni, bool *initialized)
 	u64 t64;
 	u16 t16;
 	u32 t32;
+	size_t j;
 
 	log = kzalloc_obj(struct ntfs_log, GFP_NOFS);
 	if (!log)
@@ -4566,9 +4575,12 @@ int log_replay(struct ntfs_inode *ni, bool *initialized)
 		 * whole routine a loop, case Lcns do not fit below.
 		 */
 		t16 = le16_to_cpu(lrh->lcns_follow);
+		j = le64_to_cpu(lrh->target_vcn) - le64_to_cpu(dp->vcn);
+		if (!dp_range_ok(j, t16, le32_to_cpu(dp->lcns_follow))) {
+			err = -EINVAL;
+			goto out;
+		}
 		for (i = 0; i < t16; i++) {
-			size_t j = (size_t)(le64_to_cpu(lrh->target_vcn) -
-					    le64_to_cpu(dp->vcn));
 			dp->page_lcns[j + i] = lrh->page_lcns[i];
 		}
 
@@ -4985,8 +4997,14 @@ int log_replay(struct ntfs_inode *ni, bool *initialized)
 	/* Shorten length by any Lcns which were deleted. */
 	saved_len = dlen;
 
+	j = le64_to_cpu(lrh->target_vcn) - le64_to_cpu(dp->vcn);
+	if (!dp_range_ok(j, le16_to_cpu(lrh->lcns_follow),
+			 le32_to_cpu(dp->lcns_follow))) {
+		err = -EINVAL;
+		goto out;
+	}
+
 	for (i = le16_to_cpu(lrh->lcns_follow); i; i--) {
-		size_t j;
 		u32 alen, voff;
 
 		voff = le16_to_cpu(lrh->record_off) +
@@ -4994,7 +5012,6 @@ int log_replay(struct ntfs_inode *ni, bool *initialized)
 		voff += le16_to_cpu(lrh->cluster_off) << SECTOR_SHIFT;
 
 		/* If the Vcn question is allocated, we can just get out. */
-		j = le64_to_cpu(lrh->target_vcn) - le64_to_cpu(dp->vcn);
 		if (dp->page_lcns[j + i - 1])
 			break;
 
-- 
2.43.0