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);