ntfs: support following Windows native symlink with absolute paths

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>
This commit is contained in:
Hyunchul Lee
2026-06-15 08:49:54 +09:00
committed by Namjae Jeon
parent 517cd625ad
commit 2b58f93a13
6 changed files with 212 additions and 18 deletions

View 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,

View 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;
}

View 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;
};

View 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;

View 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;

View 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);