]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
ntfs: support following Windows native symlink with relative paths
authorHyunchul Lee <hyc.lee@gmail.com>
Sun, 14 Jun 2026 23:49:53 +0000 (08:49 +0900)
committerNamjae Jeon <linkinjeon@kernel.org>
Mon, 15 Jun 2026 10:39:34 +0000 (19:39 +0900)
Make ntfs_make_symlink() parse native Windows symbolic link reparse
payloads when the SYMLINK_FLAG_RELATIVE bit is set.
Implement the following changes:
 * Add a dedicated on-disk layout definition for symbolic link reparse
   data.
 * validate the UTF-16 name ranges before decoding them.
 * convert the substitute name into the mount's NLS and normalize path
   separators.

Signed-off-by: Hyunchul Lee <hyc.lee@gmail.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
fs/ntfs/inode.c
fs/ntfs/layout.h
fs/ntfs/reparse.c

index efb34a5e94d9429760b6d7637b264743e52db6b4..8894f33b46ca5e8c916b60f1a521bb96f37c6fc9 100644 (file)
@@ -863,8 +863,26 @@ skip_attr_list_load:
                ntfs_ea_get_wsl_inode(vi, &dev, flags);
        }
 
-       if (m->flags & MFT_RECORD_IS_DIRECTORY) {
+       if (ni->flags & FILE_ATTR_REPARSE_POINT) {
+               unsigned int mode;
+
+               mode = ntfs_make_symlink(ni);
+               if (mode)
+                       vi->i_mode |= mode;
+               else {
+                       vi->i_mode &= ~S_IFLNK;
+                       if (m->flags & MFT_RECORD_IS_DIRECTORY)
+                               vi->i_mode |= S_IFDIR;
+                       else
+                               vi->i_mode |= S_IFREG;
+               }
+       } else if (m->flags & MFT_RECORD_IS_DIRECTORY) {
                vi->i_mode |= S_IFDIR;
+       } else {
+               vi->i_mode |= S_IFREG;
+       }
+
+       if (S_ISDIR(vi->i_mode)) {
                /*
                 * Apply the directory permissions mask set in the mount
                 * options.
@@ -874,18 +892,6 @@ skip_attr_list_load:
                if (vi->i_nlink > 1)
                        set_nlink(vi, 1);
        } else {
-               if (ni->flags & FILE_ATTR_REPARSE_POINT) {
-                       unsigned int mode;
-
-                       mode = ntfs_make_symlink(ni);
-                       if (mode)
-                               vi->i_mode |= mode;
-                       else {
-                               vi->i_mode &= ~S_IFLNK;
-                               vi->i_mode |= S_IFREG;
-                       }
-               } else
-                       vi->i_mode |= S_IFREG;
                /* Apply the file permissions mask set in the mount options. */
                vi->i_mode &= ~vol->fmask;
        }
@@ -894,7 +900,7 @@ skip_attr_list_load:
         * If an attribute list is present we now have the attribute list value
         * in ntfs_ino->attr_list and it is ntfs_ino->attr_list_size bytes.
         */
-       if (S_ISDIR(vi->i_mode)) {
+       if (m->flags & MFT_RECORD_IS_DIRECTORY) {
                struct index_root *ir;
 
 view_index_meta:
@@ -1018,7 +1024,7 @@ view_index_meta:
                m = NULL;
                ctx = NULL;
                /* Setup the operations for this inode. */
-               ntfs_set_vfs_operations(vi, S_IFDIR, 0);
+               ntfs_set_vfs_operations(vi, vi->i_mode, 0);
                if (ir->index.flags & LARGE_INDEX)
                        NInoSetIndexAllocPresent(ni);
        } else {
index d94f914e830f3599453d2da0ddd061ca4a8a4a80..94af6efa04af32e9393f0ba87f941ddbeb35c84e 100644 (file)
@@ -2267,6 +2267,8 @@ enum {
        IO_REPARSE_PLUGIN_SELECT        = cpu_to_le32(0xffff0fff),
 };
 
+#define SYMLINK_FLAG_RELATIVE          1
+
 /*
  * struct reparse_point - $REPARSE_POINT attribute content (0xc0)\
  *
@@ -2287,6 +2289,15 @@ struct reparse_point {
        u8 reparse_data[];
 } __packed;
 
+struct symlink_reparse_data {
+       __le16 substitute_name_offset;
+       __le16 substitute_name_length;
+       __le16 print_name_offset;
+       __le16 print_name_length;
+       __le32 flags;
+       __le16 path_buffer[];
+} __packed;
+
 /*
  * struct ea_information - $EA_INFORMATION attribute content (0xd0)
  *
index 74713716813f22f49b23c7167ccef67b0665ec37..4714196185d9f37852bac6a80e897a5283ea2264 100644 (file)
@@ -24,6 +24,47 @@ struct wsl_link_reparse_data {
        char    link[];
 };
 
+static bool reparse_name_is_valid(size_t size, size_t name_off, u16 len)
+{
+       if ((name_off | len) & 1)
+               return false;
+
+       return name_off + len <= size;
+}
+
+/*
+ * Windows-native reparse payloads store pathnames as UTF-16 strings with '\\'
+ * separators. Convert the on-disk UTF-16 target into the mount's NLS and
+ * normalize path separators.
+ */
+static int ntfs_reparse_target_to_nls(struct ntfs_volume *vol,
+                                     const __le16 *uname, u16 ulen,
+                                     char **target)
+{
+       int err, i;
+
+       *target = NULL;
+       ulen >>= 1;
+       if (!ulen)
+               return -EINVAL;
+
+       if (!uname[ulen - 1])
+               ulen--;
+
+       err = ntfs_ucstonls(vol, uname, ulen, (unsigned char **)target, 0);
+       if (err < 0) {
+               ntfs_attr_name_free((unsigned char **)target);
+               return err;
+       }
+
+       for (i = 0; i < err; i++) {
+               if ((*target)[i] == '\\')
+                       (*target)[i] = '/';
+       }
+
+       return 0;
+}
+
 /* Index entry in $Extend/$Reparse */
 struct reparse_index {
        struct index_entry_header header;
@@ -38,8 +79,10 @@ __le16 reparse_index_name[] = {cpu_to_le16('$'), cpu_to_le16('R'), 0};
  * Check if the reparse point attribute buffer is valid.
  * Returns true if valid, false otherwise.
  */
-static bool ntfs_is_valid_reparse_buffer(struct ntfs_inode *ni,
-               const struct reparse_point *reparse_attr, size_t size)
+static bool valid_reparse_buffer(struct ntfs_inode *ni,
+                                const struct reparse_point *reparse_attr,
+                                size_t size,
+                                size_t payload_min_len)
 {
        size_t expected;
 
@@ -50,6 +93,11 @@ static bool ntfs_is_valid_reparse_buffer(struct ntfs_inode *ni,
        if (size < sizeof(struct reparse_point))
                return false;
 
+       /* The payload must contain the fixed fields for the current tag. */
+       if (payload_min_len &&
+           le16_to_cpu(reparse_attr->reparse_data_length) < payload_min_len)
+               return false;
+
        /* Reserved zero tag is invalid */
        if (reparse_attr->reparse_tag == IO_REPARSE_TAG_RESERVED_ZERO)
                return false;
@@ -79,24 +127,57 @@ static bool ntfs_is_valid_reparse_buffer(struct ntfs_inode *ni,
 static bool valid_reparse_data(struct ntfs_inode *ni,
                const struct reparse_point *reparse_attr, size_t size)
 {
-       const struct wsl_link_reparse_data *wsl_reparse_data =
-               (const struct wsl_link_reparse_data *)reparse_attr->reparse_data;
-       unsigned int data_len = le16_to_cpu(reparse_attr->reparse_data_length);
-
-       if (ntfs_is_valid_reparse_buffer(ni, reparse_attr, size) == false)
+       if (size < sizeof(*reparse_attr))
                return false;
 
        switch (reparse_attr->reparse_tag) {
+       case IO_REPARSE_TAG_SYMLINK:
+       {
+               struct symlink_reparse_data *data;
+               size_t data_offs;
+
+               if (!valid_reparse_buffer(ni, reparse_attr, size,
+                                         sizeof(*data)))
+                       return false;
+
+               data = (struct symlink_reparse_data *)reparse_attr->reparse_data;
+               data_offs = offsetof(struct reparse_point, reparse_data) +
+                       offsetof(struct symlink_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_LX_SYMLINK:
-               if (data_len <= sizeof(wsl_reparse_data->type) ||
-                   wsl_reparse_data->type != cpu_to_le32(2))
+       {
+               struct wsl_link_reparse_data *data;
+
+               if (!valid_reparse_buffer(ni, reparse_attr, size,
+                                         sizeof(*data)))
+                       return false;
+
+               data = (struct wsl_link_reparse_data *)reparse_attr->reparse_data;
+
+               if (le16_to_cpu(reparse_attr->reparse_data_length) <= sizeof(data->type) ||
+                   data->type != cpu_to_le32(2))
                        return false;
                break;
+       }
        case IO_REPARSE_TAG_AF_UNIX:
        case IO_REPARSE_TAG_LX_FIFO:
        case IO_REPARSE_TAG_LX_CHR:
        case IO_REPARSE_TAG_LX_BLK:
-               if (data_len || !(ni->flags & FILE_ATTRIBUTE_RECALL_ON_OPEN))
+               if (!valid_reparse_buffer(ni, reparse_attr, size, 0))
+                       return false;
+               if (le16_to_cpu(reparse_attr->reparse_data_length) ||
+                   !(ni->flags & FILE_ATTRIBUTE_RECALL_ON_OPEN))
                        return false;
        }
 
@@ -134,16 +215,38 @@ static unsigned int ntfs_reparse_tag_mode(struct reparse_point *reparse_attr)
 unsigned int ntfs_make_symlink(struct ntfs_inode *ni)
 {
        s64 attr_size = 0;
+       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;
+
        reparse_attr = ntfs_attr_readall(ni, AT_REPARSE_POINT, NULL, 0,
                                         &attr_size);
-       if (reparse_attr && attr_size &&
+       if (reparse_attr &&
            valid_reparse_data(ni, reparse_attr, attr_size)) {
                switch (reparse_attr->reparse_tag) {
+               case IO_REPARSE_TAG_SYMLINK:
+               {
+                       struct symlink_reparse_data *data =
+                               (struct symlink_reparse_data *)reparse_attr->reparse_data;
+                       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,
+                                                        le16_to_cpu(data->substitute_name_length),
+                                                        &ni->target);
+                       if (err < 0)
+                               mode = 0;
+                       break;
+               }
                case IO_REPARSE_TAG_LX_SYMLINK:
                        wsl_link_data =
                                (struct wsl_link_reparse_data *)reparse_attr->reparse_data;
@@ -184,7 +287,7 @@ unsigned int ntfs_reparse_tag_dt_types(struct ntfs_volume *vol, unsigned long mr
        reparse_attr = (struct reparse_point *)ntfs_attr_readall(NTFS_I(vi),
                        AT_REPARSE_POINT, NULL, 0, &attr_size);
 
-       if (reparse_attr && attr_size) {
+       if (reparse_attr && attr_size >= sizeof(*reparse_attr)) {
                switch (reparse_attr->reparse_tag) {
                case IO_REPARSE_TAG_SYMLINK:
                case IO_REPARSE_TAG_LX_SYMLINK: