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 2/4] ntfs: add pathname access for named streams
Date: Wed,  7 Oct 2026 07:40:22 +0900	[thread overview]
Message-ID: <20261006224024.14535-3-linkinjeon@kernel.org> (raw)
In-Reply-To: <20261006224024.14535-1-linkinjeon@kernel.org>

Add an optional Windows-style pathname interface for named $DATA streams,
enabled with streams_interface=windows. The option defaults to none, and
the named-stream ioctls remain available regardless of this setting.

Support lookup, creation, and removal of named streams for existing regular
files and directories. A stream is opened as a regular file through the
base-name and stream-name form in the final path component. A stream can be
created only for an existing file or directory. The pathname interface
accepts a base file name and stream name only. It rejects paths that also
specify an NTFS attribute type such as $DATA. In windows mode, ordinary
filenames containing a colon are hidden from directory listings and cannot
be accessed through the pathname interface.

Match stream names case-insensitively. Streams and their base objects
report the same inode number. Removing the base object while a stream is
open detaches the stream from the namespace. Since NTFS has no orphan-stream
list, a crash can leave the stream data unreachable on disk.

Expose named streams as regular file descriptors for the file operations
requested by Wine, including read, write, fsync, fdatasync,
sync_file_range, file locking, mmap, futimes, fstat, ftruncate, SEEK_DATA,
SEEK_HOLE, fallocate, and pathname unlink. Stream timestamps are shared
with the base file. fstat reports the base metadata and inode number with
the stream's own size and allocation.

Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
 fs/ntfs/attrib.c       |  40 +-
 fs/ntfs/attrib.h       |   2 +-
 fs/ntfs/dir.c          |   7 +-
 fs/ntfs/ea.c           |  21 +-
 fs/ntfs/file.c         | 117 ++++--
 fs/ntfs/inode.c        |  18 +-
 fs/ntfs/iomap.c        |  62 +--
 fs/ntfs/named_stream.c | 897 +++++++++++++++++++++++++++++++++++++++++
 fs/ntfs/namei.c        | 132 +++++-
 fs/ntfs/stream.h       |  38 ++
 fs/ntfs/super.c        |  23 ++
 fs/ntfs/volume.h       |   3 +
 fs/ntfs/wof.c          |   6 +-
 13 files changed, 1280 insertions(+), 86 deletions(-)

diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c
index 3266415470a7..e94e6714b9a1 100644
--- a/fs/ntfs/attrib.c
+++ b/fs/ntfs/attrib.c
@@ -5485,34 +5485,46 @@ int ntfs_attr_exist(struct ntfs_inode *ni, const __le32 type, __le16 *name,
 	return !ret;
 }
 
+/*
+ * If @attr_vi is non-NULL, the attribute inode reference is returned to the
+ * caller, which must release it after dropping ni->mrec_lock.
+ */
 int ntfs_attr_remove(struct ntfs_inode *ni, const __le32 type, __le16 *name,
-		u32 name_len)
+		u32 name_len, struct inode **attr_vi)
 {
 	int err;
-	struct inode *attr_vi;
+	struct inode *vi;
 	struct ntfs_inode *attr_ni;
 
 	ntfs_debug("Entering\n");
 
+	if (attr_vi)
+		*attr_vi = NULL;
 	if (!ni)
 		return -EINVAL;
 
-	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);
+	vi = ntfs_attr_iget(VFS_I(ni), type, name, name_len);
+	if (IS_ERR(vi)) {
+		err = PTR_ERR(vi);
+		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);
+	attr_ni = NTFS_I(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);
-	iput(attr_vi);
+	if (err) {
+		ntfs_error(ni->vol->sb, "Failed to remove attribute 0x%02x of inode 0x%llx",
+				type, (unsigned long long)ni->mft_no);
+	} else {
+		NInoClearDirty(attr_ni);
+		clear_nlink(vi);
+		remove_inode_hash(vi);
+	}
+	if (attr_vi)
+		*attr_vi = vi;
+	else
+		iput(vi);
 	return err;
 }
 
diff --git a/fs/ntfs/attrib.h b/fs/ntfs/attrib.h
index 6b4fa9f57640..bee91c9f6f81 100644
--- a/fs/ntfs/attrib.h
+++ b/fs/ntfs/attrib.h
@@ -124,7 +124,7 @@ int ntfs_attr_rm(struct ntfs_inode *ni);
 int ntfs_attr_exist(struct ntfs_inode *ni, const __le32 type, __le16 *name,
 		u32 name_len);
 int ntfs_attr_remove(struct ntfs_inode *ni, const __le32 type, __le16 *name,
-		u32 name_len);
+		u32 name_len, struct inode **attr_vi);
 int ntfs_attr_record_rm(struct ntfs_attr_search_ctx *ctx);
 int ntfs_attr_record_move_to(struct ntfs_attr_search_ctx *ctx, struct ntfs_inode *ni);
 int ntfs_attr_add(struct ntfs_inode *ni, __le32 type,
diff --git a/fs/ntfs/dir.c b/fs/ntfs/dir.c
index b95173f068cb..5a0e21e7c283 100644
--- a/fs/ntfs/dir.c
+++ b/fs/ntfs/dir.c
@@ -8,6 +8,7 @@
  */
 
 #include <linux/blkdev.h>
+#include <linux/string.h>
 
 #include "dir.h"
 #include "mft.h"
@@ -624,7 +625,6 @@ static inline int ntfs_filldir(struct ntfs_volume *vol,
 		ntfs_debug("Skipping hidden file.");
 		return 0;
 	}
-
 	name_len = ntfs_ucstonls(vol, (__le16 *)&ie->key.file_name.file_name,
 			ie->key.file_name.file_name_length, &name,
 			NTFS_MAX_NAME_LEN * NLS_MAX_CHARSET_SIZE + 1);
@@ -633,7 +633,10 @@ static inline int ntfs_filldir(struct ntfs_volume *vol,
 				(long long)MREF_LE(ie->data.dir.indexed_file));
 		return 0;
 	}
-
+	if (NVolStreamsWindows(vol) && strchr((char *)name, ':')) {
+		ntfs_debug("Skipping file name containing a stream separator.");
+		return 0;
+	}
 	mref = MREF_LE(ie->data.dir.indexed_file);
 	if (ie->key.file_name.file_attributes & FILE_ATTR_REPARSE_POINT)
 		dt_type = ntfs_reparse_tag_dt_types(vol, mref);
diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c
index b4fcfbe2da4c..c6845bc8a675 100644
--- a/fs/ntfs/ea.c
+++ b/fs/ntfs/ea.c
@@ -246,7 +246,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
 			goto out;
 
 		if (ntfs_attr_exist(ni, AT_EA, AT_UNNAMED, 0)) {
-			err = ntfs_attr_remove(ni, AT_EA, AT_UNNAMED, 0);
+			err = ntfs_attr_remove(ni, AT_EA, AT_UNNAMED, 0, NULL);
 			if (err)
 				goto out;
 		}
@@ -301,11 +301,12 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
 		p_ea_info->ea_query_length = cpu_to_le32(ea_info_qsize);
 
 		if ((flags & XATTR_REPLACE) && !val_size && !ea_info_qsize) {
-			err = ntfs_attr_remove(ni, AT_EA, AT_UNNAMED, 0);
+			err = ntfs_attr_remove(ni, AT_EA, AT_UNNAMED, 0, NULL);
 			if (err)
 				goto out;
 
-			err = ntfs_attr_remove(ni, AT_EA_INFORMATION, AT_UNNAMED, 0);
+			err = ntfs_attr_remove(ni, AT_EA_INFORMATION, AT_UNNAMED,
+					0, NULL);
 			if (err) {
 				/* Restore the original $EA if $EA_INFORMATION removal failed. */
 				ntfs_attr_add(ni, AT_EA, AT_UNNAMED, 0, old_ea_buf,
@@ -610,6 +611,14 @@ static int ntfs_getxattr(const struct xattr_handler *handler,
 	struct ntfs_inode *ni = NTFS_I(inode);
 	int err;
 
+	/*
+	 * Stream permissions and privileges belong to the base inode. This
+	 * also lets VFS killpriv helpers find the base security attributes.
+	 */
+	if (ntfs_inode_is_named_stream(ni)) {
+		ni = ni->ext.base_ntfs_ino;
+		inode = VFS_I(ni);
+	}
 	if (NVolShutdown(ni->vol))
 		return -EIO;
 
@@ -716,8 +725,8 @@ static int ntfs_new_attr_flags(struct ntfs_inode *ni, __le32 fattr)
 			goto err_out;
 
 		ntfs_attr_reinit_search_ctx(ctx);
-		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) {
 			err = -EINVAL;
@@ -883,6 +892,8 @@ static int ntfs_setxattr(const struct xattr_handler *handler,
 	int err;
 	__le32 fattr;
 
+	if (ntfs_inode_is_named_stream(ni))
+		return -EOPNOTSUPP;
 	if (NVolShutdown(ni->vol))
 		return -EIO;
 
diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
index 7818d88b2133..a2e843cf947c 100644
--- a/fs/ntfs/file.c
+++ b/fs/ntfs/file.c
@@ -17,8 +17,10 @@
 #include <linux/falloc.h>
 #include <linux/file.h>
 #include <linux/filelock.h>
+#include <linux/list.h>
 #include <linux/overflow.h>
 #include <linux/security.h>
+#include <linux/slab.h>
 #include <uapi/linux/ntfs.h>
 
 #include "lcnalloc.h"
@@ -142,6 +144,11 @@ int ntfs_file_release(struct inode *vi, struct file *filp)
 	return 0;
 }
 
+struct ntfs_fsync_attr {
+	struct list_head list;
+	struct inode *inode;
+};
+
 /*
  * ntfs_file_fsync - sync a file to disk
  * @filp:	file to be synced
@@ -171,6 +178,8 @@ 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;
+	struct ntfs_fsync_attr *attr, *next;
+	LIST_HEAD(attrs);
 	bool non_resident, stream;
 
 	ntfs_debug("Entering for inode 0x%llx.", ni->mft_no);
@@ -195,8 +204,7 @@ int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end,
 
 	/*
 	 * 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.
+	 * A stream fsync does not need to flush sibling attribute mappings.
 	 */
 	if (stream)
 		goto sync_volume;
@@ -232,22 +240,53 @@ int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end,
 			name = (__le16 *)((u8 *)ctx->attr + le16_to_cpu(ctx->attr->name_offset));
 			if (ctx->attr->type == AT_DATA && ctx->attr->name_length == 0)
 				continue;
+			if (ctx->attr->type == AT_DATA &&
+			    ntfs_stream_unlinked(ni, name, ctx->attr->name_length))
+				continue;
 
+			attr = kmalloc_obj(*attr, GFP_NOFS);
+			if (!attr) {
+				err = -ENOMEM;
+				break;
+			}
 			attr_vi = ntfs_attr_iget(vi, ctx->attr->type,
 						 name, ctx->attr->name_length);
-			if (IS_ERR(attr_vi))
+			if (IS_ERR(attr_vi)) {
+				kfree(attr);
 				continue;
-			spin_lock(&attr_vi->i_lock);
-			if (inode_state_read_once(attr_vi) & I_DIRTY_PAGES) {
-				spin_unlock(&attr_vi->i_lock);
-				filemap_write_and_wait(attr_vi->i_mapping);
-			} else
-				spin_unlock(&attr_vi->i_lock);
-			iput(attr_vi);
+			}
+			if (ntfs_inode_is_named_stream(NTFS_I(attr_vi)))
+				atomic_inc(&NTFS_I(attr_vi)->stream_open_count);
+			attr->inode = attr_vi;
+			list_add_tail(&attr->list, &attrs);
 		}
 	}
 	mutex_unlock(&ni->mrec_lock);
 	ntfs_attr_put_search_ctx(ctx);
+	if (err != -ENOENT && !ret)
+		ret = err;
+
+	/* Attribute writeback takes the base inode's MFT record lock. */
+	list_for_each_entry_safe(attr, next, &attrs, list) {
+		struct inode *attr_vi = attr->inode;
+
+		err = filemap_write_and_wait(attr_vi->i_mapping);
+		if (err && !ret)
+			ret = err;
+		if (ntfs_inode_is_named_stream(NTFS_I(attr_vi))) {
+			err = ntfs_stream_put(attr_vi);
+			if (err && !ret)
+				ret = err;
+		}
+		iput(attr_vi);
+		list_del(&attr->list);
+		kfree(attr);
+	}
+
+	/* Attribute writeback may have updated the base mapping pairs. */
+	err = write_inode_now(vi, 1);
+	if (err && !ret)
+		ret = err;
 
 sync_volume:
 	write_inode_now(vol->mftbmp_ino, 1);
@@ -295,7 +334,6 @@ 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;
@@ -333,7 +371,6 @@ int ntfs_setattr_size(struct inode *vi, struct iattr *attr)
 		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);
 	}
