Contributors: 2
Author Tokens Token Proportion Commits Commit Proportion
Namjae Jeon 2457 58.10% 4 44.44%
Hyeoncheol Lee 1772 41.90% 5 55.56%
Total 4229 9


// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Processing of reparse points
 *
 * Part of this file is based on code from the NTFS-3G.
 *
 * Copyright (c) 2008-2021 Jean-Pierre Andre
 * Copyright (c) 2025 LG Electronics Co., Ltd.
 */

#include "ntfs.h"
#include "layout.h"
#include "attrib.h"
#include "inode.h"
#include "dir.h"
#include "volume.h"
#include "mft.h"
#include "index.h"
#include "lcnalloc.h"
#include "reparse.h"

struct wsl_link_reparse_data {
	__le32	type;
	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;
	struct reparse_index_key key;
	__le32 filling;
};

__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 valid_reparse_buffer(struct ntfs_inode *ni,
				 const struct reparse_point *reparse_attr,
				 size_t size,
				 size_t payload_min_len)
{
	size_t expected;

	if (!ni || !reparse_attr)
		return false;

	/* Minimum size must cover reparse_point header */
	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;

	/* Calculate expected total size */
	expected = sizeof(struct reparse_point) +
		le16_to_cpu(reparse_attr->reparse_data_length);

	/* Add GUID size for non-Microsoft tags */
	if (!(reparse_attr->reparse_tag & IO_REPARSE_TAG_IS_MICROSOFT))
		expected += sizeof(struct guid);

	/* Buffer must exactly match the expected size */
	return expected == size;
}

/*
 * Do some sanity checks on reparse data
 *
 * Microsoft reparse points have an 8-byte header whereas
 * non-Microsoft reparse points have a 24-byte header.  In each case,
 * 'reparse_data_length' must equal the number of non-header bytes.
 *
 * If the reparse data looks like a junction point or symbolic
 * link, more checks can be done.
 */
static bool valid_reparse_data(struct ntfs_inode *ni,
		const struct reparse_point *reparse_attr, size_t size)
{
	if (size < sizeof(*reparse_attr))
		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;
		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 (!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;
		break;
	default:
		if (!valid_reparse_buffer(ni, reparse_attr, size, 0))
			return false;
		break;
	}

	return true;
}

static unsigned int ntfs_reparse_tag_mode(__le32 reparse_tag)
{
	unsigned int mode = 0;

	switch (reparse_tag) {
	case IO_REPARSE_TAG_MOUNT_POINT:
	case IO_REPARSE_TAG_SYMLINK:
	case IO_REPARSE_TAG_LX_SYMLINK:
		mode = S_IFLNK;
		break;
	case IO_REPARSE_TAG_AF_UNIX:
		mode = S_IFSOCK;
		break;
	case IO_REPARSE_TAG_LX_FIFO:
		mode = S_IFIFO;
		break;
	case IO_REPARSE_TAG_LX_CHR:
		mode = S_IFCHR;
		break;
	case IO_REPARSE_TAG_LX_BLK:
		mode = S_IFBLK;
	}

	return mode;
}

/*
 * Get the target for symbolic link
 */
unsigned int ntfs_make_symlink(struct ntfs_inode *ni)
{
	s64 attr_size = 0;
	int err;
	unsigned int lth;
	struct reparse_point *reparse_attr;
	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 =
				(struct symlink_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);
			if (!err)
				ni->reparse_flags = data->flags;
			break;
		}
		case IO_REPARSE_TAG_LX_SYMLINK:
		{
			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);
				ni->target = kvzalloc(lth + 1, GFP_NOFS);
				if (ni->target) {
					memcpy(ni->target, wsl_link_data->link, lth);
					ni->target[lth] = 0;
					err = 0;
				}
			}
			break;
		}
		default:
			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;

	if (reparse_attr)
		kvfree(reparse_attr);

	return mode;
}

