diff --git a/src/basic/missing_fs.h b/src/basic/missing_fs.h index d97b190131..ed6bdf0426 100644 --- a/src/basic/missing_fs.h +++ b/src/basic/missing_fs.h @@ -95,10 +95,18 @@ assert_cc(MS_LAZYTIME == (1<<25)); #endif /* linux/nsfs.h */ -#ifndef NS_GET_NSTYPE /* d95fa3c76a66b6d76b1e109ea505c55e66360f3c (4.11) */ +#ifndef NS_GET_USERNS /* 6786741dbf99e44fb0c0ed85a37582b8a26f1c3b (4.9) */ +#define NS_GET_USERNS _IO(0xb7, 0x1) +#endif + +#ifndef NS_GET_NSTYPE /* e5ff5ce6e20ee22511398bb31fb912466cf82a36 (4.11) */ #define NS_GET_NSTYPE _IO(0xb7, 0x3) #endif +#ifndef NS_GET_OWNER_UID /* d95fa3c76a66b6d76b1e109ea505c55e66360f3c (4.11) */ +#define NS_GET_OWNER_UID _IO(0xb7, 0x4) +#endif + #ifndef FS_PROJINHERIT_FL # define FS_PROJINHERIT_FL 0x20000000 #else diff --git a/src/basic/namespace-util.c b/src/basic/namespace-util.c index 9293999a01..c7b1bcca2d 100644 --- a/src/basic/namespace-util.c +++ b/src/basic/namespace-util.c @@ -22,6 +22,7 @@ #include "process-util.h" #include "stat-util.h" #include "stdio-util.h" +#include "uid-range.h" #include "user-util.h" const struct namespace_info namespace_info[_NAMESPACE_TYPE_MAX + 1] = { @@ -71,13 +72,19 @@ static int pidref_namespace_open_by_type_internal(const PidRef *pidref, Namespac const char *p; p = pid_namespace_path(pidref->pid, type); - nsfd = open(p, O_RDONLY|O_NOCTTY|O_CLOEXEC); - if (nsfd < 0) { - if (errno == ENOENT && proc_mounted() == 0) - return -ENOSYS; + nsfd = RET_NERRNO(open(p, O_RDONLY|O_NOCTTY|O_CLOEXEC)); + if (nsfd == -ENOENT) { + r = proc_mounted(); + if (r == 0) + return -ENOSYS; /* /proc/ is not available or not set up properly, we're most likely + in some chroot environment. */ + if (r > 0) + return -ENOPKG; /* If /proc/ is definitely around then this means the namespace type is not supported */ - return -errno; + /* can't determine? then propagate original error */ } + if (nsfd < 0) + return nsfd; if (!need_verify) { /* Otherwise we verify on our own */ r = pidref_verify(pidref); @@ -143,7 +150,7 @@ int pidref_namespace_open( if (ret_userns_fd) { userns_fd = pidref_namespace_open_by_type_internal(pidref, NAMESPACE_USER, &need_verify); - if (userns_fd < 0 && !IN_SET(userns_fd, -ENOENT, -ENOPKG)) + if (userns_fd < 0 && userns_fd != -ENOPKG) return userns_fd; } @@ -301,9 +308,14 @@ int namespace_is_init(NamespaceType type) { struct stat st; r = RET_NERRNO(stat(p, &st)); - if (r == -ENOENT) + if (r == -ENOENT) { /* If the /proc/ns/ API is not around in /proc/ then ns is off in the kernel and we are in the init ns */ - return proc_mounted() == 0 ? -ENOSYS : true; + r = proc_mounted(); + if (r < 0) + return -ENOENT; /* If we can't determine if /proc/ is mounted propagate original error */ + + return r ? true : -ENOSYS; + } if (r < 0) return r; @@ -625,3 +637,129 @@ int is_idmapping_supported(const char *path) { return true; } + +int userns_get_base_uid(int userns_fd, uid_t *ret_uid, gid_t *ret_gid) { + _cleanup_(close_pairp) int pfd[2] = EBADF_PAIR; + _cleanup_(sigkill_waitp) pid_t pid = 0; + ssize_t n; + char x; + int r; + + assert(userns_fd >= 0); + + if (pipe2(pfd, O_CLOEXEC) < 0) + return -errno; + + r = safe_fork_full( + "(sd-baseuns)", + /* stdio_fds= */ NULL, + (int[]) { pfd[1], userns_fd }, 2, + FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGKILL, + &pid); + if (r < 0) + return r; + if (r == 0) { + /* Child. */ + + if (setns(userns_fd, CLONE_NEWUSER) < 0) { + log_debug_errno(errno, "Failed to join userns: %m"); + _exit(EXIT_FAILURE); + } + + userns_fd = safe_close(userns_fd); + + n = write(pfd[1], &(const char) { 'x' }, 1); + if (n < 0) { + log_debug_errno(errno, "Failed to write to pipe: %m"); + _exit(EXIT_FAILURE); + } + assert(n == 1); + + freeze(); + } + + pfd[1] = safe_close(pfd[1]); + + n = read(pfd[0], &x, 1); + if (n < 0) + return -errno; + if (n == 0) + return -EPROTO; + assert(n == 1); + assert(x == 'x'); + + uid_t uid; + r = uid_map_search_root(pid, "uid_map", &uid); + if (r < 0) + return r; + + gid_t gid; + r = uid_map_search_root(pid, "gid_map", &gid); + if (r < 0) + return r; + + if (!ret_gid && uid != gid) + return -EUCLEAN; + + if (ret_uid) + *ret_uid = uid; + if (ret_gid) + *ret_gid = gid; + + return 0; +} + +int process_is_owned_by_uid(const PidRef *pidref, uid_t uid) { + assert(uid_is_valid(uid)); + + int r; + + /* Checks if the specified process either is owned directly by the specified user, or if it is inside + * a user namespace owned by it. */ + + uid_t process_uid; + r = pidref_get_uid(pidref, &process_uid); + if (r < 0) + return r; + if (process_uid == uid) + return true; + + _cleanup_close_ int userns_fd = -EBADF; + userns_fd = pidref_namespace_open_by_type(pidref, NAMESPACE_USER); + if (userns_fd == -ENOPKG) /* If userns is not supported, then they don't matter for ownership */ + return false; + if (userns_fd < 0) + return userns_fd; + + for (unsigned iteration = 0;; iteration++) { + uid_t ns_uid; + + /* This process is in our own userns? Then we are done, in our own userns only the UIDs + * themselves matter. */ + r = is_our_namespace(userns_fd, NAMESPACE_USER); + if (r < 0) + return r; + if (r > 0) + return false; + + if (ioctl(userns_fd, NS_GET_OWNER_UID, &ns_uid) < 0) + return -errno; + if (ns_uid == uid) + return true; + + /* Paranoia check */ + if (iteration > 16) + return log_debug_errno(SYNTHETIC_ERRNO(ELOOP), "Giving up while tracing parents of user namespaces after %u steps.", iteration); + + /* Go up the tree */ + _cleanup_close_ int parent_fd = ioctl(userns_fd, NS_GET_USERNS); + if (parent_fd < 0) { + if (errno == EPERM) /* EPERM means we left our own userns */ + return false; + + return -errno; + } + + close_and_replace(userns_fd, parent_fd); + } +} diff --git a/src/basic/namespace-util.h b/src/basic/namespace-util.h index 319efec4ac..d1ac4f6ed4 100644 --- a/src/basic/namespace-util.h +++ b/src/basic/namespace-util.h @@ -91,3 +91,7 @@ int netns_acquire(void); int parse_userns_uid_range(const char *s, uid_t *ret_uid_shift, uid_t *ret_uid_range); int is_idmapping_supported(const char *path); + +int userns_get_base_uid(int userns_fd, uid_t *ret_uid, gid_t *ret_gid); + +int process_is_owned_by_uid(const PidRef *pidref, uid_t uid); diff --git a/src/basic/uid-range.c b/src/basic/uid-range.c index a7658811ec..687ddacc57 100644 --- a/src/basic/uid-range.c +++ b/src/basic/uid-range.c @@ -186,9 +186,6 @@ int uid_map_read_one(FILE *f, uid_t *ret_base, uid_t *ret_shift, uid_t *ret_rang int r; assert(f); - assert(ret_base); - assert(ret_shift); - assert(ret_range); errno = 0; r = fscanf(f, UID_FMT " " UID_FMT " " UID_FMT "\n", &uid_base, &uid_shift, &uid_range); @@ -197,10 +194,15 @@ int uid_map_read_one(FILE *f, uid_t *ret_base, uid_t *ret_shift, uid_t *ret_rang assert(r >= 0); if (r != 3) return -EBADMSG; + if (uid_range <= 0) + return -EBADMSG; - *ret_base = uid_base; - *ret_shift = uid_shift; - *ret_range = uid_range; + if (ret_base) + *ret_base = uid_base; + if (ret_shift) + *ret_shift = uid_shift; + if (ret_range) + *ret_range = uid_range; return 0; } @@ -360,3 +362,37 @@ bool uid_range_equal(const UIDRange *a, const UIDRange *b) { return true; } + +int uid_map_search_root(pid_t pid, const char *filename, uid_t *ret) { + int r; + + assert(pid_is_valid(pid)); + assert(filename); + + const char *p = procfs_file_alloca(pid, filename); + _cleanup_fclose_ FILE *f = fopen(p, "re"); + if (!f) { + if (errno != ENOENT) + return -errno; + + r = proc_mounted(); + if (r < 0) + return -ENOENT; /* original error, if we can't determine /proc/ state */ + + return r ? -ENOPKG : -ENOSYS; + } + + for (;;) { + uid_t uid_base = UID_INVALID, uid_shift = UID_INVALID; + + r = uid_map_read_one(f, &uid_base, &uid_shift, /* ret_uid_range= */ NULL); + if (r < 0) + return r; + + if (uid_base == 0) { + if (ret) + *ret = uid_shift; + return 0; + } + } +} diff --git a/src/basic/uid-range.h b/src/basic/uid-range.h index 1f687b2713..41ca671dd6 100644 --- a/src/basic/uid-range.h +++ b/src/basic/uid-range.h @@ -76,3 +76,5 @@ int uid_range_load_userns(const char *path, UIDRangeUsernsMode mode, UIDRange ** int uid_range_load_userns_by_fd(int userns_fd, UIDRangeUsernsMode mode, UIDRange **ret); bool uid_range_overlaps(const UIDRange *range, uid_t start, uid_t nr); + +int uid_map_search_root(pid_t pid, const char *filename, uid_t *ret); diff --git a/src/test/test-namespace.c b/src/test/test-namespace.c index d646306acf..46350b078d 100644 --- a/src/test/test-namespace.c +++ b/src/test/test-namespace.c @@ -7,10 +7,12 @@ #include "alloc-util.h" #include "fd-util.h" +#include "fileio.h" #include "namespace.h" #include "process-util.h" #include "string-util.h" #include "tests.h" +#include "uid-range.h" #include "user-util.h" #include "virt.h" @@ -172,7 +174,7 @@ TEST(fd_is_namespace) { ASSERT_OK_ZERO(fd_is_namespace(STDERR_FILENO, NAMESPACE_NET)); fd = namespace_open_by_type(NAMESPACE_MOUNT); - if (IN_SET(fd, -ENOSYS, -ENOENT)) { + if (IN_SET(fd, -ENOSYS, -ENOPKG)) { log_notice("Path %s not found, skipping test", "/proc/self/ns/mnt"); return; } @@ -252,6 +254,117 @@ TEST(namespace_is_init) { } } +TEST(userns_get_base_uid) { + _cleanup_close_ int fd = -EBADF; + + fd = userns_acquire("0 1 1", "0 2 1"); + if (ERRNO_IS_NEG_NOT_SUPPORTED(fd)) + return (void) log_tests_skipped("userns is not supported"); + if (ERRNO_IS_NEG_PRIVILEGE(fd)) + return (void) log_tests_skipped("lacking userns privileges"); + + uid_t base_uid, base_gid; + ASSERT_OK(userns_get_base_uid(fd, &base_uid, &base_gid)); + ASSERT_EQ(base_uid, 1U); + ASSERT_EQ(base_gid, 2U); + + ASSERT_ERROR(userns_get_base_uid(fd, &base_uid, NULL), EUCLEAN); + + fd = safe_close(fd); + + fd = userns_acquire_empty(); + ASSERT_OK(fd); + + ASSERT_ERROR(userns_get_base_uid(fd, &base_uid, &base_gid), ENOMSG); +} + +TEST(process_is_owned_by_uid) { + int r; + + /* Test our own PID */ + _cleanup_(pidref_done) PidRef pid = PIDREF_NULL; + ASSERT_OK(pidref_set_self(&pid)); + ASSERT_OK_POSITIVE(process_is_owned_by_uid(&pid, getuid())); + pidref_done(&pid); + + if (getuid() != 0) + return (void) log_tests_skipped("lacking userns privileges"); + + _cleanup_(uid_range_freep) UIDRange *range = NULL; + ASSERT_OK(uid_range_load_userns(/* path= */ NULL, UID_RANGE_USERNS_INSIDE, &range)); + if (!uid_range_contains(range, 1)) + return (void) log_tests_skipped("UID 1 not included in userns UID delegation, skipping test"); + + /* Test a child that runs as uid 1 */ + _cleanup_close_pair_ int p[2] = EBADF_PAIR; + ASSERT_OK_ERRNO(pipe2(p, O_CLOEXEC)); + + r = pidref_safe_fork("(child)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL, &pid); + ASSERT_OK(r); + if (r == 0) { + p[0] = safe_close(p[0]); + ASSERT_OK(fully_set_uid_gid(1, 1, NULL, 0)); + ASSERT_OK_EQ_ERRNO(write(p[1], &(const char[]) { 'x' }, 1), 1); + p[1] = safe_close(p[1]); + freeze(); + } + + p[1] = safe_close(p[1]); + char x = 0; + ASSERT_OK_EQ_ERRNO(read(p[0], &x, 1), 1); + ASSERT_EQ(x, 'x'); + p[0] = safe_close(p[0]); + + ASSERT_OK_ZERO(process_is_owned_by_uid(&pid, getuid())); + + ASSERT_OK(pidref_kill(&pid, SIGKILL)); + ASSERT_OK(pidref_wait_for_terminate(&pid, /* ret= */ NULL)); + + /* Test a child that runs in a userns as uid 1, but the userns is owned by us */ + ASSERT_OK_ERRNO(pipe2(p, O_CLOEXEC)); + + _cleanup_close_pair_ int pp[2] = EBADF_PAIR; + ASSERT_OK_ERRNO(pipe2(pp, O_CLOEXEC)); + + r = pidref_safe_fork("(child)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL|FORK_NEW_USERNS, &pid); + ASSERT_OK(r); + if (r == 0) { + p[0] = safe_close(p[0]); + pp[1] = safe_close(pp[1]); + + x = 0; + ASSERT_OK_EQ_ERRNO(read(pp[0], &x, 1), 1); + ASSERT_EQ(x, 'x'); + pp[0] = safe_close(pp[0]); + + ASSERT_OK(reset_uid_gid()); + + ASSERT_OK_EQ_ERRNO(write(p[1], &(const char[]) { 'x' }, 1), 1); + p[1] = safe_close(p[1]); + freeze(); + } + + p[1] = safe_close(p[1]); + pp[0] = safe_close(pp[0]); + + ASSERT_OK(write_string_file(procfs_file_alloca(pid.pid, "uid_map"), "0 1 1\n", 0)); + ASSERT_OK(write_string_file(procfs_file_alloca(pid.pid, "setgroups"), "deny", 0)); + ASSERT_OK(write_string_file(procfs_file_alloca(pid.pid, "gid_map"), "0 1 1\n", 0)); + + ASSERT_OK_EQ_ERRNO(write(pp[1], &(const char[]) { 'x' }, 1), 1); + pp[1] = safe_close(pp[1]); + + x = 0; + ASSERT_OK_EQ_ERRNO(read(p[0], &x, 1), 1); + ASSERT_EQ(x, 'x'); + p[0] = safe_close(p[0]); + + ASSERT_OK_POSITIVE(process_is_owned_by_uid(&pid, getuid())); + + ASSERT_OK(pidref_kill(&pid, SIGKILL)); + ASSERT_OK(pidref_wait_for_terminate(&pid, /* ret= */ NULL)); +} + static int intro(void) { if (!have_namespaces()) return log_tests_skipped("Don't have namespace support or lacking privileges");