[PATCH RFC v3 43/90] pidfd: let pidfd_configure() set credentials

From: Josef Bacik

Date: Thu Nov 05 2026 - 10:22:38 EST


A task that is spawned into a cell starts as uid 0 of the cell. Add a way
to start it as another user. PIDFD_SET_BINARY with the key "cred" takes a
struct pidfd_cred that holds a uid, a gid and supplementary groups, all
given as ids of the cell.

The credentials are applied in the context of the new task. The task loses
its capabilities in the process, as it would with a setresuid() away from
uid 0.

Signed-off-by: Josef Bacik <josef@xxxxxxxxxxxxxx>
---
include/uapi/linux/pidfd.h | 10 ++++
kernel/embryo.c | 116 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 126 insertions(+)

diff --git a/include/uapi/linux/pidfd.h b/include/uapi/linux/pidfd.h
index c01bce5e17ad..e1f6d27bb220 100644
--- a/include/uapi/linux/pidfd.h
+++ b/include/uapi/linux/pidfd.h
@@ -24,6 +24,16 @@ enum pidfd_configure_cmd {
PIDFD_SET_FD = 2, /* The file to have at number @key, from fd @aux */
PIDFD_CMD_START = 3, /* Make the task, apply everything and exec */
};
+
+/* pidfd_configure(PIDFD_SET_BINARY, "cred"). Ids are those of the new task. */
+struct pidfd_cred {
+ __u32 uid;
+ __u32 gid;
+ __u32 ngroups;
+ __u32 __spare;
+ __aligned_u64 groups; /* pointer to @ngroups __u32 gids */
+};
+
/* Flags for pidfd_send_signal(). */
#define PIDFD_SIGNAL_THREAD (1UL << 0)
#define PIDFD_SIGNAL_THREAD_GROUP (1UL << 1)
diff --git a/kernel/embryo.c b/kernel/embryo.c
index 2c65a99eaeea..0ad85cd2b865 100644
--- a/kernel/embryo.c
+++ b/kernel/embryo.c
@@ -69,8 +69,14 @@ struct embryo {
struct embryo_fd fds[EMBRYO_MAX_FDS];
unsigned int nr_fds;

+ bool have_cred;
+ kuid_t uid;
+ kgid_t gid;
+ struct group_info *groups;
+
struct pid *pid; /* the reference alloc_pid_struct() returned */
struct cell *cell; /* where the task is going to be */
+ struct user_namespace *user_ns; /* likewise */

int result;
struct completion done; /* embryo -> starter */
@@ -124,6 +130,9 @@ static void embryo_drop_config(struct embryo *e)
embryo_strings_free(&e->argv);
embryo_strings_free(&e->envp);
embryo_put_fds(e);
+ if (e->groups)
+ put_group_info(e->groups);
+ e->groups = NULL;
}

static void embryo_put(struct embryo *e)
@@ -132,6 +141,7 @@ static void embryo_put(struct embryo *e)
return;

embryo_drop_config(e);
+ put_user_ns(e->user_ns);
cell_put(e->cell);
kfree_rcu(e, rcu);
}
@@ -143,6 +153,52 @@ static void embryo_detach(struct embryo *e)
embryo_put(e);
}

