[RFC PATCH v2 21/41] fs: add chown_r(), lchown_r(), fchown_r() and fchownat_r()
sashiko-bot at kernel.org
sashiko-bot at kernel.org
Thu Oct 22 03:17:45 UTC 2026
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] fs: broken unchanged and malformed ID handling in chown_path() for !MULTIUSER
- [High] fs: missing s_user_ns mapping check for UUID/GUID in chown_path()
- [Medium] fs: chown_ids_r() accepts nonzero reserved bytes for UID_R_NONE and -1
--
commit c1c7ee0a217eb922615794b81d3b58341468ee6f
Author: Jan Koester <jan.koester at tuxist.de>
fs: add chown_r(), lchown_r(), fchown_r() and fchownat_r()
This commit adds the chown_r(), lchown_r(), fchown_r(), and fchownat_r()
system calls accepting struct __kernel_uid_r owners and groups, and refactors
the classic chown paths to share lookup and setattr logic via struct
chown_ids and chown_path().
> diff --git a/fs/open.c b/fs/open.c
> index 6b1c14e684a93..26175b5aa8aa6 100644
> --- a/fs/open.c
> +++ b/fs/open.c
> @@ -785,7 +785,46 @@ static inline bool setattr_vfsgid(struct iattr *attr, kgid_t kgid)
> return true;
> }
>
> -int chown_common(const struct path *path, uid_t user, gid_t group)
> +/*
> + * The new owner and group of a chown. Invalid ids stand for "unchanged";
> + * @err is an error of their conversion, reported once the path is found,
> + * which keeps the classic calls' order of errors.
> + */
> +struct chown_ids {
> + kuid_t uid;
> + kgid_t gid;
> + int err;
> +};
> +
> +static struct chown_ids chown_ids_classic(uid_t user, gid_t group)
> +{
> + struct user_namespace *ns = current_user_ns();
> + struct chown_ids ids = {
> + .uid = make_kuid(ns, user),
> + .gid = make_kgid(ns, group),
> + };
> +
> + if ((user != (uid_t)-1 && !uid_valid(ids.uid)) ||
> + (group != (gid_t)-1 && !gid_valid(ids.gid)))
> + ids.err = -EINVAL;
> + return ids;
> +}
> +
> +static int chown_ids_r(const struct __kernel_uid_r __user *user,
> + const struct __kernel_uid_r __user *group,
> + struct chown_ids *ids)
> +{
> + struct user_namespace *ns = current_user_ns();
> + int err;
> +
> + ids->err = 0;
> + err = get_kuid_r(ns, user, &ids->uid);
> + if (!err)
> + err = get_kgid_r(ns, group, &ids->gid);
> + return err;
> +}
[Severity: Medium]
When userspace calls sys_chown_r(), sys_lchown_r(), sys_fchown_r(), or
sys_fchownat_r() with a struct __kernel_uid_r whose type is UID_R_NONE or
UID_R_LEGACY with legacy == (u32)-1, can non-zero bytes in the reserved union
area bypass validation?
In include/uapi/linux/uidgid_r.h, struct __kernel_uid_r documents:
/*
* Bytes past the member .type selects are reserved and must be zero.
*/
However, chown_ids_r() delegates to get_kuid_r() and get_kgid_r() in
kernel/uidgid_r.c, which return 0 as soon as kid_r_is_none(&id) matches,
before make_kuid_r() or make_kgid_r() calls kid_r_valid(&id):
static bool kid_r_is_none(const struct __kernel_uid_r *id)
{
return id->type == UID_R_NONE ||
(id->type == UID_R_LEGACY && id->legacy == (u32)-1);
}
int get_kuid_r(struct user_namespace *from,
const struct __kernel_uid_r __user *uid, kuid_t *kuid)
{
...
if (kid_r_is_none(&id))
return 0;
*kuid = make_kuid_r(from, &id);
return uid_valid(*kuid) ? 0 : -EINVAL;
}
Because kid_r_is_none() does not check the 16 union bytes for UID_R_NONE or
the 12 trailing bytes (id->uuid + sizeof(id->legacy)) for UID_R_LEGACY with
legacy == (u32)-1, shouldn't those reserved bytes be verified to be zero so
that non-zero trailing bytes are rejected with -EINVAL?
> +
> +static int chown_path(const struct path *path, const struct chown_ids *ids)
> {
> struct mnt_idmap *idmap;
> struct user_namespace *fs_userns;
> @@ -793,11 +832,9 @@ int chown_common(const struct path *path, uid_t user, gid_t group)
> struct delegated_inode delegated_inode = { };
> int error;
> struct iattr newattrs;
> - kuid_t uid;
> - kgid_t gid;
>
> - uid = make_kuid(current_user_ns(), user);
> - gid = make_kgid(current_user_ns(), group);
> + if (ids->err)
> + return ids->err;
>
> idmap = mnt_idmap(path->mnt);
> fs_userns = i_user_ns(inode);
> @@ -806,9 +843,9 @@ int chown_common(const struct path *path, uid_t user, gid_t group)
> newattrs.ia_vfsuid = INVALID_VFSUID;
> newattrs.ia_vfsgid = INVALID_VFSGID;
> newattrs.ia_valid = ATTR_CTIME;
> - if ((user != (uid_t)-1) && !setattr_vfsuid(&newattrs, uid))
> + if (uid_valid(ids->uid) && !setattr_vfsuid(&newattrs, ids->uid))
> return -EINVAL;
> - if ((group != (gid_t)-1) && !setattr_vfsgid(&newattrs, gid))
> + if (gid_valid(ids->gid) && !setattr_vfsgid(&newattrs, ids->gid))
> return -EINVAL;
[Severity: High]
How does chown_path() handle unchanged IDs (-1, NULL, or UID_R_NONE) and
malformed IDs when CONFIG_MULTIUSER is disabled?
Previously, chown_common() checked (user != (uid_t)-1) and
(group != (gid_t)-1) before calling setattr_vfsuid() and setattr_vfsgid().
With this change, chown_ids_classic() and chown_ids_r() encode an unchanged
owner or group as INVALID_UID / INVALID_GID, and chown_path() uses
uid_valid(ids->uid) and gid_valid(ids->gid) to decide whether to set ATTR_UID
and ATTR_GID.
In include/linux/uidgid.h, when !CONFIG_MULTIUSER, __kuid_val() and
__kgid_val() always return 0:
static inline uid_t __kuid_val(kuid_t uid)
{
return 0;
}
static inline bool uid_valid(kuid_t uid)
{
return !uid_is_legacy(uid) || __kuid_val(uid) != (uid_t) -1;
}
As a result, uid_valid(INVALID_UID) and gid_valid(INVALID_GID) always return
true on !CONFIG_MULTIUSER builds:
1. Calling chown(), lchown(), fchown(), or fchownat() with -1, or calling
chown_r(), lchown_r(), fchown_r(), or fchownat_r() with NULL, UID_R_NONE,
or legacy -1 causes chown_path() to call setattr_vfsuid() and
setattr_vfsgid() with INVALID_UID and INVALID_GID. That sets ATTR_UID and
ATTR_GID in newattrs.ia_valid and copies VFSUIDT_INIT(INVALID_UID) and
VFSGIDT_INIT(INVALID_GID) (with .legacy = -1) into newattrs.ia_vfsuid and
newattrs.ia_vfsgid, causing notify_change() -> may_setattr() to reject
no-op chown calls on immutable/append-only files with -EPERM and causing
setattr_copy() to write INVALID_UID / INVALID_GID into inode->i_uid and
inode->i_gid.
2. In chown_ids_r() -> get_kuid_r() / get_kgid_r(), malformed
struct __kernel_uid_r inputs for which make_kuid_r() or make_kgid_r()
returns INVALID_UID / INVALID_GID pass uid_valid(*kuid) / gid_valid(*kgid)
and return 0 instead of -EINVAL.
3. In chown_path(), because setattr_vfsuid() and setattr_vfsgid() only return
false when !uid_valid() or !gid_valid(), gating them with
uid_valid(ids->uid) and gid_valid(ids->gid) also makes the two
return -EINVAL branches unreachable.
> error = inode_lock_killable(inode);
> if (error)
[Severity: High]
What prevents chown_path() from setting a UUID or GUID owner or group on a
non-idmapped mount of a filesystem mounted in a non-initial user namespace
(inode->i_sb->s_user_ns != &init_user_ns)?
When a process in init_user_ns calls chown_r(), lchown_r(), fchown_r(), or
fchownat_r() with a UUID or GUID on a regular mount (idmap == &nop_mnt_idmap)
of a filesystem with SB_I_UID_R mounted in a child user namespace (such as
tmpfs, ramfs, devpts, or overlayfs), chown_path() calls security_path_chown()
with from_vfsuid() / from_vfsgid() and then calls notify_change().
In fs/attr.c:notify_change(), the superblock namespace check uses
vfsuid_has_fsmapping() and vfsgid_has_fsmapping():
if (ia_valid & ATTR_UID &&
!vfsuid_has_fsmapping(idmap, inode->i_sb->s_user_ns,
attr->ia_vfsuid))
return -EOVERFLOW;
However, vfsuid_has_fsmapping() in include/linux/mnt_idmapping.h simply
checks uid_valid(from_vfsuid(idmap, fs_userns, vfsuid)), and from_vfsuid() in
fs/mnt_idmapping.c returns early for nop_mnt_idmap before checking
kuid_has_mapping(fs_userns, ...):
kuid_t from_vfsuid(struct mnt_idmap *idmap,
struct user_namespace *fs_userns, vfsuid_t vfsuid)
{
uid_t uid;
if (idmap == &nop_mnt_idmap)
return AS_KUIDT(vfsuid);
if (idmap == &invalid_mnt_idmap)
return INVALID_UID;
if (!vfsuid_is_legacy(vfsuid))
return kuid_has_mapping(fs_userns, AS_KUIDT(vfsuid)) ?
AS_KUIDT(vfsuid) : INVALID_UID;
Because UUID and GUID identities only have mappings in init_user_ns,
shouldn't non-legacy IDs also be checked against fs_userns when
idmap == &nop_mnt_idmap so that notify_change() rejects them with -EOVERFLOW
instead of writing an unmapped UUID or GUID to the inode?
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1792552161.git.jan.koester@tuxist.de?part=21
More information about the Linux-security-module-archive
mailing list