Re: [PATCH v2 1/4] ntfs: add named stream ioctls support

From: CharSyam

Date: Tue Oct 06 2026 - 22:13:24 EST


Hi Namjae,

I found an error-reporting issue in NTFS_IOC_STREAM_READ. The underlying
bug predates this patch: ntfs_inode_attr_pread() breaks when
read_mapping_folio() fails without saving PTR_ERR(folio). It therefore
returns the number of bytes read before the error. The new ioctl treats
that nonnegative result as success and reports it in bytes_returned.

An I/O error on the first 4 KiB can appear as a successful zero-byte
read; an error on the second 4 KiB can appear as a successful 4 KiB
read. A caller copying or backing up the stream may interpret either
result as EOF and stop, even though the stream is larger. The on-disk
stream size is unchanged.

I tested this in QEMU with a 16 MiB named stream filled with 'A' and
an 8 KiB read at offset 0. Using blkdebug to inject EIO on the first
4 KiB, the ioctl returned success with bytes_returned=0. Injecting EIO
on the second 4 KiB returned success with bytes_returned=4096. After
the change below, both cases return -EIO and copy no stream data to
userspace. A retry without the injected fault reads all 8192 bytes.
A read crossing the actual EOF still succeeds with a short count of
4096 bytes.

Please propagate the folio error, including when earlier pages in the
same request were read:

folio = read_mapping_folio(mapping, index, NULL);
if (IS_ERR(folio)) {
err = PTR_ERR(folio);
break;
}

The existing `return err ? (s64)err : total;` then returns the error.
The ioctl already copies its temporary kernel buffer to userspace only
on success, so it will not expose partial data on this failure. The
UAPI should also state that a successful short read indicates EOF and
that a read error copies no stream data.

2026년 10월 7일 (수) 오전 7:49, Namjae Jeon <linkinjeon@xxxxxxxxxx>님이 작성:
>
> 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@xxxxxxxxxx>
> ---
> .../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@xxxxxxxxxxxxxxxx>
> 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
>
>