ntfs: support following Windows native symlink with relative paths

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>
This commit is contained in:
Hyunchul Lee 2026-06-15 08:49:53 +09:00 committed by Namjae Jeon
parent 05a5ff86a7
commit 517cd625ad
3 changed files with 148 additions and 28 deletions

View File

@ -863,8 +863,26 @@ static int ntfs_read_locked_inode(struct inode *vi)
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 @@ static int ntfs_read_locked_inode(struct inode *vi)
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 @@ static int ntfs_read_locked_inode(struct inode *vi)
* 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 @@ static int ntfs_read_locked_inode(struct inode *vi)
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 {

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

View 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_LX_SYMLINK:
if (data_len <= sizeof(wsl_reparse_data->type) ||
wsl_reparse_data->type != cpu_to_le32(2))
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:
{
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: