NTFS3 file system kernel mode driver
 help / color / mirror / Atom feed
From: Tabriz Hasanli <cybersec467@gmail.com>
To: linux-kernel@vger.kernel.org, ntfs3@lists.linux.dev
Cc: almaz.alexandrovich@paragon-software.com, w@1wt.eu,
	Tabriz Hasanli <cybersec467@gmail.com>
Subject: [PATCH 1/1] fs/ntfs3: validate target index entry in check_if_alloc_index/check_if_root_index
Date: Mon, 31 Aug 2026 10:23:44 -0400	[thread overview]
Message-ID: <20260831142344.472594-2-cybersec467@gmail.com> (raw)
In-Reply-To: <20260831142344.472594-1-cybersec467@gmail.com>

check_if_alloc_index() and check_if_root_index() walk the index entry
chain to verify that lrh->attr_off lands on a valid entry boundary.
However they have two gaps:

(a) They do not stop at de_is_last() — they walk past the last real
    entry into the trailing gap between the last entry's end and
    hdr->used, which check_index_header() leaves unvalidated.

(b) They do not validate the TARGET entry at attr_off — the walk exits
    as soon as o == attr_off without checking the entry's size, flags,
    or bounds.

This allows attr_off to point to an attacker-controlled "fake entry"
in the trailing gap of a crafted NTFS image's $LogFile.  Four do_action()
cases then use the unvalidated entry:

  SetIndexEntryVcnAllocation / SetIndexEntryVcnRoot:
    de_set_vbn_le() writes 8 bytes at e + e->size - 8.
    Crafted e->size yields an 8-byte OOB write up to ~64KB past the
    buffer.

  UpdateFileNameAllocation / UpdateFileNameRoot:
    memmove() writes 56 bytes at a fixed offset from e.  If e is near
    the buffer end, this overflows by up to 0x50 bytes.

The sibling DeleteIndexEntryAllocation case (commit fc4626bb3656) and
UpdateRecordData cases (commit 3e127829e57f) already carry per-site
guards for the same class of attack; this patch closes the gap at the
validator level so all current and future callers are protected.

Fix both functions to:
  - stop walking at de_is_last()
  - validate the target entry: size is 8-byte aligned, >= minimum
    entry size, and does not extend past hdr->used

Found by static source analysis and confirmed with userspace ASan
harnesses transcribing the kernel's exact validation logic on a
512-byte INDEX_BUFFER with faithful check_index_buffer() geometry.

Signed-off-by: Tabriz Hasanli <cybersec467@gmail.com>
---
 fs/ntfs3/fslog.c | 42 ++++++++++++++++++++++++++++++++++++------
 1 file changed, 36 insertions(+), 6 deletions(-)

diff --git a/fs/ntfs3/fslog.c b/fs/ntfs3/fslog.c
index ed50c1d0c..d73e3ff1e 100644
--- a/fs/ntfs3/fslog.c
+++ b/fs/ntfs3/fslog.c
@@ -2953,10 +2953,13 @@ static inline bool check_if_root_index(const struct ATTRIB *attr,
 	u32 o = PtrOffset(attr, hdr) + de_off;
 	const struct NTFS_DE *e = Add2Ptr(hdr, de_off);
 	u32 asize = le32_to_cpu(attr->size);
+	u32 used = le32_to_cpu(hdr->used);
+	bool has_subnode = hdr_has_subnode(hdr);
+	u32 min_de = has_subnode ? sizeof(struct NTFS_DE) + sizeof(u64)
+				: sizeof(struct NTFS_DE);
+	u16 esize;
 
 	while (o < ao) {
-		u16 esize;
-
 		if (o >= asize)
 			break;
 
@@ -2964,11 +2967,24 @@ static inline bool check_if_root_index(const struct ATTRIB *attr,
 		if (!esize)
 			break;
 
+		if (de_is_last(e))
+			break;
+
 		o += esize;
+		de_off += esize;
 		e = Add2Ptr(e, esize);
 	}
 
-	return o == ao;
+	if (o != ao)
+		return false;
+
+	/* Validate the target entry itself. */
+	esize = le16_to_cpu(e->size);
+	if (!IS_ALIGNED(esize, 8) || esize < min_de ||
+	    size_add(de_off, esize) > used)
+		return false;
+
+	return true;
 }
 
 static inline bool check_if_alloc_index(const struct INDEX_HDR *hdr,
@@ -2978,10 +2994,12 @@ static inline bool check_if_alloc_index(const struct INDEX_HDR *hdr,
 	u32 o = offsetof(struct INDEX_BUFFER, ihdr) + de_off;
 	const struct NTFS_DE *e = Add2Ptr(hdr, de_off);
 	u32 used = le32_to_cpu(hdr->used);
+	bool has_subnode = hdr_has_subnode(hdr);
+	u32 min_de = has_subnode ? sizeof(struct NTFS_DE) + sizeof(u64)
+				: sizeof(struct NTFS_DE);
+	u16 esize;
 
 	while (o < attr_off) {
-		u16 esize;
-
 		if (de_off >= used)
 			break;
 
@@ -2989,12 +3007,24 @@ static inline bool check_if_alloc_index(const struct INDEX_HDR *hdr,
 		if (!esize)
 			break;
 
+		if (de_is_last(e))
+			break;
+
 		o += esize;
 		de_off += esize;
 		e = Add2Ptr(e, esize);
 	}
 
-	return o == attr_off;
+	if (o != attr_off)
+		return false;
+
+	/* Validate the target entry itself. */
+	esize = le16_to_cpu(e->size);
+	if (!IS_ALIGNED(esize, 8) || esize < min_de ||
+	    size_add(de_off, esize) > used)
+		return false;
+
+	return true;
 }
 
 static inline void change_attr_size(struct MFT_REC *rec, struct ATTRIB *attr,
-- 
2.53.0


      reply	other threads:[~2026-08-31 14:24 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-31 14:23 [PATCH 0/1] fs/ntfs3: fix OOB writes in do_action() log replay via unvalidated trailing index entry Tabriz Hasanli
2026-08-31 14:23 ` Tabriz Hasanli [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260831142344.472594-2-cybersec467@gmail.com \
    --to=cybersec467@gmail.com \
    --cc=almaz.alexandrovich@paragon-software.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=ntfs3@lists.linux.dev \
    --cc=w@1wt.eu \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox