namespace-util: two new operations (#35833)

(This is preparation for #35685, but since it comes with nice test cases
should be safe to add on their own)
This commit is contained in:
Lennart Poettering
2025-01-08 09:35:21 +01:00
committed by GitHub
6 changed files with 317 additions and 16 deletions
+9 -1
View File
@@ -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
+146 -8
View File
@@ -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/<type> 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);
}
}
+4
View File
@@ -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);
+42 -6
View File
@@ -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;
}
}
}
+2
View File
@@ -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);
+114 -1
View File
@@ -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");