mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH 0/7] ntfs: Support Windows native symbolic links
@ 2026-06-12  7:33 Hyunchul Lee
  2026-06-12  7:33 ` [PATCH 1/7] ntfs: fix incorrect size of symbolic link Hyunchul Lee
                   ` (6 more replies)
  0 siblings, 7 replies; 10+ messages in thread
From: Hyunchul Lee @ 2026-06-12  7:33 UTC (permalink / raw)
  To: Namjae Jeon; +Cc: Hyunchul Lee, linux-fsdevel, linux-kernel

This patch series adds support for Windows native symbolic links and
introduces mount options to configure how symbolic links are resolved
and created.

Currently, the NTFS driver creates WSL-compatible symbolic links and
has limited support for native Windows symbolic links. This series
addresses these limitations.

1. Following/Resolving Symlinks & Junctions:
  - Add support for parsing and following Windows native symbolic links.
  - Introduce the "native_symlink=raw|rel" mount option to configure
    absolute target resolution.

2. Creating Symlinks:
  - Introduce the "symlink=wsl|native" mount option to choose the
    creation behavior of symlinks. Under "native", Windows native
    symbolic links are created by converting the target path to UTF-16
    Windows format.

3. Fixes & Cleanups:
  - Fix an issue where the size of symbolic links is reported as 0.
  - Clean up target name conversion for WSL symlinks by avoiding
    redundant conversion.

4. Documentation:
  - Document the newly introduced mount options in ntfs.rst.

Signed-off-by: Hyunchul Lee <hyc.lee@gmail.com>
---
Hyunchul Lee (7):
      ntfs: fix incorrect size of symbolic link
      ntfs: support following Windows native symlink with relative paths
      ntfs: support following Windows native symlink with absolute paths
      ntfs: add native_symlink mount option
      ntfs: clean up target name conversion for WSL symlinks
      ntfs: support creating Windows native symlinks
      docs/fs/ntfs: add mount options to support Windows native symbolic links

 Documentation/filesystems/ntfs.rst |  13 ++
 fs/ntfs/file.c                     |  24 +-
 fs/ntfs/inode.c                    |  49 ++--
 fs/ntfs/inode.h                    |   2 +
 fs/ntfs/layout.h                   |  19 ++
 fs/ntfs/namei.c                    |  22 +-
 fs/ntfs/reparse.c                  | 461 +++++++++++++++++++++++++++++++++----
 fs/ntfs/reparse.h                  |   6 +-
 fs/ntfs/super.c                    |  38 +++
 fs/ntfs/volume.h                   |   5 +
 10 files changed, 563 insertions(+), 76 deletions(-)
---
base-commit: 5aec1efb11ab2a87d1e4be063830268f7980ec4a
change-id: 20260612-topic-symlink-d70873e4721e

Best regards,
-- 
Thanks,
Hyunchul


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH 1/7] ntfs: fix incorrect size of symbolic link
  2026-06-12  7:33 [PATCH 0/7] ntfs: Support Windows native symbolic links Hyunchul Lee
@ 2026-06-12  7:33 ` Hyunchul Lee
  2026-06-12  7:33 ` [PATCH 2/7] ntfs: support following Windows native symlink with relative paths Hyunchul Lee
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 10+ messages in thread
From: Hyunchul Lee @ 2026-06-12  7:33 UTC (permalink / raw)
  To: Namjae Jeon; +Cc: Hyunchul Lee, linux-fsdevel, linux-kernel

This patch fixes the issue where a symbolic link size is displayed as 0.

Signed-off-by: Hyunchul Lee <hyc.lee@gmail.com>
---
 fs/ntfs/inode.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index 2f2634baa285..efb34a5e94d9 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -1199,6 +1199,9 @@ static int ntfs_read_locked_inode(struct inode *vi)
 	else
 		vi->i_blocks = ni->allocated_size >> 9;
 
+	if (S_ISLNK(vi->i_mode) && ni->target)
+		vi->i_size = strlen(ni->target);
+
 	ntfs_debug("Done.");
 	return 0;
 unm_err_out:

