From: Hyunchul Lee Date: Sun, 14 Jun 2026 23:49:57 +0000 (+0900) Subject: ntfs: support creating Windows native symlinks X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=d8f1df2e133f203cae3f458cba44efa327b093d9;p=thirdparty%2Flinux.git ntfs: support creating Windows native symlinks And introduce the symlink= 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 Signed-off-by: Namjae Jeon --- 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) {