From: "Hallgrimur H. Gunnarsson" <halg@google.com>
To: Christian Brauner <brauner@kernel.org>,
Alexander Viro <viro@zeniv.linux.org.uk>,
Paul Moore <paul@paul-moore.com>,
Serge Hallyn <sergeh@kernel.org>, Kees Cook <kees@kernel.org>,
"David S . Miller" <davem@davemloft.net>,
Eric Dumazet <edumazet@google.com>,
Jakub Kicinski <kuba@kernel.org>,
Paolo Abeni <pabeni@redhat.com>
Cc: "Jan Kara" <jack@suse.cz>, "James Morris" <jmorris@namei.org>,
"Simon Horman" <horms@kernel.org>,
"Jonathan Corbet" <corbet@lwn.net>,
"Shuah Khan" <skhan@linuxfoundation.org>,
"Randy Dunlap" <rdunlap@infradead.org>,
"Jann Horn" <jannh@google.com>,
"Günther Noack" <gnoack@google.com>,
"KP Singh" <kpsingh@kernel.org>,
"Jonathan Calmels" <jcalmels@3xx0.net>,
"Casey Schaufler" <casey@schaufler-ca.com>,
linux-fsdevel@vger.kernel.org,
linux-security-module@vger.kernel.org, linux-api@vger.kernel.org,
containers@lists.linux.dev, netdev@vger.kernel.org,
linux-doc@vger.kernel.org, linux-kselftest@vger.kernel.org,
linux-kernel@vger.kernel.org,
"Hallgrimur H. Gunnarsson" <halg@google.com>
Subject: [PATCH 3/8] userns: allow restricting capability bounding set via clone3()
Date: Wed, 28 Oct 2026 22:17:00 +0000 [thread overview]
Message-ID: <20261028221705.3915340-4-halg@google.com> (raw)
In-Reply-To: <20261028221705.3915340-1-halg@google.com>
While the unprivileged_userns_caps_allowed sysctl enforces a
system-wide capability ceiling for unprivileged users, the
clone3() support allows sandboxes and container runtimes to
voluntarily restrict the capability bounding set of a new user
namespace on a per-namespace basis.
Introduce the CLONE_NEWUSER_BSET flag and add a cap_bset field
to struct clone_args (CLONE_ARGS_SIZE_VER3). When specified alongside
CLONE_NEWUSER, the new user namespace's capability bounding set is
intersected with cap_bset in addition to the parent namespace's
bounding set and any applicable unprivileged sysctl ceiling.
Signed-off-by: Hallgrimur H. Gunnarsson <halg@google.com>
---
Documentation/admin-guide/sysctl/kernel.rst | 5 +++++
include/linux/cred.h | 3 ++-
include/linux/sched/task.h | 1 +
include/linux/user_namespace.h | 4 ++--
include/uapi/linux/sched.h | 7 +++++++
kernel/cred.c | 10 ++++++++--
kernel/fork.c | 18 +++++++++++++++---
kernel/user_namespace.c | 11 +++++++----
8 files changed, 47 insertions(+), 12 deletions(-)
diff --git a/Documentation/admin-guide/sysctl/kernel.rst b/Documentation/admin-guide/sysctl/kernel.rst
index 25fe46e7724b..4b6654e89c6d 100644
--- a/Documentation/admin-guide/sysctl/kernel.rst
+++ b/Documentation/admin-guide/sysctl/kernel.rst
@@ -1709,6 +1709,11 @@ Privileged creators (holding ``CAP_SYS_ADMIN`` in the parent namespace) are
unaffected by this ceiling and inherit the parent namespace's bounding set
unmodified.
+Regardless of privilege level, a creator using ``clone3(2)`` with the
+``CLONE_NEWUSER_BSET`` flag can further restrict the new namespace's
+bounding set by specifying a ``cap_bset`` mask in ``struct clone_args``,
+which is intersected with the resulting bounding set.
+
Upon entering a user namespace during creation or via ``setns(2)``,
the task's permitted, effective, and bounding capability sets are clamped
to the namespace's bounding set. Tasks inside the namespace cannot exceed
diff --git a/include/linux/cred.h b/include/linux/cred.h
index 6ef1750c93e2..39eeb1584ef7 100644
--- a/include/linux/cred.h
+++ b/include/linux/cred.h
@@ -19,6 +19,7 @@
struct cred;
struct inode;
+struct kernel_clone_args;
extern struct task_struct init_task;
@@ -150,9 +151,9 @@ struct cred {
};
} __randomize_layout;
+int copy_creds(struct task_struct *p, const struct kernel_clone_args *kargs);
extern void __put_cred(struct cred *);
extern void exit_creds(struct task_struct *);
-extern int copy_creds(struct task_struct *, u64);
extern const struct cred *get_task_cred(struct task_struct *);
extern struct cred *cred_alloc_blank(void);
extern struct cred *prepare_creds(void);
diff --git a/include/linux/sched/task.h b/include/linux/sched/task.h
index e0c1ca8c6a18..498db7adce5f 100644
--- a/include/linux/sched/task.h
+++ b/include/linux/sched/task.h
@@ -45,6 +45,7 @@ struct kernel_clone_args {
struct cgroup *cgrp;
struct css_set *cset;
unsigned int kill_seq;
+ u64 cap_bset;
};
/*
diff --git a/include/linux/user_namespace.h b/include/linux/user_namespace.h
index a0ccfb93f2a7..a584ee5775af 100644
--- a/include/linux/user_namespace.h
+++ b/include/linux/user_namespace.h
@@ -194,7 +194,7 @@ static inline bool userns_has_cap(const struct user_namespace *ns, int cap)
return cap_raised(ns->cap_bset, cap);
}
-extern int create_user_ns(struct cred *new);
+int create_user_ns(struct cred *new, kernel_cap_t req_bset);
extern int unshare_userns(unsigned long unshare_flags, struct cred **new_cred);
extern void __put_user_ns(struct user_namespace *ns);
@@ -230,7 +230,7 @@ static inline bool userns_has_cap(const struct user_namespace *ns, int cap)
return true;
}
-static inline int create_user_ns(struct cred *new)
+static inline int create_user_ns(struct cred *new, kernel_cap_t req_bset)
{
return -EINVAL;
}
diff --git a/include/uapi/linux/sched.h b/include/uapi/linux/sched.h
index efd00d9f8bd3..10e4a4733005 100644
--- a/include/uapi/linux/sched.h
+++ b/include/uapi/linux/sched.h
@@ -41,6 +41,7 @@
#define CLONE_PIDFD_AUTOKILL (1ULL << 36) /* Kill child when clone pidfd closes. */
#define CLONE_EMPTY_MNTNS (1ULL << 37) /* Create an empty mount namespace. */
#define CLONE_NEWNET_LOOPBACK (1ULL << 38) /* Create network namespace with loopback UP. */
+#define CLONE_NEWUSER_BSET (1ULL << 39) /* Set userns capability bounding set. */
/*
* cloning flags intersect with CSIGNAL so can be used with unshare and clone3
@@ -96,6 +97,10 @@
* kernel's limit of nested PID namespaces.
* @cgroup: If CLONE_INTO_CGROUP is specified set this to
* a file descriptor for the cgroup.
+ * @cap_bset: If CLONE_NEWUSER_BSET is specified, this
+ * specifies the capability bounding set mask to
+ * apply to the new user namespace. Unknown
+ * capability bits (> CAP_LAST_CAP) are ignored.
*
* The structure is versioned by size and thus extensible.
* New struct members must go at the end of the struct and
@@ -113,12 +118,14 @@ struct clone_args {
__aligned_u64 set_tid;
__aligned_u64 set_tid_size;
__aligned_u64 cgroup;
+ __aligned_u64 cap_bset;
};
#endif
#define CLONE_ARGS_SIZE_VER0 64 /* sizeof first published struct */
#define CLONE_ARGS_SIZE_VER1 80 /* sizeof second published struct */
#define CLONE_ARGS_SIZE_VER2 88 /* sizeof third published struct */
+#define CLONE_ARGS_SIZE_VER3 96 /* sizeof fourth published struct */
/*
* Scheduling policies
diff --git a/kernel/cred.c b/kernel/cred.c
index 3df4e15bd67f..20f37a351602 100644
--- a/kernel/cred.c
+++ b/kernel/cred.c
@@ -260,8 +260,9 @@ struct cred *prepare_exec_creds(void)
* The new process gets the current process's subjective credentials as its
* objective and subjective credentials
*/
-int copy_creds(struct task_struct *p, u64 clone_flags)
+int copy_creds(struct task_struct *p, const struct kernel_clone_args *kargs)
{
+ u64 clone_flags = kargs->flags;
struct cred *new;
int ret;
@@ -288,7 +289,12 @@ int copy_creds(struct task_struct *p, u64 clone_flags)
return -ENOMEM;
if (clone_flags & CLONE_NEWUSER) {
- ret = create_user_ns(new);
+ kernel_cap_t cap_bset = CAP_FULL_SET;
+
+ if (clone_flags & CLONE_NEWUSER_BSET)
+ cap_bset = (kernel_cap_t){ .val = kargs->cap_bset };
+
+ ret = create_user_ns(new, cap_bset);
if (ret < 0)
goto error_put;
ret = set_cred_ucounts(new);
diff --git a/kernel/fork.c b/kernel/fork.c
index c0f7520e754a..a09f62cadeac 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -2168,7 +2168,7 @@ __latent_entropy struct task_struct *copy_process(
#ifdef CONFIG_PROVE_LOCKING
DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endif
- retval = copy_creds(p, clone_flags);
+ retval = copy_creds(p, args);
if (retval < 0)
goto bad_fork_free;
@@ -2932,7 +2932,9 @@ static noinline int copy_clone_args_from_user(struct kernel_clone_args *kargs,
CLONE_ARGS_SIZE_VER1);
BUILD_BUG_ON(offsetofend(struct clone_args, cgroup) !=
CLONE_ARGS_SIZE_VER2);
- BUILD_BUG_ON(sizeof(struct clone_args) != CLONE_ARGS_SIZE_VER2);
+ BUILD_BUG_ON(offsetofend(struct clone_args, cap_bset) !=
+ CLONE_ARGS_SIZE_VER3);
+ BUILD_BUG_ON(sizeof(struct clone_args) != CLONE_ARGS_SIZE_VER3);
if (unlikely(usize > PAGE_SIZE))
return -E2BIG;
@@ -2962,6 +2964,11 @@ static noinline int copy_clone_args_from_user(struct kernel_clone_args *kargs,
(args.cgroup > INT_MAX || usize < CLONE_ARGS_SIZE_VER2))
return -EINVAL;
+ if ((args.flags & CLONE_NEWUSER_BSET) && usize < CLONE_ARGS_SIZE_VER3)
+ return -EINVAL;
+ if (!(args.flags & CLONE_NEWUSER_BSET) && args.cap_bset)
+ return -EINVAL;
+
*kargs = (struct kernel_clone_args){
.flags = args.flags,
.pidfd = u64_to_user_ptr(args.pidfd),
@@ -2973,6 +2980,7 @@ static noinline int copy_clone_args_from_user(struct kernel_clone_args *kargs,
.tls = args.tls,
.set_tid_size = args.set_tid_size,
.cgroup = args.cgroup,
+ .cap_bset = args.cap_bset & CAP_VALID_MASK,
};
if (args.set_tid &&
@@ -3020,7 +3028,7 @@ static bool clone3_args_valid(struct kernel_clone_args *kargs)
~(CLONE_LEGACY_FLAGS | CLONE_CLEAR_SIGHAND |
CLONE_INTO_CGROUP | CLONE_AUTOREAP | CLONE_NNP |
CLONE_PIDFD_AUTOKILL | CLONE_EMPTY_MNTNS |
- CLONE_NEWNET_LOOPBACK))
+ CLONE_NEWNET_LOOPBACK | CLONE_NEWUSER_BSET))
return false;
/*
@@ -3038,6 +3046,10 @@ static bool clone3_args_valid(struct kernel_clone_args *kargs)
kargs->exit_signal)
return false;
+ if ((kargs->flags & CLONE_NEWUSER_BSET) &&
+ !(kargs->flags & CLONE_NEWUSER))
+ return false;
+
if (!clone3_stack_valid(kargs))
return false;
diff --git a/kernel/user_namespace.c b/kernel/user_namespace.c
index 92f489e268a8..6e95dd1a1d04 100644
--- a/kernel/user_namespace.c
+++ b/kernel/user_namespace.c
@@ -143,7 +143,7 @@ static unsigned long enforced_nproc_rlimit(void)
* This is called by copy_creds(), which will finish setting the target task's
* credentials.
*/
-int create_user_ns(struct cred *new)
+int create_user_ns(struct cred *new, kernel_cap_t req_bset)
{
struct user_namespace *ns, *parent_ns = new->user_ns;
kuid_t owner = new->euid;
@@ -184,15 +184,18 @@ int create_user_ns(struct cred *new)
goto fail_dec;
/*
+ * Intersect the parent's bounding set with any voluntary
+ * restrictions requested via clone3().
+ *
* If the caller has CAP_SYS_ADMIN in the parent namespace,
- * they are a privileged creator and inherit the parent's
+ * they are a privileged creator and inherit this resulting
* bounding set directly.
*
* Otherwise, enforce the unprivileged bounding ceiling, but
* allow the child namespace to retain any capabilities the
* creator already holds.
*/
- cap_bset = parent_ns->cap_bset;
+ cap_bset = cap_intersect(parent_ns->cap_bset, req_bset);
if (!ns_capable_noaudit(parent_ns, CAP_SYS_ADMIN)) {
kernel_cap_t unpriv_bset = READ_ONCE(sysctl_unpriv_userns_bset);
kernel_cap_t allowed = cap_combine(unpriv_bset,
@@ -267,7 +270,7 @@ int unshare_userns(unsigned long unshare_flags, struct cred **new_cred)
cred = prepare_creds();
if (cred) {
- err = create_user_ns(cred);
+ err = create_user_ns(cred, CAP_FULL_SET);
if (err)
put_cred(cred);
else
--
2.56.0.313.g2479322e68-goog
next prev parent reply other threads:[~2026-10-28 22:18 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-28 22:16 [PATCH 0/8] userns: per-namespace capability bounding set and opt-in netns loopback Hallgrimur H. Gunnarsson
2026-10-28 22:16 ` [PATCH 1/8] net: introduce CLONE_NEWNET_LOOPBACK to auto-enable loopback Hallgrimur H. Gunnarsson
2026-10-28 22:16 ` [PATCH 2/8] userns: introduce capability bounding set for user namespaces Hallgrimur H. Gunnarsson
2026-10-28 22:17 ` Hallgrimur H. Gunnarsson [this message]
2026-10-28 22:17 ` [PATCH 4/8] nsfs: add NS_USER_GET_INFO ioctl to inspect user namespace capabilities Hallgrimur H. Gunnarsson
2026-10-28 22:17 ` [PATCH 5/8] selftests/net: add test suite for CLONE_NEWNET_LOOPBACK Hallgrimur H. Gunnarsson
2026-10-28 22:17 ` [PATCH 6/8] selftests/capabilities: tests for user namespace capability bounding set Hallgrimur H. Gunnarsson
2026-10-28 22:17 ` [PATCH 7/8] selftests/capabilities: add clone3() tests for CLONE_NEWUSER_BSET Hallgrimur H. Gunnarsson
2026-10-28 22:17 ` [PATCH 8/8] selftests/nsfs: add tests for NS_USER_GET_INFO ioctl Hallgrimur H. Gunnarsson
2026-10-28 22:56 ` [PATCH 0/8] userns: per-namespace capability bounding set and opt-in netns loopback Andy Lutomirski
2026-10-29 10:44 ` Alexey Gladkov
2026-10-29 12:11 ` Hallgrimur H. Gunnarsson
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261028221705.3915340-4-halg@google.com \
--to=halg@google.com \
--cc=brauner@kernel.org \
--cc=casey@schaufler-ca.com \
--cc=containers@lists.linux.dev \
--cc=corbet@lwn.net \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=gnoack@google.com \
--cc=horms@kernel.org \
--cc=jack@suse.cz \
--cc=jannh@google.com \
--cc=jcalmels@3xx0.net \
--cc=jmorris@namei.org \
--cc=kees@kernel.org \
--cc=kpsingh@kernel.org \
--cc=kuba@kernel.org \
--cc=linux-api@vger.kernel.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=linux-security-module@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=paul@paul-moore.com \
--cc=rdunlap@infradead.org \
--cc=sergeh@kernel.org \
--cc=skhan@linuxfoundation.org \
--cc=viro@zeniv.linux.org.uk \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.