mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Namjae Jeon <linkinjeon@kernel.org>
To: hyc.lee@gmail.com
Cc: ntfs@lists.linux.dev, linux-fsdevel@vger.kernel.org,
	linux-kernel@vger.kernel.org, sebastian.n.feld@gmail.com,
	cedric.blancher@gmail.com, Lionelcons1972@gmail.com,
	Namjae Jeon <linkinjeon@kernel.org>
Subject: [PATCH v2 1/4] ntfs: add named stream ioctls support
Date: Wed,  7 Oct 2026 07:40:21 +0900	[thread overview]
Message-ID: <20261006224024.14535-2-linkinjeon@kernel.org> (raw)
In-Reply-To: <20261006224024.14535-1-linkinjeon@kernel.org>

NTFS supports multiple named $DATA attributes, commonly known as alternate
data streams. Add NTFS-specific ioctls to list, read, write, and remove
named streams through an opened base file or directory.

The UAPI provides separate commands for each operation.

  - NTFS_IOC_STREAM_READ
  - NTFS_IOC_STREAM_WRITE
  - NTFS_IOC_STREAM_REMOVE
  - NTFS_IOC_LIST_STREAMS

Read and write requests use struct ntfs_stream to specify the stream name,
byte offset, and transfer length. Writes create the stream if it does not
already exist. A write failure after creation may leave the new stream
visible. It is not rolled back because another caller may already have
opened it. Userspace can remove it with NTFS_IOC_STREAM_REMOVE.

The list command returns variable-length entries containing each named
stream's data size, allocated size, and name. If the buffer is too small,
the command returns -ENOSPC without copying entries. bytes_returned reports
the required buffer size, and stream_count reports the number of streams.

Stream names use the mounted filesystem NLS by default. Set
NTFS_STREAM_FL_UTF16 to pass or receive raw UTF-16LE names. In NLS mode,
names that cannot be represented by the mounted character set are omitted
from the list. Use UTF-16 mode for lossless enumeration. Read and write
transfers are limited to 16 MiB per request.

Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
 .../userspace-api/ioctl/ioctl-number.rst      |   1 +
 fs/ntfs/Makefile                              |   2 +-
 fs/ntfs/attrib.c                              | 148 +--
 fs/ntfs/attrlist.c                            |   3 +-
 fs/ntfs/file.c                                | 162 +++-
 fs/ntfs/inode.c                               | 188 +++-
 fs/ntfs/inode.h                               |  19 +
 fs/ntfs/named_stream.c                        | 915 ++++++++++++++++++
 fs/ntfs/namei.c                               |  43 +-
 fs/ntfs/stream.h                              |  43 +
 include/uapi/linux/ntfs.h                     | 123 +++
 11 files changed, 1500 insertions(+), 147 deletions(-)
 create mode 100644 fs/ntfs/named_stream.c
 create mode 100644 fs/ntfs/stream.h
 create mode 100644 include/uapi/linux/ntfs.h

diff --git a/Documentation/userspace-api/ioctl/ioctl-number.rst b/Documentation/userspace-api/ioctl/ioctl-number.rst
index 2fc53093752d..94a421970b85 100644
--- a/Documentation/userspace-api/ioctl/ioctl-number.rst
+++ b/Documentation/userspace-api/ioctl/ioctl-number.rst
@@ -401,6 +401,7 @@ Code  Seq#    Include File                                             Comments
 0xE5  00-3F  linux/fuse.h
 0xEC  00-01  drivers/platform/chrome/cros_ec_dev.h                     ChromeOS EC driver
 0xEE  00-09  uapi/linux/pfrut.h                                        Platform Firmware Runtime Update and Telemetry
+0xEF  00-0F  uapi/linux/ntfs.h                                         NTFS
 0xF3  00-3F  drivers/usb/misc/sisusbvga/sisusb.h                       sisfb (in development)
                                                                        <mailto:thomas@winischhofer.net>
 0xF6  all                                                              LTTng Linux Trace Toolkit Next Generation
diff --git a/fs/ntfs/Makefile b/fs/ntfs/Makefile
index ee8987e496a8..3e8549577653 100644
--- a/fs/ntfs/Makefile
+++ b/fs/ntfs/Makefile
@@ -5,7 +5,7 @@ obj-$(CONFIG_NTFS_FS) += ntfs.o
 ntfs-y := aops.o attrib.o collate.o dir.o file.o index.o inode.o \
 	  mft.o mst.o namei.o runlist.o super.o unistr.o attrlist.o ea.o \
 	  upcase.o bitmap.o lcnalloc.o logfile.o reparse.o compress.o \
-	  iomap.o debug.o sysctl.o object_id.o bdev-io.o
+	  iomap.o debug.o sysctl.o object_id.o bdev-io.o named_stream.o
 
 ntfs-$(CONFIG_NTFS_FS_WOF_COMPRESSION) += wof.o \
 	lib/decompress_common.o lib/lzx_decompress.o lib/xpress_decompress.o
diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c
index 4df618abc4ef..3266415470a7 100644
--- a/fs/ntfs/attrib.c
+++ b/fs/ntfs/attrib.c
@@ -2352,12 +2352,13 @@ int ntfs_attr_set(struct ntfs_inode *ni, s64 ofs, s64 cnt, const u8 val)
 int ntfs_attr_set_initialized_size(struct ntfs_inode *ni, loff_t new_size)
 {
 	struct ntfs_attr_search_ctx *ctx;
+	struct ntfs_inode *base_ni = ntfs_base_inode(ni);
 	int err = 0;
 
 	if (!NInoNonResident(ni))
 		return -EINVAL;
 
-	ctx = ntfs_attr_get_search_ctx(ni, NULL);
+	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
 	if (!ctx)
 		return -ENOMEM;
 
@@ -2439,6 +2440,7 @@ int ntfs_resident_attr_record_add(struct ntfs_inode *ni, __le32 type,
 	struct mft_record *m;
 	int err, offset;
 	struct ntfs_inode *base_ni;
+	u32 ic;
 
 	if (!ni || (!name && name_len))
 		return -EINVAL;
@@ -2468,7 +2470,8 @@ int ntfs_resident_attr_record_add(struct ntfs_inode *ni, __le32 type,
 	 * attribute in @ni->mrec, not any extent inode in case if @ni is base
 	 * file record.
 	 */
-	err = ntfs_attr_find(type, name, name_len, CASE_SENSITIVE, val, size, ctx);
+	ic = type == AT_DATA && name_len ? IGNORE_CASE : CASE_SENSITIVE;
+	err = ntfs_attr_find(type, name, name_len, ic, val, size, ctx);
 	if (!err) {
 		err = -EEXIST;
 		ntfs_debug("Attribute already present.\n");
@@ -2560,6 +2563,7 @@ static int ntfs_non_resident_attr_record_add(struct ntfs_inode *ni, __le32 type,
 	struct mft_record *m;
 	struct ntfs_inode *base_ni;
 	int err, offset;
+	u32 ic;
 
 	if (!ni || dataruns_size <= 0 || (!name && name_len))
 		return -EINVAL;
@@ -2589,7 +2593,8 @@ static int ntfs_non_resident_attr_record_add(struct ntfs_inode *ni, __le32 type,
 	 * attribute in @ni->mrec, not any extent inode in case if @ni is base
 	 * file record.
 	 */
-	err = ntfs_attr_find(type, name, name_len, CASE_SENSITIVE, NULL, 0, ctx);
+	ic = type == AT_DATA && name_len ? IGNORE_CASE : CASE_SENSITIVE;
+	err = ntfs_attr_find(type, name, name_len, ic, NULL, 0, ctx);
 	if (!err) {
 		err = -EEXIST;
 		pr_err("Attribute 0x%x already present\n", type);
@@ -2663,7 +2668,7 @@ static int ntfs_non_resident_attr_record_add(struct ntfs_inode *ni, __le32 type,
 	 * update of attribute list.
 	 */
 	ntfs_attr_reinit_search_ctx(ctx);
-	err = ntfs_attr_lookup(type, name, name_len, CASE_SENSITIVE,
+	err = ntfs_attr_lookup(type, name, name_len, ic,
 				lowest_vcn, NULL, 0, ctx);
 	if (err) {
 		pr_err("%s: attribute lookup failed\n", __func__);
@@ -3132,6 +3137,7 @@ int ntfs_attr_open(struct ntfs_inode *ni, const __le32 type,
 	struct attr_record *a;
 	bool cs;
 	struct ntfs_inode *base_ni;
+	u32 ic;
 	int err;
 
 	if (!ni || !ni->vol)
@@ -3140,10 +3146,7 @@ int ntfs_attr_open(struct ntfs_inode *ni, const __le32 type,
 	ntfs_debug("Entering for inode %lld, attr 0x%x.\n",
 			ni->mft_no, type);
 
-	if (NInoAttr(ni))
-		base_ni = ni->ext.base_ntfs_ino;
-	else
-		base_ni = ni;
+	base_ni = ntfs_base_inode(ni);
 
 	if (name && name != AT_UNNAMED && name != I30) {
 		name = ntfs_ucsndup(name, name_len);
@@ -3161,7 +3164,8 @@ int ntfs_attr_open(struct ntfs_inode *ni, const __le32 type,
 		goto err_out;
 	}
 
-	err = ntfs_attr_lookup(type, name, name_len, 0, 0, NULL, 0, ctx);
+	ic = type == AT_DATA && name_len ? IGNORE_CASE : CASE_SENSITIVE;
+	err = ntfs_attr_lookup(type, name, name_len, ic, 0, NULL, 0, ctx);
 	if (err)
 		goto put_err_out;
 
@@ -3346,7 +3350,8 @@ int ntfs_attr_map_whole_runlist(struct ntfs_inode *ni)
 			not_mapped = 1;
 
 		err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
-					CASE_SENSITIVE, next_vcn, NULL, 0, ctx);
+					CASE_SENSITIVE, next_vcn,
+					NULL, 0, ctx);
 		if (err)
 			break;
 
@@ -3431,6 +3436,7 @@ int ntfs_attr_record_move_to(struct ntfs_attr_search_ctx *ctx, struct ntfs_inode
 	struct ntfs_attr_search_ctx *nctx;
 	struct attr_record *a;
 	int err;
+	u32 ic;
 	struct mft_record *ni_mrec;
 	struct super_block *sb;
 
@@ -3466,9 +3472,11 @@ int ntfs_attr_record_move_to(struct ntfs_attr_search_ctx *ctx, struct ntfs_inode
 	 * attribute in @ni->mrec, not any extent inode in case if @ni is base
 	 * file record.
 	 */
-	err = ntfs_attr_find(a->type, (__le16 *)((u8 *)a + le16_to_cpu(a->name_offset)),
-				a->name_length, CASE_SENSITIVE, NULL,
-				0, nctx);
+	ic = a->type == AT_DATA && a->name_length ?
+			IGNORE_CASE : CASE_SENSITIVE;
+	err = ntfs_attr_find(a->type,
+			(__le16 *)((u8 *)a + le16_to_cpu(a->name_offset)),
+			a->name_length, ic, NULL, 0, nctx);
 	if (!err) {
 		ntfs_debug("Attribute of such type, with same name already present in this MFT record.\n");
 		err = -EEXIST;
@@ -3800,7 +3808,8 @@ static int __ntfs_attr_update_mapping_pairs(struct ntfs_inode *ni,
 	finished_build = false;
 	start_rl = ni->runlist.rl;
 	while (!(err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
-				CASE_SENSITIVE, from_vcn, NULL, 0, ctx))) {
+				CASE_SENSITIVE, from_vcn, NULL,
+				0, ctx))) {
 		unsigned int de_cnt = 0;
 
 		a = ctx->attr;
@@ -4011,7 +4020,8 @@ static int __ntfs_attr_update_mapping_pairs(struct ntfs_inode *ni,
 		ntfs_attr_reinit_search_ctx(ctx);
 		ntfs_debug("Deallocate marked extents.\n");
 		while (!(err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
-				CASE_SENSITIVE, 0, NULL, 0, ctx))) {
+				CASE_SENSITIVE, 0, NULL, 0,
+				ctx))) {
 			if (le64_to_cpu(ctx->attr->data.non_resident.highest_vcn) !=
 					NTFS_VCN_DELETE_MARK)
 				continue;
@@ -4345,7 +4355,7 @@ static int ntfs_non_resident_attr_shrink(struct ntfs_inode *ni,
 			goto unlock_runlist;
 		}
 
-		ctx = ntfs_attr_get_search_ctx(ni, NULL);
+		ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
 		if (!ctx) {
 			ntfs_error(vol->sb, "%s: Failed to get search context", __func__);
 			err = -ENOMEM;
@@ -4404,8 +4414,8 @@ static int ntfs_non_resident_attr_shrink(struct ntfs_inode *ni,
 		return -ENOMEM;
 	}
 
-	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, CASE_SENSITIVE,
-				0, NULL, 0, ctx);
+	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
+				CASE_SENSITIVE, 0, NULL, 0, ctx);
 	if (err) {
 		if (err == -ENOENT)
 			err = -EIO;
@@ -4724,8 +4734,8 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz
 		goto rollback;
 	}
 
-	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, CASE_SENSITIVE,
-			       0, NULL, 0, ctx);
+	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
+			       CASE_SENSITIVE, 0, NULL, 0, ctx);
 	if (err) {
 		if (err == -ENOENT)
 			err = -EIO;
@@ -4837,7 +4847,7 @@ static int ntfs_resident_attr_resize(struct ntfs_inode *attr_ni, const s64 newsi
 	}
 
 	err = ntfs_attr_lookup(attr_ni->type, attr_ni->name, attr_ni->name_len,
-			0, 0, NULL, 0, ctx);
+			CASE_SENSITIVE, 0, NULL, 0, ctx);
 	if (err) {
 		ntfs_error(sb, "ntfs_attr_lookup failed");
 		goto put_err_out;
@@ -5211,6 +5221,7 @@ int ntfs_attr_map_cluster(struct ntfs_inode *ni, s64 vcn_start, s64 *lcn_start,
 		s64 *lcn_count, s64 max_clu_count, bool *balloc, bool update_mp,
 		bool skip_holes)
 {
+	struct ntfs_inode *base_ni = ntfs_base_inode(ni);
 	struct ntfs_volume *vol = ni->vol;
 	struct ntfs_attr_search_ctx *ctx;
 	struct runlist_element *rl, *rlc;
@@ -5224,10 +5235,7 @@ int ntfs_attr_map_cluster(struct ntfs_inode *ni, s64 vcn_start, s64 *lcn_start,
 	if (err)
 		return err;
 
-	if (NInoAttr(ni))
-		ctx = ntfs_attr_get_search_ctx(ni->ext.base_ntfs_ino, NULL);
-	else
-		ctx = ntfs_attr_get_search_ctx(ni, NULL);
+	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
 	if (!ctx) {
 		ntfs_error(vol->sb, "%s: Failed to get search context", __func__);
 		return -ENOMEM;
@@ -5377,7 +5385,7 @@ int ntfs_attr_map_cluster(struct ntfs_inode *ni, s64 vcn_start, s64 *lcn_start,
 	} else {
 		VFS_I(ni)->i_blocks += clu_count << (vol->cluster_size_bits - 9);
 		NInoSetRunlistDirty(ni);
-		mark_mft_record_dirty(ni);
+		mark_mft_record_dirty(base_ni);
 	}
 
 	*lcn_start = lcn;
@@ -5403,10 +5411,7 @@ int ntfs_attr_rm(struct ntfs_inode *ni)
 	struct ntfs_inode *base_ni;
 	struct super_block *sb = ni->vol->sb;
 
-	if (NInoAttr(ni))
-		base_ni = ni->ext.base_ntfs_ino;
-	else
-		base_ni = ni;
+	base_ni = ntfs_base_inode(ni);
 
 	ntfs_debug("Entering for inode 0x%llx, attr 0x%x.\n",
 			(long long) ni->mft_no, ni->type);
@@ -5418,7 +5423,7 @@ int ntfs_attr_rm(struct ntfs_inode *ni)
 		err = ntfs_attr_map_whole_runlist(ni);
 		if (err)
 			return err;
-		ctx = ntfs_attr_get_search_ctx(ni, NULL);
+		ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
 		if (!ctx) {
 			ntfs_error(sb, "%s: Failed to get search context", __func__);
 			return -ENOMEM;
@@ -5460,6 +5465,7 @@ int ntfs_attr_exist(struct ntfs_inode *ni, const __le32 type, __le16 *name,
 		u32 name_len)
 {
 	struct ntfs_attr_search_ctx *ctx;
+	u32 ic;
 	int ret;
 
 	ntfs_debug("Entering\n");
@@ -5471,7 +5477,8 @@ int ntfs_attr_exist(struct ntfs_inode *ni, const __le32 type, __le16 *name,
 		return 0;
 	}
 
-	ret = ntfs_attr_lookup(type, name, name_len, CASE_SENSITIVE,
+	ic = type == AT_DATA && name_len ? IGNORE_CASE : CASE_SENSITIVE;
+	ret = ntfs_attr_lookup(type, name, name_len, ic,
 			0, NULL, 0, ctx);
 	ntfs_attr_put_search_ctx(ctx);
 
@@ -5493,16 +5500,18 @@ int ntfs_attr_remove(struct ntfs_inode *ni, const __le32 type, __le16 *name,
 	attr_vi = ntfs_attr_iget(VFS_I(ni), type, name, name_len);
 	if (IS_ERR(attr_vi)) {
 		err = PTR_ERR(attr_vi);
-		ntfs_error(ni->vol->sb, "Failed to open attribute 0x%02x of inode 0x%llx",
-				type, (unsigned long long)ni->mft_no);
+		ntfs_error(ni->vol->sb,
+			"Failed to open attribute 0x%02x of inode 0x%llx",
+			type, (unsigned long long)ni->mft_no);
 		return err;
 	}
 	attr_ni = NTFS_I(attr_vi);
 
 	err = ntfs_attr_rm(attr_ni);
 	if (err)
-		ntfs_error(ni->vol->sb, "Failed to remove attribute 0x%02x of inode 0x%llx",
-				type, (unsigned long long)ni->mft_no);
+		ntfs_error(ni->vol->sb,
+			"Failed to remove attribute 0x%02x of inode 0x%llx",
+			type, (unsigned long long)ni->mft_no);
 	iput(attr_vi);
 	return err;
 }
@@ -5578,13 +5587,14 @@ void *ntfs_attr_readall(struct ntfs_inode *ni, const __le32 type,
 
 int ntfs_non_resident_attr_insert_range(struct ntfs_inode *ni, s64 start_vcn, s64 len)
 {
+	struct ntfs_inode *base_ni = ntfs_base_inode(ni);
 	struct ntfs_volume *vol = ni->vol;
 	struct runlist_element *hole_rl, *rl;
 	struct ntfs_attr_search_ctx *ctx;
 	int ret;
 	size_t new_rl_count;
 
-	if (NInoAttr(ni) || ni->type != AT_DATA)
+	if (ni->type != AT_DATA)
 		return -EOPNOTSUPP;
 	if (start_vcn > ntfs_bytes_to_cluster(vol, ni->allocated_size))
 		return -EINVAL;
@@ -5633,14 +5643,14 @@ int ntfs_non_resident_attr_insert_range(struct ntfs_inode *ni, s64 start_vcn, s6
 	if (ret)
 		return ret;
 
-	ctx = ntfs_attr_get_search_ctx(ni, NULL);
+	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
 	if (!ctx) {
 		ret = -ENOMEM;
 		return ret;
 	}
 
-	ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, CASE_SENSITIVE,
-			       0, NULL, 0, ctx);
+	ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
+			       CASE_SENSITIVE, 0, NULL, 0, ctx);
 	if (ret) {
 		ntfs_attr_put_search_ctx(ctx);
 		return ret;
@@ -5657,6 +5667,7 @@ int ntfs_non_resident_attr_insert_range(struct ntfs_inode *ni, s64 start_vcn, s6
 
 int ntfs_non_resident_attr_collapse_range(struct ntfs_inode *ni, s64 start_vcn, s64 len)
 {
+	struct ntfs_inode *base_ni = ntfs_base_inode(ni);
 	struct ntfs_volume *vol = ni->vol;
 	struct runlist_element *punch_rl, *rl;
 	struct ntfs_attr_search_ctx *ctx = NULL;
@@ -5665,7 +5676,7 @@ int ntfs_non_resident_attr_collapse_range(struct ntfs_inode *ni, s64 start_vcn,
 	int ret;
 	size_t new_rl_cnt;
 
-	if (NInoAttr(ni) || ni->type != AT_DATA)
+	if (ni->type != AT_DATA)
 		return -EOPNOTSUPP;
 
 	end_vcn = ntfs_bytes_to_cluster(vol, ni->allocated_size);
@@ -5721,14 +5732,14 @@ int ntfs_non_resident_attr_collapse_range(struct ntfs_inode *ni, s64 start_vcn,
 	}
 	up_write(&ni->runlist.lock);
 
-	ctx = ntfs_attr_get_search_ctx(ni, NULL);
+	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
 	if (!ctx) {
 		ret = -ENOMEM;
 		goto out_rl;
 	}
 
-	ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, CASE_SENSITIVE,
-			       0, NULL, 0, ctx);
+	ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
+			       CASE_SENSITIVE, 0, NULL, 0, ctx);
 	if (ret)
 		goto out_ctx;
 
@@ -5746,12 +5757,13 @@ int ntfs_non_resident_attr_collapse_range(struct ntfs_inode *ni, s64 start_vcn,
 		ntfs_attr_put_search_ctx(ctx);
 out_rl:
 	kvfree(punch_rl);
-	mark_mft_record_dirty(ni);
+	mark_mft_record_dirty(base_ni);
 	return ret;
 }
 
 int ntfs_non_resident_attr_punch_hole(struct ntfs_inode *ni, s64 start_vcn, s64 len)
 {
+	struct ntfs_inode *base_ni = ntfs_base_inode(ni);
 	struct ntfs_volume *vol = ni->vol;
 	struct runlist_element *punch_rl, *rl;
 	s64 end_vcn;
@@ -5759,7 +5771,7 @@ int ntfs_non_resident_attr_punch_hole(struct ntfs_inode *ni, s64 start_vcn, s64
 	int ret;
 	size_t new_rl_count;
 
-	if (NInoAttr(ni) || ni->type != AT_DATA)
+	if (ni->type != AT_DATA)
 		return -EOPNOTSUPP;
 
 	end_vcn = ntfs_bytes_to_cluster(vol, ni->allocated_size);
@@ -5803,12 +5815,13 @@ int ntfs_non_resident_attr_punch_hole(struct ntfs_inode *ni, s64 start_vcn, s64
 		ntfs_error(vol->sb, "Freeing of clusters failed");
 
 	kvfree(punch_rl);
-	mark_mft_record_dirty(ni);
+	mark_mft_record_dirty(base_ni);
 	return ret;
 }
 
 int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bool keep_size)
 {
+	struct ntfs_inode *base_ni = ntfs_base_inode(ni);
 	struct ntfs_volume *vol = ni->vol;
 	struct mft_record *mrec;
 	struct ntfs_attr_search_ctx *ctx;
@@ -5820,7 +5833,7 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo
 	struct runlist_element *rl;
 	bool balloc;
 
-	if (NInoAttr(ni) || ni->type != AT_DATA)
+	if (ni->type != AT_DATA)
 		return -EINVAL;
 
 	if (NInoNonResident(ni) && !NInoFullyMapped(ni)) {
@@ -5831,20 +5844,21 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo
 			return err;
 	}
 
-	mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
-	mrec = map_mft_record(ni);
+	mutex_lock_nested(&base_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
+	mrec = map_mft_record(base_ni);
 	if (IS_ERR(mrec)) {
-		mutex_unlock(&ni->mrec_lock);
+		mutex_unlock(&base_ni->mrec_lock);
 		return PTR_ERR(mrec);
 	}
 
-	ctx = ntfs_attr_get_search_ctx(ni, mrec);
+	ctx = ntfs_attr_get_search_ctx(base_ni, mrec);
 	if (!ctx) {
 		err = -ENOMEM;
 		goto out_unmap;
 	}
 
-	err = ntfs_attr_lookup(AT_DATA, AT_UNNAMED, 0, 0, 0, NULL, 0, ctx);
+	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
+			CASE_SENSITIVE, 0, NULL, 0, ctx);
 	if (err) {
 		err = -EIO;
 		goto out_unmap;
@@ -5857,25 +5871,28 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo
 			goto out_unmap;
 		if (keep_size) {
 			ntfs_attr_reinit_search_ctx(ctx);
-			err = ntfs_attr_lookup(AT_DATA, AT_UNNAMED, 0, 0, 0, NULL, 0, ctx);
+			err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
+					CASE_SENSITIVE, 0, NULL, 0,
+					ctx);
 			if (err) {
 				err = -EIO;
 				goto out_unmap;
 			}
 			ni->data_size = old_data_size;
+			i_size_write(VFS_I(ni), old_data_size);
 			if (NInoNonResident(ni))
 				ctx->attr->data.non_resident.data_size =
 					cpu_to_le64(old_data_size);
 			else
 				ctx->attr->data.resident.value_length =
 					cpu_to_le32((u32)old_data_size);
-			mark_mft_record_dirty(ni);
+			mark_mft_record_dirty(base_ni);
 		}
 	}
 
 	ntfs_attr_put_search_ctx(ctx);
-	unmap_mft_record(ni);
-	mutex_unlock(&ni->mrec_lock);
+	unmap_mft_record(base_ni);
+	mutex_unlock(&base_ni->mrec_lock);
 
 	if (!NInoNonResident(ni))
 		goto out;
@@ -5917,12 +5934,13 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo
 			}
 
 			while (try_alloc_cnt > 0) {
-				mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
+				mutex_lock_nested(&base_ni->mrec_lock,
+						NTFS_INODE_MUTEX_NORMAL);
 				down_write(&ni->runlist.lock);
 				err = ntfs_attr_map_cluster(ni, vcn, &lcn, &alloc_cnt,
 							    try_alloc_cnt, &balloc, false, false);
 				up_write(&ni->runlist.lock);
-				mutex_unlock(&ni->mrec_lock);
+				mutex_unlock(&base_ni->mrec_lock);
 				if (err)
 					goto out;
 
@@ -5950,12 +5968,12 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo
 	/* allocate clusters outside of initialized_size */
 	try_alloc_cnt = vcn_end - vcn;
 	while (try_alloc_cnt > 0) {
-		mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
+		mutex_lock_nested(&base_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
 		down_write(&ni->runlist.lock);
 		err = ntfs_attr_map_cluster(ni, vcn, &lcn, &alloc_cnt,
 					    try_alloc_cnt, &balloc, false, false);
 		up_write(&ni->runlist.lock);
-		mutex_unlock(&ni->mrec_lock);
+		mutex_unlock(&base_ni->mrec_lock);
 		if (err || fatal_signal_pending(current))
 			goto signal_out;
 
@@ -5965,7 +5983,7 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo
 	}
 
 	if (NInoRunlistDirty(ni)) {
-		mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
+		mutex_lock_nested(&base_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
 		down_write(&ni->runlist.lock);
 		err = ntfs_attr_update_mapping_pairs_locked(ni, 0, ni);
 		if (err)
@@ -5973,14 +5991,14 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo
 		else
 			NInoClearRunlistDirty(ni);
 		up_write(&ni->runlist.lock);
-		mutex_unlock(&ni->mrec_lock);
+		mutex_unlock(&base_ni->mrec_lock);
 	}
 	return err;
 out_unmap:
 	if (ctx)
 		ntfs_attr_put_search_ctx(ctx);
-	unmap_mft_record(ni);
-	mutex_unlock(&ni->mrec_lock);
+	unmap_mft_record(base_ni);
+	mutex_unlock(&base_ni->mrec_lock);
 out:
 	return err >= 0 ? 0 : err;
 signal_out:
diff --git a/fs/ntfs/attrlist.c b/fs/ntfs/attrlist.c
index 6edd5d2d9bf2..8291c4c95b37 100644
--- a/fs/ntfs/attrlist.c
+++ b/fs/ntfs/attrlist.c
@@ -374,7 +374,8 @@ int ntfs_attrlist_entry_add(struct ntfs_inode *ni, struct attr_record *attr)
 
 	err = ntfs_attr_lookup(attr->type, (attr->name_length) ? (__le16 *)
 			((u8 *)attr + le16_to_cpu(attr->name_offset)) :
-			AT_UNNAMED, attr->name_length, CASE_SENSITIVE,
+			AT_UNNAMED, attr->name_length,
+			CASE_SENSITIVE,
 			le64_to_cpu(lowest_vcn),
 			(attr->non_resident) ? NULL : ((u8 *)attr +
 			le16_to_cpu(attr->data.resident.value_offset)), (attr->non_resident) ?
diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
index 1e2500a52148..7818d88b2133 100644
--- a/fs/ntfs/file.c
+++ b/fs/ntfs/file.c
@@ -15,6 +15,11 @@
 #include <linux/posix_acl_xattr.h>
 #include <linux/compat.h>
 #include <linux/falloc.h>
+#include <linux/file.h>
+#include <linux/filelock.h>
+#include <linux/overflow.h>
+#include <linux/security.h>
+#include <uapi/linux/ntfs.h>
 
 #include "lcnalloc.h"
 #include "ntfs.h"
@@ -23,8 +28,8 @@
 #include "iomap.h"
 #include "bitmap.h"
 #include "volume.h"
-
-#include <linux/filelock.h>
+#include "mft.h"
+#include "stream.h"
 
 /*
  * ntfs_file_open - called when an inode is about to be opened
@@ -44,7 +49,7 @@
  *
  * After the check passes, just call generic_file_open() to do its work.
  */
-static int ntfs_file_open(struct inode *vi, struct file *filp)
+int ntfs_file_open(struct inode *vi, struct file *filp)
 {
 	struct ntfs_inode *ni = NTFS_I(vi);
 
@@ -78,6 +83,7 @@ static int ntfs_file_open(struct inode *vi, struct file *filp)
 static int ntfs_trim_prealloc(struct inode *vi)
 {
 	struct ntfs_inode *ni = NTFS_I(vi);
+	struct ntfs_inode *mrec_ni = ntfs_base_inode(ni);
 	struct ntfs_volume *vol = ni->vol;
 	struct runlist_element *rl;
 	s64 aligned_data_size;
@@ -86,7 +92,7 @@ static int ntfs_trim_prealloc(struct inode *vi)
 	int err = 0;
 
 	inode_lock(vi);
-	mutex_lock(&ni->mrec_lock);
+	mutex_lock(&mrec_ni->mrec_lock);
 	down_write(&ni->runlist.lock);
 
 	aligned_data_size = round_up(ni->data_size, vol->cluster_size);
@@ -121,13 +127,13 @@ static int ntfs_trim_prealloc(struct inode *vi)
 
 out_unlock:
 	up_write(&ni->runlist.lock);
-	mutex_unlock(&ni->mrec_lock);
+	mutex_unlock(&mrec_ni->mrec_lock);
 	inode_unlock(vi);
 
 	return err;
 }
 
-static int ntfs_file_release(struct inode *vi, struct file *filp)
+int ntfs_file_release(struct inode *vi, struct file *filp)
 {
 	if (!NInoCompressed(NTFS_I(vi)) &&
 	    !NInoWofCompressed(NTFS_I(vi)))
@@ -156,7 +162,7 @@ static int ntfs_file_release(struct inode *vi, struct file *filp)
  * Also, if @datasync is true, we do not wait on the inode to be written out
  * but we always wait on the page cache pages to be written out.
  */
-static int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end,
+int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end,
 			   int datasync)
 {
 	struct inode *vi = filp->f_mapping->host;
@@ -165,6 +171,7 @@ static int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end,
 	int err, ret = 0;
 	struct inode *parent_vi, *ia_vi;
 	struct ntfs_attr_search_ctx *ctx;
+	bool non_resident, stream;
 
 	ntfs_debug("Entering for inode 0x%llx.", ni->mft_no);
 
@@ -175,10 +182,25 @@ static int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end,
 	if (err)
 		return err;
 
-	if (!datasync || !NInoNonResident(NTFS_I(vi)))
+	stream = ntfs_inode_is_named_stream(ni);
+	non_resident = NInoNonResident(ni);
+	if (stream) {
+		ni = ni->ext.base_ntfs_ino;
+		vi = VFS_I(ni);
+	}
+
+	if (!datasync || !non_resident)
 		ret = __ntfs_write_inode(vi, 1);
 	write_inode_now(vi, !datasync);
 
+	/*
+	 * file_write_and_wait_range() already flushed this stream mapping.
+	 * Do not walk sibling attribute mappings while holding the base MFT
+	 * lock; ordinary file fsync retains its existing behavior below.
+	 */
+	if (stream)
+		goto sync_volume;
+
 	ctx = ntfs_attr_get_search_ctx(ni, NULL);
 	if (!ctx)
 		return -ENOMEM;
@@ -227,6 +249,7 @@ static int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end,
 	mutex_unlock(&ni->mrec_lock);
 	ntfs_attr_put_search_ctx(ctx);
 
+sync_volume:
 	write_inode_now(vol->mftbmp_ino, 1);
 	down_write(&vol->lcnbmp_lock);
 	write_inode_now(vol->lcnbmp_ino, 1);
@@ -268,12 +291,18 @@ static void ntfs_pagecache_extend(struct inode *vi, loff_t from, loff_t to)
 				    PAGE_SIZE, 0);
 }
 
-static int ntfs_setattr_size(struct inode *vi, struct iattr *attr)
+int ntfs_setattr_size(struct inode *vi, struct iattr *attr)
 {
 	struct ntfs_inode *ni = NTFS_I(vi);
+	struct ntfs_inode *base_ni = ntfs_base_inode(ni);
+	struct inode *time_vi = vi;
+	bool stream = ntfs_inode_is_named_stream(ni);
 	int err;
 	loff_t old_size = vi->i_size;
 
+	if (stream && NVolShutdown(ni->vol))
+		return -EIO;
+
 	if (NInoCompressed(ni) || NInoEncrypted(ni) || NInoWofCompressed(ni)) {
 		ntfs_warning(
 			vi->i_sb,
@@ -293,7 +322,22 @@ static int ntfs_setattr_size(struct inode *vi, struct iattr *attr)
 	 * readers cannot observe the size change until the attribute
 	 * updates below have completed.
 	 */
-	filemap_invalidate_lock(vi->i_mapping);
+	if (stream) {
+		filemap_invalidate_lock(vi->i_mapping);
+		err = filemap_write_and_wait(vi->i_mapping);
+		if (err)
+			goto out_unlock_mapping;
+
+		mutex_lock(&base_ni->mrec_lock);
+		err = ntfs_stream_inode_validate(vi);
+		mutex_unlock(&base_ni->mrec_lock);
+		if (err)
+			goto out_unlock_mapping;
+		time_vi = VFS_I(base_ni);
+	} else {
+		filemap_invalidate_lock(vi->i_mapping);
+	}
+
 	if (attr->ia_size > old_size) {
 		truncate_pagecache(vi, old_size);
 		i_size_write(vi, attr->ia_size);
@@ -302,9 +346,28 @@ static int ntfs_setattr_size(struct inode *vi, struct iattr *attr)
 		truncate_setsize(vi, attr->ia_size);
 	}
 
-	err = ntfs_truncate_vfs(vi, attr->ia_size, old_size);
-	if (err)
+	if (stream) {
+		mutex_lock(&base_ni->mrec_lock);
+		err = ntfs_stream_inode_validate(vi);
+		if (!err)
+			err = __ntfs_attr_truncate_vfs(ni, attr->ia_size,
+					old_size);
+		mutex_unlock(&base_ni->mrec_lock);
+	} else {
+		err = ntfs_truncate_vfs(vi, attr->ia_size, old_size);
+	}
+	if (err) {
 		i_size_write(vi, old_size);
+		goto out_unlock_mapping;
+	}
+
+	if (stream) {
+		inode_set_mtime_to_ts(time_vi,
+				inode_set_ctime_current(time_vi));
+		mark_inode_dirty(time_vi);
+	}
+
+out_unlock_mapping:
 	filemap_invalidate_unlock(vi->i_mapping);
 
 	return err;
@@ -437,7 +500,7 @@ int ntfs_getattr(struct mnt_idmap *idmap, const struct path *path,
 	return 0;
 }
 
-static loff_t ntfs_file_llseek(struct file *file, loff_t offset, int whence)
+loff_t ntfs_file_llseek(struct file *file, loff_t offset, int whence)
 {
 	struct inode *inode = file->f_mapping->host;
 
@@ -466,7 +529,7 @@ static loff_t ntfs_file_llseek(struct file *file, loff_t offset, int whence)
 	return vfs_setpos(file, offset, inode->i_sb->s_maxbytes);
 }
 
-static ssize_t ntfs_file_read_iter(struct kiocb *iocb, struct iov_iter *to)
+ssize_t ntfs_file_read_iter(struct kiocb *iocb, struct iov_iter *to)
 {
 	struct inode *vi = file_inode(iocb->ki_filp);
 	struct super_block *sb = vi->i_sb;
@@ -513,7 +576,11 @@ static int ntfs_file_write_dio_end_io(struct kiocb *iocb, ssize_t size,
 	if (size) {
 		if (i_size_read(inode) < iocb->ki_pos + size) {
 			i_size_write(inode, iocb->ki_pos + size);
-			mark_inode_dirty(inode);
+			if (ntfs_inode_is_named_stream(NTFS_I(inode)))
+				mark_inode_dirty(VFS_I(
+					NTFS_I(inode)->ext.base_ntfs_ino));
+			else
+				mark_inode_dirty(inode);
 		}
 	}
 
@@ -583,6 +650,7 @@ static ssize_t ntfs_dio_write_iter(struct kiocb *iocb, struct iov_iter *from)
 static int ntfs_expand_for_write(struct ntfs_inode *ni, loff_t end)
 {
 	struct ntfs_volume *vol = ni->vol;
+	struct ntfs_inode *mrec_ni = ntfs_base_inode(ni);
 	loff_t prealloc_size = 0;
 	int err;
 
@@ -598,14 +666,14 @@ static int ntfs_expand_for_write(struct ntfs_inode *ni, loff_t end)
 		prealloc_size = ni->allocated_size + vol->preallocated_size;
 	}
 
-	mutex_lock(&ni->mrec_lock);
+	mutex_lock(&mrec_ni->mrec_lock);
 	err = ntfs_attr_expand(ni, end, prealloc_size);
-	mutex_unlock(&ni->mrec_lock);
+	mutex_unlock(&mrec_ni->mrec_lock);
 
 	return err;
 }
 
-static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
+ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
 {
 	struct file *file = iocb->ki_filp;
 	struct inode *vi = file->f_mapping->host;
@@ -692,13 +760,21 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
 out:
 	if (ret < 0 && ret != -EIOCBQUEUED) {
 		if (ni->initialized_size != old_init_size) {
-			mutex_lock(&ni->mrec_lock);
+			struct ntfs_inode *mrec_ni =
+				ntfs_base_inode(ni);
+
+			mutex_lock(&mrec_ni->mrec_lock);
 			ntfs_attr_set_initialized_size(ni, old_init_size);
-			mutex_unlock(&ni->mrec_lock);
+			mutex_unlock(&mrec_ni->mrec_lock);
 		}
 		if (ni->data_size != old_data_size) {
+			struct ntfs_inode *mrec_ni =
+				ntfs_base_inode(ni);
+
 			truncate_setsize(vi, old_data_size);
+			mutex_lock(&mrec_ni->mrec_lock);
 			ntfs_attr_truncate(ni, old_data_size);
+			mutex_unlock(&mrec_ni->mrec_lock);
 		}
 	}
 out_lock:
@@ -727,7 +803,6 @@ static vm_fault_t ntfs_filemap_page_mkwrite(struct vm_fault *vmf)
 	filemap_invalidate_lock_shared(mapping);
 	ret = iomap_page_mkwrite(vmf, &ntfs_page_mkwrite_iomap_ops, NULL);
 	filemap_invalidate_unlock_shared(mapping);
-
 	sb_end_pagefault(inode->i_sb);
 	return ret;
 }
@@ -738,7 +813,7 @@ static const struct vm_operations_struct ntfs_file_vm_ops = {
 	.page_mkwrite	= ntfs_filemap_page_mkwrite,
 };
 
-static int ntfs_file_mmap_prepare(struct vm_area_desc *desc)
+int ntfs_file_mmap_prepare(struct vm_area_desc *desc)
 {
 	struct file *file = desc->file;
 	struct inode *inode = file_inode(file);
@@ -771,7 +846,7 @@ static int ntfs_file_mmap_prepare(struct vm_area_desc *desc)
 	return 0;
 }
 
-static int ntfs_fiemap(struct inode *inode, struct fiemap_extent_info *fieinfo,
+int ntfs_fiemap(struct inode *inode, struct fiemap_extent_info *fieinfo,
 		u64 start, u64 len)
 {
 	if (NInoWofCompressed(NTFS_I(inode)))
@@ -808,7 +883,7 @@ static const char *ntfs_get_link(struct dentry *dentry, struct inode *inode,
 	return ni->target;
 }
 
-static ssize_t ntfs_file_splice_read(struct file *in, loff_t *ppos,
+ssize_t ntfs_file_splice_read(struct file *in, loff_t *ppos,
 		struct pipe_inode_info *pipe, size_t len, unsigned int flags)
 {
 	if (NVolShutdown(NTFS_SB(in->f_mapping->host->i_sb)))
@@ -924,6 +999,14 @@ long ntfs_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
 		return ntfs_ioctl_set_volume_label(filp, arg);
 	case FITRIM:
 		return ntfs_ioctl_fitrim(NTFS_SB(file_inode(filp)->i_sb), arg);
+	case NTFS_IOC_STREAM_READ:
+		return ntfs_ioctl_stream_read(filp, arg);
+	case NTFS_IOC_STREAM_WRITE:
+		return ntfs_ioctl_stream_write(filp, arg);
+	case NTFS_IOC_STREAM_REMOVE:
+		return ntfs_ioctl_stream_remove(filp, arg);
+	case NTFS_IOC_LIST_STREAMS:
+		return ntfs_ioctl_list_streams(filp, arg);
 	default:
 		return -ENOTTY;
 	}
@@ -980,6 +1063,7 @@ static int ntfs_allocate_range(struct ntfs_inode *ni, int mode, loff_t offset,
 static int ntfs_punch_hole(struct ntfs_inode *ni, int mode, loff_t offset,
 		loff_t len)
 {
+	struct ntfs_inode *mrec_ni = ntfs_base_inode(ni);
 	struct ntfs_volume *vol = ni->vol;
 	struct inode *vi = VFS_I(ni);
 	loff_t end_offset;
@@ -1044,16 +1128,17 @@ static int ntfs_punch_hole(struct ntfs_inode *ni, int mode, loff_t offset,
 		end_vcn--;
 	}
 
-	mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
+	mutex_lock_nested(&mrec_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
 	err = ntfs_non_resident_attr_punch_hole(ni, start_vcn,
 			end_vcn - start_vcn);
-	mutex_unlock(&ni->mrec_lock);
+	mutex_unlock(&mrec_ni->mrec_lock);
 out:
 	return err;
 }
 
 static int ntfs_collapse_range(struct ntfs_inode *ni, loff_t offset, loff_t len)
 {
+	struct ntfs_inode *mrec_ni = ntfs_base_inode(ni);
 	struct ntfs_volume *vol = ni->vol;
 	struct inode *vi = VFS_I(ni);
 	loff_t old_size, new_size;
@@ -1087,10 +1172,10 @@ static int ntfs_collapse_range(struct ntfs_inode *ni, loff_t offset, loff_t len)
 
 	truncate_pagecache(vi, offset_down);
 
-	mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
+	mutex_lock_nested(&mrec_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
 	err = ntfs_non_resident_attr_collapse_range(ni, start_vcn,
 			end_vcn - start_vcn);
-	mutex_unlock(&ni->mrec_lock);
+	mutex_unlock(&mrec_ni->mrec_lock);
 
 	if (new_size != old_size)
 		i_size_write(vi, ni->data_size);
@@ -1100,6 +1185,7 @@ static int ntfs_collapse_range(struct ntfs_inode *ni, loff_t offset, loff_t len)
 
 static int ntfs_insert_range(struct ntfs_inode *ni, loff_t offset, loff_t len)
 {
+	struct ntfs_inode *mrec_ni = ntfs_base_inode(ni);
 	struct ntfs_volume *vol = ni->vol;
 	struct inode *vi = VFS_I(ni);
 	loff_t offset_down = round_down(offset,
@@ -1143,10 +1229,10 @@ static int ntfs_insert_range(struct ntfs_inode *ni, loff_t offset, loff_t len)
 
 	truncate_pagecache(vi, offset_down);
 
-	mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
+	mutex_lock_nested(&mrec_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
 	err = ntfs_non_resident_attr_insert_range(ni, start_vcn,
 			end_vcn - start_vcn);
-	mutex_unlock(&ni->mrec_lock);
+	mutex_unlock(&mrec_ni->mrec_lock);
 
 	if (new_size != old_size)
 		i_size_write(vi, ni->data_size);
@@ -1159,11 +1245,13 @@ static int ntfs_insert_range(struct ntfs_inode *ni, loff_t offset, loff_t len)
 		 FALLOC_FL_INSERT_RANGE | FALLOC_FL_PUNCH_HOLE |	\
 		 FALLOC_FL_COLLAPSE_RANGE)
 
-static long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t len)
+long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t len)
 {
 	struct inode *vi = file_inode(file);
 	struct ntfs_inode *ni = NTFS_I(vi);
+	struct ntfs_inode *base_ni = ntfs_base_inode(ni);
 	struct ntfs_volume *vol = ni->vol;
+	bool stream = ntfs_inode_is_named_stream(ni);
 	int err = 0;
 	loff_t old_size, new_size;
 
@@ -1233,9 +1321,15 @@ static long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t le
 	filemap_invalidate_unlock(vi->i_mapping);
 
 	if (!err) {
-		NInoSetFileNameDirty(ni);
-		inode_set_mtime_to_ts(vi, inode_set_ctime_current(vi));
-		mark_inode_dirty(vi);
+		if (stream) {
+			inode_set_mtime_to_ts(VFS_I(base_ni),
+					inode_set_ctime_current(VFS_I(base_ni)));
+			mark_inode_dirty(VFS_I(base_ni));
+		} else {
+			NInoSetFileNameDirty(ni);
+			inode_set_mtime_to_ts(vi, inode_set_ctime_current(vi));
+			mark_inode_dirty(vi);
+		}
 	}
 
 	inode_unlock(vi);
diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index eca0724c4358..ed2cb7e9e5bb 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -19,6 +19,7 @@
 #include "attrib.h"
 #include "iomap.h"
 #include "object_id.h"
+#include "stream.h"
 
 /*
  * ntfs_test_inode - compare two (possibly fake) inodes for equality
@@ -55,9 +56,18 @@ int ntfs_test_inode(struct inode *vi, void *data)
 			return 0;
 		if (ni->name_len != na->name_len)
 			return 0;
-		if (na->name_len && memcmp(ni->name, na->name,
-				na->name_len * sizeof(__le16)))
-			return 0;
+		if (na->name_len) {
+			if (ntfs_inode_is_named_stream(ni)) {
+				if (!ntfs_names_are_equal(ni->name, ni->name_len,
+						na->name, na->name_len,
+						IGNORE_CASE, ni->vol->upcase,
+						ni->vol->upcase_len))
+					return 0;
+			} else if (memcmp(ni->name, na->name,
+					na->name_len * sizeof(__le16))) {
+				return 0;
+			}
+		}
 		if (!ni->ext.base_ntfs_ino)
 			return 0;
 	}
@@ -66,6 +76,33 @@ int ntfs_test_inode(struct inode *vi, void *data)
 	return 1;
 }
 
+/*
+ * ntfs_test_inode_rcu() - Test an inode during RCU hash lookup
+ * @vi: inode being tested
+ * @data: NTFS attribute key to match
+ *
+ * Reject inodes being initialized or destroyed, then run the normal NTFS
+ * inode match while holding @vi's inode lock.
+ *
+ * Return: 1 if the inode matches, or 0 otherwise.
+ */
+int ntfs_test_inode_rcu(struct inode *vi, void *data)
+{
+	unsigned long state;
+	int ret;
+
+	spin_lock(&vi->i_lock);
+	state = inode_state_read_once(vi);
+	if (state & (I_FREEING | I_WILL_FREE | I_NEW)) {
+		ret = 0;
+		goto out;
+	}
+	ret = ntfs_test_inode(vi, data);
+out:
+	spin_unlock(&vi->i_lock);
+	return ret;
+}
+
 /*
  * ntfs_init_locked_inode - initialize an inode
  * @vi:		vfs inode to initialize
@@ -467,6 +504,7 @@ void __ntfs_init_inode(struct super_block *sb, struct ntfs_inode *ni)
 	ni->seq_no = 0;
 	atomic_set(&ni->count, 1);
 	ni->vol = NTFS_SB(sb);
+	atomic_set(&ni->stream_open_count, 0);
 	ntfs_init_runlist(&ni->runlist);
 	mutex_init(&ni->mrec_lock);
 	if (ni->type == AT_ATTRIBUTE_LIST) {
@@ -1319,6 +1357,7 @@ static int ntfs_read_locked_attr_inode(struct inode *base_vi, struct inode *vi)
 	struct attr_record *a;
 	struct ntfs_attr_search_ctx *ctx;
 	int err = 0;
+	u32 ic;
 
 	ntfs_debug("Entering for i_ino 0x%llx.", ni->mft_no);
 
@@ -1333,8 +1372,12 @@ static int ntfs_read_locked_attr_inode(struct inode *base_vi, struct inode *vi)
 	inode_set_atime_to_ts(vi, inode_get_atime(base_vi));
 	vi->i_generation = ni->seq_no = base_ni->seq_no;
 
-	/* Set inode type to zero but preserve permissions. */
-	vi->i_mode	= base_vi->i_mode & ~S_IFMT;
+	/* Named data streams are regular files throughout initialization. */
+	vi->i_mode = base_vi->i_mode & ~S_IFMT;
+	if (ni->type == AT_DATA && ni->name_len) {
+		vi->i_mode |= S_IFREG;
+		vi->i_flags = base_vi->i_flags;
+	}
 
 	m = map_mft_record(base_ni);
 	if (IS_ERR(m)) {
@@ -1347,11 +1390,29 @@ static int ntfs_read_locked_attr_inode(struct inode *base_vi, struct inode *vi)
 		goto unm_err_out;
 	}
 	/* Find the attribute. */
+	ic = ntfs_inode_is_named_stream(ni) ? IGNORE_CASE : CASE_SENSITIVE;
 	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
-			CASE_SENSITIVE, 0, NULL, 0, ctx);
+			ic, 0, NULL, 0, ctx);
 	if (unlikely(err))
 		goto unm_err_out;
 	a = ctx->attr;
+	if (ntfs_inode_is_named_stream(ni)) {
+		__le16 *disk_name = (__le16 *)((u8 *)a +
+				le16_to_cpu(a->name_offset));
+
+		/* Subsequent extent and mutation lookups must select this name. */
+		if (ni->name == I30) {
+			ni->name = kmalloc_array(ni->name_len + 1,
+						sizeof(__le16), GFP_NOFS);
+			if (!ni->name) {
+				err = -ENOMEM;
+				goto unm_err_out;
+			}
+		}
+		memcpy(ni->name, disk_name,
+			ni->name_len * sizeof(__le16));
+		ni->name[ni->name_len] = 0;
+	}
 	if (a->flags & (ATTR_COMPRESSION_MASK | ATTR_IS_SPARSE)) {
 		if (a->flags & ATTR_COMPRESSION_MASK) {
 			NInoSetCompressed(ni);
@@ -2484,6 +2545,7 @@ int ntfs_extend_initialized_size(struct inode *vi, const loff_t offset,
 				 const loff_t new_size)
 {
 	struct ntfs_inode *ni = NTFS_I(vi);
+	struct ntfs_inode *mrec_ni = ntfs_base_inode(ni);
 	loff_t old_init_size;
 	unsigned long flags;
 	int err;
@@ -2511,9 +2573,9 @@ int ntfs_extend_initialized_size(struct inode *vi, const loff_t offset,
 	}
 
 
-	mutex_lock(&ni->mrec_lock);
+	mutex_lock(&mrec_ni->mrec_lock);
 	err = ntfs_attr_set_initialized_size(ni, new_size);
-	mutex_unlock(&ni->mrec_lock);
+	mutex_unlock(&mrec_ni->mrec_lock);
 	if (err)
 		truncate_setsize(vi, old_init_size);
 	return err;
@@ -3602,19 +3664,30 @@ s64 ntfs_inode_attr_pread(struct inode *vi, s64 pos, s64 count, u8 *buf)
 	struct address_space *mapping = vi->i_mapping;
 	struct folio *folio;
 	struct ntfs_inode *ni = NTFS_I(vi);
+	struct ntfs_inode *base_ni = ni->ext.base_ntfs_ino;
+	struct ntfs_inode *mrec_ni = ntfs_inode_is_named_stream(ni) ?
+			base_ni : ni;
 	s64 isize;
 	u32 attr_len, total = 0, offset;
 	pgoff_t index;
 	int err = 0;
+	bool claimed = false;
 
 	WARN_ON(!NInoAttr(ni));
 	if (!count)
 		return 0;
 
-	mutex_lock(&ni->mrec_lock);
+	mutex_lock(&mrec_ni->mrec_lock);
+	if (ntfs_inode_is_named_stream(ni)) {
+		err = ntfs_stream_inode_validate(vi);
+		if (err) {
+			mutex_unlock(&mrec_ni->mrec_lock);
+			return err;
+		}
+	}
 	isize = i_size_read(vi);
 	if (pos > isize) {
-		mutex_unlock(&ni->mrec_lock);
+		mutex_unlock(&mrec_ni->mrec_lock);
 		return -EINVAL;
 	}
 	if (pos + count > isize)
@@ -3624,30 +3697,35 @@ s64 ntfs_inode_attr_pread(struct inode *vi, s64 pos, s64 count, u8 *buf)
 		struct ntfs_attr_search_ctx *ctx;
 		u8 *attr;
 
-		ctx = ntfs_attr_get_search_ctx(ni->ext.base_ntfs_ino, NULL);
+		ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
 		if (!ctx) {
 			ntfs_error(vi->i_sb, "Failed to get attr search ctx");
 			err = -ENOMEM;
-			mutex_unlock(&ni->mrec_lock);
+			mutex_unlock(&mrec_ni->mrec_lock);
 			goto out;
 		}
 
-		err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, CASE_SENSITIVE,
+		err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
+				       CASE_SENSITIVE,
 				       0, NULL, 0, ctx);
 		if (err) {
 			ntfs_error(vi->i_sb, "Failed to look up attr %#x", ni->type);
 			ntfs_attr_put_search_ctx(ctx);
-			mutex_unlock(&ni->mrec_lock);
+			mutex_unlock(&mrec_ni->mrec_lock);
 			goto out;
 		}
 
 		attr = (u8 *)ctx->attr + le16_to_cpu(ctx->attr->data.resident.value_offset);
 		memcpy(buf, (u8 *)attr + pos, count);
 		ntfs_attr_put_search_ctx(ctx);
-		mutex_unlock(&ni->mrec_lock);
+		mutex_unlock(&mrec_ni->mrec_lock);
 		return count;
 	}
-	mutex_unlock(&ni->mrec_lock);
+	if (ntfs_inode_is_named_stream(ni)) {
+		atomic_inc(&ni->stream_open_count);
+		claimed = true;
+	}
+	mutex_unlock(&mrec_ni->mrec_lock);
 
 	index = pos >> PAGE_SHIFT;
 	do {
@@ -3671,6 +3749,8 @@ s64 ntfs_inode_attr_pread(struct inode *vi, s64 pos, s64 count, u8 *buf)
 		index++;
 	} while (count);
 out:
+	if (claimed)
+		ntfs_stream_put(vi);
 	return err ? (s64)err : total;
 }
 
@@ -3698,8 +3778,8 @@ static inline int ntfs_enlarge_attribute(struct inode *vi, s64 pos, s64 count,
 		}
 
 		ntfs_attr_reinit_search_ctx(ctx);
-		ret = ntfs_attr_lookup(ni->type,
-				       ni->name, ni->name_len, CASE_SENSITIVE,
+		ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
+				       CASE_SENSITIVE,
 				       0, NULL, 0, ctx);
 		if (ret) {
 			ntfs_error(sb, "Failed to look up attr %#x", ni->type);
@@ -3713,6 +3793,13 @@ static inline int ntfs_enlarge_attribute(struct inode *vi, s64 pos, s64 count,
 		return 0;
 	}
 
+	/* Named streams initialize gaps after dropping the MFT record lock. */
+	if (ntfs_inode_is_named_stream(ni)) {
+		if (i_size_read(vi) < ni->data_size)
+			i_size_write(vi, ni->data_size);
+		return 0;
+	}
+
 	if (pos + count > ni->initialized_size) {
 		ctx->attr->data.non_resident.initialized_size = cpu_to_le64(pos + count);
 		mark_mft_record_dirty(ctx->ntfs_ino);
@@ -3767,7 +3854,6 @@ static s64 __ntfs_inode_resident_attr_pwrite(struct inode *vi,
 
 static s64 __ntfs_inode_non_resident_attr_pwrite(struct inode *vi,
 						 s64 pos, s64 count, u8 *buf,
-						 struct ntfs_attr_search_ctx *ctx,
 						 bool sync)
 {
 	struct ntfs_inode *ni = NTFS_I(vi);
@@ -3906,37 +3992,79 @@ static s64 __ntfs_inode_non_resident_attr_pwrite(struct inode *vi,
 s64 ntfs_inode_attr_pwrite(struct inode *vi, s64 pos, s64 count, u8 *buf, bool sync)
 {
 	struct ntfs_inode *ni = NTFS_I(vi);
+	struct ntfs_inode *base_ni = ni->ext.base_ntfs_ino;
+	struct ntfs_inode *mrec_ni = ntfs_inode_is_named_stream(ni) ?
+			base_ni : ni;
 	struct ntfs_attr_search_ctx *ctx;
 	s64 ret;
 
 	WARN_ON(!NInoAttr(ni));
 
-	ctx = ntfs_attr_get_search_ctx(ni->ext.base_ntfs_ino, NULL);
+	if (ntfs_inode_is_named_stream(ni)) {
+		if (NInoEncrypted(ni))
+			return -EACCES;
+		if (NInoCompressed(ni))
+			return -EOPNOTSUPP;
+	}
+
+	mutex_lock(&mrec_ni->mrec_lock);
+	if (ntfs_inode_is_named_stream(ni)) {
+		ret = ntfs_stream_inode_validate(vi);
+		if (ret)
+			goto out_unlock;
+	}
+
+	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
 	if (!ctx) {
 		ntfs_error(vi->i_sb, "Failed to get attr search ctx");
-		return -ENOMEM;
+		ret = -ENOMEM;
+		goto out_unlock;
 	}
 
-	ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, CASE_SENSITIVE,
+	ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
+			       CASE_SENSITIVE,
 			       0, NULL, 0, ctx);
 	if (ret) {
 		ntfs_attr_put_search_ctx(ctx);
 		ntfs_error(vi->i_sb, "Failed to look up attr %#x", ni->type);
-		return ret;
+		goto out_unlock;
 	}
 
-	mutex_lock(&ni->mrec_lock);
 	ret = ntfs_enlarge_attribute(vi, pos, count, ctx);
-	mutex_unlock(&ni->mrec_lock);
 	if (ret)
-		goto out;
+		goto out_ctx;
 
-	if (NInoNonResident(ni))
-		ret = __ntfs_inode_non_resident_attr_pwrite(vi, pos, count, buf, ctx, sync);
-	else
+	if (!NInoNonResident(ni)) {
 		ret = __ntfs_inode_resident_attr_pwrite(vi, pos, count, buf, ctx);
-out:
+		goto out_ctx;
+	}
+
+	if (ntfs_inode_is_named_stream(ni))
+		atomic_inc(&ni->stream_open_count);
+	ntfs_attr_put_search_ctx(ctx);
+	mutex_unlock(&mrec_ni->mrec_lock);
+	/* Attribute writes bypass iomap's delayed-allocation preparation. */
+	if (ntfs_inode_is_named_stream(ni)) {
+		/* A failed write must not expose any newly initialized data. */
+		ret = ntfs_extend_initialized_size(vi, pos + count, pos + count);
+		if (!ret)
+			ret = ntfs_attr_fallocate(ni, pos, count, true);
+	}
+	if (!ret)
+		ret = __ntfs_inode_non_resident_attr_pwrite(vi, pos, count,
+							    buf, sync);
+	if (ntfs_inode_is_named_stream(ni))
+		ntfs_stream_put(vi);
+	if (ret > 0 && ntfs_inode_is_named_stream(ni))
+		ntfs_stream_update_base_time(base_ni);
+	return ret;
+
+out_ctx:
 	ntfs_attr_put_search_ctx(ctx);
+out_unlock:
+	mutex_unlock(&mrec_ni->mrec_lock);
+	if (ret > 0 && ntfs_inode_is_named_stream(ni))
+		ntfs_stream_update_base_time(base_ni);
 	return ret;
 }
 
diff --git a/fs/ntfs/inode.h b/fs/ntfs/inode.h
index ff61bd402df0..f4e5562d56a7 100644
--- a/fs/ntfs/inode.h
+++ b/fs/ntfs/inode.h
@@ -107,6 +107,7 @@ struct ntfs_inode {
 	__le32 type;
 	__le16 *name;
 	u32 name_len;
+	atomic_t stream_open_count;
 	struct runlist runlist;
 	s64 data_size;
 	s64 initialized_size;
@@ -178,6 +179,7 @@ struct ntfs_inode {
  * NI_BeingCreated		ntfs inode is being created.
  * NI_HasEA			ntfs inode has EA attribute.
  * NI_RunlistDirty		runlist need to be updated.
+ * NI_StreamUnlinked		Named stream is unlinked but still open.
  */
 enum {
 	NI_Dirty,
@@ -199,6 +201,7 @@ enum {
 	NI_BeingCreated,
 	NI_HasEA,
 	NI_RunlistDirty,
+	NI_StreamUnlinked,
 };
 
 /*
@@ -259,6 +262,7 @@ TAS_NINO_FNS(FileNameDirty)
 NINO_FNS(BeingDeleted)
 NINO_FNS(HasEA)
 NINO_FNS(RunlistDirty)
+NINO_FNS(StreamUnlinked)
 
 /*
  * The full structure containing a ntfs_inode and a vfs struct inode. Used for
@@ -286,6 +290,20 @@ static inline struct inode *VFS_I(struct ntfs_inode *ni)
 	return &container_of(ni, struct big_ntfs_inode, ntfs_inode)->vfs_inode;
 }
 
+/* Return true when @ni represents a named $DATA attribute inode. */
+static inline bool ntfs_inode_is_named_stream(struct ntfs_inode *ni)
+{
+	return NInoAttr(ni) && ni->type == AT_DATA && ni->name_len;
+}
+
+/* Return the base MFT inode for an attribute inode, or @ni itself. */
+static inline struct ntfs_inode *ntfs_base_inode(struct ntfs_inode *ni)
+{
+	if (NInoAttr(ni) && ni->nr_extents == -1 && ni->ext.base_ntfs_ino)
+		return ni->ext.base_ntfs_ino;
+	return ni;
+}
+
 /*
  * ntfs_attr - ntfs in memory attribute structure
  *
@@ -304,6 +322,7 @@ struct ntfs_attr {
 };
 
 int ntfs_test_inode(struct inode *vi, void *data);
+int ntfs_test_inode_rcu(struct inode *vi, void *data);
 struct inode *ntfs_iget(struct super_block *sb, u64 mft_no);
 struct inode *ntfs_attr_iget(struct inode *base_vi, __le32 type,
 		__le16 *name, u32 name_len);
diff --git a/fs/ntfs/named_stream.c b/fs/ntfs/named_stream.c
new file mode 100644
index 000000000000..f18312db9859
--- /dev/null
+++ b/fs/ntfs/named_stream.c
@@ -0,0 +1,915 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * NTFS named stream handling.
+ *
+ * Copyright (c) 2026 LG Electronics Co., Ltd.
+ */
+
+#include <linux/file.h>
+#include <linux/overflow.h>
+
+#include <uapi/linux/ntfs.h>
+
+#include "attrib.h"
+#include "dir.h"
+#include "iomap.h"
+#include "mft.h"
+#include "ntfs.h"
+#include "stream.h"
+#include "time.h"
+
+#define NTFS_STREAM_MAX_IO	(16 * 1024 * 1024)
+
+/*
+ * ntfs_check_stream_name() - Validate a named-stream name
+ * @name: UTF-16LE stream name
+ * @name_len: Length of @name in UTF-16 code units
+ *
+ * Reject empty, overlong, separator-containing, or malformed UTF-16 names.
+ *
+ * Return: 0 if valid, or -EINVAL otherwise.
+ */
+int ntfs_check_stream_name(const __le16 *name, unsigned int name_len)
+{
+	unsigned int i;
+
+	if (!name_len || name_len > NTFS_MAX_NAME_LEN)
+		return -EINVAL;
+
+	for (i = 0; i < name_len; i++) {
+		u16 c = le16_to_cpu(name[i]);
+
+		if (c == 0 || c == '/' || c == '\\' || c == ':')
+			return -EINVAL;
+		if (c >= 0xd800 && c <= 0xdbff) {
+			if (++i >= name_len)
+				return -EINVAL;
+			c = le16_to_cpu(name[i]);
+			if (c < 0xdc00 || c > 0xdfff)
+				return -EINVAL;
+		} else if (c >= 0xdc00 && c <= 0xdfff) {
+			return -EINVAL;
+		}
+	}
+
+	return 0;
+}
+
+/*
+ * ntfs_stream_inode_refresh() - Refresh base-derived stream inode metadata
+ * @vi: inode representing a named stream
+ *
+ * Synchronize ownership, mode, flags, timestamps, and generation with the
+ * base inode. Stream size and allocation remain specific to the stream.
+ */
+void ntfs_stream_inode_refresh(struct inode *vi)
+{
+	struct ntfs_inode *ni = NTFS_I(vi);
+	struct inode *base_vi;
+
+	if (!ntfs_inode_is_named_stream(ni) || ni->nr_extents != -1)
+		return;
+
+	base_vi = VFS_I(ni->ext.base_ntfs_ino);
+	vi->i_uid = base_vi->i_uid;
+	vi->i_gid = base_vi->i_gid;
+	vi->i_mode = (base_vi->i_mode & ~S_IFMT) | S_IFREG;
+	vi->i_flags = base_vi->i_flags;
+	inode_set_mtime_to_ts(vi, inode_get_mtime(base_vi));
+	inode_set_ctime_to_ts(vi, inode_get_ctime(base_vi));
+	inode_set_atime_to_ts(vi, inode_get_atime(base_vi));
+	vi->i_generation = base_vi->i_generation;
+}
+
+/*
+ * ntfs_stream_update_base_time() - Update base timestamps after a stream change
+ * @base_ni: base inode containing the stream attribute
+ *
+ * Update the base inode's mtime and ctime and mark it dirty. The helper takes
+ * the base inode lock.
+ */
+void ntfs_stream_update_base_time(struct ntfs_inode *base_ni)
+{
+	struct inode *base_vi = VFS_I(base_ni);
+
+	inode_lock_nested(base_vi, I_MUTEX_PARENT);
+	inode_set_mtime_to_ts(base_vi, inode_set_ctime_current(base_vi));
+	mark_inode_dirty(base_vi);
+	inode_unlock(base_vi);
+}
+
+/*
+ * ntfs_stream_inode_validate() - Validate a named-stream inode
+ * @vi: stream inode to validate
+ *
+ * Verify that @vi still maps to its named DATA attribute and refresh its
+ * base-derived metadata. The caller must hold the base inode's MFT-record
+ * lock.
+ *
+ * Return: 0 if valid, or a negative errno.
+ */
+int ntfs_stream_inode_validate(struct inode *vi)
+{
+	struct ntfs_inode *ni = NTFS_I(vi);
+	struct ntfs_inode *base_ni;
+	struct ntfs_attr_search_ctx *ctx;
+	int err;
+
+	if (!ntfs_inode_is_named_stream(ni) || ni->nr_extents != -1)
+		return -EINVAL;
+
+	base_ni = ni->ext.base_ntfs_ino;
+	lockdep_assert_held(&base_ni->mrec_lock);
+	if (NVolShutdown(base_ni->vol))
+		return -EIO;
+	if (!NInoStreamUnlinked(ni) &&
+	    (!vi->i_nlink || !VFS_I(base_ni)->i_nlink ||
+	     NInoBeingDeleted(base_ni)))
+		return -ESTALE;
+
+	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
+	if (!ctx)
+		return -ENOMEM;
+	err = ntfs_attr_lookup(AT_DATA, ni->name, ni->name_len,
+			CASE_SENSITIVE, 0, NULL, 0, ctx);
+	ntfs_attr_put_search_ctx(ctx);
+	if (err)
+		return err;
+
+	ntfs_stream_inode_refresh(vi);
+	return 0;
+}
+
+/*
+ * ntfs_stream_unlinked() - Check a cached stream's deferred-unlink state
+ * @base_ni: base inode containing the stream
+ * @name: UTF-16LE stream name
+ * @name_len: Length of @name in UTF-16 code units
+ *
+ * The caller must hold the base inode's MFT-record lock.
+ *
+ * Return: true if the cached stream inode is unlinked, or false if it is not
+ * cached or is still linked.
+ */
+bool ntfs_stream_unlinked(struct ntfs_inode *base_ni,
+		const __le16 *name, u32 name_len)
+{
+	struct ntfs_attr na = {
+		.mft_no = base_ni->mft_no,
+		.type = AT_DATA,
+		.name = (__le16 *)name,
+		.name_len = name_len,
+	};
+	struct inode *vi;
+	bool unlinked;
+
+	lockdep_assert_held(&base_ni->mrec_lock);
+	rcu_read_lock();
+	vi = find_inode_rcu(base_ni->vol->sb, na.mft_no,
+			ntfs_test_inode_rcu, &na);
+	if (!vi) {
+		rcu_read_unlock();
+		return false;
+	}
+	unlinked = NInoStreamUnlinked(NTFS_I(vi));
+	rcu_read_unlock();
+	return unlinked;
+}
+
+/*
+ * Hold a temporary stream reference so unlink cannot remove its attribute
+ * while an operation is using it.
+ */
+static int ntfs_stream_claim(struct inode *inode)
+{
+	struct ntfs_inode *ni = NTFS_I(inode);
+	struct ntfs_inode *base_ni = ni->ext.base_ntfs_ino;
+	int err;
+
+	mutex_lock(&base_ni->mrec_lock);
+	if (NInoStreamUnlinked(ni))
+		err = -ESTALE;
+	else
+		err = ntfs_stream_inode_validate(inode);
+	if (!err) {
+		if (atomic_read(&ni->stream_open_count) < 0)
+			err = -ESTALE;
+		else
+			atomic_inc(&ni->stream_open_count);
+	}
+	mutex_unlock(&base_ni->mrec_lock);
+	return err;
+}
+
+/*
+ * Remove an unlinked stream's DATA attribute after its final active reference
+ * is released. Base-inode deletion handles the attribute when the base is
+ * already being removed.
+ */
+static int ntfs_stream_finish_remove(struct inode *attr_vi)
+{
+	struct ntfs_inode *attr_ni = NTFS_I(attr_vi);
+	struct ntfs_inode *ni = attr_ni->ext.base_ntfs_ino;
+	int err;
+
+	mutex_lock(&ni->mrec_lock);
+	if (!NInoStreamUnlinked(attr_ni) ||
+	    atomic_cmpxchg(&attr_ni->stream_open_count, 0, -1)) {
+		err = 0;
+		goto out_unlock;
+	}
+	if (NInoBeingDeleted(ni) || !VFS_I(ni)->i_nlink) {
+		remove_inode_hash(attr_vi);
+		err = 0;
+		goto out_unlock;
+	}
+	mutex_unlock(&ni->mrec_lock);
+
+	truncate_inode_pages(attr_vi->i_mapping, 0);
+
+	mutex_lock(&ni->mrec_lock);
+	if (NVolShutdown(ni->vol)) {
+		err = -EIO;
+		goto out_unlock;
+	}
+	if (NInoBeingDeleted(ni) || !VFS_I(ni)->i_nlink) {
+		remove_inode_hash(attr_vi);
+		err = 0;
+		goto out_unlock;
+	}
+	err = ntfs_attr_rm(attr_ni);
+	if (!err) {
+		NInoClearDirty(attr_ni);
+		remove_inode_hash(attr_vi);
+	} else {
+		NInoSetBeingDeleted(attr_ni);
+		remove_inode_hash(attr_vi);
+	}
+
+out_unlock:
+	mutex_unlock(&ni->mrec_lock);
+	return err;
+}
+
+/*
+ * ntfs_stream_put() - Release a stream operation reference
+ * @vi: stream inode whose reference is being released
+ *
+ * Finish a deferred unlink when this is the last reference to an unlinked
+ * stream.
+ *
+ * Return: 0 on success, or a negative errno if deferred removal fails.
+ */
+int ntfs_stream_put(struct inode *vi)
+{
+	struct ntfs_inode *ni = NTFS_I(vi);
+
+	if (atomic_dec_and_test(&ni->stream_open_count) &&
+	    NInoStreamUnlinked(ni))
+		return ntfs_stream_finish_remove(vi);
+	return 0;
+}
+
+/*
+ * ntfs_remove_named_stream() - Remove a named DATA stream
+ * @ni: base inode containing the stream
+ * @uname: UTF-16LE stream name
+ * @uname_len: length of @uname in UTF-16 code units
+ * @expected_vi: expected cached stream inode, or NULL
+ *
+ * Refuse removal while the stream is open. If @expected_vi is non-NULL, it
+ * must be the inode currently associated with the named attribute.
+ *
+ * Return: 0 on success, -ENOENT if the stream is absent, or another negative
+ * errno.
+ */
+int ntfs_remove_named_stream(struct ntfs_inode *ni, __le16 *uname,
+		u32 uname_len, struct inode *expected_vi)
+{
+	struct inode *attr_vi;
+	struct ntfs_inode *attr_ni;
+	int err;
+
+	if (!ni || !uname || uname_len == 0)
+		return -EINVAL;
+	if (NVolShutdown(ni->vol))
+		return -EIO;
+	if (IS_APPEND(VFS_I(ni)) || IS_IMMUTABLE(VFS_I(ni)))
+		return -EPERM;
+	if (!(ni->vol->vol_flags & VOLUME_IS_DIRTY)) {
+		err = ntfs_set_volume_flags(ni->vol, VOLUME_IS_DIRTY);
+		if (err)
+			return err;
+	}
+
+	mutex_lock(&ni->mrec_lock);
+	if (NInoBeingDeleted(ni) || !VFS_I(ni)->i_nlink) {
+		err = -ENOENT;
+		goto out_unlock;
+	}
+	attr_vi = ntfs_attr_iget(VFS_I(ni), AT_DATA, uname, uname_len);
+	if (IS_ERR(attr_vi)) {
+		err = PTR_ERR(attr_vi);
+		goto out_unlock;
+	}
+	if (expected_vi && attr_vi != expected_vi) {
+		err = -ESTALE;
+		goto out_iput;
+	}
+
+	attr_ni = NTFS_I(attr_vi);
+	if (NInoStreamUnlinked(attr_ni)) {
+		err = -ENOENT;
+		goto out_iput;
+	}
+	if (atomic_read(&attr_ni->stream_open_count) > 0) {
+		err = -EBUSY;
+		goto out_iput;
+	}
+	err = ntfs_stream_inode_validate(attr_vi);
+	if (err)
+		goto out_iput;
+
+	NInoSetStreamUnlinked(attr_ni);
+	clear_nlink(attr_vi);
+	mutex_unlock(&ni->mrec_lock);
+
+	ntfs_stream_update_base_time(ni);
+	err = ntfs_stream_finish_remove(attr_vi);
+	iput(attr_vi);
+	return err;
+
+out_iput:
+	mutex_unlock(&ni->mrec_lock);
+	iput(attr_vi);
+	return err;
+out_unlock:
+	mutex_unlock(&ni->mrec_lock);
+	return err;
+}
+
+enum ntfs_stream_ioctl_op {
+	NTFS_STREAM_IOCTL_READ,
+	NTFS_STREAM_IOCTL_WRITE,
+	NTFS_STREAM_IOCTL_REMOVE,
+};
+
+/*
+ * Look up or create an ioctl target stream and return its referenced inode.
+ */
+static int ntfs_stream_ioctl_get_inode(struct inode *base_vi, __le16 *sname,
+		int sname_len, bool create, struct inode **stream_vi)
+{
+	struct ntfs_inode *base_ni = NTFS_I(base_vi);
+	struct ntfs_attr_search_ctx *ctx;
+	struct inode *attr_vi = NULL;
+	bool created = false;
+	int err;
+
+	*stream_vi = NULL;
+
+	mutex_lock(&base_ni->mrec_lock);
+	if (NVolShutdown(base_ni->vol)) {
+		err = -EIO;
+		goto out_unlock;
+	}
+	if (NInoBeingDeleted(base_ni) || !base_vi->i_nlink) {
+		err = -ENOENT;
+		goto out_unlock;
+	}
+
+	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
+	if (!ctx) {
+		err = -ENOMEM;
+		goto out_unlock;
+	}
+
+	err = ntfs_attr_lookup(AT_DATA, sname, sname_len, IGNORE_CASE,
+			0, NULL, 0, ctx);
+	if (err == -ENOENT && create) {
+		err = ntfs_attr_add(base_ni, AT_DATA, sname, sname_len,
+				NULL, 0);
+		if (!err) {
+			created = true;
+			mark_mft_record_dirty(base_ni);
+		}
+	}
+	ntfs_attr_put_search_ctx(ctx);
+	if (err)
+		goto out_unlock;
+
+	attr_vi = ntfs_attr_iget(base_vi, AT_DATA, sname, sname_len);
+	if (IS_ERR(attr_vi)) {
+		err = PTR_ERR(attr_vi);
+		attr_vi = NULL;
+		goto out_unlock;
+	}
+	if (NInoStreamUnlinked(NTFS_I(attr_vi)))
+		err = create ? -EBUSY : -ENOENT;
+	else
+		err = ntfs_stream_inode_validate(attr_vi);
+out_unlock:
+	mutex_unlock(&base_ni->mrec_lock);
+	if (created)
+		ntfs_stream_update_base_time(base_ni);
+	if (err) {
+		iput(attr_vi);
+		return err;
+	}
+
+	*stream_vi = attr_vi;
+	return 0;
+}
+
+/* Check file mode, base-inode flags, and the requested transfer range. */
+static int ntfs_stream_ioctl_access(struct file *filp,
+		enum ntfs_stream_ioctl_op op, loff_t pos, size_t len)
+{
+	struct inode *inode = file_inode(filp);
+	bool is_dir = S_ISDIR(inode->i_mode);
+	int err;
+
+	if (op == NTFS_STREAM_IOCTL_READ) {
+		if (!(filp->f_mode & FMODE_READ))
+			return -EBADF;
+		return rw_verify_area(READ, filp, &pos, len);
+	}
+
+	if (!(filp->f_mode & FMODE_WRITE) && !is_dir)
+		return -EBADF;
+	if (is_dir) {
+		err = inode_permission(file_mnt_idmap(filp), inode,
+				MAY_WRITE | MAY_EXEC);
+		if (err)
+			return err;
+	}
+	if (IS_APPEND(inode) || IS_IMMUTABLE(inode))
+		return -EPERM;
+	if (op == NTFS_STREAM_IOCTL_REMOVE)
+		return 0;
+	return rw_verify_area(WRITE, filp, &pos, len);
+}
+
+/*
+ * Validate and execute a named-stream read, write, or remove ioctl request.
+ */
+static long ntfs_ioctl_stream(struct file *filp, unsigned long arg,
+		enum ntfs_stream_ioctl_op op)
+{
+	struct inode *base_vi = file_inode(filp);
+	struct ntfs_inode *base_ni = NTFS_I(base_vi);
+	struct ntfs_stream hdr;
+	struct ntfs_stream *req;
+	struct inode *stream_vi = NULL;
+	__le16 *uname = NULL, *sname;
+	size_t data_size, total_size;
+	loff_t pos;
+	s64 ret;
+	int sname_len, err;
+	bool claimed = false, got_write = false, utf16;
+
+	if (NVolShutdown(base_ni->vol))
+		return -EIO;
+	if (NInoAttr(base_ni) ||
+	    (!S_ISREG(base_vi->i_mode) && !S_ISDIR(base_vi->i_mode)))
+		return -EOPNOTSUPP;
+	if (copy_from_user(&hdr, (void __user *)arg, sizeof(hdr)))
+		return -EFAULT;
+
+	if (hdr.io_len > NTFS_STREAM_MAX_IO ||
+	    (hdr.flags & ~NTFS_STREAM_FL_UTF16) || hdr.reserved)
+		return -EINVAL;
+	if (!hdr.name_len)
+		return -EINVAL;
+
+	utf16 = hdr.flags & NTFS_STREAM_FL_UTF16;
+	if (utf16) {
+		if ((hdr.name_len & 1) ||
+		    hdr.name_len > NTFS_MAX_NAME_LEN * sizeof(__le16))
+			return -EINVAL;
+	} else if (hdr.name_len >
+			NTFS_MAX_NAME_LEN * NLS_MAX_CHARSET_SIZE) {
+		return -EINVAL;
+	}
+
+	switch (op) {
+	case NTFS_STREAM_IOCTL_READ:
+	case NTFS_STREAM_IOCTL_WRITE:
+		if (!hdr.io_len)
+			return -EINVAL;
+		if (hdr.stream_offset > S64_MAX ||
+		    hdr.stream_offset > S64_MAX - hdr.io_len)
+			return -EOVERFLOW;
+		data_size = hdr.io_len;
+		break;
+	case NTFS_STREAM_IOCTL_REMOVE:
+		if (hdr.io_len || hdr.stream_offset)
+			return -EINVAL;
+		data_size = 0;
+		break;
+	default:
+		return -ENOTTY;
+	}
+
+	if (check_add_overflow(sizeof(hdr), (size_t)hdr.name_len,
+			&total_size) ||
+	    check_add_overflow(total_size, data_size, &total_size))
+		return -EOVERFLOW;
+
+	if (op == NTFS_STREAM_IOCTL_READ) {
+		req = kvmalloc(total_size, GFP_KERNEL);
+		if (!req)
+			return -ENOMEM;
+		if (copy_from_user(req, (void __user *)arg,
+				sizeof(hdr) + hdr.name_len)) {
+			kvfree(req);
+			return -EFAULT;
+		}
+	} else {
+		req = vmemdup_user((void __user *)arg, total_size);
+		if (IS_ERR(req))
+			return PTR_ERR(req);
+	}
+
+	req->bytes_returned = 0;
+	if (req->stream_offset != hdr.stream_offset ||
+	    req->io_len != hdr.io_len || req->name_len != hdr.name_len ||
+	    req->flags != hdr.flags || req->reserved) {
+		err = -EINVAL;
+		goto out_free;
+	}
+
+	if (utf16) {
+		sname = (__le16 *)req->buffer;
+		sname_len = req->name_len / sizeof(__le16);
+	} else {
+		if (memchr(req->buffer, '\0', req->name_len)) {
+			err = -EINVAL;
+			goto out_free;
+		}
+		sname_len = ntfs_nlstoucs(base_ni->vol, req->buffer,
+				req->name_len, &uname, NTFS_MAX_NAME_LEN);
+		if (sname_len < 0) {
+			err = sname_len;
+			goto out_free;
+		}
+		sname = uname;
+	}
+	err = ntfs_check_stream_name(sname, sname_len);
+	if (err)
+		goto out_free;
+
+	pos = hdr.stream_offset;
+	err = ntfs_stream_ioctl_access(filp, op, pos, data_size);
+	if (err)
+		goto out_free;
+
+	if (op == NTFS_STREAM_IOCTL_REMOVE) {
+		err = mnt_want_write_file(filp);
+		if (err)
+			goto out_free;
+		err = ntfs_remove_named_stream(base_ni, sname, sname_len, NULL);
+		mnt_drop_write_file(filp);
+		goto out_free;
+	}
+
+	if (op == NTFS_STREAM_IOCTL_WRITE) {
+		err = mnt_want_write_file(filp);
+		if (err)
+			goto out_free;
+		got_write = true;
+		inode_lock(base_vi);
+		err = file_remove_privs(filp);
+		inode_unlock(base_vi);
+		if (err)
+			goto out_drop_write;
+		if (!(base_ni->vol->vol_flags & VOLUME_IS_DIRTY)) {
+			err = ntfs_set_volume_flags(base_ni->vol,
+					VOLUME_IS_DIRTY);
+			if (err)
+				goto out_drop_write;
+		}
+	}
+
+	err = ntfs_stream_ioctl_get_inode(base_vi, sname, sname_len,
+			op == NTFS_STREAM_IOCTL_WRITE, &stream_vi);
+	if (err)
+		goto out_drop_write;
+
+	err = ntfs_stream_claim(stream_vi);
+	if (err)
+		goto out_drop_write;
+	claimed = true;
+
+	if (op == NTFS_STREAM_IOCTL_READ) {
+		inode_lock_shared(stream_vi);
+		if (pos < i_size_read(stream_vi))
+			ret = ntfs_inode_attr_pread(stream_vi, pos, data_size,
+					req->buffer + req->name_len);
+		else
+			ret = 0;
+		inode_unlock_shared(stream_vi);
+		if (ret < 0)
+			err = ret;
+		else
+			req->bytes_returned = ret;
+		if (!err)
+			file_accessed(filp);
+	} else {
+		inode_lock(stream_vi);
+		err = inode_newsize_ok(stream_vi, pos + data_size);
+		if (!err) {
+			ret = ntfs_inode_attr_pwrite(stream_vi, pos, data_size,
+					req->buffer + req->name_len, false);
+			if (ret < 0)
+				err = ret;
+			else
+				req->bytes_returned = ret;
+		}
+		inode_unlock(stream_vi);
+	}
+	if (claimed) {
+		int put_err;
+
+		put_err = ntfs_stream_put(stream_vi);
+		claimed = false;
+		if (!err && put_err)
+			err = put_err;
+	}
+	iput(stream_vi);
+	stream_vi = NULL;
+	if (err)
+		goto out_drop_write;
+
+	if (op == NTFS_STREAM_IOCTL_READ) {
+		if (copy_to_user((void __user *)arg, req,
+				sizeof(*req) + req->name_len +
+				req->bytes_returned))
+			err = -EFAULT;
+	} else if (copy_to_user((void __user *)arg +
+			offsetof(struct ntfs_stream, bytes_returned),
+			&req->bytes_returned, sizeof(req->bytes_returned))) {
+		err = -EFAULT;
+	}
+	goto out_drop_write;
+
+out_drop_write:
+	if (claimed)
+		ntfs_stream_put(stream_vi);
+	if (stream_vi)
+		iput(stream_vi);
+	if (got_write)
+		mnt_drop_write_file(filp);
+out_free:
+	if (uname)
+		kmem_cache_free(ntfs_name_cache, uname);
+	kvfree(req);
+	return err;
+}
+
+/*
+ * ntfs_ioctl_stream_read() - Handle a named-stream read ioctl
+ * @filp: file opened on the base file or directory
+ * @arg: userspace pointer to the request and data buffer
+ *
+ * Return: 0 on success, or a negative errno.
+ */
+long ntfs_ioctl_stream_read(struct file *filp, unsigned long arg)
+{
+	return ntfs_ioctl_stream(filp, arg, NTFS_STREAM_IOCTL_READ);
+}
+
+/*
+ * ntfs_ioctl_stream_write() - Handle a named-stream write ioctl
+ * @filp: file opened on the base file or directory
+ * @arg: userspace pointer to the request and data buffer
+ *
+ * Create the stream if needed.
+ *
+ * Return: 0 on success, or a negative errno.
+ */
+long ntfs_ioctl_stream_write(struct file *filp, unsigned long arg)
+{
+	return ntfs_ioctl_stream(filp, arg, NTFS_STREAM_IOCTL_WRITE);
+}
+
+/*
+ * ntfs_ioctl_stream_remove() - Handle a named-stream remove ioctl
+ * @filp: file opened on the base file or directory
+ * @arg: userspace pointer to the request
+ *
+ * Return: 0 on success, or a negative errno.
+ */
+long ntfs_ioctl_stream_remove(struct file *filp, unsigned long arg)
+{
+	return ntfs_ioctl_stream(filp, arg, NTFS_STREAM_IOCTL_REMOVE);
+}
+
+/*
+ * ntfs_ioctl_list_streams() - List named DATA streams
+ * @filp: file opened on the base file or directory
+ * @arg: userspace pointer to the request and output buffer
+ *
+ * On -ENOSPC, return the required buffer size in the request header.
+ *
+ * Return: 0 on success, or a negative errno.
+ */
+int ntfs_ioctl_list_streams(struct file *filp, unsigned long arg)
+{
+	struct inode *inode = file_inode(filp);
+	struct ntfs_inode *ni = NTFS_I(inode);
+	struct ntfs_list_streams hdr;
+	struct ntfs_stream_entry *entry, *last_entry = NULL;
+	size_t required = 0;
+	void *kbuf = NULL;
+	void __user *ubuf;
+	struct attr_record *a;
+	struct ntfs_attr_search_ctx *actx;
+	int ret = 0, err, count = 0;
+	size_t entry_size, offset = 0;
+	const size_t name_offset = offsetof(struct ntfs_stream_entry, name);
+	int name_len;
+	unsigned char *sn = NULL;
+	bool utf16;
+
+	if (NVolShutdown(ni->vol))
+		return -EIO;
+	if (NInoAttr(ni) ||
+	    (!S_ISREG(inode->i_mode) && !S_ISDIR(inode->i_mode)))
+		return -EOPNOTSUPP;
+	err = inode_permission(file_mnt_idmap(filp), inode, MAY_READ);
+	if (err)
+		return err;
+
+	if (copy_from_user(&hdr, (void __user *)arg, sizeof(hdr)))
+		return -EFAULT;
+
+	if ((hdr.flags & ~NTFS_STREAM_FL_UTF16) || hdr.reserved)
+		return -EINVAL;
+
+	utf16 = hdr.flags & NTFS_STREAM_FL_UTF16;
+
+	ubuf = (void __user *)arg + sizeof(hdr);
+
+	mutex_lock(&ni->mrec_lock);
+	actx = ntfs_attr_get_search_ctx(ni, NULL);
+	if (!actx) {
+		mutex_unlock(&ni->mrec_lock);
+		return -ENOMEM;
+	}
+
+	while ((err = ntfs_attrs_walk(actx)) == 0) {
+		a = actx->attr;
+		if (a->type != AT_DATA || !a->name_length)
+			continue;
+		if (a->non_resident &&
+		    a->data.non_resident.lowest_vcn)
+			continue;
+		if (ntfs_stream_unlinked(ni,
+				(__le16 *)((u8 *)a +
+				le16_to_cpu(a->name_offset)),
+				a->name_length))
+			continue;
+
+		if (utf16) {
+			name_len = a->name_length * sizeof(__le16);
+		} else {
+			name_len = ntfs_ucstonls(ni->vol,
+					(__le16 *)((u8 *)a +
+					le16_to_cpu(a->name_offset)),
+					a->name_length, &sn, 0);
+			if (name_len < 0) {
+				if (name_len == -EILSEQ ||
+				    name_len == -ENAMETOOLONG)
+					continue;
+				ret = name_len;
+				goto out;
+			}
+			kfree(sn);
+			sn = NULL;
+		}
+
+		entry_size = ALIGN(name_offset + name_len, 8);
+
+		if (required > SIZE_MAX - entry_size) {
+			ret = -EOVERFLOW;
+			goto out;
+		}
+		required += entry_size;
+		count++;
+	}
+	if (err != -ENOENT) {
+		ret = err;
+		goto out;
+	}
+
+	hdr.stream_count = count;
+	hdr.bytes_returned = required;
+
+	if (!count)
+		goto out;
+
+	if (hdr.buffer_size < required) {
+		ret = -ENOSPC;
+		goto out;
+	}
+
+	kbuf = kvzalloc(required, GFP_NOFS);
+	if (!kbuf) {
+		ret = -ENOMEM;
+		goto out;
+	}
+
+	ntfs_attr_reinit_search_ctx(actx);
+	while ((err = ntfs_attrs_walk(actx)) == 0) {
+		a = actx->attr;
+		if (a->type != AT_DATA || !a->name_length)
+			continue;
+		if (a->non_resident &&
+		    a->data.non_resident.lowest_vcn)
+			continue;
+		if (ntfs_stream_unlinked(ni,
+				(__le16 *)((u8 *)a +
+				le16_to_cpu(a->name_offset)),
+				a->name_length))
+			continue;
+		if (utf16) {
+			sn = NULL;
+			name_len = a->name_length * sizeof(__le16);
+		} else {
+			name_len = ntfs_ucstonls(ni->vol,
+					(__le16 *)((u8 *)a +
+					le16_to_cpu(a->name_offset)),
+					a->name_length, &sn, 0);
+			if (name_len < 0) {
+				if (name_len == -EILSEQ ||
+				    name_len == -ENAMETOOLONG)
+					continue;
+				ret = name_len;
+				goto out;
+			}
+		}
+
+		entry_size = ALIGN(name_offset + name_len, 8);
+		if (offset > required - entry_size) {
+			ret = -EOVERFLOW;
+			kfree(sn);
+			sn = NULL;
+			goto out;
+		}
+
+		entry = (struct ntfs_stream_entry *)(kbuf + offset);
+
+		if (a->non_resident) {
+			entry->size = le64_to_cpu(a->data.non_resident.data_size);
+			entry->alloc_size =
+				le64_to_cpu(a->data.non_resident.allocated_size);
+		} else {
+			entry->size = le32_to_cpu(a->data.resident.value_length);
+			entry->alloc_size = le32_to_cpu(a->length) -
+				le16_to_cpu(a->data.resident.value_offset);
+		}
+
+		entry->name_len = name_len;
+		entry->name_offset = name_offset;
+		if (utf16)
+			memcpy(entry->name,
+			       (u8 *)a + le16_to_cpu(a->name_offset), name_len);
+		else
+			memcpy(entry->name, sn, name_len);
+		kfree(sn);
+		sn = NULL;
+
+		entry->next_entry_off = entry_size;
+		last_entry = entry;
+		offset += entry_size;
+	}
+	if (err != -ENOENT) {
+		ret = err;
+	} else {
+		/* The chain terminates at the last entry. */
+		if (last_entry)
+			last_entry->next_entry_off = 0;
+		hdr.bytes_returned = offset;
+	}
+
+out:
+	kfree(sn);
+	ntfs_attr_put_search_ctx(actx);
+	mutex_unlock(&ni->mrec_lock);
+
+	if (ret && ret != -ENOSPC)
+		goto out_free;
+
+	if (!ret && kbuf && copy_to_user(ubuf, kbuf, hdr.bytes_returned)) {
+		ret = -EFAULT;
+		goto out_free;
+	}
+
+	if (copy_to_user((void __user *)arg, &hdr, sizeof(hdr)))
+		ret = -EFAULT;
+
+out_free:
+	kvfree(kbuf);
+	return ret;
+}
diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c
index 75e201096525..3cf58befd77f 100644
--- a/fs/ntfs/namei.c
+++ b/fs/ntfs/namei.c
@@ -805,8 +805,29 @@ static int ntfs_test_inode_attr(struct inode *vi, void *data)
 		return 0;
 	if (NInoAttr(ni) || ni->nr_extents == -1)
 		return 1;
-	else
-		return 0;
+	return 0;
+}
+
+static void ntfs_cleanup_deleted_inode(struct ntfs_inode *ni)
+{
+	struct inode *attr_vi;
+	struct super_block *sb = VFS_I(ni)->i_sb;
+
+	if (!NInoBeingDeleted(ni))
+		return;
+
+	while ((attr_vi = ilookup5(sb, ni->mft_no, ntfs_test_inode_attr,
+			(void *)(uintptr_t)ni->mft_no))) {
+		struct ntfs_inode *attr_ni = NTFS_I(attr_vi);
+
+		if (ntfs_inode_is_named_stream(attr_ni)) {
+			if (atomic_read(&attr_ni->stream_open_count) > 0)
+				NInoSetStreamUnlinked(attr_ni);
+			remove_inode_hash(attr_vi);
+		}
+		clear_nlink(attr_vi);
+		iput(attr_vi);
+	}
 }
 
 /*
@@ -976,22 +997,10 @@ static int ntfs_delete(struct ntfs_inode *ni, struct ntfs_inode *dir_ni,
 	if (need_lock == true) {
 		mutex_unlock(&dir_ni->mrec_lock);
 		mutex_unlock(&ni->mrec_lock);
+		if (link_count_zero)
+			ntfs_cleanup_deleted_inode(ni);
 	}
 
-	/*
-	 * If hard link count is not equal to zero then we are done. In other
-	 * case there are no reference to this inode left, so we should free all
-	 * non-resident attributes and mark all MFT record as not in use.
-	 */
-	if (link_count_zero == true) {
-		struct inode *attr_vi;
-
-		while ((attr_vi = ilookup5(sb, ni->mft_no, ntfs_test_inode_attr,
-					   (void *)(uintptr_t)ni->mft_no)) != NULL) {
-			clear_nlink(attr_vi);
-			iput(attr_vi);
-		}
-	}
 	ntfs_debug("Done.\n");
 	return 0;
 err_out:
@@ -1385,6 +1394,8 @@ static int ntfs_rename(struct mnt_idmap *idmap, struct inode *old_dir,
 	if (new_ni)
 		mutex_unlock(&new_ni->mrec_lock);
 	mutex_unlock(&old_ni->mrec_lock);
+	if (new_ni)
+		ntfs_cleanup_deleted_inode(new_ni);
 	if (uname_new)
 		kmem_cache_free(ntfs_name_cache, uname_new);
 	if (uname_old)
diff --git a/fs/ntfs/stream.h b/fs/ntfs/stream.h
new file mode 100644
index 000000000000..da0096d2b751
--- /dev/null
+++ b/fs/ntfs/stream.h
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef _NTFS_STREAM_H
+#define _NTFS_STREAM_H
+
+#include <linux/fs.h>
+
+struct ntfs_inode;
+struct ntfs_volume;
+
+int ntfs_check_stream_name(const __le16 *name, unsigned int name_len);
+
+void ntfs_stream_inode_refresh(struct inode *inode);
+int ntfs_stream_inode_validate(struct inode *inode);
+void ntfs_stream_update_base_time(struct ntfs_inode *base_ni);
+bool ntfs_stream_unlinked(struct ntfs_inode *base_ni,
+		const __le16 *name, u32 name_len);
+int ntfs_stream_put(struct inode *inode);
+int ntfs_remove_named_stream(struct ntfs_inode *ni, __le16 *name,
+		u32 name_len, struct inode *expected_inode);
+
+long ntfs_ioctl_stream_read(struct file *file, unsigned long arg);
+long ntfs_ioctl_stream_write(struct file *file, unsigned long arg);
+long ntfs_ioctl_stream_remove(struct file *file, unsigned long arg);
+int ntfs_ioctl_list_streams(struct file *file, unsigned long arg);
+
+int ntfs_file_open(struct inode *inode, struct file *file);
+int ntfs_file_release(struct inode *inode, struct file *file);
+int ntfs_file_fsync(struct file *file, loff_t start, loff_t end,
+		int datasync);
+int ntfs_setattr_size(struct inode *inode, struct iattr *attr);
+loff_t ntfs_file_llseek(struct file *file, loff_t offset, int whence);
+ssize_t ntfs_file_read_iter(struct kiocb *iocb, struct iov_iter *to);
+ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from);
+int ntfs_file_mmap_prepare(struct vm_area_desc *desc);
+ssize_t ntfs_file_splice_read(struct file *in, loff_t *ppos,
+		struct pipe_inode_info *pipe, size_t len, unsigned int flags);
+int ntfs_fiemap(struct inode *inode, struct fiemap_extent_info *fieinfo,
+		u64 start, u64 len);
+long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t len);
+
+int ntfs_test_inode_rcu(struct inode *inode, void *data);
+
+#endif /* _NTFS_STREAM_H */
diff --git a/include/uapi/linux/ntfs.h b/include/uapi/linux/ntfs.h
new file mode 100644
index 000000000000..62dadce58087
--- /dev/null
+++ b/include/uapi/linux/ntfs.h
@@ -0,0 +1,123 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+/*
+ * Copyright (c) 2026 LG Electronics Co., Ltd.
+ */
+
+#ifndef _UAPI_LINUX_NTFS_H
+#define _UAPI_LINUX_NTFS_H
+#include <linux/types.h>
+#include <linux/ioctl.h>
+
+#define NTFS_IOC_MAGIC	0xEF
+
+/*
+ * Flags for ntfs_stream.flags and ntfs_list_streams.flags.
+ *
+ * NTFS_STREAM_FL_UTF16 makes stream names raw UTF-16LE, exactly as stored on
+ * disk, instead of encoding them with the mounted filesystem NLS. This
+ * allows lossless round-tripping of stream names whose characters are not
+ * representable by the mount NLS (e.g. for Wine, which works in UTF-16
+ * natively). When set, the name length fields count bytes of UTF-16LE and
+ * must therefore be even.
+ */
+#define NTFS_STREAM_FL_UTF16		0x1
+
+/*
+ * ntfs named stream read, write, and remove ioctl structure.
+ *
+ * @stream_offset:	Offset within the named stream for read/write.
+ * @io_len:		Number of bytes to read/write. Must be zero for remove.
+ * @bytes_returned:	Actual bytes transferred (out).
+ * @name_len:		Stream name length in bytes, not including any
+ *			terminating NUL. When NTFS_STREAM_FL_UTF16 is set,
+ *			this counts UTF-16LE bytes and must be even.
+ * @flags:		Bit mask of NTFS_STREAM_FL_* flags. Other bits must
+ *			be zero.
+ * @reserved:		Must be zero.
+ * @buffer:		Bare stream name followed by stream data for read/write.
+ *
+ * The stream name is encoded with the mounted filesystem NLS, or as raw
+ * UTF-16LE when NTFS_STREAM_FL_UTF16 is set. A write creates the stream if
+ * it does not already exist. A write failure after creation may leave the
+ * new stream behind; it can be removed separately.
+ */
+struct ntfs_stream {
+	__aligned_u64 stream_offset;
+	__aligned_u64 io_len;
+	__aligned_u64 bytes_returned;
+	__u32 name_len;
+	__u32 flags;
+	__aligned_u64 reserved;
+	__u8 buffer[];
+};
+
+/*
+ * Single stream entry returned by NTFS_IOC_LIST_STREAMS.
+ *
+ * @next_entry_off:	Byte offset from the start of this entry to the next
+ *			entry, or zero for the last entry. Always a multiple
+ *			of 8. Consumers must use this to advance instead of
+ *			computing the stride themselves.
+ * @size:		Stream data size in bytes.
+ * @alloc_size:		Bytes allocated for the stream (cluster aligned for
+ *			non-resident streams).
+ * @name_len:		Stream name length in bytes, not including a NUL
+ *			terminator (none is stored). Counts UTF-16LE bytes
+ *			when NTFS_STREAM_FL_UTF16 was requested.
+ * @name_offset:	Byte offset from the start of this entry to @name.
+ * @reserved:		Must be zero.
+ * @name:		Bare stream name; NLS encoded, or raw UTF-16LE when
+ *			NTFS_STREAM_FL_UTF16 was requested.
+ *
+ * New fixed fields may be added after @reserved and before @name. Consumers
+ * must use @name_offset to locate the name and @next_entry_off to advance to
+ * the next entry.
+ */
+struct ntfs_stream_entry {
+	__aligned_u64 next_entry_off;
+	__aligned_u64 size;
+	__aligned_u64 alloc_size;
+	__u32 name_len;
+	__u32 name_offset;
+	__u32 reserved;
+	__u8 name[];
+};
+
+/*
+ * ntfs list streams ioctl structure.
+ *
+ * @buffer_size:	user buffer size(in).
+ * @bytes_returned:	actual bytes written or required(out).
+ * @stream_count:	number of streams(out).
+ * @flags:		Bit mask of NTFS_STREAM_FL_* flags controlling the
+ *			encoding of the returned names; other bits must be zero.
+ * @reserved:		Must be zero.
+ * @buffer:		ntfs_stream_entry array.
+ *
+ * The variable-length entries follow the header in @buffer, each aligned on
+ * an 8-byte boundary and chained via ntfs_stream_entry.next_entry_off. If
+ * @buffer_size is too small, no data is copied, @stream_count and
+ * @bytes_returned report the required values and the ioctl fails with
+ * -ENOSPC. In NLS mode, names that cannot be represented by the mounted
+ * character set are omitted; use NTFS_STREAM_FL_UTF16 to list all
+ * names without conversion loss.
+ */
+struct ntfs_list_streams {
+	__aligned_u64 buffer_size;
+	__aligned_u64 bytes_returned;
+	__aligned_u64 stream_count;
+	__u32 flags;
+	__u32 reserved;
+	__u8 buffer[];
+};
+
+#define NTFS_IOC_STREAM_READ \
+	_IOWR(NTFS_IOC_MAGIC, 1, struct ntfs_stream)
+#define NTFS_IOC_STREAM_WRITE \
+	_IOWR(NTFS_IOC_MAGIC, 2, struct ntfs_stream)
+#define NTFS_IOC_STREAM_REMOVE \
+	_IOWR(NTFS_IOC_MAGIC, 3, struct ntfs_stream)
+#define NTFS_IOC_LIST_STREAMS \
+	_IOWR(NTFS_IOC_MAGIC, 4, struct ntfs_list_streams)
+
+#endif /* _UAPI_LINUX_NTFS_H */
-- 
2.25.1


  reply	other threads:[~2026-10-06 22:41 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-06 22:40 [PATCH v2 0/4] ntfs: add named data stream support Namjae Jeon
2026-10-06 22:40 ` Namjae Jeon [this message]
2026-10-07  2:07   ` [PATCH v2 1/4] ntfs: add named stream ioctls support CharSyam
2026-10-07  2:24     ` Namjae Jeon
2026-10-06 22:40 ` [PATCH v2 2/4] ntfs: add pathname access for named streams Namjae Jeon
2026-10-07  3:38   ` CharSyam
2026-10-07  5:05     ` Namjae Jeon
2026-10-06 22:40 ` [PATCH v2 3/4] MAINTAINERS: ntfs: add UAPI header Namjae Jeon
2026-10-06 22:40 ` [PATCH v2 4/4] ntfs: document named streams Namjae Jeon

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=20261006224024.14535-2-linkinjeon@kernel.org \
    --to=linkinjeon@kernel.org \
    --cc=Lionelcons1972@gmail.com \
    --cc=cedric.blancher@gmail.com \
    --cc=hyc.lee@gmail.com \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=ntfs@lists.linux.dev \
    --cc=sebastian.n.feld@gmail.com \
    /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

all inboxes | Powered by JetHome®