Re: [PATCH 6/7] ntfs: support creating Windows native symlinks
From: CharSyam
Date: Fri Jun 12 2026 - 12:39:26 EST
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@xxxxxxxxx>님이 작성:
>
> 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@xxxxxxxxx>
> ---
> 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
>
>