* [PATCH v7 0/6] capabilities: close the ways around the CAP_SETFCAP rule for uid 0
@ 2026-10-26 21:00 Josef Bacik
2026-10-26 21:00 ` [PATCH v7 1/6] userns: record how far up CAP_SETFCAP reaches Josef Bacik
` (5 more replies)
0 siblings, 6 replies; 7+ messages in thread
From: Josef Bacik @ 2026-10-26 21:00 UTC (permalink / raw)
To: Serge Hallyn, Paul Moore, Christian Brauner
Cc: James Morris, David Howells, Jarkko Sakkinen, Andrew G. Morgan,
Serge Hallyn, linux-security-module, linux-kernel, keyrings,
linux-fsdevel, Shuah Khan, linux-kselftest, Bradley Morgan,
Josef Bacik
v1: https://lore.kernel.org/all/20261006-b4-setfcap-userns-v1-0-f47e7ed66072@toxicpanda.com/
v2: https://lore.kernel.org/all/20261014-b4-setfcap-userns-v2-0-fac4d19281d7@toxicpanda.com/
v3: https://lore.kernel.org/all/20261021-b4-setfcap-userns-v3-0-97c7ed4e43e1@toxicpanda.com/
v4: https://lore.kernel.org/all/20261022-b4-setfcap-userns-v4-0-88aa0ffaa08f@toxicpanda.com/
v5: https://lore.kernel.org/all/20261023-b4-setfcap-userns-v5-0-0620223be951@toxicpanda.com/
v6: https://lore.kernel.org/all/20261026-b4-setfcap-userns-v6-0-25f10b7ca497@toxicpanda.com/
v6->v7:
- 6/6 only, 1-5 are unchanged. Every case now reaches the step it is
named for, and a setup step that fails is an error rather than a
refusal. Cases that stopped at setns() are setns() cases now: a task
that joins has CAP_SETFCAP wherever the creator had it, so nothing
after setns() checks it. In the v3 rootid, nesting after dropping
CAP_SETFCAP and reverse idmapped mount cases a privileged task maps a
namespace created without CAP_SETFCAP, so the xattr gets written.
pidfd_getfd() is a ptrace case of its own. The session keyring case
joins a keyring of its own first, so it works under su, and skips
without keyrings (Sashiko).
v5->v6:
- New 6/6: a kselftest with every case the series has been through,
48 of them. Without the series 22 fail, with v5 the 4 below fail.
- 3/5: a task inside the namespace that is being mapped also has to
hold CAP_SETFCAP there to open or write a uid_map that maps an
ancestor's root user. Before, a task that gave up CAP_SETFCAP could
write "0 0 1" and get it back from execve() (Sashiko).
- 4/5: a v2 xattr on a filesystem whose namespace has no uid 0 is
refused with -EINVAL instead of stored unconverted, where it was
honoured nowhere. Falling through to the conversion would not do:
for a writer outside that namespace it names the writer's root user
instead (Sashiko).
v4->v5:
- 3/5: the uid_map check only looked for an extent at the parent's uid
0. The root user of an ancestor can sit at any uid in the parent, so
a task without CAP_SETFCAP could map "0 1000 1" under a "1000 0 1"
and get host uid 0 as its uid 0 unchecked. It now finds every
ancestor root user the map contains. 4/5 already refused the file
capabilities after such a map, so this is the map rule itself
(Sashiko). Dropped Bradley's Reviewed-by on 3/5, which changed.
v3->v4:
- 4/5: a v2 security.capability xattr skipped the new checks, it was
written as is whenever the writer had CAP_SETFCAP over the
filesystem's namespace. It now only does that when the filesystem's
root user is within reach, and goes through the v3 conversion and its
checks otherwise (Sashiko).
v2->v3:
- The setns() check in 2/5 and the ptrace check in 5/5 only apply to
namespaces that map the root user of an ancestor within the creator's
CAP_SETFCAP, or that have no uid_map yet. Root users stored through
an idmapped mount are left to whoever sets up the mount, so a root
task without CAP_SETFCAP can join an ordinary "0 100000" container
again (Serge, Christian).
- 2/5 adds cred_may_setfcap_userns() for those two checks and
cred_setfcap_reaches() for the uid_map check; nothing outside
create_user_ns() calls cap_setfcap_level() directly (Serge).
- Fixed the cap_setfcap_level() kernel-doc, @ns doesn't have to be the
cred's namespace or below; cap_capable() is compared with != 0
(Serge).
- Picked up Bradley's Reviewed-by on 1/5 and 3/5, not on 2/5, which
changed.
- Rebased onto current vfs.all.
v1->v2:
- The state lives on the user namespace now: ns->setfcap_level replaces
ns->parent_could_setfcap, and cred->setfcap_level is gone (Serge,
Christian).
- New 2/5: setns() refuses a task whose CAP_SETFCAP doesn't reach as far
up as the namespace's setfcap_level. It applies to every user
namespace, not only to those that map the parent's uid 0, because of
idmapped mounts (Serge, Christian).
- 5/5 asks a tracer for what setns() asks for, also when the namespace
doesn't map an ancestor's root user, again because of idmapped mounts.
- Dropped "userns: don't let setns() lend the right to map uid 0", the
setns() check covers it. Its nested case moved into 3/5, which checks
the opener and the writer.
- set_cred_user_ns() isn't touched anymore, so the put-before-use in
userns_install() is gone (Sashiko).
- Rebased onto current vfs.all.
What it does now:
Every user namespace gets a setfcap_level when it's created, which is
how far up the creator's CAP_SETFCAP counted. If its tasks can write
file capabilities for the root user of an ancestor, because the
namespace maps that root user or can still map it, setns() only lets in
tasks whose own CAP_SETFCAP reaches that far, and PTRACE_ATTACH asks the
tracer for the same. The checks on the way to file capabilities then go
by the namespace instead of the cred: a uid_map write that maps the
parent's uid 0 (opener and writer), and setting security.capability.
The visible difference from v1 is the setns() restriction. A task
without CAP_SETFCAP can't join, or attach to tasks in, a namespace that
maps the root user of an ancestor, such as host uid 0, and was created
by a task that had it. A task that joins takes on the namespace's
reach. Containers that don't map host uid 0, unprivileged users and
anything with CAP_SETFCAP aren't affected.
Testing: 6/6 runs every case on its own, 48 in all, as root in private
mount namespaces. On vfs.all without the series 22 of them fail: the
ways around the rule, and the two v2 xattrs without a root user that are
now refused; with the series all pass, including the idmapped mount
ones. The pidfd, namespaces, mount_setattr and ptrace selftests give
identical results with and without. The TLA+ model, which has no
idmapped mounts, no v2 xattrs and maps only uid 0 of the parent, finds
no violation with the narrower setns() and ptrace checks, and still
finds the setns() bypass when the check is taken out.
--- Original email (v1) ---
Hello,
Commit db2e718a4798 ("capabilities: require CAP_SETFCAP to map uid 0")
stops a root task that has given up CAP_SETFCAP from creating a user
namespace that maps uid 0 and then writing file capabilities in it that
the initial namespace honours. The check only looks at the task that
creates the namespace, so if somebody who did have CAP_SETFCAP created
one, there are still several ways around it:
- setns() into their namespace and write uid_map from inside (patch 2)
- write "0 0 1" through a uid_map fd that they opened (patch 3)
- setns() into a namespace of theirs that already maps uid 0 and set
security.capability there, no map write needed (patch 4)
- ptrace one of their tasks and have it do any of the above (patch 5)
On an unmodified kernel we took a uid 0 task with CAP_SETFCAP dropped
from its permitted, effective and bounding sets and, through each of
these, ended up with a file that a uid 1000 user execs with
CAP_SYS_ADMIN in its effective set.
Patch 1 adds cred->setfcap_level, which records how far up the
namespace tree a task's CAP_SETFCAP reached when it entered its
namespace, and patches 2-5 check it. Patch 4 is the check that closes
the class, the map patches make the uid 0 map rule mean what
db2e718a4798 meant it to, and patch 5 keeps ptrace from borrowing what
the target is entitled to.
This does change behaviour. Everything new is -EPERM:
- a task that entered a namespace without CAP_SETFCAP outside can't map
uid 0 of the outside or write fscaps for that root user anymore
- a uid_map fd opened by a task with CAP_SETFCAP can't be used by a
task without it to map uid 0
- if a privileged task maps "0 0 1" from the parent for a namespace
created by a task without CAP_SETFCAP, that namespace can no longer
write fscaps honoured outside
- PTRACE_ATTACH and PTRACE_TRACEME fail when the tracer gave up
CAP_SETFCAP and CAP_SYS_PTRACE, the target didn't, and they share a
root user
Rootless containers, privileged runtimes writing the map from the
parent, nested unprivileged namespaces and containers that don't map
host uid 0 aren't affected. The ptrace check is one compare for
targets in the initial namespace and in namespaces entered without
the capability.
Testing: a set of flows run on the base and patched kernels, every
bypass route above gets -EPERM with the series and the 16 legitimate
flows behave the same. The capabilities, namespaces, ptrace, pidfd and
proc selftests give the same results before and after. Thanks,
Josef
---
Changes in v7:
- Link to v6: https://patch.msgid.link/20261026-b4-setfcap-userns-v6-0-25f10b7ca497@toxicpanda.com
Changes in v6:
- Link to v5: https://patch.msgid.link/20261023-b4-setfcap-userns-v5-0-0620223be951@toxicpanda.com
Changes in v5:
- Link to v4: https://patch.msgid.link/20261022-b4-setfcap-userns-v4-0-88aa0ffaa08f@toxicpanda.com
Changes in v4:
- Link to v3: https://patch.msgid.link/20261021-b4-setfcap-userns-v3-0-97c7ed4e43e1@toxicpanda.com
Changes in v3:
- Link to v2: https://patch.msgid.link/20261014-b4-setfcap-userns-v2-0-fac4d19281d7@toxicpanda.com
Changes in v2:
- Link to v1: https://patch.msgid.link/20261006-b4-setfcap-userns-v1-0-f47e7ed66072@toxicpanda.com
---
Josef Bacik (6):
userns: record how far up CAP_SETFCAP reaches
userns: don't let setns() lend CAP_SETFCAP
userns: check the opener and the writer before mapping uid 0
capabilities: limit fscaps to where CAP_SETFCAP reaches
capabilities: don't let ptrace borrow CAP_SETFCAP
selftests/namespaces: test the CAP_SETFCAP rule for user namespaces
include/linux/capability.h | 6 +
include/linux/user_namespace.h | 7 +-
kernel/user_namespace.c | 95 +-
security/commoncap.c | 184 ++-
tools/testing/selftests/namespaces/Makefile | 3 +-
tools/testing/selftests/namespaces/config | 3 +
tools/testing/selftests/namespaces/setfcap_test.c | 1369 +++++++++++++++++++++
7 files changed, 1633 insertions(+), 34 deletions(-)
---
base-commit: 2c3ba1f1cb416b81d617b92a24a4bcaa48d574e3
change-id: 20261006-b4-setfcap-userns-d63185a31ce3
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v7 1/6] userns: record how far up CAP_SETFCAP reaches
2026-10-26 21:00 [PATCH v7 0/6] capabilities: close the ways around the CAP_SETFCAP rule for uid 0 Josef Bacik
@ 2026-10-26 21:00 ` Josef Bacik
2026-10-26 21:00 ` [PATCH v7 2/6] userns: don't let setns() lend CAP_SETFCAP Josef Bacik
` (4 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Josef Bacik @ 2026-10-26 21:00 UTC (permalink / raw)
To: Serge Hallyn, Paul Moore, Christian Brauner
Cc: James Morris, David Howells, Jarkko Sakkinen, Andrew G. Morgan,
Serge Hallyn, linux-security-module, linux-kernel, keyrings,
linux-fsdevel, Shuah Khan, linux-kselftest, Bradley Morgan,
Josef Bacik
create_user_ns() records in ns->parent_could_setfcap whether the creator
had CAP_SETFCAP in its effective set, and verify_root_map() trusts that
when a task in the new namespace maps the parent's uid 0. A bool only
describes the parent, though. File capabilities belong to the kuid of a
root user, and a namespace that maps the parent's uid 0 shares that kuid
with the parent and possibly with ancestors further up, so what matters
is how far up the creator's CAP_SETFCAP counted.
Replace the bool with ns->setfcap_level: CAP_SETFCAP held in the
namespace counts there and in its ancestors up to the one at that
->level. cap_setfcap_level() computes it from the creator's cred. A
creator that holds CAP_SETFCAP passes on the setfcap_level of its own
namespace, one that doesn't gets the new namespace's own level, and the
initial namespace has 0.
verify_root_map() keeps checking the same condition: the bool was false
exactly when the level is the namespace's own.
No functional change.
Reviewed-by: Bradley Morgan <brads@mainlining.org>
Assisted-by: LLM
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
include/linux/capability.h | 3 +++
include/linux/user_namespace.h | 7 ++++---
kernel/user_namespace.c | 4 ++--
security/commoncap.c | 27 +++++++++++++++++++++++++++
4 files changed, 36 insertions(+), 5 deletions(-)
diff --git a/include/linux/capability.h b/include/linux/capability.h
index 622137f66f09..026974a5111b 100644
--- a/include/linux/capability.h
+++ b/include/linux/capability.h
@@ -34,6 +34,7 @@ struct cpu_vfs_cap_data {
#define _USER_CAP_HEADER_SIZE (sizeof(struct __user_cap_header_struct))
#define _KERNEL_CAP_T_SIZE (sizeof(kernel_cap_t))
+struct cred;
struct file;
struct inode;
struct dentry;
@@ -222,4 +223,6 @@ int get_vfs_caps_from_disk(const struct mnt_idmap *idmap,
int cap_convert_nscap(const struct mnt_idmap *idmap, struct dentry *dentry,
const void **ivalue, size_t size);
+int cap_setfcap_level(const struct cred *cred, struct user_namespace *ns);
+
#endif /* !_LINUX_CAPABILITY_H */
diff --git a/include/linux/user_namespace.h b/include/linux/user_namespace.h
index 91232053775d..f3753b6b6d29 100644
--- a/include/linux/user_namespace.h
+++ b/include/linux/user_namespace.h
@@ -86,9 +86,10 @@ struct user_namespace {
kgid_t group;
struct ns_common ns;
unsigned long flags;
- /* parent_could_setfcap: true if the creator if this ns had CAP_SETFCAP
- * in its effective capability set at the child ns creation time. */
- bool parent_could_setfcap;
+ /* setfcap_level: CAP_SETFCAP held in this ns counts here and in the
+ * ancestors up to the one at this ->level, see cap_setfcap_level().
+ */
+ int setfcap_level;
#ifdef CONFIG_KEYS
/* List of joinable keyrings in this namespace. Modification access of
diff --git a/kernel/user_namespace.c b/kernel/user_namespace.c
index 50c374ed4687..a5a94367600d 100644
--- a/kernel/user_namespace.c
+++ b/kernel/user_namespace.c
@@ -125,7 +125,7 @@ int create_user_ns(struct cred *new)
if (!ns)
goto fail_dec;
- ns->parent_could_setfcap = cap_raised(new->cap_effective, CAP_SETFCAP);
+ ns->setfcap_level = cap_setfcap_level(new, parent_ns);
ret = ns_common_init(ns);
if (ret)
@@ -921,7 +921,7 @@ static bool verify_root_map(const struct file *file,
* the new namespace. Verify that the parent had CAP_SETFCAP
* when it unshared.
* */
- if (!file_ns->parent_could_setfcap)
+ if (file_ns->setfcap_level >= file_ns->level)
return false;
} else {
/* Process p1 is writing to uid_map of p2, who is in a child
diff --git a/security/commoncap.c b/security/commoncap.c
index d47ab3022343..4f49dba65426 100644
--- a/security/commoncap.c
+++ b/security/commoncap.c
@@ -131,6 +131,33 @@ int cap_capable(const struct cred *cred, struct user_namespace *target_ns,
return ret;
}
+/**
+ * cap_setfcap_level - Determine how far up CAP_SETFCAP of a cred reaches
+ * @cred: The credentials to use
+ * @ns: The user namespace to look from
+ *
+ * File capabilities belong to the kuid of a namespace's root user, and the
+ * same kuid can be the root user of ancestors of that namespace. Every task
+ * gets CAP_SETFCAP when it enters a user namespace, so having it there says
+ * nothing about those ancestors. user_ns->setfcap_level does: it is taken
+ * from the creator when the namespace is created.
+ *
+ * Return: the ->level of the topmost namespace, from @ns upwards, for which
+ * CAP_SETFCAP of @cred counts; @ns->level + 1 if it doesn't even count over
+ * @ns, which includes an @ns that isn't @cred's namespace or a descendant.
+ */
+int cap_setfcap_level(const struct cred *cred, struct user_namespace *ns)
+{
+ if (cap_capable(cred, ns, CAP_SETFCAP, CAP_OPT_NOAUDIT) != 0)
+ return ns->level + 1;
+
+ if (cap_raised(cred->cap_effective, CAP_SETFCAP))
+ return cred->user_ns->setfcap_level;
+
+ /* All we have is that we own a child of our namespace. */
+ return cred->user_ns->level + 1;
+}
+
/**
* cap_settime - Determine whether the current process may set the system clock
* @ts: The time to set
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v7 2/6] userns: don't let setns() lend CAP_SETFCAP
2026-10-26 21:00 [PATCH v7 0/6] capabilities: close the ways around the CAP_SETFCAP rule for uid 0 Josef Bacik
2026-10-26 21:00 ` [PATCH v7 1/6] userns: record how far up CAP_SETFCAP reaches Josef Bacik
@ 2026-10-26 21:00 ` Josef Bacik
2026-10-26 21:00 ` [PATCH v7 3/6] userns: check the opener and the writer before mapping uid 0 Josef Bacik
` (3 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Josef Bacik @ 2026-10-26 21:00 UTC (permalink / raw)
To: Serge Hallyn, Paul Moore, Christian Brauner
Cc: James Morris, David Howells, Jarkko Sakkinen, Andrew G. Morgan,
Serge Hallyn, linux-security-module, linux-kernel, keyrings,
linux-fsdevel, Shuah Khan, linux-kselftest, Josef Bacik
Mapping uid 0 of the parent into a user namespace requires CAP_SETFCAP,
because a task in such a namespace can write file capabilities that are
honoured in the parent. A task that is already in the namespace has a
full capability set there and can't be asked any more, so
verify_root_map() goes by what the creator had, now ns->setfcap_level.
But setns() lets other tasks in, and they get the full set as well:
task A: uid 0, full caps task B: uid 0, no CAP_SETFCAP
unshare(CLONE_NEWUSER)
setns(A's user ns)
allowed, euid == ns->owner
write "0 0 1" to /proc/self/uid_map
ns->setfcap_level is A's -> allowed
or, if A's namespace maps uid 0 already, B sets security.capability on a
file. Either way B ends up with file capabilities that are honoured in
the initial namespace, which it could not have written there, nor in a
namespace it created itself.
Refuse setns() into a user namespace whose tasks can write file
capabilities for the root user of an ancestor that the joiner's
CAP_SETFCAP doesn't reach. That is the case when the namespace maps
such a root user, or has no uid map yet and its parent maps one.
cred_may_setfcap_userns() checks it, and cred_setfcap_reaches() is the
comparison it and the later checks use. Then every task in such a
namespace is entitled to what its setfcap_level says, and the checks on
the way to file capabilities can go by the namespace.
An idmapped mount can store a root user that the namespace doesn't map,
for example a "0 100000" container whose rootfs maps 100000 to host uid
0. That is a deliberate setup and up to whoever creates the mount, so
it is not considered here.
The change in behaviour is that a task without CAP_SETFCAP gets -EPERM
when it tries to setns() into a user namespace that maps the root user
of an ancestor, and was created by a task whose CAP_SETFCAP reached that
far. Containers that don't map host uid 0, tasks with CAP_SETFCAP, and
unprivileged users joining their own namespaces are not affected.
Link: https://lore.kernel.org/all/20261021-bioenergie-allheilmittel-undurchdringlichen-35666eb62cff@brauner/
Fixes: db2e718a4798 ("capabilities: require CAP_SETFCAP to map uid 0")
Assisted-by: LLM
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
include/linux/capability.h | 3 +++
kernel/user_namespace.c | 6 +++++
security/commoncap.c | 58 ++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 67 insertions(+)
diff --git a/include/linux/capability.h b/include/linux/capability.h
index 026974a5111b..e4199b03668b 100644
--- a/include/linux/capability.h
+++ b/include/linux/capability.h
@@ -224,5 +224,8 @@ int cap_convert_nscap(const struct mnt_idmap *idmap, struct dentry *dentry,
const void **ivalue, size_t size);
int cap_setfcap_level(const struct cred *cred, struct user_namespace *ns);
+bool cred_setfcap_reaches(const struct cred *cred, struct user_namespace *ns,
+ int level);
+bool cred_may_setfcap_userns(const struct cred *cred, struct user_namespace *ns);
#endif /* !_LINUX_CAPABILITY_H */
diff --git a/kernel/user_namespace.c b/kernel/user_namespace.c
index a5a94367600d..7bd3692c2f3e 100644
--- a/kernel/user_namespace.c
+++ b/kernel/user_namespace.c
@@ -1390,6 +1390,12 @@ static int userns_install(struct nsset *nsset, struct ns_common *ns)
if (!ns_capable(user_ns, CAP_SYS_ADMIN))
return -EPERM;
+ /* Tasks in the namespace may have CAP_SETFCAP that counts outside of
+ * it, so only let in tasks whose own counts as far.
+ */
+ if (!cred_may_setfcap_userns(current_cred(), user_ns))
+ return -EPERM;
+
cred = nsset_cred(nsset);
if (!cred)
return -EINVAL;
diff --git a/security/commoncap.c b/security/commoncap.c
index 4f49dba65426..8a9d998b1897 100644
--- a/security/commoncap.c
+++ b/security/commoncap.c
@@ -158,6 +158,64 @@ int cap_setfcap_level(const struct cred *cred, struct user_namespace *ns)
return cred->user_ns->level + 1;
}
+/**
+ * cred_setfcap_reaches - Check how far up CAP_SETFCAP of a cred reaches
+ * @cred: The credentials to use
+ * @ns: The user namespace to look from
+ * @level: The ->level of the ancestor of @ns it has to count in
+ *
+ * Return: true if CAP_SETFCAP of @cred counts in @ns and in its ancestors
+ * up to the one at @level.
+ */
+bool cred_setfcap_reaches(const struct cred *cred, struct user_namespace *ns,
+ int level)
+{
+ return cap_setfcap_level(cred, ns) <= level;
+}
+
+/*
+ * The topmost ancestor of @ns whose root user tasks in @ns can write file
+ * capabilities for: within @ns->setfcap_level, and mapped into @ns, or into
+ * its parent while @ns has no uid_map yet. NULL if there is none. An
+ * idmapped mount can store other root users, but that is up to whoever sets
+ * up the mount.
+ */
+static struct user_namespace *setfcap_top(struct user_namespace *ns)
+{
+ struct user_namespace *seen, *p, *top = NULL;
+
+ if (ns->setfcap_level >= ns->level)
+ return NULL;
+
+ seen = READ_ONCE(ns->uid_map.nr_extents) ? ns : ns->parent;
+ for (p = ns->parent; p; p = p->parent) {
+ if (p->level < ns->setfcap_level)
+ break;
+ if (kuid_has_mapping(seen, make_kuid(p, 0)))
+ top = p;
+ }
+
+ return top;
+}
+
+/**
+ * cred_may_setfcap_userns - Check whether a cred covers what a userns can do
+ * @cred: The credentials to use
+ * @ns: The user namespace
+ *
+ * Tasks in @ns can have CAP_SETFCAP that counts in ancestors of @ns, see
+ * cap_setfcap_level(). Check that CAP_SETFCAP of @cred reaches as far up as
+ * tasks in @ns can make use of theirs.
+ *
+ * Return: true if it does.
+ */
+bool cred_may_setfcap_userns(const struct cred *cred, struct user_namespace *ns)
+{
+ struct user_namespace *top = setfcap_top(ns);
+
+ return !top || cred_setfcap_reaches(cred, ns->parent, top->level);
+}
+
/**
* cap_settime - Determine whether the current process may set the system clock
* @ts: The time to set
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v7 3/6] userns: check the opener and the writer before mapping uid 0
2026-10-26 21:00 [PATCH v7 0/6] capabilities: close the ways around the CAP_SETFCAP rule for uid 0 Josef Bacik
2026-10-26 21:00 ` [PATCH v7 1/6] userns: record how far up CAP_SETFCAP reaches Josef Bacik
2026-10-26 21:00 ` [PATCH v7 2/6] userns: don't let setns() lend CAP_SETFCAP Josef Bacik
@ 2026-10-26 21:00 ` Josef Bacik
2026-10-26 21:00 ` [PATCH v7 4/6] capabilities: limit fscaps to where CAP_SETFCAP reaches Josef Bacik
` (2 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Josef Bacik @ 2026-10-26 21:00 UTC (permalink / raw)
To: Serge Hallyn, Paul Moore, Christian Brauner
Cc: James Morris, David Howells, Jarkko Sakkinen, Andrew G. Morgan,
Serge Hallyn, linux-security-module, linux-kernel, keyrings,
linux-fsdevel, Shuah Khan, linux-kselftest, Josef Bacik
verify_root_map() looks at the cred that opened the map file: it asks
whether it has CAP_SETFCAP over the parent, or, for an opener inside the
namespace, whether the creator had. Two things are missing.
The writer isn't checked. For every other privileged mapping
new_idmap_permitted() wants the capability from the opener and from the
task that calls write(), but a map that consists of the single line
"0 0 1" takes the unprivileged branch, where only the opener's euid
counts. So an open uid_map file is a token for mapping uid 0:
task A: uid 0, full caps task B: uid 0, no CAP_SETFCAP
fd = open("/proc/<pid>/uid_map")
B gets fd by inheritance
or SCM_RIGHTS
write(fd, "0 0 1")
opener had CAP_SETFCAP -> allowed
And the check only happens for a map with an extent that starts at the
parent's uid 0, and only asks for CAP_SETFCAP over the parent. But the
root user of an ancestor can be mapped into the parent at any uid, and
the parent's own root user can be the root user of ancestors further up.
A task without CAP_SETFCAP that unshared a namespace, which a privileged
task then mapped "1000 0 1" for, can unshare again and map "0 1000 1":
host uid 0 is uid 0 of the new namespace, and nothing was checked.
Find every ancestor whose root user the new map contains, at whatever uid
it has in the parent, and require that the opener and the writer both
reach the topmost of them. A task outside the namespace that is being
mapped needs CAP_SETFCAP over the parent, reaching that far by
cap_setfcap_level(). A task inside it needs CAP_SETFCAP there, and
ns->setfcap_level has to reach that far: a task in the namespace that
gave up CAP_SETFCAP would otherwise map uid 0 and get it back from
execve().
This only affects maps that contain the root user of the parent or of
one of its ancestors. A write by a task without CAP_SETFCAP to a file
opened by one with it, and a map by a task whose CAP_SETFCAP stops short
of the root user it passes on, now fail with -EPERM.
Fixes: db2e718a4798 ("capabilities: require CAP_SETFCAP to map uid 0")
Assisted-by: LLM
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
kernel/user_namespace.c | 87 ++++++++++++++++++++++++++++++++++++-------------
1 file changed, 64 insertions(+), 23 deletions(-)
diff --git a/kernel/user_namespace.c b/kernel/user_namespace.c
index 7bd3692c2f3e..db078dd6406a 100644
--- a/kernel/user_namespace.c
+++ b/kernel/user_namespace.c
@@ -881,15 +881,55 @@ VISIBLE_IF_KUNIT int uid_gid_map_sort(struct uid_gid_map *map)
}
EXPORT_SYMBOL_IF_KUNIT(uid_gid_map_sort);
+static bool map_has_id(struct uid_gid_map *map, u32 id)
+{
+ struct uid_gid_extent *extent;
+ int idx;
+
+ for (idx = 0; idx < map->nr_extents; idx++) {
+ if (map->nr_extents <= UID_GID_MAP_MAX_BASE_EXTENTS)
+ extent = &map->extent[idx];
+ else
+ extent = &map->forward[idx];
+ if (id >= extent->lower_first &&
+ id - extent->lower_first < extent->count)
+ return true;
+ }
+
+ return false;
+}
+
+/*
+ * The lowest ->level among @parent and its ancestors whose root user @new_map
+ * maps, whatever uid that root user has in @parent; INT_MAX if there is none.
+ */
+static int map_root_level(struct user_namespace *parent,
+ struct uid_gid_map *new_map)
+{
+ struct user_namespace *p;
+ int level = INT_MAX;
+ uid_t id;
+
+ for (p = parent; p; p = p->parent) {
+ id = from_kuid(parent, make_kuid(p, 0));
+ if (id != (uid_t)-1 && map_has_id(new_map, id))
+ level = p->level;
+ }
+
+ return level;
+}
+
/**
* verify_root_map() - check the uid 0 mapping
* @file: idmapping file
* @map_ns: user namespace of the target process
* @new_map: requested idmap
*
- * If a process requests mapping parent uid 0 into the new ns, verify that the
- * process writing the map had the CAP_SETFCAP capability as the target process
- * will be able to write fscaps that are valid in ancestor user namespaces.
+ * If a process requests mapping the root user of the parent ns, or of one of
+ * its ancestors, into the new ns, verify that the process that opened the map
+ * file and the process writing the map had the CAP_SETFCAP capability as the
+ * target process will be able to write fscaps that are valid in ancestor user
+ * namespaces.
*
* Return: true if the mapping is allowed, false if not.
*/
@@ -897,42 +937,43 @@ static bool verify_root_map(const struct file *file,
struct user_namespace *map_ns,
struct uid_gid_map *new_map)
{
- int idx;
const struct user_namespace *file_ns = file->f_cred->user_ns;
- struct uid_gid_extent *extent0 = NULL;
-
- for (idx = 0; idx < new_map->nr_extents; idx++) {
- if (new_map->nr_extents <= UID_GID_MAP_MAX_BASE_EXTENTS)
- extent0 = &new_map->extent[idx];
- else
- extent0 = &new_map->forward[idx];
- if (extent0->lower_first == 0)
- break;
-
- extent0 = NULL;
- }
+ int level;
- if (!extent0)
+ level = map_root_level(map_ns->parent, new_map);
+ if (level == INT_MAX)
return true;
if (map_ns == file_ns) {
/* The process unshared its ns and is writing to its own
* /proc/self/uid_map. User already has full capabilites in
- * the new namespace. Verify that the parent had CAP_SETFCAP
- * when it unshared.
+ * the new namespace. Verify that it still has CAP_SETFCAP
+ * there, and that the parent had it when it unshared, as far up
+ * as those root users go.
* */
- if (file_ns->setfcap_level >= file_ns->level)
+ if (!file_ns_capable(file, map_ns, CAP_SETFCAP) ||
+ file_ns->setfcap_level > level)
return false;
} else {
/* Process p1 is writing to uid_map of p2, who is in a child
* user namespace to p1's. Verify that the opener of the map
* file has CAP_SETFCAP against the parent of the new map
- * namespace */
- if (!file_ns_capable(file, map_ns->parent, CAP_SETFCAP))
+ * namespace, and as far up as those root users go.
+ */
+ if (!file_ns_capable(file, map_ns->parent, CAP_SETFCAP) ||
+ !cred_setfcap_reaches(file->f_cred, map_ns->parent, level))
return false;
}
- return true;
+ /* The file may have been handed to someone else since it was opened,
+ * so the same goes for the process that is doing the write.
+ */
+ if (map_ns == current_user_ns())
+ return ns_capable(map_ns, CAP_SETFCAP) &&
+ map_ns->setfcap_level <= level;
+
+ return ns_capable(map_ns->parent, CAP_SETFCAP) &&
+ cred_setfcap_reaches(current_cred(), map_ns->parent, level);
}
/*
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v7 4/6] capabilities: limit fscaps to where CAP_SETFCAP reaches
2026-10-26 21:00 [PATCH v7 0/6] capabilities: close the ways around the CAP_SETFCAP rule for uid 0 Josef Bacik
` (2 preceding siblings ...)
2026-10-26 21:00 ` [PATCH v7 3/6] userns: check the opener and the writer before mapping uid 0 Josef Bacik
@ 2026-10-26 21:00 ` Josef Bacik
2026-10-26 21:00 ` [PATCH v7 5/6] capabilities: don't let ptrace borrow CAP_SETFCAP Josef Bacik
2026-10-26 21:00 ` [PATCH v7 6/6] selftests/namespaces: test the CAP_SETFCAP rule for user namespaces Josef Bacik
5 siblings, 0 replies; 7+ messages in thread
From: Josef Bacik @ 2026-10-26 21:00 UTC (permalink / raw)
To: Serge Hallyn, Paul Moore, Christian Brauner
Cc: James Morris, David Howells, Jarkko Sakkinen, Andrew G. Morgan,
Serge Hallyn, linux-security-module, linux-kernel, keyrings,
linux-fsdevel, Shuah Khan, linux-kselftest, Josef Bacik
Commit db2e718a4798 ("capabilities: require CAP_SETFCAP to map uid 0")
keeps a root process without CAP_SETFCAP from creating a user namespace
that maps uid 0, in which it could attach capabilities to a file that
are then honoured outside. But the map doesn't have to be written by
that process. A privileged task can write it from the parent, and the
file capabilities are not checked against the outside at all:
task A: uid 0, full caps task B: uid 0, no CAP_SETFCAP
unshare(CLONE_NEWUSER)
ns->setfcap_level = 1
write "0 0 1" to B's uid_map, gid_map
A has CAP_SETFCAP -> allowed
setxattr(file, "security.capability")
rootid = kuid 0
uid 1000 in the initial namespace:
execve(file)
file capabilities apply
The map doesn't even have to show uid 0 of the parent as uid 0, since a
v3 xattr can name any mapped uid as the root user.
Check at the place where it matters, in cap_convert_nscap(). On an
idmapped mount the root user named in the xattr has two identities: the
kuid seen through this mount, and the kuid that is stored and seen
through every other mount of the filesystem. If either of them is the
root user of an ancestor of the caller's namespace too, CAP_SETFCAP has
to reach up to there, which ns->setfcap_level tells.
A v2 xattr skips the conversion when the writer has CAP_SETFCAP over the
filesystem's namespace, and belongs to that namespace's root user. It
gets the same check: if that root user is out of reach, the xattr goes
through the conversion instead, which names the root user of the
writer's namespace and refuses it when that one is out of reach too.
Otherwise a task that unshared without CAP_SETFCAP could mount a tmpfs
in its namespace, write a v2 xattr on it, and have it honoured through
an overlay mounted from the initial namespace. If the filesystem's
namespace doesn't map uid 0 at all, the v2 xattr has no root user and
nobody could run the file with it, so it is refused with -EINVAL instead
of stored.
Tasks in the initial namespace, tasks in namespaces created by a task
with CAP_SETFCAP, and file capabilities for a root user that only exists
in namespaces within reach are not affected. That includes everything
an unprivileged user does in its own namespaces. The rest gets -EPERM.
Fixes: 8db6c34f1dbc ("Introduce v3 namespaced file capabilities")
Assisted-by: LLM
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
security/commoncap.c | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 52 insertions(+), 2 deletions(-)
diff --git a/security/commoncap.c b/security/commoncap.c
index 8a9d998b1897..4e0b00b7b1a3 100644
--- a/security/commoncap.c
+++ b/security/commoncap.c
@@ -216,6 +216,29 @@ bool cred_may_setfcap_userns(const struct cred *cred, struct user_namespace *ns)
return !top || cred_setfcap_reaches(cred, ns->parent, top->level);
}
+/**
+ * cap_root_level - Find the topmost namespace in which a kuid is the root user
+ * @kuid: The kuid to look for
+ * @ns: The user namespace to start from
+ *
+ * Return: the lowest ->level among @ns and its ancestors in which @kuid is
+ * uid 0, which is how far up file capabilities with that root user are
+ * honoured; INT_MAX if there is no such namespace.
+ */
+static int cap_root_level(kuid_t kuid, struct user_namespace *ns)
+{
+ int level = INT_MAX;
+
+ for (;; ns = ns->parent) {
+ if (from_kuid(ns, kuid) == 0)
+ level = ns->level;
+ if (ns == &init_user_ns)
+ break;
+ }
+
+ return level;
+}
+
/**
* cap_settime - Determine whether the current process may set the system clock
* @ts: The time to set
@@ -671,19 +694,46 @@ int cap_convert_nscap(const struct mnt_idmap *idmap, struct dentry *dentry,
return -EINVAL;
if (!capable_wrt_inode_uidgid(idmap, inode, CAP_SETFCAP))
return -EPERM;
- if (size == XATTR_CAPS_SZ_2 && (idmap == &nop_mnt_idmap))
- if (ns_capable(inode->i_sb->s_user_ns, CAP_SETFCAP))
+ /*
+ * A v2 xattr belongs to the root user of the filesystem's namespace,
+ * which may be the root user of ancestors of our namespace as well.
+ * There may be none, then nobody could ever run the file with these
+ * capabilities. Only write it unconverted if that root user is
+ * within reach, the checks below handle everything else.
+ */
+ if (size == XATTR_CAPS_SZ_2 && idmap == &nop_mnt_idmap &&
+ ns_capable(fs_ns, CAP_SETFCAP)) {
+ kuid_t fsroot = make_kuid(fs_ns, 0);
+
+ if (!uid_valid(fsroot))
+ return -EINVAL;
+ if (cap_root_level(fsroot, task_ns) >= task_ns->setfcap_level)
/* user is privileged, just write the v2 */
return size;
+ }
vfsrootid = rootid_from_xattr(*ivalue, size, task_ns);
if (!vfsuid_valid(vfsrootid))
return -EINVAL;
+ /*
+ * The root user may be the root user of ancestors of our namespace as
+ * well. CAP_SETFCAP that we got for entering it doesn't cover those.
+ * On an idmapped mount the root user is vfsrootid as seen through
+ * this mount and rootid as seen through every other mount of the
+ * filesystem, so both have to stay within reach.
+ */
+ if (cap_root_level(vfsuid_into_kuid(vfsrootid), task_ns) <
+ task_ns->setfcap_level)
+ return -EPERM;
+
rootid = from_vfsuid(idmap, fs_ns, vfsrootid);
if (!uid_valid(rootid))
return -EINVAL;
+ if (cap_root_level(rootid, task_ns) < task_ns->setfcap_level)
+ return -EPERM;
+
nsrootid = from_kuid(fs_ns, rootid);
if (nsrootid == -1)
return -EINVAL;
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v7 5/6] capabilities: don't let ptrace borrow CAP_SETFCAP
2026-10-26 21:00 [PATCH v7 0/6] capabilities: close the ways around the CAP_SETFCAP rule for uid 0 Josef Bacik
` (3 preceding siblings ...)
2026-10-26 21:00 ` [PATCH v7 4/6] capabilities: limit fscaps to where CAP_SETFCAP reaches Josef Bacik
@ 2026-10-26 21:00 ` Josef Bacik
2026-10-26 21:00 ` [PATCH v7 6/6] selftests/namespaces: test the CAP_SETFCAP rule for user namespaces Josef Bacik
5 siblings, 0 replies; 7+ messages in thread
From: Josef Bacik @ 2026-10-26 21:00 UTC (permalink / raw)
To: Serge Hallyn, Paul Moore, Christian Brauner
Cc: James Morris, David Howells, Jarkko Sakkinen, Andrew G. Morgan,
Serge Hallyn, linux-security-module, linux-kernel, keyrings,
linux-fsdevel, Shuah Khan, linux-kselftest, Josef Bacik
Tasks in a user namespace created by a task with CAP_SETFCAP may map uid
0 of the parent and write file capabilities that are honoured there.
That is the one privilege they keep outside of their namespace. setns()
no longer lets anybody in who doesn't have it, but the ptrace checks
don't know about it: for a target in another user namespace
cap_ptrace_access_check() is satisfied with CAP_SYS_PTRACE over the
target's namespace, which the owner rule hands to any task with the
right euid.
task A: uid 0, full caps task B: uid 0, no CAP_SETFCAP
unshare(CLONE_NEWUSER)
ptrace(PTRACE_ATTACH, A)
uids match, euid == ns->owner
make A write "0 0 1" to its uid_map,
or set file capabilities
A is entitled -> allowed
The same goes for /proc/<pid>/mem, process_vm_writev() and pidfd_getfd().
Require for PTRACE_MODE_ATTACH and PTRACE_TRACEME what setns() requires
to get into the target's namespace: CAP_SETFCAP of the tracer has to
reach the root users of ancestors that tasks in the namespace can write
file capabilities for, or the tracer has CAP_SYS_PTRACE over the topmost
of those ancestors, which gives control of tasks there that have
CAP_SETFCAP anyway. As in setns(), root users stored through an
idmapped mount are not considered. A tracer in the target's namespace
got in with as much and always passes.
Targets in the initial namespace, in namespaces created without
CAP_SETFCAP (what unprivileged users create) and in namespaces that
don't map the root user of an ancestor (the usual container) are not
affected. What remains is a tracer that gave up CAP_SETFCAP and
CAP_SYS_PTRACE and a target in a namespace that maps the root user of
an ancestor and was created by a task that didn't give it up, which the
tracer can't setns() into either. PTRACE_MODE_READ is unchanged.
Fixes: db2e718a4798 ("capabilities: require CAP_SETFCAP to map uid 0")
Assisted-by: LLM
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
security/commoncap.c | 45 +++++++++++++++++++++++++++++++++++++++++----
1 file changed, 41 insertions(+), 4 deletions(-)
diff --git a/security/commoncap.c b/security/commoncap.c
index 4e0b00b7b1a3..91ed4a5f4b25 100644
--- a/security/commoncap.c
+++ b/security/commoncap.c
@@ -254,14 +254,41 @@ int cap_settime(const struct timespec64 *ts, const struct timezone *tz)
return 0;
}
+/*
+ * Tasks in a user namespace have CAP_SETFCAP that can count in ancestors of
+ * the namespace, see cap_setfcap_level(). That is a privilege outside of the
+ * namespace, so capabilities over the namespace are not enough to take
+ * control of such a task.
+ */
+static bool cap_covers_setfcap(const struct cred *cred,
+ const struct cred *child_cred)
+{
+ struct user_namespace *ns = child_cred->user_ns, *top;
+
+ /* setns() only lets in tasks whose CAP_SETFCAP reaches as far. */
+ if (cred->user_ns == ns)
+ return true;
+
+ top = setfcap_top(ns);
+ if (!top)
+ return true;
+
+ /* CAP_SYS_PTRACE up there gives control of tasks with CAP_SETFCAP. */
+ return cred_setfcap_reaches(cred, ns->parent, top->level) ||
+ !cap_capable(cred, top, CAP_SYS_PTRACE, CAP_OPT_NOAUDIT);
+}
+
/**
* cap_ptrace_access_check - Determine whether the current process may access
* another
* @child: The process to be accessed
* @mode: The mode of attachment.
*
- * If we are in the same or an ancestor user_ns and have all the target
- * task's capabilities, then ptrace access is allowed.
+ * For PTRACE_MODE_ATTACH, if the target task may make use of CAP_SETFCAP in
+ * an ancestor of its user_ns that our CAP_SETFCAP or CAP_SYS_PTRACE doesn't
+ * reach, then ptrace access is denied.
+ * Otherwise, if we are in the same or an ancestor user_ns and have all the
+ * target task's capabilities, then ptrace access is allowed.
* If we have the ptrace capability to the target user_ns, then ptrace
* access is allowed.
* Else denied.
@@ -282,11 +309,15 @@ int cap_ptrace_access_check(struct task_struct *child, unsigned int mode)
caller_caps = &cred->cap_effective;
else
caller_caps = &cred->cap_permitted;
+ if ((mode & PTRACE_MODE_ATTACH) &&
+ !cap_covers_setfcap(cred, child_cred))
+ goto deny;
if (cred->user_ns == child_cred->user_ns &&
cap_issubset(child_cred->cap_permitted, *caller_caps))
goto out;
if (ns_capable(child_cred->user_ns, CAP_SYS_PTRACE))
goto out;
+deny:
ret = -EPERM;
out:
rcu_read_unlock();
@@ -297,8 +328,11 @@ int cap_ptrace_access_check(struct task_struct *child, unsigned int mode)
* cap_ptrace_traceme - Determine whether another process may trace the current
* @parent: The task proposed to be the tracer
*
- * If parent is in the same or an ancestor user_ns and has all current's
- * capabilities, then ptrace access is allowed.
+ * If current may make use of CAP_SETFCAP in an ancestor of its user_ns that
+ * parent's CAP_SETFCAP or CAP_SYS_PTRACE doesn't reach, then ptrace access
+ * is denied.
+ * Otherwise, if parent is in the same or an ancestor user_ns and has all
+ * current's capabilities, then ptrace access is allowed.
* If parent has the ptrace capability to current's user_ns, then ptrace
* access is allowed.
* Else denied.
@@ -314,11 +348,14 @@ int cap_ptrace_traceme(struct task_struct *parent)
rcu_read_lock();
cred = __task_cred(parent);
child_cred = current_cred();
+ if (!cap_covers_setfcap(cred, child_cred))
+ goto deny;
if (cred->user_ns == child_cred->user_ns &&
cap_issubset(child_cred->cap_permitted, cred->cap_permitted))
goto out;
if (has_ns_capability(parent, child_cred->user_ns, CAP_SYS_PTRACE))
goto out;
+deny:
ret = -EPERM;
out:
rcu_read_unlock();
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v7 6/6] selftests/namespaces: test the CAP_SETFCAP rule for user namespaces
2026-10-26 21:00 [PATCH v7 0/6] capabilities: close the ways around the CAP_SETFCAP rule for uid 0 Josef Bacik
` (4 preceding siblings ...)
2026-10-26 21:00 ` [PATCH v7 5/6] capabilities: don't let ptrace borrow CAP_SETFCAP Josef Bacik
@ 2026-10-26 21:00 ` Josef Bacik
5 siblings, 0 replies; 7+ messages in thread
From: Josef Bacik @ 2026-10-26 21:00 UTC (permalink / raw)
To: Serge Hallyn, Paul Moore, Christian Brauner
Cc: James Morris, David Howells, Jarkko Sakkinen, Andrew G. Morgan,
Serge Hallyn, linux-security-module, linux-kernel, keyrings,
linux-fsdevel, Shuah Khan, linux-kselftest, Josef Bacik
Add tests for who may map the root user of an ancestor user namespace,
write file capabilities that are honoured in an ancestor, join a
namespace whose tasks may do so, or attach to such a task.
Each case runs as root in a private mount namespace with a tmpfs on
/tmp. It sets up everything but the step it tests and fails if any of
that goes wrong, so a refusal comes from that step. The ones that must
be refused are the ways around the rule that the preceding patches
close: setns() into a namespace created with CAP_SETFCAP, a uid_map fd
opened by somebody else, pidfd_getfd(), a writer inside the namespace
that gave up CAP_SETFCAP, the root user of an ancestor mapped at a
non-zero uid, v2 and v3 xattrs written after a privileged task mapped
uid 0 for a namespace created without CAP_SETFCAP, a joiner of such a
namespace, and PTRACE_ATTACH. A task that joins a namespace gets
CAP_SETFCAP wherever the namespace's creator had it, so the setns()
cases end at setns(). The ones that must keep working are a task
mapping its own namespace, a privileged task mapping it from the
parent, unprivileged users nesting namespaces, joiners that have
CAP_SETFCAP, file capabilities in all of those, containers that don't
map host uid 0, and idmapped mounts of a container's rootfs. Where
file capabilities are written the test also checks whether uid 1000
gets CAP_SYS_ADMIN from them, by running a copy of itself.
The idmapped mount cases skip when tmpfs can't be idmapped, and the
session keyring case when the kernel has no keyrings.
Assisted-by: LLM
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
tools/testing/selftests/namespaces/Makefile | 3 +-
tools/testing/selftests/namespaces/config | 3 +
tools/testing/selftests/namespaces/setfcap_test.c | 1369 +++++++++++++++++++++
3 files changed, 1374 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/namespaces/Makefile b/tools/testing/selftests/namespaces/Makefile
index 9b22d12708e1..0455b6544869 100644
--- a/tools/testing/selftests/namespaces/Makefile
+++ b/tools/testing/selftests/namespaces/Makefile
@@ -14,7 +14,8 @@ TEST_GEN_PROGS := nsid_test \
stress_test \
listns_pagination_bug \
regression_pidfd_setns_test \
- uid_map_range_test
+ uid_map_range_test \
+ setfcap_test
include ../lib.mk
diff --git a/tools/testing/selftests/namespaces/config b/tools/testing/selftests/namespaces/config
index d09836260262..dfbabc35cd8a 100644
--- a/tools/testing/selftests/namespaces/config
+++ b/tools/testing/selftests/namespaces/config
@@ -5,3 +5,6 @@ CONFIG_USER_NS=y
CONFIG_PID_NS=y
CONFIG_NET_NS=y
CONFIG_CGROUPS=y
+CONFIG_TMPFS=y
+CONFIG_TMPFS_XATTR=y
+CONFIG_KEYS=y
diff --git a/tools/testing/selftests/namespaces/setfcap_test.c b/tools/testing/selftests/namespaces/setfcap_test.c
new file mode 100644
index 000000000000..cfe82cb87562
--- /dev/null
+++ b/tools/testing/selftests/namespaces/setfcap_test.c
@@ -0,0 +1,1369 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Who may map the root user of an ancestor user namespace, write file
+ * capabilities that are honoured in an ancestor, join a namespace whose
+ * tasks may do so, or take control of such a task.
+ *
+ * "R" is a task with uid 0 and all capabilities in the initial namespace,
+ * "X" one with uid 0 that gave up CAP_SETFCAP and CAP_SYS_PTRACE in its
+ * permitted, effective and bounding sets, and "U" is uid 1000.
+ *
+ * Every test runs in a private mount namespace with a tmpfs on /tmp.
+ */
+#define _GNU_SOURCE
+#include <endian.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/ptrace.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <sys/xattr.h>
+#include <linux/capability.h>
+#include <linux/keyctl.h>
+#include <linux/mount.h>
+
+#include "kselftest_harness.h"
+
+#ifndef __NR_pidfd_open
+#define __NR_pidfd_open 434
+#endif
+#ifndef __NR_pidfd_getfd
+#define __NR_pidfd_getfd 438
+#endif
+#ifndef __NR_open_tree
+#define __NR_open_tree 428
+#endif
+#ifndef __NR_move_mount
+#define __NR_move_mount 429
+#endif
+#ifndef __NR_mount_setattr
+#define __NR_mount_setattr 442
+#endif
+
+/* Exit codes of a copy of this program run with SETFCAP_TEST_CAPS set. */
+#define CAPS_HAS_SYS_ADMIN 42
+#define CAPS_NO_SYS_ADMIN 43
+
+/* A step that wasn't the one under test went wrong. */
+#define SETUP_ERROR (-1000)
+/* The kernel has no keyrings. */
+#define NO_KEYS (-1001)
+
+static int capget_v3(struct __user_cap_data_struct d[2])
+{
+ struct __user_cap_header_struct h = { _LINUX_CAPABILITY_VERSION_3, 0 };
+
+ return syscall(SYS_capget, &h, d);
+}
+
+__attribute__((constructor)) static void report_caps(void)
+{
+ struct __user_cap_data_struct d[2];
+
+ if (!getenv("SETFCAP_TEST_CAPS"))
+ return;
+ if (capget_v3(d))
+ _exit(1);
+ _exit(d[0].effective & (1u << CAP_SYS_ADMIN) ?
+ CAPS_HAS_SYS_ADMIN : CAPS_NO_SYS_ADMIN);
+}
+
+/* X: uid 0 without CAP_SETFCAP and CAP_SYS_PTRACE */
+static void restrict_root(void)
+{
+ struct __user_cap_header_struct h = { _LINUX_CAPABILITY_VERSION_3, 0 };
+ struct __user_cap_data_struct d[2];
+ unsigned int drop = (1u << CAP_SETFCAP) | (1u << CAP_SYS_PTRACE);
+
+ if (prctl(PR_CAPBSET_DROP, CAP_SETFCAP) ||
+ prctl(PR_CAPBSET_DROP, CAP_SYS_PTRACE) || capget_v3(d))
+ _exit(99);
+ d[0].effective &= ~drop;
+ d[0].permitted &= ~drop;
+ if (syscall(SYS_capset, &h, d))
+ _exit(99);
+}
+
+/* Give up CAP_SETFCAP only in the permitted and effective set. */
+static void drop_setfcap_keep_bset(void)
+{
+ struct __user_cap_header_struct h = { _LINUX_CAPABILITY_VERSION_3, 0 };
+ struct __user_cap_data_struct d[2];
+
+ if (capget_v3(d))
+ _exit(99);
+ d[0].effective &= ~(1u << CAP_SETFCAP);
+ d[0].permitted &= ~(1u << CAP_SETFCAP);
+ if (syscall(SYS_capset, &h, d))
+ _exit(99);
+}
+
+static void become_user(void)
+{
+ if (setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000))
+ _exit(99);
+ prctl(PR_SET_DUMPABLE, 1);
+}
+
+static long wr(int fd, const char *s)
+{
+ long r = write(fd, s, strlen(s));
+
+ return r < 0 ? -errno : 0;
+}
+
+static long write_file(int pid, const char *name, const char *s)
+{
+ char path[64];
+ long r;
+ int fd;
+
+ if (pid)
+ snprintf(path, sizeof(path), "/proc/%d/%s", pid, name);
+ else
+ snprintf(path, sizeof(path), "/proc/self/%s", name);
+ fd = open(path, O_WRONLY);
+ if (fd < 0)
+ return -errno;
+ r = wr(fd, s);
+ close(fd);
+ return r;
+}
+
+static long write_map(int pid, const char *map)
+{
+ return write_file(pid, "uid_map", map);
+}
+
+/* both maps, as the setup for a test rather than its subject */
+static long map_both(int pid, const char *map)
+{
+ if (write_map(pid, map) || write_file(pid, "setgroups", "deny") ||
+ write_file(pid, "gid_map", map))
+ return SETUP_ERROR;
+ return 0;
+}
+
+/* what an unprivileged user does after unshare(CLONE_NEWUSER) */
+static long map_user(void)
+{
+ return map_both(0, "0 1000 1\n");
+}
+
+static long join(int pid)
+{
+ char path[64];
+ int fd, r;
+
+ snprintf(path, sizeof(path), "/proc/%d/ns/user", pid);
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ return SETUP_ERROR;
+ r = setns(fd, CLONE_NEWUSER);
+ close(fd);
+ return r ? -errno : 0;
+}
+
+/* security.capability with CAP_SYS_ADMIN in permitted and effective; v2 if rootid < 0 */
+static long set_fcap(const char *path, int rootid)
+{
+ struct vfs_ns_cap_data d;
+ int v3 = rootid >= 0;
+
+ memset(&d, 0, sizeof(d));
+ d.magic_etc = htole32((v3 ? VFS_CAP_REVISION_3 : VFS_CAP_REVISION_2) |
+ VFS_CAP_FLAGS_EFFECTIVE);
+ d.data[0].permitted = htole32(1u << CAP_SYS_ADMIN);
+ d.rootid = htole32(rootid);
+ if (setxattr(path, "security.capability", &d,
+ v3 ? XATTR_CAPS_SZ_3 : XATTR_CAPS_SZ_2, 0))
+ return -errno;
+ return 0;
+}
+
+/* a copy of this program, owned by uid */
+static int new_file(const char *path, int uid)
+{
+ char buf[65536];
+ int in, out;
+ ssize_t n;
+
+ unlink(path);
+ in = open("/proc/self/exe", O_RDONLY);
+ out = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0755);
+ if (in < 0 || out < 0)
+ return -1;
+ while ((n = read(in, buf, sizeof(buf))) > 0)
+ if (write(out, buf, n) != n)
+ return -1;
+ if (fchown(out, uid, uid) || fchmod(out, 0755))
+ return -1;
+ close(in);
+ close(out);
+ return 0;
+}
+
+/* Does U get CAP_SYS_ADMIN from the file capabilities of path? */
+static int user_gets_sys_admin(const char *path)
+{
+ int pid, st;
+
+ pid = fork();
+ if (!pid) {
+ become_user();
+ setenv("SETFCAP_TEST_CAPS", "1", 1);
+ execl(path, path, NULL);
+ _exit(1);
+ }
+ waitpid(pid, &st, 0);
+ return WIFEXITED(st) && WEXITSTATUS(st) == CAPS_HAS_SYS_ADMIN;
+}
+
+/* run fn in a child and return what it returns */
+static long in_child(long (*fn)(void *), void *arg)
+{
+ int p[2], pid;
+ long r = SETUP_ERROR;
+
+ if (pipe(p))
+ return SETUP_ERROR;
+ pid = fork();
+ if (!pid) {
+ r = fn(arg);
+ if (write(p[1], &r, sizeof(r)) != sizeof(r))
+ _exit(1);
+ _exit(0);
+ }
+ close(p[1]);
+ if (read(p[0], &r, sizeof(r)) != sizeof(r))
+ r = SETUP_ERROR;
+ close(p[0]);
+ waitpid(pid, NULL, 0);
+ return r;
+}
+
+/* A task that unshares a user namespace and waits. */
+struct holder {
+ int pid, go;
+};
+
+enum holder_kind {
+ HOLDER_ROOT, /* R */
+ HOLDER_RESTRICTED, /* X */
+ HOLDER_USER, /* U, maps "0 1000 1" itself */
+};
+
+static struct holder spawn_holder(enum holder_kind kind, int map_fd_at)
+{
+ struct holder h = { -1, -1 };
+ int ready[2], go[2];
+ char c;
+
+ if (pipe(ready) || pipe(go))
+ return h;
+ h.pid = fork();
+ if (!h.pid) {
+ close(ready[0]);
+ close(go[1]);
+ if (kind == HOLDER_RESTRICTED)
+ restrict_root();
+ if (kind == HOLDER_USER)
+ become_user();
+ if (unshare(CLONE_NEWUSER))
+ _exit(1);
+ if (kind == HOLDER_USER && map_user())
+ _exit(1);
+ if (map_fd_at)
+ dup2(open("/proc/self/uid_map", O_WRONLY), map_fd_at);
+ if (write(ready[1], "r", 1) != 1)
+ _exit(1);
+ if (read(go[0], &c, 1) < 0)
+ _exit(1);
+ _exit(0);
+ }
+ close(ready[1]);
+ close(go[0]);
+ if (read(ready[0], &c, 1) != 1)
+ h.pid = -1;
+ close(ready[0]);
+ h.go = go[1];
+ return h;
+}
+
+/* ... whose maps are written from the parent namespace by the caller */
+static struct holder spawn_mapped(enum holder_kind kind, const char *map)
+{
+ struct holder h = spawn_holder(kind, 0);
+
+ if (h.pid > 0 && map_both(h.pid, map))
+ h.pid = -1;
+ return h;
+}
+
+static void reap_holder(struct holder h)
+{
+ if (h.pid <= 0)
+ return;
+ close(h.go);
+ kill(h.pid, SIGKILL);
+ waitpid(h.pid, NULL, 0);
+}
+
+#define PID(a) (((struct holder *)(a))->pid)
+
+static long attach_only(int pid)
+{
+ if (ptrace(PTRACE_ATTACH, pid, 0, 0))
+ return -errno;
+ waitpid(pid, NULL, 0);
+ ptrace(PTRACE_DETACH, pid, 0, 0);
+ return 0;
+}
+
+/* /tmp idmapped with the user namespace of pid, at /mnt */
+static int idmap_tmp(int pid)
+{
+ struct mount_attr attr = { .attr_set = MOUNT_ATTR_IDMAP };
+ char path[64];
+ int tfd, nsfd;
+
+ mkdir("/mnt", 0755);
+ snprintf(path, sizeof(path), "/proc/%d/ns/user", pid);
+ nsfd = open(path, O_RDONLY);
+ tfd = syscall(__NR_open_tree, AT_FDCWD, "/tmp",
+ OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+ if (nsfd < 0 || tfd < 0)
+ return -1;
+ attr.userns_fd = nsfd;
+ if (syscall(__NR_mount_setattr, tfd, "", AT_EMPTY_PATH, &attr,
+ sizeof(attr)))
+ return -1;
+ if (syscall(__NR_move_mount, tfd, "", AT_FDCWD, "/mnt",
+ MOVE_MOUNT_F_EMPTY_PATH))
+ return -1;
+ close(tfd);
+ close(nsfd);
+ return 0;
+}
+
+/* Each test: private mounts, a tmpfs on /tmp with copies of this program */
+static void prepare(struct __test_metadata *_metadata)
+{
+ ASSERT_EQ(unshare(CLONE_NEWNS), 0);
+ ASSERT_EQ(mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
+ ASSERT_EQ(mount("tmpfs", "/tmp", "tmpfs", 0, "mode=1777"), 0);
+ ASSERT_EQ(new_file("/tmp/x", 0), 0);
+ ASSERT_EQ(new_file("/tmp/u", 1000), 0);
+ mkdir("/mnt", 0755);
+}
+
+FIXTURE(setfcap) {};
+
+FIXTURE_SETUP(setfcap)
+{
+ if (geteuid())
+ SKIP(return, "needs root");
+ prepare(_metadata);
+}
+
+FIXTURE_TEARDOWN(setfcap) {}
+
+/* ---------------------------------------------------------------------- */
+/* Mapping uid 0 of the parent, or the root user of an ancestor at any uid */
+
+static long x_maps_from_parent(void *a)
+{
+ restrict_root();
+ return write_map(PID(a), "0 0 1\n");
+}
+
+TEST_F(setfcap, map_from_parent_without_setfcap)
+{
+ struct holder h;
+
+ h = spawn_holder(HOLDER_ROOT, 0);
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(x_maps_from_parent, &h), -EPERM);
+ reap_holder(h);
+}
+
+static long x_maps_own(void *a)
+{
+ restrict_root();
+ if (unshare(CLONE_NEWUSER))
+ return SETUP_ERROR;
+ return write_map(0, "0 0 1\n");
+}
+
+TEST_F(setfcap, map_own_without_setfcap)
+{
+ EXPECT_EQ(in_child(x_maps_own, NULL), -EPERM);
+}
+
+static long x_writes_fd(void *a)
+{
+ restrict_root();
+ return wr(*(int *)a, "0 0 1\n");
+}
+
+TEST_F(setfcap, map_through_borrowed_fd)
+{
+ struct holder h;
+ char path[64];
+ int fd;
+
+ h = spawn_holder(HOLDER_ROOT, 0);
+ ASSERT_GT(h.pid, 0);
+ snprintf(path, sizeof(path), "/proc/%d/uid_map", h.pid);
+ fd = open(path, O_WRONLY);
+ ASSERT_GE(fd, 0);
+ EXPECT_EQ(in_child(x_writes_fd, &fd), -EPERM);
+ close(fd);
+ reap_holder(h);
+}
+
+/* the map opened by R inside its namespace, taken with pidfd_getfd() */
+TEST_F(setfcap, map_through_fd_opened_inside)
+{
+ struct holder h;
+ int pfd, fd;
+
+ h = spawn_holder(HOLDER_ROOT, 100);
+ ASSERT_GT(h.pid, 0);
+ pfd = syscall(__NR_pidfd_open, h.pid, 0);
+ ASSERT_GE(pfd, 0);
+ fd = syscall(__NR_pidfd_getfd, pfd, 100, 0);
+ ASSERT_GE(fd, 0);
+ EXPECT_EQ(in_child(x_writes_fd, &fd), -EPERM);
+ close(fd);
+ close(pfd);
+ reap_holder(h);
+}
+
+/* the writer opened the map, then went into an unrelated namespace */
+static long r_opens_then_leaves(void *a)
+{
+ char path[64];
+ int fd;
+
+ snprintf(path, sizeof(path), "/proc/%d/uid_map", PID(a));
+ fd = open(path, O_WRONLY);
+ if (fd < 0)
+ return SETUP_ERROR;
+ if (unshare(CLONE_NEWUSER))
+ return SETUP_ERROR;
+ return wr(fd, "0 0 1\n");
+}
+
+TEST_F(setfcap, map_writer_left_for_other_ns)
+{
+ struct holder h;
+
+ h = spawn_holder(HOLDER_ROOT, 0);
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(r_opens_then_leaves, &h), -EPERM);
+ reap_holder(h);
+}
+
+/*
+ * R maps host uid 0 as uid 1000 of X's namespace. X maps "0 1000 1" one
+ * level down: host uid 0 is uid 0 there without any extent at uid 0.
+ */
+static long x_nonzero_root(void *a)
+{
+ int s1[2], s2[2], res[2], pid;
+ long r = SETUP_ERROR;
+ char c;
+
+ if (pipe(s1) || pipe(s2) || pipe(res))
+ return SETUP_ERROR;
+ pid = fork();
+ if (!pid) {
+ restrict_root();
+ if (unshare(CLONE_NEWUSER) || write(s1[1], "u", 1) != 1 ||
+ read(s2[0], &c, 1) != 1)
+ _exit(1);
+ if (unshare(CLONE_NEWUSER))
+ r = SETUP_ERROR;
+ else
+ r = write_map(0, "0 1000 1\n");
+ if (write(res[1], &r, sizeof(r)) != sizeof(r))
+ _exit(1);
+ _exit(0);
+ }
+ if (read(s1[0], &c, 1) != 1 || map_both(pid, "1000 0 1\n") ||
+ write(s2[1], "g", 1) != 1 || read(res[0], &r, sizeof(r)) != sizeof(r))
+ r = SETUP_ERROR;
+ waitpid(pid, NULL, 0);
+ return r;
+}
+
+TEST_F(setfcap, map_ancestor_root_at_nonzero_uid)
+{
+ EXPECT_EQ(in_child(x_nonzero_root, NULL), -EPERM);
+}
+
+/*
+ * R maps host uid 0 as uid 5 of its namespace and forks X, which gives up
+ * CAP_SETFCAP there and unshares again. "5 5 1" then maps host uid 0 one
+ * level down, as uid 5. X writes it itself, or R does from the parent and X
+ * writes a v3 xattr with rootid 5.
+ */
+static long r_nests_x(void *a)
+{
+ int x_maps = *(int *)a;
+ int s1[2], s2[2], res[2], pid;
+ long r = SETUP_ERROR;
+ char c;
+
+ if (unshare(CLONE_NEWUSER) || map_both(0, "5 0 1\n"))
+ return SETUP_ERROR;
+ if (pipe(s1) || pipe(s2) || pipe(res))
+ return SETUP_ERROR;
+ pid = fork();
+ if (!pid) {
+ restrict_root();
+ if (unshare(CLONE_NEWUSER))
+ r = SETUP_ERROR;
+ else if (x_maps)
+ r = write_map(0, "5 5 1\n");
+ else if (write(s1[1], "u", 1) != 1 || read(s2[0], &c, 1) != 1)
+ r = SETUP_ERROR;
+ else
+ r = set_fcap("/tmp/x", 5);
+ if (write(res[1], &r, sizeof(r)) != sizeof(r))
+ _exit(1);
+ _exit(0);
+ }
+ if (!x_maps && (read(s1[0], &c, 1) != 1 || map_both(pid, "5 5 1\n") ||
+ write(s2[1], "g", 1) != 1))
+ r = SETUP_ERROR;
+ else if (read(res[0], &r, sizeof(r)) != sizeof(r))
+ r = SETUP_ERROR;
+ kill(pid, SIGKILL);
+ waitpid(pid, NULL, 0);
+ return r;
+}
+
+TEST_F(setfcap, map_after_dropping_setfcap_and_nesting)
+{
+ int x_maps = 1;
+
+ EXPECT_EQ(in_child(r_nests_x, &x_maps), -EPERM);
+}
+
+/*
+ * R's namespace has no map yet. R opens its uid_map and hands the fd to a
+ * task of the same namespace that gave up CAP_SETFCAP (but kept it in the
+ * bounding set, so it would get it back from execve() as uid 0).
+ */
+static long dropped_writes_inherited_fd(void *a)
+{
+ int fd, pid, st;
+ long r = SETUP_ERROR;
+ int p[2];
+
+ if (unshare(CLONE_NEWUSER))
+ return SETUP_ERROR;
+ fd = open("/proc/self/uid_map", O_WRONLY);
+ if (fd < 0 || pipe(p))
+ return SETUP_ERROR;
+ pid = fork();
+ if (!pid) {
+ drop_setfcap_keep_bset();
+ r = wr(fd, "0 0 1\n");
+ if (write(p[1], &r, sizeof(r)) != sizeof(r))
+ _exit(1);
+ _exit(0);
+ }
+ if (read(p[0], &r, sizeof(r)) != sizeof(r))
+ r = SETUP_ERROR;
+ waitpid(pid, &st, 0);
+ return r;
+}
+
+TEST_F(setfcap, map_in_ns_writer_without_setfcap)
+{
+ EXPECT_EQ(in_child(dropped_writes_inherited_fd, NULL), -EPERM);
+}
+
+/* ... and the same task opening its own map */
+static long dropped_opens_and_writes(void *a)
+{
+ if (unshare(CLONE_NEWUSER))
+ return SETUP_ERROR;
+ drop_setfcap_keep_bset();
+ return write_map(0, "0 0 1\n");
+}
+
+TEST_F(setfcap, map_in_ns_opener_without_setfcap)
+{
+ EXPECT_EQ(in_child(dropped_opens_and_writes, NULL), -EPERM);
+}
+
+/* What has to keep working. */
+
+static long r_maps_own(void *a)
+{
+ if (unshare(CLONE_NEWUSER))
+ return SETUP_ERROR;
+ return write_map(0, "0 0 1\n");
+}
+
+TEST_F(setfcap, map_own)
+{
+ EXPECT_EQ(in_child(r_maps_own, NULL), 0);
+}
+
+static long r_maps_from_parent(void *a)
+{
+ return write_map(PID(a), "0 0 1000\n");
+}
+
+TEST_F(setfcap, map_from_parent)
+{
+ struct holder h;
+
+ h = spawn_holder(HOLDER_ROOT, 0);
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(r_maps_from_parent, &h), 0);
+ reap_holder(h);
+}
+
+static long child_maps(void *a)
+{
+ return write_map(0, "0 0 1\n");
+}
+
+static long r_child_maps(void *a)
+{
+ if (unshare(CLONE_NEWUSER))
+ return SETUP_ERROR;
+ return in_child(child_maps, NULL);
+}
+
+TEST_F(setfcap, map_by_child_of_creator)
+{
+ EXPECT_EQ(in_child(r_child_maps, NULL), 0);
+}
+
+static long r_joins_and_maps(void *a)
+{
+ long r = join(PID(a));
+
+ if (r)
+ return r;
+ return write_map(0, "0 0 1\n");
+}
+
+TEST_F(setfcap, map_after_setns_with_setfcap)
+{
+ struct holder h;
+
+ h = spawn_holder(HOLDER_ROOT, 0);
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(r_joins_and_maps, &h), 0);
+ reap_holder(h);
+}
+
+static long r_joins_writes_fd(void *a)
+{
+ void **v = a;
+ long r = join(PID(v[0]));
+
+ if (r)
+ return r;
+ return wr(*(int *)v[1], "0 0 1\n");
+}
+
+TEST_F(setfcap, map_opened_in_parent_written_inside)
+{
+ struct holder h;
+ char path[64];
+ void *v[2];
+ int fd;
+
+ h = spawn_holder(HOLDER_ROOT, 0);
+ ASSERT_GT(h.pid, 0);
+ snprintf(path, sizeof(path), "/proc/%d/uid_map", h.pid);
+ fd = open(path, O_WRONLY);
+ ASSERT_GE(fd, 0);
+ v[0] = &h;
+ v[1] = &fd;
+ EXPECT_EQ(in_child(r_joins_writes_fd, v), 0);
+ close(fd);
+ reap_holder(h);
+}
+
+static long u_nested_maps_own(void *a)
+{
+ become_user();
+ if (unshare(CLONE_NEWUSER) || map_user())
+ return SETUP_ERROR;
+ return in_child(r_maps_own, NULL);
+}
+
+TEST_F(setfcap, map_unprivileged_nested)
+{
+ EXPECT_EQ(in_child(u_nested_maps_own, NULL), 0);
+}
+
+static long u_nested_maps_child(void *a)
+{
+ struct holder h;
+ long r;
+
+ become_user();
+ if (unshare(CLONE_NEWUSER) || map_user())
+ return SETUP_ERROR;
+ h = spawn_holder(HOLDER_ROOT, 0);
+ if (h.pid <= 0)
+ return SETUP_ERROR;
+ r = write_map(h.pid, "0 0 1\n");
+ reap_holder(h);
+ return r;
+}
+
+TEST_F(setfcap, map_unprivileged_nested_from_parent)
+{
+ EXPECT_EQ(in_child(u_nested_maps_child, NULL), 0);
+}
+
+static long session_to_parent(void *a)
+{
+ if (syscall(SYS_keyctl, KEYCTL_JOIN_SESSION_KEYRING, NULL) < 0 ||
+ syscall(SYS_keyctl, KEYCTL_SESSION_TO_PARENT) < 0)
+ return SETUP_ERROR;
+ return 0;
+}
+
+/* a cred replaced by key_change_session_keyring() keeps what it had */
+static long r_new_session_keyring_maps(void *a)
+{
+ if (unshare(CLONE_NEWUSER))
+ return SETUP_ERROR;
+ /*
+ * KEYCTL_SESSION_TO_PARENT wants our session keyring to be ours, not
+ * one inherited from whoever ran su or sudo.
+ */
+ if (syscall(SYS_keyctl, KEYCTL_JOIN_SESSION_KEYRING, NULL) < 0)
+ return errno == ENOSYS ? NO_KEYS : SETUP_ERROR;
+ if (in_child(session_to_parent, NULL))
+ return SETUP_ERROR;
+ return write_map(0, "0 0 1\n");
+}
+
+TEST_F(setfcap, map_after_session_to_parent)
+{
+ long r = in_child(r_new_session_keyring_maps, NULL);
+
+ if (r == NO_KEYS)
+ SKIP(return, "no keyrings");
+ EXPECT_EQ(r, 0);
+}
+
+/* ---------------------------------------------------------------------- */
+/* File capabilities that are honoured in an ancestor */
+
+struct parent_maps {
+ const char *map;
+ int rootid; /* -1: a v2 xattr */
+ int own_tmpfs; /* on a tmpfs of X's namespace */
+};
+
+/* X unshares and R maps it from the parent */
+static long x_unshares_r_maps(void *a)
+{
+ const struct parent_maps *m = a;
+ int s1[2], s2[2], res[2], pid;
+ long r = SETUP_ERROR;
+ char c;
+
+ if (pipe(s1) || pipe(s2) || pipe(res))
+ return SETUP_ERROR;
+ pid = fork();
+ if (!pid) {
+ restrict_root();
+ if (unshare(CLONE_NEWUSER) || write(s1[1], "u", 1) != 1 ||
+ read(s2[0], &c, 1) != 1)
+ _exit(1);
+ if (m->own_tmpfs) {
+ if (unshare(CLONE_NEWNS) ||
+ mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) ||
+ mount("tmpfs", "/mnt", "tmpfs", 0, NULL) ||
+ new_file("/mnt/f", 0))
+ r = SETUP_ERROR;
+ else
+ r = set_fcap("/mnt/f", m->rootid);
+ } else {
+ r = set_fcap("/tmp/x", m->rootid);
+ }
+ if (write(res[1], &r, sizeof(r)) != sizeof(r))
+ _exit(1);
+ _exit(0);
+ }
+ if (read(s1[0], &c, 1) != 1 || map_both(pid, m->map) ||
+ write(s2[1], "g", 1) != 1 || read(res[0], &r, sizeof(r)) != sizeof(r))
+ r = SETUP_ERROR;
+ kill(pid, SIGKILL);
+ waitpid(pid, NULL, 0);
+ return r;
+}
+
+TEST_F(setfcap, fcap_creator_without_setfcap_parent_mapped)
+{
+ struct parent_maps m = { "0 0 1\n", -1, 0 };
+
+ EXPECT_EQ(in_child(x_unshares_r_maps, &m), -EPERM);
+ EXPECT_FALSE(user_gets_sys_admin("/tmp/x"));
+}
+
+/* a v3 xattr can name the root user by any uid it has in the namespace */
+TEST_F(setfcap, fcap_v3_rootid_creator_without_setfcap_parent_mapped)
+{
+ struct parent_maps m = { "5 0 1\n", 5, 0 };
+
+ EXPECT_EQ(in_child(x_unshares_r_maps, &m), -EPERM);
+ EXPECT_FALSE(user_gets_sys_admin("/tmp/x"));
+}
+
+/* the v2 fast path of cap_convert_nscap() */
+TEST_F(setfcap, fcap_v2_creator_without_setfcap_parent_mapped)
+{
+ struct parent_maps m = { "0 0 1\n", -1, 1 };
+
+ EXPECT_EQ(in_child(x_unshares_r_maps, &m), -EPERM);
+}
+
+/* ... one level further down, with host uid 0 at uid 5 */
+TEST_F(setfcap, fcap_after_dropping_setfcap_and_nesting)
+{
+ int x_maps = 0;
+
+ EXPECT_EQ(in_child(r_nests_x, &x_maps), -EPERM);
+ EXPECT_FALSE(user_gets_sys_admin("/tmp/x"));
+}
+
+/* X unshares, R maps "0 0 1" for it, and another R joins it */
+static long joiner_takes_ns_reach(void *a)
+{
+ struct holder h;
+ long r;
+
+ h = spawn_mapped(HOLDER_RESTRICTED, "0 0 1\n");
+ if (h.pid <= 0)
+ return SETUP_ERROR;
+ r = join(h.pid) ? SETUP_ERROR : set_fcap("/tmp/x", -1);
+ reap_holder(h);
+ return r;
+}
+
+TEST_F(setfcap, fcap_joiner_takes_namespace_reach)
+{
+ EXPECT_EQ(in_child(joiner_takes_ns_reach, NULL), -EPERM);
+ EXPECT_FALSE(user_gets_sys_admin("/tmp/x"));
+}
+
+/*
+ * A v2 xattr on a filesystem whose namespace has no uid 0 has no root user
+ * and is honoured nowhere: refuse it rather than storing it unconverted.
+ */
+static long r_v2_without_uid0(void *a)
+{
+ if (unshare(CLONE_NEWUSER) || map_both(0, "1000 0 1\n"))
+ return SETUP_ERROR;
+ /* host uid 0, our uid, is uid 1000 here */
+ if (unshare(CLONE_NEWNS) ||
+ mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) ||
+ mount("tmpfs", "/mnt", "tmpfs", 0, NULL) || new_file("/mnt/f", 1000))
+ return SETUP_ERROR;
+ return set_fcap("/mnt/f", -1);
+}
+
+TEST_F(setfcap, fcap_v2_without_uid0_in_fs_ns)
+{
+ EXPECT_EQ(in_child(r_v2_without_uid0, NULL), -EINVAL);
+}
+
+/* ... and from the parent namespace, where the writer does have a root user */
+static long r_v2_without_uid0_from_parent(void *a)
+{
+ struct holder *h = a;
+ char path[64];
+
+ snprintf(path, sizeof(path), "/proc/%d/root/mnt/f", h->pid);
+ return set_fcap(path, -1);
+}
+
+TEST_F(setfcap, fcap_v2_without_uid0_from_parent)
+{
+ int ready[2], go[2], mapped[2], pid;
+ struct holder h;
+ char c;
+
+ ASSERT_EQ(pipe(ready), 0);
+ ASSERT_EQ(pipe(go), 0);
+ ASSERT_EQ(pipe(mapped), 0);
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (!pid) {
+ /* host uid 0 is uid 1000 in here, nobody is uid 0 */
+ if (unshare(CLONE_NEWUSER) || write(ready[1], "u", 1) != 1 ||
+ read(mapped[0], &c, 1) != 1 || unshare(CLONE_NEWNS) ||
+ mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) ||
+ mount("tmpfs", "/mnt", "tmpfs", 0, NULL) ||
+ new_file("/mnt/f", 1000) || write(ready[1], "m", 1) != 1)
+ _exit(1);
+ if (read(go[0], &c, 1) < 0)
+ _exit(1);
+ _exit(0);
+ }
+ h.pid = pid;
+ h.go = go[1];
+ ASSERT_EQ(read(ready[0], &c, 1), 1);
+ ASSERT_EQ(map_both(pid, "1000 0 1\n"), 0);
+ ASSERT_EQ(write(mapped[1], "g", 1), 1);
+ ASSERT_EQ(read(ready[0], &c, 1), 1);
+ EXPECT_EQ(in_child(r_v2_without_uid0_from_parent, &h), -EINVAL);
+ reap_holder(h);
+}
+
+/* What has to keep working. */
+
+static long r_maps_sets_fcap(void *a)
+{
+ if (unshare(CLONE_NEWUSER) || map_both(0, "0 0 1\n"))
+ return SETUP_ERROR;
+ return set_fcap("/tmp/x", -1);
+}
+
+TEST_F(setfcap, fcap_own_ns)
+{
+ EXPECT_EQ(in_child(r_maps_sets_fcap, NULL), 0);
+ EXPECT_TRUE(user_gets_sys_admin("/tmp/x"));
+}
+
+static long r_joins_sets_fcap(void *a)
+{
+ long r = join(PID(a));
+
+ if (r)
+ return r;
+ return set_fcap("/tmp/x", -1);
+}
+
+TEST_F(setfcap, fcap_after_setns_with_setfcap)
+{
+ struct holder h;
+
+ h = spawn_mapped(HOLDER_ROOT, "0 0 1\n");
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(r_joins_sets_fcap, &h), 0);
+ reap_holder(h);
+}
+
+static long u_sets_fcap(void *a)
+{
+ become_user();
+ if (unshare(CLONE_NEWUSER) || map_user())
+ return SETUP_ERROR;
+ return set_fcap("/tmp/u", -1);
+}
+
+TEST_F(setfcap, fcap_unprivileged)
+{
+ EXPECT_EQ(in_child(u_sets_fcap, NULL), 0);
+}
+
+static long u_nested_sets_fcap_inner(void *a)
+{
+ if (unshare(CLONE_NEWUSER) || map_both(0, "0 0 1\n"))
+ return SETUP_ERROR;
+ return set_fcap("/tmp/u", -1);
+}
+
+static long u_nested_sets_fcap(void *a)
+{
+ become_user();
+ if (unshare(CLONE_NEWUSER) || map_user())
+ return SETUP_ERROR;
+ return in_child(u_nested_sets_fcap_inner, NULL);
+}
+
+TEST_F(setfcap, fcap_unprivileged_nested)
+{
+ EXPECT_EQ(in_child(u_nested_sets_fcap, NULL), 0);
+}
+
+static long u_joins_own_sets_fcap(void *a)
+{
+ long r;
+
+ become_user();
+ r = join(PID(a));
+ if (r)
+ return r;
+ return set_fcap("/tmp/u", -1);
+}
+
+TEST_F(setfcap, fcap_unprivileged_after_setns)
+{
+ struct holder h;
+
+ h = spawn_holder(HOLDER_USER, 0);
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(u_joins_own_sets_fcap, &h), 0);
+ reap_holder(h);
+}
+
+static long r_v2_own_fs(void *a)
+{
+ if (unshare(CLONE_NEWUSER) || map_both(0, "0 0 1\n"))
+ return SETUP_ERROR;
+ if (unshare(CLONE_NEWNS) ||
+ mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) ||
+ mount("tmpfs", "/mnt", "tmpfs", 0, NULL) || new_file("/mnt/f", 0))
+ return SETUP_ERROR;
+ return set_fcap("/mnt/f", -1);
+}
+
+TEST_F(setfcap, fcap_v2_own_ns)
+{
+ EXPECT_EQ(in_child(r_v2_own_fs, NULL), 0);
+}
+
+/* ---------------------------------------------------------------------- */
+/* Idmapped mounts: the root user is both the one seen through the mount */
+/* and the one stored. Mounts that make a container's root host uid 0 are */
+/* up to whoever sets them up. */
+
+struct idmap_case {
+ const char *ns_map, *idmap_map;
+ int file_uid, restricted;
+ enum holder_kind ns_kind;
+};
+
+/* returns 1 to skip: no idmapped mounts on tmpfs */
+static int idmap_case(const struct idmap_case *c, long *r)
+{
+ struct holder ns, im;
+ int pid, p[2], st;
+
+ ns = spawn_mapped(c->ns_kind, c->ns_map);
+ if (ns.pid <= 0)
+ return -1;
+ im = c->idmap_map ? spawn_mapped(HOLDER_ROOT, c->idmap_map) : ns;
+ if (im.pid <= 0 || new_file("/tmp/x", c->file_uid))
+ return -1;
+ if (idmap_tmp(im.pid)) {
+ reap_holder(ns);
+ if (c->idmap_map)
+ reap_holder(im);
+ return 1;
+ }
+ if (pipe(p))
+ return -1;
+ /* join from a child of the namespace's holder's sibling: pass the pid */
+ pid = fork();
+ if (!pid) {
+ long res;
+
+ if (c->restricted)
+ restrict_root();
+ res = join(ns.pid) ? SETUP_ERROR : set_fcap("/mnt/x", 0);
+ if (write(p[1], &res, sizeof(res)) != sizeof(res))
+ _exit(1);
+ _exit(0);
+ }
+ if (read(p[0], r, sizeof(*r)) != sizeof(*r))
+ *r = SETUP_ERROR;
+ waitpid(pid, &st, 0);
+ umount2("/mnt", MNT_DETACH);
+ reap_holder(ns);
+ if (c->idmap_map)
+ reap_holder(im);
+ return 0;
+}
+
+FIXTURE(idmap) {};
+
+FIXTURE_VARIANT(idmap) {
+ struct idmap_case c;
+ long expect;
+};
+
+/*
+ * The mount shows a file of host uid 100000 as host uid 0's. X's namespace,
+ * mapped "0 0 1" by R, has host uid 0 but X's CAP_SETFCAP doesn't reach it.
+ */
+FIXTURE_VARIANT_ADD(idmap, reverse_mapping) {
+ .c = { "0 0 1\n", "100000 0 1\n", 100000, 1, HOLDER_RESTRICTED },
+ .expect = -EPERM,
+};
+
+/* R in its container writes through the container's own idmapped rootfs */
+FIXTURE_VARIANT_ADD(idmap, container_root) {
+ .c = { "0 100000 1000\n", NULL, 0, 0 },
+ .expect = 0,
+};
+
+/* left to the admin: the container doesn't map host uid 0 */
+FIXTURE_VARIANT_ADD(idmap, container_without_host_root) {
+ .c = { "0 100000 1000\n", NULL, 0, 1 },
+ .expect = 0,
+};
+
+FIXTURE_VARIANT_ADD(idmap, unrelated_idmap) {
+ .c = { "0 100000 1000\n", "200000 100000 1\n", 200000, 1 },
+ .expect = 0,
+};
+
+FIXTURE_SETUP(idmap)
+{
+ if (geteuid())
+ SKIP(return, "needs root");
+ prepare(_metadata);
+}
+
+FIXTURE_TEARDOWN(idmap) {}
+
+TEST_F(idmap, fcap)
+{
+ long r;
+ int ret;
+
+ ret = idmap_case(&variant->c, &r);
+ ASSERT_GE(ret, 0);
+ if (ret == 1)
+ SKIP(return, "no idmapped mounts on tmpfs");
+ EXPECT_EQ(r, variant->expect);
+}
+
+/* ---------------------------------------------------------------------- */
+/* setns() */
+/* */
+/* A task that joins gets CAP_SETFCAP wherever the namespace's creator had */
+/* it, so mapping uid 0 or writing file capabilities afterwards has no */
+/* check of its own: setns() is the one. */
+
+static long x_joins(void *a)
+{
+ restrict_root();
+ return join(PID(a));
+}
+
+static long r_joins(void *a)
+{
+ return join(PID(a));
+}
+
+TEST_F(setfcap, setns_ns_mapping_host_root)
+{
+ struct holder h;
+
+ h = spawn_mapped(HOLDER_ROOT, "0 0 1\n");
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(x_joins, &h), -EPERM);
+ reap_holder(h);
+}
+
+/* the root user can be mapped at any uid */
+TEST_F(setfcap, setns_ns_mapping_host_root_at_nonzero_uid)
+{
+ struct holder h;
+
+ h = spawn_mapped(HOLDER_ROOT, "5 0 1\n");
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(x_joins, &h), -EPERM);
+ reap_holder(h);
+}
+
+TEST_F(setfcap, setns_ns_without_map_yet)
+{
+ struct holder h;
+
+ h = spawn_holder(HOLDER_ROOT, 0);
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(x_joins, &h), -EPERM);
+ reap_holder(h);
+}
+
+/* the same one level down, among tasks of an unprivileged user */
+static long u_nested_x_joins(void *a)
+{
+ struct holder h;
+ long r;
+
+ become_user();
+ if (unshare(CLONE_NEWUSER) || map_user())
+ return SETUP_ERROR;
+ /* R-like: full set in the user's namespace */
+ h = spawn_holder(HOLDER_ROOT, 0);
+ if (h.pid <= 0)
+ return SETUP_ERROR;
+ r = in_child(x_joins, &h);
+ reap_holder(h);
+ return r;
+}
+
+TEST_F(setfcap, setns_nested_unprivileged)
+{
+ EXPECT_EQ(in_child(u_nested_x_joins, NULL), -EPERM);
+}
+
+/* a container that doesn't map host uid 0 */
+TEST_F(setfcap, setns_container)
+{
+ struct holder h;
+
+ h = spawn_mapped(HOLDER_ROOT, "0 100000 1000\n");
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(x_joins, &h), 0);
+ reap_holder(h);
+}
+
+TEST_F(setfcap, setns_unprivileged_ns)
+{
+ struct holder h;
+
+ h = spawn_holder(HOLDER_USER, 0);
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(r_joins, &h), 0);
+ reap_holder(h);
+}
+
+/* ---------------------------------------------------------------------- */
+/* ptrace */
+
+static long x_attaches(void *a)
+{
+ restrict_root();
+ return attach_only(PID(a));
+}
+
+static long r_attaches(void *a)
+{
+ return attach_only(PID(a));
+}
+
+TEST_F(setfcap, ptrace_into_ns_without_map_yet)
+{
+ struct holder h;
+
+ h = spawn_holder(HOLDER_ROOT, 0);
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(x_attaches, &h), -EPERM);
+ reap_holder(h);
+}
+
+static long x_takes_fd(void *a)
+{
+ int pfd, fd;
+
+ restrict_root();
+ pfd = syscall(__NR_pidfd_open, PID(a), 0);
+ if (pfd < 0)
+ return SETUP_ERROR;
+ fd = syscall(__NR_pidfd_getfd, pfd, 100, 0);
+ if (fd < 0)
+ return -errno;
+ close(fd);
+ return 0;
+}
+
+/* pidfd_getfd() checks for PTRACE_MODE_ATTACH */
+TEST_F(setfcap, ptrace_pidfd_getfd)
+{
+ struct holder h;
+
+ h = spawn_holder(HOLDER_ROOT, 100);
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(x_takes_fd, &h), -EPERM);
+ reap_holder(h);
+}
+
+TEST_F(setfcap, ptrace_into_ns_mapping_host_root)
+{
+ struct holder h;
+
+ h = spawn_mapped(HOLDER_ROOT, "0 0 1\n");
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(x_attaches, &h), -EPERM);
+ reap_holder(h);
+}
+
+TEST_F(setfcap, ptrace_into_container)
+{
+ struct holder h;
+
+ h = spawn_mapped(HOLDER_ROOT, "0 100000 1000\n");
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(x_attaches, &h), 0);
+ reap_holder(h);
+}
+
+TEST_F(setfcap, ptrace_ns_mapping_only_user)
+{
+ struct holder h;
+
+ h = spawn_mapped(HOLDER_ROOT, "0 1000 1\n");
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(x_attaches, &h), 0);
+ reap_holder(h);
+}
+
+TEST_F(setfcap, ptrace_between_restricted)
+{
+ struct holder h;
+
+ h = spawn_holder(HOLDER_RESTRICTED, 0);
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(x_attaches, &h), 0);
+ reap_holder(h);
+}
+
+TEST_F(setfcap, ptrace_with_setfcap)
+{
+ struct holder h;
+
+ h = spawn_holder(HOLDER_ROOT, 0);
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(r_attaches, &h), 0);
+ reap_holder(h);
+}
+
+static long x_reads_environ(void *a)
+{
+ char path[64];
+ int fd;
+
+ restrict_root();
+ snprintf(path, sizeof(path), "/proc/%d/environ", PID(a));
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ return -errno;
+ close(fd);
+ return 0;
+}
+
+/* PTRACE_MODE_READ is unchanged */
+TEST_F(setfcap, ptrace_read_mode_unchanged)
+{
+ struct holder h;
+
+ h = spawn_holder(HOLDER_ROOT, 0);
+ ASSERT_GT(h.pid, 0);
+ EXPECT_EQ(in_child(x_reads_environ, &h), 0);
+ reap_holder(h);
+}
+
+TEST_HARNESS_MAIN
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-10-26 21:00 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-26 21:00 [PATCH v7 0/6] capabilities: close the ways around the CAP_SETFCAP rule for uid 0 Josef Bacik
2026-10-26 21:00 ` [PATCH v7 1/6] userns: record how far up CAP_SETFCAP reaches Josef Bacik
2026-10-26 21:00 ` [PATCH v7 2/6] userns: don't let setns() lend CAP_SETFCAP Josef Bacik
2026-10-26 21:00 ` [PATCH v7 3/6] userns: check the opener and the writer before mapping uid 0 Josef Bacik
2026-10-26 21:00 ` [PATCH v7 4/6] capabilities: limit fscaps to where CAP_SETFCAP reaches Josef Bacik
2026-10-26 21:00 ` [PATCH v7 5/6] capabilities: don't let ptrace borrow CAP_SETFCAP Josef Bacik
2026-10-26 21:00 ` [PATCH v7 6/6] selftests/namespaces: test the CAP_SETFCAP rule for user namespaces Josef Bacik
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®