From adcc8059292edc0b3bcb5b87a387e5d85cedee32 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 7 Jan 2025 10:53:01 +0100 Subject: [PATCH 1/4] namespace-util: return recognizable error if namespace_open_by_type() fails because ns type not supported This makes sure the the codepath that derives an nsfd from a pid works the same for the pidfd case and the non-pidfd case: if we can verify that /proc/ is mounted but the /proc/$PID/ns/ files are missing, we can assume the ns type is not supported by the kernel. Hence return the same ENOPKG error in this case as we already do in the pidfd ioctl based codepath. --- src/basic/namespace-util.c | 18 ++++++++++++------ src/test/test-namespace.c | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/basic/namespace-util.c b/src/basic/namespace-util.c index 9293999a01..88d122ea6c 100644 --- a/src/basic/namespace-util.c +++ b/src/basic/namespace-util.c @@ -71,13 +71,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 +149,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; } diff --git a/src/test/test-namespace.c b/src/test/test-namespace.c index d646306acf..6a0459e1fa 100644 --- a/src/test/test-namespace.c +++ b/src/test/test-namespace.c @@ -172,7 +172,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; } From 5b14690a3982a884f26d2f860943c54b7c5fc471 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 7 Jan 2025 10:55:29 +0100 Subject: [PATCH 2/4] namespace-util: slightly tweak proc_mounted() handling in namespace_is_init() Let's not sloppily eat up errors here. --- src/basic/namespace-util.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/basic/namespace-util.c b/src/basic/namespace-util.c index 88d122ea6c..d45d4d33c5 100644 --- a/src/basic/namespace-util.c +++ b/src/basic/namespace-util.c @@ -307,9 +307,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; From 3d57b6692f0a014df2c8e9a41ac61e5da4275506 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 14 Nov 2024 09:55:26 +0100 Subject: [PATCH 3/4] namespace-util: add helper to get base UID from userns --- src/basic/namespace-util.c | 72 ++++++++++++++++++++++++++++++++++++++ src/basic/namespace-util.h | 2 ++ src/basic/uid-range.c | 48 +++++++++++++++++++++---- src/basic/uid-range.h | 2 ++ src/test/test-namespace.c | 24 +++++++++++++ 5 files changed, 142 insertions(+), 6 deletions(-) diff --git a/src/basic/namespace-util.c b/src/basic/namespace-util.c index d45d4d33c5..7793747377 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] = { @@ -636,3 +637,74 @@ 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; +} diff --git a/src/basic/namespace-util.h b/src/basic/namespace-util.h index 319efec4ac..1bd61e6445 100644 --- a/src/basic/namespace-util.h +++ b/src/basic/namespace-util.h @@ -91,3 +91,5 @@ 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); 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 6a0459e1fa..3f9c8b83de 100644 --- a/src/test/test-namespace.c +++ b/src/test/test-namespace.c @@ -252,6 +252,30 @@ 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); +} + static int intro(void) { if (!have_namespaces()) return log_tests_skipped("Don't have namespace support or lacking privileges"); From 2ade8218598afba0802b1007535b5c8deaeceb58 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 27 Nov 2024 10:17:00 +0100 Subject: [PATCH 4/4] namespace-util: add process_is_owned_by_uid() helper --- src/basic/missing_fs.h | 10 ++++- src/basic/namespace-util.c | 55 +++++++++++++++++++++++ src/basic/namespace-util.h | 2 + src/test/test-namespace.c | 89 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+), 1 deletion(-) 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 7793747377..c7b1bcca2d 100644 --- a/src/basic/namespace-util.c +++ b/src/basic/namespace-util.c @@ -708,3 +708,58 @@ int userns_get_base_uid(int userns_fd, uid_t *ret_uid, gid_t *ret_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 1bd61e6445..d1ac4f6ed4 100644 --- a/src/basic/namespace-util.h +++ b/src/basic/namespace-util.h @@ -93,3 +93,5 @@ int parse_userns_uid_range(const char *s, uid_t *ret_uid_shift, uid_t *ret_uid_r 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/test/test-namespace.c b/src/test/test-namespace.c index 3f9c8b83de..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" @@ -276,6 +278,93 @@ TEST(userns_get_base_uid) { 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");