@@ -361,14 +398,10 @@ int ntfs_setattr_size(struct inode *vi, struct iattr *attr)
 		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);
+	if (!err && stream)
+		ntfs_stream_update_base_time(base_ni);
 
 	return err;
 }
@@ -500,6 +533,24 @@ int ntfs_getattr(struct mnt_idmap *idmap, const struct path *path,
 	return 0;
 }
 
+/*
+ * Validate a stream before VFS write handling removes privileges or updates
+ * timestamps on its base inode.
+ */
+static int ntfs_file_modified(struct kiocb *iocb)
+{
+	struct inode *inode = file_inode(iocb->ki_filp);
+	int err;
+
+	if (ntfs_inode_is_named_stream(NTFS_I(inode))) {
+		err = ntfs_stream_validate_for_mutation(inode,
+				iocb->ki_flags & IOCB_NOWAIT);
+		if (err)
+			return err;
+	}
+	return kiocb_modified(iocb);
+}
+
 loff_t ntfs_file_llseek(struct file *file, loff_t offset, int whence)
 {
 	struct inode *inode = file->f_mapping->host;
@@ -710,7 +761,7 @@ ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
 	if (ret <= 0)
 		goto out_lock;
 
-	err = file_modified(iocb->ki_filp);
+	err = ntfs_file_modified(iocb);
 	if (err) {
 		ret = err;
 		goto out_lock;
@@ -788,13 +839,24 @@ static vm_fault_t ntfs_filemap_page_mkwrite(struct vm_fault *vmf)
 {
 	struct inode *inode = file_inode(vmf->vma->vm_file);
 	struct address_space *mapping = inode->i_mapping;
+	bool stream = ntfs_inode_is_named_stream(NTFS_I(inode));
+	int err;
 	vm_fault_t ret;
 
 	if (NInoWofCompressed(NTFS_I(inode)))
 		return VM_FAULT_SIGBUS;
 
 	sb_start_pagefault(inode->i_sb);
-	file_update_time(vmf->vma->vm_file);
+	if (stream) {
+		err = ntfs_stream_validate_for_mutation(inode, false);
+		if (err)
+			goto out_error;
+		err = file_update_time(vmf->vma->vm_file);
+		if (err)
+			goto out_error;
+	} else {
+		file_update_time(vmf->vma->vm_file);
+	}
 
 	/*
 	 * Serialize against truncate/fallocate which hold the lock
@@ -805,6 +867,10 @@ static vm_fault_t ntfs_filemap_page_mkwrite(struct vm_fault *vmf)
 	filemap_invalidate_unlock_shared(mapping);
 	sb_end_pagefault(inode->i_sb);
 	return ret;
+
+out_error:
+	sb_end_pagefault(inode->i_sb);
+	return vmf_error(err);
 }
 
 static const struct vm_operations_struct ntfs_file_vm_ops = {
@@ -1281,6 +1347,13 @@ long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t len)
 		inode_unlock(vi);
 		return -EOPNOTSUPP;
 	}
+	if (stream) {
+		err = ntfs_stream_validate_for_mutation(vi, false);
+		if (err) {
+			inode_unlock(vi);
+			return err;
+		}
+	}
 	old_size = i_size_read(vi);
 
 	inode_dio_wait(vi);
@@ -1322,9 +1395,7 @@ long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t len)
 
 	if (!err) {
 		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));
+			ntfs_stream_update_base_time(base_ni);
 		} else {
 			NInoSetFileNameDirty(ni);
 			inode_set_mtime_to_ts(vi, inode_set_ctime_current(vi));
diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index ed2cb7e9e5bb..9d7bb55edfc4 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -1546,6 +1546,10 @@ static int ntfs_read_locked_attr_inode(struct inode *base_vi, struct inode *vi)
 		vi->i_blocks = ni->itype.compressed.size >> 9;
 	else
 		vi->i_blocks = ni->allocated_size >> 9;
+	if (ni->type == AT_DATA && ni->name_len) {
+		vi->i_op = &ntfs_stream_inode_ops;
+		vi->i_fop = &ntfs_stream_file_ops;
+	}
 	/*
 	 * Make sure the base inode does not go away and attach it to the
 	 * attribute inode.
@@ -2536,6 +2540,10 @@ int ntfs_show_options(struct seq_file *sf, struct dentry *root)
 		seq_puts(sf, ",symlink=native");
 	else
 		seq_puts(sf, ",symlink=wsl");
+	if (NVolStreamsWindows(vol))
+		seq_puts(sf, ",streams_interface=windows");
+	else
+		seq_puts(sf, ",streams_interface=none");
 	if (vol->sb->s_flags & SB_POSIXACL)
 		seq_puts(sf, ",acl");
 	return 0;
@@ -2584,15 +2592,19 @@ int ntfs_extend_initialized_size(struct inode *vi, const loff_t offset,
 int ntfs_truncate_vfs(struct inode *vi, loff_t new_size, loff_t i_size)
 {
 	struct ntfs_inode *ni = NTFS_I(vi);
+	struct ntfs_inode *mrec_ni = ntfs_base_inode(ni);
 	int err;
 
-	mutex_lock(&ni->mrec_lock);
+	mutex_lock(&mrec_ni->mrec_lock);
 	err = __ntfs_attr_truncate_vfs(ni, new_size, i_size);
-	mutex_unlock(&ni->mrec_lock);
+	mutex_unlock(&mrec_ni->mrec_lock);
 	if (err < 0)
 		return err;
 
-	inode_set_mtime_to_ts(vi, inode_set_ctime_current(vi));
+	if (ntfs_inode_is_named_stream(ni))
+		ntfs_stream_update_base_time(mrec_ni);
+	else
+		inode_set_mtime_to_ts(vi, inode_set_ctime_current(vi));
 	return 0;
 }
 
diff --git a/fs/ntfs/iomap.c b/fs/ntfs/iomap.c
index b4475963e57a..cfc42d82e41b 100644
--- a/fs/ntfs/iomap.c
+++ b/fs/ntfs/iomap.c
@@ -90,11 +90,7 @@ static int ntfs_read_iomap_begin_resident(struct inode *inode, loff_t offset, lo
 	int err = 0;
 	char *kattr;
 
-	if (NInoAttr(ni))
-		base_ni = ni->ext.base_ntfs_ino;
-	else
-		base_ni = ni;
-
+	base_ni = ntfs_base_inode(ni);
 	mutex_lock(&base_ni->mrec_lock);
 
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
@@ -140,7 +136,8 @@ static int ntfs_read_iomap_begin_resident(struct inode *inode, loff_t offset, lo
 	if (ctx)
 		ntfs_attr_put_search_ctx(ctx);
 
-	if (!err && keep_mrec_lock && iomap->type == IOMAP_INLINE) {
+	if (!err && keep_mrec_lock &&
+	    iomap->type == IOMAP_INLINE) {
 		iomap->private = base_ni;
 		return 0;
 	}
@@ -390,6 +387,8 @@ static int ntfs_write_simple_iomap_begin_non_resident(struct inode *inode, loff_
 						      loff_t length, struct iomap *iomap)
 {
 	struct ntfs_inode *ni = NTFS_I(inode);
+	struct ntfs_inode *mrec_ni = ntfs_inode_is_named_stream(ni) ?
+			ntfs_base_inode(ni) : ni;
 	struct ntfs_volume *vol = ni->vol;
 	loff_t vcn_ofs, rl_length;
 	struct runlist_element *rl, *rlc;
@@ -408,7 +407,7 @@ static int ntfs_write_simple_iomap_begin_non_resident(struct inode *inode, loff_
 		up_read(&ni->runlist.lock);
 		err = ntfs_map_runlist(ni, vcn);
 		if (err) {
-			mutex_unlock(&ni->mrec_lock);
+			mutex_unlock(&mrec_ni->mrec_lock);
 			return -ENOENT;
 		}
 		down_read(&ni->runlist.lock);
@@ -422,7 +421,7 @@ static int ntfs_write_simple_iomap_begin_non_resident(struct inode *inode, loff_
 	rl = __ntfs_attr_find_vcn_nolock(&ni->runlist, vcn);
 	if (IS_ERR(rl)) {
 		up_write(&ni->runlist.lock);
-		mutex_unlock(&ni->mrec_lock);
+		mutex_unlock(&mrec_ni->mrec_lock);
 		return -EIO;
 	}
 	lcn = ntfs_rl_vcn_to_lcn(rl, vcn);
@@ -453,7 +452,7 @@ static int ntfs_write_simple_iomap_begin_non_resident(struct inode *inode, loff_
 				"runlist(vcn : %lld, length : %lld) is corrupted\n",
 				rl->vcn, rl->length);
 		up_write(&ni->runlist.lock);
-		mutex_unlock(&ni->mrec_lock);
+		mutex_unlock(&mrec_ni->mrec_lock);
 		return -EIO;
 	}
 
@@ -467,7 +466,7 @@ static int ntfs_write_simple_iomap_begin_non_resident(struct inode *inode, loff_
 			if (max_clu_count < 0) {
 				err = max_clu_count;
 				up_write(&ni->runlist.lock);
-				mutex_unlock(&ni->mrec_lock);
+				mutex_unlock(&mrec_ni->mrec_lock);
 				return err;
 			}
 		}
@@ -482,7 +481,7 @@ static int ntfs_write_simple_iomap_begin_non_resident(struct inode *inode, loff_
 					GFP_NOFS);
 			if (!rlc) {
 				up_write(&ni->runlist.lock);
-				mutex_unlock(&ni->mrec_lock);
+				mutex_unlock(&mrec_ni->mrec_lock);
 				return -ENOMEM;
 			}
 
@@ -499,7 +498,7 @@ static int ntfs_write_simple_iomap_begin_non_resident(struct inode *inode, loff_
 			if (IS_ERR(rl)) {
 				ntfs_error(vol->sb, "Failed to merge runlists");
 				up_write(&ni->runlist.lock);
-				mutex_unlock(&ni->mrec_lock);
+				mutex_unlock(&mrec_ni->mrec_lock);
 				kvfree(rlc);
 				return PTR_ERR(rl);
 			}
@@ -509,7 +508,7 @@ static int ntfs_write_simple_iomap_begin_non_resident(struct inode *inode, loff_
 			ni->i_dealloc_clusters += max_clu_count;
 		}
 		up_write(&ni->runlist.lock);
-		mutex_unlock(&ni->mrec_lock);
+		mutex_unlock(&mrec_ni->mrec_lock);
 
 		if (lcn < LCN_DELALLOC)
 			ntfs_hold_dirty_clusters(vol, max_clu_count);
@@ -561,7 +560,7 @@ static int ntfs_write_simple_iomap_begin_non_resident(struct inode *inode, loff_
 		}
 	} else {
 		up_write(&ni->runlist.lock);
-		mutex_unlock(&ni->mrec_lock);
+		mutex_unlock(&mrec_ni->mrec_lock);
 
 		iomap->type = IOMAP_MAPPED;
 		iomap->addr = ntfs_cluster_to_bytes(vol, lcn) + vcn_ofs;
@@ -586,6 +585,8 @@ static int ntfs_write_da_iomap_begin_non_resident(struct inode *inode,
 		struct iomap *iomap, int ntfs_iomap_flags)
 {
 	struct ntfs_inode *ni = NTFS_I(inode);
+	struct ntfs_inode *mrec_ni = ntfs_inode_is_named_stream(ni) ?
+			ntfs_base_inode(ni) : ni;
 	struct ntfs_volume *vol = ni->vol;
 	loff_t vcn_ofs, rl_length;
 	s64 vcn, start_lcn, lcn_count;
@@ -604,7 +605,7 @@ static int ntfs_write_da_iomap_begin_non_resident(struct inode *inode,
 			max_clu_count, &balloc, update_mp,
 			ntfs_iomap_flags & NTFS_IOMAP_FLAGS_WRITEBACK);
 	up_write(&ni->runlist.lock);
-	mutex_unlock(&ni->mrec_lock);
+	mutex_unlock(&mrec_ni->mrec_lock);
 	if (err) {
 		ni->i_dealloc_clusters = 0;
 		return err;
@@ -658,8 +659,8 @@ static int ntfs_write_da_iomap_begin_non_resident(struct inode *inode,
 
 	if (ntfs_iomap_flags & NTFS_IOMAP_FLAGS_MKWRITE &&
 	    iomap->offset + iomap->length > ni->initialized_size) {
-		err = ntfs_attr_set_initialized_size(ni, iomap->offset +
-				iomap->length);
+		err = ntfs_attr_set_initialized_size(ni,
+				iomap->offset + iomap->length);
 	}
 
 	return err;
@@ -669,13 +670,15 @@ static int ntfs_write_iomap_begin_resident(struct inode *inode, loff_t offset,
 		struct iomap *iomap)
 {
 	struct ntfs_inode *ni = NTFS_I(inode);
+	struct ntfs_inode *mrec_ni = ntfs_inode_is_named_stream(ni) ?
+			ntfs_base_inode(ni) : ni;
 	struct attr_record *a;
-	struct ntfs_attr_search_ctx *ctx;
+	struct ntfs_attr_search_ctx *ctx = NULL;
 	u32 attr_len;
 	int err = 0;
 	char *kattr;
 
-	ctx = ntfs_attr_get_search_ctx(ni, NULL);
+	ctx = ntfs_attr_get_search_ctx(mrec_ni, NULL);
 	if (!ctx) {
 		err = -ENOMEM;
 		goto out;
@@ -698,13 +701,14 @@ static int ntfs_write_iomap_begin_resident(struct inode *inode, loff_t offset,
 	iomap->inline_data = kattr;
 	iomap->offset = 0;
 	iomap->length = attr_len;
+	iomap->private = mrec_ni;
 
 out:
 	if (ctx)
 		ntfs_attr_put_search_ctx(ctx);
 
 	if (err)
-		mutex_unlock(&ni->mrec_lock);
+		mutex_unlock(&mrec_ni->mrec_lock);
 
 	return err;
 }
@@ -713,7 +717,12 @@ static int ntfs_write_iomap_begin_non_resident(struct inode *inode, loff_t offse
 					       loff_t length, unsigned int flags,
 					       struct iomap *iomap, int ntfs_iomap_flags)
 {
-	mutex_lock(&NTFS_I(inode)->mrec_lock);
+	struct ntfs_inode *ni = NTFS_I(inode);
+	struct ntfs_inode *mrec_ni = ntfs_inode_is_named_stream(ni) ?
+			ntfs_base_inode(ni) : ni;
+
+	mutex_lock(&mrec_ni->mrec_lock);
+
 	if (ntfs_iomap_flags & NTFS_IOMAP_FLAGS_BEGIN)
 		return  ntfs_write_simple_iomap_begin_non_resident(inode, offset,
 								   length, iomap);
@@ -729,12 +738,15 @@ static int __ntfs_write_iomap_begin(struct inode *inode, loff_t offset,
 				    struct iomap *iomap, int ntfs_iomap_flags)
 {
 	struct ntfs_inode *ni = NTFS_I(inode);
+	struct ntfs_inode *mrec_ni;
 
 	if (NVolShutdown(ni->vol))
 		return -EIO;
 
 	if (!NInoNonResident(ni)) {
-		mutex_lock(&ni->mrec_lock);
+		mrec_ni = ntfs_inode_is_named_stream(ni) ?
+				ntfs_base_inode(ni) : ni;
+		mutex_lock(&mrec_ni->mrec_lock);
 		return ntfs_write_iomap_begin_resident(inode, offset, iomap);
 	}
 	return  ntfs_write_iomap_begin_non_resident(inode, offset, length, flags,
@@ -753,10 +765,10 @@ static int ntfs_write_iomap_end_resident(struct inode *inode, loff_t pos,
 					 loff_t length, ssize_t written,
 					 unsigned int flags, struct iomap *iomap)
 {
-	struct ntfs_inode *ni = NTFS_I(inode);
+	struct ntfs_inode *base_ni = iomap->private;
 
-	mark_mft_record_dirty(ni);
-	mutex_unlock(&ni->mrec_lock);
+	mark_mft_record_dirty(base_ni);
+	mutex_unlock(&base_ni->mrec_lock);
 	return written;
 }
 
diff --git a/fs/ntfs/named_stream.c b/fs/ntfs/named_stream.c
index f18312db9859..0c547a35cf2d 100644
--- a/fs/ntfs/named_stream.c
+++ b/fs/ntfs/named_stream.c
@@ -6,6 +6,7 @@
  */
 
 #include <linux/file.h>
+#include <linux/namei.h>
 #include <linux/overflow.h>
 
 #include <uapi/linux/ntfs.h>
@@ -176,6 +177,594 @@ bool ntfs_stream_unlinked(struct ntfs_inode *base_ni,
 	return unlinked;
 }
 
+/*
+ * ntfs_stream_path_parse() - Split a pathname stream component
+ * @vol: NTFS volume
+ * @qname: final pathname component
+ * @path: receives slices of @qname on success
+ *
+ * Parse the ``file:stream`` form when the Windows pathname interface is
+ * enabled. This does not convert or validate the component names.
+ *
+ * Return: 1 if stream syntax was parsed, 0 if streams are disabled or no
+ * stream separator is present, or -EINVAL for malformed syntax.
+ */
+int ntfs_stream_path_parse(struct ntfs_volume *vol,
+		const struct qstr *qname, struct ntfs_stream_path *path)
+{
+	const unsigned char *colon;
+
+	if (!NVolStreamsWindows(vol))
+		return 0;
+
+	colon = memchr(qname->name, ':', qname->len);
+	if (!colon)
+		return 0;
+	if (colon == qname->name || colon + 1 == qname->name + qname->len)
+		return -EINVAL;
+	if (memchr(colon + 1, ':', qname->name + qname->len - colon - 1))
+		return -EINVAL;
+
+	path->base_name = (const char *)qname->name;
+	path->base_len = colon - qname->name;
+	path->stream_name = (const char *)colon + 1;
+	path->stream_len = qname->name + qname->len - colon - 1;
+	return 1;
+}
+
+/*
+ * ntfs_stream_path_has_colon() - Check for a pathname stream separator
+ * @vol: NTFS volume
+ * @qname: final pathname component
+ *
+ * Return: true if the Windows pathname interface is enabled and @qname
+ * contains a colon, or false otherwise.
+ */
+bool ntfs_stream_path_has_colon(struct ntfs_volume *vol,
+		const struct qstr *qname)
+{
+	return NVolStreamsWindows(vol) &&
+		memchr(qname->name, ':', qname->len);
+}
+
+/*
+ * ntfs_stream_path_check() - Reject stream syntax for unsupported operations
+ * @vol: NTFS volume
+ * @qname: final pathname component
+ *
+ * Return: 0 for an ordinary name or when streams are disabled,
+ * -EOPNOTSUPP for parsed stream syntax, or -EINVAL for malformed syntax.
+ */
+int ntfs_stream_path_check(struct ntfs_volume *vol,
+		const struct qstr *qname)
+{
+	struct ntfs_stream_path path;
+	int ret;
+
+	ret = ntfs_stream_path_parse(vol, qname, &path);
+	if (ret < 0)
+		return ret;
+	return ret ? -EOPNOTSUPP : 0;
+}
+
+/*
+ * ntfs_stream_lookup_inode_by_name() - Look up a base inode by name
+ * @dir_ino: directory containing the base file or directory
+ * @uname: UTF-16LE base name
+ * @uname_len: Length of @uname in UTF-16 code units
+ *
+ * Resolve the directory entry and verify its MFT reference before returning
+ * the inode.
+ *
+ * Return: Referenced inode on success, or ERR_PTR() on failure.
+ */
+struct inode *ntfs_stream_lookup_inode_by_name(struct inode *dir_ino,
+		__le16 *uname, int uname_len)
+{
+	struct ntfs_volume *vol = NTFS_SB(dir_ino->i_sb);
+	struct ntfs_name *name = NULL;
+	struct inode *inode;
+	u64 mref;
+
+	mutex_lock(&NTFS_I(dir_ino)->mrec_lock);
+	mref = ntfs_lookup_inode_by_name(NTFS_I(dir_ino), uname, uname_len,
+			&name);
+	mutex_unlock(&NTFS_I(dir_ino)->mrec_lock);
+	kfree(name);
+
+	if (IS_ERR_MREF(mref))
+		return ERR_PTR(MREF_ERR(mref));
+
+	inode = ntfs_iget(vol->sb, MREF(mref));
+	if (IS_ERR(inode))
+		return inode;
+	if (MSEQNO(mref) != NTFS_I(inode)->seq_no &&
+	    MREF(mref) != FILE_MFT) {
+		iput(inode);
+		return ERR_PTR(-EIO);
+	}
+	return inode;
+}
+
+/*
+ * Recheck that a stream dentry still names the same base inode and DATA
+ * attribute.
+ */
+static int ntfs_stream_d_revalidate(struct inode *dir,
+		const struct qstr *qname, struct dentry *dentry,
+		unsigned int flags)
+{
+	struct inode *inode = d_inode(dentry);
+	struct ntfs_inode *ni;
+	struct ntfs_stream_path path;
+	struct inode *base_vi;
+	__le16 *base_name = NULL, *stream_name = NULL;
+	int base_len, stream_len, err, ret = 0;
+
+	if (flags & LOOKUP_RCU)
+		return -ECHILD;
+	if (!inode)
+		return 0;
+	if (!inode->i_nlink)
+		return 0;
+	if (NVolShutdown(NTFS_SB(dir->i_sb)))
+		return 0;
+
+	ni = NTFS_I(inode);
+	if (!ntfs_inode_is_named_stream(ni))
+		return 0;
+
+	err = ntfs_stream_path_parse(NTFS_SB(dir->i_sb), qname, &path);
+	if (err <= 0)
+		return 0;
+	base_len = ntfs_nlstoucs(NTFS_SB(dir->i_sb), path.base_name,
+			path.base_len, &base_name, NTFS_MAX_NAME_LEN);
+	if (base_len < 0)
+		goto out;
+	stream_len = ntfs_nlstoucs(NTFS_SB(dir->i_sb), path.stream_name,
+			path.stream_len, &stream_name, NTFS_MAX_NAME_LEN);
+	if (stream_len < 0)
+		goto out;
+	if (ntfs_check_stream_name(stream_name, stream_len))
+		goto out;
+	if (stream_len != ni->name_len ||
+	    !ntfs_names_are_equal(stream_name, stream_len, ni->name,
+			ni->name_len, IGNORE_CASE, ni->vol->upcase,
+			ni->vol->upcase_len))
+		goto out;
+
+	base_vi = ntfs_stream_lookup_inode_by_name(dir, base_name, base_len);
+	if (IS_ERR(base_vi))
+		goto out;
+	if (NTFS_I(base_vi) != ni->ext.base_ntfs_ino) {
+		iput(base_vi);
+		goto out;
+	}
+
+	mutex_lock(&NTFS_I(base_vi)->mrec_lock);
+	err = ntfs_stream_inode_validate(inode);
+	mutex_unlock(&NTFS_I(base_vi)->mrec_lock);
+	iput(base_vi);
+	if (!err)
+		ret = 1;
+out:
+	if (stream_name)
+		kmem_cache_free(ntfs_name_cache, stream_name);
+	if (base_name)
+		kmem_cache_free(ntfs_name_cache, base_name);
+	return ret;
+}
+
+/* Revalidate stream paths while leaving ordinary NTFS dentries cacheable. */
+static int ntfs_dentry_revalidate(struct inode *dir,
+		const struct qstr *qname, struct dentry *dentry,
+		unsigned int flags)
+{
+	struct inode *inode = d_inode(dentry);
+	struct ntfs_volume *vol = NTFS_SB(dir->i_sb);
+
+	if (ntfs_stream_path_has_colon(vol, qname))
+		return ntfs_stream_d_revalidate(dir, qname, dentry, flags);
+	if (inode && ntfs_inode_is_named_stream(NTFS_I(inode)))
+		return 0;
+	return 1;
+}
+
+/* Drop stream-path aliases so future lookups recheck their backing attribute. */
+static int ntfs_dentry_delete(const struct dentry *dentry)
+{
+	struct inode *inode = d_inode(dentry);
+	struct ntfs_volume *vol = NTFS_SB(dentry->d_sb);
+
+	if (ntfs_stream_path_has_colon(vol, &dentry->d_name) ||
+	    (inode && ntfs_inode_is_named_stream(NTFS_I(inode))))
+		return always_delete_dentry(dentry);
+	return 0;
+}
+
+static const struct dentry_operations ntfs_dentry_ops = {
+	.d_revalidate	= ntfs_dentry_revalidate,
+	.d_delete		= ntfs_dentry_delete,
+};
+
+/*
+ * ntfs_set_default_dentry_ops() - Install stream-aware dentry operations
+ * @sb: superblock on which to install the operations
+ *
+ * Ensure pathname stream dentries are revalidated and not kept as stale
+ * aliases.
+ */
+void ntfs_set_default_dentry_ops(struct super_block *sb)
+{
+	set_default_d_op(sb, &ntfs_dentry_ops);
+}
+
+/*
+ * ntfs_lookup_stream() - Look up a named stream by pathname
+ * @dir_ino: directory containing the base file or directory
+ * @dent: dentry for the pathname component
+ * @path: parsed base and stream name slices
+ *
+ * Find the existing base inode and its named DATA attribute, then splice the
+ * stream inode into @dent.
+ *
+ * Return: NULL if @dent was instantiated, an alternate dentry if a
+ * disconnected alias was spliced, or ERR_PTR() on failure.
+ */
+struct dentry *ntfs_lookup_stream(struct inode *dir_ino,
+		struct dentry *dent, const struct ntfs_stream_path *path)
+{
+	struct ntfs_volume *vol = NTFS_SB(dir_ino->i_sb);
+	struct inode *base_vi, *stream_vi;
+	__le16 *base_uname = NULL, *stream_uname = NULL;
+	int base_len, stream_len, err;
+
+	if (NVolShutdown(vol))
+		return ERR_PTR(-EIO);
+
+	base_len = ntfs_nlstoucs(vol, path->base_name, path->base_len,
+			&base_uname, NTFS_MAX_NAME_LEN);
+	if (base_len < 0)
+		return ERR_PTR(base_len);
+
+	stream_len = ntfs_nlstoucs(vol, path->stream_name, path->stream_len,
+			&stream_uname, NTFS_MAX_NAME_LEN);
+	if (stream_len < 0) {
+		kmem_cache_free(ntfs_name_cache, base_uname);
+		return ERR_PTR(stream_len);
+	}
+	err = ntfs_check_stream_name(stream_uname, stream_len);
+	if (err) {
+		kmem_cache_free(ntfs_name_cache, base_uname);
+		kmem_cache_free(ntfs_name_cache, stream_uname);
+		return ERR_PTR(err);
+	}
+
+	base_vi = ntfs_stream_lookup_inode_by_name(dir_ino, base_uname,
+			base_len);
+	kmem_cache_free(ntfs_name_cache, base_uname);
+	if (IS_ERR(base_vi)) {
+		err = PTR_ERR(base_vi);
+		kmem_cache_free(ntfs_name_cache, stream_uname);
+		if (err == -ENOENT)
+			return d_splice_alias(NULL, dent);
+		return ERR_PTR(err);
+	}
+
+	if (!S_ISREG(base_vi->i_mode) && !S_ISDIR(base_vi->i_mode)) {
+		iput(base_vi);
+		kmem_cache_free(ntfs_name_cache, stream_uname);
+		return ERR_PTR(-ENOTDIR);
+	}
+
+	mutex_lock(&NTFS_I(base_vi)->mrec_lock);
+	stream_vi = ntfs_attr_iget(base_vi, AT_DATA, stream_uname, stream_len);
+	if (!IS_ERR(stream_vi)) {
+		if (NInoStreamUnlinked(NTFS_I(stream_vi)))
+			err = -ENOENT;
+		else
+			err = ntfs_stream_inode_validate(stream_vi);
+	}
+	mutex_unlock(&NTFS_I(base_vi)->mrec_lock);
+	if (!IS_ERR(stream_vi) && err) {
+		iput(stream_vi);
+		stream_vi = ERR_PTR(err);
+	}
+	iput(base_vi);
+	kmem_cache_free(ntfs_name_cache, stream_uname);
+
+	if (IS_ERR(stream_vi)) {
+		err = PTR_ERR(stream_vi);
+		if (err == -ENOENT || err == -ESTALE)
+			return d_splice_alias(NULL, dent);
+		return ERR_PTR(err);
+	}
+
+	return d_splice_alias(stream_vi, dent);
+}
+
+/*
+ * ntfs_stream_path_names() - Convert and validate pathname stream names
+ * @vol: NTFS volume
+ * @qname: final pathname component
+ * @base_name: receives the allocated UTF-16LE base name
+ * @base_len: receives the base name length in UTF-16 code units
+ * @stream_name: receives the allocated UTF-16LE stream name
+ * @stream_len: receives the stream name length in UTF-16 code units
+ *
+ * Return: 1 if stream names were produced, 0 if @qname has no stream syntax,
+ * or a negative errno. The caller owns both name buffers on success.
+ */
+int ntfs_stream_path_names(struct ntfs_volume *vol,
+		const struct qstr *qname, __le16 **base_name, int *base_len,
+		__le16 **stream_name, int *stream_len)
+{
+	struct ntfs_stream_path path;
+	int err;
+
+	*base_name = NULL;
+	*stream_name = NULL;
+	err = ntfs_stream_path_parse(vol, qname, &path);
+	if (err <= 0)
+		return err;
+
+	*base_len = ntfs_nlstoucs(vol, path.base_name, path.base_len,
+			base_name, NTFS_MAX_NAME_LEN);
+	if (*base_len < 0)
+		return *base_len;
+
+	*stream_len = ntfs_nlstoucs(vol, path.stream_name, path.stream_len,
+			stream_name, NTFS_MAX_NAME_LEN);
+	if (*stream_len < 0) {
+		kmem_cache_free(ntfs_name_cache, *base_name);
+		*base_name = NULL;
+		return *stream_len;
+	}
+
+	err = ntfs_check_bad_windows_name(vol, *base_name, *base_len);
+	if (err)
+		goto out_free;
+	err = ntfs_check_stream_name(*stream_name, *stream_len);
+	if (err)
+		goto out_free;
+	err = ntfs_check_bad_windows_name(vol, *stream_name, *stream_len);
+	if (err)
+		goto out_free;
+	return 1;
+
+out_free:
+	kmem_cache_free(ntfs_name_cache, *stream_name);
+	kmem_cache_free(ntfs_name_cache, *base_name);
+	*stream_name = NULL;
+	*base_name = NULL;
+	return err;
+}
+
+/*
+ * ntfs_create_named_stream() - Create a named DATA stream
+ * @idmap: mount idmap used for permission checks
+ * @dir: directory containing the base object
+ * @base_name: UTF-16LE base name
+ * @base_len: length of @base_name in UTF-16 code units
+ * @stream_name: UTF-16LE stream name
+ * @stream_len: length of @stream_name in UTF-16 code units
+ *
+ * Create the stream on an existing regular file or directory.
+ *
+ * Return: Referenced stream inode on success, or ERR_PTR() on failure.
+ */
+struct inode *ntfs_create_named_stream(struct mnt_idmap *idmap,
+		struct inode *dir, __le16 *base_name, int base_len,
+		__le16 *stream_name, int stream_len)
+{
+	struct ntfs_inode *base_ni;
+	struct inode *base_vi, *stream_vi;
+	struct inode *rollback_vi = NULL;
+	struct ntfs_attr_search_ctx *ctx;
+	bool stream_created = false;
+	int err, rollback_err;
+
+	base_vi = ntfs_stream_lookup_inode_by_name(dir, base_name, base_len);
+	if (IS_ERR(base_vi))
+		return base_vi;
+	if (!S_ISREG(base_vi->i_mode) && !S_ISDIR(base_vi->i_mode)) {
+		iput(base_vi);
+		return ERR_PTR(-ENOTDIR);
+	}
+	err = inode_permission(idmap, base_vi, MAY_OPEN | MAY_WRITE);
+	if (err)
+		goto out_iput;
+	if (IS_APPEND(base_vi) || IS_IMMUTABLE(base_vi)) {
+		err = -EPERM;
+		goto out_iput;
+	}
+	if (!(NTFS_SB(base_vi->i_sb)->vol_flags & VOLUME_IS_DIRTY)) {
+		err = ntfs_set_volume_flags(NTFS_SB(base_vi->i_sb),
+				VOLUME_IS_DIRTY);
+		if (err)
+			goto out_iput;
+	}
+
+	base_ni = NTFS_I(base_vi);
+	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;
+	}
+	if (IS_APPEND(base_vi) || IS_IMMUTABLE(base_vi)) {
+		err = -EPERM;
+		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, stream_name, stream_len,
+			IGNORE_CASE, 0, NULL, 0, ctx);
+	if (!err) {
+		if (ntfs_stream_unlinked(base_ni, stream_name, stream_len))
+			err = -EBUSY;
+		else
+			err = -EEXIST;
+	} else if (err == -ENOENT) {
+		if (NVolShutdown(base_ni->vol)) {
+			err = -EIO;
+			goto out_put_ctx;
+		}
+		err = ntfs_attr_add(base_ni, AT_DATA, stream_name, stream_len,
+				NULL, 0);
+		if (!err) {
+			stream_created = true;
+			mark_mft_record_dirty(base_ni);
+		}
+	}
+	ntfs_attr_put_search_ctx(ctx);
+	if (err)
+		goto out_unlock;
+
+	stream_vi = ntfs_attr_iget(base_vi, AT_DATA, stream_name, stream_len);
+	if (!IS_ERR(stream_vi))
+		err = ntfs_stream_inode_validate(stream_vi);
+	else
+		err = PTR_ERR(stream_vi);
+	if (err) {
+		if (stream_created) {
+			rollback_err = ntfs_attr_remove(base_ni, AT_DATA,
+					stream_name, stream_len, &rollback_vi);
+			if (rollback_err) {
+				ntfs_error(base_ni->vol->sb,
+					"Failed to roll back named stream creation.\n");
+				if (rollback_err == -ENOMEM ||
+				    rollback_err == -EINTR ||
+				    rollback_err == -ERESTARTSYS) {
+					err = rollback_err;
+				} else {
+					NVolSetErrors(base_ni->vol);
+					NVolSetShutdown(base_ni->vol);
+					err = -EIO;
+				}
+			}
+		}
+		mutex_unlock(&base_ni->mrec_lock);
+		if (stream_created)
+			ntfs_stream_update_base_time(base_ni);
+		if (!IS_ERR(stream_vi))
+			iput(stream_vi);
+		if (rollback_vi)
+			iput(rollback_vi);
+		iput(base_vi);
+		return ERR_PTR(err);
+	}
+	mutex_unlock(&base_ni->mrec_lock);
+	if (stream_created)
+		ntfs_stream_update_base_time(base_ni);
+	iput(base_vi);
+	return stream_vi;
+
+out_put_ctx:
+	ntfs_attr_put_search_ctx(ctx);
+out_unlock:
+	mutex_unlock(&base_ni->mrec_lock);
+out_iput:
+	iput(base_vi);
+	return ERR_PTR(err);
+}
+
+/*
+ * ntfs_unlink_named_stream() - Remove a pathname named stream
+ * @dir: directory containing the base object
+ * @dentry: dentry for the named stream
+ *
+ * Return: 0 on success, or a negative errno.
+ */
+int ntfs_unlink_named_stream(struct inode *dir, struct dentry *dentry)
+{
+	struct ntfs_volume *vol = NTFS_SB(dir->i_sb);
+	struct inode *base_vi;
+	__le16 *base_name = NULL, *stream_name = NULL;
+	int base_len, stream_len, path_result, err;
+
+	path_result = ntfs_stream_path_names(vol, &dentry->d_name, &base_name,
+			&base_len, &stream_name, &stream_len);
+	if (path_result < 0)
+		return path_result;
+	if (!path_result)
+		return -EINVAL;
+
+	base_vi = ntfs_stream_lookup_inode_by_name(dir, base_name, base_len);
+	if (IS_ERR(base_vi)) {
+		err = PTR_ERR(base_vi);
+		goto out_free;
+	}
+	if (!S_ISREG(base_vi->i_mode) && !S_ISDIR(base_vi->i_mode)) {
+		err = -ENOTDIR;
+		goto out_iput;
+	}
+	if (!ntfs_inode_is_named_stream(NTFS_I(dentry->d_inode))) {
+		err = -ESTALE;
+		goto out_iput;
+	}
+	if (NTFS_I(dentry->d_inode)->ext.base_ntfs_ino != NTFS_I(base_vi)) {
+		err = -ESTALE;
+		goto out_iput;
+	}
+
+	err = ntfs_remove_named_stream(NTFS_I(base_vi), stream_name,
+			stream_len, dentry->d_inode);
+
+out_iput:
+	iput(base_vi);
+out_free:
+	kmem_cache_free(ntfs_name_cache, stream_name);
+	kmem_cache_free(ntfs_name_cache, base_name);
+	return err;
+}
+
+/* Obtain a dentry for the base inode behind a stream inode. */
+static struct dentry *ntfs_stream_base_dentry(struct inode *inode)
+{
+	struct inode *base_vi =
+		VFS_I(NTFS_I(inode)->ext.base_ntfs_ino);
+	struct dentry *dentry;
+
+	dentry = d_find_alias(base_vi);
+	if (dentry)
+		return dentry;
+	if (!igrab(base_vi))
+		return ERR_PTR(-ESTALE);
+	return d_obtain_alias(base_vi);
+}
+
+/*
+ * ntfs_stream_validate_for_mutation() - Validate a stream before mutation
+ * @inode: stream inode to validate
+ * @nowait: do not wait for the base MFT-record lock
+ *
+ * Return: 0 if the backing DATA attribute is valid, -EAGAIN if a nonblocking
+ * lock attempt fails, or another negative errno.
+ */
+int ntfs_stream_validate_for_mutation(struct inode *inode, bool nowait)
+{
+	struct ntfs_inode *base_ni = NTFS_I(inode)->ext.base_ntfs_ino;
+	int err;
+
+	if (nowait) {
+		if (!mutex_trylock(&base_ni->mrec_lock))
+			return -EAGAIN;
+	} else {
+		mutex_lock(&base_ni->mrec_lock);
+	}
+	err = ntfs_stream_inode_validate(inode);
+	mutex_unlock(&base_ni->mrec_lock);
+	return err;
+}
+
 /*
  * Hold a temporary stream reference so unlink cannot remove its attribute
  * while an operation is using it.
@@ -201,6 +790,210 @@ static int ntfs_stream_claim(struct inode *inode)
 	return err;
 }
 
+/* Stream permission checks use the base inode's permissions. */
+static int ntfs_stream_permission(struct mnt_idmap *idmap,
+		struct inode *inode, int mask)
+{
+	return inode_permission(idmap,
+			VFS_I(NTFS_I(inode)->ext.base_ntfs_ino), mask);
+}
+
+/* Report base metadata with the stream's own size and allocation. */
+static int ntfs_stream_getattr(struct mnt_idmap *idmap,
+		const struct path *path, struct kstat *stat,
+		unsigned int request_mask, unsigned int query_flags)
+{
+	struct inode *inode = d_backing_inode(path->dentry);
+	struct ntfs_inode *ni = NTFS_I(inode);
+	struct inode *base_vi = VFS_I(ni->ext.base_ntfs_ino);
+
+	generic_fillattr(idmap, request_mask, base_vi, stat);
+	stat->size = i_size_read(inode);
+	stat->blocks = (((u64)ni->i_dealloc_clusters <<
+			NTFS_SB(inode->i_sb)->cluster_size_bits) >> 9) +
+			inode->i_blocks;
+	stat->blksize = NTFS_SB(inode->i_sb)->cluster_size;
+	stat->result_mask |= STATX_BTIME;
+	stat->btime = NTFS_I(base_vi)->i_crtime;
+
+	if (NInoCompressed(ni))
+		stat->attributes |= STATX_ATTR_COMPRESSED;
+	if (NInoEncrypted(ni))
+		stat->attributes |= STATX_ATTR_ENCRYPTED;
+	if (base_vi->i_flags & S_IMMUTABLE)
+		stat->attributes |= STATX_ATTR_IMMUTABLE;
+	if (base_vi->i_flags & S_APPEND)
+		stat->attributes |= STATX_ATTR_APPEND;
+	stat->attributes_mask |= STATX_ATTR_COMPRESSED | STATX_ATTR_ENCRYPTED |
+			STATX_ATTR_IMMUTABLE | STATX_ATTR_APPEND;
+	stat->mode = (stat->mode & ~S_IFMT) | S_IFREG;
+
+	if (request_mask & STATX_DIOALIGN) {
+		unsigned int align =
+			bdev_logical_block_size(inode->i_sb->s_bdev);
+
+		stat->result_mask |= STATX_DIOALIGN;
+		if (!NInoCompressed(ni) && !NInoEncrypted(ni)) {
+			stat->dio_mem_align = align;
+			stat->dio_offset_align = align;
+		}
+	}
+
+	return 0;
+}
+
+/*
+ * Apply size changes to the stream and route shared inode metadata changes to
+ * its base inode.
+ */
+static int ntfs_stream_setattr_common(struct mnt_idmap *idmap,
+		struct inode *inode, struct iattr *attr)
+{
+	struct ntfs_inode *ni = NTFS_I(inode);
+	struct inode *base_vi = VFS_I(ni->ext.base_ntfs_ino);
+	struct dentry *base_dentry = NULL;
+	struct iattr base_attr = *attr;
+	int err;
+
+	base_attr.ia_valid &= ~ATTR_FILE;
+	base_attr.ia_file = NULL;
+	if (base_attr.ia_valid & (ATTR_KILL_SUID | ATTR_KILL_SGID))
+		base_attr.ia_valid &= ~ATTR_MODE;
+	else if (base_attr.ia_valid & ATTR_MODE)
+		base_attr.ia_mode = (base_attr.ia_mode & ~S_IFMT) |
+				(base_vi->i_mode & S_IFMT);
+
+	if (attr->ia_valid & ATTR_SIZE) {
+		err = inode_permission(idmap, base_vi, MAY_WRITE);
+		if (err)
+			goto out;
+		if (IS_APPEND(base_vi) || IS_IMMUTABLE(base_vi)) {
+			err = -EPERM;
+			goto out;
+		}
+		if (!(ni->vol->vol_flags & VOLUME_IS_DIRTY)) {
+			err = ntfs_set_volume_flags(ni->vol, VOLUME_IS_DIRTY);
+			if (err)
+				goto out;
+		}
+		base_attr.ia_valid &= ~ATTR_SIZE;
+	}
+
+	if ((attr->ia_valid & ATTR_SIZE) || base_attr.ia_valid) {
+		base_dentry = ntfs_stream_base_dentry(inode);
+		if (IS_ERR(base_dentry)) {
+			err = PTR_ERR(base_dentry);
+			base_dentry = NULL;
+			goto out;
+		}
+		inode_lock_nested(base_vi, I_MUTEX_PARENT);
+		err = ntfs_stream_validate_for_mutation(inode, false);
+		if (err) {
+			inode_unlock(base_vi);
+			goto out;
+		}
+		if (base_attr.ia_valid)
+			err = notify_change(idmap, base_dentry, &base_attr,
+					NULL);
+		else
+			err = 0;
+		if (!err)
+			ntfs_stream_inode_refresh(inode);
+		inode_unlock(base_vi);
+		if (err)
+			goto out;
+	}
+
+	if (attr->ia_valid & ATTR_SIZE)
+		err = ntfs_setattr_size(inode, attr);
+	else
+		err = 0;
+out:
+	dput(base_dentry);
+	return err;
+}
+
+/* Hold the stream against unlink while applying VFS setattr operations. */
+static int ntfs_stream_setattr(struct mnt_idmap *idmap,
+		struct dentry *dentry, struct iattr *attr)
+{
+	struct inode *inode = d_inode(dentry);
+	struct ntfs_inode *ni = NTFS_I(inode);
+	bool claimed = false;
+	int err;
+
+	if (NVolShutdown(ni->vol))
+		return -EIO;
+
+	if (!(attr->ia_valid & ATTR_FILE)) {
+		err = ntfs_stream_claim(inode);
+		if (err)
+			return err;
+		claimed = true;
+	}
+
+	err = ntfs_stream_setattr_common(idmap, inode, attr);
+	if (claimed)
+		ntfs_stream_put(inode);
+	return err;
+}
+
+/*
+ * Apply stream timestamp updates to the base inode after validating the
+ * backing attribute under its MFT-record lock. Nonblocking callers get
+ * -EAGAIN.
+ */
+static int ntfs_stream_update_time_common(struct inode *inode,
+		enum fs_update_time type, unsigned int flags)
+{
+	struct ntfs_inode *base_ni = NTFS_I(inode)->ext.base_ntfs_ino;
+	struct inode *base_vi = VFS_I(base_ni);
+	int err;
+
+	if (NVolShutdown(base_ni->vol))
+		return -EIO;
+	if (flags & IOCB_NOWAIT)
+		return -EAGAIN;
+
+	mutex_lock(&base_ni->mrec_lock);
+	if (NVolShutdown(base_ni->vol)) {
+		err = -EIO;
+		goto out_mrec;
+	}
+	err = ntfs_stream_inode_validate(inode);
+	if (!err)
+		err = generic_update_time(base_vi, type, flags);
+out_mrec:
+	mutex_unlock(&base_ni->mrec_lock);
+	return err;
+}
+
+static int ntfs_stream_update_time(struct inode *inode,
+		enum fs_update_time type, unsigned int flags)
+{
+	return ntfs_stream_update_time_common(inode, type, flags);
+}
+
+static ssize_t ntfs_stream_listxattr(struct dentry *dentry, char *buffer,
+		size_t size)
+{
+	return -EOPNOTSUPP;
+}
+
+#ifdef CONFIG_NTFS_FS_POSIX_ACL
+static struct posix_acl *ntfs_stream_get_acl(struct mnt_idmap *idmap,
+		struct dentry *dentry, int type)
+{
+	return ERR_PTR(-EOPNOTSUPP);
+}
+
+static int ntfs_stream_set_acl(struct mnt_idmap *idmap,
+		struct dentry *dentry, struct posix_acl *acl, int type)
+{
+	return -EOPNOTSUPP;
+}
+#endif
+
 /*
  * Remove an unlinked stream's DATA attribute after its final active reference
  * is released. Base-inode deletion handles the attribute when the base is
@@ -348,6 +1141,80 @@ int ntfs_remove_named_stream(struct ntfs_inode *ni, __le16 *uname,
 	return err;
 }
 
+/*
+ * Validate access against the base inode and hold the stream against unlink
+ * until the file is released.
+ */
+static int ntfs_stream_file_open(struct inode *inode, struct file *file)
+{
+	struct ntfs_inode *ni = NTFS_I(inode);
+	struct ntfs_inode *base_ni = ni->ext.base_ntfs_ino;
+	struct inode *base_vi = VFS_I(base_ni);
+	int mask = MAY_OPEN;
+	int err;
+
+	if (file->f_mode & FMODE_READ)
+		mask |= MAY_READ;
+	if (file->f_mode & FMODE_WRITE)
+		mask |= MAY_WRITE;
+	err = inode_permission(file_mnt_idmap(file), base_vi, mask);
+	if (err)
+		return err;
+
+	mutex_lock(&base_ni->mrec_lock);
+	if (NInoStreamUnlinked(ni))
+		err = -ENOENT;
+	else
+		err = ntfs_stream_inode_validate(inode);
+	if (err)
+		goto out_unlock;
+	if ((file->f_mode & FMODE_WRITE) &&
+	    (IS_IMMUTABLE(base_vi) ||
+	     (IS_APPEND(base_vi) && !(file->f_flags & O_APPEND)))) {
+		err = -EPERM;
+		goto out_unlock;
+	}
+	if ((file->f_flags & O_TRUNC) && IS_APPEND(base_vi)) {
+		err = -EPERM;
+		goto out_unlock;
+	}
+	if ((file->f_flags & O_NOATIME) &&
+	    !inode_owner_or_capable(file_mnt_idmap(file), base_vi)) {
+		err = -EPERM;
+		goto out_unlock;
+	}
+	err = ntfs_file_open(inode, file);
+	if (err)
+		goto out_unlock;
+	if (file->f_mode & FMODE_WRITE) {
+		err = get_write_access(base_vi);
+		if (err)
+			goto out_unlock;
+		file->private_data = base_vi;
+	}
+
+	atomic_inc(&ni->stream_open_count);
+out_unlock:
+	mutex_unlock(&base_ni->mrec_lock);
+	return err;
+}
+
+/* Drop open-time references and complete any deferred stream unlink. */
+static int ntfs_stream_file_release(struct inode *inode, struct file *file)
+{
+	int err, remove_err;
+
+	err = ntfs_file_release(inode, file);
+	if (file->private_data) {
+		put_write_access(file->private_data);
+		file->private_data = NULL;
+	}
+	remove_err = ntfs_stream_put(inode);
+	if (!err)
+		err = remove_err;
+	return err;
+}
+
 enum ntfs_stream_ioctl_op {
 	NTFS_STREAM_IOCTL_READ,
 	NTFS_STREAM_IOCTL_WRITE,
@@ -617,6 +1484,9 @@ static long ntfs_ioctl_stream(struct file *filp, unsigned long arg,
 			file_accessed(filp);
 	} else {
 		inode_lock(stream_vi);
+		inode_dio_wait(stream_vi);
+		/* Exclude faults before taking MFT record and folio locks. */
+		filemap_invalidate_lock(stream_vi->i_mapping);
 		err = inode_newsize_ok(stream_vi, pos + data_size);
 		if (!err) {
 			ret = ntfs_inode_attr_pwrite(stream_vi, pos, data_size,
@@ -626,6 +1496,7 @@ static long ntfs_ioctl_stream(struct file *filp, unsigned long arg,
 			else
 				req->bytes_returned = ret;
 		}
+		filemap_invalidate_unlock(stream_vi->i_mapping);
 		inode_unlock(stream_vi);
 	}
 	if (claimed) {
@@ -913,3 +1784,29 @@ int ntfs_ioctl_list_streams(struct file *filp, unsigned long arg)
 	kvfree(kbuf);
 	return ret;
 }
+
+const struct file_operations ntfs_stream_file_ops = {
+	.llseek		= ntfs_file_llseek,
+	.read_iter	= ntfs_file_read_iter,
+	.write_iter	= ntfs_file_write_iter,
+	.fsync		= ntfs_file_fsync,
+	.fallocate	= ntfs_fallocate,
+	.mmap_prepare	= ntfs_file_mmap_prepare,
+	.open		= ntfs_stream_file_open,
+	.release	= ntfs_stream_file_release,
+	.splice_read	= ntfs_file_splice_read,
+	.splice_write	= iter_file_splice_write,
+};
+
+const struct inode_operations ntfs_stream_inode_ops = {
+	.permission	= ntfs_stream_permission,
+	.setattr	= ntfs_stream_setattr,
+	.getattr	= ntfs_stream_getattr,
+	.listxattr	= ntfs_stream_listxattr,
+#ifdef CONFIG_NTFS_FS_POSIX_ACL
+	.get_acl	= ntfs_stream_get_acl,
+	.set_acl	= ntfs_stream_set_acl,
+#endif
+	.update_time	= ntfs_stream_update_time,
+	.fiemap		= ntfs_fiemap,
+};
diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c
index 3cf58befd77f..9251951a76fb 100644
--- a/fs/ntfs/namei.c
+++ b/fs/ntfs/namei.c
@@ -8,6 +8,7 @@
 
 #include <linux/exportfs.h>
 #include <linux/iversion.h>
+#include <linux/namei.h>
 
 #include "ntfs.h"
 #include "time.h"
@@ -15,6 +16,7 @@
 #include "reparse.h"
 #include "object_id.h"
 #include "ea.h"
+#include "stream.h"
 
 static const __le16 aux_name_le[3] = {
 	cpu_to_le16('A'), cpu_to_le16('U'), cpu_to_le16('X')
@@ -57,7 +59,18 @@ static inline int ntfs_check_bad_char(const __le16 *wc, unsigned int wc_len)
 	return 0;
 }
 
-static int ntfs_check_bad_windows_name(struct ntfs_volume *vol,
+/*
+ * ntfs_check_bad_windows_name() - Validate a name against Windows rules
+ * @vol: NTFS volume
+ * @wc: UTF-16LE name
+ * @wc_len: length of @wc in UTF-16 code units
+ *
+ * When Windows-name checks are enabled, reject disallowed characters,
+ * trailing spaces or dots, and reserved DOS device names.
+ *
+ * Return: 0 if valid, or -EINVAL otherwise.
+ */
+int ntfs_check_bad_windows_name(struct ntfs_volume *vol,
 				       const __le16 *wc,
 				       unsigned int wc_len)
 {
@@ -167,19 +180,29 @@ static int ntfs_check_bad_windows_name(struct ntfs_volume *vol,
  *
  * Locking: Caller must hold i_mutex on the directory.
  */
+
 static struct dentry *ntfs_lookup(struct inode *dir_ino, struct dentry *dent,
 		unsigned int flags)
 {
 	struct ntfs_volume *vol = NTFS_SB(dir_ino->i_sb);
+	struct ntfs_stream_path stream_path;
 	struct inode *dent_inode;
 	__le16 *uname;
 	struct ntfs_name *name = NULL;
 	u64 mref;
 	unsigned long dent_ino;
 	int uname_len;
+	int stream_path_len;
 
 	ntfs_debug("Looking up %pd in directory inode 0x%llx.",
 			dent, NTFS_I(dir_ino)->mft_no);
+	stream_path_len = ntfs_stream_path_parse(vol, &dent->d_name,
+			&stream_path);
+	if (stream_path_len < 0)
+		return ERR_PTR(stream_path_len);
+	if (stream_path_len)
+		return ntfs_lookup_stream(dir_ino, dent, &stream_path);
+
 	/* Convert the name of the dentry to Unicode. */
 	uname_len = ntfs_nlstoucs(vol, dent->d_name.name, dent->d_name.len,
 				  &uname, NTFS_MAX_NAME_LEN);
@@ -405,6 +428,7 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d
 	struct inode *vi;
 	struct mft_record *ni_mrec, *dni_mrec;
 	struct super_block *sb = dir_ni->vol->sb;
+	struct inode *rollback_data_vi = NULL, *rollback_sd_vi = NULL;
 	__le64 parent_mft_ref;
 	u64 child_mft_ref;
 	__le16 ea_size;
@@ -694,11 +718,25 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d
 	return ni;
 
 err_out:
-	if (rollback_sd)
-		ntfs_attr_remove(ni, AT_SECURITY_DESCRIPTOR, AT_UNNAMED, 0);
+	if (rollback_sd) {
+		int rollback_err;
 
-	if (rollback_data)
-		ntfs_attr_remove(ni, AT_DATA, AT_UNNAMED, 0);
+		rollback_err = ntfs_attr_remove(ni,
+				AT_SECURITY_DESCRIPTOR, AT_UNNAMED, 0,
+				&rollback_sd_vi);
+		if (rollback_err)
+			ntfs_error(sb,
+				"Failed to roll back security descriptor.\n");
+	}
+
+	if (rollback_data) {
+		int rollback_err;
+
+		rollback_err = ntfs_attr_remove(ni, AT_DATA, AT_UNNAMED,
+				0, &rollback_data_vi);
+		if (rollback_err)
+			ntfs_error(sb, "Failed to roll back DATA attribute.\n");
+	}
 
 	if (rollback_reparse)
 		ntfs_delete_reparse_index(ni);
@@ -726,6 +764,10 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d
 	mutex_unlock(&dir_ni->mrec_lock);
 	mutex_unlock(&ni->mrec_lock);
 
+	if (rollback_data_vi)
+		iput(rollback_data_vi);
+	if (rollback_sd_vi)
+		iput(rollback_sd_vi);
 	remove_inode_hash(vi);
 	discard_new_inode(vi);
 	return ERR_PTR(err);
@@ -736,12 +778,35 @@ static int ntfs_create(struct mnt_idmap *idmap, struct inode *dir,
 {
 	struct ntfs_volume *vol = NTFS_SB(dir->i_sb);
 	struct ntfs_inode *ni;
-	__le16 *uname;
-	int uname_len, err;
+	struct inode *stream_vi = NULL;
+	__le16 *uname, *base_name = NULL, *stream_name = NULL;
+	int uname_len, stream_len, path_result, err;
 
 	if (NVolShutdown(vol))
 		return -EIO;
 
+	path_result = ntfs_stream_path_names(vol, &dentry->d_name, &base_name,
+			&uname_len, &stream_name, &stream_len);
+	if (path_result) {
+		if (path_result < 0)
+			return path_result;
+		stream_vi = ntfs_create_named_stream(idmap, dir, base_name,
+				uname_len, stream_name, stream_len);
+		if (IS_ERR(stream_vi)) {
+			err = PTR_ERR(stream_vi);
+			goto out_free_stream_names;
+		}
+		kmem_cache_free(ntfs_name_cache, stream_name);
+		kmem_cache_free(ntfs_name_cache, base_name);
+		d_instantiate(dentry, stream_vi);
+		return 0;
+
+out_free_stream_names:
+		kmem_cache_free(ntfs_name_cache, stream_name);
+		kmem_cache_free(ntfs_name_cache, base_name);
+		return err;
+	}
+
 	uname_len = ntfs_nlstoucs(vol, dentry->d_name.name, dentry->d_name.len,
 				  &uname, NTFS_MAX_NAME_LEN);
 	if (uname_len < 0) {
@@ -758,7 +823,8 @@ static int ntfs_create(struct mnt_idmap *idmap, struct inode *dir,
 
 	ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
 
-	ni = __ntfs_create(idmap, dir, uname, uname_len, S_IFREG | mode, 0, NULL, 0);
+	ni = __ntfs_create(idmap, dir, uname, uname_len, S_IFREG | mode, 0,
+			NULL, 0);
 	kmem_cache_free(ntfs_name_cache, uname);
 	if (IS_ERR(ni))
 		return PTR_ERR(ni);
@@ -850,10 +916,10 @@ static int ntfs_delete(struct ntfs_inode *ni, struct ntfs_inode *dir_ni,
 	struct file_name_attr *fn = NULL;
 	bool looking_for_dos_name = false, looking_for_win32_name = false;
 	bool case_sensitive_match = true;
+	bool link_count_zero = false;
 	int err = 0;
 	struct mft_record *ni_mrec;
 	struct super_block *sb;
-	bool link_count_zero = false;
 
 	ntfs_debug("Entering.\n");
 
@@ -1025,6 +1091,11 @@ static int ntfs_unlink(struct inode *dir, struct dentry *dentry)
 	if (NVolShutdown(vol))
 		return -EIO;
 
+	if (ntfs_stream_path_has_colon(vol, &dentry->d_name))
+		return ntfs_unlink_named_stream(dir, dentry);
+	if (NInoAttr(ni))
+		return -EOPNOTSUPP;
+
 	uname_len = ntfs_nlstoucs(vol, dentry->d_name.name, dentry->d_name.len,
 				  &uname, NTFS_MAX_NAME_LEN);
 	if (uname_len < 0) {
@@ -1068,6 +1139,10 @@ static struct dentry *ntfs_mkdir(struct mnt_idmap *idmap, struct inode *dir,
 	if (NVolShutdown(vol))
 		return ERR_PTR(-EIO);
 
+	err = ntfs_stream_path_check(vol, &dentry->d_name);
+	if (err)
+		return ERR_PTR(err);
+
 	uname_len = ntfs_nlstoucs(vol, dentry->d_name.name, dentry->d_name.len,
 				  &uname, NTFS_MAX_NAME_LEN);
 	if (uname_len < 0) {
@@ -1084,7 +1159,8 @@ static struct dentry *ntfs_mkdir(struct mnt_idmap *idmap, struct inode *dir,
 
 	ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
 
-	ni = __ntfs_create(idmap, dir, uname, uname_len, mode, 0, NULL, 0);
+	ni = __ntfs_create(idmap, dir, uname, uname_len, mode, 0,
+			NULL, 0);
 	kmem_cache_free(ntfs_name_cache, uname);
 	if (IS_ERR(ni)) {
 		err = PTR_ERR(ni);
@@ -1108,6 +1184,10 @@ static int ntfs_rmdir(struct inode *dir, struct dentry *dentry)
 	if (NVolShutdown(vol))
 		return -EIO;
 
+	err = ntfs_stream_path_check(vol, &dentry->d_name);
+	if (err)
+		return err;
+
 	ni = NTFS_I(vi);
 	uname_len = ntfs_nlstoucs(vol, dentry->d_name.name, dentry->d_name.len,
 				  &uname, NTFS_MAX_NAME_LEN);
@@ -1272,6 +1352,13 @@ static int ntfs_rename(struct mnt_idmap *idmap, struct inode *old_dir,
 	if (NVolShutdown(old_dir_ni->vol))
 		return -EIO;
 
+	err = ntfs_stream_path_check(vol, &old_dentry->d_name);
+	if (err)
+		return err;
+	err = ntfs_stream_path_check(vol, &new_dentry->d_name);
+	if (err)
+		return err;
+
 	if (flags & (RENAME_EXCHANGE | RENAME_WHITEOUT))
 		return -EINVAL;
 
@@ -1419,6 +1506,10 @@ static int ntfs_symlink(struct mnt_idmap *idmap, struct inode *dir,
 	if (NVolShutdown(vol))
 		return -EIO;
 
+	err = ntfs_stream_path_check(vol, &dentry->d_name);
+	if (err)
+		return err;
+
 	usrc_len = ntfs_nlstoucs(vol, dentry->d_name.name,
 				 dentry->d_name.len, &usrc, NTFS_MAX_NAME_LEN);
 	if (usrc_len < 0) {
@@ -1464,6 +1555,10 @@ static int ntfs_mknod(struct mnt_idmap *idmap, struct inode *dir,
 	if (NVolShutdown(vol))
 		return -EIO;
 
+	err = ntfs_stream_path_check(vol, &dentry->d_name);
+	if (err)
+		return err;
+
 	uname_len = ntfs_nlstoucs(vol, dentry->d_name.name,
 			dentry->d_name.len, &uname, NTFS_MAX_NAME_LEN);
 	if (uname_len < 0) {
@@ -1516,6 +1611,13 @@ static int ntfs_link(struct dentry *old_dentry, struct inode *dir,
 	if (NVolShutdown(vol))
 		return -EIO;
 
+	if (NInoAttr(ni))
+		return -EOPNOTSUPP;
+
+	err = ntfs_stream_path_check(vol, &dentry->d_name);
+	if (err)
+		return err;
+
 	uname_len = ntfs_nlstoucs(vol, dentry->d_name.name,
 			dentry->d_name.len, &uname, NTFS_MAX_NAME_LEN);
 	if (uname_len < 0) {
@@ -1669,11 +1771,19 @@ static struct dentry *ntfs_fh_to_parent(struct super_block *sb, struct fid *fid,
 				    ntfs_nfs_get_inode);
 }
 
+static int ntfs_encode_fh(struct inode *inode, u32 *fh, int *max_len,
+		struct inode *parent)
+{
+	if (ntfs_inode_is_named_stream(NTFS_I(inode)))
+		return -EOPNOTSUPP;
+	return generic_encode_ino32_fh(inode, fh, max_len, parent);
+}
+
 /*
  * Export operations allowing NFS exporting of mounted NTFS partitions.
  */
 const struct export_operations ntfs_export_ops = {
-	.encode_fh = generic_encode_ino32_fh,
+	.encode_fh	= ntfs_encode_fh,
 	.get_parent	= ntfs_get_parent,	/* Find the parent of a given directory. */
 	.fh_to_dentry	= ntfs_fh_to_dentry,
 	.fh_to_parent	= ntfs_fh_to_parent,
diff --git a/fs/ntfs/stream.h b/fs/ntfs/stream.h
index da0096d2b751..5cbf6e8f6b68 100644
--- a/fs/ntfs/stream.h
+++ b/fs/ntfs/stream.h
@@ -7,8 +7,38 @@
 struct ntfs_inode;
 struct ntfs_volume;
 
+struct ntfs_stream_path {
+	const char *base_name;
+	unsigned int base_len;
+	const char *stream_name;
+	unsigned int stream_len;
+};
+
+int ntfs_check_bad_windows_name(struct ntfs_volume *vol,
+		const __le16 *name, unsigned int name_len);
 int ntfs_check_stream_name(const __le16 *name, unsigned int name_len);
 
+int ntfs_stream_path_parse(struct ntfs_volume *vol,
+		const struct qstr *qname, struct ntfs_stream_path *path);
+bool ntfs_stream_path_has_colon(struct ntfs_volume *vol,
+		const struct qstr *qname);
+int ntfs_stream_path_check(struct ntfs_volume *vol,
+		const struct qstr *qname);
+int ntfs_stream_path_names(struct ntfs_volume *vol,
+		const struct qstr *qname, __le16 **base_name, int *base_len,
+		__le16 **stream_name, int *stream_len);
+
+struct inode *ntfs_stream_lookup_inode_by_name(struct inode *dir,
+		__le16 *name, int name_len);
+struct dentry *ntfs_lookup_stream(struct inode *dir, struct dentry *dentry,
+		const struct ntfs_stream_path *path);
+struct inode *ntfs_create_named_stream(struct mnt_idmap *idmap,
+		struct inode *dir, __le16 *base_name, int base_len,
+		__le16 *stream_name, int stream_len);
+int ntfs_unlink_named_stream(struct inode *dir, struct dentry *dentry);
+
+void ntfs_set_default_dentry_ops(struct super_block *sb);
+
 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);
@@ -18,11 +48,19 @@ 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);
 
+int ntfs_stream_validate_for_mutation(struct inode *inode, bool nowait);
 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);
 
+extern const struct file_operations ntfs_stream_file_ops;
+extern const struct inode_operations ntfs_stream_inode_ops;
+
+/*
+ * Common file operations used by both ordinary files and named streams.
+ * Their implementations remain in file.c.
+ */
 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,
diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
index 2c6685342999..a88fb935aa9c 100644
--- a/fs/ntfs/super.c
+++ b/fs/ntfs/super.c
@@ -22,6 +22,7 @@
 #include "ntfs.h"
 #include "ea.h"
 #include "volume.h"
+#include "stream.h"
 
 /* A global default upcase table and a corresponding reference count. */
 static __le16 *default_upcase;
@@ -66,6 +67,17 @@ static const struct constant_table ntfs_symlink_enums[] = {
 	{}
 };
 
+enum {
+	STREAMS_INTERFACE_NONE,
+	STREAMS_INTERFACE_WINDOWS,
+};
+
+static const struct constant_table ntfs_streams_interface_enums[] = {
+	{ "none",	STREAMS_INTERFACE_NONE },
+	{ "windows",	STREAMS_INTERFACE_WINDOWS },
+	{}
+};
+
 enum {
 	Opt_uid,
 	Opt_gid,
@@ -91,6 +103,7 @@ enum {
 	Opt_nocase,
 	Opt_native_symlink,
 	Opt_symlink,
+	Opt_streams_interface,
 };
 
 static const struct fs_parameter_spec ntfs_parameters[] = {
@@ -118,6 +131,8 @@ static const struct fs_parameter_spec ntfs_parameters[] = {
 	fsparam_flag("nocase",			Opt_nocase),
 	fsparam_enum("native_symlink",		Opt_native_symlink, ntfs_native_symlink_enums),
 	fsparam_enum("symlink",			Opt_symlink, ntfs_symlink_enums),
+	fsparam_enum("streams_interface",	Opt_streams_interface,
+		     ntfs_streams_interface_enums),
 	{}
 };
 
@@ -254,6 +269,12 @@ static int ntfs_parse_param(struct fs_context *fc, struct fs_parameter *param)
 		else
 			NVolClearSymlinkNative(vol);
 		break;
+	case Opt_streams_interface:
+		if (result.uint_32 == STREAMS_INTERFACE_WINDOWS)
+			NVolSetStreamsWindows(vol);
+		else
+			NVolClearStreamsWindows(vol);
+		break;
 	case Opt_sparse:
 		break;
 	default:
@@ -2586,6 +2607,8 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc)
 	 * operations and associated address space operations to function.
 	 */
 	sb->s_op = &ntfs_sops;
+	if (NVolStreamsWindows(vol))
+		ntfs_set_default_dentry_ops(sb);
 	tmp_ino = new_inode(sb);
 	if (!tmp_ino) {
 		if (!silent)
diff --git a/fs/ntfs/volume.h b/fs/ntfs/volume.h
index 0473b602084c..8ee2a904a96e 100644
--- a/fs/ntfs/volume.h
+++ b/fs/ntfs/volume.h
@@ -195,6 +195,7 @@ struct ntfs_volume {
  * NV_DisableSparse		Disable creation of sparse regions.
  * NV_NativeSymlinkRel		Translate absolute Windows reparse targets (native_symlink=rel).
  * NV_MftBootstrap		Mount is still assembling $MFT's own runlist.
+ * NV_StreamsWindows		Interpret the final path component as file:stream.
  */
 enum {
 	NV_Errors,
@@ -216,6 +217,7 @@ enum {
 	NV_NativeSymlinkRel,
 	NV_SymlinkNative,
 	NV_MftBootstrap,
+	NV_StreamsWindows,
 };
 
 /*
@@ -256,6 +258,7 @@ DEFINE_NVOL_BIT_OPS(DisableSparse)
 DEFINE_NVOL_BIT_OPS(NativeSymlinkRel)
 DEFINE_NVOL_BIT_OPS(SymlinkNative)
 DEFINE_NVOL_BIT_OPS(MftBootstrap)
+DEFINE_NVOL_BIT_OPS(StreamsWindows)
 
 static inline void ntfs_inc_free_clusters(struct ntfs_volume *vol, s64 nr)
 {
diff --git a/fs/ntfs/wof.c b/fs/ntfs/wof.c
index 9847259e5b1a..294dbe5d711f 100644
--- a/fs/ntfs/wof.c
+++ b/fs/ntfs/wof.c
@@ -311,7 +311,8 @@ static int parse_wof_chunk_table(struct ntfs_inode *base_ni,
 			goto out_unlock_mrec;
 		}
 		ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
-				       CASE_SENSITIVE, 0, NULL, 0, ctx);
+				       CASE_SENSITIVE,
+				       0, NULL, 0, ctx);
 		if (ret)
 			goto out_put_ctx;
 
@@ -399,7 +400,8 @@ static int ntfs_read_wof_chunk(struct ntfs_volume *vol,
 	}
 
 	err = ntfs_attr_lookup(wof_ni->type, wof_ni->name, wof_ni->name_len,
-			       CASE_SENSITIVE, 0, NULL, 0, ctx);
+			       CASE_SENSITIVE,
+			       0, NULL, 0, ctx);
 	if (err)
 		goto out_put_ctx;
 
-- 
2.25.1


  parent 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 ` [PATCH v2 1/4] ntfs: add named stream ioctls support Namjae Jeon
2026-10-07  2:07   ` CharSyam
2026-10-07  2:24     ` Namjae Jeon
2026-10-06 22:40 ` Namjae Jeon [this message]
2026-10-07  3:38   ` [PATCH v2 2/4] ntfs: add pathname access for named streams 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-3-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®