blob: fa5c5c625751058ec210a2a6c72d45d4b54e79e9 [file]
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Pocessing of EA's
*
* Part of this file is based on code from the NTFS-3G.
*
* Copyright (c) 2014-2021 Jean-Pierre Andre
* Copyright (c) 2025 LG Electronics Co., Ltd.
*/
#include <linux/fs.h>
#include <linux/posix_acl.h>
#include <linux/posix_acl_xattr.h>
#include <linux/xattr.h>
#include "layout.h"
#include "attrib.h"
#include "index.h"
#include "dir.h"
#include "ea.h"
static int ntfs_write_ea(struct ntfs_inode *ni, __le32 type, char *value, s64 ea_off,
s64 ea_size, bool need_truncate)
{
struct inode *ea_vi;
int err = 0;
s64 written;
ea_vi = ntfs_attr_iget(VFS_I(ni), type, AT_UNNAMED, 0);
if (IS_ERR(ea_vi))
return PTR_ERR(ea_vi);
written = ntfs_inode_attr_pwrite(ea_vi, ea_off, ea_size, value, false);
if (written != ea_size)
err = written < 0 ? written : -EIO;
else {
struct ntfs_inode *ea_ni = NTFS_I(ea_vi);
if (need_truncate && ea_ni->data_size > ea_off + ea_size)
err = ntfs_attr_truncate(ea_ni, ea_off + ea_size);
mark_mft_record_dirty(ni);
}
iput(ea_vi);
return err;
}
static int ntfs_ea_lookup(char *ea_buf, s64 ea_buf_size, const char *name,
int name_len, s64 *ea_offset, s64 *ea_size)
{
const struct ea_attr *p_ea;
size_t actual_size;
loff_t offset, p_ea_size;
unsigned int next;
if (!ea_buf_size)
return -ENOENT;
offset = 0;
do {
if (ea_buf_size - offset < sizeof(struct ea_attr))
return -EUCLEAN;
p_ea = (const struct ea_attr *)&ea_buf[offset];
next = le32_to_cpu(p_ea->next_entry_offset);
p_ea_size = next ? next : (ea_buf_size - offset);
if (p_ea_size < sizeof(struct ea_attr) ||
offset + p_ea_size > ea_buf_size)
return -EUCLEAN;
if ((s64)p_ea->ea_name_length + 1 >
p_ea_size - offsetof(struct ea_attr, ea_name))
return -EUCLEAN;
actual_size = ALIGN(struct_size(p_ea, ea_name, 1 + p_ea->ea_name_length +
le16_to_cpu(p_ea->ea_value_length)), 4);
if (actual_size > p_ea_size ||
p_ea->ea_name[p_ea->ea_name_length])
return -EUCLEAN;
if (name && p_ea->ea_name_length == name_len &&
!memcmp(p_ea->ea_name, name, name_len)) {
*ea_offset = offset;
*ea_size = p_ea_size;
if (ea_buf_size < *ea_offset + *ea_size)
return -EUCLEAN;
return 0;
}
offset += next;
} while (next > 0 && offset < ea_buf_size);
return -ENOENT;
}
/*
* Return the existing EA
*
* The EA_INFORMATION is not examined and the consistency of the
* existing EA is not checked.
*
* If successful, the full attribute is returned unchanged
* and its size is returned.
* If the designated buffer is too small, the needed size is
* returned, and the buffer is left unchanged.
* If there is an error, a negative value is returned and errno
* is set according to the error.
*/
static int ntfs_get_ea(struct inode *inode, const char *name, size_t name_len,
void *buffer, size_t size)
{
struct ntfs_inode *ni = NTFS_I(inode);
const struct ea_attr *p_ea;
char *ea_buf;
s64 ea_off, ea_size, all_ea_size, ea_info_size;
int err;
u32 ea_info_qlen;
u16 ea_value_len;
struct ea_information *p_ea_info;
if (!NInoHasEA(ni))
return -ENODATA;
p_ea_info = ntfs_attr_readall(ni, AT_EA_INFORMATION, NULL, 0,
&ea_info_size);
if (IS_ERR(p_ea_info))
return PTR_ERR(p_ea_info);
if (ea_info_size != sizeof(struct ea_information)) {
kvfree(p_ea_info);
return -EUCLEAN;
}
ea_info_qlen = le32_to_cpu(p_ea_info->ea_query_length);
kvfree(p_ea_info);
ea_buf = ntfs_attr_readall(ni, AT_EA, NULL, 0, &all_ea_size);
if (IS_ERR(ea_buf))
return PTR_ERR(ea_buf);
if (ea_info_qlen > all_ea_size) {
err = -EUCLEAN;
goto free_ea_buf;
}
err = ntfs_ea_lookup(ea_buf, ea_info_qlen, name, name_len, &ea_off,
&ea_size);
if (!err) {
p_ea = (struct ea_attr *)&ea_buf[ea_off];
ea_value_len = le16_to_cpu(p_ea->ea_value_length);
if (!buffer) {
kvfree(ea_buf);
return ea_value_len;
}
if (ea_value_len > size) {
err = -ERANGE;
goto free_ea_buf;
}
memcpy(buffer, &p_ea->ea_name[p_ea->ea_name_length + 1],
ea_value_len);
kvfree(ea_buf);
return ea_value_len;
}
if (err == -ENOENT)
err = -ENODATA;
free_ea_buf:
kvfree(ea_buf);
return err;
}
static inline int ea_packed_size(const struct ea_attr *p_ea)
{
/*
* 4 bytes for header (flags and lengths) + name length + 1 +
* value length.
*/
return 5 + p_ea->ea_name_length + le16_to_cpu(p_ea->ea_value_length);
}
/* Unlike ntfs_attr_exist(), preserve lookup errors. */
static int ntfs_ea_attr_lookup(struct ntfs_inode *ni, __le32 type)
{
struct ntfs_attr_search_ctx *ctx;
struct mft_record *mrec;
int err;
mrec = map_mft_record(ni);
if (IS_ERR(mrec))
return PTR_ERR(mrec);
ctx = ntfs_attr_get_search_ctx(ni, mrec);
if (!ctx) {
err = -ENOMEM;
goto out;
}
err = ntfs_attr_lookup(type, AT_UNNAMED, 0, CASE_SENSITIVE,
0, NULL, 0, ctx);
ntfs_attr_put_search_ctx(ctx);
out:
unmap_mft_record(ni);
return err;
}
/* A NULL value means that the attribute should not exist. */
static int ntfs_ea_update_attr(struct ntfs_inode *ni, __le32 type,
char *value, s64 size)
{
int err = ntfs_ea_attr_lookup(ni, type);
if (err && err != -ENOENT)
return err;
if (!value)
return err == -ENOENT ? 0 :
ntfs_attr_remove(ni, type, AT_UNNAMED, 0, NULL);
if (err == -ENOENT)
return ntfs_attr_add(ni, type, AT_UNNAMED, 0, value, size);
return ntfs_write_ea(ni, type, value, 0, size, true);
}
/*
* Prepare the new EA list before changing either NTFS attribute. Keep the
* original values until both updates succeed, so a failed update can be
* rolled back. This does not provide atomicity across a crash or power loss.
*/
static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
const void *value, size_t val_size, int flags,
__le16 *packed_ea_size)
{
struct ntfs_inode *ni = NTFS_I(inode);
struct ea_information *old_info = NULL, info = {};
char *old_ea = NULL, *ea = NULL;
struct ea_attr *entry;
s64 old_size = 0, info_size, offset = 0, entry_size = 0;
u32 size = 0, new_size, next;
size_t add_size;
int err, info_err, ea_err, packed = 0, needed = 0;
bool exists, remove = !value;
if (name_len > U8_MAX)
return -ENAMETOOLONG;
if (val_size > U16_MAX)
return -E2BIG;
err = ntfs_ea_attr_lookup(ni, AT_EA_INFORMATION);
if (err && err != -ENOENT)
return err;
exists = !err;
err = ntfs_ea_attr_lookup(ni, AT_EA);
if (err && err != -ENOENT)
return err;
/* Do not silently discard an orphaned attribute. */
if (exists != !err)
return -EUCLEAN;
if (exists) {
old_info = ntfs_attr_readall(ni, AT_EA_INFORMATION, NULL, 0,
&info_size);
if (IS_ERR(old_info))
return PTR_ERR(old_info);
if (info_size != sizeof(*old_info)) {
err = -EUCLEAN;
goto out;
}
old_ea = ntfs_attr_readall(ni, AT_EA, NULL, 0, &old_size);
if (IS_ERR(old_ea)) {
err = PTR_ERR(old_ea);
old_ea = NULL;
goto out;
}
size = le32_to_cpu(old_info->ea_query_length);
if (size > old_size) {
err = -EUCLEAN;
goto out;
}
}
/* Validate even the entries following the one being replaced. */
err = ntfs_ea_lookup(old_ea, size, NULL, 0, &offset, &entry_size);
if (err != -ENOENT)
goto out;
err = ntfs_ea_lookup(old_ea, size, name, name_len, &offset, &entry_size);
if (err && err != -ENOENT)
goto out;
if (!err) {
if (flags & XATTR_CREATE) {
err = -EEXIST;
goto out;
}
entry = (struct ea_attr *)(old_ea + offset);
if (!remove && le16_to_cpu(entry->ea_value_length) == val_size &&
!memcmp(entry->ea_name + name_len + 1, value, val_size)) {
info = *old_info;
goto done;
}
} else {
if (remove || (flags & XATTR_REPLACE)) {
err = -ENODATA;
goto out;
}
offset = size;
entry_size = 0;
}
add_size = remove ? 0 : ALIGN(sizeof(*entry) + name_len + 1 + val_size, 4);
new_size = size - entry_size + add_size;
if (new_size && ntfs_attr_size_bounds_check(ni->vol, AT_EA, new_size)) {
err = -EFBIG;
goto out;
}
if (new_size) {
ea = kvzalloc(new_size, GFP_NOFS);
if (!ea) {
err = -ENOMEM;
goto out;
}
if (offset)
memcpy(ea, old_ea, offset);
if (size > offset + entry_size)
memcpy(ea + offset, old_ea + offset + entry_size,
size - offset - entry_size);
}
size -= entry_size;
/* Link a zero-terminated old tail before appending the new entry. */
for (offset = 0; offset < size; offset += next) {
entry = (struct ea_attr *)(ea + offset);
next = le32_to_cpu(entry->next_entry_offset);
if (!next) {
next = size - offset;
entry->next_entry_offset = cpu_to_le32(next);
}
packed += ea_packed_size(entry);
if (entry->flags & NEED_EA)
needed++;
}
if (!remove) {
entry = (struct ea_attr *)(ea + size);
entry->next_entry_offset = cpu_to_le32(add_size);
entry->ea_name_length = name_len;
entry->ea_value_length = cpu_to_le16(val_size);
memcpy(entry->ea_name, name, name_len);
memcpy(entry->ea_name + name_len + 1, value, val_size);
packed += ea_packed_size(entry);
}
if (packed > U16_MAX) {
err = -EFBIG;
goto out;
}
info.ea_length = cpu_to_le16(packed);
info.need_ea_count = cpu_to_le16(needed);
info.ea_query_length = cpu_to_le32(new_size);
/* All preparation is complete; errors from here require rollback. */
err = ntfs_ea_update_attr(ni, AT_EA, ea, new_size);
if (!err)
err = ntfs_ea_update_attr(ni, AT_EA_INFORMATION,
new_size ? (char *)&info : NULL,
sizeof(info));
if (err) {
/* Try both restorations even if the first one fails. */
ea_err = ntfs_ea_update_attr(ni, AT_EA, old_ea, old_size);
info_err = ntfs_ea_update_attr(ni, AT_EA_INFORMATION,
(char *)old_info, sizeof(info));
if (ea_err || info_err) {
NVolSetErrors(ni->vol);
NVolSetShutdown(ni->vol);
ntfs_error(inode->i_sb,
"Failed to restore EAs of inode 0x%llx (%d, %d); shutting down.",
ni->mft_no, ea_err, info_err);
err = -EIO;
}
goto out;
}
size = new_size;
mark_mft_record_dirty(ni);
done:
if (packed_ea_size)
*packed_ea_size = info.ea_length;
if (size)
NInoSetHasEA(ni);
else
NInoClearHasEA(ni);
out:
kvfree(ea);
kvfree(old_ea);
kvfree(old_info);
return err;
}
/*
* Check for the presence of an EA "$LXDEV" (used by WSL)
* and return its value as a device address
*/
int ntfs_ea_get_wsl_inode(struct inode *inode, dev_t *rdevp, unsigned int flags,
bool *has_lxmod)
{
int err;
__le32 v;
*has_lxmod = false;
if (!(flags & NTFS_VOL_UID)) {
/* Load uid to lxuid EA */
err = ntfs_get_ea(inode, "$LXUID", sizeof("$LXUID") - 1, &v,
sizeof(v));
if (err == sizeof(v))
i_uid_write(inode, le32_to_cpu(v));
}
if (!(flags & NTFS_VOL_GID)) {
/* Load gid to lxgid EA */
err = ntfs_get_ea(inode, "$LXGID", sizeof("$LXGID") - 1, &v,
sizeof(v));
if (err == sizeof(v))
i_gid_write(inode, le32_to_cpu(v));
}
/* Load mode to lxmod EA */
err = ntfs_get_ea(inode, "$LXMOD", sizeof("$LXMOD") - 1, &v, sizeof(v));
if (err == sizeof(v)) {
inode->i_mode = le32_to_cpu(v);
*has_lxmod = true;
} else {
/* Everyone gets all permissions. */
inode->i_mode |= 0777;
}
/* Load mode to lxdev EA */
err = ntfs_get_ea(inode, "$LXDEV", sizeof("$LXDEV") - 1, &v, sizeof(v));
if (err == sizeof(v))
*rdevp = le32_to_cpu(v);
err = 0;
return err;
}
int ntfs_ea_set_wsl_inode(struct inode *inode, dev_t rdev, __le16 *ea_size,
unsigned int flags)
{
__le32 v;
int err = 0;
if (ea_size)
*ea_size = 0;
if (flags & NTFS_EA_UID) {
/* Store uid to lxuid EA */
v = cpu_to_le32(i_uid_read(inode));
err = ntfs_set_ea(inode, "$LXUID", sizeof("$LXUID") - 1, &v,
sizeof(v), 0, ea_size);
if (err)
return err;
}
if (flags & NTFS_EA_GID) {
/* Store gid to lxgid EA */
v = cpu_to_le32(i_gid_read(inode));
err = ntfs_set_ea(inode, "$LXGID", sizeof("$LXGID") - 1, &v,
sizeof(v), 0, ea_size);
if (err)
return err;
}
if (flags & NTFS_EA_MODE) {
/* Store mode to lxmod EA */
v = cpu_to_le32(inode->i_mode);
err = ntfs_set_ea(inode, "$LXMOD", sizeof("$LXMOD") - 1, &v,
sizeof(v), 0, ea_size);
if (err)
return err;
}
if (rdev) {
v = cpu_to_le32(rdev);
err = ntfs_set_ea(inode, "$LXDEV", sizeof("$LXDEV") - 1, &v, sizeof(v),
0, ea_size);
}
return err;
}
int ntfs_ea_get_linuxflags(struct inode *inode)
{
struct ntfs_inode *ni = NTFS_I(inode);
__le32 value;
int err;
err = ntfs_get_ea(inode, "$LINUXFLAGS", sizeof("$LINUXFLAGS") - 1,
&value, sizeof(value));
if (err == -ENODATA)
return 0;
if (err < 0)
return err == -ERANGE ? -EUCLEAN : err;
if (err != sizeof(value))
return -EUCLEAN;
ni->linuxflags = le32_to_cpu(value);
if (ni->linuxflags & NTFS_LINUXFLAGS_IMMUTABLE)
inode->i_flags |= S_IMMUTABLE;
if (ni->linuxflags & NTFS_LINUXFLAGS_APPEND)
inode->i_flags |= S_APPEND;
if (ni->linuxflags & NTFS_LINUXFLAGS_NOATIME)
inode->i_flags |= S_NOATIME;
return 0;
}
int ntfs_ea_set_linuxflags(struct inode *inode, u32 linuxflags)
{
struct ntfs_inode *ni = NTFS_I(inode);
__le32 value;
int err;
if (linuxflags) {
value = cpu_to_le32(linuxflags);
err = ntfs_set_ea(inode, "$LINUXFLAGS", sizeof("$LINUXFLAGS") - 1,
&value, sizeof(value), 0, NULL);
} else if (NInoHasEA(ni)) {
err = ntfs_set_ea(inode, "$LINUXFLAGS", sizeof("$LINUXFLAGS") - 1,
NULL, 0, XATTR_REPLACE, NULL);
if (err == -ENODATA)
err = 0;
} else {
err = 0;
}
if (!err)
ni->linuxflags = linuxflags;
return err;
}
ssize_t ntfs_listxattr(struct dentry *dentry, char *buffer, size_t size)
{
struct inode *inode = d_inode(dentry);
struct ntfs_inode *ni = NTFS_I(inode);
const struct ea_attr *p_ea;
s64 offset, ea_buf_size, ea_info_size;
s64 ea_size;
u32 next;
int err = 0;
u32 ea_info_qsize;
char *ea_buf = NULL;
ssize_t ret = 0;
struct ea_information *ea_info;
if (!NInoHasEA(ni))
return 0;
mutex_lock(&NTFS_I(inode)->mrec_lock);
ea_info = ntfs_attr_readall(ni, AT_EA_INFORMATION, NULL, 0,
&ea_info_size);
if (IS_ERR(ea_info)) {
err = PTR_ERR(ea_info);
ea_info = NULL;
goto out;
}
if (ea_info_size != sizeof(struct ea_information)) {
err = -EUCLEAN;
goto out;
}
ea_info_qsize = le32_to_cpu(ea_info->ea_query_length);
ea_buf = ntfs_attr_readall(ni, AT_EA, NULL, 0, &ea_buf_size);
if (IS_ERR(ea_buf)) {
err = PTR_ERR(ea_buf);
ea_buf = NULL;
goto out;
}
if (ea_info_qsize > ea_buf_size) {
err = -EUCLEAN;
goto out;
}
/* Validate the entire chain, including size-only queries. */
err = ntfs_ea_lookup(ea_buf, ea_info_qsize, NULL, 0, &offset,
&ea_size);
if (err != -ENOENT)
goto out;
err = 0;
if (!ea_info_qsize)
goto out;
offset = 0;
do {
p_ea = (const struct ea_attr *)&ea_buf[offset];
next = le32_to_cpu(p_ea->next_entry_offset);
ea_size = next ? next : (ea_info_qsize - offset);
if (buffer) {
if (ret + p_ea->ea_name_length + 1 > size) {
err = -ERANGE;
goto out;
}
memcpy(buffer + ret, p_ea->ea_name, p_ea->ea_name_length);
buffer[ret + p_ea->ea_name_length] = 0;
}
ret += p_ea->ea_name_length + 1;
offset += ea_size;
} while (next > 0 && offset < ea_info_qsize);
out:
mutex_unlock(&NTFS_I(inode)->mrec_lock);
kvfree(ea_info);
kvfree(ea_buf);
return err ? err : ret;
}
// clang-format off
#define SYSTEM_DOS_ATTRIB "system.dos_attrib"
#define SYSTEM_NTFS_ATTRIB "system.ntfs_attrib"
#define SYSTEM_NTFS_ATTRIB_BE "system.ntfs_attrib_be"
// clang-format on
static int ntfs_getxattr(const struct xattr_handler *handler,
struct dentry *unused, struct inode *inode, const char *name,
void *buffer, size_t size)
{
struct ntfs_inode *ni = NTFS_I(inode);
int err;
/*
* Stream permissions and privileges belong to the base inode. This
* also lets VFS killpriv helpers find the base security attributes.
*/
if (ntfs_inode_is_named_stream(ni)) {
ni = ni->ext.base_ntfs_ino;
inode = VFS_I(ni);
}
if (NVolShutdown(ni->vol))
return -EIO;
if (!strcmp(name, SYSTEM_DOS_ATTRIB)) {
if (!buffer) {
err = sizeof(u8);
} else if (size < sizeof(u8)) {
err = -ERANGE;
} else {
err = sizeof(u8);
*(u8 *)buffer = (u8)(le32_to_cpu(ni->flags) & 0x3F);
}
goto out;
}
if (!strcmp(name, SYSTEM_NTFS_ATTRIB) ||
!strcmp(name, SYSTEM_NTFS_ATTRIB_BE)) {
if (!buffer) {
err = sizeof(u32);
} else if (size < sizeof(u32)) {
err = -ERANGE;
} else {
err = sizeof(u32);
*(u32 *)buffer = le32_to_cpu(ni->flags);
if (!strcmp(name, SYSTEM_NTFS_ATTRIB_BE))
*(__be32 *)buffer = cpu_to_be32(*(u32 *)buffer);
}
goto out;
}
mutex_lock(&ni->mrec_lock);
err = ntfs_get_ea(inode, name, strlen(name), buffer, size);
mutex_unlock(&ni->mrec_lock);
out:
return err;
}
static int ntfs_new_attr_flags(struct ntfs_inode *ni, __le32 fattr)
{
struct ntfs_attr_search_ctx *ctx;
struct mft_record *m;
struct attr_record *a;
__le16 new_aflags;
u16 old_name_ofs, old_mp_ofs;
int mp_size, mp_ofs, name_ofs, old_arec_size, arec_size, err;
m = map_mft_record(ni);
if (IS_ERR(m))
return PTR_ERR(m);
ctx = ntfs_attr_get_search_ctx(ni, m);
if (!ctx) {
err = -ENOMEM;
goto err_out;
}
err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
CASE_SENSITIVE, 0, NULL, 0, ctx);
if (err) {
err = -EINVAL;
goto err_out;
}
a = ctx->attr;
new_aflags = ctx->attr->flags;
if (fattr & FILE_ATTR_SPARSE_FILE)
new_aflags |= ATTR_IS_SPARSE;
else
new_aflags &= ~ATTR_IS_SPARSE;
if (fattr & FILE_ATTR_COMPRESSED)
new_aflags |= ATTR_IS_COMPRESSED;
else
new_aflags &= ~ATTR_IS_COMPRESSED;
if (new_aflags == a->flags) {
err = 0;
goto err_out;
}
if ((new_aflags & (ATTR_IS_SPARSE | ATTR_IS_COMPRESSED)) ==
(ATTR_IS_SPARSE | ATTR_IS_COMPRESSED)) {
pr_err("file can't be sparsed and compressed\n");
err = -EOPNOTSUPP;
goto err_out;
}
if (!a->non_resident) {
if (!(new_aflags & (ATTR_IS_SPARSE | ATTR_IS_COMPRESSED))) {
err = 0;
goto err_out;
}
if (le32_to_cpu(a->data.resident.value_length)) {
pr_err("Can't change sparse/compressed for non-empty file");
err = -EOPNOTSUPP;
goto err_out;
}
err = ntfs_attr_make_non_resident(ni, 0);
if (err)
goto err_out;
ntfs_attr_reinit_search_ctx(ctx);
err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
CASE_SENSITIVE,
0, NULL, 0, ctx);
if (err) {
err = -EINVAL;
goto err_out;
}
a = ctx->attr;
} else {
if (a->data.non_resident.data_size) {
pr_err("Can't change sparsed/compressed for non-empty file");
err = -EOPNOTSUPP;
goto err_out;
}
}
old_name_ofs = le16_to_cpu(a->name_offset);
old_mp_ofs = le16_to_cpu(a->data.non_resident.mapping_pairs_offset);
if (new_aflags & (ATTR_IS_SPARSE | ATTR_IS_COMPRESSED))
name_ofs = (offsetof(struct attr_record,
data.non_resident.compressed_size) +
sizeof(a->data.non_resident.compressed_size) + 7) & ~7;
else
name_ofs = (offsetof(struct attr_record,
data.non_resident.compressed_size) + 7) & ~7;
mp_size = ntfs_get_size_for_mapping_pairs(ni->vol, ni->runlist.rl, 0, -1, -1);
if (unlikely(mp_size < 0)) {
err = mp_size;
ntfs_debug("Failed to get size for mapping pairs array, error code %i.\n", err);
goto err_out;
}
mp_ofs = (name_ofs + a->name_length * sizeof(__le16) + 7) & ~7;
arec_size = (mp_ofs + mp_size + 7) & ~7;
old_arec_size = le32_to_cpu(a->length);
/*
* Move payloads before shrinking the record. Otherwise resizing moves
* the following attribute over the old payload before it can be copied.
*
* When offsets increase, move mapping_pairs first to avoid name
* overwriting the start of mapping_pairs.
*/
if (arec_size < old_arec_size) {
if (name_ofs > old_name_ofs) {
/* Payload offsets increased: move mapping pairs first. */
if (mp_ofs != old_mp_ofs)
memmove((u8 *)a + mp_ofs,
(u8 *)a + old_mp_ofs,
mp_size);
if (a->name_length && name_ofs != old_name_ofs)
memmove((u8 *)a + name_ofs,
(u8 *)a + old_name_ofs,
a->name_length *
sizeof(__le16));
} else {
/* Payload offsets decreased or unchanged: move name first. */
if (a->name_length && name_ofs != old_name_ofs)
memmove((u8 *)a + name_ofs,
(u8 *)a + old_name_ofs,
a->name_length *
sizeof(__le16));
if (mp_ofs != old_mp_ofs)
memmove((u8 *)a + mp_ofs,
(u8 *)a + old_mp_ofs,
mp_size);
}
}
err = ntfs_attr_record_resize(ctx->mrec, a, arec_size);
if (unlikely(err))
goto err_out;
/*
* When compressed/sparse state changes, the non-resident header grows or
* shrinks by the compressed_size field. Update the in-record payload layout
* to match the new offsets before exposing the new mapping_pairs_offset.
*/
if (arec_size > old_arec_size) {
if (mp_ofs != old_mp_ofs)
memmove((u8 *)a + mp_ofs, (u8 *)a + old_mp_ofs, mp_size);
if (a->name_length)
memmove((u8 *)a + name_ofs, (u8 *)a + old_name_ofs,
a->name_length * sizeof(__le16));
}
if (new_aflags & (ATTR_IS_SPARSE | ATTR_IS_COMPRESSED)) {
a->data.non_resident.compression_unit = 0;
if (new_aflags & ATTR_IS_COMPRESSED || ni->vol->major_ver < 3)
a->data.non_resident.compression_unit = 4;
a->data.non_resident.compressed_size = 0;
ni->itype.compressed.size = 0;
if (a->data.non_resident.compression_unit) {
ni->itype.compressed.block_size = 1U <<
(a->data.non_resident.compression_unit +
ni->vol->cluster_size_bits);
ni->itype.compressed.block_size_bits =
ffs(ni->itype.compressed.block_size) -
1;
ni->itype.compressed.block_clusters = 1U <<
a->data.non_resident.compression_unit;
} else {
ni->itype.compressed.block_size = 0;
ni->itype.compressed.block_size_bits = 0;
ni->itype.compressed.block_clusters = 0;
}
} else {
a->data.non_resident.compression_unit = 0;
}
a->name_offset = cpu_to_le16(name_ofs);
a->data.non_resident.mapping_pairs_offset = cpu_to_le16(mp_ofs);
a->flags = new_aflags;
if (new_aflags & ATTR_IS_SPARSE) {
NInoSetSparse(ni);
ni->flags |= FILE_ATTR_SPARSE_FILE;
} else {
NInoClearSparse(ni);
ni->flags &= ~FILE_ATTR_SPARSE_FILE;
}
if (new_aflags & ATTR_IS_COMPRESSED) {
NInoSetCompressed(ni);
ni->flags |= FILE_ATTR_COMPRESSED;
} else {
NInoClearCompressed(ni);
ni->flags &= ~FILE_ATTR_COMPRESSED;
}
mark_mft_record_dirty(ctx->ntfs_ino);
err_out:
if (ctx)
ntfs_attr_put_search_ctx(ctx);
unmap_mft_record(ni);
return err;
}
static bool ntfs_is_reserved_lxattr(const char *name)
{
return !strcmp(name, "$LXUID") || !strcmp(name, "$LXGID") ||
!strcmp(name, "$LXMOD") || !strcmp(name, "$LXDEV");
}
static int ntfs_validate_fattr(struct ntfs_inode *ni, __le32 fattr)
{
const __le32 wof_flags = FILE_ATTR_SPARSE_FILE |
FILE_ATTR_REPARSE_POINT;
if (NInoWofCompressed(ni) && ((ni->flags ^ fattr) & wof_flags))
return -EPERM;
return 0;
}
static int ntfs_setxattr(const struct xattr_handler *handler,
const struct mnt_idmap *idmap, struct dentry *unused,
struct inode *inode, const char *name, const void *value,
size_t size, int flags)
{
struct ntfs_inode *ni = NTFS_I(inode);
int err;
__le32 fattr;
if (ntfs_inode_is_named_stream(ni))
return -EOPNOTSUPP;
if (NVolShutdown(ni->vol))
return -EIO;
/* Only fileattr_set may change the flags and their cached state. */
if (!strcmp(name, "$LINUXFLAGS"))
return -EPERM;
if (ntfs_is_reserved_lxattr(name) && !capable(CAP_SYS_ADMIN))
return -EPERM;
if (!strcmp(name, SYSTEM_DOS_ATTRIB)) {
if (sizeof(u8) != size) {
err = -EINVAL;
goto out;
}
fattr = cpu_to_le32((le32_to_cpu(ni->flags) & ~0xffU) |
*(u8 *)value);
goto set_fattr;
}
if (!strcmp(name, SYSTEM_NTFS_ATTRIB) ||
!strcmp(name, SYSTEM_NTFS_ATTRIB_BE)) {
if (size != sizeof(u32)) {
err = -EINVAL;
goto out;
}
if (!strcmp(name, SYSTEM_NTFS_ATTRIB_BE))
fattr = cpu_to_le32(be32_to_cpu(*(__be32 *)value));
else
fattr = cpu_to_le32(*(u32 *)value);
err = ntfs_validate_fattr(ni, fattr);
if (err)
goto out;
if (S_ISREG(inode->i_mode)) {
mutex_lock(&ni->mrec_lock);
err = ntfs_new_attr_flags(ni, fattr);
mutex_unlock(&ni->mrec_lock);
if (err)
goto out;
}
set_fattr:
if (S_ISDIR(inode->i_mode))
fattr |= FILE_ATTR_DIRECTORY;
else
fattr &= ~FILE_ATTR_DIRECTORY;
err = ntfs_validate_fattr(ni, fattr);
if (err)
goto out;
if (ni->flags != fattr) {
ni->flags = fattr;
if (fattr & FILE_ATTR_READONLY)
inode->i_mode &= ~0222;
else
inode->i_mode |= 0222;
NInoSetFileNameDirty(ni);
mark_inode_dirty(inode);
}
err = 0;
goto out;
}
mutex_lock(&ni->mrec_lock);
err = ntfs_set_ea(inode, name, strlen(name), value, size, flags, NULL);
mutex_unlock(&ni->mrec_lock);
out:
if (!err) {
inode_set_ctime_current(inode);
mark_inode_dirty(inode);
}
return err;
}
static bool ntfs_xattr_user_list(struct dentry *dentry)
{
return true;
}
// clang-format off
static const struct xattr_handler ntfs_other_xattr_handler = {
.prefix = "",
.get = ntfs_getxattr,
.set = ntfs_setxattr,
.list = ntfs_xattr_user_list,
};
const struct xattr_handler * const ntfs_xattr_handlers[] = {
&ntfs_other_xattr_handler,
NULL,
};
// clang-format on
#ifdef CONFIG_NTFS_FS_POSIX_ACL
struct posix_acl *ntfs_get_acl(const struct mnt_idmap *idmap, struct dentry *dentry,
int type)
{
struct inode *inode = d_inode(dentry);
struct ntfs_inode *ni = NTFS_I(inode);
const char *name;
size_t name_len;
struct posix_acl *acl;
int err;
void *buf;
buf = kmalloc(PATH_MAX, GFP_KERNEL);
if (!buf)
return ERR_PTR(-ENOMEM);
/* Possible values of 'type' was already checked above. */
if (type == ACL_TYPE_ACCESS) {
name = XATTR_NAME_POSIX_ACL_ACCESS;
name_len = sizeof(XATTR_NAME_POSIX_ACL_ACCESS) - 1;
} else {
name = XATTR_NAME_POSIX_ACL_DEFAULT;
name_len = sizeof(XATTR_NAME_POSIX_ACL_DEFAULT) - 1;
}
mutex_lock(&ni->mrec_lock);
err = ntfs_get_ea(inode, name, name_len, buf, PATH_MAX);
mutex_unlock(&ni->mrec_lock);
/* Translate extended attribute to acl. */
if (err >= 0)
acl = posix_acl_from_xattr(&init_user_ns, buf, err);
else if (err == -ENODATA)
acl = NULL;
else
acl = ERR_PTR(err);
if (!IS_ERR(acl))
set_cached_acl(inode, type, acl);
kfree(buf);
return acl;
}
static noinline int ntfs_set_acl_ex(const struct mnt_idmap *idmap,
struct inode *inode, struct posix_acl *acl,
int type, bool init_acl)
{
const char *name;
size_t size, name_len;
void *value, *old_value = NULL;
int err, old_len = -ENODATA;
bool acl_committed = false;
int flags;
umode_t mode;
if (S_ISLNK(inode->i_mode))
return -EOPNOTSUPP;
mode = inode->i_mode;
switch (type) {
case ACL_TYPE_ACCESS:
/* Do not change i_mode if we are in init_acl */
if (acl && !init_acl) {
err = posix_acl_update_mode(idmap, inode, &mode, &acl);
if (err)
return err;
}
name = XATTR_NAME_POSIX_ACL_ACCESS;
name_len = sizeof(XATTR_NAME_POSIX_ACL_ACCESS) - 1;
break;
case ACL_TYPE_DEFAULT:
if (!S_ISDIR(inode->i_mode))
return acl ? -EACCES : 0;
name = XATTR_NAME_POSIX_ACL_DEFAULT;
name_len = sizeof(XATTR_NAME_POSIX_ACL_DEFAULT) - 1;
break;
default:
return -EINVAL;
}
if (!acl) {
/* Remove xattr if it can be presented via mode. */
size = 0;
value = NULL;
flags = XATTR_REPLACE;
} else {
value = posix_acl_to_xattr(&init_user_ns, acl, &size, GFP_NOFS);
if (!value)
return -ENOMEM;
flags = 0;
}
mutex_lock(&NTFS_I(inode)->mrec_lock);
if (!init_acl) {
/* Save the old xattr for rollback. */
old_len = ntfs_get_ea(inode, name, name_len, NULL, 0);
if (old_len >= 0) {
/* kmalloc(0) returns the non-NULL ZERO_SIZE_PTR. */
old_value = kmalloc(old_len, GFP_NOFS);
if (!old_value) {
err = -ENOMEM;
goto out_unlock;
}
err = ntfs_get_ea(inode, name, name_len,
old_value, old_len);
if (err != old_len) {
kfree(old_value);
old_value = NULL;
if (err >= 0)
err = -EIO;
goto out_unlock;
}
} else if (old_len != -ENODATA) {
err = old_len;
goto out_unlock;
}
}
err = ntfs_set_ea(inode, name, name_len, value, size, flags, NULL);
if (!err)
acl_committed = true;
out_unlock:
mutex_unlock(&NTFS_I(inode)->mrec_lock);
if (err == -ENODATA && !size)
err = 0; /* Removing non existed xattr. */
if (!err) {
__le16 ea_size = 0;
umode_t old_mode = inode->i_mode;
inode->i_mode = mode;
mutex_lock(&NTFS_I(inode)->mrec_lock);
err = ntfs_ea_set_wsl_inode(inode, 0, &ea_size, NTFS_EA_MODE);
if (err) {
if (acl_committed) {
int undo_err;
if (old_len >= 0) {
/*
* A removed or zero-length xattr must
* be recreated without XATTR_REPLACE.
*/
int undo_flags = acl && old_len ?
XATTR_REPLACE : 0;
undo_err = ntfs_set_ea(inode, name,
name_len, old_value,
old_len, undo_flags,
NULL);
} else {
undo_err = ntfs_set_ea(inode, name,
name_len, NULL, 0,
XATTR_REPLACE, NULL);
}
if (undo_err) {
/*
* Record a failed rollback so the next
* persistence point marks the volume
* dirty. Run chkdsk.
*/
NVolSetErrors(NTFS_I(inode)->vol);
ntfs_error(inode->i_sb,
"Failed to undo the ACL xattr: %d",
undo_err);
}
}
mutex_unlock(&NTFS_I(inode)->mrec_lock);
inode->i_mode = old_mode;
goto out;
}
mutex_unlock(&NTFS_I(inode)->mrec_lock);
set_cached_acl(inode, type, acl);
inode_set_ctime_current(inode);
mark_inode_dirty(inode);
}
out:
kfree(old_value);
kfree(value);
return err;
}
int ntfs_set_acl(const struct mnt_idmap *idmap, struct dentry *dentry,
struct posix_acl *acl, int type)
{
return ntfs_set_acl_ex(idmap, d_inode(dentry), acl, type, false);
}
int ntfs_init_acl(const struct mnt_idmap *idmap, struct inode *inode,
struct inode *dir)
{
struct posix_acl *default_acl, *acl;
int err;
err = posix_acl_create(dir, &inode->i_mode, &default_acl, &acl);
if (err)
return err;
if (default_acl) {
err = ntfs_set_acl_ex(idmap, inode, default_acl,
ACL_TYPE_DEFAULT, true);
posix_acl_release(default_acl);
} else {
inode->i_default_acl = NULL;
}
if (acl) {
if (!err)
err = ntfs_set_acl_ex(idmap, inode, acl,
ACL_TYPE_ACCESS, true);
posix_acl_release(acl);
} else {
inode->i_acl = NULL;
}
return err;
}
#endif