| // 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[]; | |
| }; | |
| struct wof_reparse_data { | |
| __le32 version; | |
| __le32 provider; | |
| __le32 provider_version; | |
| __le32 compression_format; | |
| } __packed; | |
| #define WOF_CURRENT_VERSION cpu_to_le32(1) | |
| #define WOF_PROVIDER_WIM cpu_to_le32(1) | |
| #define WOF_PROVIDER_FILE cpu_to_le32(2) | |
| #define WOF_PROVIDER_CURRENT_VERSION cpu_to_le32(1) | |
| #define WOF_COMPRESSION_XPRESS4K cpu_to_le32(0) | |
| #define WOF_COMPRESSION_LZX cpu_to_le32(1) | |
| #define WOF_COMPRESSION_XPRESS8K cpu_to_le32(2) | |
| #define WOF_COMPRESSION_XPRESS16K cpu_to_le32(3) | |
| 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; | |
| case IO_REPARSE_TAG_WOF: { | |
| if (!valid_reparse_buffer(ni, reparse_attr, size, | |
| sizeof(struct wof_reparse_data))) | |
| 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; | |
| } | |
| /* | |
| * Parse reparse point data and initialize its in-memory representation. | |
| */ | |
| int ntfs_parse_reparse(struct ntfs_inode *ni, unsigned int *mode) | |
| { | |
| s64 attr_size = 0; | |
| int err = -EINVAL; | |
| unsigned int lth; | |
| struct reparse_point *reparse_attr; | |
| kvfree(ni->target); | |
| ni->target = NULL; | |
| ni->reparse_tag = 0; | |
| ni->reparse_flags = 0; | |
| *mode = 0; | |
| reparse_attr = ntfs_attr_readall(ni, AT_REPARSE_POINT, NULL, 0, | |
| &attr_size); | |
| if (IS_ERR(reparse_attr)) { | |
| err = PTR_ERR(reparse_attr); | |
| ntfs_error(ni->vol->sb, | |
| "Failed to read reparse point: %d.", err); | |
| return err; | |
| } | |
| if (!valid_reparse_data(ni, reparse_attr, attr_size)) { | |
| ntfs_error(ni->vol->sb, "Invalid reparse point."); | |
| err = -EFSCORRUPTED; | |
| goto out; | |
| } | |
| 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; | |
| } | |
| case IO_REPARSE_TAG_WOF: | |
| { | |
| #ifdef CONFIG_NTFS_FS_WOF_COMPRESSION | |
| const struct wof_reparse_data *wof_data = | |
| (const struct wof_reparse_data *)reparse_attr->reparse_data; | |
| ni->itype.compressed.block_size_bits = 0; | |
| ni->itype.compressed.block_size = 0; | |
| if (wof_data->version == WOF_CURRENT_VERSION && | |
| wof_data->provider == WOF_PROVIDER_FILE && | |
| wof_data->provider_version == | |
| WOF_PROVIDER_CURRENT_VERSION) { | |
| switch (wof_data->compression_format) { | |
| case WOF_COMPRESSION_XPRESS4K: | |
| ni->itype.compressed.block_size_bits = | |
| 12; | |
| break; | |
| case WOF_COMPRESSION_XPRESS8K: | |
| ni->itype.compressed.block_size_bits = | |
| 13; | |
| break; | |
| case WOF_COMPRESSION_XPRESS16K: | |
| ni->itype.compressed.block_size_bits = | |
| 14; | |
| break; | |
| case WOF_COMPRESSION_LZX: | |
| ni->itype.compressed.block_size_bits = | |
| 15; | |
| break; | |
| } | |
| } | |
| if (ni->itype.compressed.block_size_bits) | |
| ni->itype.compressed.block_size = | |
| 1 | |
| << ni->itype.compressed.block_size_bits; | |
| #endif | |
| NInoSetWofCompressed(ni); | |
| VFS_I(ni)->i_mode &= ~0222; | |
| err = 0; | |
| break; | |
| } | |
| default: | |
| err = 0; | |
| } | |
| if (!err) { | |
| *mode = ntfs_reparse_tag_mode( | |
| reparse_attr->reparse_tag); | |
| ni->reparse_tag = reparse_attr->reparse_tag; | |
| } | |
| out: | |
| kvfree(reparse_attr); | |
| return err; | |
| } | |
| 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 DT_UNKNOWN; | |
| reparse_attr = (struct reparse_point *)ntfs_attr_readall(NTFS_I(vi), | |
| AT_REPARSE_POINT, NULL, 0, &attr_size); | |
| if (IS_ERR(reparse_attr)) | |
| reparse_attr = NULL; | |
| 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; | |
| } | |
| } | |
| 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; | |
| } | |
| err = set_reparse_index(ni, xr, | |
| ((const struct reparse_point *)value)->reparse_tag); | |
| if (err && 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) { | |
| strreplace(sub_name, '\\', '/'); | |
| 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; | |
| } |