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