[PATCH 0/1] ntfs: introduce named data stream ioctls

Namjae Jeon <[email protected]> Wed, 29 Jul 2026 19:52:58 +0900
Newsgroups dev.linux.lists.ntfs,org.kernel.vger.linux-fsdevel,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
NTFS supports multiple named $DATA attributes, commonly known as
alternate data streams. This adds NTFS-specific ioctls to list,
read, write, and remove named streams from an opened file.

Example code (ntfs-stream-ioctl.c):

 Usage: ./ntfs-stream-ioctl <file> <command> [args...] [--utf16]

 Commands:
   list                          List all named streams
   read <stream_name>            Read content of a stream
   write <stream_name> <data>    Write data to a stream
   remove <stream_name>          Delete a named stream

 Options:
   --utf16    Use raw UTF-16LE mode (lossless)


Example usage:

  $ ./ntfs-stream-ioctl /mnt/file write Note "This is a secret note"
      Successfully written to stream: Note
     
  $ ./ntfs-stream-ioctl /mnt/file read Note
      This is a secret note

  $ ./ntfs-stream-ioctl /mnt/file list
      Total named streams: 2
        Note   (size: 21 bytes)
        stream0   (size: 12 bytes)


ntfs-stream-ioctl.c:

#include <errno.h>
#include <fcntl.h>
#include <linux/ntfs.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdbool.h>
#include <stdint.h>

static void die(const char *msg)
{
	perror(msg);
	exit(EXIT_FAILURE);
}

static void usage(const char *prog)
{
	fprintf(stderr, "Usage: %s <file> <command> [args...] [--utf16]\n\n", prog);
	fprintf(stderr, "Commands:\n");
	fprintf(stderr, "  list                          List all named streams\n");
	fprintf(stderr, "  read <stream_name>            Read content of a stream\n");
	fprintf(stderr, "  write <stream_name> <data>    Write data to a stream\n");
	fprintf(stderr, "  remove <stream_name>          Delete a named stream\n\n");
	fprintf(stderr, "Options:\n");
	fprintf(stderr, "  --utf16    Use raw UTF-16LE mode (lossless)\n\n");
	exit(EXIT_FAILURE);
}

static void copy_name_utf16le(void *dest, const char *src, size_t len)
{
	uint8_t *d = dest;
	for (size_t i = 0; i < len; i++) {
		d[i*2]     = (uint8_t)src[i];   // Little-endian low byte
		d[i*2 + 1] = 0;                 // high byte = 0 (ASCII)
	}
}

static void stream_write(int fd, const char *name, const char *data, bool utf16)
{
	size_t name_len = strlen(name);
	size_t data_len = strlen(data);
	size_t name_buf_len = utf16 ? name_len * 2 : name_len;
	size_t req_len = sizeof(struct ntfs_stream) + name_buf_len + data_len;

	struct ntfs_stream *req = calloc(1, req_len);
	if (!req) die("calloc");

	req->flags = utf16 ? NTFS_STREAM_FL_UTF16 : 0;
	req->name_len = name_buf_len;
	req->io_len = data_len;

	if (utf16)
		copy_name_utf16le(req->buffer, name, name_len);
	else
		memcpy(req->buffer, name, name_len);

	memcpy(req->buffer + name_buf_len, data, data_len);

	if (ioctl(fd, NTFS_IOC_STREAM_WRITE, req) < 0)
		die("NTFS_IOC_STREAM_WRITE");

	printf("Successfully written to stream: %s\n", name);
	free(req);
}

static void stream_read(int fd, const char *name, bool utf16)
{
	size_t name_len = strlen(name);
	size_t name_buf_len = utf16 ? name_len * 2 : name_len;
	size_t buf_size = 8192;
	size_t req_len = sizeof(struct ntfs_stream) + name_buf_len + buf_size;

	struct ntfs_stream *req = calloc(1, req_len);
	if (!req) die("calloc");

	req->flags = utf16 ? NTFS_STREAM_FL_UTF16 : 0;
	req->name_len = name_buf_len;
	req->io_len = buf_size;
	req->stream_offset = 0;

	if (utf16)
		copy_name_utf16le(req->buffer, name, name_len);
	else
		memcpy(req->buffer, name, name_len);

	if (ioctl(fd, NTFS_IOC_STREAM_READ, req) < 0)
		die("NTFS_IOC_STREAM_READ");

	printf("%.*s\n", (int)req->bytes_returned,
			(char *)req->buffer + req->name_len);

	free(req);
}

