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

From: Xiang Mei

Date: Wed Jun 17 2026 - 19:14:56 EST


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: stable@xxxxxxxxxxxxxxx
Reported-by: Weiming Shi <bestswngs@xxxxxxxxx>
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Xiang Mei <xmei5@xxxxxxx>
---
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