]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
ntfs: support creating Windows native symlinks
authorHyunchul Lee <hyc.lee@gmail.com>
Sun, 14 Jun 2026 23:49:57 +0000 (08:49 +0900)
committerNamjae Jeon <linkinjeon@kernel.org>
Mon, 15 Jun 2026 10:39:43 +0000 (19:39 +0900)
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>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
fs/ntfs/inode.c
fs/ntfs/namei.c
fs/ntfs/reparse.c
fs/ntfs/reparse.h
fs/ntfs/super.c
fs/ntfs/volume.h

index 76595f2e30ffbe9af015a99e6bb06762ebbcbb53..c2715521e5628657b547b9cc9e7d37fc8e6f0c3d 100644 (file)
@@ -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;
index 88c0b05dde3bed914dbc36b6553b3c6a92a30df2..78c159519f9ce535f2265ce01a40eea1fe3c92d7 100644 (file)
@@ -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) ||
index 91ae0c75e275463f6d2457364f49e7314506c521..fa523dc3691ea1262c1db3cb3734a9685dd3ffe5 100644 (file)
@@ -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)
index e36557f29677de95f8c29470de6a8a11b40ca2e8..c11a5bb7e6a54f75024a66a8b1fe0910bbe541ec 100644 (file)
@@ -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);
index e032a247455c2de4b1e755434b9ab9c975accac3..8abe7bee4c0d9b6fe97d52c77bfbe35171aa6e84 100644 (file)
@@ -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:
index 55298689a7bbb45758ac9bb05333389228f2fd52..65fd3908af261a1a32b21aca8cabea2b89eb1a62 100644 (file)
@@ -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)
 {