[PATCH v2 3/3] ntfs: do not convert a named $DATA stream when making room in a record

From: Wahid Khan

Date: Fri Nov 06 2026 - 07:52:05 EST


When an attribute has to grow and the mft record is full,
ntfs_resident_attr_resize() walks the other resident attributes and
converts them to non-resident ones. For $DATA it always opens the
unnamed data inode of the base inode, even if the attribute found is a
named stream:

tvi = ntfs_iget(sb, base_ni->mft_no);
ntfs_attr_make_non_resident(NTFS_I(tvi), value_len);

value_len is the length of the stream, so it does not match the
attribute that ntfs_attr_make_non_resident() looks up for the unnamed
inode, and the WARN_ON() on entry fires:

WARNING: fs/ntfs/attrib.c:2115 at ntfs_attr_make_non_resident
ntfs_resident_attr_resize
ntfs_attr_expand_locked
ntfs_expand_for_write
ntfs_file_write_iter
vfs_write

With debug output the values were attr_size 13 (unnamed $DATA) and
data_size 550 (named stream) for the same mft record.

It does not need a damaged volume. A file with an alternate data stream
whose record has no room left is enough. The fuzzer found it through a
single changed byte in a file name of an image from mkntfs, which made
a write grow a resident attribute in such a record.

Opening the stream by name instead would convert it with only the mft
record lock held. Its folio would be locked under that lock, while the
read path takes the folio lock first and the mft record lock inside
ntfs_read_iomap_begin_resident(). Streams are not converted from this
loop, which only tries to make room, so skip them.

Fixes: 495e90fa3348 ("ntfs: update attrib operations")
Cc: stable@xxxxxxxxxxxxxxx # v7.1+
Signed-off-by: Wahid Khan <wahidzk0091@xxxxxxxxx>
---
fs/ntfs/attrib.c | 6 ++++++
1 file changed, 6 insertions(+)

diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c
index ce7c1b524..800e53df5 100644
--- a/fs/ntfs/attrib.c
+++ b/fs/ntfs/attrib.c
@@ -4932,6 +4932,12 @@ static int ntfs_resident_attr_resize(struct ntfs_inode *attr_ni, const s64 newsi
continue;
if (a->type == AT_DATA && !value_len)
continue;
+ /*
+ * Named streams have inodes of their own and are not converted
+ * from here.
+ */
+ if (a->type == AT_DATA && a->name_length)
+ continue;

if (a->type == AT_DATA)
tvi = ntfs_iget(sb, base_ni->mft_no);
--
2.53.0