[PATCH 3/7] ntfs: support following Windows native symlink with absolute paths

From: Hyunchul Lee

Date: Fri Jun 12 2026 - 03:38:22 EST


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@xxxxxxxxx>
---
fs/ntfs/file.c | 22 ++++++-
fs/ntfs/inode.c | 2 +
fs/ntfs/inode.h | 2 +
fs/ntfs/layout.h | 8 +++
fs/ntfs/reparse.c | 182 +++++++++++++++++++++++++++++++++++++++++++++++++-----
fs/ntfs/reparse.h | 2 +
6 files changed, 202 insertions(+), 16 deletions(-)

diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
index 264cf8404385..6b0dfc56577b 100644
--- a/fs/ntfs/file.c
+++ b/fs/ntfs/file.c
@@ -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,
diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index 8894f33b46ca..07ca799a8f9a 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -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;
}
diff --git a/fs/ntfs/inode.h b/fs/ntfs/inode.h
index 67942b97fac6..9aacd5787ffe 100644
--- a/fs/ntfs/inode.h
+++ b/fs/ntfs/inode.h
@@ -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;
};

diff --git a/fs/ntfs/layout.h b/fs/ntfs/layout.h
index 94af6efa04af..9438fd9b668e 100644
--- a/fs/ntfs/layout.h
+++ b/fs/ntfs/layout.h
@@ -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;
diff --git a/fs/ntfs/reparse.c b/fs/ntfs/reparse.c
index 4cc37f1c9c90..fb8c42a27699 100644
--- a/fs/ntfs/reparse.c
+++ b/fs/ntfs/reparse.c
@@ -128,6 +128,29 @@ static bool valid_reparse_data(struct ntfs_inode *ni,
const struct reparse_point *reparse_attr, size_t size)
{
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;
@@ -176,16 +199,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;
@@ -215,17 +244,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 && attr_size &&
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 =
@@ -233,33 +278,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;
@@ -286,6 +336,7 @@ unsigned int ntfs_reparse_tag_dt_types(struct ntfs_volume *vol, unsigned long mr

if (reparse_attr && attr_size) {
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;
@@ -311,6 +362,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
*/
diff --git a/fs/ntfs/reparse.h b/fs/ntfs/reparse.h
index 28da40257f2a..ed7b93c359c1 100644
--- a/fs/ntfs/reparse.h
+++ b/fs/ntfs/reparse.h
@@ -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);

--
2.43.0