static void list_streams(int fd, bool utf16)
{
	struct ntfs_list_streams *list;
	struct ntfs_stream_entry *entry;
	size_t buf_len = 8192 * 4;
	size_t off = 0;

	list = calloc(1, sizeof(*list) + buf_len);
	if (!list) die("calloc");

	list->buffer_size = buf_len;
	list->flags = utf16 ? NTFS_STREAM_FL_UTF16 : 0;

	if (ioctl(fd, NTFS_IOC_LIST_STREAMS, list) < 0)
		die("NTFS_IOC_LIST_STREAMS");

	printf("Total named streams: %llu\n", (unsigned long long)list->stream_count);

	while (off < list->bytes_returned) {
		entry = (struct ntfs_stream_entry *)(list->buffer + off);

		if (utf16) {
			printf("  UTF16: ");
			for (size_t i = 0; i + 1 < entry->name_len; i += 2) {
				uint16_t c = entry->name[i] | (entry->name[i+1] << 8);
				if (c >= 0x20 && c < 0x7F)
					putchar(c);
				else
					printf("\\u%04x", c);
			}
			printf("  (len=%u bytes, size=%llu)\n",
					entry->name_len, (unsigned long long)entry->size);
		} else {
			printf("  %.*s   (size: %llu bytes)\n",
					(int)entry->name_len, entry->name,
					(unsigned long long)entry->size);
		}

		if (entry->next_entry_off == 0)
			break;

		off += entry->next_entry_off;
	}

	free(list);
}

static void stream_remove(int fd, const char *name, bool utf16)
{
	size_t name_len = strlen(name);
	size_t name_buf_len = utf16 ? name_len * 2 : name_len;
	size_t req_len = sizeof(struct ntfs_stream) + name_buf_len;

	struct ntfs_stream *req = calloc(1, req_len);
	if (!req) die("calloc");

	req->flags = utf16 ? NTFS_STREAM_FL_UTF16 : 0;
	req->name_len = name_buf_len;
	req->io_len = 0;

	if (utf16)
		copy_name_utf16le(req->buffer, name, name_len);
	else
		memcpy(req->buffer, name, name_len);

	if (ioctl(fd, NTFS_IOC_STREAM_REMOVE, req) < 0)
		die("NTFS_IOC_STREAM_REMOVE");

	printf("Successfully removed stream: %s\n", name);
	free(req);
}

int main(int argc, char **argv)
{
	int fd;
	const char *filename;
	bool utf16 = false;
	int cmd_idx = 2;

	if (argc < 3)
		usage(argv[0]);

	filename = argv[1];

	for (int i = 3; i < argc; i++) {
		if (strcmp(argv[i], "--utf16") == 0) {
			utf16 = true;
			argc = i;
			break;
		}
	}

	fd = open(filename, O_RDWR);
	if (fd < 0)
		die("open");

	if (strcmp(argv[cmd_idx], "list") == 0) {
		list_streams(fd, utf16);
	}
	else if (strcmp(argv[cmd_idx], "read") == 0) {
		if (argc < cmd_idx + 2) usage(argv[0]);
		stream_read(fd, argv[cmd_idx + 1], utf16);
	}
	else if (strcmp(argv[cmd_idx], "write") == 0) {
		if (argc < cmd_idx + 3) usage(argv[0]);
		stream_write(fd, argv[cmd_idx + 1], argv[cmd_idx + 2], utf16);
	}
	else if (strcmp(argv[cmd_idx], "remove") == 0) {
		if (argc < cmd_idx + 2) usage(argv[0]);
		stream_remove(fd, argv[cmd_idx + 1], utf16);
	}
	else {
		fprintf(stderr, "Unknown command: %s\n", argv[cmd_idx]);
		usage(argv[0]);
	}

	close(fd);
	return EXIT_SUCCESS;
}

Namjae Jeon (1):
  ntfs: add ioctl support for named data streams

 .../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

-- 
2.25.1