summaryrefslogtreecommitdiff
path: root/kernel
diff options
context:
space:
mode:
authorJosef Bacik <josef@toxicpanda.com>2026-10-04 00:10:59 +0000
committerChristian Brauner <brauner@kernel.org>2026-10-29 15:46:57 +0100
commitda29ac46d34a19e8cc328ad17cb88713fd4563fa (patch)
treee1a1459842133901a8acf439c46982e66cdaf316 /kernel
parent02a336a831f02a18047bacbd49018a237af8a172 (diff)
downloadlinux-da29ac46d34a19e8cc328ad17cb88713fd4563fa.tar.gz
linux-da29ac46d34a19e8cc328ad17cb88713fd4563fa.zip
nstree: don't end listns() at an inactive namespace
listns() resumes after the last returned id by looking up the first namespace with a larger id with lookup_ns_id_after(), or with lookup_ns_owner_after() when filtering by owner. Both also take a reference with ns_get_unless_inactive(). If that namespace is inactive, e.g. a network namespace that is only pinned by a socket or a user namespace that is only pinned by a file's credentials, or if it is being torn down, the lookup returns NULL and listns() fails with -ENOENT. Userspace takes that as the end of the list, so every namespace after it is silently missed: ids 10 (active), 11 (inactive), 12 (active) listns(ns_id = 0, nr = 1) -> 10 listns(ns_id = 10, nr = 1) -> lookup_ns_id_after(10) -> NULL, -ENOENT 12 is never listed A call without a cursor doesn't have this problem because the loop just skips the namespaces that legitimize_ns() rejects. The reference only kept the starting point valid until the RCU walk began. Look it up under rcu_read_lock() instead, without taking a reference. The namespace can't be freed before the RCU read-side section ends, and if it is unlinked in the meantime its ->next still leads back into the list, which the walk already relies on for every other entry. The starting namespace then goes through legitimize_ns() like every other one, and kls->first_ns isn't needed anymore. Reported-by: shaos <shawnq1@163.com> Link: https://lore.kernel.org/r/20261020110746.959806-1-shawnq1@163.com Fixes: 76b6f5dfb3fd ("nstree: add listns()") Cc: stable@vger.kernel.org Signed-off-by: Josef Bacik <josef@toxicpanda.com> Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Diffstat (limited to 'kernel')
-rw-r--r--kernel/nstree.c55
1 files changed, 28 insertions, 27 deletions
diff --git a/kernel/nstree.c b/kernel/nstree.c
index 58b2b55fa8d2..a756ae2e7881 100644
--- a/kernel/nstree.c
+++ b/kernel/nstree.c
@@ -408,15 +408,12 @@ struct klistns {
u32 ns_type;
struct user_namespace *user_ns;
bool userns_capable;
- struct ns_common *first_ns;
};
static void __free_klistns_free(const struct klistns *kls)
{
if (kls->user_ns_id != LISTNS_CURRENT_USER)
put_user_ns(kls->user_ns);
- if (kls->first_ns && kls->first_ns->ops)
- kls->first_ns->ops->put(kls->first_ns);
}
#define NS_ALL (PID_NS | USER_NS | MNT_NS | UTS_NS | IPC_NS | NET_NS | CGROUP_NS | TIME_NS)
@@ -461,6 +458,9 @@ static inline int prepare_klistns(struct klistns *kls, struct ns_id_req *kreq,
/*
* Lookup a namespace owned by owner with id > ns_id.
* Returns the namespace with the smallest id that is > ns_id.
+ *
+ * No reference is taken. The caller must hold rcu_read_lock() and may only
+ * use the result as the starting point of an RCU list walk.
*/
static struct ns_common *lookup_ns_owner_after(u64 ns_id, struct ns_common *owner)
{
@@ -484,8 +484,6 @@ static struct ns_common *lookup_ns_owner_after(u64 ns_id, struct ns_common *owne
}
}
- if (ret)
- ret = ns_get_unless_inactive(ret);
return ret;
}
@@ -558,25 +556,27 @@ static ssize_t do_listns_userns(struct klistns *kls)
return -EINVAL;
kls->user_ns = to_user_ns(ns);
- /*
- * Use the rbtree to find the first namespace we care about and
- * then use it's list entry to iterate from there.
- */
- if (kls->last_ns_id) {
- kls->first_ns = lookup_ns_owner_after(kls->last_ns_id, ns);
- if (!kls->first_ns)
- return -ENOENT;
- first_ns = kls->first_ns;
- }
-
ret = 0;
head = &to_ns_common(kls->user_ns)->ns_owner_root.ns_list_head;
kls->userns_capable = may_see_all_namespaces();
rcu_read_lock();
- if (!first_ns)
+ /*
+ * Use the rbtree to find the first namespace we care about and
+ * then use it's list entry to iterate from there. That namespace
+ * is filtered like any other, so the walk doesn't end early just
+ * because it is inactive or already dying.
+ */
+ if (kls->last_ns_id) {
+ first_ns = lookup_ns_owner_after(kls->last_ns_id, ns);
+ if (!first_ns) {
+ rcu_read_unlock();
+ return -ENOENT;
+ }
+ } else {
first_ns = list_entry_rcu(head->next, typeof(*first_ns), ns_owner_node.ns_list_entry);
+ }
ns = first_ns;
list_for_each_entry_from_rcu(ns, head, ns_owner_node.ns_list_entry) {
@@ -613,6 +613,9 @@ static ssize_t do_listns_userns(struct klistns *kls)
/*
* Lookup a namespace with id > ns_id in either the unified tree or a type-specific tree.
* Returns the namespace with the smallest id that is > ns_id.
+ *
+ * No reference is taken. The caller must hold rcu_read_lock() and may only
+ * use the result as the starting point of an RCU list walk.
*/
static struct ns_common *lookup_ns_id_after(u64 ns_id, int ns_type)
{
@@ -649,8 +652,6 @@ static struct ns_common *lookup_ns_id_after(u64 ns_id, int ns_type)
}
}
- if (ret)
- ret = ns_get_unless_inactive(ret);
return ret;
}
@@ -700,13 +701,6 @@ static ssize_t do_listns(struct klistns *kls)
return -EINVAL;
}
- if (kls->last_ns_id) {
- kls->first_ns = lookup_ns_id_after(kls->last_ns_id, ns_type);
- if (!kls->first_ns)
- return -ENOENT;
- first_ns = kls->first_ns;
- }
-
ret = 0;
if (ns_tree)
head = &ns_tree->ns_list_head;
@@ -715,8 +709,15 @@ static ssize_t do_listns(struct klistns *kls)
rcu_read_lock();
- if (!first_ns)
+ if (kls->last_ns_id) {
+ first_ns = lookup_ns_id_after(kls->last_ns_id, ns_type);
+ if (!first_ns) {
+ rcu_read_unlock();
+ return -ENOENT;
+ }
+ } else {
first_ns = first_ns_common(head, ns_tree);
+ }
for (ns = first_ns; !ns_common_is_head(ns, head, ns_tree) && nr_ns_ids;
ns = next_ns_common(ns, ns_tree)) {