blob: 8f60ec6f66c19e67a4c77901e784196d45d7e951 [file]
// 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;
}