-- 
2.43.0


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH 2/7] ntfs: support following Windows native symlink with relative paths
  2026-06-12  7:33 [PATCH 0/7] ntfs: Support Windows native symbolic links Hyunchul Lee
  2026-06-12  7:33 ` [PATCH 1/7] ntfs: fix incorrect size of symbolic link Hyunchul Lee
@ 2026-06-12  7:33 ` Hyunchul Lee
  2026-06-12 16:20   ` CharSyam
  2026-06-12  7:33 ` [PATCH 3/7] ntfs: support following Windows native symlink with absolute paths Hyunchul Lee
                   ` (4 subsequent siblings)
  6 siblings, 1 reply; 10+ messages in thread
From: Hyunchul Lee @ 2026-06-12  7:33 UTC (permalink / raw)
  To: Namjae Jeon; +Cc: Hyunchul Lee, linux-fsdevel, linux-kernel

Make ntfs_make_symlink() parse native Windows symbolic link reparse
payloads when the SYMLINK_FLAG_RELATIVE bit is set.
Implement the following changes:
 * Add a dedicated on-disk layout definition for symbolic link reparse
   data.
 * validate the UTF-16 name ranges before decoding them.
 * convert the substitute name into the mount's NLS and normalize path
   separators.

Signed-off-by: Hyunchul Lee <hyc.lee@gmail.com>
---
 fs/ntfs/inode.c   |  36 +++++++++-------
 fs/ntfs/layout.h  |  11 +++++
 fs/ntfs/reparse.c | 122 +++++++++++++++++++++++++++++++++++++++++++++++++-----
 3 files changed, 143 insertions(+), 26 deletions(-)

diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index efb34a5e94d9..8894f33b46ca 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -863,8 +863,26 @@ static int ntfs_read_locked_inode(struct inode *vi)
 		ntfs_ea_get_wsl_inode(vi, &dev, flags);
 	}
 
-	if (m->flags & MFT_RECORD_IS_DIRECTORY) {
+	if (ni->flags & FILE_ATTR_REPARSE_POINT) {
+		unsigned int mode;
+
+		mode = ntfs_make_symlink(ni);
+		if (mode)
+			vi->i_mode |= mode;
+		else {
+			vi->i_mode &= ~S_IFLNK;
+			if (m->flags & MFT_RECORD_IS_DIRECTORY)
+				vi->i_mode |= S_IFDIR;
+			else
+				vi->i_mode |= S_IFREG;
+		}
+	} else if (m->flags & MFT_RECORD_IS_DIRECTORY) {
 		vi->i_mode |= S_IFDIR;
+	} else {
+		vi->i_mode |= S_IFREG;
+	}
+
+	if (S_ISDIR(vi->i_mode)) {
 		/*
 		 * Apply the directory permissions mask set in the mount
 		 * options.
@@ -874,18 +892,6 @@ static int ntfs_read_locked_inode(struct inode *vi)
 		if (vi->i_nlink > 1)
 			set_nlink(vi, 1);
 	} else {
-		if (ni->flags & FILE_ATTR_REPARSE_POINT) {
-			unsigned int mode;
-
-			mode = ntfs_make_symlink(ni);
-			if (mode)
-				vi->i_mode |= mode;
-			else {
-				vi->i_mode &= ~S_IFLNK;
-				vi->i_mode |= S_IFREG;
-			}
-		} else
-			vi->i_mode |= S_IFREG;
 		/* Apply the file permissions mask set in the mount options. */
 		vi->i_mode &= ~vol->fmask;
 	}
@@ -894,7 +900,7 @@ static int ntfs_read_locked_inode(struct inode *vi)
 	 * If an attribute list is present we now have the attribute list value
 	 * in ntfs_ino->attr_list and it is ntfs_ino->attr_list_size bytes.
 	 */
-	if (S_ISDIR(vi->i_mode)) {
+	if (m->flags & MFT_RECORD_IS_DIRECTORY) {
 		struct index_root *ir;
 
 view_index_meta:
@@ -1018,7 +1024,7 @@ static int ntfs_read_locked_inode(struct inode *vi)
 		m = NULL;
 		ctx = NULL;
 		/* Setup the operations for this inode. */
-		ntfs_set_vfs_operations(vi, S_IFDIR, 0);
+		ntfs_set_vfs_operations(vi, vi->i_mode, 0);
 		if (ir->index.flags & LARGE_INDEX)
 			NInoSetIndexAllocPresent(ni);
 	} else {
diff --git a/fs/ntfs/layout.h b/fs/ntfs/layout.h
index d94f914e830f..94af6efa04af 100644
--- a/fs/ntfs/layout.h
+++ b/fs/ntfs/layout.h
@@ -2267,6 +2267,8 @@ enum {
 	IO_REPARSE_PLUGIN_SELECT	= cpu_to_le32(0xffff0fff),
 };
 
+#define SYMLINK_FLAG_RELATIVE		1
+
 /*
  * struct reparse_point - $REPARSE_POINT attribute content (0xc0)\
  *
@@ -2287,6 +2289,15 @@ struct reparse_point {
 	u8 reparse_data[];
 } __packed;
 
+struct symlink_reparse_data {
+	__le16 substitute_name_offset;
+	__le16 substitute_name_length;
+	__le16 print_name_offset;
+	__le16 print_name_length;
+	__le32 flags;
+	__le16 path_buffer[];
+} __packed;
+
 /*
  * struct ea_information - $EA_INFORMATION attribute content (0xd0)
  *
diff --git a/fs/ntfs/reparse.c b/fs/ntfs/reparse.c
index 74713716813f..4cc37f1c9c90 100644
--- a/fs/ntfs/reparse.c
+++ b/fs/ntfs/reparse.c
@@ -24,6 +24,47 @@ struct wsl_link_reparse_data {
 	char	link[];
 };
 
+static bool reparse_name_is_valid(size_t size, size_t name_off, u16 len)
+{
+	if ((name_off | len) & 1)
+		return false;
+
+	return name_off + len <= size;
+}
+
+/*
+ * Windows-native reparse payloads store pathnames as UTF-16 strings with '\\'
+ * separators. Convert the on-disk UTF-16 target into the mount's NLS and
+ * normalize path separators.
+ */
+static int ntfs_reparse_target_to_nls(struct ntfs_volume *vol,
+				      const __le16 *uname, u16 ulen,
+				      char **target)
+{
+	int err, i;
+
+	*target = NULL;
+	ulen >>= 1;
+	if (!ulen)
+		return -EINVAL;
+
+	if (!uname[ulen - 1])
+		ulen--;
+
+	err = ntfs_ucstonls(vol, uname, ulen, (unsigned char **)target, 0);
+	if (err < 0) {
+		ntfs_attr_name_free((unsigned char **)target);
+		return err;
+	}
+
+	for (i = 0; i < err; i++) {
+		if ((*target)[i] == '\\')
+			(*target)[i] = '/';
+	}
+
+	return 0;
+}
+
 /* Index entry in $Extend/$Reparse */
 struct reparse_index {
 	struct index_entry_header header;
@@ -38,8 +79,10 @@ __le16 reparse_index_name[] = {cpu_to_le16('$'), cpu_to_le16('R'), 0};
  * Check if the reparse point attribute buffer is valid.
  * Returns true if valid, false otherwise.
  */
-static bool ntfs_is_valid_reparse_buffer(struct ntfs_inode *ni,
-		const struct reparse_point *reparse_attr, size_t size)
+static bool valid_reparse_buffer(struct ntfs_inode *ni,
+				 const struct reparse_point *reparse_attr,
+				 size_t size,
+				 size_t payload_min_len)
 {
 	size_t expected;
 
@@ -50,6 +93,11 @@ static bool ntfs_is_valid_reparse_buffer(struct ntfs_inode *ni,
 	if (size < sizeof(struct reparse_point))
 		return false;
 
+	/* The payload must contain the fixed fields for the current tag. */
+	if (payload_min_len &&
+	    le16_to_cpu(reparse_attr->reparse_data_length) < payload_min_len)
+		return false;
+
 	/* Reserved zero tag is invalid */
 	if (reparse_attr->reparse_tag == IO_REPARSE_TAG_RESERVED_ZERO)
 		return false;
@@ -79,24 +127,54 @@ static bool ntfs_is_valid_reparse_buffer(struct ntfs_inode *ni,
 static bool valid_reparse_data(struct ntfs_inode *ni,
 		const struct reparse_point *reparse_attr, size_t size)
 {
-	const struct wsl_link_reparse_data *wsl_reparse_data =
-		(const struct wsl_link_reparse_data *)reparse_attr->reparse_data;
-	unsigned int data_len = le16_to_cpu(reparse_attr->reparse_data_length);
+	switch (reparse_attr->reparse_tag) {
+	case IO_REPARSE_TAG_SYMLINK:
+	{
+		struct symlink_reparse_data *data;
+		size_t data_offs;
 
-	if (ntfs_is_valid_reparse_buffer(ni, reparse_attr, size) == false)
-		return false;
+		if (!valid_reparse_buffer(ni, reparse_attr, size,
+					  sizeof(*data)))
+			return false;
 
-	switch (reparse_attr->reparse_tag) {
+		data = (struct symlink_reparse_data *)reparse_attr->reparse_data;
+		data_offs = offsetof(struct reparse_point, reparse_data) +
+			offsetof(struct symlink_reparse_data, path_buffer);
+
+		if (!reparse_name_is_valid(size,
+					   data_offs +
+					   le16_to_cpu(data->substitute_name_offset),
+					   le16_to_cpu(data->substitute_name_length)) ||
+		    !reparse_name_is_valid(size,
+					   data_offs +
+					   le16_to_cpu(data->print_name_offset),
+					   le16_to_cpu(data->print_name_length)))
+			return false;
+		break;
+	}
 	case IO_REPARSE_TAG_LX_SYMLINK:
-		if (data_len <= sizeof(wsl_reparse_data->type) ||
-		    wsl_reparse_data->type != cpu_to_le32(2))
+	{
+		struct wsl_link_reparse_data *data;
+
+		if (!valid_reparse_buffer(ni, reparse_attr, size,
+					  sizeof(*data)))
+			return false;
+
+		data = (struct wsl_link_reparse_data *)reparse_attr->reparse_data;
+
+		if (le16_to_cpu(reparse_attr->reparse_data_length) <= sizeof(data->type) ||
+		    data->type != cpu_to_le32(2))
 			return false;
 		break;
+	}
 	case IO_REPARSE_TAG_AF_UNIX:
 	case IO_REPARSE_TAG_LX_FIFO:
 	case IO_REPARSE_TAG_LX_CHR:
 	case IO_REPARSE_TAG_LX_BLK:
-		if (data_len || !(ni->flags & FILE_ATTRIBUTE_RECALL_ON_OPEN))
+		if (!valid_reparse_buffer(ni, reparse_attr, size, 0))
+			return false;
+		if (le16_to_cpu(reparse_attr->reparse_data_length) ||
+		    !(ni->flags & FILE_ATTRIBUTE_RECALL_ON_OPEN))
 			return false;
 	}
 
@@ -134,16 +212,38 @@ static unsigned int ntfs_reparse_tag_mode(struct reparse_point *reparse_attr)
 unsigned int ntfs_make_symlink(struct ntfs_inode *ni)
 {
 	s64 attr_size = 0;
+	int err;
 	unsigned int lth;
 	struct reparse_point *reparse_attr;
 	struct wsl_link_reparse_data *wsl_link_data;
 	unsigned int mode = 0;
 
+	kvfree(ni->target);
+	ni->target = NULL;
+
 	reparse_attr = ntfs_attr_readall(ni, AT_REPARSE_POINT, NULL, 0,
 					 &attr_size);
 	if (reparse_attr && attr_size &&
 	    valid_reparse_data(ni, reparse_attr, attr_size)) {
 		switch (reparse_attr->reparse_tag) {
+		case IO_REPARSE_TAG_SYMLINK:
+		{
+			struct symlink_reparse_data *data =
+				(struct symlink_reparse_data *)reparse_attr->reparse_data;
+			const __le16 *name = (const __le16 *)((u8 *)data->path_buffer +
+							le16_to_cpu(data->substitute_name_offset));
+
+			mode = ntfs_reparse_tag_mode(reparse_attr);
+			if (!(data->flags & cpu_to_le32(SYMLINK_FLAG_RELATIVE)))
+				break;
+
+			err = ntfs_reparse_target_to_nls(ni->vol, name,
+							 le16_to_cpu(data->substitute_name_length),
+							 &ni->target);
+			if (err < 0)
+				mode = 0;
+			break;
+		}
 		case IO_REPARSE_TAG_LX_SYMLINK:
 			wsl_link_data =
 				(struct wsl_link_reparse_data *)reparse_attr->reparse_data;

-- 
2.43.0


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH 3/7] ntfs: support following Windows native symlink with absolute paths
  2026-06-12  7:33 [PATCH 0/7] ntfs: Support Windows native symbolic links Hyunchul Lee
  2026-06-12  7:33 ` [PATCH 1/7] ntfs: fix incorrect size of symbolic link Hyunchul Lee
  2026-06-12  7:33 ` [PATCH 2/7] ntfs: support following Windows native symlink with relative paths Hyunchul Lee
@ 2026-06-12  7:33 ` Hyunchul Lee
  2026-06-12  7:33 ` [PATCH 4/7] ntfs: add native_symlink mount option Hyunchul Lee
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 10+ messages in thread
From: Hyunchul Lee @ 2026-06-12  7:33 UTC (permalink / raw)
  To: Namjae Jeon; +Cc: Hyunchul Lee, linux-fsdevel, linux-kernel

Extend reparse-point handling beyond relative symlinks so NTFS can
expose the Windows absolute forms used by non-relative symbolic links
and junctions.
* Store the reparse tag and symlink flags in the inode.
* Validate junction payloads, and parse targets from substitute_name.
* Add function to rewrite supported Windows absolute path into Linux
  path relative to the mounted NTFS volume.

Signed-off-by: Hyunchul Lee <hyc.lee@gmail.com>
---
 fs/ntfs/file.c    |  22 ++++++-
 fs/ntfs/inode.c   |   2 +
 fs/ntfs/inode.h   |   2 +
 fs/ntfs/layout.h  |   8 +++
 fs/ntfs/reparse.c | 182 +++++++++++++++++++++++++++++++++++++++++++++++++-----
 fs/ntfs/reparse.h |   2 +
 6 files changed, 202 insertions(+), 16 deletions(-)

diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
index 264cf8404385..6b0dfc56577b 100644
--- a/fs/ntfs/file.c
+++ b/fs/ntfs/file.c
@@ -675,10 +675,28 @@ static int ntfs_fiemap(struct inode *inode, struct fiemap_extent_info *fieinfo,
 static const char *ntfs_get_link(struct dentry *dentry, struct inode *inode,
 		struct delayed_call *done)
 {
-	if (!NTFS_I(inode)->target)
+	struct ntfs_inode *ni = NTFS_I(inode);
+	char *target;
+	int err;
+
+	if (!dentry)
+		return ERR_PTR(-ECHILD);
+
+	if (!ni->target)
 		return ERR_PTR(-EINVAL);
 
-	return NTFS_I(inode)->target;
+	if (ni->reparse_tag == IO_REPARSE_TAG_MOUNT_POINT ||
+	    (ni->reparse_tag == IO_REPARSE_TAG_SYMLINK &&
+	     !(ni->reparse_flags & cpu_to_le32(SYMLINK_FLAG_RELATIVE)))) {
+		err = ntfs_translate_symlink_path(dentry, ni->target, &target);
+		if (err < 0)
+			return ERR_PTR(err);
+
+		set_delayed_call(done, kfree_link, target);
+		return target;
+	}
+
+	return ni->target;
 }
 
 static ssize_t ntfs_file_splice_read(struct file *in, loff_t *ppos,
diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index 8894f33b46ca..07ca799a8f9a 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -488,6 +488,8 @@ void __ntfs_init_inode(struct super_block *sb, struct ntfs_inode *ni)
 	ni->flags = 0;
 	ni->mft_lcn[0] = LCN_RL_NOT_MAPPED;
 	ni->mft_lcn_count = 0;
+	ni->reparse_tag = 0;
+	ni->reparse_flags = 0;
 	ni->target = NULL;
 	ni->i_dealloc_clusters = 0;
 }
diff --git a/fs/ntfs/inode.h b/fs/ntfs/inode.h
index 67942b97fac6..9aacd5787ffe 100644
--- a/fs/ntfs/inode.h
+++ b/fs/ntfs/inode.h
@@ -142,6 +142,8 @@ struct ntfs_inode {
 		struct ntfs_inode *base_ntfs_ino;
 	} ext;
 	unsigned int i_dealloc_clusters;
+	__le32 reparse_tag;
+	__le32 reparse_flags;
 	char *target;
 };
 
diff --git a/fs/ntfs/layout.h b/fs/ntfs/layout.h
index 94af6efa04af..9438fd9b668e 100644
--- a/fs/ntfs/layout.h
+++ b/fs/ntfs/layout.h
@@ -2289,6 +2289,14 @@ struct reparse_point {
 	u8 reparse_data[];
 } __packed;
 
+struct mount_point_reparse_data {
+	__le16 substitute_name_offset;
+	__le16 substitute_name_length;
+	__le16 print_name_offset;
+	__le16 print_name_length;
+	__le16 path_buffer[];
+} __packed;
+
 struct symlink_reparse_data {
 	__le16 substitute_name_offset;
 	__le16 substitute_name_length;
diff --git a/fs/ntfs/reparse.c b/fs/ntfs/reparse.c
index 4cc37f1c9c90..fb8c42a27699 100644
--- a/fs/ntfs/reparse.c
+++ b/fs/ntfs/reparse.c
@@ -128,6 +128,29 @@ static bool valid_reparse_data(struct ntfs_inode *ni,
 		const struct reparse_point *reparse_attr, size_t size)
 {
 	switch (reparse_attr->reparse_tag) {
+	case IO_REPARSE_TAG_MOUNT_POINT:
+	{
+		struct mount_point_reparse_data *data;
+		size_t data_offs;
+
+		if (!valid_reparse_buffer(ni, reparse_attr, size, sizeof(*data)))
+			return false;
+
+		data = (struct mount_point_reparse_data *)reparse_attr->reparse_data;
+		data_offs = offsetof(struct reparse_point, reparse_data) +
+			offsetof(struct mount_point_reparse_data, path_buffer);
+
+		if (!reparse_name_is_valid(size,
+					   data_offs +
+					   le16_to_cpu(data->substitute_name_offset),
+					   le16_to_cpu(data->substitute_name_length)) ||
+		    !reparse_name_is_valid(size,
+					   data_offs +
+					   le16_to_cpu(data->print_name_offset),
+					   le16_to_cpu(data->print_name_length)))
+			return false;
+		break;
+	}
 	case IO_REPARSE_TAG_SYMLINK:
 	{
 		struct symlink_reparse_data *data;
@@ -176,16 +199,22 @@ static bool valid_reparse_data(struct ntfs_inode *ni,
 		if (le16_to_cpu(reparse_attr->reparse_data_length) ||
 		    !(ni->flags & FILE_ATTRIBUTE_RECALL_ON_OPEN))
 			return false;
+		break;
+	default:
+		if (!valid_reparse_buffer(ni, reparse_attr, size, 0))
+			return false;
+		break;
 	}
 
 	return true;
 }
 
-static unsigned int ntfs_reparse_tag_mode(struct reparse_point *reparse_attr)
+static unsigned int ntfs_reparse_tag_mode(__le32 reparse_tag)
 {
 	unsigned int mode = 0;
 
-	switch (reparse_attr->reparse_tag) {
+	switch (reparse_tag) {
+	case IO_REPARSE_TAG_MOUNT_POINT:
 	case IO_REPARSE_TAG_SYMLINK:
 	case IO_REPARSE_TAG_LX_SYMLINK:
 		mode = S_IFLNK;
@@ -215,17 +244,33 @@ unsigned int ntfs_make_symlink(struct ntfs_inode *ni)
 	int err;
 	unsigned int lth;
 	struct reparse_point *reparse_attr;
-	struct wsl_link_reparse_data *wsl_link_data;
 	unsigned int mode = 0;
 
 	kvfree(ni->target);
 	ni->target = NULL;
+	ni->reparse_tag = 0;
+	ni->reparse_flags = 0;
 
 	reparse_attr = ntfs_attr_readall(ni, AT_REPARSE_POINT, NULL, 0,
 					 &attr_size);
 	if (reparse_attr && attr_size &&
 	    valid_reparse_data(ni, reparse_attr, attr_size)) {
+		err = -EINVAL;
+
 		switch (reparse_attr->reparse_tag) {
+		case IO_REPARSE_TAG_MOUNT_POINT:
+		{
+			struct mount_point_reparse_data *data =
+				(struct mount_point_reparse_data *)reparse_attr->reparse_data;
+			const __le16 *name = (const __le16 *)((u8 *)data->path_buffer +
+					      le16_to_cpu(data->substitute_name_offset));
+
+			err = ntfs_reparse_target_to_nls(ni->vol,
+							 name,
+							 le16_to_cpu(data->substitute_name_length),
+							 &ni->target);
+			break;
+		}
 		case IO_REPARSE_TAG_SYMLINK:
 		{
 			struct symlink_reparse_data *data =
@@ -233,33 +278,38 @@ unsigned int ntfs_make_symlink(struct ntfs_inode *ni)
 			const __le16 *name = (const __le16 *)((u8 *)data->path_buffer +
 							le16_to_cpu(data->substitute_name_offset));
 
-			mode = ntfs_reparse_tag_mode(reparse_attr);
-			if (!(data->flags & cpu_to_le32(SYMLINK_FLAG_RELATIVE)))
-				break;
-
-			err = ntfs_reparse_target_to_nls(ni->vol, name,
+			err = ntfs_reparse_target_to_nls(ni->vol,
+							 name,
 							 le16_to_cpu(data->substitute_name_length),
 							 &ni->target);
-			if (err < 0)
-				mode = 0;
+			if (!err)
+				ni->reparse_flags = data->flags;
 			break;
 		}
 		case IO_REPARSE_TAG_LX_SYMLINK:
-			wsl_link_data =
+		{
+			struct wsl_link_reparse_data *wsl_link_data =
 				(struct wsl_link_reparse_data *)reparse_attr->reparse_data;
+
 			if (wsl_link_data->type == cpu_to_le32(2)) {
 				lth = le16_to_cpu(reparse_attr->reparse_data_length) -
-						  sizeof(wsl_link_data->type);
+					  sizeof(wsl_link_data->type);
 				ni->target = kvzalloc(lth + 1, GFP_NOFS);
 				if (ni->target) {
 					memcpy(ni->target, wsl_link_data->link, lth);
 					ni->target[lth] = 0;
-					mode = ntfs_reparse_tag_mode(reparse_attr);
+					err = 0;
 				}
 			}
 			break;
+		}
 		default:
-			mode = ntfs_reparse_tag_mode(reparse_attr);
+			err = 0;
+		}
+
+		if (!err) {
+			mode = ntfs_reparse_tag_mode(reparse_attr->reparse_tag);
+			ni->reparse_tag = reparse_attr->reparse_tag;
 		}
 	} else
 		ni->flags &= ~FILE_ATTR_REPARSE_POINT;
@@ -286,6 +336,7 @@ unsigned int ntfs_reparse_tag_dt_types(struct ntfs_volume *vol, unsigned long mr
 
 	if (reparse_attr && attr_size) {
 		switch (reparse_attr->reparse_tag) {
+		case IO_REPARSE_TAG_MOUNT_POINT:
 		case IO_REPARSE_TAG_SYMLINK:
 		case IO_REPARSE_TAG_LX_SYMLINK:
 			dt_type = DT_LNK;
@@ -311,6 +362,109 @@ unsigned int ntfs_reparse_tag_dt_types(struct ntfs_volume *vol, unsigned long mr
 	return dt_type;
 }
 
+/*
+ * ntfs_translate_symlink_path
+ *
+ * @dentry: dentry of the symlink/junction being resolved
+ * @target: NUL-terminated NLS target string with '\\' already normalized to '/'
+ * @translated: out parameter, set to a newly kmalloc'd relative path on success
+ *
+ * Windows junctions (IO_REPARSE_TAG_MOUNT_POINT) and non-relative symlinks
+ * (IO_REPARSE_TAG_SYMLINK without SYMLINK_FLAG_RELATIVE) store substitute
+ * names such as "/??/C:/foo", "//?/C:/foo", "/foo", or "C:/foo". Linux
+ * cannot continue pathname lookup from those syntaxes, so rewrite them as a
+ * path relative to the symlink's containing directory on this NTFS volume,
+ * anchored at the volume root via "../".
+ *
+ * Note: bind-mounted subtrees of the volume may resolve to unexpected
+ * locations because the computed "../" depth is relative to the NTFS volume
+ * root, not the bind-mounted subtree root.
+ *
+ * Return: 0 on success with *translated set to a newly allocated string the
+ * caller must kfree(); negative errno on failure.
+ */
+int ntfs_translate_symlink_path(struct dentry *dentry, const char *target,
+				char **translated)
+{
+	char *buf, *link_path, *out, *p;
+	const char *path, *tail;
+	unsigned int up_levels = 0;
+	size_t tail_len, out_len;
+	int err;
+
+	if (!dentry || !target || !translated)
+		return -EINVAL;
+
+	path = target;
+	/* reject UNC path. */
+	if (path[0] == '/' && path[1] == '/' &&
+	    !(path[2] == '?' && path[3] == '/'))
+		return -EOPNOTSUPP;
+
+	/* target starts with "/??/" or "//?/"? */
+	if ((path[0] == '/' && path[1] == '?' && path[2] == '?' && path[3] == '/') ||
+	    (path[0] == '/' && path[1] == '/' && path[2] == '?' && path[3] == '/'))
+		path += 4;
+
+	/* target must start with a drive character or '/'. */
+	if (((path[0] >= 'A' && path[0] <= 'Z') ||
+	     (path[0] >= 'a' && path[0] <= 'z')) && path[1] == ':') {
+		if (path[2] && path[2] != '/')
+			return -EOPNOTSUPP;
+		tail = path + 2;
+		if (*tail == '/')
+			tail++;
+	} else if (*path == '/') {
+		tail = path + 1;
+	} else {
+		return -EOPNOTSUPP;
+	}
+
+	tail_len = strlen(tail);
+
+	buf = kmalloc(PATH_MAX, GFP_NOFS);
+	if (!buf)
+		return -ENOMEM;
+
+	link_path = dentry_path_raw(dentry, buf, PATH_MAX);
+	if (IS_ERR(link_path)) {
+		err = PTR_ERR(link_path);
+		goto out;
+	}
+
+	/* count '/' after the leading slash. */
+	for (p = link_path + 1; *p; p++)
+		if (*p == '/')
+			up_levels++;
+
+	/* build "./" + ("../" * up_levels) + tail. */
+	out_len = 2 + up_levels * 3 + tail_len;
+	if (out_len >= PATH_MAX) {
+		err = -ENAMETOOLONG;
+		goto out;
+	}
+
+	out = kmalloc(out_len + 1, GFP_NOFS);
+	if (!out) {
+		err = -ENOMEM;
+		goto out;
+	}
+
+	memcpy(out, "./", 2);
+	p = out + 2;
+	while (up_levels--) {
+		memcpy(p, "../", 3);
+		p += 3;
+	}
+	memcpy(p, tail, tail_len + 1);
+
+	*translated = out;
+	err = 0;
+out:
+	kfree(buf);
+	return err;
+}
+
 /*
  * Set the index for new reparse data
  */
diff --git a/fs/ntfs/reparse.h b/fs/ntfs/reparse.h
index 28da40257f2a..ed7b93c359c1 100644
--- a/fs/ntfs/reparse.h
+++ b/fs/ntfs/reparse.h
@@ -11,6 +11,8 @@ extern __le16 reparse_index_name[];
 
 unsigned int ntfs_make_symlink(struct ntfs_inode *ni);
 unsigned int ntfs_reparse_tag_dt_types(struct ntfs_volume *vol, unsigned long mref);
+int ntfs_translate_symlink_path(struct dentry *dentry, const char *target,
+				char **translated);
 int ntfs_reparse_set_wsl_symlink(struct ntfs_inode *ni,
 			const __le16 *target, int target_len);
 int ntfs_reparse_set_wsl_not_symlink(struct ntfs_inode *ni, mode_t mode);

-- 
2.43.0


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH 4/7] ntfs: add native_symlink mount option
  2026-06-12  7:33 [PATCH 0/7] ntfs: Support Windows native symbolic links Hyunchul Lee
                   ` (2 preceding siblings ...)
  2026-06-12  7:33 ` [PATCH 3/7] ntfs: support following Windows native symlink with absolute paths Hyunchul Lee
@ 2026-06-12  7:33 ` Hyunchul Lee
  2026-06-12  7:33 ` [PATCH 5/7] ntfs: clean up target name conversion for WSL symlinks Hyunchul Lee
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 10+ messages in thread
From: Hyunchul Lee @ 2026-06-12  7:33 UTC (permalink / raw)
  To: Namjae Jeon; +Cc: Hyunchul Lee, linux-fsdevel, linux-kernel

Because bind-mounted subtrees of the volume may resolve to unexpected
locations, change converting junctions and non-relative symbolic links
into paths relative to the NTFS volume to be allowed only if the
native_symlink=rel mount option is specified.

Add the native_symlink=<value> mount option to configure how absolute
symbolic links and mount points (junctions) are handled.
The option accepts "raw" or "rel", with "raw" being the default.

Under "raw", the absolute target path (ni->target) is returned as-is
without translation. Under "rel", ntfs_translate_junction() is called
to rewrite the absolute path as a relative path anchored at the volume
root.

Signed-off-by: Hyunchul Lee <hyc.lee@gmail.com>
---
 fs/ntfs/file.c   | 14 ++++++++------
 fs/ntfs/inode.c  |  4 ++++
 fs/ntfs/super.c  | 19 +++++++++++++++++++
 fs/ntfs/volume.h |  3 +++
 4 files changed, 34 insertions(+), 6 deletions(-)

diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
index 6b0dfc56577b..6a7b638e523d 100644
--- a/fs/ntfs/file.c
+++ b/fs/ntfs/file.c
@@ -22,6 +22,7 @@
 #include "ea.h"
 #include "iomap.h"
 #include "bitmap.h"
+#include "volume.h"
 
 #include <linux/filelock.h>
 
@@ -688,12 +689,13 @@ static const char *ntfs_get_link(struct dentry *dentry, struct inode *inode,
 	if (ni->reparse_tag == IO_REPARSE_TAG_MOUNT_POINT ||
 	    (ni->reparse_tag == IO_REPARSE_TAG_SYMLINK &&
 	     !(ni->reparse_flags & cpu_to_le32(SYMLINK_FLAG_RELATIVE)))) {
-		err = ntfs_translate_symlink_path(dentry, ni->target, &target);
-		if (err < 0)
-			return ERR_PTR(err);
-
-		set_delayed_call(done, kfree_link, target);
-		return target;
+		if (NVolNativeSymlinkRel(ni->vol)) {
+			err = ntfs_translate_symlink_path(dentry, ni->target, &target);
+			if (err < 0)
+				return ERR_PTR(err);
+			set_delayed_call(done, kfree_link, target);
+			return target;
+		}
 	}
 
 	return ni->target;
diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index 07ca799a8f9a..76595f2e30ff 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -2378,6 +2378,10 @@ int ntfs_show_options(struct seq_file *sf, struct dentry *root)
 		seq_puts(sf, ",discard");
 	if (NVolDisableSparse(vol))
 		seq_puts(sf, ",disable_sparse");
+	if (NVolNativeSymlinkRel(vol))
+		seq_puts(sf, ",native_symlink=rel");
+	else
+		seq_puts(sf, ",native_symlink=raw");
 	if (vol->sb->s_flags & SB_POSIXACL)
 		seq_puts(sf, ",acl");
 	return 0;
diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
index ca882e946a22..e032a247455c 100644
--- a/fs/ntfs/super.c
+++ b/fs/ntfs/super.c
@@ -43,6 +43,17 @@ static const struct constant_table ntfs_param_enums[] = {
 	{}
 };
 
+enum {
+	NATIVE_SYMLINK_RAW,
+	NATIVE_SYMLINK_REL,
+};
+
+static const struct constant_table ntfs_native_symlink_enums[] = {
+	{ "raw",		NATIVE_SYMLINK_RAW },
+	{ "rel",		NATIVE_SYMLINK_REL },
+	{}
+};
+
 enum {
 	Opt_uid,
 	Opt_gid,
@@ -66,6 +77,7 @@ enum {
 	Opt_acl,
 	Opt_discard,
 	Opt_nocase,
+	Opt_native_symlink,
 };
 
 static const struct fs_parameter_spec ntfs_parameters[] = {
@@ -91,6 +103,7 @@ static const struct fs_parameter_spec ntfs_parameters[] = {
 	fsparam_flag("discard",			Opt_discard),
 	fsparam_flag("sparse",			Opt_sparse),
 	fsparam_flag("nocase",			Opt_nocase),
+	fsparam_enum("native_symlink",		Opt_native_symlink, ntfs_native_symlink_enums),
 	{}
 };
 
@@ -215,6 +228,12 @@ static int ntfs_parse_param(struct fs_context *fc, struct fs_parameter *param)
 		else
 			NVolClearDisableSparse(vol);
 		break;
+	case Opt_native_symlink:
+		if (result.uint_32 == NATIVE_SYMLINK_REL)
+			NVolSetNativeSymlinkRel(vol);
+		else
+			NVolClearNativeSymlinkRel(vol);
+		break;
 	case Opt_sparse:
 		break;
 	default:
diff --git a/fs/ntfs/volume.h b/fs/ntfs/volume.h
index 3348394dbc0d..55298689a7bb 100644
--- a/fs/ntfs/volume.h
+++ b/fs/ntfs/volume.h
@@ -177,6 +177,7 @@ struct ntfs_volume {
  *
  * NV_Discard			Issue discard/TRIM commands for freed clusters.
  * NV_DisableSparse		Disable creation of sparse regions.
+ * NV_NativeSymlinkRel		Translate absolute Windows reparse targets (native_symlink=rel).
  */
 enum {
 	NV_Errors,
@@ -194,6 +195,7 @@ enum {
 	NV_CheckWindowsNames,
 	NV_Discard,
 	NV_DisableSparse,
+	NV_NativeSymlinkRel,
 };
 
 /*
@@ -230,6 +232,7 @@ DEFINE_NVOL_BIT_OPS(HideDotFiles)
 DEFINE_NVOL_BIT_OPS(CheckWindowsNames)
 DEFINE_NVOL_BIT_OPS(Discard)
 DEFINE_NVOL_BIT_OPS(DisableSparse)
+DEFINE_NVOL_BIT_OPS(NativeSymlinkRel)
 
 static inline void ntfs_inc_free_clusters(struct ntfs_volume *vol, s64 nr)
 {

-- 
2.43.0


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH 5/7] ntfs: clean up target name conversion for WSL symlinks
  2026-06-12  7:33 [PATCH 0/7] ntfs: Support Windows native symbolic links Hyunchul Lee
                   ` (3 preceding siblings ...)
  2026-06-12  7:33 ` [PATCH 4/7] ntfs: add native_symlink mount option Hyunchul Lee
@ 2026-06-12  7:33 ` Hyunchul Lee
  2026-06-12  7:33 ` [PATCH 6/7] ntfs: support creating Windows native symlinks Hyunchul Lee
  2026-06-12  7:33 ` [PATCH 7/7] docs/fs/ntfs: add mount options to support Windows native symbolic links Hyunchul Lee
  6 siblings, 0 replies; 10+ messages in thread
