Re: [PATCH 2/7] ntfs: support following Windows native symlink with relative paths
From: CharSyam
Date: Fri Jun 12 2026 - 12:24:42 EST
Hi, Hyunchul.
Can we validate `attr_size >= sizeof(struct reparse_point)` before this
switch? I can reproduce a KASAN slab-out-of-bounds by setting a resident
`$REPARSE_POINT` value length to 1 byte. `ntfs_attr_readall()` allocates a
1-byte buffer, then this line reads 4 bytes from `reparse_tag` before
`valid_reparse_buffer()` rejects the malformed attribute. The same guard is
also needed in `ntfs_reparse_tag_dt_types()`, which KASAN hits via
`ls -> getdents64 -> ntfs_readdir()`.
Thanks.
DaeMyung.
2026년 6월 12일 (금) 오후 4:37, Hyunchul Lee <hyc.lee@xxxxxxxxx>님이 작성:
>
> 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@xxxxxxxxx>
> ---
> fs/ntfs/inode.c | 36 +++++++++-------
> fs/ntfs/layout.h | 11 +++++
> fs/ntfs/reparse.c | 122 +++++++++++++++++++++++++++++++++++++++++++++++++-----
> 3 files changed, 143 insertions(+), 26 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 @@ 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 {
> 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..4cc37f1c9c90 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,54 @@ 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);
> + switch (reparse_attr->reparse_tag) {
> + case IO_REPARSE_TAG_SYMLINK:
> + {
> + struct symlink_reparse_data *data;
> + size_t data_offs;
>
> - if (ntfs_is_valid_reparse_buffer(ni, reparse_attr, size) == false)
> - return false;
> + if (!valid_reparse_buffer(ni, reparse_attr, size,
> + sizeof(*data)))
> + return false;
>
> - switch (reparse_attr->reparse_tag) {
> + 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 +212,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 &&
> 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;
>
> --
> 2.43.0
>
>