unsigned int ntfs_reparse_tag_dt_types(struct ntfs_volume *vol, unsigned long mref)
{
	s64 attr_size = 0;
	struct reparse_point *reparse_attr;
	unsigned int dt_type = DT_UNKNOWN;
	struct inode *vi;

	vi = ntfs_iget(vol->sb, mref);
	if (IS_ERR(vi))
		return PTR_ERR(vi);

	reparse_attr = (struct reparse_point *)ntfs_attr_readall(NTFS_I(vi),
			AT_REPARSE_POINT, NULL, 0, &attr_size);

	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;
			break;
		case IO_REPARSE_TAG_AF_UNIX:
			dt_type = DT_SOCK;
			break;
		case IO_REPARSE_TAG_LX_FIFO:
			dt_type = DT_FIFO;
			break;
		case IO_REPARSE_TAG_LX_CHR:
			dt_type = DT_CHR;
			break;
		case IO_REPARSE_TAG_LX_BLK:
			dt_type = DT_BLK;
		}
	}

	if (reparse_attr)
		kvfree(reparse_attr);

	iput(vi);
	return dt_type;
}

static bool ntfs_is_drive_letter(const char *target)
{
	return ((target[0] >= 'A' && target[0] <= 'Z') ||
		(target[0] >= 'a' && target[0] <= 'z')) &&
		target[1] == ':';
}

/*
 * 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 (ntfs_is_drive_letter(path)) {
		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
 */
static int set_reparse_index(struct ntfs_inode *ni, struct ntfs_index_context *xr,
		__le32 reparse_tag)
{
	struct reparse_index indx;
	u64 file_id_cpu;
	__le64 file_id;

	file_id_cpu = MK_MREF(ni->mft_no, ni->seq_no);
	file_id = cpu_to_le64(file_id_cpu);
	indx.header.data.vi.data_offset =
		cpu_to_le16(sizeof(struct index_entry_header) + sizeof(struct reparse_index_key));
	indx.header.data.vi.data_length = 0;
	indx.header.data.vi.reservedV = 0;
	indx.header.length = cpu_to_le16(sizeof(struct reparse_index));
	indx.header.key_length = cpu_to_le16(sizeof(struct reparse_index_key));
	indx.header.flags = 0;
	indx.header.reserved = 0;
	indx.key.reparse_tag = reparse_tag;
	/* danger on processors which require proper alignment! */
	memcpy(&indx.key.file_id, &file_id, 8);
	indx.filling = 0;
	ntfs_index_ctx_reinit(xr);

	return ntfs_ie_add(xr, (struct index_entry *)&indx);
}

/*
 * Remove a reparse data index entry if attribute present
 */
static int remove_reparse_index(struct inode *rp, struct ntfs_index_context *xr,
				__le32 *preparse_tag)
{
	struct reparse_index_key key;
	u64 file_id_cpu;
	__le64 file_id;
	s64 size;
	struct ntfs_inode *ni = NTFS_I(rp);
	int err = 0, ret = ni->data_size;

	if (ni->data_size == 0)
		return 0;

	/* read the existing reparse_tag */
	size = ntfs_inode_attr_pread(rp, 0, 4, (char *)preparse_tag);
	if (size != 4)
		return -ENODATA;

	file_id_cpu = MK_MREF(ni->mft_no, ni->seq_no);
	file_id = cpu_to_le64(file_id_cpu);
	key.reparse_tag = *preparse_tag;
	/* danger on processors which require proper alignment! */
	memcpy(&key.file_id, &file_id, 8);
	if (!ntfs_index_lookup(&key, sizeof(struct reparse_index_key), xr)) {
		err = ntfs_index_rm(xr);
		if (err)
			ret = err;
	}
	return ret;
}

/*
 * Open the $Extend/$Reparse file and its index
 */
static struct ntfs_index_context *open_reparse_index(struct ntfs_volume *vol)
{
	struct ntfs_index_context *xr = NULL;
	u64 mref;
	__le16 *uname;
	struct ntfs_name *name = NULL;
	int uname_len;
	struct inode *vi, *dir_vi;

	/* do not use path_name_to inode - could reopen root */
	dir_vi = ntfs_iget(vol->sb, FILE_Extend);
	if (IS_ERR(dir_vi))
		return NULL;

	uname_len = ntfs_nlstoucs(vol, "$Reparse", 8, &uname,
				  NTFS_MAX_NAME_LEN);
	if (uname_len < 0) {
		iput(dir_vi);
		return NULL;
	}

	mutex_lock_nested(&NTFS_I(dir_vi)->mrec_lock, NTFS_EXTEND_MUTEX_PARENT);
	mref = ntfs_lookup_inode_by_name(NTFS_I(dir_vi), uname, uname_len,
					 &name);
	mutex_unlock(&NTFS_I(dir_vi)->mrec_lock);
	kfree(name);
	kmem_cache_free(ntfs_name_cache, uname);
	if (IS_ERR_MREF(mref))
		goto put_dir_vi;

	vi = ntfs_iget(vol->sb, MREF(mref));
	if (IS_ERR(vi))
		goto put_dir_vi;

	xr = ntfs_index_ctx_get(NTFS_I(vi), reparse_index_name, 2);
	if (!xr)
		iput(vi);
put_dir_vi:
	iput(dir_vi);
	return xr;
}


/*
 * Update the reparse data and index
 *
 * The reparse data attribute should have been created, and
 * an existing index is expected if there is an existing value.
 *
 */
static int update_reparse_data(struct ntfs_inode *ni, struct ntfs_index_context *xr,
		char *value, size_t size)
{
	struct inode *rp_inode;
	int err = 0;
	s64 written;
	int oldsize;
	__le32 reparse_tag;
	struct ntfs_inode *rp_ni;

	rp_inode = ntfs_attr_iget(VFS_I(ni), AT_REPARSE_POINT, AT_UNNAMED, 0);
	if (IS_ERR(rp_inode))
		return -EINVAL;
	rp_ni = NTFS_I(rp_inode);

	/* remove the existing reparse data */
	oldsize = remove_reparse_index(rp_inode, xr, &reparse_tag);
	if (oldsize < 0) {
		err = oldsize;
		goto put_rp_inode;
	}

	/* overwrite value if any */
	written = ntfs_inode_attr_pwrite(rp_inode, 0, size, value, false);
	if (written != size) {
		ntfs_error(ni->vol->sb, "Failed to update reparse data\n");
		err = -EIO;
		goto put_rp_inode;
	}

	if (set_reparse_index(ni, xr, ((const struct reparse_point *)value)->reparse_tag) &&
	    oldsize > 0) {
		/*
		 * If cannot index, try to remove the reparse
		 * data and log the error. There will be an
		 * inconsistency if removal fails.
		 */
		ntfs_attr_rm(rp_ni);
		ntfs_error(ni->vol->sb,
			   "Failed to index reparse data. Possible corruption.\n");
	}

	mark_mft_record_dirty(ni);
put_rp_inode:
	iput(rp_inode);

	return err;
}

/*
 * Delete a reparse index entry
 */
int ntfs_delete_reparse_index(struct ntfs_inode *ni)
{
	struct inode *vi;
	struct ntfs_index_context *xr;
	struct ntfs_inode *xrni;
	__le32 reparse_tag;
	int err = 0;

	if (!(ni->flags & FILE_ATTR_REPARSE_POINT))
		return 0;

	vi = ntfs_attr_iget(VFS_I(ni), AT_REPARSE_POINT, AT_UNNAMED, 0);
	if (IS_ERR(vi))
		return PTR_ERR(vi);

	/*
	 * read the existing reparse data (the tag is enough)
	 * and un-index it
	 */
	xr = open_reparse_index(ni->vol);
	if (xr) {
		xrni = xr->idx_ni;
		mutex_lock_nested(&xrni->mrec_lock, NTFS_EXTEND_MUTEX_PARENT);
		err = remove_reparse_index(vi, xr, &reparse_tag);
		if (err < 0) {
			ntfs_index_ctx_put(xr);
			mutex_unlock(&xrni->mrec_lock);
			iput(VFS_I(xrni));
			goto out;
		}
		mark_mft_record_dirty(xrni);
		ntfs_index_ctx_put(xr);
		mutex_unlock(&xrni->mrec_lock);
		iput(VFS_I(xrni));
	}

	ni->flags &= ~FILE_ATTR_REPARSE_POINT;
	NInoSetFileNameDirty(ni);
	mark_mft_record_dirty(ni);

out:
	iput(vi);
	return err;
}

/*
 * Set the reparse data from an extended attribute
 */
