mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH 6.1.y 0/4] fs/ntfs3: backport slab-out-of-bounds fix (CVE-2026-90048)
@ 2026-10-01 11:45 Nikita Nagornyi
  2026-10-01 11:45 ` [PATCH 6.1.y 1/4] fs/ntfs3: Add more checks in mi_enum_attr (part 2) Nikita Nagornyi
                   ` (3 more replies)
  0 siblings, 4 replies; 7+ messages in thread
From: Nikita Nagornyi @ 2026-10-01 11:45 UTC (permalink / raw)
  To: stable, Konstantin Komarov, ntfs3, linux-kernel

This series backports the fix for a slab-out-of-bounds write in the ntfs3
driver (CVE-2026-90048, an Important / CVSS 7.8 issue) to 6.1.y, together
with its stable dependency chain.

Four commits do not cherry-pick cleanly onto 6.1.y, so each carries a comment,
where adapted. Applied in order:

  a8f659ce14f8 ("fs/ntfs3: Add more checks in mi_enum_attr (part 2)")
      - dependency
  2afd4d267e6d ("fs/ntfs3: Mark inode as bad as soon as error detected in
      mi_enum_attr()")
      - dependency; introduces the struct ntfs_inode *ni parameter threaded
        through mi_enum_attr()/mi_find_attr()/mi_insert_attr()/mi_new_attt_id()
  5f33da04e6ce ("fs/ntfs3: fix KMSAN uninit-value in ni_create_attr_list")
      - dependency
  7c4841e2a627 ("fs/ntfs3: fix slab-out-of-bounds write in
      ni_create_attr_list()")
      - the fix

Built and boot-tested on x86_64 and aarch64. Tested on Amazon EC2 using our
internal test suite, including xfstests, kselftest, LTP; passed with no regressions.

HE WEI(ギカク) (1):
  fs/ntfs3: fix slab-out-of-bounds write in ni_create_attr_list()

Konstantin Komarov (2):
  fs/ntfs3: Add more checks in mi_enum_attr (part 2)
  fs/ntfs3: Mark inode as bad as soon as error detected in
    mi_enum_attr()

Nirbhay Sharma (1):
  fs/ntfs3: fix KMSAN uninit-value in ni_create_attr_list

 fs/ntfs3/attrib.c  | 11 +++---
 fs/ntfs3/frecord.c | 81 +++++++++++++++++++++++++----------------
 fs/ntfs3/ntfs_fs.h | 23 ++++++------
 fs/ntfs3/record.c  | 91 +++++++++++++++++++++++++++-------------------
 4 files changed, 120 insertions(+), 86 deletions(-)


base-commit: 1a8763b93150b2c8f3992c27a5ce7857fee4ab0f
--
2.50.1


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

* [PATCH 6.1.y 1/4] fs/ntfs3: Add more checks in mi_enum_attr (part 2)
  2026-10-01 11:45 [PATCH 6.1.y 0/4] fs/ntfs3: backport slab-out-of-bounds fix (CVE-2026-90048) Nikita Nagornyi
@ 2026-10-01 11:45 ` Nikita Nagornyi
  2026-10-02 14:19   ` Sasha Levin
  2026-10-01 11:45 ` [PATCH 6.1.y 2/4] fs/ntfs3: Mark inode as bad as soon as error detected in mi_enum_attr() Nikita Nagornyi
                   ` (2 subsequent siblings)
  3 siblings, 1 reply; 7+ messages in thread
From: Nikita Nagornyi @ 2026-10-01 11:45 UTC (permalink / raw)
  To: stable, Konstantin Komarov, ntfs3, linux-kernel; +Cc: lei lu

From: Konstantin Komarov <almaz.alexandrovich@paragon-software.com>

Add offset check before access to attr->non_res field as mentioned in [1].

[1] https://lore.kernel.org/ntfs3/20241010110005.42792-1-llfamsec@gmail.com/

Suggested-by: lei lu <llfamsec@gmail.com>
Signed-off-by: Konstantin Komarov <almaz.alexandrovich@paragon-software.com>
Signed-off-by: Nikita Nagornyi <nagornyi@amazon.de>
(cherry picked from commit a8f659ce14f8f734894227ba2c5ca78c8451501e)
---
 fs/ntfs3/record.c | 16 +++++++++++++---
 1 file changed, 13 insertions(+), 3 deletions(-)

diff --git a/fs/ntfs3/record.c b/fs/ntfs3/record.c
index bfb1f4c2f271..d8d43f6a96fd 100644
--- a/fs/ntfs3/record.c
+++ b/fs/ntfs3/record.c
@@ -206,7 +206,7 @@ struct ATTRIB *mi_enum_attr(struct mft_inode *mi, struct ATTRIB *attr)
 			return NULL;

 		if (off >= used || off < MFTRECORD_FIXUP_OFFSET_1 ||
-		    !IS_ALIGNED(off, 4)) {
+		    !IS_ALIGNED(off, 8)) {
 			return NULL;
 		}

@@ -230,8 +230,11 @@ struct ATTRIB *mi_enum_attr(struct mft_inode *mi, struct ATTRIB *attr)
 		off += asize;
 	}

-	/* Can we use the first field (attr->type). */
-	/* NOTE: this code also checks attr->size availability. */
+	/*
+	 * Can we use the first fields:
+	 * attr->type,
+	 * attr->size
+	 */
 	if (off + 8 > used) {
 		static_assert(ALIGN(sizeof(enum ATTR_TYPE), 8) == 8);
 		return NULL;
@@ -253,10 +256,17 @@ struct ATTRIB *mi_enum_attr(struct mft_inode *mi, struct ATTRIB *attr)

 	asize = le32_to_cpu(attr->size);

+	if (!IS_ALIGNED(asize, 8))
+		return NULL;
+
 	/* Check overflow and boundary. */
 	if (off + asize < off || off + asize > used)
 		return NULL;

+	/* Can we use the field attr->non_res. */
+	if (off + 9 > used)
+		return NULL;
+
 	/* Check size of attribute. */
 	if (!attr->non_res) {
 		/* Check resident fields. */

base-commit: 1a8763b93150b2c8f3992c27a5ce7857fee4ab0f
--
2.50.1


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

* [PATCH 6.1.y 2/4] fs/ntfs3: Mark inode as bad as soon as error detected in mi_enum_attr()
  2026-10-01 11:45 [PATCH 6.1.y 0/4] fs/ntfs3: backport slab-out-of-bounds fix (CVE-2026-90048) Nikita Nagornyi
  2026-10-01 11:45 ` [PATCH 6.1.y 1/4] fs/ntfs3: Add more checks in mi_enum_attr (part 2) Nikita Nagornyi
@ 2026-10-01 11:45 ` Nikita Nagornyi
  2026-10-01 11:45 ` [PATCH 6.1.y 3/4] fs/ntfs3: fix KMSAN uninit-value in ni_create_attr_list Nikita Nagornyi
  2026-10-01 11:45 ` [PATCH 6.1.y 4/4] fs/ntfs3: fix slab-out-of-bounds write in ni_create_attr_list() Nikita Nagornyi
  3 siblings, 0 replies; 7+ messages in thread
From: Nikita Nagornyi @ 2026-10-01 11:45 UTC (permalink / raw)
  To: stable, Konstantin Komarov, ntfs3, linux-kernel
  Cc: syzbot+73d8fc29ec7cba8286fa

From: Konstantin Komarov <almaz.alexandrovich@paragon-software.com>

[ Upstream commit 2afd4d267e6dbaec8d3ccd4f5396cb84bc67aa2e ]

Extended the `mi_enum_attr()` function interface with an additional
parameter, `struct ntfs_inode *ni`, to allow marking the inode
as bad as soon as an error is detected.

Reported-by: syzbot+73d8fc29ec7cba8286fa@syzkaller.appspotmail.com
Signed-off-by: Konstantin Komarov <almaz.alexandrovich@paragon-software.com>

[ Backport to 6.1.y: passed `ni` to an extra `mi_find_attr()` caller
that mainline had refactored differently. `mi_find_attr()`'s `name_len`
argument is kept as 6.1.y's `size_t`. Kept 6.1.y's last resident fields
check in `mi_enum_attr()` but replaced `return NULL` with `goto out`. ]

Signed-off-by: Nikita Nagornyi <nagornyi@amazon.de>
(cherry picked from commit 2afd4d267e6dbaec8d3ccd4f5396cb84bc67aa2e)
---
 fs/ntfs3/attrib.c  | 11 ++++---
 fs/ntfs3/frecord.c | 59 ++++++++++++++++++----------------
 fs/ntfs3/ntfs_fs.h | 21 ++++++------
 fs/ntfs3/record.c  | 79 ++++++++++++++++++++++++----------------------
 4 files changed, 90 insertions(+), 80 deletions(-)

diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c
index f4573d276a2c..db4f2dffed95 100644
--- a/fs/ntfs3/attrib.c
+++ b/fs/ntfs3/attrib.c
@@ -815,7 +815,8 @@ int attr_set_size(struct ntfs_inode *ni, enum ATTR_TYPE type,
 		if (err)
 			goto out;

-		attr = mi_find_attr(mi, NULL, type, name, name_len, &le->id);
+		attr = mi_find_attr(ni, mi, NULL, type, name, name_len,
+				    &le->id);
 		if (!attr) {
 			err = -EINVAL;
 			goto bad_inode;
@@ -1166,7 +1167,7 @@ int attr_data_get_block(struct ntfs_inode *ni, CLST vcn, CLST clen, CLST *lcn,
 				goto out;
 			}

-			attr = mi_find_attr(mi, NULL, ATTR_DATA, NULL, 0,
+			attr = mi_find_attr(ni, mi, NULL, ATTR_DATA, NULL, 0,
 					    &le->id);
 			if (!attr) {
 				err = -EINVAL;
@@ -1786,7 +1787,7 @@ int attr_allocate_frame(struct ntfs_inode *ni, CLST frame, size_t compr_size,
 				goto out;
 			}

-			attr = mi_find_attr(mi, NULL, ATTR_DATA, NULL, 0,
+			attr = mi_find_attr(ni, mi, NULL, ATTR_DATA, NULL, 0,
 					    &le->id);
 			if (!attr) {
 				err = -EINVAL;
@@ -2031,8 +2032,8 @@ int attr_collapse_range(struct ntfs_inode *ni, u64 vbo, u64 bytes)
 				}

 				/* Look for required attribute. */
-				attr = mi_find_attr(mi, NULL, ATTR_DATA, NULL,
-						    0, &le->id);
+				attr = mi_find_attr(ni, mi, NULL, ATTR_DATA,
+						    NULL, 0, &le->id);
 				if (!attr) {
 					err = -EINVAL;
 					goto out;
diff --git a/fs/ntfs3/frecord.c b/fs/ntfs3/frecord.c
index fcdaea02ba04..17559fca5984 100644
--- a/fs/ntfs3/frecord.c
+++ b/fs/ntfs3/frecord.c
@@ -75,7 +75,7 @@ struct ATTR_STD_INFO *ni_std(struct ntfs_inode *ni)
 {
 	const struct ATTRIB *attr;

-	attr = mi_find_attr(&ni->mi, NULL, ATTR_STD, NULL, 0, NULL);
+	attr = mi_find_attr(ni, &ni->mi, NULL, ATTR_STD, NULL, 0, NULL);
 	return attr ? resident_data_ex(attr, sizeof(struct ATTR_STD_INFO))
 		    : NULL;
 }
@@ -89,7 +89,7 @@ struct ATTR_STD_INFO5 *ni_std5(struct ntfs_inode *ni)
 {
 	const struct ATTRIB *attr;

-	attr = mi_find_attr(&ni->mi, NULL, ATTR_STD, NULL, 0, NULL);
+	attr = mi_find_attr(ni, &ni->mi, NULL, ATTR_STD, NULL, 0, NULL);

 	return attr ? resident_data_ex(attr, sizeof(struct ATTR_STD_INFO5))
 		    : NULL;
@@ -201,7 +201,8 @@ struct ATTRIB *ni_find_attr(struct ntfs_inode *ni, struct ATTRIB *attr,
 			*mi = &ni->mi;

 		/* Look for required attribute in primary record. */
-		return mi_find_attr(&ni->mi, attr, type, name, name_len, NULL);
+		return mi_find_attr(ni, &ni->mi, attr, type, name, name_len,
+				    NULL);
 	}

 	/* First look for list entry of required type. */
@@ -217,7 +218,7 @@ struct ATTRIB *ni_find_attr(struct ntfs_inode *ni, struct ATTRIB *attr,
 		return NULL;

 	/* Look for required attribute. */
-	attr = mi_find_attr(m, NULL, type, name, name_len, &le->id);
+	attr = mi_find_attr(ni, m, NULL, type, name, name_len, &le->id);

 	if (!attr)
 		goto out;
@@ -258,7 +259,7 @@ struct ATTRIB *ni_enum_attr_ex(struct ntfs_inode *ni, struct ATTRIB *attr,
 		if (mi)
 			*mi = &ni->mi;
 		/* Enum attributes in primary record. */
-		return mi_enum_attr(&ni->mi, attr);
+		return mi_enum_attr(ni, &ni->mi, attr);
 	}

 	/* Get next list entry. */
@@ -274,7 +275,7 @@ struct ATTRIB *ni_enum_attr_ex(struct ntfs_inode *ni, struct ATTRIB *attr,
 		*mi = mi2;

 	/* Find attribute in loaded record. */
-	return rec_find_attr_le(mi2, le2);
+	return rec_find_attr_le(ni, mi2, le2);
 }

 /*
@@ -292,7 +293,8 @@ struct ATTRIB *ni_load_attr(struct ntfs_inode *ni, enum ATTR_TYPE type,
 	if (!ni->attr_list.size) {
 		if (pmi)
 			*pmi = &ni->mi;
-		return mi_find_attr(&ni->mi, NULL, type, name, name_len, NULL);
+		return mi_find_attr(ni, &ni->mi, NULL, type, name, name_len,
+				    NULL);
 	}

 	le = al_find_ex(ni, NULL, type, name, name_len, NULL);
@@ -318,7 +320,7 @@ struct ATTRIB *ni_load_attr(struct ntfs_inode *ni, enum ATTR_TYPE type,
 	if (pmi)
 		*pmi = mi;

-	attr = mi_find_attr(mi, NULL, type, name, name_len, &le->id);
+	attr = mi_find_attr(ni, mi, NULL, type, name, name_len, &le->id);
 	if (!attr)
 		return NULL;

@@ -399,7 +401,8 @@ int ni_remove_attr(struct ntfs_inode *ni, enum ATTR_TYPE type,
 	int diff;

 	if (base_only || type == ATTR_LIST || !ni->attr_list.size) {
-		attr = mi_find_attr(&ni->mi, NULL, type, name, name_len, id);
+		attr = mi_find_attr(ni, &ni->mi, NULL, type, name, name_len,
+				    id);
 		if (!attr)
 			return -ENOENT;

@@ -438,7 +441,7 @@ int ni_remove_attr(struct ntfs_inode *ni, enum ATTR_TYPE type,

 		al_remove_le(ni, le);

-		attr = mi_find_attr(mi, NULL, type, name, name_len, id);
+		attr = mi_find_attr(ni, mi, NULL, type, name, name_len, id);
 		if (!attr)
 			return -ENOENT;

@@ -486,7 +489,7 @@ ni_ins_new_attr(struct ntfs_inode *ni, struct mft_inode *mi,
 		name = le->name;
 	}

-	attr = mi_insert_attr(mi, type, name, name_len, asize, name_off);
+	attr = mi_insert_attr(ni, mi, type, name, name_len, asize, name_off);
 	if (!attr) {
 		if (le_added)
 			al_remove_le(ni, le);
@@ -670,7 +673,7 @@ static int ni_try_remove_attr_list(struct ntfs_inode *ni)
 	if (err)
 		return err;

-	attr_list = mi_find_attr(&ni->mi, NULL, ATTR_LIST, NULL, 0, NULL);
+	attr_list = mi_find_attr(ni, &ni->mi, NULL, ATTR_LIST, NULL, 0, NULL);
 	if (!attr_list)
 		return 0;

@@ -692,7 +695,7 @@ static int ni_try_remove_attr_list(struct ntfs_inode *ni)
 		if (!mi)
 			return 0;

-		attr = mi_find_attr(mi, NULL, le->type, le_name(le),
+		attr = mi_find_attr(ni, mi, NULL, le->type, le_name(le),
 				    le->name_len, &le->id);
 		if (!attr)
 			return 0;
@@ -728,7 +731,7 @@ static int ni_try_remove_attr_list(struct ntfs_inode *ni)
 			goto out;
 		}

-		attr = mi_find_attr(mi, NULL, le->type, le_name(le),
+		attr = mi_find_attr(ni, mi, NULL, le->type, le_name(le),
 				    le->name_len, &le->id);
 		if (!attr) {
 			/* Should never happened, 'cause already checked. */
@@ -737,7 +740,7 @@ static int ni_try_remove_attr_list(struct ntfs_inode *ni)
 		asize = le32_to_cpu(attr->size);

 		/* Insert into primary record. */
-		attr_ins = mi_insert_attr(&ni->mi, le->type, le_name(le),
+		attr_ins = mi_insert_attr(ni, &ni->mi, le->type, le_name(le),
 					  le->name_len, asize,
 					  le16_to_cpu(attr->name_off));
 		if (!attr_ins) {
@@ -765,7 +768,7 @@ static int ni_try_remove_attr_list(struct ntfs_inode *ni)
 		if (!mi)
 			continue;

-		attr = mi_find_attr(mi, NULL, le->type, le_name(le),
+		attr = mi_find_attr(ni, mi, NULL, le->type, le_name(le),
 				    le->name_len, &le->id);
 		if (!attr)
 			continue;
@@ -830,7 +833,7 @@ int ni_create_attr_list(struct ntfs_inode *ni)
 	free_b = 0;
 	attr = NULL;

-	for (; (attr = mi_enum_attr(&ni->mi, attr)); le = Add2Ptr(le, sz)) {
+	for (; (attr = mi_enum_attr(ni, &ni->mi, attr)); le = Add2Ptr(le, sz)) {
 		sz = le_size(attr->name_len);
 		le->type = attr->type;
 		le->size = cpu_to_le16(sz);
@@ -885,7 +888,7 @@ int ni_create_attr_list(struct ntfs_inode *ni)
 		u32 asize = le32_to_cpu(b->size);
 		u16 name_off = le16_to_cpu(b->name_off);

-		attr = mi_insert_attr(mi, b->type, Add2Ptr(b, name_off),
+		attr = mi_insert_attr(ni, mi, b->type, Add2Ptr(b, name_off),
 				      b->name_len, asize, name_off);
 		if (!attr)
 			goto out1;
@@ -908,7 +911,7 @@ int ni_create_attr_list(struct ntfs_inode *ni)
 			goto out1;
 	}

-	attr = mi_insert_attr(&ni->mi, ATTR_LIST, NULL, 0,
+	attr = mi_insert_attr(ni, &ni->mi, ATTR_LIST, NULL, 0,
 			      lsize + SIZEOF_RESIDENT, SIZEOF_RESIDENT);
 	if (!attr)
 		goto out1;
@@ -995,13 +998,13 @@ static int ni_ins_attr_ext(struct ntfs_inode *ni, struct ATTR_LIST_ENTRY *le,
 		mi = rb_entry(node, struct mft_inode, node);

 		if (is_mft_data &&
-		    (mi_enum_attr(mi, NULL) ||
+		    (mi_enum_attr(ni, mi, NULL) ||
 		     vbo <= ((u64)mi->rno << sbi->record_bits))) {
 			/* We can't accept this record 'cause MFT's bootstrapping. */
 			continue;
 		}
 		if (is_mft &&
-		    mi_find_attr(mi, NULL, ATTR_DATA, NULL, 0, NULL)) {
+		    mi_find_attr(ni, mi, NULL, ATTR_DATA, NULL, 0, NULL)) {
 			/*
 			 * This child record already has a ATTR_DATA.
 			 * So it can't accept any other records.
@@ -1010,7 +1013,7 @@ static int ni_ins_attr_ext(struct ntfs_inode *ni, struct ATTR_LIST_ENTRY *le,
 		}

 		if ((type != ATTR_NAME || name_len) &&
-		    mi_find_attr(mi, NULL, type, name, name_len, NULL)) {
+		    mi_find_attr(ni, mi, NULL, type, name, name_len, NULL)) {
 			/* Only indexed attributes can share same record. */
 			continue;
 		}
@@ -1159,7 +1162,7 @@ static int ni_insert_attr(struct ntfs_inode *ni, enum ATTR_TYPE type,
 	/* Estimate the result of moving all possible attributes away. */
 	attr = NULL;

-	while ((attr = mi_enum_attr(&ni->mi, attr))) {
+	while ((attr = mi_enum_attr(ni, &ni->mi, attr))) {
 		if (attr->type == ATTR_STD)
 			continue;
 		if (attr->type == ATTR_LIST)
@@ -1177,7 +1180,7 @@ static int ni_insert_attr(struct ntfs_inode *ni, enum ATTR_TYPE type,
 	attr = NULL;

 	for (;;) {
-		attr = mi_enum_attr(&ni->mi, attr);
+		attr = mi_enum_attr(ni, &ni->mi, attr);
 		if (!attr) {
 			/* We should never be here 'cause we have already check this case. */
 			err = -EINVAL;
@@ -1261,7 +1264,7 @@ static int ni_expand_mft_list(struct ntfs_inode *ni)
 	for (node = rb_first(&ni->mi_tree); node; node = rb_next(node)) {
 		mi = rb_entry(node, struct mft_inode, node);

-		attr = mi_enum_attr(mi, NULL);
+		attr = mi_enum_attr(ni, mi, NULL);

 		if (!attr) {
 			mft_min = mi->rno;
@@ -1282,7 +1285,7 @@ static int ni_expand_mft_list(struct ntfs_inode *ni)
 		ni_remove_mi(ni, mi_new);
 	}

-	attr = mi_find_attr(&ni->mi, NULL, ATTR_DATA, NULL, 0, NULL);
+	attr = mi_find_attr(ni, &ni->mi, NULL, ATTR_DATA, NULL, 0, NULL);
 	if (!attr) {
 		err = -EINVAL;
 		goto out;
@@ -1399,7 +1402,7 @@ int ni_expand_list(struct ntfs_inode *ni)
 			continue;

 		/* Find attribute in primary record. */
-		attr = rec_find_attr_le(&ni->mi, le);
+		attr = rec_find_attr_le(ni, &ni->mi, le);
 		if (!attr) {
 			err = -EINVAL;
 			goto out;
@@ -3436,7 +3439,7 @@ int ni_write_inode(struct inode *inode, int sync, const char *hint)
 		if (!mi->dirty)
 			continue;

-		is_empty = !mi_enum_attr(mi, NULL);
+		is_empty = !mi_enum_attr(ni, mi, NULL);

 		if (is_empty)
 			clear_rec_inuse(mi->mrec);
diff --git a/fs/ntfs3/ntfs_fs.h b/fs/ntfs3/ntfs_fs.h
index 482958c01644..d29fdefb477b 100644
--- a/fs/ntfs3/ntfs_fs.h
+++ b/fs/ntfs3/ntfs_fs.h
@@ -733,23 +733,24 @@ int mi_get(struct ntfs_sb_info *sbi, CLST rno, struct mft_inode **mi);
 void mi_put(struct mft_inode *mi);
 int mi_init(struct mft_inode *mi, struct ntfs_sb_info *sbi, CLST rno);
 int mi_read(struct mft_inode *mi, bool is_mft);
-struct ATTRIB *mi_enum_attr(struct mft_inode *mi, struct ATTRIB *attr);
-// TODO: id?
-struct ATTRIB *mi_find_attr(struct mft_inode *mi, struct ATTRIB *attr,
-			    enum ATTR_TYPE type, const __le16 *name,
-			    size_t name_len, const __le16 *id);
-static inline struct ATTRIB *rec_find_attr_le(struct mft_inode *rec,
+struct ATTRIB *mi_enum_attr(struct ntfs_inode *ni, struct mft_inode *mi,
+			    struct ATTRIB *attr);
+struct ATTRIB *mi_find_attr(struct ntfs_inode *ni, struct mft_inode *mi,
+			    struct ATTRIB *attr, enum ATTR_TYPE type,
+			    const __le16 *name, size_t name_len, const __le16 *id);
+static inline struct ATTRIB *rec_find_attr_le(struct ntfs_inode *ni,
+					      struct mft_inode *rec,
 					      struct ATTR_LIST_ENTRY *le)
 {
-	return mi_find_attr(rec, NULL, le->type, le_name(le), le->name_len,
+	return mi_find_attr(ni, rec, NULL, le->type, le_name(le), le->name_len,
 			    &le->id);
 }
 int mi_write(struct mft_inode *mi, int wait);
 int mi_format_new(struct mft_inode *mi, struct ntfs_sb_info *sbi, CLST rno,
 		  __le16 flags, bool is_mft);
-struct ATTRIB *mi_insert_attr(struct mft_inode *mi, enum ATTR_TYPE type,
-			      const __le16 *name, u8 name_len, u32 asize,
-			      u16 name_off);
+struct ATTRIB *mi_insert_attr(struct ntfs_inode *ni, struct mft_inode *mi,
+			      enum ATTR_TYPE type, const __le16 *name,
+			      u8 name_len, u32 asize, u16 name_off);

 bool mi_remove_attr(struct ntfs_inode *ni, struct mft_inode *mi,
 		    struct ATTRIB *attr);
diff --git a/fs/ntfs3/record.c b/fs/ntfs3/record.c
index d8d43f6a96fd..1393e93fef13 100644
--- a/fs/ntfs3/record.c
+++ b/fs/ntfs3/record.c
@@ -31,7 +31,7 @@ static inline int compare_attr(const struct ATTRIB *left, enum ATTR_TYPE type,
  *
  * Return: Unused attribute id that is less than mrec->next_attr_id.
  */
-static __le16 mi_new_attt_id(struct mft_inode *mi)
+static __le16 mi_new_attt_id(struct ntfs_inode *ni, struct mft_inode *mi)
 {
 	u16 free_id, max_id, t16;
 	struct MFT_REC *rec = mi->mrec;
@@ -52,7 +52,7 @@ static __le16 mi_new_attt_id(struct mft_inode *mi)
 	attr = NULL;

 	for (;;) {
-		attr = mi_enum_attr(mi, attr);
+		attr = mi_enum_attr(ni, mi, attr);
 		if (!attr) {
 			rec->next_attr_id = cpu_to_le16(max_id + 1);
 			mi->dirty = true;
@@ -189,7 +189,8 @@ int mi_read(struct mft_inode *mi, bool is_mft)
 	return err;
 }

-struct ATTRIB *mi_enum_attr(struct mft_inode *mi, struct ATTRIB *attr)
+struct ATTRIB *mi_enum_attr(struct ntfs_inode *ni, struct mft_inode *mi,
+			    struct ATTRIB *attr)
 {
 	const struct MFT_REC *rec = mi->mrec;
 	u32 used = le32_to_cpu(rec->used);
@@ -203,11 +204,11 @@ struct ATTRIB *mi_enum_attr(struct mft_inode *mi, struct ATTRIB *attr)
 		off = le16_to_cpu(rec->attr_off);

 		if (used > total)
-			return NULL;
+			goto out;

 		if (off >= used || off < MFTRECORD_FIXUP_OFFSET_1 ||
 		    !IS_ALIGNED(off, 8)) {
-			return NULL;
+			goto out;
 		}

 		/* Skip non-resident records. */
@@ -237,7 +238,7 @@ struct ATTRIB *mi_enum_attr(struct mft_inode *mi, struct ATTRIB *attr)
 	 */
 	if (off + 8 > used) {
 		static_assert(ALIGN(sizeof(enum ATTR_TYPE), 8) == 8);
-		return NULL;
+		goto out;
 	}

 	if (attr->type == ATTR_END) {
@@ -248,41 +249,41 @@ struct ATTRIB *mi_enum_attr(struct mft_inode *mi, struct ATTRIB *attr)
 	/* 0x100 is last known attribute for now. */
 	t32 = le32_to_cpu(attr->type);
 	if (!t32 || (t32 & 0xf) || (t32 > 0x100))
-		return NULL;
+		goto out;

 	/* attributes in record must be ordered by type */
 	if (t32 < prev_type)
-		return NULL;
+		goto out;

 	asize = le32_to_cpu(attr->size);

 	if (!IS_ALIGNED(asize, 8))
-		return NULL;
+		goto out;

 	/* Check overflow and boundary. */
 	if (off + asize < off || off + asize > used)
-		return NULL;
+		goto out;

 	/* Can we use the field attr->non_res. */
 	if (off + 9 > used)
-		return NULL;
+		goto out;

 	/* Check size of attribute. */
 	if (!attr->non_res) {
 		/* Check resident fields. */
 		if (asize < SIZEOF_RESIDENT)
-			return NULL;
+			goto out;

 		t16 = le16_to_cpu(attr->res.data_off);
 		if (t16 > asize)
-			return NULL;
+			goto out;

 		if (le32_to_cpu(attr->res.data_size) > asize - t16)
-			return NULL;
+			goto out;

 		if (attr->name_len &&
 		    le16_to_cpu(attr->name_off) + sizeof(short) * attr->name_len > t16) {
-			return NULL;
+			goto out;
 		}

 		return attr;
@@ -290,71 +291,75 @@ struct ATTRIB *mi_enum_attr(struct mft_inode *mi, struct ATTRIB *attr)

 	/* Check nonresident fields. */
 	if (attr->non_res != 1)
-		return NULL;
+		goto out;

 	/* Can we use memory including attr->nres.valid_size? */
 	if (asize < SIZEOF_NONRESIDENT)
-		return NULL;
+		goto out;

 	t16 = le16_to_cpu(attr->nres.run_off);
 	if (t16 > asize)
-		return NULL;
+		goto out;

 	t32 = sizeof(short) * attr->name_len;
 	if (t32 && le16_to_cpu(attr->name_off) + t32 > t16)
-		return NULL;
+		goto out;

 	/* Check start/end vcn. */
 	if (le64_to_cpu(attr->nres.svcn) > le64_to_cpu(attr->nres.evcn) + 1)
-		return NULL;
+		goto out;

 	data_size = le64_to_cpu(attr->nres.data_size);
 	if (le64_to_cpu(attr->nres.valid_size) > data_size)
-		return NULL;
+		goto out;

 	alloc_size = le64_to_cpu(attr->nres.alloc_size);
 	if (data_size > alloc_size)
-		return NULL;
+		goto out;

 	t32 = mi->sbi->cluster_mask;
 	if (alloc_size & t32)
-		return NULL;
+		goto out;

 	if (!attr->nres.svcn && is_attr_ext(attr)) {
 		/* First segment of sparse/compressed attribute */
 		/* Can we use memory including attr->nres.total_size? */
 		if (asize < SIZEOF_NONRESIDENT_EX)
-			return NULL;
+			goto out;

 		tot_size = le64_to_cpu(attr->nres.total_size);
 		if (tot_size & t32)
-			return NULL;
+			goto out;

 		if (tot_size > alloc_size)
-			return NULL;
+			goto out;
 	} else {
 		if (attr->nres.c_unit)
-			return NULL;
+			goto out;

 		if (alloc_size > mi->sbi->volume.size)
-			return NULL;
+			goto out;
 	}

 	return attr;
+
+out:
+	_ntfs_bad_inode(&ni->vfs_inode);
+	return NULL;
 }

 /*
  * mi_find_attr - Find the attribute by type and name and id.
  */
-struct ATTRIB *mi_find_attr(struct mft_inode *mi, struct ATTRIB *attr,
-			    enum ATTR_TYPE type, const __le16 *name,
-			    size_t name_len, const __le16 *id)
+struct ATTRIB *mi_find_attr(struct ntfs_inode *ni, struct mft_inode *mi,
+			    struct ATTRIB *attr, enum ATTR_TYPE type,
+			    const __le16 *name, size_t name_len, const __le16 *id)
 {
 	u32 type_in = le32_to_cpu(type);
 	u32 atype;

 next_attr:
-	attr = mi_enum_attr(mi, attr);
+	attr = mi_enum_attr(ni, mi, attr);
 	if (!attr)
 		return NULL;

@@ -460,9 +465,9 @@ int mi_format_new(struct mft_inode *mi, struct ntfs_sb_info *sbi, CLST rno,
  *
  * Return: Not full constructed attribute or NULL if not possible to create.
  */
-struct ATTRIB *mi_insert_attr(struct mft_inode *mi, enum ATTR_TYPE type,
-			      const __le16 *name, u8 name_len, u32 asize,
-			      u16 name_off)
+struct ATTRIB *mi_insert_attr(struct ntfs_inode *ni, struct mft_inode *mi,
+			      enum ATTR_TYPE type, const __le16 *name,
+			      u8 name_len, u32 asize, u16 name_off)
 {
 	size_t tail;
 	struct ATTRIB *attr;
@@ -482,7 +487,7 @@ struct ATTRIB *mi_insert_attr(struct mft_inode *mi, enum ATTR_TYPE type,
 	 * at which we should insert it.
 	 */
 	attr = NULL;
-	while ((attr = mi_enum_attr(mi, attr))) {
+	while ((attr = mi_enum_attr(ni, mi, attr))) {
 		diff = compare_attr(attr, type, name, name_len, upcase);

 		if (diff < 0)
@@ -500,7 +505,7 @@ struct ATTRIB *mi_insert_attr(struct mft_inode *mi, enum ATTR_TYPE type,
 		tail = used - PtrOffset(rec, attr);
 	}

-	id = mi_new_attt_id(mi);
+	id = mi_new_attt_id(ni, mi);

 	memmove(Add2Ptr(attr, asize), attr, tail);
 	memset(attr, 0, asize);
--
2.50.1


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

* [PATCH 6.1.y 3/4] fs/ntfs3: fix KMSAN uninit-value in ni_create_attr_list
  2026-10-01 11:45 [PATCH 6.1.y 0/4] fs/ntfs3: backport slab-out-of-bounds fix (CVE-2026-90048) Nikita Nagornyi
  2026-10-01 11:45 ` [PATCH 6.1.y 1/4] fs/ntfs3: Add more checks in mi_enum_attr (part 2) Nikita Nagornyi
  2026-10-01 11:45 ` [PATCH 6.1.y 2/4] fs/ntfs3: Mark inode as bad as soon as error detected in mi_enum_attr() Nikita Nagornyi
@ 2026-10-01 11:45 ` Nikita Nagornyi
  2026-10-01 11:45 ` [PATCH 6.1.y 4/4] fs/ntfs3: fix slab-out-of-bounds write in ni_create_attr_list() Nikita Nagornyi
  3 siblings, 0 replies; 7+ messages in thread
From: Nikita Nagornyi @ 2026-10-01 11:45 UTC (permalink / raw)
  To: stable, Konstantin Komarov, ntfs3, linux-kernel
  Cc: Nirbhay Sharma, syzbot+83c9dd5c0dcf6184fdbf

From: Nirbhay Sharma <nirbhay.lkd@gmail.com>

[ Upstream commit 5f33da04e6ceee849e76e6592cc283c72fef7af9 ]

The call to kmalloc() to allocate the attribute list buffer is given a
size of al_aligned(rs). This size can be larger than the data
subsequently copied into the buffer, leaving trailing bytes uninitialized.

This can trigger a KMSAN "uninit-value" warning if that memory is
later accessed.

Fix this by using kzalloc() instead, which ensures the entire
allocated buffer is zero-initialized, preventing the warning.

Reported-by: syzbot+83c9dd5c0dcf6184fdbf@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=83c9dd5c0dcf6184fdbf
Signed-off-by: Nirbhay Sharma <nirbhay.lkd@gmail.com>
Signed-off-by: Konstantin Komarov <almaz.alexandrovich@paragon-software.com>

[ Backport to 6.1.y: 6.1.y's ni_create_attr_list() uses a `goto out` cleanup
  convention rather than upstream's direct returns, so only kmalloc -> kzalloc
  was applied; the surrounding return/error path was left as is. ]

Signed-off-by: Nikita Nagornyi <nagornyi@amazon.de>
(cherry picked from commit 5f33da04e6ceee849e76e6592cc283c72fef7af9)
---
 fs/ntfs3/frecord.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fs/ntfs3/frecord.c b/fs/ntfs3/frecord.c
index 17559fca5984..c9d0947651c4 100644
--- a/fs/ntfs3/frecord.c
+++ b/fs/ntfs3/frecord.c
@@ -819,7 +819,7 @@ int ni_create_attr_list(struct ntfs_inode *ni)
 	 * Skip estimating exact memory requirement.
 	 * Looks like one record_size is always enough.
 	 */
-	le = kmalloc(al_aligned(rs), GFP_NOFS);
+	le = kzalloc(al_aligned(rs), GFP_NOFS);
 	if (!le) {
 		err = -ENOMEM;
 		goto out;
--
2.50.1


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

* [PATCH 6.1.y 4/4] fs/ntfs3: fix slab-out-of-bounds write in ni_create_attr_list()
  2026-10-01 11:45 [PATCH 6.1.y 0/4] fs/ntfs3: backport slab-out-of-bounds fix (CVE-2026-90048) Nikita Nagornyi
                   ` (2 preceding siblings ...)
  2026-10-01 11:45 ` [PATCH 6.1.y 3/4] fs/ntfs3: fix KMSAN uninit-value in ni_create_attr_list Nikita Nagornyi
@ 2026-10-01 11:45 ` Nikita Nagornyi
  2026-10-02 14:19   ` Sasha Levin
  3 siblings, 1 reply; 7+ messages in thread
From: Nikita Nagornyi @ 2026-10-01 11:45 UTC (permalink / raw)
  To: stable, Konstantin Komarov, ntfs3, linux-kernel
  Cc: HE WEI(ギカク)

From: HE WEI(ギカク) <skyexpoc@gmail.com>

[ Upstream commit 7c4841e2a62794a3bab7c1ff0540580f387e377f ]

ni_create_attr_list() allocates a fixed buffer of al_aligned(record_size)
(== record_size) bytes and then walks every attribute of the primary MFT
record, writing one ATTR_LIST_ENTRY per attribute and advancing the cursor
by le_size(name_len), with no check against the end of the buffer; the
total size is only computed after the loop.

A minimum-size resident attribute occupies SIZEOF_RESIDENT (0x18 = 24)
bytes on disk, but an unnamed attribute expands to le_size(0) (0x20 = 32)
bytes in the list.  Because the number of attributes in a record is not
bounded (mi_enum_attr() accepts arbitrarily many equal-type, nameless
minimum-size attributes), a crafted record packed with such attributes
produces a list larger than record_size and overflows the heap buffer.

This is reachable from a crafted, loop-mounted NTFS image: opening the file
and adding an attribute (e.g. via setxattr) drives ntfs_set_ea() ->
ni_insert_resident() -> ni_insert_attr() -> ni_ins_attr_ext() ->
ni_create_attr_list().

  BUG: KASAN: slab-out-of-bounds in ni_create_attr_list+0xc48/0x1058
  Write of size 4 at addr ffff000008984c00 by task setfattr/345
   ni_create_attr_list+0xc48/0x1058
   ni_ins_attr_ext+0x510/0x7c0
   ni_insert_attr+0x3f8/0x70c
   ni_insert_resident+0xc8/0x3b0
   ntfs_set_ea+0x66c/0xd28
   ntfs_setxattr+0x4d8/0x5b0
   __arm64_sys_setxattr+0xa4/0x124
  Allocated by task 345:
   ni_create_attr_list+0x188/0x1058
  The buggy address belongs to the cache kmalloc-1k of size 1024
  (the write lands at object+1024).

Size the buffer from the actual attributes instead of assuming a single
record_size is always enough.

Fixes: 4342306f0f0d ("fs/ntfs3: Add file operations and implementation")
Reported-by: HE WEI(ギカク) <skyexpoc@gmail.com>
Signed-off-by: HE WEI(ギカク) <skyexpoc@gmail.com>
Signed-off-by: Konstantin Komarov <almaz.alexandrovich@paragon-software.com>

[ Backport to 6.1.y: 6.1.y's ni_create_attr_list() uses a `goto out` cleanup
  convention rather than upstream's early returns, so the new `return` error
  paths were rewritten as `err = ...; goto out`. ]

Signed-off-by: Nikita Nagornyi <nagornyi@amazon.de>
(cherry picked from commit 7c4841e2a62794a3bab7c1ff0540580f387e377f)
---
 fs/ntfs3/frecord.c | 22 ++++++++++++++++++----
 1 file changed, 18 insertions(+), 4 deletions(-)

diff --git a/fs/ntfs3/frecord.c b/fs/ntfs3/frecord.c
index c9d0947651c4..5c544d24f9c3 100644
--- a/fs/ntfs3/frecord.c
+++ b/fs/ntfs3/frecord.c
@@ -816,10 +816,25 @@ int ni_create_attr_list(struct ntfs_inode *ni)
 	rs = sbi->record_size;

 	/*
-	 * Skip estimating exact memory requirement.
-	 * Looks like one record_size is always enough.
+	 * Compute the exact size of the attribute list.  Each attribute in the
+	 * record yields one ATTR_LIST_ENTRY of le_size(name_len) bytes.  The
+	 * minimum on-disk attribute is SIZEOF_RESIDENT (0x18) bytes, but an
+	 * unnamed one expands to le_size(0) (0x20) here, so a record crafted
+	 * with many such attributes needs more than a single record_size; the
+	 * previous fixed kzalloc(record_size) could therefore be overflowed by
+	 * an attacker-controlled record.
 	 */
-	le = kzalloc(al_aligned(rs), GFP_NOFS);
+	lsize = 0;
+	attr = NULL;
+	while ((attr = mi_enum_attr(ni, &ni->mi, attr)))
+		lsize += le_size(attr->name_len);
+
+	if (!lsize) {
+		err = -EINVAL;
+		goto out;
+	}
+
+	le = kzalloc(al_aligned(lsize), GFP_NOFS);
 	if (!le) {
 		err = -ENOMEM;
 		goto out;
@@ -831,7 +846,6 @@ int ni_create_attr_list(struct ntfs_inode *ni)
 	attr = NULL;
 	nb = 0;
 	free_b = 0;
-	attr = NULL;

 	for (; (attr = mi_enum_attr(ni, &ni->mi, attr)); le = Add2Ptr(le, sz)) {
 		sz = le_size(attr->name_len);
--
2.50.1


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

* Re: [PATCH 6.1.y 4/4] fs/ntfs3: fix slab-out-of-bounds write in ni_create_attr_list()
  2026-10-01 11:45 ` [PATCH 6.1.y 4/4] fs/ntfs3: fix slab-out-of-bounds write in ni_create_attr_list() Nikita Nagornyi
@ 2026-10-02 14:19   ` Sasha Levin
  0 siblings, 0 replies; 7+ messages in thread
From: Sasha Levin @ 2026-10-02 14:19 UTC (permalink / raw)
  To: stable, Konstantin Komarov, ntfs3, linux-kernel
  Cc: Sasha Levin, HE WEI(ギカク),
	Nikita Nagornyi

> [ Backport to 6.1.y: 6.1.y's ni_create_attr_list() uses a `goto out` cleanup
>   convention rather than upstream's early returns, so the new `return` error
>   paths were rewritten as `err = ...; goto out`. ]

[...]

> +	if (!lsize) {
> +		err = -EINVAL;
> +		goto out;
> +	}

On 6.1 the out: label in ni_create_attr_list() is "return 0;", because
14f527d44de6 ("fs/ntfs3: Correct checking while generating attr_list")
isn't in 6.1. So this error path reports success. Please return -EINVAL
directly here, as upstream does.

Could you send a v2 with that fixed? 1/4 also needs its upstream tag,
see my reply there.

-- 
Thanks,
Sasha

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

* Re: [PATCH 6.1.y 1/4] fs/ntfs3: Add more checks in mi_enum_attr (part 2)
  2026-10-01 11:45 ` [PATCH 6.1.y 1/4] fs/ntfs3: Add more checks in mi_enum_attr (part 2) Nikita Nagornyi
@ 2026-10-02 14:19   ` Sasha Levin
  0 siblings, 0 replies; 7+ messages in thread
From: Sasha Levin @ 2026-10-02 14:19 UTC (permalink / raw)
  To: stable, Konstantin Komarov, ntfs3, linux-kernel
  Cc: Sasha Levin, lei lu, Nikita Nagornyi

> Signed-off-by: Nikita Nagornyi <nagornyi@amazon.de>
> (cherry picked from commit a8f659ce14f8f734894227ba2c5ca78c8451501e)

This one is missing the upstream tag. In v2, please start the commit
message with:

  [ Upstream commit a8f659ce14f8f734894227ba2c5ca78c8451501e ]

-- 
Thanks,
Sasha

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

end of thread, other threads:[~2026-10-02 14:20 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-01 11:45 [PATCH 6.1.y 0/4] fs/ntfs3: backport slab-out-of-bounds fix (CVE-2026-90048) Nikita Nagornyi
2026-10-01 11:45 ` [PATCH 6.1.y 1/4] fs/ntfs3: Add more checks in mi_enum_attr (part 2) Nikita Nagornyi
2026-10-02 14:19   ` Sasha Levin
2026-10-01 11:45 ` [PATCH 6.1.y 2/4] fs/ntfs3: Mark inode as bad as soon as error detected in mi_enum_attr() Nikita Nagornyi
2026-10-01 11:45 ` [PATCH 6.1.y 3/4] fs/ntfs3: fix KMSAN uninit-value in ni_create_attr_list Nikita Nagornyi
2026-10-01 11:45 ` [PATCH 6.1.y 4/4] fs/ntfs3: fix slab-out-of-bounds write in ni_create_attr_list() Nikita Nagornyi
2026-10-02 14:19   ` Sasha Levin

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®