summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHyunchul Lee <hyc.lee@gmail.com>2026-06-15 02:49:53 +0300
committerNamjae Jeon <linkinjeon@kernel.org>2026-06-15 13:39:34 +0300
commit517cd625ad79b8bfa429092ad1536ee2dd477e68 (patch)
tree4f4c739e7a7943c955452b696b6321cc804a0b9d
parent05a5ff86a7f12c861e3516d3dc4d092ce620742d (diff)
downloadlinux-517cd625ad79b8bfa429092ad1536ee2dd477e68.tar.xz
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>
-rw-r--r--fs/ntfs/inode.c36
-rw-r--r--fs/ntfs/layout.h11
-rw-r--r--fs/ntfs/reparse.c127
3 files changed, 147 insertions, 27 deletions
diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index efb34a5e94d9..8894f33b46ca 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -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 {
diff --git a/fs/ntfs/layout.h b/fs/ntfs/layout.h
index d94f914e830f..94af6efa04af 100644
--- a/fs/ntfs/layout.h
+++ b/fs/ntfs/layout.h
@@ -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)
*
diff --git a/fs/ntfs/reparse.c b/fs/ntfs/reparse.c
index 74713716813f..4714196185d9 100644
--- a/fs/ntfs/reparse.c
+++ b/fs/ntfs/reparse.c
@@ -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: