From 051d228b1cba2de97d4e2eec23342bc773623876 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Fri, 13 Feb 2026 11:15:05 +0100 Subject: [PATCH 1/3] tree-wide: Add some extra debug logging --- src/basic/terminal-util.c | 3 ++- src/machine/machine-dbus.c | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index 1013e38d0d..732ab0824d 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -31,6 +31,7 @@ #include "namespace-util.h" #include "parse-util.h" #include "path-util.h" +#include "pidref.h" #include "proc-cmdline.h" #include "process-util.h" #include "signal-util.h" @@ -1666,7 +1667,7 @@ int openpt_allocate_in_namespace( r = pidref_namespace_open(pidref, &pidnsfd, &mntnsfd, /* ret_netns_fd= */ NULL, &usernsfd, &rootfd); if (r < 0) - return r; + return log_debug_errno(r, "Failed to open namespaces of PID "PID_FMT": %m", pidref->pid); if (socketpair(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0, pair) < 0) return -errno; diff --git a/src/machine/machine-dbus.c b/src/machine/machine-dbus.c index 6bee74fd30..e3281e1d38 100644 --- a/src/machine/machine-dbus.c +++ b/src/machine/machine-dbus.c @@ -376,7 +376,10 @@ int bus_machine_method_open_shell(sd_bus_message *message, void *userdata, sd_bu if (m->uid != 0 && m->class != MACHINE_HOST) { r = pidref_in_same_namespace(&PIDREF_MAKE_FROM_PID(1), &m->leader, NAMESPACE_USER); if (r < 0) - return r; + return log_debug_errno( + r, + "Failed to check if machine '%s' is running in the root user namespace: %m", + m->name); if (r != 0) return sd_bus_error_set( error, From e2c1f3ca2a69a033c748d62f9be0db8817826839 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Fri, 13 Feb 2026 12:24:49 +0100 Subject: [PATCH 2/3] user-util: Don't setgroups() if /proc/self/gid_map is empty If /proc/self/gid_map is empty, the kernel will refuse setgroups(), so don't attempt it if that's the case on top of the /proc/self/setgroups check we already have. --- src/basic/user-util.c | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/basic/user-util.c b/src/basic/user-util.c index 6f5b597694..e434fbec8f 100644 --- a/src/basic/user-util.c +++ b/src/basic/user-util.c @@ -920,19 +920,28 @@ int maybe_setgroups(size_t size, const gid_t *list) { /* Check if setgroups is allowed before we try to drop all the auxiliary groups */ if (size == 0) { /* Dropping all aux groups? */ + + /* The kernel refuses setgroups() if there are no GID mappings in the current + * user namespace, so check that beforehand and don't try to setgroups() if + * there are no GID mappings. */ + _cleanup_fclose_ FILE *f = fopen("/proc/self/gid_map", "re"); + if (!f && errno != ENOENT) + return -errno; + if (f) { + r = safe_fgetc(f, /* ret= */ NULL); + if (r < 0) + return r; + if (r == 0) { + log_debug("Skipping setgroups(), /proc/self/gid_map is empty"); + return 0; + } + } + _cleanup_free_ char *setgroups_content = NULL; - bool can_setgroups; - r = read_one_line_file("/proc/self/setgroups", &setgroups_content); - if (r == -ENOENT) - /* Old kernels don't have /proc/self/setgroups, so assume we can use setgroups */ - can_setgroups = true; - else if (r < 0) + if (r < 0 && r != -ENOENT) return r; - else - can_setgroups = streq(setgroups_content, "allow"); - - if (!can_setgroups) { + if (r > 0 && streq(setgroups_content, "deny")) { log_debug("Skipping setgroups(), /proc/self/setgroups is set to 'deny'"); return 0; } From 211c3c9d41c18b63c9aba66d31d75ddf5448247a Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Sun, 15 Feb 2026 14:22:44 +0100 Subject: [PATCH 3/3] namespace-util: Merge namespace_enter_delegated() into namespace_enter() There's no need to pass in a boolean to decide whether we use namespace_enter_delegated() or not. Instead, we can just check if we have CAP_SYS_ADMIN in our own user namespace. If we don't, then we have to insist on a child user namespace being passed in and we have to enter it first to get CAP_SYS_ADMIN as without CAP_SYS_ADMIN we wouldn't be able to call setns() in the first place. If we do have CAP_SYS_ADMIN, we can always enter the other namespaces first before entering the user namespace. Additionally, we don't fail anymore if we can't reset the UID/GID since a root user might not always be available in every user namespace we might enter. --- src/basic/namespace-util.c | 103 +++++++++++++++------------------ src/basic/namespace-util.h | 1 - src/basic/process-util.c | 6 +- src/basic/process-util.h | 3 +- src/core/exec-credential.c | 1 - src/test/meson.build | 1 + src/test/test-namespace-util.c | 48 +++++++++++++++ 7 files changed, 97 insertions(+), 66 deletions(-) create mode 100644 src/test/test-namespace-util.c diff --git a/src/basic/namespace-util.c b/src/basic/namespace-util.c index 69bd4945df..6a73eed913 100644 --- a/src/basic/namespace-util.c +++ b/src/basic/namespace-util.c @@ -8,6 +8,7 @@ #include #include +#include "capability-util.h" #include "dlfcn-util.h" #include "errno-util.h" #include "fd-util.h" @@ -215,53 +216,6 @@ int namespace_open( return pidref_namespace_open(&pidref, ret_pidns_fd, ret_mntns_fd, ret_netns_fd, ret_userns_fd, ret_root_fd); } -int namespace_enter(int pidns_fd, int mntns_fd, int netns_fd, int userns_fd, int root_fd) { - int r; - - /* Block dlopen() now, to avoid us inadvertently loading shared library from another namespace */ - block_dlopen(); - - if (userns_fd >= 0) { - /* Can't setns to your own userns, since then you could escalate from non-root to root in - * your own namespace, so check if namespaces are equal before attempting to enter. */ - - r = is_our_namespace(userns_fd, NAMESPACE_USER); - if (r < 0) - return r; - if (r > 0) - userns_fd = -EBADF; - } - - if (pidns_fd >= 0) - if (setns(pidns_fd, CLONE_NEWPID) < 0) - return -errno; - - if (mntns_fd >= 0) - if (setns(mntns_fd, CLONE_NEWNS) < 0) - return -errno; - - if (netns_fd >= 0) - if (setns(netns_fd, CLONE_NEWNET) < 0) - return -errno; - - if (userns_fd >= 0) - if (setns(userns_fd, CLONE_NEWUSER) < 0) - return -errno; - - if (root_fd >= 0) { - if (fchdir(root_fd) < 0) - return -errno; - - if (chroot(".") < 0) - return -errno; - } - - if (userns_fd >= 0) - return reset_uid_gid(); - - return 0; -} - static int namespace_enter_one_idempotent(int nsfd, NamespaceType type) { int r; @@ -283,20 +237,43 @@ static int namespace_enter_one_idempotent(int nsfd, NamespaceType type) { return 1; } -int namespace_enter_delegated(int userns_fd, int pidns_fd, int mntns_fd, int netns_fd, int root_fd) { +int namespace_enter(int pidns_fd, int mntns_fd, int netns_fd, int userns_fd, int root_fd) { int r; - /* Similar to namespace_enter(), but operates on a set of namespaces that are potentially owned - * by the userns ("delegated"), in which case we'll need to gain CAP_SYS_ADMIN by joining - * the userns first, and the rest later. */ - - assert(userns_fd >= 0); - /* Block dlopen() now, to avoid us inadvertently loading shared library from another namespace */ block_dlopen(); - if (setns(userns_fd, CLONE_NEWUSER) < 0) - return -errno; + if (userns_fd >= 0) { + /* Can't setns to your own userns, since then you could escalate from non-root to root in + * your own namespace, so check if namespaces are equal before attempting to enter. */ + + r = is_our_namespace(userns_fd, NAMESPACE_USER); + if (r < 0) + return r; + if (r > 0) + userns_fd = -EBADF; + } + + r = have_effective_cap(CAP_SYS_ADMIN); + if (r < 0) + return r; + + bool have_cap_sys_admin = r > 0; + + if (!have_cap_sys_admin) { + /* If we don't have CAP_SYS_ADMIN in our own user namespace, our best bet is to enter the + * user namespace first (if we got one) to get CAP_SYS_ADMIN within the child user namespace, + * and then hope the other namespaces are owned by the child user namespace. If they aren't, + * we'll just get an EPERM later on when trying to setns() to them. */ + + if (userns_fd < 0) + return log_debug_errno( + SYNTHETIC_ERRNO(EPERM), + "Need CAP_SYS_ADMIN or a child user namespace to enter namespaces."); + + if (setns(userns_fd, CLONE_NEWUSER) < 0) + return -errno; + } if (pidns_fd >= 0) { r = namespace_enter_one_idempotent(pidns_fd, NAMESPACE_PID); @@ -316,6 +293,10 @@ int namespace_enter_delegated(int userns_fd, int pidns_fd, int mntns_fd, int net return r; } + if (userns_fd >= 0 && have_cap_sys_admin) + if (setns(userns_fd, CLONE_NEWUSER) < 0) + return -errno; + if (root_fd >= 0) { if (fchdir(root_fd) < 0) return -errno; @@ -324,7 +305,15 @@ int namespace_enter_delegated(int userns_fd, int pidns_fd, int mntns_fd, int net return -errno; } - return maybe_setgroups(/* size = */ 0, NULL); + if (userns_fd >= 0) { + /* Try to become root in the user namespace but don't error out if we can't, since it's not + * uncommon to have user namespaces without a root user in them. */ + r = reset_uid_gid(); + if (r < 0) + log_debug_errno(r, "Unable to drop auxiliary groups or reset UID/GID, ignoring: %m"); + } + + return 0; } int fd_is_namespace(int fd, NamespaceType type) { diff --git a/src/basic/namespace-util.h b/src/basic/namespace-util.h index 994125818f..32e64fa9e6 100644 --- a/src/basic/namespace-util.h +++ b/src/basic/namespace-util.h @@ -47,7 +47,6 @@ int namespace_open( int *ret_root_fd); int namespace_enter(int pidns_fd, int mntns_fd, int netns_fd, int userns_fd, int root_fd); -int namespace_enter_delegated(int userns_fd, int pidns_fd, int mntns_fd, int netns_fd, int root_fd); int fd_is_namespace(int fd, NamespaceType type); int is_our_namespace(int fd, NamespaceType type); diff --git a/src/basic/process-util.c b/src/basic/process-util.c index 52af6a01c8..9c44193b67 100644 --- a/src/basic/process-util.c +++ b/src/basic/process-util.c @@ -1778,7 +1778,6 @@ int namespace_fork_full( int netns_fd, int userns_fd, int root_fd, - bool delegated, PidRef *ret) { _cleanup_(pidref_done_sigkill_wait) PidRef pidref_outer = PIDREF_NULL; @@ -1824,10 +1823,7 @@ int namespace_fork_full( errno_pipe_fd[0] = safe_close(errno_pipe_fd[0]); - if (delegated) - r = namespace_enter_delegated(userns_fd, pidns_fd, mntns_fd, netns_fd, root_fd); - else - r = namespace_enter(pidns_fd, mntns_fd, netns_fd, userns_fd, root_fd); + r = namespace_enter(pidns_fd, mntns_fd, netns_fd, userns_fd, root_fd); if (r < 0) { log_full_errno(prio, r, "Failed to join namespace: %m"); report_errno_and_exit(errno_pipe_fd[1], r); diff --git a/src/basic/process-util.h b/src/basic/process-util.h index 66bb194bac..46a5612048 100644 --- a/src/basic/process-util.h +++ b/src/basic/process-util.h @@ -201,7 +201,6 @@ int namespace_fork_full( int netns_fd, int userns_fd, int root_fd, - bool delegated, PidRef *ret); static inline int namespace_fork( @@ -216,7 +215,7 @@ static inline int namespace_fork( PidRef *ret) { return namespace_fork_full(outer_name, inner_name, NULL, 0, flags, - pidns_fd, mntns_fd, netns_fd, userns_fd, root_fd, false, + pidns_fd, mntns_fd, netns_fd, userns_fd, root_fd, ret); } diff --git a/src/core/exec-credential.c b/src/core/exec-credential.c index 07d83ac7a6..c1b2fcda85 100644 --- a/src/core/exec-credential.c +++ b/src/core/exec-credential.c @@ -1255,7 +1255,6 @@ int unit_refresh_credentials(Unit *u) { (int[]) { tunnel_fds[1] }, 1, FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL|FORK_CLOSE_ALL_FDS|FORK_REOPEN_LOG, pidns_fd, mntns_fd, /* netns_fd = */ -EBADF, userns_fd, root_fd, - /* delegated = */ MANAGER_IS_USER(u->manager), &child); if (r < 0) return log_full_errno(ERRNO_IS_NEG_PRIVILEGE(r) ? LOG_WARNING : LOG_ERR, r, diff --git a/src/test/meson.build b/src/test/meson.build index c68477dc60..cd563654df 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -149,6 +149,7 @@ simple_tests += files( 'test-mkdir.c', 'test-modhex.c', 'test-mountpoint-util.c', + 'test-namespace-util.c', 'test-net-naming-scheme.c', 'test-notify-recv.c', 'test-nsresource.c', diff --git a/src/test/test-namespace-util.c b/src/test/test-namespace-util.c new file mode 100644 index 0000000000..ccc8e17cf9 --- /dev/null +++ b/src/test/test-namespace-util.c @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "capability-util.h" +#include "errno-util.h" +#include "pidref.h" +#include "process-util.h" +#include "fd-util.h" +#include "namespace-util.h" +#include "tests.h" + +TEST(namespace_enter) { + _cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL; + int r; + + r = pidref_safe_fork( + "test-ns-enter-1", + FORK_NEW_USERNS|FORK_NEW_MOUNTNS|FORK_LOG|FORK_FREEZE|FORK_DEATHSIG_SIGKILL, + &pidref); + if (ERRNO_IS_NEG_PRIVILEGE(r)) + return (void) log_tests_skipped_errno(r, "Unable to unshare user namespace"); + + ASSERT_OK(r); + + _cleanup_close_ int mntns_fd = -EBADF, userns_fd = -EBADF, root_fd = -EBADF; + ASSERT_OK(pidref_namespace_open(&pidref, NULL, &mntns_fd, NULL, &userns_fd, &root_fd)); + + r = ASSERT_OK(pidref_safe_fork( + "test-ns-enter-2", + FORK_LOG|FORK_WAIT|FORK_DEATHSIG_SIGKILL, + NULL)); + if (r == 0) { + ASSERT_OK(namespace_enter(-EBADF, mntns_fd, -EBADF, userns_fd, root_fd)); + _exit(EXIT_SUCCESS); + } + + /* Make sure we can enter the namespaces as well if we don't have CAP_SYS_ADMIN. */ + r = ASSERT_OK(pidref_safe_fork( + "test-ns-enter-3", + FORK_LOG|FORK_WAIT|FORK_DEATHSIG_SIGKILL, + NULL)); + if (r == 0) { + ASSERT_OK(drop_capability(CAP_SYS_ADMIN)); + ASSERT_OK(namespace_enter(-EBADF, mntns_fd, -EBADF, userns_fd, root_fd)); + _exit(EXIT_SUCCESS); + } +} + +DEFINE_TEST_MAIN(LOG_DEBUG);