]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
ntfs: support following Windows native symlink with absolute paths
authorHyunchul Lee <hyc.lee@gmail.com>
Sun, 14 Jun 2026 23:49:54 +0000 (08:49 +0900)
committerNamjae Jeon <linkinjeon@kernel.org>
Mon, 15 Jun 2026 10:39:37 +0000 (19:39 +0900)
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>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
fs/ntfs/file.c
fs/ntfs/inode.c
fs/ntfs/inode.h
fs/ntfs/layout.h
fs/ntfs/reparse.c
fs/ntfs/reparse.h

index 264cf8404385c15bf9de8288f519f2cc629af904..6b0dfc56577ba7c73db8fde3fa449100c234360c 100644 (file)
@@ -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,
index 8894f33b46ca5e8c916b60f1a521bb96f37c6fc9..07ca799a8f9a7d0508fad1a8da85b15177d28abf 100644 (file)
@@ -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;
 }
index 67942b97fac6d6ed1b68badf012bac705306729d..9aacd5787ffea080d6acb693af3d124bfab90650 100644 (file)
@@ -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;
 };
 
index 94af6efa04af32e9393f0ba87f941ddbeb35c84e..9438fd9b668e231214694228a90239664c4adc79 100644 (file)
@@ -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;
index 4714196185d9f37852bac6a80e897a5283ea2264..33e5a4198e9bbf30ba72c9947037111cdc32bbb1 100644 (file)
@@ -131,6 +131,29 @@ static bool valid_reparse_data(struct ntfs_inode *ni,
                return false;
 
        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;
@@ -179,16 +202,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;
@@ -218,17 +247,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 &&
            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 =
@@ -236,33 +281,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;
@@ -289,6 +339,7 @@ unsigned int ntfs_reparse_tag_dt_types(struct ntfs_volume *vol, unsigned long mr
 
        if (reparse_attr && attr_size >= sizeof(*reparse_attr)) {
                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;
@@ -314,6 +365,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
  */
@@ -628,10 +782,14 @@ int ntfs_reparse_set_wsl_symlink(struct ntfs_inode *ni,
                err = ntfs_set_ntfs_reparse_data(ni,
                                (char *)reparse, reparse_len);
                kvfree(reparse);
-               if (!err)
+               if (!err) {
                        ni->target = utarget;
-               else
+                       ni->reparse_tag = IO_REPARSE_TAG_LX_SYMLINK;
+                       ni->reparse_flags = 0;
+               } else {
                        kfree(utarget);
+                       ni->target = NULL;
+               }
        }
        return err;
 }
@@ -671,6 +829,10 @@ int ntfs_reparse_set_wsl_not_symlink(struct ntfs_inode *ni, mode_t mode)
                err = ntfs_set_ntfs_reparse_data(ni, (char *)reparse,
                                                 reparse_len);
                kvfree(reparse);
+               if (!err) {
+                       ni->reparse_tag = reparse_tag;
+                       ni->reparse_flags = 0;
+               }
        }
 
        return err;
index 28da40257f2a407ef82fa82be04a843cebbe8fb7..ed7b93c359c122304f33f79b33820582a2afecc5 100644 (file)
@@ -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);