static int ntfs_set_ntfs_reparse_data(struct ntfs_inode *ni, char *value, size_t size)
{
	int err = 0;
	struct ntfs_inode *xrni;
	struct ntfs_index_context *xr;

	if (!ni)
		return -EINVAL;

	/*
	 * reparse data compatibily with EA is not checked
	 * any more, it is required by Windows 10, but may
	 * lead to problems with earlier versions.
	 */
	if (valid_reparse_data(ni, (const struct reparse_point *)value, size) == false)
		return -EINVAL;

	xr = open_reparse_index(ni->vol);
	if (!xr)
		return -EINVAL;
	xrni = xr->idx_ni;

	if (!ntfs_attr_exist(ni, AT_REPARSE_POINT, AT_UNNAMED, 0)) {
		struct reparse_point rp = {0, };

		/*
		 * no reparse data attribute : add one,
		 * apparently, this does not feed the new value in
		 * Note : NTFS version must be >= 3
		 */
		if (ni->vol->major_ver < 3) {
			err = -EOPNOTSUPP;
			ntfs_index_ctx_put(xr);
			goto out;
		}

		err = ntfs_attr_add(ni, AT_REPARSE_POINT, AT_UNNAMED, 0, (u8 *)&rp, sizeof(rp));
		if (err) {
			ntfs_index_ctx_put(xr);
			goto out;
		}
		ni->flags |= FILE_ATTR_REPARSE_POINT;
		NInoSetFileNameDirty(ni);
		mark_mft_record_dirty(ni);
	}

	/* update value and index */
	mutex_lock_nested(&xrni->mrec_lock, NTFS_EXTEND_MUTEX_PARENT);
	err = update_reparse_data(ni, xr, value, size);
	if (err) {
		ni->flags &= ~FILE_ATTR_REPARSE_POINT;
		NInoSetFileNameDirty(ni);
		mark_mft_record_dirty(ni);
	}
	ntfs_index_ctx_put(xr);
	mutex_unlock(&xrni->mrec_lock);

out:
	if (!err)
		mark_mft_record_dirty(xrni);
	iput(VFS_I(xrni));

	return err;
}

/*
 * Set reparse data for a WSL type symlink
 */
int ntfs_reparse_set_wsl_symlink(struct ntfs_inode *ni,
				 const char *target, int target_len)
{
	int err = 0;
	int reparse_len;
	struct reparse_point *reparse;
	struct wsl_link_reparse_data *data;

	reparse_len = sizeof(struct reparse_point) + sizeof(data->type) +
		target_len;
	reparse = kvzalloc(reparse_len, GFP_NOFS);
	if (!reparse)
		return -ENOMEM;

	ni->target = kstrdup(target, GFP_NOFS);
	if (!ni->target) {
		kvfree(reparse);
		return -ENOMEM;
	}

	data = (struct wsl_link_reparse_data *)reparse->reparse_data;
	reparse->reparse_tag = IO_REPARSE_TAG_LX_SYMLINK;
	reparse->reparse_data_length =
		cpu_to_le16(sizeof(data->type) + target_len);
	reparse->reserved = 0;
	data->type = cpu_to_le32(2);
	memcpy(data->link, target, target_len);
	err = ntfs_set_ntfs_reparse_data(ni,
					 (char *)reparse, reparse_len);
	kvfree(reparse);
	if (err) {
		kfree(ni->target);
		ni->target = NULL;
	} else {
		ni->reparse_tag = IO_REPARSE_TAG_LX_SYMLINK;
		ni->reparse_flags = 0;
	}
	return err;
}