From: Hyunchul Lee @ 2026-06-12  7:33 UTC (permalink / raw)
  To: Namjae Jeon; +Cc: Hyunchul Lee, linux-fsdevel, linux-kernel

WSL symlink target names are stored as narrow NLS/UTF-8 strings on
disk. Converting the target name to Unicode in ntfs_symlink and
converting it back to NLS in ntfs_reparse_set_wsl_symlink is
redundant.

Remove this conversion and pass the symname directly to the reparse
data setter.

Signed-off-by: Hyunchul Lee <hyc.lee@gmail.com>
---
 fs/ntfs/namei.c   | 17 ++---------------
 fs/ntfs/reparse.c | 49 ++++++++++++++++++++++++-------------------------
 fs/ntfs/reparse.h |  2 +-
 3 files changed, 27 insertions(+), 41 deletions(-)

diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c
index 9c1c36acfad2..88c0b05dde3b 100644
--- a/fs/ntfs/namei.c
+++ b/fs/ntfs/namei.c
@@ -394,7 +394,7 @@ static int ntfs_sd_add_everyone(struct ntfs_inode *ni)
 
 static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *dir,
 		__le16 *name, u8 name_len, mode_t mode, dev_t dev,
-		__le16 *target, int target_len)
+		const char *target, int target_len)
 {
 	struct ntfs_inode *dir_ni = NTFS_I(dir);
 	struct ntfs_volume *vol = dir_ni->vol;
@@ -1409,9 +1409,7 @@ static int ntfs_symlink(struct mnt_idmap *idmap, struct inode *dir,
 	int err = 0;
 	struct ntfs_inode *ni;
 	__le16 *usrc;
-	__le16 *utarget;
 	int usrc_len;
-	int utarget_len;
 	int symlen = strlen(symname);
 
 	if (NVolShutdown(vol))
@@ -1432,23 +1430,12 @@ static int ntfs_symlink(struct mnt_idmap *idmap, struct inode *dir,
 		goto out;
 	}
 
-	utarget_len = ntfs_nlstoucs(vol, symname, symlen, &utarget,
-				    PATH_MAX);
-	if (utarget_len < 0) {
-		if (utarget_len != -ENAMETOOLONG)
-			ntfs_error(sb, "Failed to convert target name to Unicode.");
-		err =  -ENOMEM;
-		kmem_cache_free(ntfs_name_cache, usrc);
-		goto out;
-	}
-
 	if (!(vol->vol_flags & VOLUME_IS_DIRTY))
 		ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
 
 	ni = __ntfs_create(idmap, dir, usrc, usrc_len, S_IFLNK | 0777, 0,
-			utarget, utarget_len);
+			   symname, symlen);
 	kmem_cache_free(ntfs_name_cache, usrc);
-	kvfree(utarget);
 	if (IS_ERR(ni)) {
 		err = PTR_ERR(ni);
 		goto out;
diff --git a/fs/ntfs/reparse.c b/fs/ntfs/reparse.c
index fb8c42a27699..eb1e4424e50d 100644
--- a/fs/ntfs/reparse.c
+++ b/fs/ntfs/reparse.c
@@ -750,39 +750,38 @@ static int ntfs_set_ntfs_reparse_data(struct ntfs_inode *ni, char *value, size_t
  * Set reparse data for a WSL type symlink
  */
 int ntfs_reparse_set_wsl_symlink(struct ntfs_inode *ni,
-		const __le16 *target, int target_len)
+				 const char *target, int target_len)
 {
 	int err = 0;
-	int len;
 	int reparse_len;
-	unsigned char *utarget = NULL;
 	struct reparse_point *reparse;
 	struct wsl_link_reparse_data *data;
 
-	len = ntfs_ucstonls(ni->vol, target, target_len, &utarget, 0);
-	if (len <= 0)
-		return -EINVAL;
-
-	reparse_len = sizeof(struct reparse_point) + sizeof(data->type) + len;
+	reparse_len = sizeof(struct reparse_point) + sizeof(data->type) +
+		target_len;
 	reparse = kvzalloc(reparse_len, GFP_NOFS);
-	if (!reparse) {
-		err = -ENOMEM;
-		kfree(utarget);
-	} else {
-		data = (struct wsl_link_reparse_data *)reparse->reparse_data;
-		reparse->reparse_tag = IO_REPARSE_TAG_LX_SYMLINK;
-		reparse->reparse_data_length =
-			cpu_to_le16(sizeof(data->type) + len);
-		reparse->reserved = 0;
-		data->type = cpu_to_le32(2);
-		memcpy(data->link, utarget, len);
-		err = ntfs_set_ntfs_reparse_data(ni,
-				(char *)reparse, reparse_len);
+	if (!reparse)
+		return -ENOMEM;
+
+	ni->target = kstrdup(target, GFP_NOFS);
+	if (!ni->target) {
 		kvfree(reparse);
-		if (!err)
-			ni->target = utarget;
-		else
-			kfree(utarget);
+		return -ENOMEM;
+	}
+
+	data = (struct wsl_link_reparse_data *)reparse->reparse_data;
+	reparse->reparse_tag = IO_REPARSE_TAG_LX_SYMLINK;
+	reparse->reparse_data_length =
+		cpu_to_le16(sizeof(data->type) + target_len);
+	reparse->reserved = 0;
+	data->type = cpu_to_le32(2);
+	memcpy(data->link, target, target_len);
+	err = ntfs_set_ntfs_reparse_data(ni,
+					 (char *)reparse, reparse_len);
+	kvfree(reparse);
+	if (err) {
+		kfree(ni->target);
+		ni->target = NULL;
 	}
 	return err;
 }
diff --git a/fs/ntfs/reparse.h b/fs/ntfs/reparse.h
index ed7b93c359c1..e36557f29677 100644
--- a/fs/ntfs/reparse.h
+++ b/fs/ntfs/reparse.h
@@ -14,7 +14,7 @@ unsigned int ntfs_reparse_tag_dt_types(struct ntfs_volume *vol, unsigned long mr
 int ntfs_translate_symlink_path(struct dentry *dentry, const char *target,
 				char **translated);
 int ntfs_reparse_set_wsl_symlink(struct ntfs_inode *ni,
-			const __le16 *target, int target_len);
+				 const char *target, int target_len);
 int ntfs_reparse_set_wsl_not_symlink(struct ntfs_inode *ni, mode_t mode);
 int ntfs_delete_reparse_index(struct ntfs_inode *ni);
 int ntfs_remove_ntfs_reparse_data(struct ntfs_inode *ni);

-- 
2.43.0


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH 6/7] ntfs: support creating Windows native symlinks
  2026-06-12  7:33 [PATCH 0/7] ntfs: Support Windows native symbolic links Hyunchul Lee
                   ` (4 preceding siblings ...)
  2026-06-12  7:33 ` [PATCH 5/7] ntfs: clean up target name conversion for WSL symlinks Hyunchul Lee
@ 2026-06-12  7:33 ` Hyunchul Lee
  2026-06-12 16:28   ` CharSyam
  2026-06-12  7:33 ` [PATCH 7/7] docs/fs/ntfs: add mount options to support Windows native symbolic links Hyunchul Lee
  6 siblings, 1 reply; 10+ messages in thread
From: Hyunchul Lee @ 2026-06-12  7:33 UTC (permalink / raw)
  To: Namjae Jeon; +Cc: Hyunchul Lee, linux-fsdevel, linux-kernel

And introduce the symlink=<value> mount option to configure how symbolic
links are created. The option accepts "wsl" or "native", with "wsl"
being the default.

Signed-off-by: Hyunchul Lee <hyc.lee@gmail.com>
---
 fs/ntfs/inode.c   |   4 ++
 fs/ntfs/namei.c   |   5 ++-
 fs/ntfs/reparse.c | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 fs/ntfs/reparse.h |   2 +
 fs/ntfs/super.c   |  19 +++++++++
 fs/ntfs/volume.h  |   2 +
 6 files changed, 155 insertions(+), 1 deletion(-)

diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index 76595f2e30ff..c2715521e562 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -2382,6 +2382,10 @@ int ntfs_show_options(struct seq_file *sf, struct dentry *root)
 		seq_puts(sf, ",native_symlink=rel");
 	else
 		seq_puts(sf, ",native_symlink=raw");
+	if (NVolSymlinkNative(vol))
+		seq_puts(sf, ",symlink=native");
+	else
+		seq_puts(sf, ",symlink=wsl");
 	if (vol->sb->s_flags & SB_POSIXACL)
 		seq_puts(sf, ",acl");
 	return 0;
diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c
index 88c0b05dde3b..78c159519f9c 100644
--- a/fs/ntfs/namei.c
+++ b/fs/ntfs/namei.c
@@ -608,7 +608,10 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d
 			goto err_out;
 
 		if (S_ISLNK(mode)) {
-			err = ntfs_reparse_set_wsl_symlink(ni, target, target_len);
+			if (NVolSymlinkNative(vol))
+				err = ntfs_reparse_set_native_symlink(ni, target, target_len);
+			else
+				err = ntfs_reparse_set_wsl_symlink(ni, target, target_len);
 			if (!err)
 				rollback_reparse = true;
 		} else if (S_ISBLK(mode) || S_ISCHR(mode) || S_ISSOCK(mode) ||
diff --git a/fs/ntfs/reparse.c b/fs/ntfs/reparse.c
index eb1e4424e50d..f5b2a853bea1 100644
--- a/fs/ntfs/reparse.c
+++ b/fs/ntfs/reparse.c
@@ -786,6 +786,130 @@ int ntfs_reparse_set_wsl_symlink(struct ntfs_inode *ni,
 	return err;
 }
 
+int ntfs_reparse_set_native_symlink(struct ntfs_inode *ni,
+				    const char *target, int target_len)
+{
+	int err = 0;
+	bool is_absolute;
+	char *norm_name = NULL;
+	char *sub_name = NULL;
+	char *prt_name = NULL;
+	__le16 *sub_name_utf16 = NULL;
+	__le16 *prt_name_utf16 = NULL;
+	int sub_len, prt_len;
+	int total_data_len, total_reparse_len;
+	struct reparse_point *reparse = NULL;
+	struct symlink_reparse_data *data;
+	int i;
+
+	/* Determine if target is absolute (starts with drive letter like C:) */
+	is_absolute = (target_len > 1 && target[1] == ':');
+
+	/* Normalize and prepare NLS paths */
+	norm_name = kstrdup(target, GFP_NOFS);
+	if (!norm_name)
+		return -ENOMEM;
+
+	/* Replace '/' with '\' */
+	for (i = 0; i < target_len; i++) {
+		if (norm_name[i] == '/')
+			norm_name[i] = '\\';
+	}
+
+	if (is_absolute) {
+		prt_name = kstrdup(norm_name, GFP_NOFS);
+		if (!prt_name) {
+			err = -ENOMEM;
+			goto out;
+		}
+		/* Prepend '\??\' to Substitutename */
+		sub_name = kmalloc(target_len + 5, GFP_NOFS);
+		if (!sub_name) {
+			err = -ENOMEM;
+			goto out;
+		}
+		strscpy(sub_name, "\\??\\", target_len + 5);
+		strcat(sub_name, norm_name);
+	} else {
+		/* For relative symlinks (including absolute paths without drive letters),
+		 * SubstituteName and PrintName are identical.
+		 */
+		prt_name = kstrdup(norm_name, GFP_NOFS);
+		sub_name = kstrdup(norm_name, GFP_NOFS);
+		if (!prt_name || !sub_name) {
+			err = -ENOMEM;
+			goto out;
+		}
+	}
+
+	/* Convert NLS paths to UTF-16 */
+	sub_len = ntfs_nlstoucs(ni->vol, sub_name, strlen(sub_name),
+				&sub_name_utf16, PATH_MAX);
+	if (sub_len < 0) {
+		err = sub_len;
+		goto out;
+	}
+
+	prt_len = ntfs_nlstoucs(ni->vol, prt_name, strlen(prt_name),
+				&prt_name_utf16, PATH_MAX);
+	if (prt_len < 0) {
+		err = prt_len;
+		goto out;
+	}
+
+	/* Check for buffer size limits */
+	total_data_len = sizeof(struct symlink_reparse_data) +
+		(sub_len + prt_len) * sizeof(__le16);
+	if (total_data_len > 16384) { /* 16KB max reparse tag size */
+		err = -EFBIG;
+		goto out;
+	}
+
+	total_reparse_len = sizeof(struct reparse_point) + total_data_len;
+	reparse = kvzalloc(total_reparse_len, GFP_NOFS);
+	if (!reparse) {
+		err = -ENOMEM;
+		goto out;
+	}
+
+	/* Pack fields in reparse buffer */
+	reparse->reparse_tag = IO_REPARSE_TAG_SYMLINK;
+	reparse->reparse_data_length = cpu_to_le16(total_data_len);
+	reparse->reserved = 0;
+
+	data = (struct symlink_reparse_data *)reparse->reparse_data;
+	data->substitute_name_offset = 0;
+	data->substitute_name_length = cpu_to_le16(sub_len * sizeof(__le16));
+	data->print_name_offset = data->substitute_name_length;
+	data->print_name_length = cpu_to_le16(prt_len * sizeof(__le16));
+	data->flags = cpu_to_le32(is_absolute ? 0 : SYMLINK_FLAG_RELATIVE);
+
+	/* Copy names to path_buffer */
+	memcpy(data->path_buffer, sub_name_utf16, sub_len * sizeof(__le16));
+	memcpy(data->path_buffer + sub_len, prt_name_utf16, prt_len * sizeof(__le16));
+
+	err = ntfs_set_ntfs_reparse_data(ni, (char *)reparse, total_reparse_len);
+	if (!err) {
+		for (i = 0; i < target_len; i++) {
+			if (norm_name[i] == '\\')
+				norm_name[i] = '/';
+		}
+		ni->target = norm_name;
+		norm_name = NULL;
+	}
+
+out:
+	kfree(norm_name);
+	kfree(sub_name);
+	kfree(prt_name);
+	if (sub_name_utf16)
+		kvfree(sub_name_utf16);
+	if (prt_name_utf16)
+		kvfree(prt_name_utf16);
+	kvfree(reparse);
+	return err;
+}
+
 /*
  * Set reparse data for a WSL special file other than a symlink
  * (socket, fifo, character or block device)
diff --git a/fs/ntfs/reparse.h b/fs/ntfs/reparse.h
index e36557f29677..c11a5bb7e6a5 100644
--- a/fs/ntfs/reparse.h
+++ b/fs/ntfs/reparse.h
@@ -15,6 +15,8 @@ int ntfs_translate_symlink_path(struct dentry *dentry, const char *target,
 				char **translated);
 int ntfs_reparse_set_wsl_symlink(struct ntfs_inode *ni,
 				 const char *target, int target_len);
+int ntfs_reparse_set_native_symlink(struct ntfs_inode *ni,
+				    const char *symname, int symlen);
 int ntfs_reparse_set_wsl_not_symlink(struct ntfs_inode *ni, mode_t mode);
 int ntfs_delete_reparse_index(struct ntfs_inode *ni);
 int ntfs_remove_ntfs_reparse_data(struct ntfs_inode *ni);
diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
index e032a247455c..8abe7bee4c0d 100644
--- a/fs/ntfs/super.c
+++ b/fs/ntfs/super.c
@@ -54,6 +54,17 @@ static const struct constant_table ntfs_native_symlink_enums[] = {
 	{}
 };
 
+enum {
+	SYMLINK_WSL,
+	SYMLINK_NATIVE,
+};
+
+static const struct constant_table ntfs_symlink_enums[] = {
+	{ "wsl",		SYMLINK_WSL },
+	{ "native",		SYMLINK_NATIVE },
+	{}
+};
+
 enum {
 	Opt_uid,
 	Opt_gid,
@@ -78,6 +89,7 @@ enum {
 	Opt_discard,
 	Opt_nocase,
 	Opt_native_symlink,
+	Opt_symlink,
 };
 
 static const struct fs_parameter_spec ntfs_parameters[] = {
@@ -104,6 +116,7 @@ static const struct fs_parameter_spec ntfs_parameters[] = {
 	fsparam_flag("sparse",			Opt_sparse),
 	fsparam_flag("nocase",			Opt_nocase),
 	fsparam_enum("native_symlink",		Opt_native_symlink, ntfs_native_symlink_enums),
+	fsparam_enum("symlink",			Opt_symlink, ntfs_symlink_enums),
 	{}
 };
 
@@ -234,6 +247,12 @@ static int ntfs_parse_param(struct fs_context *fc, struct fs_parameter *param)
 		else
 			NVolClearNativeSymlinkRel(vol);
 		break;
+	case Opt_symlink:
+		if (result.uint_32 == SYMLINK_NATIVE)
+			NVolSetSymlinkNative(vol);
+		else
+			NVolClearSymlinkNative(vol);
+		break;
 	case Opt_sparse:
 		break;
 	default:
diff --git a/fs/ntfs/volume.h b/fs/ntfs/volume.h
index 55298689a7bb..65fd3908af26 100644
--- a/fs/ntfs/volume.h
+++ b/fs/ntfs/volume.h
@@ -196,6 +196,7 @@ enum {
 	NV_Discard,
 	NV_DisableSparse,
 	NV_NativeSymlinkRel,
+	NV_SymlinkNative,
 };
 
 /*
@@ -233,6 +234,7 @@ DEFINE_NVOL_BIT_OPS(CheckWindowsNames)
 DEFINE_NVOL_BIT_OPS(Discard)
 DEFINE_NVOL_BIT_OPS(DisableSparse)
 DEFINE_NVOL_BIT_OPS(NativeSymlinkRel)
+DEFINE_NVOL_BIT_OPS(SymlinkNative)
 
 static inline void ntfs_inc_free_clusters(struct ntfs_volume *vol, s64 nr)
 {

-- 
2.43.0


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH 7/7] docs/fs/ntfs: add mount options to support Windows native symbolic links
  2026-06-12  7:33 [PATCH 0/7] ntfs: Support Windows native symbolic links Hyunchul Lee
                   ` (5 preceding siblings ...)
  2026-06-12  7:33 ` [PATCH 6/7] ntfs: support creating Windows native symlinks Hyunchul Lee
@ 2026-06-12  7:33 ` Hyunchul Lee
  6 siblings, 0 replies; 10+ messages in thread
From: Hyunchul Lee @ 2026-06-12  7:33 UTC (permalink / raw)
  To: Namjae Jeon; +Cc: Hyunchul Lee, linux-fsdevel, linux-kernel

Introduce the "symlink=<value>" and the "native_symlink=<value>" mount
options to configure the creation behavior of symbolic links and support
creating Windows native symbolic links (reparse points with the
IO_REPARSE_TAG_SYMLINK tag).

Signed-off-by: Hyunchul Lee <hyc.lee@gmail.com>
---
 Documentation/filesystems/ntfs.rst | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/Documentation/filesystems/ntfs.rst b/Documentation/filesystems/ntfs.rst
index 5c96b04a4d7a..4bfa392daec6 100644
--- a/Documentation/filesystems/ntfs.rst
+++ b/Documentation/filesystems/ntfs.rst
@@ -156,4 +156,17 @@ windows_names=<BOOL>    Refuse creation/rename of files with characters or
 discard=<BOOL>          Issue block device discard for clusters freed on
                         file deletion/truncation to inform underlying
                         storage.
+
+native_symlink=raw|rel  Configure how absolute symbolic links and mount
+                        points (junctions) are handled. Under "raw"
+                        (default), the absolute target path is returned
+                        as-is without translation. Under "rel", it is
+                        rewritten as a relative path anchored at
+                        the volume root.
+
+symlink=wsl|native      Configure how symbolic links are created. Under
+                        "wsl" (default), WSL (Windows Subsystem for
+                        Linux) compatible symlinks are created. Under
+                        "native", Windows native symbolic links are
+                        created.
 ======================= ====================================================

-- 
2.43.0


^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH 2/7] ntfs: support following Windows native symlink with relative paths
  2026-06-12  7:33 ` [PATCH 2/7] ntfs: support following Windows native symlink with relative paths Hyunchul Lee
@ 2026-06-12 16:20   ` CharSyam
  0 siblings, 0 replies; 10+ messages in thread
From: CharSyam @ 2026-06-12 16:20 UTC (permalink / raw)
  To: Hyunchul Lee; +Cc: Namjae Jeon, linux-fsdevel, linux-kernel

Hi, Hyunchul.

Can we validate `attr_size >= sizeof(struct reparse_point)` before this
switch? I can reproduce a KASAN slab-out-of-bounds by setting a resident
`$REPARSE_POINT` value length to 1 byte. `ntfs_attr_readall()` allocates a
1-byte buffer, then this line reads 4 bytes from `reparse_tag` before
`valid_reparse_buffer()` rejects the malformed attribute. The same guard is
also needed in `ntfs_reparse_tag_dt_types()`, which KASAN hits via
`ls -> getdents64 -> ntfs_readdir()`.

Thanks.
DaeMyung.

2026년 6월 12일 (금) 오후 4:37, Hyunchul Lee <hyc.lee@gmail.com>님이 작성:
>
> Make ntfs_make_symlink() parse native Windows symbolic link reparse
> payloads when the SYMLINK_FLAG_RELATIVE bit is set.
> Implement the following changes:
>  * Add a dedicated on-disk layout definition for symbolic link reparse
>    data.
>  * validate the UTF-16 name ranges before decoding them.
>  * convert the substitute name into the mount's NLS and normalize path
>    separators.
>
> Signed-off-by: Hyunchul Lee <hyc.lee@gmail.com>
> ---
>  fs/ntfs/inode.c   |  36 +++++++++-------
>  fs/ntfs/layout.h  |  11 +++++
>  fs/ntfs/reparse.c | 122 +++++++++++++++++++++++++++++++++++++++++++++++++-----
>  3 files changed, 143 insertions(+), 26 deletions(-)
>
> diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
> index efb34a5e94d9..8894f33b46ca 100644
> --- a/fs/ntfs/inode.c
> +++ b/fs/ntfs/inode.c
> @@ -863,8 +863,26 @@ static int ntfs_read_locked_inode(struct inode *vi)
>                 ntfs_ea_get_wsl_inode(vi, &dev, flags);
>         }
>
> -       if (m->flags & MFT_RECORD_IS_DIRECTORY) {
> +       if (ni->flags & FILE_ATTR_REPARSE_POINT) {
> +               unsigned int mode;
> +
> +               mode = ntfs_make_symlink(ni);
> +               if (mode)
> +                       vi->i_mode |= mode;
> +               else {
> +                       vi->i_mode &= ~S_IFLNK;
> +                       if (m->flags & MFT_RECORD_IS_DIRECTORY)
> +                               vi->i_mode |= S_IFDIR;
> +                       else
> +                               vi->i_mode |= S_IFREG;
> +               }
> +       } else if (m->flags & MFT_RECORD_IS_DIRECTORY) {
>                 vi->i_mode |= S_IFDIR;
> +       } else {
> +               vi->i_mode |= S_IFREG;
> +       }
> +
> +       if (S_ISDIR(vi->i_mode)) {
>                 /*
>                  * Apply the directory permissions mask set in the mount
>                  * options.
> @@ -874,18 +892,6 @@ static int ntfs_read_locked_inode(struct inode *vi)
>                 if (vi->i_nlink > 1)
>                         set_nlink(vi, 1);
>         } else {
> -               if (ni->flags & FILE_ATTR_REPARSE_POINT) {
> -                       unsigned int mode;
> -
> -                       mode = ntfs_make_symlink(ni);
> -                       if (mode)
> -                               vi->i_mode |= mode;
> -                       else {
> -                               vi->i_mode &= ~S_IFLNK;
> -                               vi->i_mode |= S_IFREG;
> -                       }
> -               } else
> -                       vi->i_mode |= S_IFREG;
>                 /* Apply the file permissions mask set in the mount options. */
>                 vi->i_mode &= ~vol->fmask;
>         }
> @@ -894,7 +900,7 @@ static int ntfs_read_locked_inode(struct inode *vi)
>          * If an attribute list is present we now have the attribute list value
>          * in ntfs_ino->attr_list and it is ntfs_ino->attr_list_size bytes.
>          */
> -       if (S_ISDIR(vi->i_mode)) {
> +       if (m->flags & MFT_RECORD_IS_DIRECTORY) {
>                 struct index_root *ir;
>
>  view_index_meta:
> @@ -1018,7 +1024,7 @@ static int ntfs_read_locked_inode(struct inode *vi)
>                 m = NULL;
>                 ctx = NULL;
>                 /* Setup the operations for this inode. */
> -               ntfs_set_vfs_operations(vi, S_IFDIR, 0);
> +               ntfs_set_vfs_operations(vi, vi->i_mode, 0);
>                 if (ir->index.flags & LARGE_INDEX)
>                         NInoSetIndexAllocPresent(ni);
>         } else {
> diff --git a/fs/ntfs/layout.h b/fs/ntfs/layout.h
> index d94f914e830f..94af6efa04af 100644
> --- a/fs/ntfs/layout.h
> +++ b/fs/ntfs/layout.h
> @@ -2267,6 +2267,8 @@ enum {
>         IO_REPARSE_PLUGIN_SELECT        = cpu_to_le32(0xffff0fff),
>  };
>
> +#define SYMLINK_FLAG_RELATIVE          1
> +
>  /*
>   * struct reparse_point - $REPARSE_POINT attribute content (0xc0)\
>   *
> @@ -2287,6 +2289,15 @@ struct reparse_point {
>         u8 reparse_data[];
>  } __packed;
>
> +struct symlink_reparse_data {
> +       __le16 substitute_name_offset;
> +       __le16 substitute_name_length;
> +       __le16 print_name_offset;
> +       __le16 print_name_length;
> +       __le32 flags;
> +       __le16 path_buffer[];
> +} __packed;
> +
>  /*
>   * struct ea_information - $EA_INFORMATION attribute content (0xd0)
>   *
> diff --git a/fs/ntfs/reparse.c b/fs/ntfs/reparse.c
> index 74713716813f..4cc37f1c9c90 100644
> --- a/fs/ntfs/reparse.c
> +++ b/fs/ntfs/reparse.c
> @@ -24,6 +24,47 @@ struct wsl_link_reparse_data {
>         char    link[];
>  };
>
> +static bool reparse_name_is_valid(size_t size, size_t name_off, u16 len)
> +{
> +       if ((name_off | len) & 1)
> +               return false;
> +
> +       return name_off + len <= size;
> +}
> +
> +/*
> + * Windows-native reparse payloads store pathnames as UTF-16 strings with '\\'
> + * separators. Convert the on-disk UTF-16 target into the mount's NLS and
> + * normalize path separators.
> + */
> +static int ntfs_reparse_target_to_nls(struct ntfs_volume *vol,
> +                                     const __le16 *uname, u16 ulen,
> +                                     char **target)
> +{
> +       int err, i;
> +
> +       *target = NULL;
> +       ulen >>= 1;
> +       if (!ulen)
> +               return -EINVAL;
> +
> +       if (!uname[ulen - 1])
> +               ulen--;
> +
> +       err = ntfs_ucstonls(vol, uname, ulen, (unsigned char **)target, 0);
> +       if (err < 0) {
> +               ntfs_attr_name_free((unsigned char **)target);
> +               return err;
> +       }
> +
> +       for (i = 0; i < err; i++) {
> +               if ((*target)[i] == '\\')
> +                       (*target)[i] = '/';
> +       }
> +
> +       return 0;
> +}
> +
>  /* Index entry in $Extend/$Reparse */
>  struct reparse_index {
>         struct index_entry_header header;
> @@ -38,8 +79,10 @@ __le16 reparse_index_name[] = {cpu_to_le16('$'), cpu_to_le16('R'), 0};
>   * Check if the reparse point attribute buffer is valid.
>   * Returns true if valid, false otherwise.
>   */
> -static bool ntfs_is_valid_reparse_buffer(struct ntfs_inode *ni,
> -               const struct reparse_point *reparse_attr, size_t size)
> +static bool valid_reparse_buffer(struct ntfs_inode *ni,
> +                                const struct reparse_point *reparse_attr,
> +                                size_t size,
> +                                size_t payload_min_len)
>  {
>         size_t expected;
>
> @@ -50,6 +93,11 @@ static bool ntfs_is_valid_reparse_buffer(struct ntfs_inode *ni,
>         if (size < sizeof(struct reparse_point))
>                 return false;
>
> +       /* The payload must contain the fixed fields for the current tag. */
> +       if (payload_min_len &&
> +           le16_to_cpu(reparse_attr->reparse_data_length) < payload_min_len)
> +               return false;
> +
>         /* Reserved zero tag is invalid */
>         if (reparse_attr->reparse_tag == IO_REPARSE_TAG_RESERVED_ZERO)
>                 return false;
> @@ -79,24 +127,54 @@ static bool ntfs_is_valid_reparse_buffer(struct ntfs_inode *ni,
>  static bool valid_reparse_data(struct ntfs_inode *ni,
>                 const struct reparse_point *reparse_attr, size_t size)
>  {
> -       const struct wsl_link_reparse_data *wsl_reparse_data =
> -               (const struct wsl_link_reparse_data *)reparse_attr->reparse_data;
> -       unsigned int data_len = le16_to_cpu(reparse_attr->reparse_data_length);
> +       switch (reparse_attr->reparse_tag) {
> +       case IO_REPARSE_TAG_SYMLINK:
> +       {
> +               struct symlink_reparse_data *data;
> +               size_t data_offs;
>
> -       if (ntfs_is_valid_reparse_buffer(ni, reparse_attr, size) == false)
> -               return false;
> +               if (!valid_reparse_buffer(ni, reparse_attr, size,
> +                                         sizeof(*data)))
> +                       return false;
>
> -       switch (reparse_attr->reparse_tag) {
> +               data = (struct symlink_reparse_data *)reparse_attr->reparse_data;
> +               data_offs = offsetof(struct reparse_point, reparse_data) +
> +                       offsetof(struct symlink_reparse_data, path_buffer);
> +
> +               if (!reparse_name_is_valid(size,
> +                                          data_offs +
> +                                          le16_to_cpu(data->substitute_name_offset),
> +                                          le16_to_cpu(data->substitute_name_length)) ||
> +                   !reparse_name_is_valid(size,
> +                                          data_offs +
> +                                          le16_to_cpu(data->print_name_offset),
> +                                          le16_to_cpu(data->print_name_length)))
> +                       return false;
> +               break;
> +       }
>         case IO_REPARSE_TAG_LX_SYMLINK:
> -               if (data_len <= sizeof(wsl_reparse_data->type) ||
> -                   wsl_reparse_data->type != cpu_to_le32(2))
> +       {
> +               struct wsl_link_reparse_data *data;
> +
> +               if (!valid_reparse_buffer(ni, reparse_attr, size,
> +                                         sizeof(*data)))
> +                       return false;
> +
> +               data = (struct wsl_link_reparse_data *)reparse_attr->reparse_data;
> +
> +               if (le16_to_cpu(reparse_attr->reparse_data_length) <= sizeof(data->type) ||
> +                   data->type != cpu_to_le32(2))
>                         return false;
>                 break;
> +       }
>         case IO_REPARSE_TAG_AF_UNIX:
>         case IO_REPARSE_TAG_LX_FIFO:
>         case IO_REPARSE_TAG_LX_CHR:
>         case IO_REPARSE_TAG_LX_BLK:
> -               if (data_len || !(ni->flags & FILE_ATTRIBUTE_RECALL_ON_OPEN))
> +               if (!valid_reparse_buffer(ni, reparse_attr, size, 0))
> +                       return false;
> +               if (le16_to_cpu(reparse_attr->reparse_data_length) ||
> +                   !(ni->flags & FILE_ATTRIBUTE_RECALL_ON_OPEN))
>                         return false;
>         }
>
> @@ -134,16 +212,38 @@ static unsigned int ntfs_reparse_tag_mode(struct reparse_point *reparse_attr)
>  unsigned int ntfs_make_symlink(struct ntfs_inode *ni)
>  {
>         s64 attr_size = 0;
> +       int err;
>         unsigned int lth;
>         struct reparse_point *reparse_attr;
>         struct wsl_link_reparse_data *wsl_link_data;
>         unsigned int mode = 0;
>
> +       kvfree(ni->target);
> +       ni->target = NULL;
> +
>         reparse_attr = ntfs_attr_readall(ni, AT_REPARSE_POINT, NULL, 0,
>                                          &attr_size);
>         if (reparse_attr && attr_size &&
>             valid_reparse_data(ni, reparse_attr, attr_size)) {
>                 switch (reparse_attr->reparse_tag) {
> +               case IO_REPARSE_TAG_SYMLINK:
> +               {
> +                       struct symlink_reparse_data *data =
> +                               (struct symlink_reparse_data *)reparse_attr->reparse_data;
> +                       const __le16 *name = (const __le16 *)((u8 *)data->path_buffer +
> +                                                       le16_to_cpu(data->substitute_name_offset));
> +
> +                       mode = ntfs_reparse_tag_mode(reparse_attr);
> +                       if (!(data->flags & cpu_to_le32(SYMLINK_FLAG_RELATIVE)))
> +                               break;
> +
> +                       err = ntfs_reparse_target_to_nls(ni->vol, name,
> +                                                        le16_to_cpu(data->substitute_name_length),
> +                                                        &ni->target);
> +                       if (err < 0)
> +                               mode = 0;
> +                       break;
> +               }
>                 case IO_REPARSE_TAG_LX_SYMLINK:
>                         wsl_link_data =
>                                 (struct wsl_link_reparse_data *)reparse_attr->reparse_data;
>
> --
> 2.43.0
>
>

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH 6/7] ntfs: support creating Windows native symlinks
  2026-06-12  7:33 ` [PATCH 6/7] ntfs: support creating Windows native symlinks Hyunchul Lee
@ 2026-06-12 16:28   ` CharSyam
  0 siblings, 0 replies; 10+ messages in thread
From: CharSyam @ 2026-06-12 16:28 UTC (permalink / raw)
  To: Hyunchul Lee; +Cc: Namjae Jeon, linux-fsdevel, linux-kernel

Hi, Hyunchul.

+       err = ntfs_set_ntfs_reparse_data(ni, (char *)reparse,
total_reparse_len);
+       if (!err) {
+               for (i = 0; i < target_len; i++) {
+                       if (norm_name[i] == '\\')
+                               norm_name[i] = '/';
+               }
+               ni->target = norm_name;
+               norm_name = NULL;
+       }
+

This leaves newly-created native absolute symlinks inconsistent with the
same inode after remount. For `ln -s C:/foo`, readlink returns `C:/foo`
immediately after creation, but after remount it returns `./foo` in
`native_symlink=rel` and `/??/C:/foo` in `native_symlink=raw`.

The create path only updates `ni->target`; it does not populate
`ni->reparse_tag`/`ni->reparse_flags`, so `ntfs_get_link()` cannot apply
the absolute native symlink handling until the inode is reloaded from disk.

+ /* Determine if target is absolute (starts with drive letter like C:) */
+ is_absolute = (target_len > 1 && target[1] == ':');

This treats `C:foo` as an absolute Windows path, but that form is
drive-relative and should remain a relative symlink target. In QEMU,
`ln -s C:foo` works before remount, but after remount readlink fails in
`native_symlink=rel` and raw mode exposes `/??/C:foo`.

Please only classify drive-letter targets as absolute when the colon is
followed by a path separator, e.g. `C:/foo` or `C:\foo`.

Thanks.
DaeMyung.

2026년 6월 12일 (금) 오후 4:41, Hyunchul Lee <hyc.lee@gmail.com>님이 작성:
>
> And introduce the symlink=<value> mount option to configure how symbolic
> links are created. The option accepts "wsl" or "native", with "wsl"
> being the default.
>
> Signed-off-by: Hyunchul Lee <hyc.lee@gmail.com>
> ---
>  fs/ntfs/inode.c   |   4 ++
>  fs/ntfs/namei.c   |   5 ++-
>  fs/ntfs/reparse.c | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
>  fs/ntfs/reparse.h |   2 +
>  fs/ntfs/super.c   |  19 +++++++++
>  fs/ntfs/volume.h  |   2 +
>  6 files changed, 155 insertions(+), 1 deletion(-)
>
> diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
> index 76595f2e30ff..c2715521e562 100644
> --- a/fs/ntfs/inode.c
> +++ b/fs/ntfs/inode.c
> @@ -2382,6 +2382,10 @@ int ntfs_show_options(struct seq_file *sf, struct dentry *root)
>                 seq_puts(sf, ",native_symlink=rel");
>         else
>                 seq_puts(sf, ",native_symlink=raw");
> +       if (NVolSymlinkNative(vol))
> +               seq_puts(sf, ",symlink=native");
> +       else
> +               seq_puts(sf, ",symlink=wsl");
>         if (vol->sb->s_flags & SB_POSIXACL)
>                 seq_puts(sf, ",acl");
>         return 0;
> diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c
> index 88c0b05dde3b..78c159519f9c 100644
> --- a/fs/ntfs/namei.c
> +++ b/fs/ntfs/namei.c
> @@ -608,7 +608,10 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d
>                         goto err_out;
>
>                 if (S_ISLNK(mode)) {
> -                       err = ntfs_reparse_set_wsl_symlink(ni, target, target_len);
> +                       if (NVolSymlinkNative(vol))
> +                               err = ntfs_reparse_set_native_symlink(ni, target, target_len);
> +                       else
> +                               err = ntfs_reparse_set_wsl_symlink(ni, target, target_len);
>                         if (!err)
>                                 rollback_reparse = true;
>                 } else if (S_ISBLK(mode) || S_ISCHR(mode) || S_ISSOCK(mode) ||
> diff --git a/fs/ntfs/reparse.c b/fs/ntfs/reparse.c
> index eb1e4424e50d..f5b2a853bea1 100644
> --- a/fs/ntfs/reparse.c
> +++ b/fs/ntfs/reparse.c
> @@ -786,6 +786,130 @@ int ntfs_reparse_set_wsl_symlink(struct ntfs_inode *ni,
>         return err;
>  }
>
> +int ntfs_reparse_set_native_symlink(struct ntfs_inode *ni,
> +                                   const char *target, int target_len)
> +{
> +       int err = 0;
> +       bool is_absolute;
> +       char *norm_name = NULL;
> +       char *sub_name = NULL;
> +       char *prt_name = NULL;
> +       __le16 *sub_name_utf16 = NULL;
> +       __le16 *prt_name_utf16 = NULL;
> +       int sub_len, prt_len;
> +       int total_data_len, total_reparse_len;
> +       struct reparse_point *reparse = NULL;
> +       struct symlink_reparse_data *data;
> +       int i;
> +
> +       /* Determine if target is absolute (starts with drive letter like C:) */
> +       is_absolute = (target_len > 1 && target[1] == ':');
> +
> +       /* Normalize and prepare NLS paths */
> +       norm_name = kstrdup(target, GFP_NOFS);
> +       if (!norm_name)
> +               return -ENOMEM;
> +
> +       /* Replace '/' with '\' */
> +       for (i = 0; i < target_len; i++) {
> +               if (norm_name[i] == '/')
> +                       norm_name[i] = '\\';
> +       }
> +
> +       if (is_absolute) {
> +               prt_name = kstrdup(norm_name, GFP_NOFS);
> +               if (!prt_name) {
> +                       err = -ENOMEM;
> +                       goto out;
> +               }
> +               /* Prepend '\??\' to Substitutename */
> +               sub_name = kmalloc(target_len + 5, GFP_NOFS);
> +               if (!sub_name) {
> +                       err = -ENOMEM;
> +                       goto out;
> +               }
> +               strscpy(sub_name, "\\??\\", target_len + 5);
> +               strcat(sub_name, norm_name);
> +       } else {
> +               /* For relative symlinks (including absolute paths without drive letters),
> +                * SubstituteName and PrintName are identical.
> +                */
> +               prt_name = kstrdup(norm_name, GFP_NOFS);
> +               sub_name = kstrdup(norm_name, GFP_NOFS);
> +               if (!prt_name || !sub_name) {
> +                       err = -ENOMEM;
> +                       goto out;
> +               }
> +       }
> +
> +       /* Convert NLS paths to UTF-16 */
> +       sub_len = ntfs_nlstoucs(ni->vol, sub_name, strlen(sub_name),
> +                               &sub_name_utf16, PATH_MAX);
> +       if (sub_len < 0) {
> +               err = sub_len;
> +               goto out;
> +       }
> +
> +       prt_len = ntfs_nlstoucs(ni->vol, prt_name, strlen(prt_name),
> +                               &prt_name_utf16, PATH_MAX);
> +       if (prt_len < 0) {
> +               err = prt_len;
> +               goto out;
> +       }
> +
> +       /* Check for buffer size limits */
> +       total_data_len = sizeof(struct symlink_reparse_data) +
> +               (sub_len + prt_len) * sizeof(__le16);
> +       if (total_data_len > 16384) { /* 16KB max reparse tag size */
> +               err = -EFBIG;
> +               goto out;
> +       }
> +
> +       total_reparse_len = sizeof(struct reparse_point) + total_data_len;
> +       reparse = kvzalloc(total_reparse_len, GFP_NOFS);
> +       if (!reparse) {
> +               err = -ENOMEM;
> +               goto out;
> +       }
> +
> +       /* Pack fields in reparse buffer */
> +       reparse->reparse_tag = IO_REPARSE_TAG_SYMLINK;
> +       reparse->reparse_data_length = cpu_to_le16(total_data_len);
> +       reparse->reserved = 0;
> +
> +       data = (struct symlink_reparse_data *)reparse->reparse_data;
> +       data->substitute_name_offset = 0;
> +       data->substitute_name_length = cpu_to_le16(sub_len * sizeof(__le16));
> +       data->print_name_offset = data->substitute_name_length;
> +       data->print_name_length = cpu_to_le16(prt_len * sizeof(__le16));
> +       data->flags = cpu_to_le32(is_absolute ? 0 : SYMLINK_FLAG_RELATIVE);
> +
> +       /* Copy names to path_buffer */
> +       memcpy(data->path_buffer, sub_name_utf16, sub_len * sizeof(__le16));
> +       memcpy(data->path_buffer + sub_len, prt_name_utf16, prt_len * sizeof(__le16));
> +
> +       err = ntfs_set_ntfs_reparse_data(ni, (char *)reparse, total_reparse_len);
> +       if (!err) {
> +               for (i = 0; i < target_len; i++) {
> +                       if (norm_name[i] == '\\')
> +                               norm_name[i] = '/';
> +               }
> +               ni->target = norm_name;
> +               norm_name = NULL;
> +       }
> +
> +out:
> +       kfree(norm_name);
> +       kfree(sub_name);
> +       kfree(prt_name);
> +       if (sub_name_utf16)
> +               kvfree(sub_name_utf16);
> +       if (prt_name_utf16)
> +               kvfree(prt_name_utf16);
> +       kvfree(reparse);
> +       return err;
> +}
> +
>  /*
>   * Set reparse data for a WSL special file other than a symlink
>   * (socket, fifo, character or block device)
> diff --git a/fs/ntfs/reparse.h b/fs/ntfs/reparse.h
> index e36557f29677..c11a5bb7e6a5 100644
> --- a/fs/ntfs/reparse.h
> +++ b/fs/ntfs/reparse.h
> @@ -15,6 +15,8 @@ int ntfs_translate_symlink_path(struct dentry *dentry, const char *target,
>                                 char **translated);
>  int ntfs_reparse_set_wsl_symlink(struct ntfs_inode *ni,
>                                  const char *target, int target_len);
> +int ntfs_reparse_set_native_symlink(struct ntfs_inode *ni,
> +                                   const char *symname, int symlen);
>  int ntfs_reparse_set_wsl_not_symlink(struct ntfs_inode *ni, mode_t mode);
>  int ntfs_delete_reparse_index(struct ntfs_inode *ni);
>  int ntfs_remove_ntfs_reparse_data(struct ntfs_inode *ni);
> diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
> index e032a247455c..8abe7bee4c0d 100644
> --- a/fs/ntfs/super.c
> +++ b/fs/ntfs/super.c
> @@ -54,6 +54,17 @@ static const struct constant_table ntfs_native_symlink_enums[] = {
>         {}
>  };
>
> +enum {
> +       SYMLINK_WSL,
> +       SYMLINK_NATIVE,
> +};
> +
> +static const struct constant_table ntfs_symlink_enums[] = {
> +       { "wsl",                SYMLINK_WSL },
> +       { "native",             SYMLINK_NATIVE },
> +       {}
> +};
> +
>  enum {
>         Opt_uid,
>         Opt_gid,
> @@ -78,6 +89,7 @@ enum {
>         Opt_discard,
>         Opt_nocase,
>         Opt_native_symlink,
> +       Opt_symlink,
>  };
>
>  static const struct fs_parameter_spec ntfs_parameters[] = {
> @@ -104,6 +116,7 @@ static const struct fs_parameter_spec ntfs_parameters[] = {
>         fsparam_flag("sparse",                  Opt_sparse),
>         fsparam_flag("nocase",                  Opt_nocase),
>         fsparam_enum("native_symlink",          Opt_native_symlink, ntfs_native_symlink_enums),
> +       fsparam_enum("symlink",                 Opt_symlink, ntfs_symlink_enums),
>         {}
>  };
>
> @@ -234,6 +247,12 @@ static int ntfs_parse_param(struct fs_context *fc, struct fs_parameter *param)
>                 else
>                         NVolClearNativeSymlinkRel(vol);
>                 break;
> +       case Opt_symlink:
> +               if (result.uint_32 == SYMLINK_NATIVE)
> +                       NVolSetSymlinkNative(vol);
> +               else
> +                       NVolClearSymlinkNative(vol);
> +               break;
>         case Opt_sparse:
>                 break;
>         default:
> diff --git a/fs/ntfs/volume.h b/fs/ntfs/volume.h
> index 55298689a7bb..65fd3908af26 100644
> --- a/fs/ntfs/volume.h
> +++ b/fs/ntfs/volume.h
> @@ -196,6 +196,7 @@ enum {
>         NV_Discard,
>         NV_DisableSparse,
>         NV_NativeSymlinkRel,
> +       NV_SymlinkNative,
>  };
>
>  /*
> @@ -233,6 +234,7 @@ DEFINE_NVOL_BIT_OPS(CheckWindowsNames)
>  DEFINE_NVOL_BIT_OPS(Discard)
>  DEFINE_NVOL_BIT_OPS(DisableSparse)
>  DEFINE_NVOL_BIT_OPS(NativeSymlinkRel)
> +DEFINE_NVOL_BIT_OPS(SymlinkNative)
>
>  static inline void ntfs_inc_free_clusters(struct ntfs_volume *vol, s64 nr)
>  {
>
> --
> 2.43.0
>
>

^ permalink raw reply	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2026-06-12 16:29 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-06-12  7:33 [PATCH 0/7] ntfs: Support Windows native symbolic links Hyunchul Lee
2026-06-12  7:33 ` [PATCH 1/7] ntfs: fix incorrect size of symbolic link Hyunchul Lee
2026-06-12  7:33 ` [PATCH 2/7] ntfs: support following Windows native symlink with relative paths Hyunchul Lee
2026-06-12 16:20   ` CharSyam
2026-06-12  7:33 ` [PATCH 3/7] ntfs: support following Windows native symlink with absolute paths Hyunchul Lee
2026-06-12  7:33 ` [PATCH 4/7] ntfs: add native_symlink mount option Hyunchul Lee
2026-06-12  7:33 ` [PATCH 5/7] ntfs: clean up target name conversion for WSL symlinks Hyunchul Lee
2026-06-12  7:33 ` [PATCH 6/7] ntfs: support creating Windows native symlinks Hyunchul Lee
2026-06-12 16:28   ` CharSyam
2026-06-12  7:33 ` [PATCH 7/7] docs/fs/ntfs: add mount options to support Windows native symbolic links Hyunchul Lee

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®