+static int embryo_apply_cred(struct embryo *e)
+{
+ const struct cred *old = current_cred();
+ struct user_struct *user;
+ struct cred *new;
+ int ret;
+
+ if (!e->have_cred)
+ return 0;
+
+ new = prepare_creds();
+ if (!new)
+ return -ENOMEM;
+
+ new->uid = new->euid = new->suid = new->fsuid = e->uid;
+ new->gid = new->egid = new->sgid = new->fsgid = e->gid;
+ if (e->groups)
+ set_groups(new, e->groups);
+
+ ret = -EAGAIN;
+ user = alloc_uid(e->uid);
+ if (!user)
+ goto err;
+ free_uid(new->user);
+ new->user = user;
+
+ /* Leaving uid 0 behind costs the capabilities, as setresuid() would. */
+ ret = security_task_fix_setuid(new, old, LSM_SETID_RES);
+ if (ret)
+ goto err;
+ ret = security_task_fix_setgid(new, old, LSM_SETID_RES);
+ if (ret)
+ goto err;
+ ret = security_task_fix_setgroups(new, old);
+ if (ret)
+ goto err;
+ ret = set_cred_ucounts(new);
+ if (ret)
+ goto err;
+ return commit_creds(new);
+
+err:
+ abort_creds(new);
+ return ret;
+}
+
static int embryo_exec(struct embryo *e)
{
static const char * const no_strings[] = { NULL };
@@ -150,6 +206,9 @@ static int embryo_exec(struct embryo *e)
int ret;

ret = embryo_install_fds(e);
+ if (ret)
+ return ret;
+ ret = embryo_apply_cred(e);
if (ret)
return ret;

@@ -286,6 +345,7 @@ int embryo_pidfd_open(int cfd, unsigned int flags)
}
e->pid = alloc_pid_struct(cell_nsproxy(cell)->pid_ns_for_children);
e->cell = cell_get(cell);
+ e->user_ns = get_user_ns(cell_user_ns(cell));
cell_fork_finish(cell);

ret = -ENOMEM;
@@ -389,6 +449,60 @@ static int embryo_set_strings(struct embryo_strings *s, const void __user *ubloc
return 0;
}

+static int embryo_set_cred(struct embryo *e, const void __user *uvalue, size_t len)
+{
+ struct group_info *groups = NULL;
+ struct pidfd_cred tc;
+ kuid_t uid;
+ kgid_t gid;
+ u32 i;
+
+ if (len != sizeof(tc))
+ return -EINVAL;
+ if (copy_from_user(&tc, uvalue, sizeof(tc)))
+ return -EFAULT;
+ if (tc.__spare || tc.ngroups > NGROUPS_MAX)
+ return -EINVAL;
+
+ /* The ids are the cell's own. One it does not have cannot be named. */
+ uid = make_kuid(e->user_ns, tc.uid);
+ gid = make_kgid(e->user_ns, tc.gid);
+ if (!uid_valid(uid) || !gid_valid(gid))
+ return -EINVAL;
+
+ if (tc.ngroups) {
+ const u32 __user *ugroups = u64_to_user_ptr(tc.groups);
+
+ if (!userns_may_setgroups(e->user_ns))
+ return -EPERM;
+ groups = groups_alloc(tc.ngroups);
+ if (!groups)
+ return -ENOMEM;
+ for (i = 0; i < tc.ngroups; i++) {
+ u32 g;
+
+ if (get_user(g, ugroups + i)) {
+ put_group_info(groups);
+ return -EFAULT;
+ }
+ groups->gid[i] = make_kgid(e->user_ns, g);
+ if (!gid_valid(groups->gid[i])) {
+ put_group_info(groups);
+ return -EINVAL;
+ }
+ }
+ groups_sort(groups);
+ }
+
+ if (e->groups)
+ put_group_info(e->groups);
+ e->groups = groups;
+ e->uid = uid;
+ e->gid = gid;
+ e->have_cred = true;
+ return 0;
+}
+
/*
* The file table of the new task is exactly what is put here. After this
* nothing crosses the boundary of the cell any more, so this is where it is
@@ -576,6 +690,8 @@ SYSCALL_DEFINE5(pidfd_configure, int, pidfd, unsigned int, cmd,
ret = embryo_set_strings(&e->argv, uvalue, aux);
else if (!strcmp(key, "envp"))
ret = embryo_set_strings(&e->envp, uvalue, aux);
+ else if (!strcmp(key, "cred"))
+ ret = embryo_set_cred(e, uvalue, aux);
else
ret = -EINVAL;
break;

--
2.55.0