* [PATCH v4 1/5] userns: record how far up CAP_SETFCAP reaches
2026-10-22 18:36 [PATCH v4 0/5] capabilities: close the ways around the CAP_SETFCAP rule for uid 0 Josef Bacik
@ 2026-10-22 18:36 ` Josef Bacik
2026-10-22 18:36 ` [PATCH v4 2/5] userns: don't let setns() lend CAP_SETFCAP Josef Bacik
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Josef Bacik @ 2026-10-22 18:36 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, 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] 6+ messages in thread* [PATCH v4 2/5] userns: don't let setns() lend CAP_SETFCAP
2026-10-22 18:36 [PATCH v4 0/5] capabilities: close the ways around the CAP_SETFCAP rule for uid 0 Josef Bacik
2026-10-22 18:36 ` [PATCH v4 1/5] userns: record how far up CAP_SETFCAP reaches Josef Bacik
@ 2026-10-22 18:36 ` Josef Bacik
2026-10-22 18:36 ` [PATCH v4 3/5] userns: check the opener and the writer before mapping uid 0 Josef Bacik
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Josef Bacik @ 2026-10-22 18:36 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, 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] 6+ messages in thread* [PATCH v4 3/5] userns: check the opener and the writer before mapping uid 0
2026-10-22 18:36 [PATCH v4 0/5] capabilities: close the ways around the CAP_SETFCAP rule for uid 0 Josef Bacik
2026-10-22 18:36 ` [PATCH v4 1/5] userns: record how far up CAP_SETFCAP reaches Josef Bacik
2026-10-22 18:36 ` [PATCH v4 2/5] userns: don't let setns() lend CAP_SETFCAP Josef Bacik
@ 2026-10-22 18:36 ` Josef Bacik
2026-10-22 18:36 ` [PATCH v4 4/5] capabilities: limit fscaps to where CAP_SETFCAP reaches Josef Bacik
2026-10-22 18:36 ` [PATCH v4 5/5] capabilities: don't let ptrace borrow CAP_SETFCAP Josef Bacik
4 siblings, 0 replies; 6+ messages in thread
From: Josef Bacik @ 2026-10-22 18:36 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, Bradley Morgan, 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 CAP_SETFCAP over the parent isn't always enough. The parent may map
its own parent's uid 0, so its root user is the root user of ancestors
further up, and the capability has to count there too. A task without
CAP_SETFCAP that unshared a namespace, which a privileged task then
mapped "0 0 1" for, has CAP_SETFCAP in it. It could unshare again and
pass on uid 0 of the initial namespace one level further down.
Find the topmost namespace that shares the parent's root user with
cap_root_level(), and require that the opener and the writer both reach
that far: by ns->setfcap_level for a task in the namespace that is being
mapped, by cap_setfcap_level() for one outside it.
This only affects maps that contain uid 0 of the parent namespace. 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")
Reviewed-by: Bradley Morgan <brads@mainlining.org>
Assisted-by: LLM
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
include/linux/capability.h | 1 +
kernel/user_namespace.c | 29 +++++++++++++++++++++--------
security/commoncap.c | 23 +++++++++++++++++++++++
3 files changed, 45 insertions(+), 8 deletions(-)
diff --git a/include/linux/capability.h b/include/linux/capability.h
index e4199b03668b..2eaef87748e9 100644
--- a/include/linux/capability.h
+++ b/include/linux/capability.h
@@ -227,5 +227,6 @@ 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);
+int cap_root_level(kuid_t kuid, struct user_namespace *ns);
#endif /* !_LINUX_CAPABILITY_H */
diff --git a/kernel/user_namespace.c b/kernel/user_namespace.c
index 7bd3692c2f3e..f948cf31ecdf 100644
--- a/kernel/user_namespace.c
+++ b/kernel/user_namespace.c
@@ -888,8 +888,9 @@ EXPORT_SYMBOL_IF_KUNIT(uid_gid_map_sort);
* @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.
+ * 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,7 +898,7 @@ static bool verify_root_map(const struct file *file,
struct user_namespace *map_ns,
struct uid_gid_map *new_map)
{
- int idx;
+ int idx, level;
const struct user_namespace *file_ns = file->f_cred->user_ns;
struct uid_gid_extent *extent0 = NULL;
@@ -915,24 +916,36 @@ static bool verify_root_map(const struct file *file,
if (!extent0)
return true;
+ /* The parent may in turn share its root user with its ancestors. */
+ level = cap_root_level(make_kuid(map_ns->parent, 0), map_ns->parent);
+
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.
+ * when it unshared, as far up as that root user goes.
* */
- if (file_ns->setfcap_level >= file_ns->level)
+ if (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 that root user goes.
+ */
+ 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 map_ns->setfcap_level <= level;
+
+ return ns_capable(map_ns->parent, CAP_SETFCAP) &&
+ cred_setfcap_reaches(current_cred(), map_ns->parent, level);
}
/*
diff --git a/security/commoncap.c b/security/commoncap.c
index 8a9d998b1897..b06914223039 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.
+ */
+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
--
2.55.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [PATCH v4 4/5] capabilities: limit fscaps to where CAP_SETFCAP reaches
2026-10-22 18:36 [PATCH v4 0/5] capabilities: close the ways around the CAP_SETFCAP rule for uid 0 Josef Bacik
` (2 preceding siblings ...)
2026-10-22 18:36 ` [PATCH v4 3/5] userns: check the opener and the writer before mapping uid 0 Josef Bacik
@ 2026-10-22 18:36 ` Josef Bacik
2026-10-22 18:36 ` [PATCH v4 5/5] capabilities: don't let ptrace borrow CAP_SETFCAP Josef Bacik
4 siblings, 0 replies; 6+ messages in thread
From: Josef Bacik @ 2026-10-22 18:36 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, 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, and is refused there. 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.
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 | 30 ++++++++++++++++++++++++++----
1 file changed, 26 insertions(+), 4 deletions(-)
diff --git a/security/commoncap.c b/security/commoncap.c
index b06914223039..655e822c8e76 100644
--- a/security/commoncap.c
+++ b/security/commoncap.c
@@ -694,19 +694,41 @@ 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))
- /* user is privileged, just write the v2 */
- return size;
+ /*
+ * 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.
+ * 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) &&
+ cap_root_level(make_kuid(fs_ns, 0), 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] 6+ messages in thread* [PATCH v4 5/5] capabilities: don't let ptrace borrow CAP_SETFCAP
2026-10-22 18:36 [PATCH v4 0/5] capabilities: close the ways around the CAP_SETFCAP rule for uid 0 Josef Bacik
` (3 preceding siblings ...)
2026-10-22 18:36 ` [PATCH v4 4/5] capabilities: limit fscaps to where CAP_SETFCAP reaches Josef Bacik
@ 2026-10-22 18:36 ` Josef Bacik
4 siblings, 0 replies; 6+ messages in thread
From: Josef Bacik @ 2026-10-22 18:36 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, 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 655e822c8e76..b33b10b444bf 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] 6+ messages in thread