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
prev parent 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.