linux-kernel.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH] fs/ntfs3: validate dirty page table entry sizes
@ 2026-06-11 21:33 Kyle Zeng
  2026-07-23 14:25 ` Konstantin Komarov
  2026-07-24 11:30 ` Konstantin Komarov
  0 siblings, 2 replies; 6+ messages in thread
From: Kyle Zeng @ 2026-06-11 21:33 UTC (permalink / raw)
  To: ntfs3; +Cc: linux-kernel, Konstantin Komarov, outbounddisclosures, Kyle Zeng

The generic restart table check only verifies the table header and
free-list shape.  Dirty Page Table entries also carry an entry-local
LCN array whose length is controlled by lcns_follow, and replay later
trusts that value when converting version-0 entries and when copying
LCNs from log records.

A malformed $LogFile can provide a Dirty Page Table dump whose generic
restart table is valid but whose entry is too small for the declared
lcns_follow count.  log_replay() can then run the version-0 conversion
memmove() past the kmemdup() allocation, or later copy/read past
dp->page_lcns when a log record spans beyond the matched DPT entry.

Add Dirty Page Table-specific validation before copying the table:
require entries to be large enough for the typed DPT layout, require
the old version-0 source layout to fit before conversion, and require
lcns_follow to fit within the restart table entry.  Also validate each
log record's LCN span against the matched DPT entry before touching
dp->page_lcns.

Assisted-by: Codex:gpt-5.5
Signed-off-by: Kyle Zeng <kylebot@openai.com>
---
 fs/ntfs3/fslog.c | 61 +++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 55 insertions(+), 6 deletions(-)

diff --git a/fs/ntfs3/fslog.c b/fs/ntfs3/fslog.c
index acfa18b84401..1fc9d04806b5 100644
--- a/fs/ntfs3/fslog.c
+++ b/fs/ntfs3/fslog.c
@@ -647,6 +647,23 @@ static inline void *enum_rstbl(struct RESTART_TABLE *t, void *c)
 	return NULL;
 }
 
+static inline bool dptbl_lcns_in_range(const struct DIR_PAGE_ENTRY *dp,
+				       u64 vcn, u16 lcns_follow)
+{
+	u64 dp_vcn = le64_to_cpu(dp->vcn);
+	u32 dp_lcns = le32_to_cpu(dp->lcns_follow);
+	u64 off;
+
+	if (vcn < dp_vcn)
+		return false;
+
+	off = vcn - dp_vcn;
+	if (off > dp_lcns)
+		return false;
+
+	return lcns_follow <= dp_lcns - (u32)off;
+}
+
 /*
  * find_dp - Search for a @vcn in Dirty Page Table.
  */
@@ -657,12 +674,8 @@ static inline struct DIR_PAGE_ENTRY *find_dp(struct RESTART_TABLE *dptbl,
 	struct DIR_PAGE_ENTRY *dp = NULL;
 
 	while ((dp = enum_rstbl(dptbl, dp))) {
-		u64 dp_vcn = le64_to_cpu(dp->vcn);
-
-		if (dp->target_attr == ta && vcn >= dp_vcn &&
-		    vcn < dp_vcn + le32_to_cpu(dp->lcns_follow)) {
+		if (dp->target_attr == ta && dptbl_lcns_in_range(dp, vcn, 1))
 			return dp;
-		}
 	}
 	return NULL;
 }
@@ -778,6 +791,37 @@ static bool check_rstbl(const struct RESTART_TABLE *rt, size_t bytes)
 	return true;
 }
 
+static bool check_dptbl(const struct RESTART_TABLE *rt, bool is_restart_v0)
+{
+	u16 rsize = le16_to_cpu(rt->size);
+	struct DIR_PAGE_ENTRY *dp = NULL;
+
+	if (rsize < sizeof(struct DIR_PAGE_ENTRY))
+		return false;
+
+	if (is_restart_v0 && rsize < sizeof(struct DIR_PAGE_ENTRY_32))
+		return false;
+
+	while ((dp = enum_rstbl((struct RESTART_TABLE *)rt, dp))) {
+		u32 lcns_follow = le32_to_cpu(dp->lcns_follow);
+		size_t bytes;
+
+		bytes = struct_size(dp, page_lcns, lcns_follow);
+		if (bytes > rsize)
+			return false;
+
+		if (!is_restart_v0)
+			continue;
+
+		bytes = size_add(offsetof(struct DIR_PAGE_ENTRY_32, page_lcns_low),
+				 array_size(lcns_follow, sizeof(u64)));
+		if (bytes > rsize)
+			return false;
+	}
+
+	return true;
+}
+
 /*
  * free_rsttbl_idx - Free a previously allocated index a Restart Table.
  */
@@ -4204,7 +4248,7 @@ int log_replay(struct ntfs_inode *ni, bool *initialized)
 	t32 = rec_len - t16;
 
 	/* Now check that this is a valid restart table. */
-	if (!check_rstbl(rt, t32)) {
+	if (!check_rstbl(rt, t32) || !check_dptbl(rt, !rst->major_ver)) {
 		err = -EINVAL;
 		goto out;
 	}
@@ -4547,9 +4591,13 @@ 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);
+		if (!dptbl_lcns_in_range(dp, t64, t16)) {
+			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));
+			size_t j = (size_t)(t64 - le64_to_cpu(dp->vcn));
 			dp->page_lcns[j + i] = lrh->page_lcns[i];
 		}
 
@@ -4928,6 +4976,12 @@ int log_replay(struct ntfs_inode *ni, bool *initialized)
 	if (!dp)
 		goto read_next_log_do_action;
 
+	t16 = le16_to_cpu(lrh->lcns_follow);
+	if (!dptbl_lcns_in_range(dp, t64, t16)) {
+		err = -EINVAL;
+		goto out;
+	}
+
 	if (rec_lsn < le64_to_cpu(dp->oldest_lsn))
 		goto read_next_log_do_action;
 
-- 
2.43.0

^ permalink raw reply related	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-08-04  1:16 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-11 21:33 [PATCH] fs/ntfs3: validate dirty page table entry sizes Kyle Zeng
2026-07-23 14:25 ` Konstantin Komarov
2026-07-24 11:30 ` Konstantin Komarov
2026-07-25 18:25   ` Kyle Zeng
2026-07-29 10:56     ` Konstantin Komarov
2026-08-04  1:16       ` Kyle Zeng

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).