[PATCH 1/1] ntfs: ntfs: add ioctl support for named data streams
Namjae Jeon <[email protected]> Wed, 29 Jul 2026 19:52:59 +0900
| Newsgroups | dev.linux.lists.ntfs,org.kernel.vger.linux-fsdevel,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
Sebastian and Cedric requested NTFS driver support for an interface that WINE can use to access named data streams. NTFS supports multiple named $DATA attributes, commonly known as alternate data streams, but they are not directly accessible through the existing file operations. Add NTFS-specific ioctl commands to let userspace list, read, write, and remove named data streams on an opened file. The new UAPI provides separate commands for each operation: - NTFS_IOC_STREAM_READ - NTFS_IOC_STREAM_WRITE - NTFS_IOC_STREAM_REMOVE - NTFS_IOC_LIST_STREAMS The read and write commands use an offset and length based request format. Writes create the named stream when it does not already exist. The list command returns variable-length stream entries containing stream size, allocated size, and stream name. Stream names use the mounted filesystem NLS by default, or raw UTF-16LE when the UTF-16 flag is requested. Read and write transfers are limited to 16 MiB per request. If the list buffer is too small, the list command returns -ENOSPC while reporting the required size and stream count. Signed-off-by: Namjae Jeon <[email protected]> --- .../userspace-api/ioctl/ioctl-number.rst | 1 + fs/ntfs/file.c | 509 ++++++++++++++++++ include/uapi/linux/ntfs.h | 115 ++++ 3 files changed, 625 insertions(+) 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 3f0ef1e27eb0..2b9869aeb811 100644 --- a/Documentation/userspace-api/ioctl/ioctl-number.rst +++ b/Documentation/userspace-api/ioctl/ioctl-number.rst @@ -399,6 +399,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:[email protected]> 0xF6 all LTTng Linux Trace Toolkit Next Generation diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index d4282822b3ce..961b94f545f2 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -15,6 +15,8 @@ #include <linux/posix_acl_xattr.h> #include <linux/compat.h> #include <linux/falloc.h> +#include <linux/overflow.h> +#include <uapi/linux/ntfs.h> #include "lcnalloc.h" #include "ntfs.h" @@ -23,6 +25,7 @@ #include "iomap.h" #include "bitmap.h" #include "volume.h" +#include "mft.h" #include <linux/filelock.h> @@ -839,6 +842,504 @@ static int ntfs_ioctl_fitrim(struct ntfs_volume *vol, unsigned long arg) return 0; } +#define NTFS_STREAM_MAX_IO (16 * 1024 * 1024) /* 16MB limit */ + +/* + * ntfs_read_named_stream - Read data from a named $DATA stream. + * + * @ni: NTFS inode + * @uname: Stream name + * @uname_len: Length of name in Unicode characters + * @offset: Byte offset in the stream + * @len: Number of bytes to read + * @data_buf: Buffer to copy data to + * + * Return: 0 on success, negative error code otherwise. + * If offset >= stream size, returns 0. + */ +static int ntfs_read_named_stream(struct ntfs_inode *ni, __le16 *uname, + u32 uname_len, u64 offset, u64 len, void *data_buf, + u64 *rbytes) +{ + struct ntfs_attr_search_ctx *ctx; + struct inode *attr_vi; + int err = 0; + u64 data_size; + s64 bytes; + char *rbuf = NULL; + + if (!ni || !uname || uname_len == 0) + return -EINVAL; + + if (len == 0 || len > NTFS_STREAM_MAX_IO) + return -EINVAL; + + mutex_lock(&ni->mrec_lock); + ctx = ntfs_attr_get_search_ctx(ni, NULL); + if (!ctx) { + mutex_unlock(&ni->mrec_lock); + return -ENOMEM; + } + + err = ntfs_attr_lookup(AT_DATA, uname, uname_len, CASE_SENSITIVE, 0, + NULL, 0, ctx); + if (err) + goto out_lock; + + if (ctx->attr->non_resident) + data_size = le64_to_cpu(ctx->attr->data.non_resident.data_size); + else + data_size = le32_to_cpu(ctx->attr->data.resident.value_length); + + if (offset >= data_size) { + *rbytes = 0; + /* EOF: nothing to read */ + goto out_lock; + } + + /* Adjust length to not read beyond EOF */ + if (len > data_size - offset) + len = data_size - offset; + + attr_vi = ntfs_attr_iget(VFS_I(ni), AT_DATA, uname, uname_len); + ntfs_attr_put_search_ctx(ctx); + mutex_unlock(&ni->mrec_lock); + + if (IS_ERR(attr_vi)) { + err = PTR_ERR(attr_vi); + goto out_free; + } + + rbuf = kvzalloc(len, GFP_NOFS); + if (!rbuf) { + err = -ENOMEM; + iput(attr_vi); + goto out_free; + } + + bytes = ntfs_inode_attr_pread(attr_vi, offset, len, rbuf); + iput(attr_vi); + if (bytes < 0) { + err = bytes; + goto out_free; + } + + *rbytes = bytes; + memcpy(data_buf, rbuf, bytes); + +out_free: + kvfree(rbuf); + return err; + +out_lock: + ntfs_attr_put_search_ctx(ctx); + mutex_unlock(&ni->mrec_lock); + return err; +} + +/* + * ntfs_write_named_stream - Write data from a named $DATA stream. + * + * @ni: NTFS inode + * @uname: Stream name + * @uname_len: Length of name in Unicode characters + * @offset: Byte offset in the stream + * @len: Number of bytes to write + * @data_stream: Data to write + * + * Return: 0 on success, negative error code otherwise. + */ +static int ntfs_write_named_stream(struct ntfs_inode *ni, __le16 *uname, + u32 uname_len, u64 offset, u64 len, void *data_stream, + u64 *wbytes) +{ + struct ntfs_attr_search_ctx *ctx; + struct inode *attr_vi; + int err; + s64 bytes; + + if (!ni || !uname || uname_len == 0 || !data_stream) + return -EINVAL; + + if (len == 0 || len > NTFS_STREAM_MAX_IO) + return -EINVAL; + + mutex_lock(&ni->mrec_lock); + ctx = ntfs_attr_get_search_ctx(ni, NULL); + if (!ctx) { + err = -ENOMEM; + goto out_unlock; + } + + err = ntfs_attr_lookup(AT_DATA, uname, uname_len, CASE_SENSITIVE, 0, + NULL, 0, ctx); + if (err) { + if (err != -ENOENT) + goto out_unlock; + err = ntfs_attr_add(ni, AT_DATA, uname, uname_len, NULL, 0); + if (err) + goto out_unlock; + mark_mft_record_dirty(ni); + } + + attr_vi = ntfs_attr_iget(VFS_I(ni), AT_DATA, uname, uname_len); + ntfs_attr_put_search_ctx(ctx); + mutex_unlock(&ni->mrec_lock); + + if (IS_ERR(attr_vi)) { + err = PTR_ERR(attr_vi); + goto out; + } + + bytes = ntfs_inode_attr_pwrite(attr_vi, offset, len, data_stream, + false); + if (bytes < 0) + err = bytes; + else + *wbytes = bytes; + iput(attr_vi); + +out: + return err; + +out_unlock: + if (ctx) + ntfs_attr_put_search_ctx(ctx); + mutex_unlock(&ni->mrec_lock); + return err; +} + +/* + * ntfs_remove_named_stream - Remove a named $DATA stream. + * + * @ni: NTFS inode + * @uname: Stream name + * @uname_len: Name length in Unicode characters + * + * Return: 0 on success, negative error + * -ENOENT if stream not found + */ +static int ntfs_remove_named_stream(struct ntfs_inode *ni, __le16 *uname, + u32 uname_len) +{ + if (!ni || !uname || uname_len == 0) + return -EINVAL; + + return ntfs_attr_remove(ni, AT_DATA, uname, uname_len); +} + +enum ntfs_stream_op { + NTFS_STREAM_OP_READ, + NTFS_STREAM_OP_WRITE, + NTFS_STREAM_OP_REMOVE, +}; + +static long ntfs_ioctl_stream(struct file *filp, unsigned long arg, + enum ntfs_stream_op op) +{ + struct inode *inode = file_inode(filp); + struct ntfs_inode *ni = NTFS_I(inode); + struct ntfs_stream __user *ureq = + (struct ntfs_stream __user *)arg; + struct ntfs_stream *req; + size_t header_size = sizeof(struct ntfs_stream); + size_t data_size, total_size; + char *kname, *kdata; + __le16 *uname = NULL, *sname; + int err = 0, sname_len; + bool utf16; + struct ntfs_stream hdr; + + /* First copy fixed header */ + if (copy_from_user(&hdr, ureq, header_size)) + return -EFAULT; + + if (hdr.io_len > NTFS_STREAM_MAX_IO || + (hdr.flags & ~NTFS_STREAM_FL_UTF16) || hdr.reserved) + return -EINVAL; + + utf16 = hdr.flags & NTFS_STREAM_FL_UTF16; + + if (hdr.name_len == 0) + return -EINVAL; + if (utf16) { + /* UTF-16LE byte count: must be even and within the limit. */ + 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) { + return -EINVAL; + } + + switch (op) { + case NTFS_STREAM_OP_READ: + if (!(filp->f_mode & FMODE_READ) || hdr.io_len == 0) + return -EINVAL; + data_size = hdr.io_len; + break; + case NTFS_STREAM_OP_WRITE: + if (!(filp->f_mode & FMODE_WRITE) || hdr.io_len == 0) + return -EINVAL; + data_size = hdr.io_len; + break; + case NTFS_STREAM_OP_REMOVE: + if (!(filp->f_mode & FMODE_WRITE) || hdr.io_len != 0) + return -EINVAL; + data_size = 0; + break; + default: + return -ENOTTY; + } + + if (check_add_overflow(header_size, (size_t)hdr.name_len, &total_size) || + check_add_overflow(total_size, data_size, &total_size)) + return -EOVERFLOW; + + req = memdup_user(ureq, total_size); + if (IS_ERR(req)) + return PTR_ERR(req); + + req->bytes_returned = 0; + + kname = (char *)req->buffer; + kdata = (char *)req->buffer + req->name_len; + + if (req->name_len != hdr.name_len || req->io_len != hdr.io_len || + req->flags != hdr.flags || req->reserved) { + err = -EINVAL; + goto out_free; + } + + if (utf16) { + /* Name is already on-disk UTF-16LE; use it in place. */ + sname = (__le16 *)kname; + sname_len = req->name_len / sizeof(__le16); + } else { + if (memchr(kname, '\0', req->name_len)) { + err = -EINVAL; + goto out_free; + } + sname_len = ntfs_nlstoucs(ni->vol, kname, req->name_len, + &uname, NTFS_MAX_NAME_LEN); + if (sname_len < 0) { + err = sname_len; + goto out_free; + } + sname = uname; + } + + switch (op) { + case NTFS_STREAM_OP_READ: + err = ntfs_read_named_stream(ni, sname, sname_len, + req->stream_offset, req->io_len, + kdata, + &req->bytes_returned); + if (!err) { + if (copy_to_user(ureq, req, + header_size + req->name_len + req->bytes_returned)) + err = -EFAULT; + } + break; + case NTFS_STREAM_OP_WRITE: + err = mnt_want_write_file(filp); + if (err) + break; + err = ntfs_write_named_stream(ni, sname, sname_len, + req->stream_offset, req->io_len, + kdata, + &req->bytes_returned); + mnt_drop_write_file(filp); + if (!err) { + if (copy_to_user(&ureq->bytes_returned, + &req->bytes_returned, + sizeof(req->bytes_returned))) + err = -EFAULT; + } + break; + case NTFS_STREAM_OP_REMOVE: + err = mnt_want_write_file(filp); + if (err) + break; + err = ntfs_remove_named_stream(ni, sname, sname_len); + mnt_drop_write_file(filp); + break; + default: + err = -ENOTTY; + } + +out_free: + if (uname) + kmem_cache_free(ntfs_name_cache, uname); + kfree(req); + return err; +} + +static 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; + unsigned int name_len; + unsigned char *sn; + bool utf16; + + 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 (utf16) { + name_len = a->name_length * sizeof(__le16); + } else { + sn = ntfs_attr_name_get(ni->vol, + (__le16 *)((u8 *)a + le16_to_cpu(a->name_offset)), + a->name_length); + if (!sn) { + ret = -EIO; + goto out; + } + name_len = strlen(sn); + ntfs_attr_name_free(&sn); + } + + entry_size = sizeof(*entry) + name_len; + entry_size = ALIGN(entry_size, 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 (utf16) { + sn = NULL; + name_len = a->name_length * sizeof(__le16); + } else { + sn = ntfs_attr_name_get(ni->vol, + (__le16 *)((u8 *)a + le16_to_cpu(a->name_offset)), + a->name_length); + if (!sn) { + ret = -EIO; + goto out; + } + name_len = strlen(sn); + } + + entry_size = sizeof(*entry) + name_len; + entry_size = ALIGN(entry_size, 8); + if (offset > required - entry_size) { + ret = -EOVERFLOW; + ntfs_attr_name_free(&sn); + 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; + if (utf16) + memcpy(entry->name, + (u8 *)a + le16_to_cpu(a->name_offset), name_len); + else + memcpy(entry->name, sn, name_len); + ntfs_attr_name_free(&sn); + + 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: + mutex_unlock(&ni->mrec_lock); + ntfs_attr_put_search_ctx(actx); + + 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; +} + long ntfs_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { switch (cmd) { @@ -850,6 +1351,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(filp, arg, NTFS_STREAM_OP_READ); + case NTFS_IOC_STREAM_WRITE: + return ntfs_ioctl_stream(filp, arg, NTFS_STREAM_OP_WRITE); + case NTFS_IOC_STREAM_REMOVE: + return ntfs_ioctl_stream(filp, arg, NTFS_STREAM_OP_REMOVE); + case NTFS_IOC_LIST_STREAMS: + return ntfs_ioctl_list_streams(filp, arg); default: return -ENOTTY; } diff --git a/include/uapi/linux/ntfs.h b/include/uapi/linux/ntfs.h new file mode 100644 index 000000000000..a5783dc3b905 --- /dev/null +++ b/include/uapi/linux/ntfs.h @@ -0,0 +1,115 @@ +/* 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. + * + * The stream name is raw UTF-16LE, exactly as stored on disk, instead of + * being encoded with the mounted filesystem NLS. This allows lossless + * round-tripping of stream names whose characters are not representable + * in 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 ioctl structure. + * + * @stream_offset: Offset within the named stream. + * @io_len: Number of bytes to read/write. + * @bytes_returned: Actual bytes transferred (out). + * @name_len: Stream name length in bytes, not including any + * terminating NUL (which must not be present). When + * NTFS_STREAM_FL_UTF16 is set this is a UTF-16LE byte + * count and must be even. + * @flags: Bit mask of NTFS_STREAM_FL_* flags. other bits must be + * zero. + * @reserved: Must be zero. + * @buffer: Stream name followed by stream data. + * + * The stream name is the bare stream name, without the leading ':' or the + * trailing ":$DATA" decoration used by Windows. By default it is encoded with + * the mounted filesystem NLS. If NTFS_STREAM_FL_UTF16 is set in @flags it is + * raw UTF-16LE instead. For read and write, @io_len bytes of data follow the + * name. For remove, only the name is required and @io_len must be zero. + */ +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[]; /* name + data */ +}; + +/* + * 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, so the entry can grow + * new fields without breaking the ABI. + * @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. + * @reserved: Must be zero. + * @name: Bare stream name; NLS encoded, or raw UTF-16LE when + * NTFS_STREAM_FL_UTF16 was requested (see struct + * ntfs_stream). + */ +struct ntfs_stream_entry { + __aligned_u64 next_entry_off; + __aligned_u64 size; + __aligned_u64 alloc_size; + __u32 name_len; + __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. + */ +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