From e08601d3a7b2236f4915497dca353dcf117bab5c Mon Sep 17 00:00:00 2001 From: Nickolai Zeldovich Date: Fri, 16 Oct 2026 23:35:38 -0400 Subject: [PATCH] nsproxy: Fix cred leak in the error path of prepare_nsset() A task that calls setns() with CLONE_NEWUSER together with CLONE_NEWNS and at least one more namespace flag leaks a struct cred (and the user_namespace, ucounts and group_info references it holds) whenever the call fails at the fs_struct copy. On a kernel with CONFIG_USER_NS the leak is one cred per failed setns(); under memory pressure, or driven by a fault-injecting attacker, it accumulates and pins user namespaces that can then never be freed. prepare_nsset() sets nsset->flags only on the success path: if (flags & CLONE_NEWUSER) nsset->cred = prepare_creds(); ... } else if (flags & CLONE_NEWNS) { nsset->fs = copy_fs_struct(me->fs); if (!nsset->fs) goto out; /* flags still 0 here */ } nsset->flags = flags; return 0; out: put_nsset(nsset); put_nsset() decides what to release from nsset->flags: if (flags & CLONE_NEWUSER) put_cred(nsset_cred(nsset)); On the copy_fs_struct() failure arm prepare_creds() has already allocated the cred, but nsset->flags is still the zero from the caller's "struct nsset nsset = {}", so put_nsset() does not see CLONE_NEWUSER and never drops it. The caller (__do_sys_setns) returns the error without touching nsset, so the cred is lost. Set nsset->flags at the start of prepare_nsset(), before anything can fail, so every error exit through put_nsset() releases what was taken. commit_nsset(), which clears nsset->cred on success, is unaffected. Reproduced on x86_64 under QEMU with failslab aimed at the fs_struct cache and kmemleak enabled: 500 calls of setns(pidfd, CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWUTS) each fail with -ENOMEM and leave 851 unreferenced cred objects allocated from prepare_creds() via setns() before the fix, and none after (the program is in the notes below). Found by formal verification of kernel/nsproxy.c. Fixes: 303cc571d107 ("nsproxy: attach to namespaces via pidfds") Signed-off-by: Nickolai Zeldovich Link: https://patch.msgid.link/20261017033538.793280-1-nickolai@csail.mit.edu Reviewed-by: Serge Hallyn Reviewed-by: Bradley Morgan Signed-off-by: Christian Brauner (Amutable) --- kernel/nsproxy.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/nsproxy.c b/kernel/nsproxy.c index 454427b09ed9..2dcf105e8ee2 100644 --- a/kernel/nsproxy.c +++ b/kernel/nsproxy.c @@ -355,6 +355,7 @@ static int prepare_nsset(unsigned flags, struct nsset *nsset) { struct task_struct *me = current; + nsset->flags = flags; nsset->nsproxy = create_new_namespaces(0, me, current_user_ns(), me->fs); if (IS_ERR(nsset->nsproxy)) return PTR_ERR(nsset->nsproxy); @@ -375,7 +376,6 @@ static int prepare_nsset(unsigned flags, struct nsset *nsset) goto out; } - nsset->flags = flags; return 0; out: