diff options
| author | Josef Bacik <josef@toxicpanda.com> | 2026-10-04 00:10:59 +0000 |
|---|---|---|
| committer | Christian Brauner <brauner@kernel.org> | 2026-10-29 15:46:57 +0100 |
| commit | da29ac46d34a19e8cc328ad17cb88713fd4563fa (patch) | |
| tree | e1a1459842133901a8acf439c46982e66cdaf316 /kernel | |
| parent | 02a336a831f02a18047bacbd49018a237af8a172 (diff) | |
| download | linux-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.c | 55 |
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)) { |
