linux/fs/file_attr.c
Dan Carpenter e85931d1cd
fs: tighten a sanity check in file_attr_to_fileattr()
The fattr->fa_xflags is a u64 that comes from the user.  This is a sanity
check to ensure that the users are only setting allowed flags.  The
problem is that it doesn't check the upper 32 bits.  It doesn't really
affect anything but for more flexibility in the future, we want to enforce
users zero out those bits.

Fixes: be7efb2d20 ("fs: introduce file_getattr and file_setattr syscalls")
Signed-off-by: Dan Carpenter <dan.carpenter@linaro.org>
Link: https://lore.kernel.org/baf7b808-bcf2-4ac1-9313-882c91cc87b2@sabinyo.mountain
Signed-off-by: Christian Brauner <brauner@kernel.org>
2025-07-16 10:22:01 +02:00

498 lines
12 KiB
C

// SPDX-License-Identifier: GPL-2.0
#include <linux/fs.h>
#include <linux/security.h>
#include <linux/fscrypt.h>
#include <linux/fileattr.h>
#include <linux/export.h>
#include <linux/syscalls.h>
#include <linux/namei.h>
#include "internal.h"
/**
* fileattr_fill_xflags - initialize fileattr with xflags
* @fa: fileattr pointer
* @xflags: FS_XFLAG_* flags
*
* Set ->fsx_xflags, ->fsx_valid and ->flags (translated xflags). All
* other fields are zeroed.
*/
void fileattr_fill_xflags(struct file_kattr *fa, u32 xflags)
{
memset(fa, 0, sizeof(*fa));
fa->fsx_valid = true;
fa->fsx_xflags = xflags;
if (fa->fsx_xflags & FS_XFLAG_IMMUTABLE)
fa->flags |= FS_IMMUTABLE_FL;
if (fa->fsx_xflags & FS_XFLAG_APPEND)
fa->flags |= FS_APPEND_FL;
if (fa->fsx_xflags & FS_XFLAG_SYNC)
fa->flags |= FS_SYNC_FL;
if (fa->fsx_xflags & FS_XFLAG_NOATIME)
fa->flags |= FS_NOATIME_FL;
if (fa->fsx_xflags & FS_XFLAG_NODUMP)
fa->flags |= FS_NODUMP_FL;
if (fa->fsx_xflags & FS_XFLAG_DAX)
fa->flags |= FS_DAX_FL;
if (fa->fsx_xflags & FS_XFLAG_PROJINHERIT)
fa->flags |= FS_PROJINHERIT_FL;
}
EXPORT_SYMBOL(fileattr_fill_xflags);
/**
* fileattr_fill_flags - initialize fileattr with flags
* @fa: fileattr pointer
* @flags: FS_*_FL flags
*
* Set ->flags, ->flags_valid and ->fsx_xflags (translated flags).
* All other fields are zeroed.
*/
void fileattr_fill_flags(struct file_kattr *fa, u32 flags)
{
memset(fa, 0, sizeof(*fa));
fa->flags_valid = true;
fa->flags = flags;
if (fa->flags & FS_SYNC_FL)
fa->fsx_xflags |= FS_XFLAG_SYNC;
if (fa->flags & FS_IMMUTABLE_FL)
fa->fsx_xflags |= FS_XFLAG_IMMUTABLE;
if (fa->flags & FS_APPEND_FL)
fa->fsx_xflags |= FS_XFLAG_APPEND;
if (fa->flags & FS_NODUMP_FL)
fa->fsx_xflags |= FS_XFLAG_NODUMP;
if (fa->flags & FS_NOATIME_FL)
fa->fsx_xflags |= FS_XFLAG_NOATIME;
if (fa->flags & FS_DAX_FL)
fa->fsx_xflags |= FS_XFLAG_DAX;
if (fa->flags & FS_PROJINHERIT_FL)
fa->fsx_xflags |= FS_XFLAG_PROJINHERIT;
}
EXPORT_SYMBOL(fileattr_fill_flags);
/**
* vfs_fileattr_get - retrieve miscellaneous file attributes
* @dentry: the object to retrieve from
* @fa: fileattr pointer
*
* Call i_op->fileattr_get() callback, if exists.
*
* Return: 0 on success, or a negative error on failure.
*/
int vfs_fileattr_get(struct dentry *dentry, struct file_kattr *fa)
{
struct inode *inode = d_inode(dentry);
int error;
if (!inode->i_op->fileattr_get)
return -EOPNOTSUPP;
error = security_inode_file_getattr(dentry, fa);
if (error)
return error;
return inode->i_op->fileattr_get(dentry, fa);
}
EXPORT_SYMBOL(vfs_fileattr_get);
static void fileattr_to_file_attr(const struct file_kattr *fa,
struct file_attr *fattr)
{
__u32 mask = FS_XFLAGS_MASK;
memset(fattr, 0, sizeof(struct file_attr));
fattr->fa_xflags = fa->fsx_xflags & mask;
fattr->fa_extsize = fa->fsx_extsize;
fattr->fa_nextents = fa->fsx_nextents;
fattr->fa_projid = fa->fsx_projid;
fattr->fa_cowextsize = fa->fsx_cowextsize;
}
/**
* copy_fsxattr_to_user - copy fsxattr to userspace.
* @fa: fileattr pointer
* @ufa: fsxattr user pointer
*
* Return: 0 on success, or -EFAULT on failure.
*/
int copy_fsxattr_to_user(const struct file_kattr *fa, struct fsxattr __user *ufa)
{
struct fsxattr xfa;
__u32 mask = FS_XFLAGS_MASK;
memset(&xfa, 0, sizeof(xfa));
xfa.fsx_xflags = fa->fsx_xflags & mask;
xfa.fsx_extsize = fa->fsx_extsize;
xfa.fsx_nextents = fa->fsx_nextents;
xfa.fsx_projid = fa->fsx_projid;
xfa.fsx_cowextsize = fa->fsx_cowextsize;
if (copy_to_user(ufa, &xfa, sizeof(xfa)))
return -EFAULT;
return 0;
}
EXPORT_SYMBOL(copy_fsxattr_to_user);
static int file_attr_to_fileattr(const struct file_attr *fattr,
struct file_kattr *fa)
{
__u64 mask = FS_XFLAGS_MASK;
if (fattr->fa_xflags & ~mask)
return -EINVAL;
fileattr_fill_xflags(fa, fattr->fa_xflags);
fa->fsx_xflags &= ~FS_XFLAG_RDONLY_MASK;
fa->fsx_extsize = fattr->fa_extsize;
fa->fsx_projid = fattr->fa_projid;
fa->fsx_cowextsize = fattr->fa_cowextsize;
return 0;
}
static int copy_fsxattr_from_user(struct file_kattr *fa,
struct fsxattr __user *ufa)
{
struct fsxattr xfa;
__u32 mask = FS_XFLAGS_MASK;
if (copy_from_user(&xfa, ufa, sizeof(xfa)))
return -EFAULT;
if (xfa.fsx_xflags & ~mask)
return -EOPNOTSUPP;
fileattr_fill_xflags(fa, xfa.fsx_xflags);
fa->fsx_xflags &= ~FS_XFLAG_RDONLY_MASK;
fa->fsx_extsize = xfa.fsx_extsize;
fa->fsx_nextents = xfa.fsx_nextents;
fa->fsx_projid = xfa.fsx_projid;
fa->fsx_cowextsize = xfa.fsx_cowextsize;
return 0;
}
/*
* Generic function to check FS_IOC_FSSETXATTR/FS_IOC_SETFLAGS values and reject
* any invalid configurations.
*
* Note: must be called with inode lock held.
*/
static int fileattr_set_prepare(struct inode *inode,
const struct file_kattr *old_ma,
struct file_kattr *fa)
{
int err;
/*
* The IMMUTABLE and APPEND_ONLY flags can only be changed by
* the relevant capability.
*/
if ((fa->flags ^ old_ma->flags) & (FS_APPEND_FL | FS_IMMUTABLE_FL) &&
!capable(CAP_LINUX_IMMUTABLE))
return -EPERM;
err = fscrypt_prepare_setflags(inode, old_ma->flags, fa->flags);
if (err)
return err;
/*
* Project Quota ID state is only allowed to change from within the init
* namespace. Enforce that restriction only if we are trying to change
* the quota ID state. Everything else is allowed in user namespaces.
*/
if (current_user_ns() != &init_user_ns) {
if (old_ma->fsx_projid != fa->fsx_projid)
return -EINVAL;
if ((old_ma->fsx_xflags ^ fa->fsx_xflags) &
FS_XFLAG_PROJINHERIT)
return -EINVAL;
} else {
/*
* Caller is allowed to change the project ID. If it is being
* changed, make sure that the new value is valid.
*/
if (old_ma->fsx_projid != fa->fsx_projid &&
!projid_valid(make_kprojid(&init_user_ns, fa->fsx_projid)))
return -EINVAL;
}
/* Check extent size hints. */
if ((fa->fsx_xflags & FS_XFLAG_EXTSIZE) && !S_ISREG(inode->i_mode))
return -EINVAL;
if ((fa->fsx_xflags & FS_XFLAG_EXTSZINHERIT) &&
!S_ISDIR(inode->i_mode))
return -EINVAL;
if ((fa->fsx_xflags & FS_XFLAG_COWEXTSIZE) &&
!S_ISREG(inode->i_mode) && !S_ISDIR(inode->i_mode))
return -EINVAL;
/*
* It is only valid to set the DAX flag on regular files and
* directories on filesystems.
*/
if ((fa->fsx_xflags & FS_XFLAG_DAX) &&
!(S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode)))
return -EINVAL;
/* Extent size hints of zero turn off the flags. */
if (fa->fsx_extsize == 0)
fa->fsx_xflags &= ~(FS_XFLAG_EXTSIZE | FS_XFLAG_EXTSZINHERIT);
if (fa->fsx_cowextsize == 0)
fa->fsx_xflags &= ~FS_XFLAG_COWEXTSIZE;
return 0;
}
/**
* vfs_fileattr_set - change miscellaneous file attributes
* @idmap: idmap of the mount
* @dentry: the object to change
* @fa: fileattr pointer
*
* After verifying permissions, call i_op->fileattr_set() callback, if
* exists.
*
* Verifying attributes involves retrieving current attributes with
* i_op->fileattr_get(), this also allows initializing attributes that have
* not been set by the caller to current values. Inode lock is held
* thoughout to prevent racing with another instance.
*
* Return: 0 on success, or a negative error on failure.
*/
int vfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry,
struct file_kattr *fa)
{
struct inode *inode = d_inode(dentry);
struct file_kattr old_ma = {};
int err;
if (!inode->i_op->fileattr_set)
return -EOPNOTSUPP;
if (!inode_owner_or_capable(idmap, inode))
return -EPERM;
inode_lock(inode);
err = vfs_fileattr_get(dentry, &old_ma);
if (!err) {
/* initialize missing bits from old_ma */
if (fa->flags_valid) {
fa->fsx_xflags |= old_ma.fsx_xflags & ~FS_XFLAG_COMMON;
fa->fsx_extsize = old_ma.fsx_extsize;
fa->fsx_nextents = old_ma.fsx_nextents;
fa->fsx_projid = old_ma.fsx_projid;
fa->fsx_cowextsize = old_ma.fsx_cowextsize;
} else {
fa->flags |= old_ma.flags & ~FS_COMMON_FL;
}
err = fileattr_set_prepare(inode, &old_ma, fa);
if (err)
goto out;
err = security_inode_file_setattr(dentry, fa);
if (err)
goto out;
err = inode->i_op->fileattr_set(idmap, dentry, fa);
if (err)
goto out;
}
out:
inode_unlock(inode);
return err;
}
EXPORT_SYMBOL(vfs_fileattr_set);
int ioctl_getflags(struct file *file, unsigned int __user *argp)
{
struct file_kattr fa = { .flags_valid = true }; /* hint only */
int err;
err = vfs_fileattr_get(file->f_path.dentry, &fa);
if (err == -EOPNOTSUPP)
err = -ENOIOCTLCMD;
if (!err)
err = put_user(fa.flags, argp);
return err;
}
EXPORT_SYMBOL(ioctl_getflags);
int ioctl_setflags(struct file *file, unsigned int __user *argp)
{
struct mnt_idmap *idmap = file_mnt_idmap(file);
struct dentry *dentry = file->f_path.dentry;
struct file_kattr fa;
unsigned int flags;
int err;
err = get_user(flags, argp);
if (!err) {
err = mnt_want_write_file(file);
if (!err) {
fileattr_fill_flags(&fa, flags);
err = vfs_fileattr_set(idmap, dentry, &fa);
mnt_drop_write_file(file);
if (err == -EOPNOTSUPP)
err = -ENOIOCTLCMD;
}
}
return err;
}
EXPORT_SYMBOL(ioctl_setflags);
int ioctl_fsgetxattr(struct file *file, void __user *argp)
{
struct file_kattr fa = { .fsx_valid = true }; /* hint only */
int err;
err = vfs_fileattr_get(file->f_path.dentry, &fa);
if (err == -EOPNOTSUPP)
err = -ENOIOCTLCMD;
if (!err)
err = copy_fsxattr_to_user(&fa, argp);
return err;
}
EXPORT_SYMBOL(ioctl_fsgetxattr);
int ioctl_fssetxattr(struct file *file, void __user *argp)
{
struct mnt_idmap *idmap = file_mnt_idmap(file);
struct dentry *dentry = file->f_path.dentry;
struct file_kattr fa;
int err;
err = copy_fsxattr_from_user(&fa, argp);
if (!err) {
err = mnt_want_write_file(file);
if (!err) {
err = vfs_fileattr_set(idmap, dentry, &fa);
mnt_drop_write_file(file);
if (err == -EOPNOTSUPP)
err = -ENOIOCTLCMD;
}
}
return err;
}
EXPORT_SYMBOL(ioctl_fssetxattr);
SYSCALL_DEFINE5(file_getattr, int, dfd, const char __user *, filename,
struct file_attr __user *, ufattr, size_t, usize,
unsigned int, at_flags)
{
struct path filepath __free(path_put) = {};
struct filename *name __free(putname) = NULL;
unsigned int lookup_flags = 0;
struct file_attr fattr;
struct file_kattr fa;
int error;
BUILD_BUG_ON(sizeof(struct file_attr) < FILE_ATTR_SIZE_VER0);
BUILD_BUG_ON(sizeof(struct file_attr) != FILE_ATTR_SIZE_LATEST);
if ((at_flags & ~(AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH)) != 0)
return -EINVAL;
if (!(at_flags & AT_SYMLINK_NOFOLLOW))
lookup_flags |= LOOKUP_FOLLOW;
if (usize > PAGE_SIZE)
return -E2BIG;
if (usize < FILE_ATTR_SIZE_VER0)
return -EINVAL;
name = getname_maybe_null(filename, at_flags);
if (IS_ERR(name))
return PTR_ERR(name);
if (!name && dfd >= 0) {
CLASS(fd, f)(dfd);
if (fd_empty(f))
return -EBADF;
filepath = fd_file(f)->f_path;
path_get(&filepath);
} else {
error = filename_lookup(dfd, name, lookup_flags, &filepath,
NULL);
if (error)
return error;
}
error = vfs_fileattr_get(filepath.dentry, &fa);
if (error)
return error;
fileattr_to_file_attr(&fa, &fattr);
error = copy_struct_to_user(ufattr, usize, &fattr,
sizeof(struct file_attr), NULL);
return error;
}
SYSCALL_DEFINE5(file_setattr, int, dfd, const char __user *, filename,
struct file_attr __user *, ufattr, size_t, usize,
unsigned int, at_flags)
{
struct path filepath __free(path_put) = {};
struct filename *name __free(putname) = NULL;
unsigned int lookup_flags = 0;
struct file_attr fattr;
struct file_kattr fa;
int error;
BUILD_BUG_ON(sizeof(struct file_attr) < FILE_ATTR_SIZE_VER0);
BUILD_BUG_ON(sizeof(struct file_attr) != FILE_ATTR_SIZE_LATEST);
if ((at_flags & ~(AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH)) != 0)
return -EINVAL;
if (!(at_flags & AT_SYMLINK_NOFOLLOW))
lookup_flags |= LOOKUP_FOLLOW;
if (usize > PAGE_SIZE)
return -E2BIG;
if (usize < FILE_ATTR_SIZE_VER0)
return -EINVAL;
error = copy_struct_from_user(&fattr, sizeof(struct file_attr), ufattr,
usize);
if (error)
return error;
error = file_attr_to_fileattr(&fattr, &fa);
if (error)
return error;
name = getname_maybe_null(filename, at_flags);
if (IS_ERR(name))
return PTR_ERR(name);
if (!name && dfd >= 0) {
CLASS(fd, f)(dfd);
if (fd_empty(f))
return -EBADF;
filepath = fd_file(f)->f_path;
path_get(&filepath);
} else {
error = filename_lookup(dfd, name, lookup_flags, &filepath,
NULL);
if (error)
return error;
}
error = mnt_want_write(filepath.mnt);
if (!error) {
error = vfs_fileattr_set(mnt_idmap(filepath.mnt),
filepath.dentry, &fa);
mnt_drop_write(filepath.mnt);
}
return error;
}