From cfd2f7c73e21d6890b3a5b26203a9ba69e690c26 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 15 Sep 2026 17:26:49 +0200 Subject: [PATCH] namespace-util: fail if we cannot become root when entering a userns namespace_enter() tried to become root after joining a user namespace but ignored failures, so if root wasn't mapped we'd just carry on with our original UID/GID. Let's make that an error instead. Dropping supplementary groups stays best effort, as setgroups() might be denied (e.g. by the nsresourced BPF LSM) even when root is mapped. Some callers do need to enter namespaces without root, so add NAMESPACE_ENTER_KEEP_UID_GID_IF_ROOT_UNMAPPED for those. It's used by systemd-analyze unit-shell and by the credential refresh in the user manager. Everything else enters containers or namespaces we allocated ourselves, where root is always mapped. --- src/analyze/analyze-unit-shell.c | 7 +- src/basic/forward.h | 1 + src/basic/namespace-util.c | 75 +++++++++++++-- src/basic/namespace-util.h | 21 ++++- src/basic/process-util.c | 19 +++- src/basic/process-util.h | 10 +- src/core/exec-credential.c | 4 + src/nsresourced/test-userns-restrict.c | 10 +- src/test/test-namespace-util.c | 124 +++++++++++++++++++++++-- 9 files changed, 239 insertions(+), 32 deletions(-) diff --git a/src/analyze/analyze-unit-shell.c b/src/analyze/analyze-unit-shell.c index 98dc474e62..b28faf2f1c 100644 --- a/src/analyze/analyze-unit-shell.c +++ b/src/analyze/analyze-unit-shell.c @@ -80,15 +80,20 @@ int verb_unit_shell(int argc, char *argv[], uintptr_t _data, void *userdata) { } _cleanup_(pidref_done) PidRef child = PIDREF_NULL; - r = namespace_fork( + r = namespace_fork_full( "(unit-shell-ns)", "(unit-shell)", + /* except_fds= */ NULL, /* n_except_fds= */ 0, FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL, pidns_fd, mntns_fd, netns_fd, userns_fd, root_fd, + /* We usually become root in the unit's user namespace. If it doesn't map root, + * e.g. for user units or with UserNamespacePath=, still provide a shell, as + * ourselves. */ + NAMESPACE_ENTER_KEEP_UID_GID_IF_ROOT_UNMAPPED, &child); if (r < 0) return log_error_errno(r, "Failed to fork and enter the namespace of %s: %m", unit); diff --git a/src/basic/forward.h b/src/basic/forward.h index 383cd5dbd2..52afa29852 100644 --- a/src/basic/forward.h +++ b/src/basic/forward.h @@ -104,6 +104,7 @@ typedef enum ForkFlags ForkFlags; typedef enum Glyph Glyph; typedef enum ImageClass ImageClass; typedef enum JobMode JobMode; +typedef enum NamespaceEnterFlags NamespaceEnterFlags; typedef enum RuntimeScope RuntimeScope; typedef enum StringSafeFlags StringSafeFlags; typedef enum TimestampStyle TimestampStyle; diff --git a/src/basic/namespace-util.c b/src/basic/namespace-util.c index 72e7005c94..c103df5c00 100644 --- a/src/basic/namespace-util.c +++ b/src/basic/namespace-util.c @@ -230,7 +230,14 @@ 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 namespace_enter_full( + int pidns_fd, + int mntns_fd, + int netns_fd, + int userns_fd, + int root_fd, + NamespaceEnterFlags flags) { + int r; /* Block dlopen() now, to avoid us inadvertently loading shared library from another namespace */ @@ -274,6 +281,15 @@ int namespace_enter(int pidns_fd, int mntns_fd, int netns_fd, int userns_fd, int userns_fd = -EBADF; } + /* Determine this before joining anything, as /proc might not be available afterwards. */ + bool keep_uid_gid = false; + if (userns_fd >= 0 && FLAGS_SET(flags, NAMESPACE_ENTER_KEEP_UID_GID_IF_ROOT_UNMAPPED)) { + r = userns_has_root_mapping(userns_fd); + if (r < 0) + return r; + keep_uid_gid = r == 0; + } + r = have_effective_cap(CAP_SYS_ADMIN); if (r < 0) return r; @@ -319,13 +335,26 @@ int namespace_enter(int pidns_fd, int mntns_fd, int netns_fd, int userns_fd, int return -errno; } - 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"); - } + if (userns_fd < 0) + return 0; + + /* Drop supplementary groups where possible. setgroups() may be denied, either via + * /proc/self/setgroups or by policy (e.g. a BPF LSM), in which case there's nothing we can do + * about them. */ + r = maybe_setgroups(/* size= */ 0, /* list= */ NULL); + if (r < 0) + log_debug_errno(r, "Unable to drop supplementary groups, ignoring: %m"); + + if (keep_uid_gid) + return 0; + + /* Joining a user namespace preserves the underlying UID/GID. Require becoming root to succeed, as + * otherwise we'd continue running with our original identity inside the namespace. */ + if (setresgid(0, 0, 0) < 0) + return log_debug_errno(errno, "Failed to become root group in user namespace: %m"); + + if (setresuid(0, 0, 0) < 0) + return log_debug_errno(errno, "Failed to become root in user namespace: %m"); return 0; } @@ -831,6 +860,36 @@ int userns_get_base_uid(int userns_fd, uid_t *ret_uid, gid_t *ret_gid) { return 0; } +int userns_has_root_mapping(int userns_fd) { + _cleanup_(uid_range_freep) UIDRange *uid_range = NULL, *gid_range = NULL; + _cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL; + int r; + + assert(userns_fd >= 0); + + r = userns_enter_and_pin(userns_fd, &pidref); + if (r < 0) + return r; + + const char *p = procfs_file_alloca(pidref.pid, "uid_map"); + r = uid_range_load_userns(p, UID_RANGE_USERNS_INSIDE, &uid_range); + if (r < 0) + return r; + + p = procfs_file_alloca(pidref.pid, "gid_map"); + r = uid_range_load_userns(p, GID_RANGE_USERNS_INSIDE, &gid_range); + if (r < 0) + return r; + + /* The maps can be written only once. While they are empty the namespace is still being set up and + * root might yet get mapped, so refuse to answer rather than guess. */ + if (uid_range_is_empty(uid_range) || uid_range_is_empty(gid_range)) + return log_debug_errno(SYNTHETIC_ERRNO(ENODATA), + "User namespace has no UID/GID mapping yet."); + + return uid_range_contains(uid_range, 0) && uid_range_contains(gid_range, 0); +} + int process_is_owned_by_uid(const PidRef *pidref, uid_t uid) { int r; diff --git a/src/basic/namespace-util.h b/src/basic/namespace-util.h index d0eff636a9..5cff7595fe 100644 --- a/src/basic/namespace-util.h +++ b/src/basic/namespace-util.h @@ -46,7 +46,25 @@ int namespace_open( int *ret_userns_fd, int *ret_root_fd); -int namespace_enter(int pidns_fd, int mntns_fd, int netns_fd, int userns_fd, int root_fd); +typedef enum NamespaceEnterFlags { + /* If root is not mapped in the user namespace, keep our UID/GID instead of failing. The caller must + * ensure that retaining its original identity is appropriate in that case. Entering still fails + * with -ENODATA if the namespace has no mappings at all yet, since root might still get mapped. */ + NAMESPACE_ENTER_KEEP_UID_GID_IF_ROOT_UNMAPPED = 1 << 0, +} NamespaceEnterFlags; + +/* If a user namespace is entered we become root in it, and fail if that's not possible. */ +int namespace_enter_full( + int pidns_fd, + int mntns_fd, + int netns_fd, + int userns_fd, + int root_fd, + NamespaceEnterFlags flags); + +static inline int namespace_enter(int pidns_fd, int mntns_fd, int netns_fd, int userns_fd, int root_fd) { + return namespace_enter_full(pidns_fd, mntns_fd, netns_fd, userns_fd, root_fd, /* flags= */ 0); +} int fd_is_namespace(int fd, NamespaceType type); int is_our_namespace(int fd, NamespaceType type); @@ -88,6 +106,7 @@ int userns_enter_and_pin(int userns_fd, PidRef *ret); bool userns_supported(void); int userns_get_base_uid(int userns_fd, uid_t *ret_uid, gid_t *ret_gid); +int userns_has_root_mapping(int userns_fd); int namespace_open_by_id(uint64_t ns_id); diff --git a/src/basic/process-util.c b/src/basic/process-util.c index 133acc62b2..9790e4b130 100644 --- a/src/basic/process-util.c +++ b/src/basic/process-util.c @@ -1802,6 +1802,7 @@ int namespace_fork_full( int netns_fd, int userns_fd, int root_fd, + NamespaceEnterFlags namespace_flags, PidRef *ret) { _cleanup_(pidref_done_sigkill_wait) PidRef pidref_outer = PIDREF_NULL; @@ -1847,7 +1848,7 @@ int namespace_fork_full( errno_pipe_fd[0] = safe_close(errno_pipe_fd[0]); - r = namespace_enter(pidns_fd, mntns_fd, netns_fd, userns_fd, root_fd); + r = namespace_enter_full(pidns_fd, mntns_fd, netns_fd, userns_fd, root_fd, namespace_flags); if (r < 0) { log_full_errno(prio, r, "Failed to join namespace: %m"); report_errno_and_exit(errno_pipe_fd[1], r); @@ -1907,6 +1908,22 @@ int namespace_fork_full( return 1; } +int namespace_fork( + const char *outer_name, + const char *inner_name, + ForkFlags flags, + int pidns_fd, + int mntns_fd, + int netns_fd, + int userns_fd, + int root_fd, + PidRef *ret) { + + return namespace_fork_full(outer_name, inner_name, /* except_fds= */ NULL, /* n_except_fds= */ 0, flags, + pidns_fd, mntns_fd, netns_fd, userns_fd, root_fd, + /* namespace_flags= */ 0, ret); +} + bool oom_score_adjust_is_valid(int oa) { return oa >= OOM_SCORE_ADJ_MIN && oa <= OOM_SCORE_ADJ_MAX; } diff --git a/src/basic/process-util.h b/src/basic/process-util.h index 1d67ee86a0..4edaa7d215 100644 --- a/src/basic/process-util.h +++ b/src/basic/process-util.h @@ -201,9 +201,10 @@ int namespace_fork_full( int netns_fd, int userns_fd, int root_fd, + NamespaceEnterFlags namespace_flags, PidRef *ret); -static inline int namespace_fork( +int namespace_fork( const char *outer_name, const char *inner_name, ForkFlags flags, @@ -212,12 +213,7 @@ static inline int namespace_fork( int netns_fd, int userns_fd, int root_fd, - PidRef *ret) { - - return namespace_fork_full(outer_name, inner_name, NULL, 0, flags, - pidns_fd, mntns_fd, netns_fd, userns_fd, root_fd, - ret); -} + PidRef *ret); bool oom_score_adjust_is_valid(int oa); int set_oom_score_adjust(int value); diff --git a/src/core/exec-credential.c b/src/core/exec-credential.c index 5b6bd943cd..9c209404d3 100644 --- a/src/core/exec-credential.c +++ b/src/core/exec-credential.c @@ -1255,10 +1255,14 @@ int unit_refresh_credentials(Unit *u) { if (socketpair(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC, 0, tunnel_fds) < 0) return log_error_errno(errno, "Failed to allocate socket pair: %m"); + /* User units with PrivateUsers=self only map the user manager's own identity. Keep it + * in that case, but still become root when a managed UID/GID range is mapped. */ r = namespace_fork_full("(sd-creds-ns)", "(sd-creds-ns-inner)", (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, + MANAGER_IS_USER(u->manager) ? + NAMESPACE_ENTER_KEEP_UID_GID_IF_ROOT_UNMAPPED : 0, &child); if (r < 0) return log_full_errno(ERRNO_IS_NEG_PRIVILEGE(r) ? LOG_WARNING : LOG_ERR, r, diff --git a/src/nsresourced/test-userns-restrict.c b/src/nsresourced/test-userns-restrict.c index 69e70eabaf..28b9251c01 100644 --- a/src/nsresourced/test-userns-restrict.c +++ b/src/nsresourced/test-userns-restrict.c @@ -399,14 +399,8 @@ TEST(setgroups_deny) { r = ASSERT_OK(pidref_safe_fork("(test-deny)", FORK_LOG|FORK_DEATHSIG_SIGKILL, &pidref)); if (r == 0) { - /* Enter the userns manually without going through namespace_enter(), because - * that calls reset_uid_gid() which calls setgroups() internally. Since the - * BPF LSM denies setgroups(), reset_uid_gid() would fail before calling - * setresuid()/setresgid(), leaving us as the overflow UID without - * capabilities. */ - ASSERT_OK_ERRNO(setns(deny_userns_fd, CLONE_NEWUSER)); - ASSERT_OK_ERRNO(setresgid(0, 0, 0)); - ASSERT_OK_ERRNO(setresuid(0, 0, 0)); + /* Entering must work even though the BPF LSM denies the setgroups() involved. */ + ASSERT_OK(namespace_enter(-EBADF, -EBADF, -EBADF, deny_userns_fd, -EBADF)); /* setgroups() should be denied by BPF LSM */ ASSERT_ERROR_ERRNO(setgroups(0, NULL), EPERM); diff --git a/src/test/test-namespace-util.c b/src/test/test-namespace-util.c index ccc8e17cf9..b05bbe08b9 100644 --- a/src/test/test-namespace-util.c +++ b/src/test/test-namespace-util.c @@ -1,11 +1,17 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include +#include + +#include "alloc-util.h" #include "capability-util.h" #include "errno-util.h" +#include "fd-util.h" +#include "format-util.h" +#include "namespace-util.h" #include "pidref.h" #include "process-util.h" -#include "fd-util.h" -#include "namespace-util.h" +#include "socket-util.h" #include "tests.h" TEST(namespace_enter) { @@ -16,7 +22,7 @@ TEST(namespace_enter) { "test-ns-enter-1", FORK_NEW_USERNS|FORK_NEW_MOUNTNS|FORK_LOG|FORK_FREEZE|FORK_DEATHSIG_SIGKILL, &pidref); - if (ERRNO_IS_NEG_PRIVILEGE(r)) + if (ERRNO_IS_NEG_PRIVILEGE(r) || ERRNO_IS_NEG_NOT_SUPPORTED(r)) return (void) log_tests_skipped_errno(r, "Unable to unshare user namespace"); ASSERT_OK(r); @@ -24,25 +30,131 @@ TEST(namespace_enter) { _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)); + /* Without any mapping the namespace is still being set up, we can't tell yet if root is mapped. */ + ASSERT_ERROR(userns_has_root_mapping(userns_fd), ENODATA); + + /* The user namespace maps nothing, hence becoming root in it must fail, with or without CAP_SYS_ADMIN. */ 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)); + ASSERT_ERROR(namespace_enter(-EBADF, mntns_fd, -EBADF, userns_fd, root_fd), EINVAL); _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)); + ASSERT_ERROR(namespace_enter(-EBADF, mntns_fd, -EBADF, userns_fd, root_fd), EINVAL); _exit(EXIT_SUCCESS); } } +TEST(namespace_enter_mapped) { + static const struct { + uid_t uid; + gid_t gid; + NamespaceEnterFlags flags; + int error; + } cases[] = { + /* Root is mapped: we become root. */ + { 0, 0, 0, 0 }, + /* Root is not mapped: entering fails instead of silently keeping our identity. */ + { 1, 1, 0, EINVAL }, + /* Root is mapped: the flag has no effect, we still become root. */ + { 0, 0, NAMESPACE_ENTER_KEEP_UID_GID_IF_ROOT_UNMAPPED, 0 }, + /* Root is not mapped: the flag lets us enter and keep our identity. */ + { 1, 1, NAMESPACE_ENTER_KEEP_UID_GID_IF_ROOT_UNMAPPED, 0 }, + }; + + FOREACH_ELEMENT(c, cases) { + _cleanup_free_ char *uid_map = NULL, *gid_map = NULL; + _cleanup_close_ int userns_fd = -EBADF; + int r; + + /* Map ourselves either to root or to a nonzero identity. This also works unprivileged. */ + ASSERT_OK(asprintf(&uid_map, UID_FMT " " UID_FMT " 1", c->uid, getuid())); + ASSERT_OK(asprintf(&gid_map, GID_FMT " " GID_FMT " 1", c->gid, getgid())); + + userns_fd = userns_acquire(uid_map, gid_map, /* setgroups_deny= */ true); + if (ERRNO_IS_NEG_PRIVILEGE(userns_fd) || ERRNO_IS_NEG_NOT_SUPPORTED(userns_fd)) + return (void) log_tests_skipped_errno(userns_fd, "Unable to acquire user namespace"); + ASSERT_OK(userns_fd); + + r = namespace_fork_full( + "test-ns-mapped", "test-ns-mapped-inner", + /* except_fds= */ NULL, /* n_except_fds= */ 0, + FORK_LOG|FORK_WAIT|FORK_DEATHSIG_SIGKILL, + /* pidns_fd= */ -EBADF, /* mntns_fd= */ -EBADF, /* netns_fd= */ -EBADF, + userns_fd, /* root_fd= */ -EBADF, c->flags, /* ret= */ NULL); + if (r == 0) { + uid_t ruid, euid, suid; + gid_t rgid, egid, sgid; + + ASSERT_EQ(c->error, 0); + ASSERT_OK_ERRNO(getresuid(&ruid, &euid, &suid)); + ASSERT_EQ(ruid, c->uid); + ASSERT_EQ(euid, c->uid); + ASSERT_EQ(suid, c->uid); + ASSERT_OK_ERRNO(getresgid(&rgid, &egid, &sgid)); + ASSERT_EQ(rgid, c->gid); + ASSERT_EQ(egid, c->gid); + ASSERT_EQ(sgid, c->gid); + + _exit(EXIT_SUCCESS); + } + + if (c->error > 0) + ASSERT_ERROR(r, c->error); + else + ASSERT_OK(r); + } +} + +TEST(namespace_enter_peercred) { + _cleanup_close_pair_ int transport[2] = EBADF_PAIR; + _cleanup_close_ int userns_fd = -EBADF, fd = -EBADF; + struct ucred ucred; + int r; + + /* Map root to identities different from ours, so the parent can verify that the reset changed the + * underlying credentials. Unlike the other cases, configuring these mappings requires privileges. */ + uid_t uid = getuid() == 1 ? 2 : 1; + gid_t gid = getgid() == 2 ? 1 : 2; + _cleanup_free_ char *uid_map = NULL, *gid_map = NULL; + ASSERT_OK(asprintf(&uid_map, "0 " UID_FMT " 1", uid)); + ASSERT_OK(asprintf(&gid_map, "0 " GID_FMT " 1", gid)); + + userns_fd = userns_acquire(uid_map, gid_map, /* setgroups_deny= */ true); + if (ERRNO_IS_NEG_PRIVILEGE(userns_fd) || ERRNO_IS_NEG_NOT_SUPPORTED(userns_fd)) + return (void) log_tests_skipped_errno(userns_fd, "Unable to map other UID/GID into user namespace"); + ASSERT_OK(userns_fd); + + ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0, transport)); + + r = ASSERT_OK(namespace_fork( + "test-ns-peercred", "test-ns-peercred-inner", + FORK_LOG|FORK_WAIT|FORK_DEATHSIG_SIGKILL, + /* pidns_fd= */ -EBADF, /* mntns_fd= */ -EBADF, /* netns_fd= */ -EBADF, + userns_fd, /* root_fd= */ -EBADF, /* ret= */ NULL)); + if (r == 0) { + _cleanup_close_pair_ int pair[2] = EBADF_PAIR; + + /* Capture credentials after namespace entry, then let the parent inspect them from outside. */ + ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, pair)); + ASSERT_OK(send_one_fd(transport[1], pair[0], 0)); + _exit(EXIT_SUCCESS); + } + + transport[1] = safe_close(transport[1]); + fd = ASSERT_OK(receive_one_fd(transport[0], 0)); + ASSERT_OK(getpeercred(fd, &ucred)); + ASSERT_EQ(ucred.uid, uid); + ASSERT_EQ(ucred.gid, gid); +} + DEFINE_TEST_MAIN(LOG_DEBUG);