[PATCH v1] nsproxy: Fix cred leak in the error path of prepare_nsset()
From: Nickolai Zeldovich
Date: Fri Oct 16 2026 - 23:35:51 EST
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")
Assisted-by: LLM
Signed-off-by: Nickolai Zeldovich <nickolai@xxxxxxxxxxxxx>
---
Notes:
The leak is reachable from userspace on a CONFIG_USER_NS kernel:
setns(pidfd, CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWUTS) where
copy_fs_struct() returns NULL. copy_fs_struct() allocates from fs_cachep
("fs_cache"), so CONFIG_FAILSLAB with that cache's per-slab failslab knob
forces the error arm deterministically; otherwise it needs real memory
pressure. The leaked object is the cred from prepare_creds() and the
user_namespace, ucounts and group_info references it holds; CONFIG_USER_NS
LSM blobs hung off that cred (security_prepare_creds) leak with it.
Reproduced under QEMU (x86_64 defconfig + CONFIG_USER_NS, CONFIG_FAILSLAB,
CONFIG_FAULT_INJECTION_DEBUG_FS, CONFIG_SLUB_DEBUG, CONFIG_DEBUG_KMEMLEAK;
booted with slab_nomerge so fs_cache keeps refcount 1 for the per-cache
knob). A task marks fs_cache for failslab, marks itself (make-it-fail), and
calls setns() 500 times; every call returns -ENOMEM. On 70dd4ee72eb2 the
"cred" slab grows (active 120 -> 660) and kmemleak reports the leaked creds:
unreferenced object 0xff4bc4614195c000 (size 184):
comm "test", ...
backtrace:
kmem_cache_alloc_noprof+0x188/0x3c0
prepare_creds+0x22/0x280
__se_sys_setns+0x131/0x710 (prepare_nsset is inlined here)
do_syscall_64+0x109/0x330
kmemleak: before 851 unreferenced cred objects via prepare_creds<-setns,
RESULT FAIL; after 0, RESULT PASS. (The SLUB /proc/slabinfo active count is
approximate, so kmemleak is the signal.)
Reproducer (needs CONFIG_FAILSLAB + CONFIG_DEBUG_KMEMLEAK, boot slab_nomerge,
run as root):
cc -O2 -Wall -static -o setns_cred_leak setns_cred_leak.c
./setns_cred_leak # before: RESULT: FAIL; after: RESULT: PASS
// SPDX-License-Identifier: GPL-2.0
/* setns() prepare_nsset() cred leak: fail copy_fs_struct() with failslab. */
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/mount.h>
#include <sys/syscall.h>
#define N 500
static void w(const char *path, const char *val)
{
int fd = open(path, O_WRONLY);
if (fd >= 0) {
if (write(fd, val, strlen(val)) < 0)
perror(path);
close(fd);
}
}
int main(void)
{
char line[512];
int pidfd, i, enomem = 0, leaks = 0;
FILE *f;
mount("debugfs", "/sys/kernel/debug", "debugfs", 0, NULL);
w("/sys/kernel/slab/fs_cache/failslab", "1"); /* needs slab_nomerge */
w("/sys/kernel/debug/failslab/cache-filter", "1");
w("/sys/kernel/debug/failslab/task-filter", "1");
w("/sys/kernel/debug/failslab/ignore-gfp-wait", "0"); /* fail GFP_KERNEL */
w("/sys/kernel/debug/failslab/probability", "100");
w("/sys/kernel/debug/failslab/interval", "1");
w("/sys/kernel/debug/failslab/times", "-1");
pidfd = syscall(SYS_pidfd_open, getpid(), 0);
w("/proc/self/make-it-fail", "1");
for (i = 0; i < N; i++)
if (setns(pidfd, CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWUTS) &&
errno == ENOMEM)
enomem++;
w("/proc/self/make-it-fail", "0");
w("/sys/kernel/slab/fs_cache/failslab", "0");
w("/sys/kernel/debug/failslab/times", "0");
w("/sys/kernel/debug/kmemleak", "scan");
sleep(5);
w("/sys/kernel/debug/kmemleak", "scan");
sleep(1);
f = fopen("/sys/kernel/debug/kmemleak", "r");
while (f && fgets(line, sizeof(line), f))
if (strstr(line, "prepare_creds")) /* one per leaked cred */
leaks++;
if (f)
fclose(f);
printf("setns x%d: ENOMEM=%d; kmemleak cred leaks=%d\n", N, enomem, leaks);
printf("RESULT: %s\n", leaks > N / 10 ? "FAIL" : "PASS");
return leaks > N / 10;
}
kernel/nsproxy.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/kernel/nsproxy.c b/kernel/nsproxy.c
index 3fb1595a4a59..028019f3e387 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:
base-commit: 70dd4ee72eb2d5793c62e71c14e3aba78e5a4e56
--
2.53.0