mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Hyunchul Lee <hyc.lee@gmail.com>
To: Namjae Jeon <linkinjeon@kernel.org>
Cc: Hyunchul Lee <hyc.lee@gmail.com>,
	linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [PATCH v2 6/7] ntfs: support creating Windows native symlinks
Date: Mon, 15 Jun 2026 08:49:57 +0900	[thread overview]
Message-ID: <20260615-topic-symlink-v2-6-648a769d01f8@gmail.com> (raw)
In-Reply-To: <20260615-topic-symlink-v2-0-648a769d01f8@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 | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
 fs/ntfs/reparse.h |   2 +
 fs/ntfs/super.c   |  19 ++++++++
 fs/ntfs/volume.h  |   2 +
 6 files changed, 160 insertions(+), 3 deletions(-)

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 91ae0c75e275..fa523dc3691e 100644
--- a/fs/ntfs/reparse.c
+++ b/fs/ntfs/reparse.c
@@ -365,6 +365,13 @@ unsigned int ntfs_reparse_tag_dt_types(struct ntfs_volume *vol, unsigned long mr
 	return dt_type;
 }
 
+static bool ntfs_is_drive_letter(const char *target)
+{
+	return ((target[0] >= 'A' && target[0] <= 'Z') ||
+		(target[0] >= 'a' && target[0] <= 'z')) &&
+		target[1] == ':';
+}
+
 /*
  * ntfs_translate_symlink_path
  *
@@ -410,8 +417,7 @@ int ntfs_translate_symlink_path(struct dentry *dentry, const char *target,
 		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 (ntfs_is_drive_letter(path)) {
 		if (path[2] && path[2] != '/')
 			return -EOPNOTSUPP;
 		tail = path + 2;
@@ -792,6 +798,127 @@ 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, prt_sub_shared = true;
+	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:/ or C:\) */
+	is_absolute = target_len > 2 &&
+		ntfs_is_drive_letter(target) &&
+		(target[2] == '/' || target[2] == '\\');
+
+
+	/* Normalize and prepare NLS paths */
+	prt_name = kstrdup(target, GFP_NOFS);
+	if (!prt_name)
+		return -ENOMEM;
+
+	/* Replace '/' with '\' */
+	for (i = 0; i < target_len; i++) {
+		if (prt_name[i] == '/')
+			prt_name[i] = '\\';
+	}
+
+	if (is_absolute) {
+		/* Prepend '\??\' to Substitutename */
+		sub_name = kmalloc(target_len + 5, GFP_NOFS);
+		if (!sub_name) {
+			err = -ENOMEM;
+			goto out;
+		}
+		snprintf(sub_name, target_len + 5, "\\??\\%s", prt_name);
+		prt_sub_shared = false;
+	} else {
+		/* For relative symlinks (including absolute paths without drive letters),
+		 * SubstituteName and PrintName are identical.
+		 */
+		sub_name = prt_name;
+	}
+
+	/* 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 = is_absolute ? 0 : cpu_to_le32(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) {
+		int len = strlen(sub_name);
+
+		for (i = 0; i < len; i++) {
+			if (sub_name[i] == '\\')
+				sub_name[i] = '/';
+		}
+		ni->target = sub_name;
+		sub_name = NULL;
+		if (prt_sub_shared)
+			prt_name = NULL;
+		ni->reparse_tag = IO_REPARSE_TAG_SYMLINK;
+		ni->reparse_flags = is_absolute ? 0 :
+			cpu_to_le32(SYMLINK_FLAG_RELATIVE);
+	}
+
+out:
+	kfree(prt_name);
+	if (!prt_sub_shared)
+		kfree(sub_name);
+	kvfree(sub_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


  parent reply	other threads:[~2026-06-14 23:51 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-14 23:49 [PATCH v2 0/7] ntfs: Support Windows native symbolic links Hyunchul Lee
2026-06-14 23:49 ` [PATCH v2 1/7] ntfs: fix incorrect size of symbolic link Hyunchul Lee
2026-06-14 23:49 ` [PATCH v2 2/7] ntfs: support following Windows native symlink with relative paths Hyunchul Lee
2026-06-14 23:49 ` [PATCH v2 3/7] ntfs: support following Windows native symlink with absolute paths Hyunchul Lee
2026-06-14 23:49 ` [PATCH v2 4/7] ntfs: add native_symlink mount option Hyunchul Lee
2026-06-14 23:49 ` [PATCH v2 5/7] ntfs: clean up target name conversion for WSL symlinks Hyunchul Lee
2026-06-14 23:49 ` Hyunchul Lee [this message]
2026-06-14 23:49 ` [PATCH v2 7/7] docs/fs/ntfs: add mount options to support Windows native symbolic links Hyunchul Lee
2026-06-15 23:57 ` [PATCH v2 0/7] ntfs: Support " Namjae Jeon

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260615-topic-symlink-v2-6-648a769d01f8@gmail.com \
    --to=hyc.lee@gmail.com \
    --cc=linkinjeon@kernel.org \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®