int ntfs_reparse_set_native_symlink(struct ntfs_inode *ni,
				    const char *target, int target_len)
{
	int err = 0;
	bool is_absolute, prt_sub_shared = true;
	char *sub_name = NULL;
	char *prt_name = NULL;
	__le16 *sub_name_utf16 = NULL;
	__le16 *prt_name_utf16 = NULL;
	int sub_len, prt_len;
	int total_data_len, total_reparse_len;
	struct reparse_point *reparse = NULL;
	struct symlink_reparse_data *data;
	int i;

	/* Determine if target is absolute (starts with drive letter like C:/ or C:\) */
	is_absolute = target_len > 2 &&
		ntfs_is_drive_letter(target) &&
		(target[2] == '/' || target[2] == '\\');


	/* Normalize and prepare NLS paths */
	prt_name = kstrdup(target, GFP_NOFS);
	if (!prt_name)
		return -ENOMEM;

	/* Replace '/' with '\' */
	for (i = 0; i < target_len; i++) {
		if (prt_name[i] == '/')
			prt_name[i] = '\\';
	}

	if (is_absolute) {
		/* Prepend '\??\' to Substitutename */
		sub_name = kmalloc(target_len + 5, GFP_NOFS);
		if (!sub_name) {
			err = -ENOMEM;
			goto out;
		}
		snprintf(sub_name, target_len + 5, "\\??\\%s", prt_name);
		prt_sub_shared = false;
	} else {
		/* For relative symlinks (including absolute paths without drive letters),
		 * SubstituteName and PrintName are identical.
		 */
		sub_name = prt_name;
	}

	/* Convert NLS paths to UTF-16 */
	sub_len = ntfs_nlstoucs(ni->vol, sub_name, strlen(sub_name),
				&sub_name_utf16, PATH_MAX);
	if (sub_len < 0) {
		err = sub_len;
		goto out;
	}

	prt_len = ntfs_nlstoucs(ni->vol, prt_name, strlen(prt_name),
				&prt_name_utf16, PATH_MAX);
	if (prt_len < 0) {
		err = prt_len;
		goto out;
	}

	/* Check for buffer size limits */
	total_data_len = sizeof(struct symlink_reparse_data) +
		(sub_len + prt_len) * sizeof(__le16);
	if (total_data_len > 16384) { /* 16KB max reparse tag size */
		err = -EFBIG;
		goto out;
	}

	total_reparse_len = sizeof(struct reparse_point) + total_data_len;
	reparse = kvzalloc(total_reparse_len, GFP_NOFS);
	if (!reparse) {
		err = -ENOMEM;
		goto out;
	}

	/* Pack fields in reparse buffer */
	reparse->reparse_tag = IO_REPARSE_TAG_SYMLINK;
	reparse->reparse_data_length = cpu_to_le16(total_data_len);
	reparse->reserved = 0;

	data = (struct symlink_reparse_data *)reparse->reparse_data;
	data->substitute_name_offset = 0;
	data->substitute_name_length = cpu_to_le16(sub_len * sizeof(__le16));
	data->print_name_offset = data->substitute_name_length;
	data->print_name_length = cpu_to_le16(prt_len * sizeof(__le16));
	data->flags = is_absolute ? 0 : cpu_to_le32(SYMLINK_FLAG_RELATIVE);

	/* Copy names to path_buffer */
	memcpy(data->path_buffer, sub_name_utf16, sub_len * sizeof(__le16));
	memcpy(data->path_buffer + sub_len, prt_name_utf16, prt_len * sizeof(__le16));

	err = ntfs_set_ntfs_reparse_data(ni, (char *)reparse, total_reparse_len);
	if (!err) {
		int len = strlen(sub_name);

		for (i = 0; i < len; i++) {
			if (sub_name[i] == '\\')
				sub_name[i] = '/';
		}
		ni->target = sub_name;
		sub_name = NULL;
		if (prt_sub_shared)
			prt_name = NULL;
		ni->reparse_tag = IO_REPARSE_TAG_SYMLINK;
		ni->reparse_flags = is_absolute ? 0 :
			cpu_to_le32(SYMLINK_FLAG_RELATIVE);
	}

out:
	kfree(prt_name);
	if (!prt_sub_shared)
		kfree(sub_name);
	kvfree(sub_name_utf16);
	kvfree(prt_name_utf16);
	kvfree(reparse);
	return err;
}

/*
 * Set reparse data for a WSL special file other than a symlink
 * (socket, fifo, character or block device)
 */
int ntfs_reparse_set_wsl_not_symlink(struct ntfs_inode *ni, mode_t mode)
{
	int err;
	int len;
	int reparse_len;
	__le32 reparse_tag;
	struct reparse_point *reparse;

	len = 0;
	if (S_ISSOCK(mode))
		reparse_tag = IO_REPARSE_TAG_AF_UNIX;
	else if (S_ISFIFO(mode))
		reparse_tag = IO_REPARSE_TAG_LX_FIFO;
	else if (S_ISCHR(mode))
		reparse_tag = IO_REPARSE_TAG_LX_CHR;
	else if (S_ISBLK(mode))
		reparse_tag = IO_REPARSE_TAG_LX_BLK;
	else
		return -EOPNOTSUPP;

	reparse_len = sizeof(struct reparse_point) + len;
	reparse = kvzalloc(reparse_len, GFP_NOFS);
	if (!reparse)
		err = -ENOMEM;
	else {
		reparse->reparse_tag = reparse_tag;
		reparse->reparse_data_length = cpu_to_le16(len);
		reparse->reserved = cpu_to_le16(0);
		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;
}