| // 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[]; |
| }; |
| |
| /* 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 ntfs_is_valid_reparse_buffer(struct ntfs_inode *ni, |
| const struct reparse_point *reparse_attr, size_t size) |
| { |
| size_t expected; |
| |
| if (!ni || !reparse_attr) |
| return false; |
| |
| /* Minimum size must cover reparse_point header */ |
| if (size < sizeof(struct reparse_point)) |
| 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) |
| { |
| 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) |
| return false; |
| |
| switch (reparse_attr->reparse_tag) { |
| case IO_REPARSE_TAG_LX_SYMLINK: |
| if (data_len <= sizeof(wsl_reparse_data->type) || |
| wsl_reparse_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)) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static unsigned int ntfs_reparse_tag_mode(struct reparse_point *reparse_attr) |
| { |
| unsigned int mode = 0; |
| |
| switch (reparse_attr->reparse_tag) { |
| 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; |
| unsigned int lth; |
| struct reparse_point *reparse_attr; |
| struct wsl_link_reparse_data *wsl_link_data; |
| unsigned int mode = 0; |
| |
| 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_LX_SYMLINK: |
| 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; |
| mode = ntfs_reparse_tag_mode(reparse_attr); |
| } |
| } |
| break; |
| default: |
| mode = ntfs_reparse_tag_mode(reparse_attr); |
| } |
| } 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) { |
| switch (reparse_attr->reparse_tag) { |
| 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; |
| } |
| |
| /* |
| * 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 __le16 *target, int target_len) |
| { |
| int err = 0; |
| int len; |
| int reparse_len; |
| unsigned char *utarget = NULL; |
| struct reparse_point *reparse; |
| struct wsl_link_reparse_data *data; |
| |
| utarget = (char *)NULL; |
| len = ntfs_ucstonls(ni->vol, target, target_len, &utarget, 0); |
| if (len <= 0) |
| return -EINVAL; |
| |
| reparse_len = sizeof(struct reparse_point) + sizeof(data->type) + len; |
| reparse = kvzalloc(reparse_len, GFP_NOFS); |
| if (!reparse) { |
| err = -ENOMEM; |
| kvfree(utarget); |
| } else { |
| 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) + len); |
| reparse->reserved = 0; |
| data->type = cpu_to_le32(2); |
| memcpy(data->link, utarget, len); |
| err = ntfs_set_ntfs_reparse_data(ni, |
| (char *)reparse, reparse_len); |
| kvfree(reparse); |
| if (!err) |
| ni->target = utarget; |
| } |
| 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); |
| } |
| |
| return err; |
| } |