From 52bd61373b89bb6b458f5020b23a92253b12f235 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 19 Dec 2023 13:12:14 +0100 Subject: [PATCH 01/31] varlink: add varlink_get_peer_gid() helper --- src/shared/varlink.c | 19 ++++++++++++++++++- src/shared/varlink.h | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/shared/varlink.c b/src/shared/varlink.c index d4072ec398..1bfa4c7081 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -2807,12 +2807,29 @@ int varlink_get_peer_uid(Varlink *v, uid_t *ret) { return varlink_log_errno(v, r, "Failed to acquire credentials: %m"); if (!uid_is_valid(v->ucred.uid)) - return varlink_log_errno(v, SYNTHETIC_ERRNO(ENODATA), "Peer uid is invalid."); + return varlink_log_errno(v, SYNTHETIC_ERRNO(ENODATA), "Peer UID is invalid."); *ret = v->ucred.uid; return 0; } +int varlink_get_peer_gid(Varlink *v, gid_t *ret) { + int r; + + assert_return(v, -EINVAL); + assert_return(ret, -EINVAL); + + r = varlink_acquire_ucred(v); + if (r < 0) + return varlink_log_errno(v, r, "Failed to acquire credentials: %m"); + + if (!gid_is_valid(v->ucred.gid)) + return varlink_log_errno(v, SYNTHETIC_ERRNO(ENODATA), "Peer GID is invalid."); + + *ret = v->ucred.gid; + return 0; +} + int varlink_get_peer_pid(Varlink *v, pid_t *ret) { int r; diff --git a/src/shared/varlink.h b/src/shared/varlink.h index db7227b215..c8639f4d7f 100644 --- a/src/shared/varlink.h +++ b/src/shared/varlink.h @@ -173,6 +173,7 @@ void* varlink_set_userdata(Varlink *v, void *userdata); void* varlink_get_userdata(Varlink *v); int varlink_get_peer_uid(Varlink *v, uid_t *ret); +int varlink_get_peer_gid(Varlink *v, gid_t *ret); int varlink_get_peer_pid(Varlink *v, pid_t *ret); int varlink_get_peer_pidref(Varlink *v, PidRef *ret); From b219dcd45ac88ee18d2e3217f9f00631503bb77a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 23 Jan 2024 10:22:27 +0100 Subject: [PATCH 02/31] varlink: add varlink_peek_dup_fd() helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This new call is like varlink_peek_fd() (i.e. gets an fd out of the connection but leaving it also in there), and combines ith with F_DUPFD_CLOEXEC to make a copy of it. We previously already had varlink_dup_fd() which was a duplicating version for pushing an fd *into* the connection. To reduce confusion, let's rename that one varlink_push_dup_fd() to make the symmetry to valrink_push_fd() clear so that we have no: varlink_peer_push_fd() → put fd in without dup'ing varlink_peer_push_dup_fd() → same with F_DUPFD_CLOEXEC varlink_peer_peek_fd() → get fd out without dup'ing varlink_peer_peek_dup_fd() → same with F_DUPFD_CLOEXEC --- src/shared/varlink.c | 12 +++++++++++- src/shared/varlink.h | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/shared/varlink.c b/src/shared/varlink.c index 1bfa4c7081..713aff2895 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -3093,7 +3093,7 @@ int varlink_push_fd(Varlink *v, int fd) { return i; } -int varlink_dup_fd(Varlink *v, int fd) { +int varlink_push_dup_fd(Varlink *v, int fd) { _cleanup_close_ int dp = -1; int r; @@ -3141,6 +3141,16 @@ int varlink_peek_fd(Varlink *v, size_t i) { return v->input_fds[i]; } +int varlink_peek_dup_fd(Varlink *v, size_t i) { + int fd; + + fd = varlink_peek_fd(v, i); + if (fd < 0) + return fd; + + return RET_NERRNO(fcntl(fd, F_DUPFD_CLOEXEC, 3)); +} + int varlink_take_fd(Varlink *v, size_t i) { assert_return(v, -EINVAL); diff --git a/src/shared/varlink.h b/src/shared/varlink.h index c8639f4d7f..f6dda63549 100644 --- a/src/shared/varlink.h +++ b/src/shared/varlink.h @@ -156,11 +156,12 @@ int varlink_dispatch(Varlink *v, JsonVariant *parameters, const JsonDispatch tab /* Write outgoing fds into the socket (to be associated with the next enqueued message) */ int varlink_push_fd(Varlink *v, int fd); -int varlink_dup_fd(Varlink *v, int fd); +int varlink_push_dup_fd(Varlink *v, int fd); int varlink_reset_fds(Varlink *v); /* Read incoming fds from the socket (associated with the currently handled message) */ int varlink_peek_fd(Varlink *v, size_t i); +int varlink_peek_dup_fd(Varlink *v, size_t i); int varlink_take_fd(Varlink *v, size_t i); int varlink_set_allow_fd_passing_input(Varlink *v, bool b); From 2251e4ef900adff02dcc1a645d90ec66d20bda19 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 26 Apr 2023 21:51:53 +0200 Subject: [PATCH 03/31] image-policy: add a new image_policy_intersect() call This new call takes two image policy objects and generates an "intersection" policy, i.e. only allows what is allowed by both. Or in other words it conceptually implements a binary AND of the policy flags. (Except that it's a bit harder, due to normalization, and underspecified flags). We can use this later for mountfsd: a client can specify a policy, and mountfsd can specify another policy, and we'll then apply only what both allow. Note that a policy generated like this might be invalid. For example, if one policy says root must exist and be verity or luks protected, and the other policy says root must be absent, then the intersection is invalid, since one policy only allows what the other prohibits and vice versa. We'll return a clear error code in that case (ENAVAIL). (This is because we simply don't allow encoding such impossible policies in an ImagePolicy structure, for good reasons.) --- src/shared/image-policy.c | 98 ++++++++++++++++++++++++++++++++++++ src/shared/image-policy.h | 3 ++ src/test/test-image-policy.c | 32 ++++++++++++ 3 files changed, 133 insertions(+) diff --git a/src/shared/image-policy.c b/src/shared/image-policy.c index 47ca62c731..e7bd84e144 100644 --- a/src/shared/image-policy.c +++ b/src/shared/image-policy.c @@ -50,6 +50,20 @@ PartitionPolicyFlags partition_policy_flags_extend(PartitionPolicyFlags flags) { return flags; } +PartitionPolicyFlags partition_policy_flags_reduce(PartitionPolicyFlags flags) { + /* The reverse of partition_policy_flags_extend(): if some parts of the flags field allow all + * possible options, let's remove it from the flags to make them shorter */ + + if (FLAGS_SET(flags, _PARTITION_POLICY_USE_MASK)) + flags &= ~_PARTITION_POLICY_USE_MASK; + if (FLAGS_SET(flags, _PARTITION_POLICY_READ_ONLY_MASK)) + flags &= ~_PARTITION_POLICY_READ_ONLY_MASK; + if (FLAGS_SET(flags, _PARTITION_POLICY_GROWFS_MASK)) + flags &= ~_PARTITION_POLICY_GROWFS_MASK; + + return flags; +} + static PartitionPolicyFlags partition_policy_normalized_flags(const PartitionPolicy *policy) { PartitionPolicyFlags flags = ASSERT_PTR(policy)->flags; @@ -676,6 +690,90 @@ int parse_image_policy_argument(const char *s, ImagePolicy **policy) { return free_and_replace_full(*policy, np, image_policy_free); } +static bool partition_policy_flags_has_unspecified(PartitionPolicyFlags flags) { + + if ((flags & _PARTITION_POLICY_USE_MASK) == 0) + return true; + if ((flags & _PARTITION_POLICY_READ_ONLY_MASK) == 0) + return true; + if ((flags & _PARTITION_POLICY_GROWFS_MASK) == 0) + return true; + + return false; +} + +int image_policy_intersect(const ImagePolicy *a, const ImagePolicy *b, ImagePolicy **ret) { + _cleanup_(image_policy_freep) ImagePolicy *p = NULL; + + /* Calculates the intersection of the specified policies, i.e. only what is permitted in both. This + * might fail with -ENAVAIL if the intersection is an "impossible policy". For example, if a root + * partition my neither be used, nor be absent, nor be unused then this is considered + * "impossible". */ + + p = image_policy_new(_PARTITION_DESIGNATOR_MAX); + if (!p) + return -ENOMEM; + + p->default_flags = + partition_policy_flags_extend(image_policy_default(a)) & + partition_policy_flags_extend(image_policy_default(b)); + + if (partition_policy_flags_has_unspecified(p->default_flags)) /* Intersection empty? */ + return -ENAVAIL; + + p->default_flags = partition_policy_flags_reduce(p->default_flags); + + for (PartitionDesignator d = 0; d < _PARTITION_DESIGNATOR_MAX; d++) { + PartitionPolicyFlags x, y, z, df; + + /* If this designator has no entry in either policy we don't need to include it in the intersection either. */ + if (!image_policy_bsearch(a, d) && !image_policy_bsearch(b, d)) + continue; + + /* Expand this policy flags field to the "long" form, i.e. for each part of the flags that + * are left unspcified add in all possible options */ + x = image_policy_get_exhaustively(a, d); + if (x < 0) + return x; + + y = image_policy_get_exhaustively(b, d); + if (y < 0) + return y; + + /* Mask it */ + z = x & y; + + /* Check if the intersection is empty for this partition. If so, generate a clear error */ + if (partition_policy_flags_has_unspecified(z)) + return -ENAVAIL; + + df = partition_policy_normalized_flags( + &(const PartitionPolicy) { + .flags = image_policy_default(p), + .designator = d, + }); + if (df < 0) + return df; + if (df == z) /* Same as default? then let's skip this */ + continue; + + /* image_policy_get_exhaustively() may have extended the flags mask to include all + * read-only/growfs flags if not set. Let's remove them again, if they are both set to + * minimize the policy again. */ + z = partition_policy_flags_reduce(z); + + p->policies[p->n_policies++] = (struct PartitionPolicy) { + .designator = d, + .flags = z, + }; + } + + if (ret) + *ret = TAKE_PTR(p); + + return 0; +} + const ImagePolicy image_policy_allow = { /* Allow policy */ .n_policies = 0, diff --git a/src/shared/image-policy.h b/src/shared/image-policy.h index c2a0a5ac40..a1a6afa345 100644 --- a/src/shared/image-policy.h +++ b/src/shared/image-policy.h @@ -80,6 +80,7 @@ static inline size_t image_policy_n_entries(const ImagePolicy *policy) { } PartitionPolicyFlags partition_policy_flags_extend(PartitionPolicyFlags flags); +PartitionPolicyFlags partition_policy_flags_reduce(PartitionPolicyFlags flags); PartitionPolicyFlags partition_policy_flags_from_string(const char *s); int partition_policy_flags_to_string(PartitionPolicyFlags flags, bool simplify, char **ret); @@ -95,6 +96,8 @@ bool image_policy_equiv_deny(const ImagePolicy *policy); bool image_policy_equal(const ImagePolicy *a, const ImagePolicy *b); /* checks if defined the same way, i.e. has literally the same ruleset */ int image_policy_equivalent(const ImagePolicy *a, const ImagePolicy *b); /* checks if the outcome is the same, i.e. for all partitions results in the same decisions. */ +int image_policy_intersect(const ImagePolicy *a, const ImagePolicy *b, ImagePolicy **ret); + static inline ImagePolicy* image_policy_free(ImagePolicy *p) { return mfree(p); } diff --git a/src/test/test-image-policy.c b/src/test/test-image-policy.c index 1572093c9d..12fc10acf0 100644 --- a/src/test/test-image-policy.c +++ b/src/test/test-image-policy.c @@ -130,4 +130,36 @@ TEST(extend) { assert_se(partition_policy_flags_extend(PARTITION_POLICY_GROWFS_ON) == (PARTITION_POLICY_GROWFS_ON|_PARTITION_POLICY_USE_MASK|_PARTITION_POLICY_READ_ONLY_MASK)); } +static void test_policy_intersect_one(const char *a, const char *b, const char *c) { + _cleanup_(image_policy_freep) ImagePolicy *x = NULL, *y = NULL, *z = NULL, *t = NULL; + + assert_se(image_policy_from_string(a, &x) >= 0); + assert_se(image_policy_from_string(b, &y) >= 0); + assert_se(image_policy_from_string(c, &z) >= 0); + + assert_se(image_policy_intersect(x, y, &t) >= 0); + + _cleanup_free_ char *s1 = NULL, *s2 = NULL, *s3 = NULL, *s4 = NULL; + assert_se(image_policy_to_string(x, false, &s1) >= 0); + assert_se(image_policy_to_string(y, false, &s2) >= 0); + assert_se(image_policy_to_string(z, false, &s3) >= 0); + assert_se(image_policy_to_string(t, false, &s4) >= 0); + + log_info("%s ^ %s → %s vs. %s", s1, s2, s3, s4); + + assert_se(image_policy_equivalent(z, t) > 0); +} + +TEST(image_policy_intersect) { + test_policy_intersect_one("", "", ""); + test_policy_intersect_one("-", "-", "-"); + test_policy_intersect_one("*", "*", "*"); + test_policy_intersect_one("~", "~", "~"); + test_policy_intersect_one("root=verity+signed", "root=signed+verity", "root=verity+signed"); + test_policy_intersect_one("root=verity+signed", "root=signed", "root=signed"); + test_policy_intersect_one("root=verity+signed", "root=verity", "root=verity"); + test_policy_intersect_one("root=open", "root=verity", "root=verity"); + test_policy_intersect_one("root=open", "=verity+ignore", "root=verity+ignore:=ignore"); +} + DEFINE_TEST_MAIN(LOG_INFO); From 5bff40e719ba2eca0c8cb894930b8d4928c2c1f9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Mar 2023 10:38:58 +0100 Subject: [PATCH 04/31] uid-range: add uid_range_overlaps() helper --- src/basic/uid-range.c | 20 ++++++++++++++++++++ src/basic/uid-range.h | 2 ++ 2 files changed, 22 insertions(+) diff --git a/src/basic/uid-range.c b/src/basic/uid-range.c index 90eb0cd911..65504b899a 100644 --- a/src/basic/uid-range.c +++ b/src/basic/uid-range.c @@ -253,3 +253,23 @@ int uid_range_load_userns(UIDRange **ret, const char *path) { *ret = TAKE_PTR(range); return 0; } + +bool uid_range_overlaps(const UIDRange *range, uid_t start, uid_t nr) { + + if (!range) + return false; + + /* Avoid overflow */ + if (start > UINT32_MAX - nr) + nr = UINT32_MAX - start; + + if (nr == 0) + return false; + + FOREACH_ARRAY(entry, range->entries, range->n_entries) + if (start < entry->start + entry->nr && + start + nr >= entry->start) + return true; + + return false; +} diff --git a/src/basic/uid-range.h b/src/basic/uid-range.h index 5c4aac3726..acfb7191e9 100644 --- a/src/basic/uid-range.h +++ b/src/basic/uid-range.h @@ -34,3 +34,5 @@ static inline bool uid_range_contains(const UIDRange *range, uid_t uid) { int uid_map_read_one(FILE *f, uid_t *ret_base, uid_t *ret_shift, uid_t *ret_range); int uid_range_load_userns(UIDRange **ret, const char *path); + +bool uid_range_overlaps(const UIDRange *range, uid_t start, uid_t nr); From 6ebb53d945481b9dc1336b2708d0937a8ca6486f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Apr 2023 18:51:03 +0200 Subject: [PATCH 05/31] uid-range: optionally load outside view of UID range from uid_map procfs file --- src/basic/uid-range.c | 12 +++++++++--- src/basic/uid-range.h | 11 ++++++++++- src/core/manager.c | 2 +- src/libsystemd/sd-device/device-monitor.c | 2 +- src/test/test-uid-range.c | 4 ++-- src/userdb/userdbctl.c | 4 ++-- 6 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/basic/uid-range.c b/src/basic/uid-range.c index 65504b899a..4c35df5ea8 100644 --- a/src/basic/uid-range.c +++ b/src/basic/uid-range.c @@ -204,7 +204,7 @@ int uid_map_read_one(FILE *f, uid_t *ret_base, uid_t *ret_shift, uid_t *ret_rang return 0; } -int uid_range_load_userns(UIDRange **ret, const char *path) { +int uid_range_load_userns(UIDRange **ret, const char *path, UIDRangeUsernsMode mode) { _cleanup_(uid_range_freep) UIDRange *range = NULL; _cleanup_fclose_ FILE *f = NULL; int r; @@ -216,9 +216,11 @@ int uid_range_load_userns(UIDRange **ret, const char *path) { * To simplify things this will modify the passed array in case of later failure. */ assert(ret); + assert(mode >= 0); + assert(mode < _UID_RANGE_USERNS_MODE_MAX); if (!path) - path = "/proc/self/uid_map"; + path = IN_SET(mode, UID_RANGE_USERNS_INSIDE, UID_RANGE_USERNS_OUTSIDE) ? "/proc/self/uid_map" : "/proc/self/gid_map"; f = fopen(path, "re"); if (!f) { @@ -243,7 +245,11 @@ int uid_range_load_userns(UIDRange **ret, const char *path) { if (r < 0) return r; - r = uid_range_add_internal(&range, uid_base, uid_range, /* coalesce = */ false); + r = uid_range_add_internal( + &range, + IN_SET(mode, UID_RANGE_USERNS_INSIDE, GID_RANGE_USERNS_INSIDE) ? uid_base : uid_shift, + uid_range, + /* coalesce = */ false); if (r < 0) return r; } diff --git a/src/basic/uid-range.h b/src/basic/uid-range.h index acfb7191e9..d6b62f2365 100644 --- a/src/basic/uid-range.h +++ b/src/basic/uid-range.h @@ -33,6 +33,15 @@ static inline bool uid_range_contains(const UIDRange *range, uid_t uid) { int uid_map_read_one(FILE *f, uid_t *ret_base, uid_t *ret_shift, uid_t *ret_range); -int uid_range_load_userns(UIDRange **ret, const char *path); +typedef enum UIDRangeUsernsMode { + UID_RANGE_USERNS_INSIDE, + UID_RANGE_USERNS_OUTSIDE, + GID_RANGE_USERNS_INSIDE, + GID_RANGE_USERNS_OUTSIDE, + _UID_RANGE_USERNS_MODE_MAX, + _UID_RANGE_USERNS_MODE_INVALID = -EINVAL, +} UIDRangeUsernsMode; + +int uid_range_load_userns(UIDRange **ret, const char *path, UIDRangeUsernsMode mode); bool uid_range_overlaps(const UIDRange *range, uid_t start, uid_t nr); diff --git a/src/core/manager.c b/src/core/manager.c index 0103c582b5..b333be9afd 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -4805,7 +4805,7 @@ static int short_uid_range(const char *path) { /* Taint systemd if we the UID range assigned to this environment doesn't at least cover 0…65534, * i.e. from root to nobody. */ - r = uid_range_load_userns(&p, path); + r = uid_range_load_userns(&p, path, UID_RANGE_USERNS_INSIDE); if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) return false; if (r < 0) diff --git a/src/libsystemd/sd-device/device-monitor.c b/src/libsystemd/sd-device/device-monitor.c index 018a68d1db..408cd17581 100644 --- a/src/libsystemd/sd-device/device-monitor.c +++ b/src/libsystemd/sd-device/device-monitor.c @@ -473,7 +473,7 @@ static bool check_sender_uid(sd_device_monitor *m, uid_t uid) { return true; if (!m->mapped_userns_uid_range) { - r = uid_range_load_userns(&m->mapped_userns_uid_range, NULL); + r = uid_range_load_userns(&m->mapped_userns_uid_range, NULL, UID_RANGE_USERNS_INSIDE); if (r < 0) log_monitor_errno(m, r, "Failed to load UID ranges mapped to the current user namespace, ignoring: %m"); } diff --git a/src/test/test-uid-range.c b/src/test/test-uid-range.c index e8799e7810..b908cdaf11 100644 --- a/src/test/test-uid-range.c +++ b/src/test/test-uid-range.c @@ -100,7 +100,7 @@ TEST(load_userns) { _cleanup_fclose_ FILE *f = NULL; int r; - r = uid_range_load_userns(&p, NULL); + r = uid_range_load_userns(&p, NULL, UID_RANGE_USERNS_INSIDE); if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) return; @@ -123,7 +123,7 @@ TEST(load_userns) { p = uid_range_free(p); - assert_se(uid_range_load_userns(&p, fn) >= 0); + assert_se(uid_range_load_userns(&p, fn, UID_RANGE_USERNS_INSIDE) >= 0); assert_se(uid_range_contains(p, 0)); assert_se(uid_range_contains(p, 19)); diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c index ecbdcb0f41..c16e44d6b6 100644 --- a/src/userdb/userdbctl.c +++ b/src/userdb/userdbctl.c @@ -428,7 +428,7 @@ static int display_user(int argc, char *argv[], void *userdata) { _cleanup_(uid_range_freep) UIDRange *uid_range = NULL; int boundary_lines, uid_map_lines; - r = uid_range_load_userns(&uid_range, "/proc/self/uid_map"); + r = uid_range_load_userns(&uid_range, "/proc/self/uid_map", UID_RANGE_USERNS_INSIDE); if (r < 0) log_debug_errno(r, "Failed to load /proc/self/uid_map, ignoring: %m"); @@ -731,7 +731,7 @@ static int display_group(int argc, char *argv[], void *userdata) { _cleanup_(uid_range_freep) UIDRange *gid_range = NULL; int boundary_lines, gid_map_lines; - r = uid_range_load_userns(&gid_range, "/proc/self/gid_map"); + r = uid_range_load_userns(&gid_range, "/proc/self/gid_map", UID_RANGE_USERNS_INSIDE); if (r < 0) log_debug_errno(r, "Failed to load /proc/self/gid_map, ignoring: %m"); From 20ba086e775838112e777f8f6dc3c03af4f3d772 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Apr 2023 18:51:55 +0200 Subject: [PATCH 06/31] uid-range: add new uid_range_load_userns_by_fd() helper This is similar to uid_range_load_userns() but instead of reading the uid_map off a process it reads it off a userns fd. (Of course the kernel has no API for this right now, hence we fork off a throw-away process which joins the user namespace, and then read off the data from there.) --- src/basic/uid-range.c | 61 +++++++++++++++++++++++++++++++++++++++++++ src/basic/uid-range.h | 1 + 2 files changed, 62 insertions(+) diff --git a/src/basic/uid-range.c b/src/basic/uid-range.c index 4c35df5ea8..f1b086711d 100644 --- a/src/basic/uid-range.c +++ b/src/basic/uid-range.c @@ -10,6 +10,7 @@ #include "format-util.h" #include "macro.h" #include "path-util.h" +#include "process-util.h" #include "sort-util.h" #include "stat-util.h" #include "uid-range.h" @@ -260,6 +261,66 @@ int uid_range_load_userns(UIDRange **ret, const char *path, UIDRangeUsernsMode m return 0; } +int uid_range_load_userns_by_fd(int userns_fd, UIDRangeUsernsMode mode, UIDRange **ret) { + _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); + assert(mode >= 0); + assert(mode < _UID_RANGE_USERNS_MODE_MAX); + assert(ret); + + if (pipe2(pfd, O_CLOEXEC) < 0) + return -errno; + + r = safe_fork_full( + "(sd-mkuserns)", + /* 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 fifo: %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'); + + const char *p = procfs_file_alloca( + pid, + IN_SET(mode, UID_RANGE_USERNS_INSIDE, UID_RANGE_USERNS_OUTSIDE) ? "uid_map" : "gid_map"); + + return uid_range_load_userns(ret, p, mode); +} + bool uid_range_overlaps(const UIDRange *range, uid_t start, uid_t nr) { if (!range) diff --git a/src/basic/uid-range.h b/src/basic/uid-range.h index d6b62f2365..f4d3d41db5 100644 --- a/src/basic/uid-range.h +++ b/src/basic/uid-range.h @@ -43,5 +43,6 @@ typedef enum UIDRangeUsernsMode { } UIDRangeUsernsMode; int uid_range_load_userns(UIDRange **ret, const char *path, UIDRangeUsernsMode mode); +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); From afdd0efa633330475b1ab6e17c226ac5b926b2c6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Apr 2023 18:51:26 +0200 Subject: [PATCH 07/31] uid-range: add some basic operations on UidRange objects Helpers to compare and get size, and whether the object is empty. --- src/basic/uid-range.c | 20 ++++++++++++++++++++ src/basic/uid-range.h | 30 ++++++++++++++++++++++++++++++ src/test/test-uid-range.c | 37 ++++++++++++++++++++++++++++++------- 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/src/basic/uid-range.c b/src/basic/uid-range.c index f1b086711d..4fc79c07c2 100644 --- a/src/basic/uid-range.c +++ b/src/basic/uid-range.c @@ -340,3 +340,23 @@ bool uid_range_overlaps(const UIDRange *range, uid_t start, uid_t nr) { return false; } + +bool uid_range_equal(const UIDRange *a, const UIDRange *b) { + if (a == b) + return true; + + if (!a || !b) + return false; + + if (a->n_entries != b->n_entries) + return false; + + for (size_t i = 0; i < a->n_entries; i++) { + if (a->entries[i].start != b->entries[i].start) + return false; + if (a->entries[i].nr != b->entries[i].nr) + return false; + } + + return true; +} diff --git a/src/basic/uid-range.h b/src/basic/uid-range.h index f4d3d41db5..48dfebbf02 100644 --- a/src/basic/uid-range.h +++ b/src/basic/uid-range.h @@ -33,6 +33,36 @@ static inline bool uid_range_contains(const UIDRange *range, uid_t uid) { int uid_map_read_one(FILE *f, uid_t *ret_base, uid_t *ret_shift, uid_t *ret_range); +static inline size_t uid_range_entries(const UIDRange *range) { + return range ? range->n_entries : 0; +} + +static inline unsigned uid_range_size(const UIDRange *range) { + if (!range) + return 0; + + unsigned n = 0; + + FOREACH_ARRAY(e, range->entries, range->n_entries) + n += e->nr; + + return n; +} + +static inline bool uid_range_is_empty(const UIDRange *range) { + + if (!range) + return true; + + FOREACH_ARRAY(e, range->entries, range->n_entries) + if (e->nr > 0) + return false; + + return true; +} + +bool uid_range_equal(const UIDRange *a, const UIDRange *b); + typedef enum UIDRangeUsernsMode { UID_RANGE_USERNS_INSIDE, UID_RANGE_USERNS_OUTSIDE, diff --git a/src/test/test-uid-range.c b/src/test/test-uid-range.c index b908cdaf11..c31fb30e7e 100644 --- a/src/test/test-uid-range.c +++ b/src/test/test-uid-range.c @@ -23,9 +23,15 @@ TEST(uid_range) { assert_se(!uid_range_covers(p, UINT32_MAX, 1)); assert_se(!uid_range_covers(p, UINT32_MAX - 10, 11)); + assert_se(uid_range_entries(p) == 0); + assert_se(uid_range_size(p) == 0); + assert_se(uid_range_is_empty(p)); + assert_se(uid_range_add_str(&p, "500-999") >= 0); assert_se(p); - assert_se(p->n_entries == 1); + assert_se(uid_range_entries(p) == 1); + assert_se(uid_range_size(p) == 500); + assert_se(!uid_range_is_empty(p)); assert_se(p->entries[0].start == 500); assert_se(p->entries[0].nr == 500); @@ -56,19 +62,23 @@ TEST(uid_range) { assert_se(uid_range_next_lower(p, &search) == -EBUSY); assert_se(uid_range_add_str(&p, "1000") >= 0); - assert_se(p->n_entries == 1); + assert_se(uid_range_entries(p) == 1); assert_se(p->entries[0].start == 500); assert_se(p->entries[0].nr == 501); assert_se(uid_range_add_str(&p, "30-40") >= 0); - assert_se(p->n_entries == 2); + assert_se(uid_range_entries(p) == 2); + assert_se(uid_range_size(p) == 500 + 1 + 11); + assert_se(!uid_range_is_empty(p)); assert_se(p->entries[0].start == 30); assert_se(p->entries[0].nr == 11); assert_se(p->entries[1].start == 500); assert_se(p->entries[1].nr == 501); assert_se(uid_range_add_str(&p, "60-70") >= 0); - assert_se(p->n_entries == 3); + assert_se(uid_range_entries(p) == 3); + assert_se(uid_range_size(p) == 500 + 1 + 11 + 11); + assert_se(!uid_range_is_empty(p)); assert_se(p->entries[0].start == 30); assert_se(p->entries[0].nr == 11); assert_se(p->entries[1].start == 60); @@ -77,21 +87,34 @@ TEST(uid_range) { assert_se(p->entries[2].nr == 501); assert_se(uid_range_add_str(&p, "20-2000") >= 0); - assert_se(p->n_entries == 1); + assert_se(uid_range_entries(p) == 1); + assert_se(uid_range_size(p) == 1981); assert_se(p->entries[0].start == 20); assert_se(p->entries[0].nr == 1981); assert_se(uid_range_add_str(&p, "2002") >= 0); - assert_se(p->n_entries == 2); + assert_se(uid_range_entries(p) == 2); + assert_se(uid_range_size(p) == 1982); assert_se(p->entries[0].start == 20); assert_se(p->entries[0].nr == 1981); assert_se(p->entries[1].start == 2002); assert_se(p->entries[1].nr == 1); + _cleanup_(uid_range_freep) UIDRange *q = NULL; + assert_se(!uid_range_equal(p, q)); + assert_se(uid_range_add_str(&q, "20-2000") >= 0); + assert_se(!uid_range_equal(p, q)); + assert_se(uid_range_add_str(&q, "2002") >= 0); + assert_se(uid_range_equal(p, q)); + assert_se(uid_range_add_str(&p, "2001") >= 0); - assert_se(p->n_entries == 1); + assert_se(uid_range_entries(p) == 1); + assert_se(uid_range_size(p) == 1983); assert_se(p->entries[0].start == 20); assert_se(p->entries[0].nr == 1983); + + assert_se(uid_range_add_str(&q, "2001") >= 0); + assert_se(uid_range_equal(p, q)); } TEST(load_userns) { From 5783b4a954e910280079c47ad917faeba8986d14 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 13 Mar 2023 15:23:11 +0100 Subject: [PATCH 08/31] namespace-util: add detach_mount_namespace_harder() This is just like detach_mount_namespace() but if need be uses unpriv user namespaces to be able to execute CLONE_NEWNS. --- src/basic/namespace-util.c | 48 ++++++++++++++++++++++++++++++++++++++ src/basic/namespace-util.h | 1 + 2 files changed, 49 insertions(+) diff --git a/src/basic/namespace-util.c b/src/basic/namespace-util.c index c537c0f9b7..fbbaead8f3 100644 --- a/src/basic/namespace-util.c +++ b/src/basic/namespace-util.c @@ -214,6 +214,54 @@ int detach_mount_namespace(void) { return 0; } +int detach_mount_namespace_harder(uid_t target_uid, gid_t target_gid) { + int r; + + /* Tried detach_mount_namespace() first. If that doesn't work due to permissions, opens up an + * unprivileged user namespace with a mapping of the originating UID/GID to the specified target + * UID/GID. Then, tries detach_mount_namespace() again. + * + * Or in other words: tries much harder to get a mount namespace, making use of unprivileged user + * namespaces if need be. + * + * Note that after this function completed: + * + * → if we had privs, afterwards uids/gids on files and processes are as before + * + * → if we had no privs, our own id and all our files will show up owned by target_uid/target_gid, + * and everything else owned by nobody. + * + * Yes, that's quite a difference. */ + + if (!uid_is_valid(target_uid)) + return -EINVAL; + if (!gid_is_valid(target_gid)) + return -EINVAL; + + r = detach_mount_namespace(); + if (r != -EPERM) + return r; + + if (unshare(CLONE_NEWUSER) < 0) + return log_debug_errno(errno, "Failed to acquire user namespace: %m"); + + r = write_string_filef("/proc/self/uid_map", 0, + UID_FMT " " UID_FMT " 1\n", target_uid, getuid()); + if (r < 0) + return log_debug_errno(r, "Failed to write uid map: %m"); + + r = write_string_file("/proc/self/setgroups", "deny", 0); + if (r < 0) + return log_debug_errno(r, "Failed to write setgroups file: %m"); + + r = write_string_filef("/proc/self/gid_map", 0, + GID_FMT " " GID_FMT " 1\n", target_gid, getgid()); + if (r < 0) + return log_debug_errno(r, "Failed to write gid map: %m"); + + return detach_mount_namespace(); +} + int userns_acquire(const char *uid_map, const char *gid_map) { char path[STRLEN("/proc//uid_map") + DECIMAL_STR_MAX(pid_t) + 1]; _cleanup_(sigkill_waitp) pid_t pid = 0; diff --git a/src/basic/namespace-util.h b/src/basic/namespace-util.h index 34cbec3f6e..735df663d1 100644 --- a/src/basic/namespace-util.h +++ b/src/basic/namespace-util.h @@ -34,6 +34,7 @@ int namespace_enter(int pidns_fd, int mntns_fd, int netns_fd, int userns_fd, int int fd_is_ns(int fd, unsigned long nsflag); int detach_mount_namespace(void); +int detach_mount_namespace_harder(uid_t target_uid, gid_t target_gid); static inline bool userns_shift_range_valid(uid_t shift, uid_t range) { /* Checks that the specified userns range makes sense, i.e. contains at least one UID, and the end From e02fb2099cbac381a63f1816ec04b09a17daae79 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Mar 2023 17:22:18 +0100 Subject: [PATCH 09/31] namespace-util: add helper for allocating an empty userns fd --- src/basic/namespace-util.c | 19 +++++++++++++++++++ src/basic/namespace-util.h | 3 +++ 2 files changed, 22 insertions(+) diff --git a/src/basic/namespace-util.c b/src/basic/namespace-util.c index fbbaead8f3..347a4ac80a 100644 --- a/src/basic/namespace-util.c +++ b/src/basic/namespace-util.c @@ -262,6 +262,25 @@ int detach_mount_namespace_harder(uid_t target_uid, gid_t target_gid) { return detach_mount_namespace(); } +int userns_acquire_empty(void) { + _cleanup_(sigkill_waitp) pid_t pid = 0; + _cleanup_close_ int userns_fd = -EBADF; + int r; + + r = safe_fork("(sd-mkuserns)", FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGKILL|FORK_NEW_USERNS, &pid); + if (r < 0) + return r; + if (r == 0) + /* Child. We do nothing here, just freeze until somebody kills us. */ + freeze(); + + r = namespace_open(pid, NULL, NULL, NULL, &userns_fd, NULL); + if (r < 0) + return log_error_errno(r, "Failed to open userns fd: %m"); + + return TAKE_FD(userns_fd); +} + int userns_acquire(const char *uid_map, const char *gid_map) { char path[STRLEN("/proc//uid_map") + DECIMAL_STR_MAX(pid_t) + 1]; _cleanup_(sigkill_waitp) pid_t pid = 0; diff --git a/src/basic/namespace-util.h b/src/basic/namespace-util.h index 735df663d1..a15e262edf 100644 --- a/src/basic/namespace-util.h +++ b/src/basic/namespace-util.h @@ -51,8 +51,11 @@ static inline bool userns_shift_range_valid(uid_t shift, uid_t range) { return true; } +int userns_acquire_empty(void); int userns_acquire(const char *uid_map, const char *gid_map); + int netns_acquire(void); + int in_same_namespace(pid_t pid1, pid_t pid2, NamespaceType type); int parse_userns_uid_range(const char *s, uid_t *ret_uid_shift, uid_t *ret_uid_range); From 2ad2f0c89e19d3c5af1ef002ade44910b8297006 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Mar 2023 22:36:35 +0100 Subject: [PATCH 10/31] namespace-util: add detach_mount_namespace_userns() --- src/basic/namespace-util.c | 15 +++++++++++++++ src/basic/namespace-util.h | 1 + 2 files changed, 16 insertions(+) diff --git a/src/basic/namespace-util.c b/src/basic/namespace-util.c index 347a4ac80a..53633c7e8f 100644 --- a/src/basic/namespace-util.c +++ b/src/basic/namespace-util.c @@ -262,6 +262,21 @@ int detach_mount_namespace_harder(uid_t target_uid, gid_t target_gid) { return detach_mount_namespace(); } +int detach_mount_namespace_userns(int userns_fd) { + int r; + + assert(userns_fd >= 0); + + if (setns(userns_fd, CLONE_NEWUSER) < 0) + return log_debug_errno(errno, "Failed to join user namespace: %m"); + + r = reset_uid_gid(); + if (r < 0) + return log_debug_errno(r, "Failed to become root in user namespace: %m"); + + return detach_mount_namespace(); +} + int userns_acquire_empty(void) { _cleanup_(sigkill_waitp) pid_t pid = 0; _cleanup_close_ int userns_fd = -EBADF; diff --git a/src/basic/namespace-util.h b/src/basic/namespace-util.h index a15e262edf..e7cf1a2201 100644 --- a/src/basic/namespace-util.h +++ b/src/basic/namespace-util.h @@ -35,6 +35,7 @@ int fd_is_ns(int fd, unsigned long nsflag); int detach_mount_namespace(void); int detach_mount_namespace_harder(uid_t target_uid, gid_t target_gid); +int detach_mount_namespace_userns(int userns_fd); static inline bool userns_shift_range_valid(uid_t shift, uid_t range) { /* Checks that the specified userns range makes sense, i.e. contains at least one UID, and the end From 574a07c79d0518040fd44492fa3ee0e21e65d495 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 5 Dec 2023 11:51:26 +0100 Subject: [PATCH 11/31] namespace-util: add namespace_open_by_type() helper --- src/basic/namespace-util.c | 16 ++++++++++++++++ src/basic/namespace-util.h | 2 ++ 2 files changed, 18 insertions(+) diff --git a/src/basic/namespace-util.c b/src/basic/namespace-util.c index 53633c7e8f..0e54a1e235 100644 --- a/src/basic/namespace-util.c +++ b/src/basic/namespace-util.c @@ -426,3 +426,19 @@ int parse_userns_uid_range(const char *s, uid_t *ret_uid_shift, uid_t *ret_uid_r return 0; } + +int namespace_open_by_type(NamespaceType type) { + const char *p; + int fd; + + assert(type >= 0); + assert(type < _NAMESPACE_TYPE_MAX); + + p = pid_namespace_path(0, type); + + fd = RET_NERRNO(open(p, O_RDONLY|O_NOCTTY|O_CLOEXEC)); + if (fd == -ENOENT && proc_mounted() == 0) + return -ENOSYS; + + return fd; +} diff --git a/src/basic/namespace-util.h b/src/basic/namespace-util.h index e7cf1a2201..972ef05d53 100644 --- a/src/basic/namespace-util.h +++ b/src/basic/namespace-util.h @@ -60,3 +60,5 @@ int netns_acquire(void); int in_same_namespace(pid_t pid1, pid_t pid2, NamespaceType type); int parse_userns_uid_range(const char *s, uid_t *ret_uid_shift, uid_t *ret_uid_range); + +int namespace_open_by_type(NamespaceType type); From e4f62e7a120366703b4d99631ba0390bcd5b0b58 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 6 Dec 2023 11:35:04 +0100 Subject: [PATCH 12/31] namespace-util: add new helper is_our_namespace() --- src/basic/namespace-util.c | 39 ++++++++++++++++++++++++++++++++++++++ src/basic/namespace-util.h | 2 ++ 2 files changed, 41 insertions(+) diff --git a/src/basic/namespace-util.c b/src/basic/namespace-util.c index 0e54a1e235..5b4e43f921 100644 --- a/src/basic/namespace-util.c +++ b/src/basic/namespace-util.c @@ -34,6 +34,14 @@ const struct namespace_info namespace_info[] = { #define pid_namespace_path(pid, type) procfs_file_alloca(pid, namespace_info[type].proc_path) +static NamespaceType clone_flag_to_namespace_type(unsigned long clone_flag) { + for (NamespaceType t = 0; t < _NAMESPACE_TYPE_MAX; t++) + if (((namespace_info[t].clone_flag ^ clone_flag) & (CLONE_NEWCGROUP|CLONE_NEWIPC|CLONE_NEWNET|CLONE_NEWNS|CLONE_NEWPID|CLONE_NEWUSER|CLONE_NEWUTS|CLONE_NEWTIME)) == 0) + return t; + + return _NAMESPACE_TYPE_INVALID; +} + int namespace_open( pid_t pid, int *ret_pidns_fd, @@ -442,3 +450,34 @@ int namespace_open_by_type(NamespaceType type) { return fd; } + +int is_our_namespace(int fd, NamespaceType request_type) { + int clone_flag; + + assert(fd >= 0); + + clone_flag = ioctl(fd, NS_GET_NSTYPE); + if (clone_flag < 0) + return -errno; + + NamespaceType found_type = clone_flag_to_namespace_type(clone_flag); + if (found_type < 0) + return -EBADF; /* Uh? Unknown namespace type? */ + + if (request_type >= 0 && request_type != found_type) /* It's a namespace, but not of the right type? */ + return -EUCLEAN; + + struct stat st_fd, st_ours; + if (fstat(fd, &st_fd) < 0) + return -errno; + + const char *p = pid_namespace_path(0, found_type); + if (stat(p, &st_ours) < 0) { + if (errno == ENOENT) + return proc_mounted() == 0 ? -ENOSYS : -ENOENT; + + return -errno; + } + + return stat_inode_same(&st_ours, &st_fd); +} diff --git a/src/basic/namespace-util.h b/src/basic/namespace-util.h index 972ef05d53..545952a5d1 100644 --- a/src/basic/namespace-util.h +++ b/src/basic/namespace-util.h @@ -62,3 +62,5 @@ int in_same_namespace(pid_t pid1, pid_t pid2, NamespaceType type); int parse_userns_uid_range(const char *s, uid_t *ret_uid_shift, uid_t *ret_uid_range); int namespace_open_by_type(NamespaceType type); + +int is_our_namespace(int fd, NamespaceType type); From 0f716ace4106ca396a7b697e2fd3f74b98987110 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 1 Mar 2024 17:31:06 +0100 Subject: [PATCH 13/31] bpf-dlopen: pick up more symbols from libbpf --- src/shared/bpf-compat.h | 1 + src/shared/bpf-dlopen.c | 68 +++++++++++++++++++++++++++++------------ src/shared/bpf-dlopen.h | 32 +++++++++++++------ 3 files changed, 72 insertions(+), 29 deletions(-) diff --git a/src/shared/bpf-compat.h b/src/shared/bpf-compat.h index 9ccb7d8205..5a7945cb9b 100644 --- a/src/shared/bpf-compat.h +++ b/src/shared/bpf-compat.h @@ -26,6 +26,7 @@ struct bpf_map_create_opts; * When removing this file move these back to bpf-dlopen.h */ extern int (*sym_bpf_map_create)(enum bpf_map_type, const char *, __u32, __u32, __u32, const struct bpf_map_create_opts *); extern int (*sym_libbpf_probe_bpf_prog_type)(enum bpf_prog_type, const void *); +extern struct bpf_map* (*sym_bpf_object__next_map)(const struct bpf_object *obj, const struct bpf_map *map); /* compat symbols removed in libbpf 1.0 */ extern int (*sym_bpf_create_map)(enum bpf_map_type, int key_size, int value_size, int max_entries, __u32 map_flags); diff --git a/src/shared/bpf-dlopen.c b/src/shared/bpf-dlopen.c index 6d59001b2c..de3e40516e 100644 --- a/src/shared/bpf-dlopen.c +++ b/src/shared/bpf-dlopen.c @@ -18,28 +18,41 @@ #define MODERN_LIBBPF 0 #endif -DLSYM_FUNCTION(bpf_program__attach_cgroup); -DLSYM_FUNCTION(bpf_program__attach_lsm); -DLSYM_FUNCTION(bpf_link__fd); DLSYM_FUNCTION(bpf_link__destroy); +DLSYM_FUNCTION(bpf_link__fd); +DLSYM_FUNCTION(bpf_link__open); +DLSYM_FUNCTION(bpf_link__pin); DLSYM_FUNCTION(bpf_map__fd); DLSYM_FUNCTION(bpf_map__name); -DLSYM_FUNCTION(bpf_map__set_max_entries); -DLSYM_FUNCTION(bpf_map_update_elem); -DLSYM_FUNCTION(bpf_map_delete_elem); DLSYM_FUNCTION(bpf_map__set_inner_map_fd); -DLSYM_FUNCTION(bpf_object__open_skeleton); -DLSYM_FUNCTION(bpf_object__load_skeleton); +DLSYM_FUNCTION(bpf_map__set_max_entries); +DLSYM_FUNCTION(bpf_map__set_pin_path); +DLSYM_FUNCTION(bpf_map_delete_elem); +DLSYM_FUNCTION(bpf_map_get_fd_by_id); +DLSYM_FUNCTION(bpf_map_lookup_elem); +DLSYM_FUNCTION(bpf_map_update_elem); DLSYM_FUNCTION(bpf_object__attach_skeleton); -DLSYM_FUNCTION(bpf_object__detach_skeleton); DLSYM_FUNCTION(bpf_object__destroy_skeleton); +DLSYM_FUNCTION(bpf_object__detach_skeleton); +DLSYM_FUNCTION(bpf_object__load_skeleton); +DLSYM_FUNCTION(bpf_object__name); +DLSYM_FUNCTION(bpf_object__open_skeleton); +DLSYM_FUNCTION(bpf_object__pin_maps); +DLSYM_FUNCTION(bpf_program__attach); +DLSYM_FUNCTION(bpf_program__attach_cgroup); +DLSYM_FUNCTION(bpf_program__attach_lsm); DLSYM_FUNCTION(bpf_program__name); -DLSYM_FUNCTION(libbpf_set_print); DLSYM_FUNCTION(libbpf_get_error); +DLSYM_FUNCTION(libbpf_set_print); +DLSYM_FUNCTION(ring_buffer__epoll_fd); +DLSYM_FUNCTION(ring_buffer__free); +DLSYM_FUNCTION(ring_buffer__new); +DLSYM_FUNCTION(ring_buffer__poll); /* new symbols available from libbpf 0.7.0 */ int (*sym_bpf_map_create)(enum bpf_map_type, const char *, __u32, __u32, __u32, const struct bpf_map_create_opts *); int (*sym_libbpf_probe_bpf_prog_type)(enum bpf_prog_type, const void *); +struct bpf_map* (*sym_bpf_object__next_map)(const struct bpf_object *obj, const struct bpf_map *map); /* compat symbols removed in libbpf 1.0 */ int (*sym_bpf_create_map)(enum bpf_map_type, int key_size, int value_size, int max_entries, __u32 map_flags); @@ -90,6 +103,8 @@ int dlopen_bpf(void) { DLSYM_ARG(bpf_probe_prog_type) #endif ); + + /* NB: we don't try to load bpf_object__next_map() on old versions */ } else { log_debug("Loaded 'libbpf.so.1' via dlopen()"); @@ -98,11 +113,13 @@ int dlopen_bpf(void) { dl, LOG_DEBUG, #if MODERN_LIBBPF DLSYM_ARG(bpf_map_create), - DLSYM_ARG(libbpf_probe_bpf_prog_type) + DLSYM_ARG(libbpf_probe_bpf_prog_type), + DLSYM_ARG(bpf_object__next_map) #else /* These symbols did not exist in old libbpf, hence we cannot type check them */ DLSYM_ARG_FORCE(bpf_map_create), - DLSYM_ARG_FORCE(libbpf_probe_bpf_prog_type) + DLSYM_ARG_FORCE(libbpf_probe_bpf_prog_type), + DLSYM_ARG_FORCE(bpf_object__next_map) #endif ); } @@ -113,28 +130,41 @@ int dlopen_bpf(void) { dl, LOG_DEBUG, DLSYM_ARG(bpf_link__destroy), DLSYM_ARG(bpf_link__fd), + DLSYM_ARG(bpf_link__open), + DLSYM_ARG(bpf_link__pin), DLSYM_ARG(bpf_map__fd), DLSYM_ARG(bpf_map__name), - DLSYM_ARG(bpf_map__set_max_entries), - DLSYM_ARG(bpf_map_update_elem), - DLSYM_ARG(bpf_map_delete_elem), DLSYM_ARG(bpf_map__set_inner_map_fd), - DLSYM_ARG(bpf_object__open_skeleton), - DLSYM_ARG(bpf_object__load_skeleton), + DLSYM_ARG(bpf_map__set_max_entries), + DLSYM_ARG(bpf_map__set_pin_path), + DLSYM_ARG(bpf_map_delete_elem), + DLSYM_ARG(bpf_map_get_fd_by_id), + DLSYM_ARG(bpf_map_lookup_elem), + DLSYM_ARG(bpf_map_update_elem), DLSYM_ARG(bpf_object__attach_skeleton), - DLSYM_ARG(bpf_object__detach_skeleton), DLSYM_ARG(bpf_object__destroy_skeleton), + DLSYM_ARG(bpf_object__detach_skeleton), + DLSYM_ARG(bpf_object__load_skeleton), + DLSYM_ARG(bpf_object__name), + DLSYM_ARG(bpf_object__open_skeleton), + DLSYM_ARG(bpf_object__pin_maps), #if MODERN_LIBBPF + DLSYM_ARG(bpf_program__attach), DLSYM_ARG(bpf_program__attach_cgroup), DLSYM_ARG(bpf_program__attach_lsm), #else /* libbpf added a "const" to function parameters where it should not have, ignore this type incompatibility */ + DLSYM_ARG_FORCE(bpf_program__attach), DLSYM_ARG_FORCE(bpf_program__attach_cgroup), DLSYM_ARG_FORCE(bpf_program__attach_lsm), #endif DLSYM_ARG(bpf_program__name), + DLSYM_ARG(libbpf_get_error), DLSYM_ARG(libbpf_set_print), - DLSYM_ARG(libbpf_get_error)); + DLSYM_ARG(ring_buffer__epoll_fd), + DLSYM_ARG(ring_buffer__free), + DLSYM_ARG(ring_buffer__new), + DLSYM_ARG(ring_buffer__poll)); if (r < 0) return r; diff --git a/src/shared/bpf-dlopen.h b/src/shared/bpf-dlopen.h index 859405e3c6..6deca829f1 100644 --- a/src/shared/bpf-dlopen.h +++ b/src/shared/bpf-dlopen.h @@ -9,26 +9,38 @@ #include "bpf-compat.h" #include "dlfcn-util.h" -DLSYM_PROTOTYPE(bpf_program__attach_cgroup); -DLSYM_PROTOTYPE(bpf_program__attach_lsm); -DLSYM_PROTOTYPE(bpf_link__fd); DLSYM_PROTOTYPE(bpf_link__destroy); +DLSYM_PROTOTYPE(bpf_link__fd); +DLSYM_PROTOTYPE(bpf_link__open); +DLSYM_PROTOTYPE(bpf_link__pin); DLSYM_PROTOTYPE(bpf_map__fd); DLSYM_PROTOTYPE(bpf_map__name); -DLSYM_PROTOTYPE(bpf_map__set_max_entries); -DLSYM_PROTOTYPE(bpf_map_update_elem); -DLSYM_PROTOTYPE(bpf_map_delete_elem); DLSYM_PROTOTYPE(bpf_map__set_inner_map_fd); +DLSYM_PROTOTYPE(bpf_map__set_max_entries); +DLSYM_PROTOTYPE(bpf_map__set_pin_path); +DLSYM_PROTOTYPE(bpf_map_delete_elem); +DLSYM_PROTOTYPE(bpf_map_get_fd_by_id); +DLSYM_PROTOTYPE(bpf_map_lookup_elem); +DLSYM_PROTOTYPE(bpf_map_update_elem); /* The *_skeleton APIs are autogenerated by bpftool, the targets can be found * in ./build/src/core/bpf/socket_bind/socket-bind.skel.h */ -DLSYM_PROTOTYPE(bpf_object__open_skeleton); -DLSYM_PROTOTYPE(bpf_object__load_skeleton); DLSYM_PROTOTYPE(bpf_object__attach_skeleton); -DLSYM_PROTOTYPE(bpf_object__detach_skeleton); DLSYM_PROTOTYPE(bpf_object__destroy_skeleton); +DLSYM_PROTOTYPE(bpf_object__detach_skeleton); +DLSYM_PROTOTYPE(bpf_object__load_skeleton); +DLSYM_PROTOTYPE(bpf_object__name); +DLSYM_PROTOTYPE(bpf_object__open_skeleton); +DLSYM_PROTOTYPE(bpf_object__pin_maps); +DLSYM_PROTOTYPE(bpf_program__attach); +DLSYM_PROTOTYPE(bpf_program__attach_cgroup); +DLSYM_PROTOTYPE(bpf_program__attach_lsm); DLSYM_PROTOTYPE(bpf_program__name); -DLSYM_PROTOTYPE(libbpf_set_print); DLSYM_PROTOTYPE(libbpf_get_error); +DLSYM_PROTOTYPE(libbpf_set_print); +DLSYM_PROTOTYPE(ring_buffer__epoll_fd); +DLSYM_PROTOTYPE(ring_buffer__free); +DLSYM_PROTOTYPE(ring_buffer__new); +DLSYM_PROTOTYPE(ring_buffer__poll); #endif From cb1b813f0df60219063f2accd3b92a10f55aa741 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 8 Dec 2023 10:47:38 +0100 Subject: [PATCH 14/31] lock-util: make global lock return parameter to image_path_lock() optional When adding unprivileged nspawn support we don't really want a global lock file, since we cannot even access the dir they are stored in, hence make the concept optional. Some minor other modernizations. --- src/basic/lock-util.h | 2 +- src/shared/discover-image.c | 46 ++++++++++++++++++++++--------------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/basic/lock-util.h b/src/basic/lock-util.h index 91b332f803..8fb4757968 100644 --- a/src/basic/lock-util.h +++ b/src/basic/lock-util.h @@ -17,7 +17,7 @@ static inline int make_lock_file(const char *p, int operation, LockFile *ret) { int make_lock_file_for(const char *p, int operation, LockFile *ret); void release_lock_file(LockFile *f); -#define LOCK_FILE_INIT { .dir_fd = -EBADF, .fd = -EBADF } +#define LOCK_FILE_INIT (LockFile) { .dir_fd = -EBADF, .fd = -EBADF } /* POSIX locks with the same interface as flock(). */ int posix_lock(int fd, int operation); diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index 00d32a9c87..bef64a6b04 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -1295,7 +1295,12 @@ static void make_lock_dir(void) { (void) mkdir("/run/systemd/nspawn/locks", 0700); } -int image_path_lock(const char *path, int operation, LockFile *global, LockFile *local) { +int image_path_lock( + const char *path, + int operation, + LockFile *ret_global, + LockFile *ret_local) { + _cleanup_free_ char *p = NULL; LockFile t = LOCK_FILE_INIT; struct stat st; @@ -1303,8 +1308,7 @@ int image_path_lock(const char *path, int operation, LockFile *global, LockFile int r; assert(path); - assert(global); - assert(local); + assert(ret_local); /* Locks an image path. This actually creates two locks: one "local" one, next to the image path * itself, which might be shared via NFS. And another "global" one, in /run, that uses the @@ -1326,7 +1330,9 @@ int image_path_lock(const char *path, int operation, LockFile *global, LockFile } if (getenv_bool("SYSTEMD_NSPAWN_LOCK") == 0) { - *local = *global = (LockFile) LOCK_FILE_INIT; + *ret_local = LOCK_FILE_INIT; + if (ret_global) + *ret_global = LOCK_FILE_INIT; return 0; } @@ -1342,19 +1348,23 @@ int image_path_lock(const char *path, int operation, LockFile *global, LockFile if (exclusive) return -EBUSY; - *local = *global = (LockFile) LOCK_FILE_INIT; + *ret_local = LOCK_FILE_INIT; + if (ret_global) + *ret_global = LOCK_FILE_INIT; return 0; } - if (stat(path, &st) >= 0) { - if (S_ISBLK(st.st_mode)) - r = asprintf(&p, "/run/systemd/nspawn/locks/block-%u:%u", major(st.st_rdev), minor(st.st_rdev)); - else if (S_ISDIR(st.st_mode) || S_ISREG(st.st_mode)) - r = asprintf(&p, "/run/systemd/nspawn/locks/inode-%lu:%lu", (unsigned long) st.st_dev, (unsigned long) st.st_ino); - else - return -ENOTTY; - if (r < 0) - return -ENOMEM; + if (ret_global) { + if (stat(path, &st) >= 0) { + if (S_ISBLK(st.st_mode)) + r = asprintf(&p, "/run/systemd/nspawn/locks/block-%u:%u", major(st.st_rdev), minor(st.st_rdev)); + else if (S_ISDIR(st.st_mode) || S_ISREG(st.st_mode)) + r = asprintf(&p, "/run/systemd/nspawn/locks/inode-%lu:%lu", (unsigned long) st.st_dev, (unsigned long) st.st_ino); + else + return -ENOTTY; + if (r < 0) + return -ENOMEM; + } } /* For block devices we don't need the "local" lock, as the major/minor lock above should be @@ -1372,15 +1382,15 @@ int image_path_lock(const char *path, int operation, LockFile *global, LockFile if (p) { make_lock_dir(); - r = make_lock_file(p, operation, global); + r = make_lock_file(p, operation, ret_global); if (r < 0) { release_lock_file(&t); return r; } - } else - *global = (LockFile) LOCK_FILE_INIT; + } else if (ret_global) + *ret_global = LOCK_FILE_INIT; - *local = t; + *ret_local = t; return 0; } From 3b2874952fed9b71726ca08e4c888e02ca275ca2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 12 Dec 2023 10:44:57 +0100 Subject: [PATCH 15/31] cgroup-util: add helpers for opening cgroup by id --- src/basic/cgroup-util.c | 49 +++++++++++++++++++++++++++++++++++++++- src/basic/cgroup-util.h | 11 ++++++++- src/basic/missing_fs.h | 5 ++++ src/shared/cgroup-show.c | 13 ++--------- src/test/test-cgroup.c | 44 ++++++++++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 13 deletions(-) diff --git a/src/basic/cgroup-util.c b/src/basic/cgroup-util.c index 6991460f86..8303b37684 100644 --- a/src/basic/cgroup-util.c +++ b/src/basic/cgroup-util.c @@ -22,6 +22,7 @@ #include "log.h" #include "login-util.h" #include "macro.h" +#include "missing_fs.h" #include "missing_magic.h" #include "missing_threads.h" #include "mkdir.h" @@ -39,6 +40,38 @@ #include "user-util.h" #include "xattr-util.h" +int cg_path_open(const char *controller, const char *path) { + _cleanup_free_ char *fs = NULL; + int r; + + r = cg_get_path(controller, path, /* item=*/ NULL, &fs); + if (r < 0) + return r; + + return RET_NERRNO(open(fs, O_DIRECTORY|O_CLOEXEC)); +} + +int cg_cgroupid_open(int cgroupfs_fd, uint64_t id) { + _cleanup_close_ int fsfd = -EBADF; + + if (cgroupfs_fd < 0) { + fsfd = open("/sys/fs/cgroup", O_CLOEXEC|O_DIRECTORY); + if (fsfd < 0) + return -errno; + + cgroupfs_fd = fsfd; + } + + cg_file_handle fh = CG_FILE_HANDLE_INIT; + CG_FILE_HANDLE_CGROUPID(fh) = id; + + int fd = open_by_handle_at(cgroupfs_fd, &fh.file_handle, O_DIRECTORY|O_CLOEXEC); + if (fd < 0) + return -errno; + + return fd; +} + static int cg_enumerate_items(const char *controller, const char *path, FILE **ret, const char *item) { _cleanup_free_ char *fs = NULL; FILE *f; @@ -1404,7 +1437,7 @@ int cg_pid_get_machine_name(pid_t pid, char **ret_machine) { int cg_path_get_cgroupid(const char *path, uint64_t *ret) { cg_file_handle fh = CG_FILE_HANDLE_INIT; - int mnt_id = -1; + int mnt_id; assert(path); assert(ret); @@ -1418,6 +1451,20 @@ int cg_path_get_cgroupid(const char *path, uint64_t *ret) { return 0; } +int cg_fd_get_cgroupid(int fd, uint64_t *ret) { + cg_file_handle fh = CG_FILE_HANDLE_INIT; + int mnt_id = -1; + + assert(fd >= 0); + assert(ret); + + if (name_to_handle_at(fd, "", &fh.file_handle, &mnt_id, AT_EMPTY_PATH) < 0) + return -errno; + + *ret = CG_FILE_HANDLE_CGROUPID(fh); + return 0; +} + int cg_path_get_session(const char *path, char **ret_session) { _cleanup_free_ char *unit = NULL; char *start, *end; diff --git a/src/basic/cgroup-util.h b/src/basic/cgroup-util.h index 244f3b657b..9ad6dd8eb6 100644 --- a/src/basic/cgroup-util.h +++ b/src/basic/cgroup-util.h @@ -180,6 +180,9 @@ typedef enum CGroupUnified { * generate paths with multiple adjacent / removed. */ +int cg_path_open(const char *controller, const char *path); +int cg_cgroupid_open(int fsfd, uint64_t id); + int cg_enumerate_processes(const char *controller, const char *path, FILE **ret); int cg_read_pid(FILE *f, pid_t *ret); int cg_read_pidref(FILE *f, PidRef *ret); @@ -267,6 +270,7 @@ int cg_is_empty_recursive(const char *controller, const char *path); int cg_get_root_path(char **path); int cg_path_get_cgroupid(const char *path, uint64_t *ret); +int cg_fd_get_cgroupid(int fd, uint64_t *ret); int cg_path_get_session(const char *path, char **ret_session); int cg_path_get_owner_uid(const char *path, uid_t *ret_uid); int cg_path_get_unit(const char *path, char **ret_unit); @@ -352,5 +356,10 @@ typedef union { uint8_t space[offsetof(struct file_handle, f_handle) + sizeof(uint64_t)]; } cg_file_handle; -#define CG_FILE_HANDLE_INIT { .file_handle.handle_bytes = sizeof(uint64_t) } +#define CG_FILE_HANDLE_INIT \ + (cg_file_handle) { \ + .file_handle.handle_bytes = sizeof(uint64_t), \ + .file_handle.handle_type = FILEID_KERNFS, \ + } + #define CG_FILE_HANDLE_CGROUPID(fh) (*(uint64_t*) (fh).file_handle.f_handle) diff --git a/src/basic/missing_fs.h b/src/basic/missing_fs.h index ee70ae08db..d97b190131 100644 --- a/src/basic/missing_fs.h +++ b/src/basic/missing_fs.h @@ -111,3 +111,8 @@ assert_cc(FS_PROJINHERIT_FL == 0x20000000); #else assert_cc(FS_KEY_DESCRIPTOR_SIZE == 8); #endif + +/* linux/exportfs.h */ +#ifndef FILEID_KERNFS +#define FILEID_KERNFS 0xfe +#endif diff --git a/src/shared/cgroup-show.c b/src/shared/cgroup-show.c index c2ee1c5aef..c7af10687d 100644 --- a/src/shared/cgroup-show.c +++ b/src/shared/cgroup-show.c @@ -150,18 +150,9 @@ static int show_cgroup_name( delegate = r > 0; if (FLAGS_SET(flags, OUTPUT_CGROUP_ID)) { - cg_file_handle fh = CG_FILE_HANDLE_INIT; - int mnt_id = -1; - - if (name_to_handle_at( - fd, - "", - &fh.file_handle, - &mnt_id, - AT_EMPTY_PATH) < 0) + r = cg_fd_get_cgroupid(fd, &cgroupid); + if (r < 0) log_debug_errno(errno, "Failed to determine cgroup ID of %s, ignoring: %m", path); - else - cgroupid = CG_FILE_HANDLE_CGROUPID(fh); } r = path_extract_filename(path, &b); diff --git a/src/test/test-cgroup.c b/src/test/test-cgroup.c index 9b174d708b..06cf886729 100644 --- a/src/test/test-cgroup.c +++ b/src/test/test-cgroup.c @@ -5,6 +5,7 @@ #include "cgroup-setup.h" #include "cgroup-util.h" #include "errno-util.h" +#include "fd-util.h" #include "path-util.h" #include "process-util.h" #include "string-util.h" @@ -129,4 +130,47 @@ TEST(cg_create) { assert_se(cg_rmdir(SYSTEMD_CGROUP_CONTROLLER, test_a) == 0); } +TEST(id) { + _cleanup_free_ char *p = NULL, *p2 = NULL; + _cleanup_close_ int fd = -EBADF, fd2 = -EBADF; + uint64_t id, id2; + int r; + + r = cg_all_unified(); + if (r == 0) { + log_tests_skipped("skipping cgroupid test, not running in unified mode"); + return; + } + if (IN_SET(r, -ENOMEDIUM, -ENOENT)) { + log_tests_skipped("cgroupfs is not mounted"); + return; + } + assert_se(r > 0); + + fd = cg_path_open(SYSTEMD_CGROUP_CONTROLLER, "/"); + assert_se(fd >= 0); + + assert_se(fd_get_path(fd, &p) >= 0); + assert_se(path_equal(p, "/sys/fs/cgroup")); + + assert_se(cg_fd_get_cgroupid(fd, &id) >= 0); + + fd2 = cg_cgroupid_open(fd, id); + + if (ERRNO_IS_NEG_PRIVILEGE(fd2)) + log_notice("Skipping open-by-cgroup-id test because lacking privs."); + else { + assert_se(fd2 >= 0); + + assert_se(fd_get_path(fd2, &p2) >= 0); + assert_se(path_equal(p2, "/sys/fs/cgroup")); + + assert_se(cg_fd_get_cgroupid(fd2, &id2) >= 0); + + assert_se(id == id2); + + assert_se(inode_same_at(fd, NULL, fd2, NULL, AT_EMPTY_PATH) > 0); + } +} + DEFINE_TEST_MAIN(LOG_DEBUG); From b2dcfd8e11977d4369560b8b4de81b947b0e963e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 12 Dec 2023 11:02:59 +0100 Subject: [PATCH 16/31] cgroup-setup: add fd-based version of cg_attach() --- src/shared/cgroup-setup.c | 14 ++++++++++++++ src/shared/cgroup-setup.h | 1 + 2 files changed, 15 insertions(+) diff --git a/src/shared/cgroup-setup.c b/src/shared/cgroup-setup.c index d9cca6ced4..f82af4cf43 100644 --- a/src/shared/cgroup-setup.c +++ b/src/shared/cgroup-setup.c @@ -384,6 +384,20 @@ int cg_attach(const char *controller, const char *path, pid_t pid) { return 0; } +int cg_fd_attach(int fd, pid_t pid) { + char c[DECIMAL_STR_MAX(pid_t) + 2]; + + assert(fd >= 0); + assert(pid >= 0); + + if (pid == 0) + pid = getpid_cached(); + + xsprintf(c, PID_FMT "\n", pid); + + return write_string_file_at(fd, "cgroup.procs", c, WRITE_STRING_FILE_DISABLE_BUFFER); +} + int cg_attach_fallback(const char *controller, const char *path, pid_t pid) { int r; diff --git a/src/shared/cgroup-setup.h b/src/shared/cgroup-setup.h index c35884ee4d..283ab6757c 100644 --- a/src/shared/cgroup-setup.h +++ b/src/shared/cgroup-setup.h @@ -21,6 +21,7 @@ int cg_trim(const char *controller, const char *path, bool delete_root); int cg_create(const char *controller, const char *path); int cg_attach(const char *controller, const char *path, pid_t pid); +int cg_fd_attach(int fd, pid_t pid); int cg_attach_fallback(const char *controller, const char *path, pid_t pid); int cg_create_and_attach(const char *controller, const char *path, pid_t pid); From f7178a04dbfc9247f1d8b9c4840a6c5121ed37b4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 26 Apr 2023 21:22:06 +0200 Subject: [PATCH 17/31] discover-image: export search paths array This way we can use it to validate image paths later. --- src/shared/discover-image.c | 2 +- src/shared/discover-image.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index bef64a6b04..564c7e05c0 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -48,7 +48,7 @@ #include "vpick.h" #include "xattr-util.h" -static const char* const image_search_path[_IMAGE_CLASS_MAX] = { +const char* const image_search_path[_IMAGE_CLASS_MAX] = { [IMAGE_MACHINE] = "/etc/machines\0" /* only place symlinks here */ "/run/machines\0" /* and here too */ "/var/lib/machines\0" /* the main place for images */ diff --git a/src/shared/discover-image.h b/src/shared/discover-image.h index 350c3a22cc..6491cec886 100644 --- a/src/shared/discover-image.h +++ b/src/shared/discover-image.h @@ -122,3 +122,5 @@ int image_to_json(const struct Image *i, JsonVariant **ret); const char *image_root_to_string(ImageClass c) _const_; extern const struct hash_ops image_hash_ops; + +extern const char* const image_search_path[_IMAGE_CLASS_MAX]; From 9444e54e56b3d9ea81135700abe0792ead7f69ae Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Mar 2023 22:46:56 +0100 Subject: [PATCH 18/31] dissect-image: add dissected_image_close() that closes all references to resources --- src/shared/dissect-image.c | 14 ++++++++++++++ src/shared/dissect-image.h | 1 + 2 files changed, 15 insertions(+) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index a0832c9828..ebaff0df12 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -1643,6 +1643,20 @@ int dissect_image_file_and_warn( verity); } +void dissected_image_close(DissectedImage *m) { + if (!m) + return; + + /* Closes all fds we keep open assocated with this, but nothing else */ + + FOREACH_ARRAY(p, m->partitions, _PARTITION_DESIGNATOR_MAX) { + p->mount_node_fd = safe_close(p->mount_node_fd); + p->fsmount_fd = safe_close(p->fsmount_fd); + } + + m->loop = loop_device_unref(m->loop); +} + DissectedImage* dissected_image_unref(DissectedImage *m) { if (!m) return NULL; diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index 559d5b861d..416c9cb46b 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -163,6 +163,7 @@ int dissect_image_file_and_warn(const char *path, const VeritySettings *verity, int dissect_loop_device(LoopDevice *loop, const VeritySettings *verity, const MountOptions *mount_options, const ImagePolicy *image_policy, DissectImageFlags flags, DissectedImage **ret); int dissect_loop_device_and_warn(LoopDevice *loop, const VeritySettings *verity, const MountOptions *mount_options, const ImagePolicy *image_policy, DissectImageFlags flags, DissectedImage **ret); +void dissected_image_close(DissectedImage *m); DissectedImage* dissected_image_unref(DissectedImage *m); DEFINE_TRIVIAL_CLEANUP_FUNC(DissectedImage*, dissected_image_unref); From 77740bddbe0b79450a76552cf0810ae791b23a78 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 4 Dec 2023 18:12:58 +0100 Subject: [PATCH 19/31] dissect-image: add a new helper that checks if VeritySettings has anything set at all --- src/shared/dissect-image.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index 416c9cb46b..d1f5b2c580 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -199,6 +199,14 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(DecryptedImage*, decrypted_image_unref); int dissected_image_relinquish(DissectedImage *m); int verity_settings_load(VeritySettings *verity, const char *image, const char *root_hash_path, const char *root_hash_sig_path); + +static inline bool verity_settings_set(const VeritySettings *settings) { + return settings && + (settings->root_hash_size > 0 || + (settings->root_hash_sig_size > 0 || + settings->data_path)); +} + void verity_settings_done(VeritySettings *verity); static inline bool verity_settings_data_covers(const VeritySettings *verity, PartitionDesignator d) { From 44e3097dff3d25f4aaae5adeaca773232a16691d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 4 Dec 2023 18:23:31 +0100 Subject: [PATCH 20/31] dissect-image: make dissected_image_acquire_metadata() operate within a userns if possible This opens the door for making the call work without privileges: if we pass in a userns fd and DissectedImage that has mount fds then we can acquire all information without privs. --- src/dissect/dissect.c | 2 +- src/shared/discover-image.c | 5 ++++- src/shared/dissect-image.c | 18 +++++++++++++++--- src/shared/dissect-image.h | 2 +- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index e4cc536092..d6939152e0 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -875,7 +875,7 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { fflush(stdout); } - r = dissected_image_acquire_metadata(m, 0); + r = dissected_image_acquire_metadata(m, /* userns_fd= */ -EBADF, /* extra_flags= */ 0); if (r == -ENXIO) return log_error_errno(r, "No root partition discovered."); if (r == -EUCLEAN) diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index 564c7e05c0..b7a97f2b63 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -1522,7 +1522,10 @@ int image_read_metadata(Image *i, const ImagePolicy *image_policy) { if (r < 0) return r; - r = dissected_image_acquire_metadata(m, flags); + r = dissected_image_acquire_metadata( + m, + /* userns_fd= */ -EBADF, + flags); if (r < 0) return r; diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index ebaff0df12..a46891bffa 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -3400,7 +3400,10 @@ int dissected_image_load_verity_sig_partition( return 1; } -int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_flags) { +int dissected_image_acquire_metadata( + DissectedImage *m, + int userns_fd, + DissectImageFlags extra_flags) { enum { META_HOSTNAME, @@ -3459,13 +3462,22 @@ int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_ goto finish; } - r = safe_fork("(sd-dissect)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE, &child); + r = safe_fork("(sd-dissect)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM, &child); if (r < 0) goto finish; if (r == 0) { - /* Child in a new mount namespace */ + /* Child */ error_pipe[0] = safe_close(error_pipe[0]); + if (userns_fd < 0) + r = detach_mount_namespace_harder(0, 0); + else + r = detach_mount_namespace_userns(userns_fd); + if (r < 0) { + log_debug_errno(r, "Failed to detach mount namespace: %m"); + goto inner_fail; + } + r = dissected_image_mount( m, t, diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index d1f5b2c580..7623c1181e 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -172,7 +172,7 @@ int dissected_image_decrypt_interactively(DissectedImage *m, const char *passphr int dissected_image_mount(DissectedImage *m, const char *dest, uid_t uid_shift, uid_t uid_range, int userns_fd, DissectImageFlags flags); int dissected_image_mount_and_warn(DissectedImage *m, const char *where, uid_t uid_shift, uid_t uid_range, int userns_fd, DissectImageFlags flags); -int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_flags); +int dissected_image_acquire_metadata(DissectedImage *m, int userns_fd, DissectImageFlags extra_flags); Architecture dissected_image_architecture(DissectedImage *m); From 5300fe74a18fa34820025d8675434dc69f7111c4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 23 Jan 2024 16:19:38 +0100 Subject: [PATCH 21/31] dissect-image: document one more dissected_image_decrypt() error code --- src/shared/dissect-image.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index a46891bffa..ad10f6bfdd 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -2951,6 +2951,7 @@ int dissected_image_decrypt( * > 0 → Decrypted successfully * -ENOKEY → There's something to decrypt but no key was supplied * -EKEYREJECTED → Passed key was not correct + * -EBUSY → Generic Verity error (kernel is not very explanatory) */ if (verity && verity->root_hash && verity->root_hash_size < sizeof(sd_id128_t)) From 593428680cfe5326d84bdcd00037412ff1c416b3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 6 Dec 2023 18:37:31 +0100 Subject: [PATCH 22/31] build-sys: pick up vmlinux.h from running kernel BTF or user --- meson.build | 73 +++++++++++++++++++++++++++++++++++++++++++++++ meson_options.txt | 8 ++++-- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index 1365ddc978..f48ba6bcd7 100644 --- a/meson.build +++ b/meson.build @@ -1839,6 +1839,79 @@ conf.set10('ENABLE_UKIFY', want_ukify) ##################################################################### check_efi_alignment_py = find_program('tools/check-efi-alignment.py') + +##################################################################### + +use_provided_vmlinux_h = false +use_generated_vmlinux_h = false +provided_vmlinux_h_path = get_option('vmlinux-h-path') + +# For the more complex BPF programs we really want a vmlinux.h (which is arch +# specific, but only somewhat bound to kernel version). Ideally the kernel +# development headers would ship that, but right now they don't. Hence address +# this in two ways: +# +# 1. Provide a vmlinux.h at build time +# 2. Generate the file on the fly where possible (which requires /sys/ to be mounted) +# +# We generally prefer the former (to support reproducible builds), but will +# fallback to the latter. + +if conf.get('BPF_FRAMEWORK') == 1 + enable_vmlinux_h = get_option('vmlinux-h') + + if enable_vmlinux_h == 'auto' + if provided_vmlinux_h_path != '' + use_provided_vmlinux_h = true + elif fs.exists('/sys/kernel/btf/vmlinux') and \ + bpftool.found() and \ + (host_machine.cpu_family() == build_machine.cpu_family()) and \ + host_machine.cpu_family() in ['x86_64', 'aarch64'] + + # We will only generate a vmlinux.h from the running + # kernel if the host and build machine are of the same + # family. Also for now we focus on x86_64 and aarch64, + # since other archs don't seem to be ready yet. + + use_generated_vmlinux_h = true + endif + elif enable_vmlinux_h == 'provided' + use_provided_vmlinux_h = true + elif enable_vmlinux_h == 'generated' + if not fs.exists('/sys/kernel/btf/vmlinux') + error('BTF data from kernel not available (/sys/kernel/btf/vmlinux missing), cannot generate vmlinux.h, but was asked to.') + endif + if not bpftool.found() + error('bpftool not available, cannot generate vmlinux.h, but was asked to.') + endif + use_generated_vmlinux_h = true + endif +endif + +if use_provided_vmlinux_h + if not fs.exists(provided_vmlinux_h_path) + error('Path to provided vmlinux.h does not exist.') + endif + vmlinux_h_dependency = [] + bpf_o_unstripped_cmd += ['-I' + fs.parent(provided_vmlinux_h_path)] + message('Using provided @0@'.format(provided_vmlinux_h_path)) +elif use_generated_vmlinux_h + vmlinux_h_dependency = custom_target( + 'vmlinux.h', + output: 'vmlinux.h', + command : [ bpftool, 'btf', 'dump', 'file', '/sys/kernel/btf/vmlinux', 'format', 'c' ], + capture : true) + + bpf_o_unstripped_cmd += ['-I' + fs.parent(vmlinux_h_dependency.full_path())] + message('Using generated @0@'.format(vmlinux_h_dependency.full_path())) +else + message('Using neither provided nor generated vmlinux.h, some features will not be available.') +endif + +conf.set10('HAVE_VMLINUX_H', use_provided_vmlinux_h or use_generated_vmlinux_h) + +##################################################################### + check_version_history_py = find_program('tools/check-version-history.py') elf2efi_py = find_program('tools/elf2efi.py') export_dbus_interfaces_py = find_program('tools/dbus_exporter.py') diff --git a/meson_options.txt b/meson_options.txt index af9a006563..e3a33244fd 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -519,6 +519,10 @@ option('analyze', type: 'boolean', value: true, description : 'install systemd-analyze') option('bpf-compiler', type : 'combo', choices : ['clang', 'gcc'], - description: 'compiler used to build BPF programs') + description : 'compiler used to build BPF programs') option('bpf-framework', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' }, - description: 'build BPF programs from source code in restricted C') + description : 'build BPF programs from source code in restricted C') +option('vmlinux-h', type : 'combo', choices : ['auto', 'provided', 'generated', 'disabled'], + description : 'which vmlinux.h to use') +option('vmlinux-h-path', type : 'string', value : '', + description : 'path to vmlinux.h to use') From 8aee931e7ae1adb01eeac0e1e4c0aef6ed3969ec Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Apr 2023 19:07:33 +0200 Subject: [PATCH 23/31] nsresourced: add new daemon for granting clients user namespaces and assigning resources to them This adds a small, socket-activated Varlink daemon that can delegate UID ranges for user namespaces to clients asking for it. The primary call is AllocateUserRange() where the user passes in an uninitialized userns fd, which is then set up. There are other calls that allow assigning a mount fd to a userns allocated that way, to set up permissions for a cgroup subtree, and to allocate a veth for such a user namespace. Since the UID assignments are supposed to be transitive, i.e. not permanent, care is taken to ensure that users cannot create inodes owned by these UIDs, so that persistancy cannot be acquired. This is implemented via a BPF-LSM module that ensures that any member of a userns allocated that way cannot create files unless the mount it operates on is owned by the userns itself, or is explicitly allowelisted. BPF LSM program with contributions from Alexei Starovoitov. --- man/rules/meson.build | 4 + man/systemd-nsresourced.service.xml | 81 + meson.build | 3 + meson_options.txt | 2 + network/80-namespace-ns.network | 31 + network/meson.build | 1 + presets/90-systemd.preset | 3 +- .../bpf/userns_restrict/meson.build | 25 + .../userns_restrict/userns-restrict-skel.h | 17 + .../bpf/userns_restrict/userns-restrict.bpf.c | 174 ++ src/nsresourced/meson.build | 48 + src/nsresourced/nsresourced-manager.c | 647 ++++++ src/nsresourced/nsresourced-manager.h | 40 + src/nsresourced/nsresourced.c | 46 + src/nsresourced/nsresourcework.c | 1782 +++++++++++++++++ src/nsresourced/test-userns-restrict.c | 182 ++ src/nsresourced/userns-registry.c | 646 ++++++ src/nsresourced/userns-registry.h | 42 + src/nsresourced/userns-restrict.c | 346 ++++ src/nsresourced/userns-restrict.h | 22 + src/shared/meson.build | 1 + .../varlink-io.systemd.NamespaceResource.c | 62 + .../varlink-io.systemd.NamespaceResource.h | 6 + src/test/test-varlink-idl.c | 5 +- units/meson.build | 8 + units/systemd-nsresourced.service.in | 47 + units/systemd-nsresourced.socket | 23 + 27 files changed, 4292 insertions(+), 2 deletions(-) create mode 100644 man/systemd-nsresourced.service.xml create mode 100644 network/80-namespace-ns.network create mode 100644 src/nsresourced/bpf/userns_restrict/meson.build create mode 100644 src/nsresourced/bpf/userns_restrict/userns-restrict-skel.h create mode 100644 src/nsresourced/bpf/userns_restrict/userns-restrict.bpf.c create mode 100644 src/nsresourced/meson.build create mode 100644 src/nsresourced/nsresourced-manager.c create mode 100644 src/nsresourced/nsresourced-manager.h create mode 100644 src/nsresourced/nsresourced.c create mode 100644 src/nsresourced/nsresourcework.c create mode 100644 src/nsresourced/test-userns-restrict.c create mode 100644 src/nsresourced/userns-registry.c create mode 100644 src/nsresourced/userns-registry.h create mode 100644 src/nsresourced/userns-restrict.c create mode 100644 src/nsresourced/userns-restrict.h create mode 100644 src/shared/varlink-io.systemd.NamespaceResource.c create mode 100644 src/shared/varlink-io.systemd.NamespaceResource.h create mode 100644 units/systemd-nsresourced.service.in create mode 100644 units/systemd-nsresourced.socket diff --git a/man/rules/meson.build b/man/rules/meson.build index 1ca5b105b3..e6c0ac9b52 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1007,6 +1007,10 @@ manpages = [ ['systemd-networkd.service', '8', ['systemd-networkd'], 'ENABLE_NETWORKD'], ['systemd-notify', '1', [], ''], ['systemd-nspawn', '1', [], ''], + ['systemd-nsresourced.service', + '8', + ['systemd-nsresourced'], + 'ENABLE_NSRESOURCED'], ['systemd-oomd.service', '8', ['systemd-oomd'], 'ENABLE_OOMD'], ['systemd-path', '1', [], ''], ['systemd-pcrlock', diff --git a/man/systemd-nsresourced.service.xml b/man/systemd-nsresourced.service.xml new file mode 100644 index 0000000000..d0a561e492 --- /dev/null +++ b/man/systemd-nsresourced.service.xml @@ -0,0 +1,81 @@ + + + + + + + + systemd-nsresourced.service + systemd + + + + systemd-nsresourced.service + 8 + + + + systemd-nsresourced.service + systemd-nsresourced + User Namespace Resource Delegation Service + + + + systemd-nsresourced.service + /usr/lib/systemd/systemd-nsresourced + + + + Description + + systemd-nsresourced is a system service that permits transient delegation of a a + UID/GID range to a user namespace (see user_namespaces7) + allocated by a client, via a Varlink IPC API. + + Unprivileged clients may allocate a user namespace, and then request a UID/GID range to be assigned + to it via this service. The user namespace may then be used to run containers and other sandboxes, and/or + apply it to an id-mapped mount. + + Allocations of UIDs/GIDs this way are transient: when a user namespace goes away, its UID/GID range + is returned to the pool of available ranges. In order to ensure that clients cannot gain persistency in + their transient UID/GID range a BPF-LSM based policy is enforced that ensures that user namespaces set up + this way can only write to file systems they allocate themselves or that are explicitly allowlisted via + systemd-nsresourced. + + systemd-nsresourced automatically ensures that any registered UID ranges show up + in the system's NSS database via the User/Group Record + Lookup API via Varlink. + + Currently, only UID/GID ranges consisting of either exactly 1 or exactly 65536 UIDs/GIDs can be + registered with this service. Moreover, UIDs and GIDs are always allocated together, and + symmetrically. + + The service provides API calls to allowlist mounts (referenced via their mount file descriptors as + per Linux fsmount() API), to pass ownership of a cgroup subtree to the user + namespace and to delegate a virtual Ethernet device pair to the user namespace. When used in combination + this is sufficient to implement fully unprivileged container environments, as implemented by + systemd-nspawn1, fully + unprivileged RootImage= (see + systemd.exec5) or + fully unprivileged disk image tools such as + systemd-dissect1. + + This service provides one Varlink service: + io.systemd.NamespaceResource allows registering user namespaces, and assign mounts, + cgroups and network interfaces to it. + + + + See Also + + systemd1, + systemd-mountfsd.service8, + systemd-nspawn1, + systemd.exec5, + systemd-dissect1, + user_namespaces7 + + + diff --git a/meson.build b/meson.build index f48ba6bcd7..8fd6774799 100644 --- a/meson.build +++ b/meson.build @@ -272,6 +272,7 @@ conf.set_quoted('SYSTEMD_TEST_DATA', testdata_dir) conf.set_quoted('SYSTEMD_TTY_ASK_PASSWORD_AGENT_BINARY_PATH', bindir / 'systemd-tty-ask-password-agent') conf.set_quoted('SYSTEMD_UPDATE_HELPER_PATH', libexecdir / 'systemd-update-helper') conf.set_quoted('SYSTEMD_USERWORK_PATH', libexecdir / 'systemd-userwork') +conf.set_quoted('SYSTEMD_NSRESOURCEWORK_PATH', libexecdir / 'systemd-nsresourcework') conf.set_quoted('SYSTEMD_VERITYSETUP_PATH', libexecdir / 'systemd-veritysetup') conf.set_quoted('SYSTEM_CONFIG_UNIT_DIR', pkgsysconfdir / 'system') conf.set_quoted('SYSTEM_DATA_UNIT_DIR', systemunitdir) @@ -1619,6 +1620,7 @@ foreach term : ['analyze', 'machined', 'networkd', 'nscd', + 'nsresourced', 'nss-myhostname', 'nss-systemd', 'oomd', @@ -2279,6 +2281,7 @@ subdir('src/mount') subdir('src/network') subdir('src/notify') subdir('src/nspawn') +subdir('src/nsresourced') subdir('src/nss-myhostname') subdir('src/nss-mymachines') subdir('src/nss-resolve') diff --git a/meson_options.txt b/meson_options.txt index e3a33244fd..b75f074600 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -140,6 +140,8 @@ option('remote', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : description : 'support for "journal over the network"') option('create-log-dirs', type : 'boolean', description : 'create /var/log/journal{,/remote}') +option('nsresourced', type : 'boolean', + description : 'install the systemd-nsresourced stack') option('nss-myhostname', type : 'boolean', description : 'install nss-myhostname module') option('nss-mymachines', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' }, diff --git a/network/80-namespace-ns.network b/network/80-namespace-ns.network new file mode 100644 index 0000000000..8a84de9ea7 --- /dev/null +++ b/network/80-namespace-ns.network @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: MIT-0 +# +# This config file is installed as part of systemd. +# It may be freely copied and edited (following the MIT No Attribution license). +# +# To make local modifications, one of the following methods may be used: +# 1. add a drop-in file that extends this file by creating the +# /etc/systemd/network/80-namespace-ns.network.d/ directory and creating a +# new .conf file there. +# 2. copy this file into /etc/systemd/network or one of the other paths checked +# by systemd-networkd and edit it there. +# This file should not be edited in place, because it'll be overwritten on upgrades. + +# This network file matches the host-side of the virtual Ethernet link +# created by systemd-nsresourced's network support. See systemd-nsresourced(1) for +# details. + +[Match] +Kind=veth +Name=ns-* + +[Network] +# Default to using a /28 prefix, giving up to 13 addresses per namespace +Address=0.0.0.0/28 +LinkLocalAddressing=yes +DHCPServer=yes +IPMasquerade=both +LLDP=yes +EmitLLDP=customer-bridge +IPv6AcceptRA=no +IPv6SendRA=yes diff --git a/network/meson.build b/network/meson.build index 9df0bea760..a1a87272f7 100644 --- a/network/meson.build +++ b/network/meson.build @@ -11,6 +11,7 @@ if conf.get('ENABLE_NETWORKD') == 1 '80-container-ve.link', '80-container-vz.network', '80-container-vz.link', + '80-namespace-ns.network', '80-vm-vt.network', '80-vm-vt.link', '80-wifi-adhoc.network', diff --git a/presets/90-systemd.preset b/presets/90-systemd.preset index 5f1c5b072f..f896ade2a4 100644 --- a/presets/90-systemd.preset +++ b/presets/90-systemd.preset @@ -24,8 +24,9 @@ enable systemd-homed-activate.service enable systemd-homed-firstboot.service enable systemd-journald-audit.socket enable systemd-network-generator.service -enable systemd-networkd.service enable systemd-networkd-wait-online.service +enable systemd-networkd.service +enable systemd-nsresourced.socket enable systemd-pstore.service enable systemd-resolved.service enable systemd-sysext.service diff --git a/src/nsresourced/bpf/userns_restrict/meson.build b/src/nsresourced/bpf/userns_restrict/meson.build new file mode 100644 index 0000000000..d773c75859 --- /dev/null +++ b/src/nsresourced/bpf/userns_restrict/meson.build @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +if conf.get('HAVE_VMLINUX_H') != 1 + subdir_done() +endif + +userns_restrict_bpf_o_unstripped = custom_target( + 'userns-restrict.bpf.unstripped.o', + input : 'userns-restrict.bpf.c', + output : 'userns-restrict.bpf.unstripped.o', + command : bpf_o_unstripped_cmd, + depends : vmlinux_h_dependency) + +userns_restrict_bpf_o = custom_target( + 'userns-restrict.bpf.o', + input : userns_restrict_bpf_o_unstripped, + output : 'userns-restrict.bpf.o', + command : bpf_o_cmd) + +userns_restrict_skel_h = custom_target( + 'userns-restrict.skel.h', + input : userns_restrict_bpf_o, + output : 'userns-restrict.skel.h', + command : skel_h_cmd, + capture : true) diff --git a/src/nsresourced/bpf/userns_restrict/userns-restrict-skel.h b/src/nsresourced/bpf/userns_restrict/userns-restrict-skel.h new file mode 100644 index 0000000000..271caf4662 --- /dev/null +++ b/src/nsresourced/bpf/userns_restrict/userns-restrict-skel.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* The SPDX header above is actually correct in claiming this was + * LGPL-2.1-or-later, because it is. Since the kernel doesn't consider that + * compatible with GPL we will claim this to be GPL however, which should be + * fine given that LGPL-2.1-or-later downgrades to GPL if needed. + */ + +#include "bpf-dlopen.h" + +/* libbpf is used via dlopen(), so rename symbols */ +#define bpf_object__attach_skeleton sym_bpf_object__attach_skeleton +#define bpf_object__destroy_skeleton sym_bpf_object__destroy_skeleton +#define bpf_object__load_skeleton sym_bpf_object__load_skeleton +#define bpf_object__open_skeleton sym_bpf_object__open_skeleton + +#include "bpf/userns_restrict/userns-restrict.skel.h" diff --git a/src/nsresourced/bpf/userns_restrict/userns-restrict.bpf.c b/src/nsresourced/bpf/userns_restrict/userns-restrict.bpf.c new file mode 100644 index 0000000000..956f91759a --- /dev/null +++ b/src/nsresourced/bpf/userns_restrict/userns-restrict.bpf.c @@ -0,0 +1,174 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* The SPDX header above is actually correct in claiming this was + * LGPL-2.1-or-later, because it is. Since the kernel doesn't consider that + * compatible with GPL we will claim this to be GPL however, which should be + * fine given that LGPL-2.1-or-later downgrades to GPL if needed. + */ + +/* If offsetof() is implemented via __builtin_offset() then it doesn't work on current compilers, since the + * built-ins do not understand CO-RE. Let's undefine any such macros here, to force bpf_helpers.h to define + * its own definitions for this. (In new versions it will do so automatically, but at least in libbpf 1.1.0 + * it does not.) */ +#undef offsetof +#undef container_of + +#include "vmlinux.h" + +#include +#include +#include +#include + +void *bpf_rdonly_cast(void *, __u32) __ksym; + +/* BPF module that implements an allowlist of mounts (identified by mount ID) for user namespaces (identified + * by their inode number in nsfs) that restricts creation of inodes (which would inherit the callers UID/GID) + * or changing of ownership (similar). + * + * This hooks into the varius path-based LSM entrypoints that control inode creation as well as chmod(), and + * then looks up the calling process' user namespace in a global map of namespaces, which points us to + * another map that is simply a list of allowed mnt_ids. */ + +// FIXME: ACL adjustments are currently not blocked. There's no path-based LSM hook available in the kernel +// for setting xattrs or ACLs, hence we cannot easily block them, even though we want that. We can get away +// with ignoring this for now, as ACLs never define ownership, but purely access: i.e. ACLs never allow +// taking possession of an object, but only control access to it. Thus, things like suid access modes should +// not be reachable through it. It still sucks though that a user can persistently add an ACL entry to a file +// with their transient UIDs/GIDs. + +/* kernel currently enforces a maximum usernamespace nesting depth of 32, see create_user_ns() in the kernel sources */ +#define USER_NAMESPACE_DEPTH_MAX 32U + +struct mnt_id_map { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 1); /* placeholder, configured otherwise by nsresourced */ + __type(key, int); + __type(value, int); +}; + +struct { + __uint(type, BPF_MAP_TYPE_HASH_OF_MAPS); + __uint(max_entries, 1); /* placeholder, configured otherwise by nsresourced */ + __type(key, unsigned); /* userns inode */ + __array(values, struct mnt_id_map); +} userns_mnt_id_hash SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 4096); +} userns_ringbuf SEC(".maps"); + +static inline struct mount *real_mount(struct vfsmount *mnt) { + return container_of(mnt, struct mount, mnt); +} + +static int validate_inode_on_mount(struct inode *inode, struct vfsmount *v) { + struct user_namespace *mount_userns, *task_userns, *p; + unsigned task_userns_inode; + struct task_struct *task; + void *mnt_id_map; + struct mount *m; + int mnt_id; + + /* Get user namespace from vfsmount */ + m = bpf_rdonly_cast(real_mount(v), bpf_core_type_id_kernel(struct mount)); + mount_userns = m->mnt_ns->user_ns; + + /* Get user namespace from task */ + task = (struct task_struct*) bpf_get_current_task_btf(); + task_userns = task->cred->user_ns; + + /* Is the file on a mount that belongs to our own user namespace or a child of it? If so, say + * yes immediately. */ + p = mount_userns; + for (unsigned i = 0; i < USER_NAMESPACE_DEPTH_MAX; i++) { + if (p == task_userns) + return 0; /* our task's user namespace (or a child thereof) owns this superblock: allow! */ + + p = p->parent; + if (!p) + break; + } + + /* Hmm, something is fishy if there's more than 32 levels of namespaces involved. Let's better be + * safe than sorry, and refuse. */ + if (p) + return -EPERM; + + /* This is a mount foreign to our task's user namespace, let's consult our allow list */ + task_userns_inode = task_userns->ns.inum; + + mnt_id_map = bpf_map_lookup_elem(&userns_mnt_id_hash, &task_userns_inode); + if (!mnt_id_map) /* No rules installed for this userns? Then say yes, too! */ + return 0; + + mnt_id = m->mnt_id; + + /* Otherwise, say yes if the mount ID is allowlisted */ + if (bpf_map_lookup_elem(mnt_id_map, &mnt_id)) + return 0; + + return -EPERM; +} + +static int validate_path(const struct path *path, int ret) { + struct inode *inode; + struct vfsmount *v; + + if (ret != 0) /* propagate earlier error */ + return ret; + + inode = path->dentry->d_inode; + v = path->mnt; + + return validate_inode_on_mount(inode, v); +} + +SEC("lsm/path_chown") +int BPF_PROG(userns_restrict_path_chown, struct path *path, void* uid, void *gid, int ret) { + return validate_path(path, ret); +} + +SEC("lsm/path_mkdir") +int BPF_PROG(userns_restrict_path_mkdir, struct path *dir, struct dentry *dentry, umode_t mode, int ret) { + return validate_path(dir, ret); +} + +SEC("lsm/path_mknod") +int BPF_PROG(userns_restrict_path_mknod, const struct path *dir, struct dentry *dentry, umode_t mode, unsigned int dev, int ret) { + return validate_path(dir, ret); +} + +SEC("lsm/path_symlink") +int BPF_PROG(userns_restrict_path_symlink, const struct path *dir, struct dentry *dentry, const char *old_name, int ret) { + return validate_path(dir, ret); +} + +SEC("lsm/path_link") +int BPF_PROG(userns_restrict_path_link, struct dentry *old_dentry, const struct path *new_dir, struct dentry *new_dentry, int ret) { + return validate_path(new_dir, ret); +} + +SEC("kprobe/free_user_ns") +void BPF_KPROBE(userns_restrict_free_user_ns, struct work_struct *work) { + struct user_namespace *userns; + unsigned inode; + void *mnt_id_map; + + /* Inform userspace that a user namespace just went away. I wish there was a nicer way to hook into + * user namespaces being deleted than using kprobes, but couldn't find any. */ + + userns = bpf_rdonly_cast(container_of(work, struct user_namespace, work), + bpf_core_type_id_kernel(struct user_namespace)); + + inode = userns->ns.inum; + + mnt_id_map = bpf_map_lookup_elem(&userns_mnt_id_hash, &inode); + if (!mnt_id_map) /* No rules installed for this userns? Then send no notification. */ + return; + + bpf_ringbuf_output(&userns_ringbuf, &inode, sizeof(inode), 0); +} + +static const char _license[] SEC("license") = "GPL"; diff --git a/src/nsresourced/meson.build b/src/nsresourced/meson.build new file mode 100644 index 0000000000..cb131f0b9d --- /dev/null +++ b/src/nsresourced/meson.build @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +subdir('bpf/userns_restrict') + +systemd_nsresourcework_sources = files( + 'nsresourcework.c', + 'userns-restrict.c', + 'userns-registry.c', +) + +systemd_nsresourced_sources = files( + 'nsresourced-manager.c', + 'nsresourced.c', + 'userns-restrict.c', + 'userns-registry.c', +) + +userns_restrict_include = include_directories('.') + +if conf.get('HAVE_VMLINUX_H') == 1 + systemd_nsresourcework_sources += userns_restrict_skel_h + systemd_nsresourced_sources += userns_restrict_skel_h + + executables += [ + test_template + { + 'sources' : files('test-userns-restrict.c', 'userns-restrict.c') + userns_restrict_skel_h, + 'conditions' : ['ENABLE_NSRESOURCED', 'HAVE_VMLINUX_H'], + 'include_directories' : [ includes, userns_restrict_include ], + }, + ] +endif + +executables += [ + libexec_template + { + 'name' : 'systemd-nsresourcework', + 'conditions' : ['ENABLE_NSRESOURCED'], + 'sources' : systemd_nsresourcework_sources, + 'dependencies' : threads, + 'include_directories' : [ includes, userns_restrict_include ], + }, + libexec_template + { + 'name' : 'systemd-nsresourced', + 'conditions' : ['ENABLE_NSRESOURCED'], + 'sources' : systemd_nsresourced_sources, + 'dependencies' : threads, + 'include_directories' : [ includes, userns_restrict_include ], + }, +] diff --git a/src/nsresourced/nsresourced-manager.c b/src/nsresourced/nsresourced-manager.c new file mode 100644 index 0000000000..d87da586f7 --- /dev/null +++ b/src/nsresourced/nsresourced-manager.c @@ -0,0 +1,647 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "sd-daemon.h" + +#include "bpf-dlopen.h" +#include "build-path.h" +#include "common-signal.h" +#include "env-util.h" +#include "fd-util.h" +#include "fs-util.h" +#include "mkdir.h" +#include "nsresourced-manager.h" +#include "parse-util.h" +#include "process-util.h" +#include "recurse-dir.h" +#include "set.h" +#include "signal-util.h" +#include "socket-util.h" +#include "stat-util.h" +#include "stdio-util.h" +#include "strv.h" +#include "umask-util.h" +#include "unaligned.h" +#include "user-util.h" +#include "userns-registry.h" +#include "userns-restrict.h" + +#define LISTEN_TIMEOUT_USEC (25 * USEC_PER_SEC) + +static int start_workers(Manager *m, bool explicit_request); + +static int on_worker_exit(sd_event_source *s, const siginfo_t *si, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + assert(s); + + assert_se(!set_remove(m->workers_dynamic, s) != !set_remove(m->workers_fixed, s)); + sd_event_source_disable_unref(s); + + if (si->si_code == CLD_EXITED) { + if (si->si_status == EXIT_SUCCESS) + log_debug("Worker " PID_FMT " exited successfully.", si->si_pid); + else + log_warning("Worker " PID_FMT " died with a failure exit status %i, ignoring.", si->si_pid, si->si_status); + } else if (si->si_code == CLD_KILLED) + log_warning("Worker " PID_FMT " was killed by signal %s, ignoring.", si->si_pid, signal_to_string(si->si_status)); + else if (si->si_code == CLD_DUMPED) + log_warning("Worker " PID_FMT " dumped core by signal %s, ignoring.", si->si_pid, signal_to_string(si->si_status)); + else + log_warning("Got unexpected exit code via SIGCHLD, ignoring."); + + (void) start_workers(m, /* explicit_request= */ false); /* Fill up workers again if we fell below the low watermark */ + return 0; +} + +static int on_sigusr2(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + assert(s); + + (void) start_workers(m, /* explicit_request=*/ true); /* Workers told us there's more work, let's add one more worker as long as we are below the high watermark */ + return 0; +} + +static int on_deferred_start_worker(sd_event_source *s, uint64_t usec, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + assert(s); + + m->deferred_start_worker_event_source = sd_event_source_unref(m->deferred_start_worker_event_source); + + (void) start_workers(m, /* explicit_request=*/ false); + return 0; +} + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + event_source_hash_ops, + sd_event_source, + (void (*)(const sd_event_source*, struct siphash*)) trivial_hash_func, + (int (*)(const sd_event_source*, const sd_event_source*)) trivial_compare_func, + sd_event_source_disable_unref); + +int manager_new(Manager **ret) { + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + m = new(Manager, 1); + if (!m) + return -ENOMEM; + + *m = (Manager) { + .listen_fd = -EBADF, + .worker_ratelimit = { + .interval = 2 * USEC_PER_SEC, + .burst = 250, + }, + .registry_fd = -EBADF, + }; + + r = sd_event_new(&m->event); + if (r < 0) + return r; + + r = sd_event_set_signal_exit(m->event, true); + if (r < 0) + return r; + + r = sd_event_add_signal(m->event, NULL, (SIGRTMIN+18)|SD_EVENT_SIGNAL_PROCMASK, sigrtmin18_handler, NULL); + if (r < 0) + return r; + + r = sd_event_add_memory_pressure(m->event, NULL, NULL, NULL); + if (r < 0) + log_debug_errno(r, "Failed allocate memory pressure event source, ignoring: %m"); + + r = sd_event_set_watchdog(m->event, true); + if (r < 0) + log_debug_errno(r, "Failed to enable watchdog handling, ignoring: %m"); + + r = sd_event_add_signal(m->event, NULL, SIGUSR2|SD_EVENT_SIGNAL_PROCMASK, on_sigusr2, m); + if (r < 0) + return r; + + *ret = TAKE_PTR(m); + return 0; +} + +Manager* manager_free(Manager *m) { + if (!m) + return NULL; + + set_free(m->workers_fixed); + set_free(m->workers_dynamic); + + m->deferred_start_worker_event_source = sd_event_source_unref(m->deferred_start_worker_event_source); + + safe_close(m->listen_fd); + +#if HAVE_VMLINUX_H + sd_event_source_disable_unref(m->userns_restrict_bpf_ring_buffer_event_source); + if (m->userns_restrict_bpf_ring_buffer) + sym_ring_buffer__free(m->userns_restrict_bpf_ring_buffer); + userns_restrict_bpf_free(m->userns_restrict_bpf); +#endif + + safe_close(m->registry_fd); + + sd_event_unref(m->event); + + return mfree(m); +} + +static size_t manager_current_workers(Manager *m) { + assert(m); + + return set_size(m->workers_fixed) + set_size(m->workers_dynamic); +} + +static int start_one_worker(Manager *m) { + _cleanup_(sd_event_source_disable_unrefp) sd_event_source *source = NULL; + bool fixed; + pid_t pid; + int r; + + assert(m); + + fixed = set_size(m->workers_fixed) < NSRESOURCE_WORKERS_MIN; + + r = safe_fork_full( + "(sd-worker)", + /* stdio_fds= */ NULL, + &m->listen_fd, 1, + FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_REOPEN_LOG|FORK_LOG|FORK_CLOSE_ALL_FDS, + &pid); + if (r < 0) + return log_error_errno(r, "Failed to fork new worker child: %m"); + if (r == 0) { + char pids[DECIMAL_STR_MAX(pid_t)]; + /* Child */ + + if (m->listen_fd == 3) { + r = fd_cloexec(3, false); + if (r < 0) { + log_error_errno(r, "Failed to turn off O_CLOEXEC for fd 3: %m"); + _exit(EXIT_FAILURE); + } + } else { + if (dup2(m->listen_fd, 3) < 0) { /* dup2() creates with O_CLOEXEC off */ + log_error_errno(errno, "Failed to move listen fd to 3: %m"); + _exit(EXIT_FAILURE); + } + + safe_close(m->listen_fd); + } + + xsprintf(pids, PID_FMT, pid); + if (setenv("LISTEN_PID", pids, 1) < 0) { + log_error_errno(errno, "Failed to set $LISTEN_PID: %m"); + _exit(EXIT_FAILURE); + } + + if (setenv("LISTEN_FDS", "1", 1) < 0) { + log_error_errno(errno, "Failed to set $LISTEN_FDS: %m"); + _exit(EXIT_FAILURE); + } + + if (setenv("NSRESOURCE_FIXED_WORKER", one_zero(fixed), 1) < 0) { + log_error_errno(errno, "Failed to set $NSRESOURCE_FIXED_WORKER: %m"); + _exit(EXIT_FAILURE); + } + +#if HAVE_VMLINUX_H + bool supported = m->userns_restrict_bpf; +#else + bool supported = false; +#endif + + /* Tell the workers whether to enable the userns API */ + if (setenv("NSRESOURCE_API", one_zero(supported), 1) < 0) { + log_error_errno(errno, "Failed to set $NSRESOURCE_API: %m"); + _exit(EXIT_FAILURE); + } + + r = setenv_systemd_log_level(); + if (r < 0) { + log_error_errno(r, "Failed to set $SYSTEMD_LOG_LEVEL: %m"); + _exit(EXIT_FAILURE); + } + + r = invoke_callout_binary(SYSTEMD_NSRESOURCEWORK_PATH, STRV_MAKE("systemd-nsresourcework", "xxxxxxxxxxxxxxxx")); /* With some extra space rename_process() can make use of */ + log_error_errno(r, "Failed start worker process: %m"); + _exit(EXIT_FAILURE); + } + + r = sd_event_add_child(m->event, &source, pid, WEXITED, on_worker_exit, m); + if (r < 0) + return log_error_errno(r, "Failed to watch child " PID_FMT ": %m", pid); + + r = set_ensure_put( + fixed ? &m->workers_fixed : &m->workers_dynamic, + &event_source_hash_ops, + source); + if (r < 0) + return log_error_errno(r, "Failed to add child process to set: %m"); + + TAKE_PTR(source); + + return 0; +} + +static int start_workers(Manager *m, bool explicit_request) { + int r; + + assert(m); + + for (;;) { + size_t n; + + n = manager_current_workers(m); + if (n >= NSRESOURCE_WORKERS_MIN && (!explicit_request || n >= NSRESOURCE_WORKERS_MAX)) + break; + + if (!ratelimit_below(&m->worker_ratelimit)) { + + /* If we keep starting workers too often but none sticks, let's fail the whole + * daemon, something is wrong */ + if (n == 0) { + sd_event_exit(m->event, EXIT_FAILURE); + return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "Worker threads requested too frequently, but worker count is zero, something is wrong."); + } + + /* Otherwise, let's stop spawning more for a while. */ + log_warning("Worker threads requested too frequently, not starting new ones for a while."); + + if (!m->deferred_start_worker_event_source) { + r = sd_event_add_time( + m->event, + &m->deferred_start_worker_event_source, + CLOCK_MONOTONIC, + ratelimit_end(&m->worker_ratelimit), + /* accuracy_usec= */ 0, + on_deferred_start_worker, + m); + if (r < 0) + return log_error_errno(r, "Failed to allocate deferred start worker event source: %m"); + } + + break; + } + + r = start_one_worker(m); + if (r < 0) + return r; + + explicit_request = false; + } + + return 0; +} + +static void manager_release_userns_bpf(Manager *m, uint64_t inode) { +#if HAVE_VMLINUX_H + int r; + + assert(m); + + if (inode == 0) + return; + + assert(m->userns_restrict_bpf); + + r = userns_restrict_reset_by_inode(m->userns_restrict_bpf, inode); + if (r < 0) + return (void) log_warning_errno(r, "Failed to remove namespace inode from BPF map, ignoring: %m"); +#endif +} + +static void manager_release_userns_fds(Manager *m, uint64_t inode) { + int r; + + assert(m); + assert(inode != 0); + + r = sd_notifyf(/* unset_environment= */ false, + "FDSTOREREMOVE=1\n" + "FDNAME=userns-%" PRIu64 "\n", inode); + if (r < 0) + log_warning_errno(r, "Failed to send fd store removal message, ignoring: %m"); +} + +static void manager_release_userns_by_inode(Manager *m, uint64_t inode) { + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + _cleanup_close_ int lock_fd = -EBADF; + int r; + + assert(m); + assert(inode != 0); + + lock_fd = userns_registry_lock(m->registry_fd); + if (lock_fd < 0) + return (void) log_error_errno(lock_fd, "Failed to lock registry: %m"); + + r = userns_registry_load_by_userns_inode(m->registry_fd, inode, &userns_info); + if (r < 0) + log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r, + "Failed to find userns for inode %" PRIu64 ", ignoring: %m", inode); + + if (userns_info && uid_is_valid(userns_info->start)) + log_debug("Removing user namespace mapping %" PRIu64 " for UID " UID_FMT ".", inode, userns_info->start); + else + log_debug("Removing user namespace mapping %" PRIu64 ".", inode); + + /* Remove the BPF rules */ + manager_release_userns_bpf(m, inode); + + /* Remove the resources from the fdstore */ + manager_release_userns_fds(m, inode); + + /* And finally remove the resources file from disk */ + if (userns_info) { + /* Remove the cgroups of this userns */ + r = userns_info_remove_cgroups(userns_info); + if (r < 0) + log_warning_errno(r, "Failed to remove cgroups of user namespace: %m"); + + r = userns_registry_remove(m->registry_fd, userns_info); + if (r < 0) + log_warning_errno(r, "Failed to remove user namespace '%s', ignoring.", userns_info->name); + } +} + +static int manager_scan_registry(Manager *m, Set **registry_inodes) { + _cleanup_free_ DirectoryEntries *de = NULL; + int r; + + assert(m); + assert(registry_inodes); + assert(m->registry_fd >= 0); + + r = readdir_all(m->registry_fd, RECURSE_DIR_IGNORE_DOT, &de); + if (r < 0) + return log_error_errno(r, "Failed to enumerate registry."); + + for (size_t i = 0; i < de->n_entries; i++) { + struct dirent *dentry = de->entries[i]; + _cleanup_free_ char *u = NULL; + const char *e, *p; + uint64_t inode; + + p = startswith(dentry->d_name, "i"); + if (!p) + continue; + + e = endswith(p, ".userns"); + if (!e) + continue; + + u = strndup(p, e - p); + if (!u) + return log_oom(); + + r = safe_atou64(u, &inode); + if (r < 0) { + log_warning_errno(r, "Failed to parse userns inode number from '%s', skipping: %m", dentry->d_name); + continue; + } + + if (inode > UINT32_MAX) { /* namespace inode numbers are 23bit only right now */ + log_warning("userns inode number outside of 32bit range, skipping."); + continue; + } + + if (set_ensure_put(registry_inodes, NULL, UINT32_TO_PTR(inode)) < 0) + return log_oom(); + + log_debug("Found user namespace %" PRIu64 " in registry directory", inode); + } + + return 0; +} + +static int manager_make_listen_socket(Manager *m) { + static const union sockaddr_union sockaddr = { + .un.sun_family = AF_UNIX, + .un.sun_path = "/run/systemd/io.systemd.NamespaceResource", + }; + int r; + + assert(m); + + if (m->listen_fd >= 0) + return 0; + + m->listen_fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0); + if (m->listen_fd < 0) + return log_error_errno(errno, "Failed to bind on socket: %m"); + + (void) sockaddr_un_unlink(&sockaddr.un); + + WITH_UMASK(0000) + if (bind(m->listen_fd, &sockaddr.sa, SOCKADDR_UN_LEN(sockaddr.un)) < 0) + return log_error_errno(errno, "Failed to bind socket: %m"); + + r = mkdir_p("/run/systemd/userdb", 0755); + if (r < 0) + return log_error_errno(r, "Failed to create /run/systemd/userdb: %m"); + + r = symlink_idempotent("../io.systemd.NamespaceResource", "/run/systemd/userdb/io.systemd.NamespaceResource", /* make_relative= */ false); + if (r < 0) + return log_error_errno(r, "Failed to symlink userdb socket: %m"); + + if (listen(m->listen_fd, SOMAXCONN) < 0) + return log_error_errno(errno, "Failed to listen on socket: %m"); + + return 1; +} + +static int manager_scan_listen_fds(Manager *m, Set **fdstore_inodes) { + _cleanup_strv_free_ char **names = NULL; + int n, r; + + assert(m); + assert(fdstore_inodes); + + n = sd_listen_fds_with_names(/* unset_environment= */ true, &names); + if (n < 0) + return log_error_errno(n, "Failed to determine number of passed file descriptors: %m"); + + for (int i = 0; i < n; i++) { + _cleanup_close_ int fd = SD_LISTEN_FDS_START + i; /* Take possession */ + const char *e; + + /* If this is a BPF allowlist related fd, just close it, but remember which start UIDs this covers */ + e = startswith(names[i], "userns-"); + if (e) { + uint64_t inode; + + r = safe_atou64(e, &inode); + if (r < 0) { + log_warning_errno(r, "Failed to parse UID from fd name '%s', ignoring: %m", e); + continue; + } + + if (inode > UINT32_MAX) { + log_warning("Inode number outside of 32bit range, ignoring"); + continue; + } + + if (set_ensure_put(fdstore_inodes, NULL, UINT32_TO_PTR(inode)) < 0) + return log_oom(); + + continue; + } + + /* We don't check the name for the stream socket, for compatibility with older versions */ + r = sd_is_socket(fd, AF_UNIX, SOCK_STREAM, 1); + if (r < 0) + return log_error_errno(r, "Failed to detect if passed file descriptor is a socket: %m"); + if (r > 0) { + if (m->listen_fd >= 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Passed more than one AF_UNIX/SOCK_STREAM socket, refusing."); + + m->listen_fd = TAKE_FD(fd); + continue; + } + + log_warning("Closing passed file descriptor %i (%s) we don't recognize.", fd, names[i]); + } + + return 0; +} + +#if HAVE_VMLINUX_H +static int ringbuf_event(void *userdata, void *data, size_t size) { + Manager *m = ASSERT_PTR(userdata); + size_t n; + + if ((size % sizeof(unsigned int)) != 0) /* Not multiples of "unsigned int"? */ + return -EIO; + + n = size / sizeof(unsigned int); + for (size_t i = 0; i < n; i++) { + const void *d; + uint64_t inode; + + d = (const uint8_t*) data + i * sizeof(unsigned int); + inode = unaligned_read_ne32(d); + + log_debug("Got BPF ring buffer notification that user namespace %" PRIu64 " is now dead.", inode); + manager_release_userns_by_inode(m, inode); + } + + return 0; +} + +static int on_ringbuf_io(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + int r; + + r = sym_ring_buffer__poll(m->userns_restrict_bpf_ring_buffer, 0); + if (r < 0) + return log_error_errno(r, "Got failure reading from BPF ring buffer: %m"); + + return 0; +} + +static int manager_setup_bpf(Manager *m) { + int rb_fd = -EBADF, poll_fd = -EBADF, r; + + assert(m); + assert(!m->userns_restrict_bpf); + assert(!m->userns_restrict_bpf_ring_buffer); + assert(!m->userns_restrict_bpf_ring_buffer_event_source); + + r = userns_restrict_install(/* pin= */ true, &m->userns_restrict_bpf); + if (r < 0) { + log_notice_errno(r, "Proceeding with user namespace interfaces disabled."); + return 0; + } + + rb_fd = sym_bpf_map__fd(m->userns_restrict_bpf->maps.userns_ringbuf); + if (rb_fd < 0) + return log_error_errno(rb_fd, "Failed to get fd of ring buffer: %m"); + + m->userns_restrict_bpf_ring_buffer = sym_ring_buffer__new(rb_fd, ringbuf_event, m, NULL); + if (!m->userns_restrict_bpf_ring_buffer) + return log_error_errno(errno, "Failed to allocate BPF ring buffer object: %m"); + + poll_fd = sym_ring_buffer__epoll_fd(m->userns_restrict_bpf_ring_buffer); + if (poll_fd < 0) + return log_error_errno(poll_fd, "Failed to get poll fd of ring buffer: %m"); + + r = sd_event_add_io( + m->event, + &m->userns_restrict_bpf_ring_buffer_event_source, + poll_fd, + EPOLLIN, + on_ringbuf_io, + m); + if (r < 0) + return log_error_errno(r, "Failed to allocate event source for BPF ring buffer: %m"); + + return 0; +} +#else +static int manager_setup_bpf(Manager *m) { + log_notice("Not setting up BPF subsystem, as functionality has been disabled at compile time."); + return 0; +} +#endif + +int manager_startup(Manager *m) { + _cleanup_(set_freep) Set *fdstore_inodes = NULL, *registry_inodes = NULL; + void *p; + int r; + + assert(m); + assert(m->registry_fd < 0); + assert(m->listen_fd < 0); + + m->registry_fd = userns_registry_open_fd(); + if (m->registry_fd < 0) + return log_error_errno(m->registry_fd, "Failed to open registry directory: %m"); + + r = manager_setup_bpf(m); + if (r < 0) + return r; + + r = manager_scan_listen_fds(m, &fdstore_inodes); + if (r < 0) + return r; + + r = manager_scan_registry(m, ®istry_inodes); + if (r < 0) + return r; + + /* If there are resources tied to UIDs not found in the registry, then release them */ + SET_FOREACH(p, fdstore_inodes) { + uint64_t inode; + + if (set_contains(registry_inodes, p)) + continue; + + inode = PTR_TO_UINT32(p); + + log_debug("Found stale fd store entry for user namespace %" PRIu64 ", removing.", inode); + manager_release_userns_by_inode(m, inode); + } + + r = manager_make_listen_socket(m); + if (r < 0) + return r; + + /* Let's make sure every accept() call on this socket times out after 25s. This allows workers to be + * GC'ed on idle */ + if (setsockopt(m->listen_fd, SOL_SOCKET, SO_RCVTIMEO, TIMEVAL_STORE(LISTEN_TIMEOUT_USEC), sizeof(struct timeval)) < 0) + return log_error_errno(errno, "Failed to se SO_RCVTIMEO: %m"); + + r = start_workers(m, /* explicit_request= */ false); + if (r < 0) + return r; + + return 0; +} diff --git a/src/nsresourced/nsresourced-manager.h b/src/nsresourced/nsresourced-manager.h new file mode 100644 index 0000000000..5ecf378a60 --- /dev/null +++ b/src/nsresourced/nsresourced-manager.h @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-bus.h" +#include "sd-event.h" + +typedef struct Manager Manager; + +#include "hashmap.h" +#include "ratelimit.h" + +#define NSRESOURCE_WORKERS_MIN 5 +#define NSRESOURCE_WORKERS_MAX 4096 + +struct Manager { + sd_event *event; + + Set *workers_fixed; /* Workers 0…NSRESOURCE_WORKERS_MIN */ + Set *workers_dynamic; /* Workers NSRESOURCES_WORKERS_MIN+1…NSRESOURCES_WORKERS_MAX */ + + int listen_fd; + + RateLimit worker_ratelimit; + + sd_event_source *deferred_start_worker_event_source; + +#if HAVE_VMLINUX_H + struct userns_restrict_bpf *userns_restrict_bpf; + struct ring_buffer *userns_restrict_bpf_ring_buffer; + sd_event_source *userns_restrict_bpf_ring_buffer_event_source; +#endif + + int registry_fd; +}; + +int manager_new(Manager **ret); +Manager* manager_free(Manager *m); +DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); + +int manager_startup(Manager *m); diff --git a/src/nsresourced/nsresourced.c b/src/nsresourced/nsresourced.c new file mode 100644 index 0000000000..7056897243 --- /dev/null +++ b/src/nsresourced/nsresourced.c @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "daemon-util.h" +#include "nsresourced-manager.h" +#include "log.h" +#include "main-func.h" +#include "signal-util.h" + +static int run(int argc, char *argv[]) { + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + log_setup(); + + umask(0022); + + if (argc != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments."); + + if (setenv("SYSTEMD_BYPASS_USERDB", "io.systemd.NamespaceResource", 1) < 0) + return log_error_errno(errno, "Failed to set $SYSTEMD_BYPASS_USERDB: %m"); + + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD) >= 0); + + r = manager_new(&m); + if (r < 0) + return log_error_errno(r, "Could not create manager: %m"); + + r = manager_startup(m); + if (r < 0) + return log_error_errno(r, "Failed to start up daemon: %m"); + + _unused_ _cleanup_(notify_on_cleanup) const char *notify_stop = NULL; + notify_stop = notify_start(NOTIFY_READY, NOTIFY_STOPPING); + + r = sd_event_loop(m->event); + if (r < 0) + return log_error_errno(r, "Event loop failed: %m"); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/nsresourced/nsresourcework.c b/src/nsresourced/nsresourcework.c new file mode 100644 index 0000000000..9e68249ea8 --- /dev/null +++ b/src/nsresourced/nsresourcework.c @@ -0,0 +1,1782 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include +#include +#include +#include + +#include "sd-daemon.h" +#include "sd-netlink.h" + +#include "env-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "fs-util.h" +#include "group-record.h" +#include "io-util.h" +#include "lock-util.h" +#include "main-func.h" +#include "missing_magic.h" +#include "missing_mount.h" +#include "missing_syscall.h" +#include "mount-util.h" +#include "mountpoint-util.h" +#include "namespace-util.h" +#include "netlink-util.h" +#include "process-util.h" +#include "random-util.h" +#include "socket-util.h" +#include "stat-util.h" +#include "strv.h" +#include "time-util.h" +#include "uid-classification.h" +#include "uid-range.h" +#include "user-record-nss.h" +#include "user-record.h" +#include "user-util.h" +#include "userdb.h" +#include "userns-registry.h" +#include "userns-restrict.h" +#include "varlink-io.systemd.NamespaceResource.h" +#include "varlink-io.systemd.UserDatabase.h" +#include "varlink.h" + +#define ITERATIONS_MAX 64U +#define RUNTIME_MAX_USEC (5 * USEC_PER_MINUTE) +#define PRESSURE_SLEEP_TIME_USEC (50 * USEC_PER_MSEC) +#define CONNECTION_IDLE_USEC (15 * USEC_PER_SEC) +#define LISTEN_IDLE_USEC (90 * USEC_PER_SEC) +#define USERNS_PER_UID 256 + +typedef struct LookupParameters { + const char *user_name; + const char *group_name; + union { + uid_t uid; + gid_t gid; + }; + const char *service; +} LookupParameters; + +static int build_user_json(UserNamespaceInfo *userns_info, uid_t offset, JsonVariant **ret) { + _cleanup_free_ char *name = NULL, *realname = NULL; + UserDisposition disposition; + int r; + + assert(userns_info); + assert(offset < userns_info->size); + + if (asprintf(&name, "ns-%s-" UID_FMT, userns_info->name, offset) < 0) + return -ENOMEM; + + if (userns_info->size > 1) { + disposition = USER_CONTAINER; + r = asprintf(&realname, "User " UID_FMT " of Allocated Namespace %s", offset, userns_info->name); + } else { + disposition = USER_DYNAMIC; + r = asprintf(&realname, "Allocated Namespace %s", userns_info->name); + } + if (r < 0) + return -ENOMEM; + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(name)), + JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(userns_info->start + offset)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(GID_NOBODY)), + JSON_BUILD_PAIR("realName", JSON_BUILD_STRING(realname)), + JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_CONST_STRING("/")), + JSON_BUILD_PAIR("shell", JSON_BUILD_STRING(NOLOGIN)), + JSON_BUILD_PAIR("locked", JSON_BUILD_BOOLEAN(true)), + JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NamespaceResource")), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING(user_disposition_to_string(disposition))))); +} + +static int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, uid), 0 }, + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), 0 }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + LookupParameters p = { + .uid = UID_INVALID, + }; + uid_t offset; + int r; + + assert(parameters); + + r = varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.NamespaceResource")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + if (p.user_name) { + _cleanup_free_ char *n = NULL; + const char *e, *f; + + e = startswith(p.user_name, "ns-"); + if (!e) + goto not_found; + + f = strrchr(e, '-'); + if (!f) + goto not_found; + + if (parse_uid(f+1, &offset) < 0) + goto not_found; + + n = strndup(e, f - e); + if (!n) + return log_oom(); + + r = userns_registry_load_by_name( + /* registry_fd= */ -EBADF, + n, + &userns_info); + if (r == -ENOENT) + goto not_found; + if (r < 0) + return r; + + if (offset >= userns_info->size) /* Outside of range? */ + goto not_found; + + if (uid_is_valid(p.uid) && p.uid != userns_info->start + offset) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + } else if (uid_is_valid(p.uid)) { + uid_t start, uidmask; + + if (uid_is_container(p.uid)) + uidmask = (uid_t) UINT32_C(0xFFFF0000); + else if (uid_is_dynamic(p.uid)) + uidmask = (uid_t) UINT32_C(0xFFFFFFFF); + else + goto not_found; + + start = p.uid & uidmask; + offset = p.uid - start; + + r = userns_registry_load_by_start_uid( + /* registry_fd= */ -EBADF, + start, + &userns_info); + if (r == -ENOENT) + goto not_found; + if (r < 0) + return r; + + if (offset >= userns_info->size) /* Outside of range? */ + goto not_found; + } else + return varlink_error(link, "io.systemd.UserDatabase.EnumerationNotSupported", NULL); + + r = build_user_json(userns_info, offset, &v); + if (r < 0) + return r; + + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(v)))); + +not_found: + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); +} + +static int build_group_json(UserNamespaceInfo *userns_info, gid_t offset, JsonVariant **ret) { + _cleanup_free_ char *name = NULL, *description = NULL; + UserDisposition disposition; + int r; + + assert(userns_info); + assert(offset < userns_info->size); + + if (asprintf(&name, "ns-%s-" GID_FMT, userns_info->name, offset) < 0) + return -ENOMEM; + + if (userns_info->size > 1) { + disposition = USER_CONTAINER; + r = asprintf(&description, "Group " GID_FMT " of Allocated Namespace %s", offset, userns_info->name); + } else { + disposition = USER_DYNAMIC; + r = asprintf(&description, "Allocated Namespace %s", userns_info->name); + } + if (r < 0) + return -ENOMEM; + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(name)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(userns_info->start + offset)), + JSON_BUILD_PAIR("description", JSON_BUILD_STRING(description)), + JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NamespaceResource")), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING(user_disposition_to_string(disposition))))); +} + +static int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, gid), 0 }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), 0 }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + LookupParameters p = { + .gid = GID_INVALID, + }; + gid_t offset; + int r; + + assert(parameters); + + r = varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.NamespaceResource")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + if (p.group_name) { + _cleanup_free_ char *n = NULL; + const char *e, *f; + + e = startswith(p.group_name, "ns-"); + if (!e) + goto not_found; + + f = strrchr(e, '-'); + if (!f) + goto not_found; + + if (parse_gid(f+1, &offset) < 0) + goto not_found; + + n = strndup(e, f - e); + if (!n) + return log_oom(); + + r = userns_registry_load_by_name( + /* registry_fd= */ -EBADF, + n, + &userns_info); + if (r == -ENOENT) + goto not_found; + if (r < 0) + return r; + + if (offset >= userns_info->size) /* Outside of range? */ + goto not_found; + + if (gid_is_valid(p.gid) && p.uid != userns_info->start + offset) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + } else if (gid_is_valid(p.gid)) { + gid_t start, gidmask; + + if (gid_is_container(p.gid)) + gidmask = (gid_t) UINT32_C(0xFFFF0000); + else if (gid_is_dynamic(p.gid)) + gidmask = (gid_t) UINT32_C(0xFFFFFFFF); + else + goto not_found; + + start = p.gid & gidmask; + offset = p.gid - start; + + r = userns_registry_load_by_start_uid( + /* registry_fd= */ -EBADF, + (uid_t) start, + &userns_info); + if (r == -ENOENT) + goto not_found; + if (r < 0) + return r; + + if (offset >= userns_info->size) /* Outside of range? */ + goto not_found; + } else + return varlink_error(link, "io.systemd.UserDatabase.EnumerationNotSupported", NULL); + + r = build_group_json(userns_info, offset, &v); + if (r < 0) + return r; + + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(v)))); + +not_found: + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); +} + +static int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + static const JsonDispatch dispatch_table[] = { + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), 0 }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), 0 }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + LookupParameters p = {}; + int r; + + assert(parameters); + + r = varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.NamespaceResource")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + /* We don't support auxiliary groups for namespace allocations */ + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); +} + +static int uid_is_available( + int registry_dir_fd, + uid_t candidate) { + + int r; + + assert(registry_dir_fd >= 0); + + log_debug("Checking if UID " UID_FMT " is available.", candidate); + + r = userns_registry_uid_exists(registry_dir_fd, candidate); + if (r < 0) + return r; + if (r > 0) + return false; + + r = userdb_by_uid(candidate, USERDB_AVOID_MULTIPLEXER, NULL); + if (r >= 0) + return false; + if (r != -ESRCH) + return r; + + r = groupdb_by_gid(candidate, USERDB_AVOID_MULTIPLEXER, NULL); + if (r >= 0) + return false; + if (r != -ESRCH) + return r; + + log_debug("UID " UID_FMT " is available.", candidate); + + return true; +} + +static int name_is_available( + int registry_dir_fd, + const char *name) { + + _cleanup_free_ char *user_name = NULL; + int r; + + assert(registry_dir_fd >= 0); + assert(name); + + r = userns_registry_name_exists(registry_dir_fd, name); + if (r < 0) + return r; + if (r > 0) + return false; + + user_name = strjoin("ns-", name, "-0"); + if (!user_name) + return -ENOMEM; + + r = userdb_by_name(user_name, USERDB_AVOID_MULTIPLEXER, NULL); + if (r >= 0) + return false; + if (r != -ESRCH) + return r; + + r = groupdb_by_name(user_name, USERDB_AVOID_MULTIPLEXER, NULL); + if (r >= 0) + return false; + if (r != -ESRCH) + return r; + + log_debug("Namespace name '%s' is available.", name); + + return true; +} + +static int allocate_now( + int registry_dir_fd, + UserNamespaceInfo *info, + int *ret_lock_fd) { + + static const uint8_t hash_key[16] = { + 0xd4, 0xd7, 0x33, 0xa7, 0x4d, 0xd3, 0x42, 0xcd, + 0xaa, 0xe9, 0x45, 0xd0, 0xfb, 0xec, 0x79, 0xee, + }; + + _cleanup_(uid_range_freep) UIDRange *valid_range = NULL; + uid_t candidate, uidmin, uidmax, uidmask; + unsigned n_tries = 100; + int r; + + /* Returns the following error codes: + * + * EBUSY → all UID candidates we checked are already taken + * EEXIST → the name for the userns already exists + * EDEADLK → the userns is already registered in the registry + */ + + assert(registry_dir_fd >= 0); + assert(info); + + switch (info->size) { + + case 0x10000U: + uidmin = CONTAINER_UID_BASE_MIN; + uidmax = CONTAINER_UID_BASE_MAX; + uidmask = (uid_t) UINT32_C(0xFFFF0000); + break; + + case 1U: + uidmin = DYNAMIC_UID_MIN; + uidmax = DYNAMIC_UID_MAX; + uidmask = (uid_t) UINT32_C(0xFFFFFFFF); + break; + + default: + assert_not_reached(); + } + + r = uid_range_load_userns(&valid_range, /* path= */ NULL, UID_RANGE_USERNS_INSIDE); + if (r < 0) + return r; + + /* Check early whether we have any chance at all given our own uid range */ + if (!uid_range_overlaps(valid_range, uidmin, uidmax)) + return log_debug_errno(SYNTHETIC_ERRNO(EHOSTDOWN), "Relevant UID range not delegated, can't allocate."); + + _cleanup_close_ int lock_fd = -EBADF; + lock_fd = userns_registry_lock(registry_dir_fd); + if (lock_fd < 0) + return log_debug_errno(lock_fd, "Failed to open nsresource registry lock file: %m"); + + /* Enforce limit on user namespaces per UID */ + r = userns_registry_per_uid(registry_dir_fd, info->owner); + if (r < 0) + return log_debug_errno(r, "Failed to determine number of currently registered user namespaces per UID " UID_FMT ": %m", info->owner); + if (r >= USERNS_PER_UID) + return log_debug_errno(SYNTHETIC_ERRNO(EUSERS), "User already registered %i user namespaces, refusing.", r); + + r = userns_registry_inode_exists(registry_dir_fd, info->userns_inode); + if (r < 0) + return r; + if (r > 0) + return -EDEADLK; + + r = name_is_available(registry_dir_fd, info->name); + if (r < 0) + return r; + if (r == 0) + return -EEXIST; + + for (candidate = siphash24_string(info->name, hash_key) & UINT32_MAX;; /* Start from a hash of the input name */ + candidate = random_u32()) { /* Use random values afterwards */ + + if (--n_tries <= 0) + return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), "Try limit hit, no UIDs available."); + + candidate = (candidate % (uidmax - uidmin)) + uidmin; + candidate &= uidmask; + + if (!uid_range_covers(valid_range, candidate, info->size)) + continue; + + /* We only check the base UID for each range (!) */ + r = uid_is_available(registry_dir_fd, candidate); + if (r < 0) + return log_debug_errno(r, "Can't determine if UID range " UID_FMT " is available: %m", candidate); + if (r > 0) { + info->start = candidate; + + log_debug("Allocating UID range " UID_FMT "…" UID_FMT, candidate, candidate + info->size - 1); + + if (ret_lock_fd) + *ret_lock_fd = TAKE_FD(lock_fd); + + return 0; + } + + log_debug("UID range " UID_FMT " already taken.", candidate); + } +} + +static int write_userns(int usernsfd, const UserNamespaceInfo *userns_info) { + _cleanup_(sigkill_waitp) pid_t pid = 0; + _cleanup_close_ int efd = -EBADF; + uint64_t u; + int r; + + assert(usernsfd >= 0); + assert(userns_info); + assert(uid_is_valid(userns_info->target)); + assert(uid_is_valid(userns_info->start)); + assert(userns_info->size > 0); + assert(userns_info->size <= UINT32_MAX - userns_info->start); + + efd = eventfd(0, EFD_CLOEXEC); + if (efd < 0) + return log_error_errno(errno, "Failed to allocate eventfd(): %m"); + + r = safe_fork("(sd-userns)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL|FORK_LOG, &pid); + if (r < 0) + return r; + if (r == 0) { + /* child */ + + if (setns(usernsfd, CLONE_NEWUSER) < 0) { + log_error_errno(errno, "Failed to join user namespace: %m"); + goto child_fail; + } + + if (eventfd_write(efd, 1) < 0) { + log_error_errno(errno, "Failed to ping event fd: %m"); + goto child_fail; + } + + freeze(); + + child_fail: + _exit(EXIT_FAILURE); + } + + /* Wait until child joined the user namespace */ + if (eventfd_read(efd, &u) < 0) + return log_error_errno(errno, "Failed to wait for event fd: %m"); + + /* Now write mapping */ + + _cleanup_free_ char *pmap = NULL; + + if (asprintf(&pmap, "/proc/" PID_FMT "/uid_map", pid) < 0) + return log_oom(); + + r = write_string_filef(pmap, 0, UID_FMT " " UID_FMT " " UID_FMT "\n", userns_info->target, userns_info->start, userns_info->size); + if (r < 0) + return log_error_errno(r, "Failed to write 'uid_map' file of user namespace: %m"); + + pmap = mfree(pmap); + if (asprintf(&pmap, "/proc/" PID_FMT "/gid_map", pid) < 0) + return log_oom(); + + r = write_string_filef(pmap, 0, GID_FMT " " GID_FMT " " GID_FMT "\n", (gid_t) userns_info->target, (gid_t) userns_info->start, (gid_t) userns_info->size); + if (r < 0) + return log_error_errno(r, "Failed to write 'gid_map' file of user namespace: %m"); + + /* We are done! */ + + log_debug("Successfully configured user namespace."); + return 0; +} + +static int test_userns_api_support(Varlink *link) { + int r; + + assert(link); + + /* We only expose the userns API if our manager daemon told us this OK to do. It will set this + * boolean only if it managed to set up BPF correctly for itself (i.e. watches for userns going away + * via BPF APIs). This should make very sure we don't accidentally allow any of the userns stuff to + * go through without the BPF LSM in effect. */ + + r = getenv_bool("NSRESOURCE_API"); + if (r < 0) + return log_error_errno(r, "Failed to parse $NSRESOURCE_API: %m"); + if (r == 0) + return varlink_error(link, "io.systemd.NamespaceResource.UserNamespaceInterfaceNotSupported", NULL); + + return 0; +} + +static int validate_name(Varlink *link, const char *name, char **ret) { + _cleanup_free_ char *un = NULL; + int r; + + assert(link); + assert(name); + assert(ret); + + uid_t peer_uid; + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + + if (peer_uid == 0) { + if (!userns_name_is_valid(name)) + return varlink_error_invalid_parameter_name(link, "name"); + + un = strdup(name); + if (!un) + return -ENOMEM; + } else { + /* The the client is not root then prefix the name with the UID of the peer, so that they + * live in separate namespaces and cannot steal each other's names. */ + + if (asprintf(&un, UID_FMT "-%s", peer_uid, name) < 0) + return -ENOMEM; + + if (!userns_name_is_valid(un)) + return varlink_error_invalid_parameter_name(link, "name"); + } + + *ret = TAKE_PTR(un); + return 0; +} + +static int validate_target_and_size(Varlink *link, unsigned target, unsigned size) { + assert(link); + + if (!IN_SET(size, 1U, 0x10000)) + return varlink_error_invalid_parameter_name(link, "size"); + + if (!uid_is_valid(target) || target > UINT32_MAX - size) + return varlink_error_invalid_parameter_name(link, "target"); + + return 0; +} + +static int validate_userns(Varlink *link, int userns_fd) { + int r; + + assert(link); + assert(userns_fd >= 0); + + r = fd_verify_safe_flags(userns_fd); + if (r < 0) + return log_debug_errno(r, "User namespace file descriptor has unsafe flags set: %m"); + + /* Validate this is actually a valid user namespace fd */ + r = fd_is_ns(userns_fd, CLONE_NEWUSER); + if (r < 0) + return log_debug_errno(r, "Failed to check if user namespace fd is actually a user namespace: %m"); + if (r == 0) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + /* And refuse the thing if it is our own */ + r = is_our_namespace(userns_fd, NAMESPACE_USER); + if (r < 0) + return log_debug_errno(r, "Failed to check if user namespace fd refers to our own user namespace: %m"); + if (r > 0) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + uid_t peer_uid; + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return log_debug_errno(r, "Failed to acquire peer UID: %m"); + + if (peer_uid != 0) { + /* Refuse if the userns is not actually owned by our client. */ + uid_t owner_uid; + if (ioctl(userns_fd, NS_GET_OWNER_UID, &owner_uid) < 0) + return log_debug_errno(errno, "Failed to get owner UID of user namespace: %m"); + + if (owner_uid != peer_uid) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + } + + return 0; +} + +static int validate_userns_is_empty(Varlink *link, int userns_fd) { + int r; + + assert(link); + assert(userns_fd >= 0); + + _cleanup_(uid_range_freep) UIDRange *range = NULL; + r = uid_range_load_userns_by_fd(userns_fd, UID_RANGE_USERNS_OUTSIDE, &range); + if (r < 0) + return log_debug_errno(r, "Failed to read userns UID range: %m"); + + if (!uid_range_is_empty(range)) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + range = uid_range_free(range); + r = uid_range_load_userns_by_fd(userns_fd, GID_RANGE_USERNS_OUTSIDE, &range); + if (r < 0) + return log_debug_errno(r, "Failed to read userns GID range: %m"); + + if (!uid_range_is_empty(range)) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + return 0; +} + +typedef struct AllocateParameters { + const char *name; + unsigned size; + unsigned target; + unsigned userns_fd_idx; +} AllocateParameters; + +static int vl_method_allocate_user_range(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "name", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(AllocateParameters, name), JSON_MANDATORY }, + { "size", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AllocateParameters, size), JSON_MANDATORY }, + { "target", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AllocateParameters, target), 0 }, + { "userNamespaceFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AllocateParameters, userns_fd_idx), JSON_MANDATORY }, + {} + }; + + struct userns_restrict_bpf **bpf = ASSERT_PTR(userdata); + _cleanup_close_ int userns_fd = -EBADF, registry_dir_fd = -EBADF, lock_fd = -EBADF; + _cleanup_free_ char *userns_name = NULL; + uid_t peer_uid; + struct stat userns_st; + AllocateParameters p = { + .size = UINT_MAX, + .userns_fd_idx = UINT_MAX, + }; + int r; + + assert(link); + assert(parameters); + + r = test_userns_api_support(link); + if (r != 0) + return r; + + r = varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + r = validate_name(link, p.name, &userns_name); + if (r != 0) + return r; + + r = validate_target_and_size(link, p.target, p.size); + if (r != 0) + return r; + + userns_fd = varlink_take_fd(link, p.userns_fd_idx); + if (userns_fd < 0) + return log_debug_errno(userns_fd, "Failed to take user namespace fd from Varlink connection: %m"); + + r = validate_userns(link, userns_fd); + if (r != 0) + return r; + + r = validate_userns_is_empty(link, userns_fd); + if (r != 0) + return r; + + if (fstat(userns_fd, &userns_st) < 0) + return log_debug_errno(errno, "Failed to fstat() user namespace fd: %m"); + + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + + if (!*bpf) { + r = userns_restrict_install(/* pin= */ true, bpf); + if (r < 0) + return r; + } + + registry_dir_fd = userns_registry_open_fd(); + if (registry_dir_fd < 0) + return registry_dir_fd; + + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = userns_info_new(); + if (!userns_info) + return -ENOMEM; + + userns_info->name = TAKE_PTR(userns_name); + if (!userns_info->name) + return -ENOMEM; + + userns_info->owner = peer_uid; + userns_info->userns_inode = userns_st.st_ino; + userns_info->size = p.size; + userns_info->target = p.target; + + r = allocate_now(registry_dir_fd, userns_info, &lock_fd); + if (r == -EHOSTDOWN) /* The needed UID range is not delegated to us */ + return varlink_error(link, "io.systemd.NamespaceResource.DynamicRangeUnavailable", NULL); + if (r == -EBUSY) /* All used up */ + return varlink_error(link, "io.systemd.NamespaceResource.NoDynamicRange", NULL); + if (r == -EDEADLK) + return varlink_error(link, "io.systemd.NamespaceResource.UserNamespaceExists", NULL); + if (r == -EEXIST) + return varlink_error(link, "io.systemd.NamespaceResource.NameExists", NULL); + if (r < 0) + return r; + + r = userns_registry_store(registry_dir_fd, userns_info); + if (r < 0) + return r; + + /* Register the userns in the BPF map with an empty allowlist */ + r = userns_restrict_put_by_fd( + *bpf, + userns_fd, + /* replace= */ true, + /* mount_fds= */ NULL, + /* n_mount_fds= */ 0); + if (r < 0) + goto fail; + + r = write_userns(userns_fd, userns_info); + if (r < 0) + goto fail; + + lock_fd = safe_close(lock_fd); + + /* Send user namespace and process fd to our manager process, which will watch the process and user namespace */ + r = sd_pid_notifyf_with_fds( + /* pid= */ 0, + /* unset_environment= */ false, + &userns_fd, 1, + "FDSTORE=1\n" + "FDNAME=userns-" INO_FMT "\n", userns_info->userns_inode); + if (r < 0) + goto fail; + + /* Note, we'll not return UID values from the host, since the child might not run in the same + * user namespace as us. If they want to know the ranges they should read them off the userns fd, so + * that they are translated into their PoV */ + return varlink_replyb(link, JSON_BUILD_EMPTY_OBJECT); + +fail: + /* Note: we don't have to clean-up the BPF maps in the error path: the bpf map type used will + * automatically do that once the userns inode goes away */ + userns_registry_remove(registry_dir_fd, userns_info); + return r; +} + +static int validate_userns_is_safe(Varlink *link, int userns_fd) { + int r; + + assert(link); + assert(userns_fd >= 0); + + /* Read the outside UID range and verify it isn't empty */ + _cleanup_(uid_range_freep) UIDRange *outside_range = NULL; + r = uid_range_load_userns_by_fd(userns_fd, UID_RANGE_USERNS_OUTSIDE, &outside_range); + if (r < 0) + return log_debug_errno(r, "Failed to read userns UID range: %m"); + if (uid_range_is_empty(outside_range)) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + /* Read the outside GID range and check it is the same as the UID range */ + _cleanup_(uid_range_freep) UIDRange *outside_range_gid = NULL; + r = uid_range_load_userns_by_fd(userns_fd, GID_RANGE_USERNS_OUTSIDE, &outside_range_gid); + if (r < 0) + return log_debug_errno(r, "Failed to read userns GID range: %m"); + if (!uid_range_equal(outside_range, outside_range_gid)) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + /* Read the inside UID range, and verify it matches the size of the outside UID range */ + _cleanup_(uid_range_freep) UIDRange *inside_range = NULL; + r = uid_range_load_userns_by_fd(userns_fd, UID_RANGE_USERNS_INSIDE, &inside_range); + if (r < 0) + return log_debug_errno(r, "Failed to read userns contents: %m"); + if (uid_range_size(outside_range) != uid_range_size(inside_range)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Uh, inside and outside UID range sizes don't match."); + + /* Read the inside GID range, and verify it matches the inside UID range */ + _cleanup_(uid_range_freep) UIDRange *inside_range_gid = NULL; + r = uid_range_load_userns_by_fd(userns_fd, GID_RANGE_USERNS_INSIDE, &inside_range_gid); + if (r < 0) + return log_debug_errno(r, "Failed to read userns contents: %m"); + if (!uid_range_equal(inside_range, inside_range_gid)) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + uid_t peer_uid; + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + + uid_t peer_gid; + r = varlink_get_peer_gid(link, &peer_gid); + if (r < 0) + return r; + + /* Insist that the first UID/GID in the range matches the client's UID/GID */ + if (outside_range->entries[0].start != peer_uid || + outside_range_gid->entries[0].start != peer_gid) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + /* If there are more than one UID in the range, then also insist that the first UID maps to root inside the userns */ + if (uid_range_size(outside_range) > 1 && inside_range->entries[0].start != 0) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + return 0; +} + +typedef struct RegisterParameters { + const char *name; + unsigned userns_fd_idx; +} RegisterParameters; + +static int vl_method_register_user_namespace(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "name", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(RegisterParameters, name), JSON_MANDATORY }, + { "userNamespaceFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(RegisterParameters, userns_fd_idx), JSON_MANDATORY }, + {} + }; + + struct userns_restrict_bpf **bpf = ASSERT_PTR(userdata); + _cleanup_close_ int userns_fd = -EBADF, registry_dir_fd = -EBADF; + _cleanup_free_ char *userns_name = NULL; + uid_t peer_uid; + struct stat userns_st; + RegisterParameters p = { + .userns_fd_idx = UINT_MAX, + }; + int r; + + assert(link); + assert(parameters); + + r = test_userns_api_support(link); + if (r != 0) + return r; + + r = varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + r = validate_name(link, p.name, &userns_name); + if (r != 0) + return r; + + userns_fd = varlink_take_fd(link, p.userns_fd_idx); + if (userns_fd < 0) + return userns_fd; + + r = validate_userns(link, userns_fd); + if (r != 0) + return r; + + r = validate_userns_is_safe(link, userns_fd); + if (r != 0) + return r; + + if (fstat(userns_fd, &userns_st) < 0) + return log_debug_errno(errno, "Failed to fstat() user namespace fd: %m"); + + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + + if (!*bpf) { + r = userns_restrict_install(/* pin= */ true, bpf); + if (r < 0) + return r; + } + + registry_dir_fd = userns_registry_open_fd(); + if (registry_dir_fd < 0) + return registry_dir_fd; + + _cleanup_close_ int lock_fd = -EBADF; + lock_fd = userns_registry_lock(registry_dir_fd); + if (lock_fd < 0) + return log_debug_errno(lock_fd, "Failed to open nsresource registry lock file: %m"); + + r = userns_registry_inode_exists(registry_dir_fd, userns_st.st_ino); + if (r < 0) + return r; + if (r > 0) + return varlink_error(link, "io.systemd.NamespaceResource.UserNamespaceExists", NULL); + + r = name_is_available(registry_dir_fd, userns_name); + if (r < 0) + return r; + if (r == 0) + return varlink_error(link, "io.systemd.NamespaceResource.NameExists", NULL); + + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = userns_info_new(); + if (!userns_info) + return -ENOMEM; + + userns_info->name = TAKE_PTR(userns_name); + if (!userns_info->name) + return -ENOMEM; + + userns_info->owner = peer_uid; + userns_info->userns_inode = userns_st.st_ino; + + r = userns_registry_store(registry_dir_fd, userns_info); + if (r < 0) + return log_debug_errno(r, "Failed to update userns registry: %m"); + + /* Register the userns in the BPF map with an empty allowlist */ + r = userns_restrict_put_by_fd( + *bpf, + userns_fd, + /* replace= */ true, + /* mount_fds= */ NULL, + /* n_mount_fds= */ 0); + if (r < 0) + goto fail; + + /* Send user namespace and process fd to our manager process, which will watch the process and user namespace */ + r = sd_pid_notifyf_with_fds( + /* pid= */ 0, + /* unset_environment= */ false, + &userns_fd, 1, + "FDSTORE=1\n" + "FDNAME=userns-" INO_FMT "\n", userns_info->userns_inode); + if (r < 0) + goto fail; + + return varlink_replyb(link, JSON_BUILD_EMPTY_OBJECT); + +fail: + userns_registry_remove(registry_dir_fd, userns_info); + return r; +} + +typedef struct AddMountParameters { + unsigned userns_fd_idx; + unsigned mount_fd_idx; +} AddMountParameters; + +static int vl_method_add_mount_to_user_namespace(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch parameter_dispatch_table[] = { + { "userNamespaceFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AddMountParameters, userns_fd_idx), JSON_MANDATORY }, + { "mountFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AddMountParameters, mount_fd_idx), JSON_MANDATORY }, + {} + }; + + _cleanup_close_ int userns_fd = -EBADF, mount_fd = -EBADF, registry_dir_fd = -EBADF; + struct userns_restrict_bpf **bpf = ASSERT_PTR(userdata); + AddMountParameters p = { + .userns_fd_idx = UINT_MAX, + .mount_fd_idx = UINT_MAX, + }; + int r, mnt_id = 0; + struct stat userns_st; + uid_t peer_uid; + + assert(link); + assert(parameters); + + r = test_userns_api_support(link); + if (r != 0) + return r; + + /* Allowlisting arbitrary mounts is a privileged operation */ + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + if (peer_uid != 0) + return varlink_error(link, VARLINK_ERROR_PERMISSION_DENIED, NULL); + + r = varlink_dispatch(link, parameters, parameter_dispatch_table, &p); + if (r != 0) + return r; + + userns_fd = varlink_take_fd(link, p.userns_fd_idx); + if (userns_fd < 0) + return userns_fd; + + r = validate_userns(link, userns_fd); + if (r != 0) + return r; + + if (fstat(userns_fd, &userns_st) < 0) + return -errno; + + mount_fd = varlink_take_fd(link, p.mount_fd_idx); + if (mount_fd < 0) + return mount_fd; + + r = fd_verify_safe_flags_full(mount_fd, O_PATH|O_DIRECTORY); + if (r < 0) + return log_debug_errno(r, "Mount file descriptor has unsafe flags set: %m"); + + r = fd_verify_directory(mount_fd); + if (r < 0) + return r; + + r = path_get_mnt_id_at(mount_fd, NULL, &mnt_id); + if (r < 0) + return r; + + registry_dir_fd = userns_registry_open_fd(); + if (registry_dir_fd < 0) + return registry_dir_fd; + + _cleanup_close_ int lock_fd = -EBADF; + lock_fd = userns_registry_lock(registry_dir_fd); + if (lock_fd < 0) + return log_debug_errno(lock_fd, "Failed to open nsresource registry lock file: %m"); + + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + r = userns_registry_load_by_userns_inode( + registry_dir_fd, + userns_st.st_ino, + &userns_info); + if (r == -ENOENT) + return varlink_error(link, "io.systemd.NamespaceResource.UserNamespaceNotRegistered", NULL); + if (r < 0) + return r; + + if (!*bpf) { + r = userns_restrict_install(/* pin= */ true, bpf); + if (r < 0) + return r; + } + + /* Pin the mount fd */ + r = sd_pid_notifyf_with_fds( + /* pid= */ 0, + /* unset_environment= */ false, + &mount_fd, 1, + "FDSTORE=1\n" + "FDNAME=userns-" INO_FMT "\n", userns_st.st_ino); + if (r < 0) + return r; + + /* Add this mount to the user namespace's BPF map allowlist entry. */ + r = userns_restrict_put_by_fd( + *bpf, + userns_fd, + /* replace= */ false, + &mount_fd, + 1); + if (r < 0) + return r; + + if (userns_info->size > 0) + log_debug("Granting access to mount %i to user namespace " INO_FMT " ('%s' @ UID " UID_FMT ")", + mnt_id, userns_st.st_ino, userns_info->name, userns_info->start); + else + log_debug("Granting access to mount %i to user namespace " INO_FMT " ('%s')", + mnt_id, userns_st.st_ino, userns_info->name); + + return varlink_replyb(link, JSON_BUILD_EMPTY_OBJECT); +} + +static int validate_cgroup(Varlink *link, int fd, uint64_t *ret_cgroup_id) { + int r; + + assert(link); + assert(fd >= 0); + assert(ret_cgroup_id); + + r = fd_verify_safe_flags_full(fd, O_DIRECTORY); + if (r < 0) + return log_debug_errno(r, "Control group file descriptor has unsafe flags set: %m"); + + r = fd_verify_directory(fd); + if (r < 0) + return log_debug_errno(r, "Verification that cgroup fd refers to directory failed: %m"); + + r = fd_is_fs_type(fd, CGROUP2_SUPER_MAGIC); + if (r < 0) + return log_debug_errno(r, "Failed to check if cgroup fd actually refers to cgroupfs: %m"); + if (r == 0) + return varlink_error_invalid_parameter_name(link, "controlGroupFileDescriptor"); + + r = cg_fd_get_cgroupid(fd, ret_cgroup_id); + if (r < 0) + return log_debug_errno(r, "Failed to read cgroup ID from cgroupfs: %m"); + + return 0; +} + +typedef struct AddCGroupParameters { + unsigned userns_fd_idx; + unsigned cgroup_fd_idx; +} AddCGroupParameters; + +static int vl_method_add_cgroup_to_user_namespace(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + static const JsonDispatch parameter_dispatch_table[] = { + { "userNamespaceFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AddCGroupParameters, userns_fd_idx), JSON_MANDATORY }, + { "controlGroupFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AddCGroupParameters, cgroup_fd_idx), JSON_MANDATORY }, + {} + }; + + _cleanup_close_ int userns_fd = -EBADF, cgroup_fd = -EBADF, registry_dir_fd = -EBADF; + AddCGroupParameters p = { + .userns_fd_idx = UINT_MAX, + .cgroup_fd_idx = UINT_MAX, + }; + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + struct stat userns_st, cgroup_st; + uid_t peer_uid; + int r; + + assert(link); + assert(parameters); + + r = test_userns_api_support(link); + if (r != 0) + return r; + + r = varlink_dispatch(link, parameters, parameter_dispatch_table, &p); + if (r != 0) + return r; + + userns_fd = varlink_take_fd(link, p.userns_fd_idx); + if (userns_fd < 0) + return log_debug_errno(userns_fd, "Failed to take user namespace fd from Varlink connection: %m"); + + r = validate_userns(link, userns_fd); + if (r != 0) + return r; + + if (fstat(userns_fd, &userns_st) < 0) + return log_debug_errno(errno, "Failed to fstat() user namespace fd: %m"); + + cgroup_fd = varlink_take_fd(link, p.cgroup_fd_idx); + if (cgroup_fd < 0) + return log_debug_errno(cgroup_fd, "Failed to take cgroup fd from Varlink connection: %m"); + + uint64_t cgroup_id; + r = validate_cgroup(link, cgroup_fd, &cgroup_id); + if (r != 0) + return r; + + if (fstat(cgroup_fd, &cgroup_st) < 0) + return log_debug_errno(errno, "Failed to fstat() cgroup fd: %m"); + + registry_dir_fd = userns_registry_open_fd(); + if (registry_dir_fd < 0) + return registry_dir_fd; + + _cleanup_close_ int lock_fd = -EBADF; + lock_fd = userns_registry_lock(registry_dir_fd); + if (lock_fd < 0) + return lock_fd; + + r = userns_registry_load_by_userns_inode( + registry_dir_fd, + userns_st.st_ino, + &userns_info); + if (r == -ENOENT) + return varlink_error(link, "io.systemd.NamespaceResource.UserNamespaceNotRegistered", NULL); + if (r < 0) + return r; + + /* The user namespace must have a user assigned */ + if (userns_info->size == 0) + return varlink_error(link, "io.systemd.NamespaceResource.UserNamespaceWithoutUserRange", NULL); + if (userns_info_has_cgroup(userns_info, cgroup_id)) + return varlink_error(link, "io.systemd.NamespaceResource.ControlGroupAlreadyAdded", NULL); + if (userns_info->n_cgroups > USER_NAMESPACE_CGROUPS_DELEGATE_MAX) + return varlink_error(link, "io.systemd.NamespaceResource.TooManyControlGroups", NULL); + + /* Registering a cgroup for this client is only allowed for the root or the owner of a userns */ + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return log_debug_errno(r, "Failed to get connection peer: %m"); + if (peer_uid != 0) { + if (peer_uid != userns_info->owner) + return varlink_error(link, VARLINK_ERROR_PERMISSION_DENIED, NULL); + + /* The cgroup must be owned by the owner of the userns */ + if (cgroup_st.st_uid != userns_info->owner) + return varlink_error(link, VARLINK_ERROR_PERMISSION_DENIED, NULL); + } + + r = userns_info_add_cgroup(userns_info, cgroup_id); + if (r < 0) + return r; + + r = userns_registry_store(registry_dir_fd, userns_info); + if (r < 0) + return r; + + if (fchown(cgroup_fd, userns_info->start, userns_info->start) < 0) + return log_debug_errno(errno, "Failed to change ownership of cgroup: %m"); + + if (fchmod(cgroup_fd, 0755) < 0) + return log_debug_errno(errno, "Failed to change access mode of cgroup: %m"); + + FOREACH_STRING(attr, "cgroup.procs", "cgroup.subtree_control", "cgroup.threads") { + (void) fchmodat(cgroup_fd, attr, 0644, AT_SYMLINK_NOFOLLOW); + (void) fchownat(cgroup_fd, attr, userns_info->start, userns_info->start, AT_SYMLINK_NOFOLLOW); + } + + log_debug("Granting ownership to cgroup %" PRIu64 " to userns " INO_FMT " ('%s' @ UID " UID_FMT ")", + cgroup_id, userns_st.st_ino, userns_info->name, userns_info->start); + + return varlink_replyb(link, JSON_BUILD_EMPTY_OBJECT); +} + +static uint64_t hash_ifname_id(UserNamespaceInfo *userns_info, const char *ifname) { + struct siphash state; + + assert(userns_info); + + siphash24_init(&state, (const uint8_t[]) { 0xc4, 0x6c, 0x96, 0xe8, 0xad, 0x37, 0x4d, 0x5f, 0xa1, 0xae, 0xfe, 0x70, 0x40, 0xed, 0x41, 0x5f }); + siphash24_compress_string(userns_info->name, &state); + siphash24_compress_byte(0, &state); /* separator */ + siphash24_compress_string(strempty(ifname), &state); + + return siphash24_finalize(&state); +} + +static void hash_ether_addr(UserNamespaceInfo *userns_info, const char *ifname, uint64_t n, struct ether_addr *ret) { + struct siphash state; + uint64_t h; + + assert(userns_info); + assert(ret); + + siphash24_init(&state, (const uint8_t[]) { 0x36, 0xaa, 0xd1, 0x69, 0xc7, 0xe5, 0x4c, 0xaa, 0x1e, 0xb2, 0x9e, 0xb3, 0x3a, 0x6b, 0xd4, 0x71 }); + siphash24_compress_string(userns_info->name, &state); + siphash24_compress_byte(0, &state); /* separator */ + siphash24_compress_string(strempty(ifname), &state); + siphash24_compress_byte(0, &state); /* separator */ + n = htole64(n); /* add the 'index' to the mix in an endianess-independent fashion */ + siphash24_compress(&n, sizeof(n), &state); + + h = htole64(siphash24_finalize(&state)); + + assert(sizeof(h) >= sizeof_field(struct ether_addr, ether_addr_octet)); + + memcpy(ret->ether_addr_octet, &h, sizeof_field(struct ether_addr, ether_addr_octet)); + ether_addr_mark_random(ret); +} + +static int create_veth( + int netns_fd, + const char *ifname_host, + const char *altifname_host, + struct ether_addr *mac_host, + const char *ifname_namespace, + struct ether_addr *mac_namespace) { + + int r; + + assert(netns_fd >= 0); + assert(ifname_host); + assert(mac_host); + assert(ifname_namespace); + assert(mac_namespace); + + log_debug("Creating veth link on host %s (%s) with address %s to container as %s with address %s", + ifname_host, strna(altifname_host), ETHER_ADDR_TO_STR(mac_host), + ifname_namespace, ETHER_ADDR_TO_STR(mac_namespace)); + + _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; + r = sd_netlink_open(&rtnl); + if (r < 0) + return r; + + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + r = sd_rtnl_message_new_link(rtnl, &m, RTM_NEWLINK, 0); + if (r < 0) + return log_error_errno(r, "Failed to allocate netlink message: %m"); + + r = sd_netlink_message_append_string(m, IFLA_IFNAME, ifname_host); + if (r < 0) + return log_error_errno(r, "Failed to add netlink interface name: %m"); + + r = sd_netlink_message_append_ether_addr(m, IFLA_ADDRESS, mac_host); + if (r < 0) + return log_error_errno(r, "Failed to add netlink MAC address: %m"); + + r = sd_netlink_message_open_container(m, IFLA_LINKINFO); + if (r < 0) + return log_error_errno(r, "Failed to open netlink container: %m"); + + r = sd_netlink_message_open_container_union(m, IFLA_INFO_DATA, "veth"); + if (r < 0) + return log_error_errno(r, "Failed to open netlink container: %m"); + + r = sd_netlink_message_open_container(m, VETH_INFO_PEER); + if (r < 0) + return log_error_errno(r, "Failed to open netlink container: %m"); + + r = sd_netlink_message_append_string(m, IFLA_IFNAME, ifname_namespace); + if (r < 0) + return log_error_errno(r, "Failed to add netlink interface name: %m"); + + r = sd_netlink_message_append_ether_addr(m, IFLA_ADDRESS, mac_namespace); + if (r < 0) + return log_error_errno(r, "Failed to add netlink MAC address: %m"); + + r = sd_netlink_message_append_u32(m, IFLA_NET_NS_FD, netns_fd); + if (r < 0) + return log_error_errno(r, "Failed to add netlink namespace field: %m"); + + r = sd_netlink_message_close_container(m); + if (r < 0) + return log_error_errno(r, "Failed to close netlink container: %m"); + + r = sd_netlink_message_close_container(m); + if (r < 0) + return log_error_errno(r, "Failed to close netlink container: %m"); + + r = sd_netlink_message_close_container(m); + if (r < 0) + return log_error_errno(r, "Failed to close netlink container: %m"); + + r = sd_netlink_call(rtnl, m, 0, NULL); + if (r < 0) + return log_error_errno(r, "Failed to add new veth interfaces (%s:%s): %m", ifname_host, ifname_namespace); + + r = rtnl_set_link_alternative_names_by_ifname(&rtnl, ifname_host, STRV_MAKE(altifname_host)); + if (r < 0) + log_warning_errno(r, "Failed to set alternative interface name to '%s', ignoring: %m", altifname_host); + + return 0; +} + +static int validate_netns(Varlink *link, int userns_fd, int netns_fd) { + int r; + + assert(link); + assert(userns_fd >= 0); + assert(netns_fd >= 0); + + r = fd_verify_safe_flags(netns_fd); + if (r < 0) + return log_debug_errno(r, "Network namespace file descriptor has unsafe flags set: %m"); + + /* Validate this is actually a valid network namespace fd */ + r = fd_is_ns(netns_fd, CLONE_NEWNET); + if (r < 0) + return r; + if (r == 0) + return varlink_error_invalid_parameter_name(link, "networkNamespaceFileDescriptor"); + + /* And refuse the thing if it is our own */ + r = is_our_namespace(netns_fd, NAMESPACE_NET); + if (r < 0) + return r; + if (r > 0) + return varlink_error_invalid_parameter_name(link, "networkNamespaceFileDescriptor"); + + /* Check if the netns actually belongs to the userns */ + _cleanup_close_ int owner_userns_fd = -EBADF; + owner_userns_fd = ioctl(netns_fd, NS_GET_USERNS); + if (owner_userns_fd < 0) + return -errno; + + r = inode_same_at(owner_userns_fd, /* path_a= */ NULL, userns_fd, /* path_b= */ NULL, AT_EMPTY_PATH); + if (r < 0) + return r; + if (r == 0) + return varlink_error_invalid_parameter_name(link, "networkNamespaceFileDescriptor"); + + uid_t peer_uid; + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + + if (peer_uid != 0) { + /* Refuse if the netns is not actually owned by our client. */ + + uid_t owner_uid; + if (ioctl(owner_userns_fd, NS_GET_OWNER_UID, &owner_uid) < 0) + return -errno; + + if (owner_uid != peer_uid) + return varlink_error_invalid_parameter_name(link, "networkNamespaceFileDescriptor"); + } + + return 0; +} + +typedef struct AddNetworkParameters { + unsigned userns_fd_idx; + unsigned netns_fd_idx; + const char *ifname; + const char *mode; +} AddNetworkParameters; + +static int vl_method_add_netif_to_user_namespace(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + static const JsonDispatch parameter_dispatch_table[] = { + { "userNamespaceFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AddNetworkParameters, userns_fd_idx), JSON_MANDATORY }, + { "networkNamespaceFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AddNetworkParameters, netns_fd_idx), JSON_MANDATORY }, + { "namespaceInterfaceName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(AddNetworkParameters, ifname), 0 }, + { "mode", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(AddNetworkParameters, mode), JSON_MANDATORY }, + {} + }; + + _cleanup_close_ int userns_fd = -EBADF, netns_fd = -EBADF, registry_dir_fd = -EBADF; + AddNetworkParameters p = { + .userns_fd_idx = UINT_MAX, + }; + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + struct stat userns_st; + uid_t peer_uid; + int r; + + assert(link); + assert(parameters); + + r = test_userns_api_support(link); + if (r != 0) + return r; + + r = varlink_dispatch(link, parameters, parameter_dispatch_table, &p); + if (r != 0) + return r; + + userns_fd = varlink_take_fd(link, p.userns_fd_idx); + if (userns_fd < 0) + return userns_fd; + + r = validate_userns(link, userns_fd); + if (r != 0) + return r; + + if (fstat(userns_fd, &userns_st) < 0) + return -errno; + + netns_fd = varlink_take_fd(link, p.netns_fd_idx); + if (netns_fd < 0) + return netns_fd; + + r = validate_netns(link, userns_fd, netns_fd); + if (r != 0) + return r; + + if (!streq_ptr(p.mode, "veth")) + return varlink_error_invalid_parameter_name(link, "mode"); + + if (p.ifname && !ifname_valid(p.ifname)) + return varlink_error_invalid_parameter_name(link, "interfaceName"); + + registry_dir_fd = userns_registry_open_fd(); + if (registry_dir_fd < 0) + return registry_dir_fd; + + _cleanup_close_ int lock_fd = -EBADF; + lock_fd = userns_registry_lock(registry_dir_fd); + if (lock_fd < 0) + return log_debug_errno(lock_fd, "Failed to open nsresource registry lock file: %m"); + + r = userns_registry_load_by_userns_inode( + registry_dir_fd, + userns_st.st_ino, + &userns_info); + if (r == -ENOENT) + return varlink_error(link, "io.systemd.NamespaceResource.UserNamespaceNotRegistered", NULL); + if (r < 0) + return r; + + /* Registering a network interface for this client is only allowed for the root or the owner of a userns */ + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + if (peer_uid != 0 && peer_uid != userns_info->owner) + return varlink_error(link, VARLINK_ERROR_PERMISSION_DENIED, NULL); + + _cleanup_free_ char *ifname_host = NULL, *altifname_host = NULL; + const char *ifname_namespace = p.ifname ?: "host0"; + + /* The short ifname is just too short to generate readable and unique names where unprivileged users + * can't take each others names. Hence just hash it. The alternative name however contains more useful + * information. */ + if (asprintf(&ifname_host, "ns-%08" PRIx64, hash_ifname_id(userns_info, p.ifname)) < 0) + return -ENOMEM; + strshorten(ifname_host, IFNAMSIZ-1); + + if (p.ifname) + r = asprintf(&altifname_host, "ns-" UID_FMT "-%s-%s", userns_info->owner, userns_info->name, p.ifname); + else + r = asprintf(&altifname_host, "ns-" UID_FMT "-%s", userns_info->owner, userns_info->name); + if (r < 0) + return -ENOMEM; + + struct ether_addr ether_addr_host, ether_addr_namespace; + + hash_ether_addr(userns_info, p.ifname, 0, ðer_addr_host); + hash_ether_addr(userns_info, p.ifname, 1, ðer_addr_namespace); + + r = create_veth(netns_fd, + ifname_host, altifname_host, ðer_addr_host, + ifname_namespace, ðer_addr_namespace); + if (r < 0) + return r; + + log_debug("Adding veth tunnel %s from host to userns " INO_FMT " ('%s' @ UID " UID_FMT ", interface %s).", + ifname_host, userns_st.st_ino, userns_info->name, userns_info->start, ifname_namespace); + + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("hostInterfaceName", JSON_BUILD_STRING(ifname_host)), + JSON_BUILD_PAIR("namespaceInterfaceName", JSON_BUILD_STRING(ifname_namespace)))); +} + +static int process_connection(VarlinkServer *server, int _fd) { + _cleanup_close_ int fd = TAKE_FD(_fd); /* always take possession */ + _cleanup_(varlink_close_unrefp) Varlink *vl = NULL; + int r; + + r = varlink_server_add_connection(server, fd, &vl); + if (r < 0) + return log_error_errno(r, "Failed to add connection: %m"); + + TAKE_FD(fd); + vl = varlink_ref(vl); + + r = varlink_set_allow_fd_passing_input(vl, true); + if (r < 0) + return log_error_errno(r, "Failed to enable fd passing for read: %m"); + + r = varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_error_errno(r, "Failed to enable fd passing for write: %m"); + + for (;;) { + r = varlink_process(vl); + if (r == -ENOTCONN) { + log_debug("Connection terminated."); + break; + } + if (r < 0) + return log_error_errno(r, "Failed to process connection: %m"); + if (r > 0) + continue; + + r = varlink_wait(vl, CONNECTION_IDLE_USEC); + if (r < 0) + return log_error_errno(r, "Failed to wait for connection events: %m"); + if (r == 0) + break; + } + + return 0; +} + +static int run(int argc, char *argv[]) { + _cleanup_(userns_restrict_bpf_freep) struct userns_restrict_bpf *bpf = NULL; + usec_t start_time, listen_idle_usec, last_busy_usec = USEC_INFINITY; + _cleanup_(varlink_server_unrefp) VarlinkServer *server = NULL; + _cleanup_(pidref_done) PidRef parent = PIDREF_NULL; + unsigned n_iterations = 0; + int m, listen_fd, r; + + log_setup(); + + m = sd_listen_fds(false); + if (m < 0) + return log_error_errno(m, "Failed to determine number of listening fds: %m"); + if (m == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No socket to listen on received."); + if (m > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Worker can only listen on a single socket at a time."); + + listen_fd = SD_LISTEN_FDS_START; + + r = fd_nonblock(listen_fd, false); + if (r < 0) + return log_error_errno(r, "Failed to turn off non-blocking mode for listening socket: %m"); + + r = varlink_server_new(&server, VARLINK_SERVER_INHERIT_USERDATA); + if (r < 0) + return log_error_errno(r, "Failed to allocate server: %m"); + + r = varlink_server_add_interface_many( + server, + &vl_interface_io_systemd_NamespaceResource, + &vl_interface_io_systemd_UserDatabase); + if (r < 0) + return log_error_errno(r, "Failed to add UserDatabase and NamespaceResource interface to varlink server: %m"); + + r = varlink_server_bind_method_many( + server, + "io.systemd.NamespaceResource.AllocateUserRange", vl_method_allocate_user_range, + "io.systemd.NamespaceResource.RegisterUserNamespace", vl_method_register_user_namespace, + "io.systemd.NamespaceResource.AddMountToUserNamespace", vl_method_add_mount_to_user_namespace, + "io.systemd.NamespaceResource.AddControlGroupToUserNamespace", vl_method_add_cgroup_to_user_namespace, + "io.systemd.NamespaceResource.AddNetworkToUserNamespace", vl_method_add_netif_to_user_namespace, + "io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record, + "io.systemd.UserDatabase.GetGroupRecord", vl_method_get_group_record, + "io.systemd.UserDatabase.GetMemberships", vl_method_get_memberships); + if (r < 0) + return log_error_errno(r, "Failed to bind methods: %m"); + + varlink_server_set_userdata(server, &bpf); + + r = getenv_bool("NSRESOURCE_FIXED_WORKER"); + if (r < 0) + return log_error_errno(r, "Failed to parse NSRESOURCE_FIXED_WORKER: %m"); + listen_idle_usec = r ? USEC_INFINITY : LISTEN_IDLE_USEC; + + r = pidref_set_parent(&parent); + if (r < 0) + return log_error_errno(r, "Failed to acquire pidfd of parent process: %m"); + + start_time = now(CLOCK_MONOTONIC); + + for (;;) { + _cleanup_close_ int fd = -EBADF; + usec_t n; + + /* Exit the worker in regular intervals, to flush out all memory use */ + if (n_iterations++ > ITERATIONS_MAX) { + log_debug("Exiting worker, processed %u iterations, that's enough.", n_iterations); + break; + } + + n = now(CLOCK_MONOTONIC); + if (n >= usec_add(start_time, RUNTIME_MAX_USEC)) { + log_debug("Exiting worker, ran for %s, that's enough.", + FORMAT_TIMESPAN(usec_sub_unsigned(n, start_time), 0)); + break; + } + + if (last_busy_usec == USEC_INFINITY) + last_busy_usec = n; + else if (listen_idle_usec != USEC_INFINITY && n >= usec_add(last_busy_usec, listen_idle_usec)) { + log_debug("Exiting worker, been idle for %s.", + FORMAT_TIMESPAN(usec_sub_unsigned(n, last_busy_usec), 0)); + break; + } + + (void) rename_process("systemd-nsresourcework: waiting..."); + fd = RET_NERRNO(accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK|SOCK_CLOEXEC)); + (void) rename_process("systemd-nsresourcework: processing..."); + + if (fd == -EAGAIN) + continue; /* The listening socket has SO_RECVTIMEO set, hence a timeout is expected + * after a while, let's check if it's time to exit though. */ + if (fd == -EINTR) + continue; /* Might be that somebody attached via strace, let's just continue in that + * case */ + if (fd < 0) + return log_error_errno(fd, "Failed to accept() from listening socket: %m"); + + if (now(CLOCK_MONOTONIC) <= usec_add(n, PRESSURE_SLEEP_TIME_USEC)) { + /* We only slept a very short time? If so, let's see if there are more sockets + * pending, and if so, let's ask our parent for more workers */ + + r = fd_wait_for_event(listen_fd, POLLIN, 0); + if (r < 0) + return log_error_errno(r, "Failed to test for POLLIN on listening socket: %m"); + + if (FLAGS_SET(r, POLLIN)) { + r = pidref_kill(&parent, SIGUSR2); + if (r == -ESRCH) + return log_error_errno(r, "Parent already died?"); + if (r < 0) + return log_error_errno(r, "Failed to send SIGUSR2 signal to parent. %m"); + } + } + + (void) process_connection(server, TAKE_FD(fd)); + last_busy_usec = USEC_INFINITY; + } + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/nsresourced/test-userns-restrict.c b/src/nsresourced/test-userns-restrict.c new file mode 100644 index 0000000000..7ef1d7b423 --- /dev/null +++ b/src/nsresourced/test-userns-restrict.c @@ -0,0 +1,182 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "fd-util.h" +#include "main-func.h" +#include "missing_mount.h" +#include "missing_syscall.h" +#include "namespace-util.h" +#include "process-util.h" +#include "rm-rf.h" +#include "tmpfile-util.h" +#include "userns-restrict.h" + +static int make_tmpfs_fsmount(void) { + _cleanup_close_ int fsfd = -EBADF, mntfd = -EBADF; + + fsfd = fsopen("tmpfs", FSOPEN_CLOEXEC); + assert_se(fsfd >= 0); + assert_se(fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0) >= 0); + + mntfd = fsmount(fsfd, FSMOUNT_CLOEXEC, 0); + assert_se(mntfd >= 0); + + return TAKE_FD(mntfd); +} + +static void test_works_reg(int parent_fd, const char *fname) { + _cleanup_close_ int fd = -EBADF; + + fd = openat(parent_fd, fname, O_RDWR|O_CREAT|O_CLOEXEC, 0666); + assert_se(fd >= 0); +} + +static void test_fails_reg(int parent_fd, const char *fname) { + errno = 0; + assert_se(openat(parent_fd, fname, O_RDWR|O_CREAT|O_CLOEXEC, 0666) < 0); + assert_se(errno == EPERM); +} + +static void test_works_dir(int parent_fd, const char *fname) { + assert_se(mkdirat(parent_fd, fname, 0666) >= 0); +} + +static void test_fails_dir(int parent_fd, const char *fname) { + errno = 0; + assert_se(mkdirat(parent_fd, fname, 0666) < 0); + assert_se(errno == EPERM); +} + +static int run(int argc, char *argv[]) { + _cleanup_(userns_restrict_bpf_freep) struct userns_restrict_bpf *obj = NULL; + _cleanup_close_ int userns_fd = -EBADF, host_fd1 = -EBADF, host_tmpfs = -EBADF, afd = -EBADF, bfd = -EBADF; + _cleanup_(rm_rf_physical_and_freep) char *t = NULL; + _cleanup_(sigkill_waitp) pid_t pid = 0; + int r; + + log_set_max_level(LOG_DEBUG); + log_open(); + + r = userns_restrict_install(/* pin= */ false, &obj); + if (ERRNO_IS_NOT_SUPPORTED(r)) { + log_notice("Skipping test, LSM-BPF logic not supported."); + return EXIT_TEST_SKIP; + } + if (ERRNO_IS_PRIVILEGE(r)) { + log_notice("Skipping test, lacking privileges."); + return EXIT_TEST_SKIP; + } + if (r < 0) + return r; + + assert_se(mkdtemp_malloc(NULL, &t) >= 0); + + host_fd1 = open(t, O_DIRECTORY|O_CLOEXEC); + assert_se(host_fd1 >= 0); + + host_tmpfs = make_tmpfs_fsmount(); + assert_se(host_tmpfs >= 0); + + userns_fd = userns_acquire("0 0 1", "0 0 1"); + if (userns_fd < 0) + return log_error_errno(userns_fd, "Failed to make user namespace: %m"); + + r = userns_restrict_put_by_fd( + obj, + userns_fd, + /* replace= */ true, + /* mount_fds= */ NULL, + /* n_mount_fds= */ 0); + if (r < 0) + return log_error_errno(r, "Failed to restrict user namespace: %m"); + + afd = eventfd(0, EFD_CLOEXEC); + bfd = eventfd(0, EFD_CLOEXEC); + + assert_se(afd >= 0 && bfd >= 0); + + r = safe_fork("(test)", FORK_DEATHSIG_SIGKILL, &pid); + assert_se(r >= 0); + if (r == 0) { + _cleanup_close_ int private_tmpfs = -EBADF; + + assert_se(setns(userns_fd, CLONE_NEWUSER) >= 0); + assert_se(unshare(CLONE_NEWNS) >= 0); + + /* Allocate tmpfs locally */ + private_tmpfs = make_tmpfs_fsmount(); + + /* These two host mounts should be inaccessible */ + test_fails_reg(host_fd1, "test"); + test_fails_reg(host_tmpfs, "xxx"); + test_fails_dir(host_fd1, "test2"); + test_fails_dir(host_tmpfs, "xxx2"); + + /* But this mount created locally should be fine */ + test_works_reg(private_tmpfs, "yyy"); + test_works_dir(private_tmpfs, "yyy2"); + + /* Let's sync with the parent, so that it allowlists more stuff for us */ + assert_se(eventfd_write(afd, 1) >= 0); + uint64_t x; + assert_se(eventfd_read(bfd, &x) >= 0); + + /* And now we should also have access to the host tmpfs */ + test_works_reg(host_tmpfs, "zzz"); + test_works_reg(private_tmpfs, "aaa"); + test_works_dir(host_tmpfs, "zzz2"); + test_works_dir(private_tmpfs, "aaa2"); + + /* But this one should still fail */ + test_fails_reg(host_fd1, "bbb"); + test_fails_dir(host_fd1, "bbb2"); + + /* Sync again, to get more stuff allowlisted */ + assert_se(eventfd_write(afd, 1) >= 0); + assert_se(eventfd_read(bfd, &x) >= 0); + + /* Everything should now be allowed */ + test_works_reg(host_tmpfs, "ccc"); + test_works_reg(host_fd1, "ddd"); + test_works_reg(private_tmpfs, "eee"); + test_works_dir(host_tmpfs, "ccc2"); + test_works_reg(host_fd1, "ddd2"); + test_works_dir(private_tmpfs, "eee2"); + + _exit(EXIT_SUCCESS); + } + + uint64_t x; + assert_se(eventfd_read(afd, &x) >= 0); + + r = userns_restrict_put_by_fd( + obj, + userns_fd, + /* replace= */ false, + &host_tmpfs, + 1); + if (r < 0) + return log_error_errno(r, "Failed to loosen user namespace: %m"); + + assert_se(eventfd_write(bfd, 1) >= 0); + + assert_se(eventfd_read(afd, &x) >= 0); + + r = userns_restrict_put_by_fd( + obj, + userns_fd, + /* replace= */ false, + &host_fd1, + 1); + if (r < 0) + return log_error_errno(r, "Failed to loosen user namespace: %m"); + + assert_se(eventfd_write(bfd, 1) >= 0); + + assert_se(wait_for_terminate_and_check("(test)", pid, WAIT_LOG) >= 0); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/nsresourced/userns-registry.c b/src/nsresourced/userns-registry.c new file mode 100644 index 0000000000..2cc1b1f9ef --- /dev/null +++ b/src/nsresourced/userns-registry.c @@ -0,0 +1,646 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "chase.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-util.h" +#include "fs-util.h" +#include "json.h" +#include "missing_magic.h" +#include "path-util.h" +#include "recurse-dir.h" +#include "rm-rf.h" +#include "user-util.h" +#include "userns-registry.h" + +int userns_registry_open_fd(void) { + int fd; + + fd = chase_and_open( + "/run/systemd/nsresource/registry", + /* root= */ NULL, + CHASE_MKDIR_0755, + O_CLOEXEC|O_DIRECTORY|O_CREAT, + /* ret_path= */ NULL); + if (fd < 0) + return log_debug_errno(fd, "Failed to open registry dir: %m"); + + return fd; +} + +int userns_registry_lock(int dir_fd) { + _cleanup_close_ int registry_fd = -EBADF, lock_fd = -EBADF; + + if (dir_fd < 0) { + registry_fd = userns_registry_open_fd(); + if (registry_fd < 0) + return registry_fd; + + dir_fd = registry_fd; + } + + lock_fd = xopenat_lock_full(dir_fd, "lock", O_CREAT|O_RDWR|O_CLOEXEC, /* xopen_flags= */ 0, 0600, LOCK_BSD, LOCK_EX); + if (lock_fd < 0) + return log_debug_errno(lock_fd, "Failed to open nsresource registry lock file: %m"); + + return TAKE_FD(lock_fd); +} + +UserNamespaceInfo* userns_info_new(void) { + UserNamespaceInfo *info = new(UserNamespaceInfo, 1); + if (!info) + return NULL; + + *info = (UserNamespaceInfo) { + .owner = UID_INVALID, + .start = UID_INVALID, + .target = UID_INVALID, + }; + + return info; +} + +UserNamespaceInfo *userns_info_free(UserNamespaceInfo *userns) { + if (!userns) + return NULL; + + free(userns->cgroups); + free(userns->name); + + return mfree(userns); +} + +static int dispatch_cgroups_array(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + UserNamespaceInfo *info = ASSERT_PTR(userdata); + _cleanup_free_ uint64_t *cgroups = NULL; + size_t n_cgroups = 0; + + if (json_variant_is_null(variant)) { + info->cgroups = mfree(info->cgroups); + info->n_cgroups = 0; + return 0; + } + + if (!json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name)); + + cgroups = new(uint64_t, json_variant_elements(variant)); + if (!cgroups) + return json_log_oom(variant, flags); + + JsonVariant *e; + JSON_VARIANT_ARRAY_FOREACH(e, variant) { + bool found = false; + + if (!json_variant_is_unsigned(e)) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not a number."); + + FOREACH_ARRAY(cg, cgroups, n_cgroups) + if (*cg == json_variant_unsigned(e)) { + found = true; + break; + } + if (found) /* suppress duplicate */ + continue; + + cgroups[n_cgroups++] = json_variant_unsigned(e); + } + + assert(n_cgroups <= json_variant_elements(variant)); + + free_and_replace(info->cgroups, cgroups); + info->n_cgroups = n_cgroups; + + return 0; +} + +static int userns_registry_load(int dir_fd, const char *fn, UserNamespaceInfo **ret) { + + static const JsonDispatch dispatch_table[] = { + { "owner", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserNamespaceInfo, owner), JSON_MANDATORY }, + { "name", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserNamespaceInfo, name), JSON_MANDATORY }, + { "userns", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserNamespaceInfo, userns_inode), JSON_MANDATORY }, + { "start", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserNamespaceInfo, start), 0 }, + { "size", JSON_VARIANT_UNSIGNED, json_dispatch_uint32, offsetof(UserNamespaceInfo, size), 0 }, + { "target", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserNamespaceInfo, target), 0 }, + { "cgroups", JSON_VARIANT_ARRAY, dispatch_cgroups_array, 0, 0 }, + {} + }; + + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_close_ int registry_fd = -EBADF; + int r; + + if (dir_fd < 0) { + registry_fd = userns_registry_open_fd(); + if (registry_fd < 0) + return registry_fd; + + dir_fd = registry_fd; + } + + r = json_parse_file_at(NULL, dir_fd, fn, 0, &v, NULL, NULL); + if (r < 0) + return r; + + userns_info = userns_info_new(); + if (!userns_info) + return -ENOMEM; + + r = json_dispatch(v, dispatch_table, 0, userns_info); + if (r < 0) + return r; + + if (userns_info->userns_inode == 0) + return -EBADMSG; + if (userns_info->start == 0) + return -EBADMSG; + if (userns_info->size == 0) { + if (uid_is_valid(userns_info->start) || uid_is_valid(userns_info->target)) + return -EBADMSG; + } else { + if (!uid_is_valid(userns_info->start) || !uid_is_valid(userns_info->target)) + return -EBADMSG; + + if (userns_info->size > UINT32_MAX - userns_info->start || + userns_info->size > UINT32_MAX - userns_info->target) + return -EBADMSG; + } + + if (ret) + *ret = TAKE_PTR(userns_info); + return 0; +} + +int userns_registry_uid_exists(int dir_fd, uid_t start) { + _cleanup_free_ char *fn = NULL; + + assert(dir_fd >= 0); + + if (!uid_is_valid(start)) + return -ENOENT; + + if (start == 0) + return true; + + if (asprintf(&fn, "u" UID_FMT ".userns", start) < 0) + return -ENOMEM; + + if (faccessat(dir_fd, fn, F_OK, AT_SYMLINK_NOFOLLOW) < 0) + return errno == ENOENT ? false : -errno; + + return true; +} + +int userns_registry_name_exists(int dir_fd, const char *name) { + _cleanup_free_ char *fn = NULL; + + assert(dir_fd >= 0); + + if (!userns_name_is_valid(name)) + return -EINVAL; + + fn = strjoin("n", name, ".userns"); + if (!fn) + return -ENOMEM; + + if (faccessat(dir_fd, fn, F_OK, AT_SYMLINK_NOFOLLOW) < 0) + return errno == ENOENT ? false : -errno; + + return true; +} + +int userns_registry_inode_exists(int dir_fd, uint64_t inode) { + _cleanup_free_ char *fn = NULL; + + assert(dir_fd >= 0); + + if (inode <= 0) + return -EINVAL; + + if (asprintf(&fn, "i%" PRIu64 ".userns", inode) < 0) + return -ENOMEM; + + if (faccessat(dir_fd, fn, F_OK, AT_SYMLINK_NOFOLLOW) < 0) + return errno == ENOENT ? false : -errno; + + return true; +} + +int userns_registry_load_by_start_uid(int dir_fd, uid_t start, UserNamespaceInfo **ret) { + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + _cleanup_close_ int registry_fd = -EBADF; + _cleanup_free_ char *fn = NULL; + int r; + + if (!uid_is_valid(start)) + return -ENOENT; + + if (dir_fd < 0) { + registry_fd = userns_registry_open_fd(); + if (registry_fd < 0) + return registry_fd; + + dir_fd = registry_fd; + } + + if (asprintf(&fn, "u" UID_FMT ".userns", start) < 0) + return -ENOMEM; + + r = userns_registry_load(dir_fd, fn, &userns_info); + if (r < 0) + return r; + + if (userns_info->start != start) + return -EBADMSG; + + if (ret) + *ret = TAKE_PTR(userns_info); + + return 0; +} + +int userns_registry_load_by_userns_inode(int dir_fd, uint64_t inode, UserNamespaceInfo **ret) { + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + _cleanup_close_ int registry_fd = -EBADF; + _cleanup_free_ char *fn = NULL; + int r; + + if (inode == 0) + return -ENOENT; + + if (dir_fd < 0) { + registry_fd = userns_registry_open_fd(); + if (registry_fd < 0) + return registry_fd; + + dir_fd = registry_fd; + } + + if (asprintf(&fn, "i%" PRIu64 ".userns", inode) < 0) + return -ENOMEM; + + r = userns_registry_load(dir_fd, fn, &userns_info); + if (r < 0) + return r; + + if (userns_info->userns_inode != inode) + return -EBADMSG; + + if (ret) + *ret = TAKE_PTR(userns_info); + + return 0; +} + +int userns_registry_load_by_name(int dir_fd, const char *name, UserNamespaceInfo **ret) { + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + _cleanup_close_ int registry_fd = -EBADF; + _cleanup_free_ char *fn = NULL; + int r; + + assert(name); + + if (!userns_name_is_valid(name)) /* Invalid names never exist */ + return -ENOENT; + + if (dir_fd < 0) { + registry_fd = userns_registry_open_fd(); + if (registry_fd < 0) + return registry_fd; + + dir_fd = registry_fd; + } + + fn = strjoin("n", name, ".userns"); + if (!fn) + return -ENOMEM; + + r = userns_registry_load(dir_fd, fn, &userns_info); + if (r < 0) + return r; + + if (!streq_ptr(userns_info->name, name)) + return -EBADMSG; + + if (ret) + *ret = TAKE_PTR(userns_info); + + return 0; +} + +int userns_registry_store(int dir_fd, UserNamespaceInfo *info) { + _cleanup_close_ int registry_fd = -EBADF; + int r; + + assert(info); + + if (!uid_is_valid(info->owner) || + !info->name || + info->userns_inode == 0) + return -EINVAL; + + if (dir_fd < 0) { + registry_fd = userns_registry_open_fd(); + if (registry_fd < 0) + return registry_fd; + + dir_fd = registry_fd; + } + + _cleanup_(json_variant_unrefp) JsonVariant *cgroup_array = NULL; + FOREACH_ARRAY(cg, info->cgroups, info->n_cgroups) { + r = json_variant_append_arrayb( + &cgroup_array, + JSON_BUILD_UNSIGNED(*cg)); + if (r < 0) + return r; + } + + _cleanup_(json_variant_unrefp) JsonVariant *def = NULL; + r = json_build(&def, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("owner", JSON_BUILD_UNSIGNED(info->owner)), + JSON_BUILD_PAIR("name", JSON_BUILD_STRING(info->name)), + JSON_BUILD_PAIR("userns", JSON_BUILD_UNSIGNED(info->userns_inode)), + JSON_BUILD_PAIR_CONDITION(uid_is_valid(info->start), "start", JSON_BUILD_UNSIGNED(info->start)), + JSON_BUILD_PAIR_CONDITION(uid_is_valid(info->start), "size", JSON_BUILD_UNSIGNED(info->size)), + JSON_BUILD_PAIR_CONDITION(uid_is_valid(info->start), "target", JSON_BUILD_UNSIGNED(info->target)), + JSON_BUILD_PAIR_CONDITION(cgroup_array, "cgroups", JSON_BUILD_VARIANT(cgroup_array)))); + if (r < 0) + return r; + + _cleanup_free_ char *def_buf = NULL; + r = json_variant_format(def, 0, &def_buf); + if (r < 0) + return log_debug_errno(r, "Failed to format userns JSON object: %m"); + + _cleanup_free_ char *reg_fn = NULL, *link1_fn = NULL, *link2_fn = NULL, *owner_fn = NULL, *uid_fn = NULL; + if (asprintf(®_fn, "i%" PRIu64 ".userns", info->userns_inode) < 0) + return log_oom_debug(); + + r = write_string_file_at(dir_fd, reg_fn, def_buf, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC); + if (r < 0) + return log_debug_errno(r, "Failed to write userns data to '%s' in registry: %m", reg_fn); + + link1_fn = strjoin("n", info->name, ".userns"); + if (!link1_fn) { + r = log_oom_debug(); + goto fail; + } + + r = linkat_replace(dir_fd, reg_fn, dir_fd, link1_fn); + if (r < 0) { + log_debug_errno(r, "Failed to link userns data to '%s' in registry: %m", link1_fn); + goto fail; + } + + if (uid_is_valid(info->start)) { + if (asprintf(&link2_fn, "u" UID_FMT ".userns", info->start) < 0) { + r = log_oom_debug(); + goto fail; + } + + r = linkat_replace(dir_fd, reg_fn, dir_fd, link2_fn); + if (r < 0) { + log_debug_errno(r, "Failed to link userns data to '%s' in registry: %m", link2_fn); + goto fail; + } + } + + if (asprintf(&uid_fn, "o" UID_FMT ".owns", info->owner) < 0) { + r = log_oom_debug(); + goto fail; + } + + if (mkdirat(dir_fd, uid_fn, 0755) < 0 && errno != EEXIST) { + r = log_debug_errno(errno, "Failed to create per-UID subdir '%s' of registry: %m", uid_fn); + goto fail; + } + + if (asprintf(&owner_fn, "%s/i%" PRIu64 ".userns", uid_fn, info->userns_inode) < 0) { + r = log_oom_debug(); + goto fail; + } + + r = linkat_replace(dir_fd, reg_fn, dir_fd, owner_fn); + if (r < 0) { + log_debug_errno(r, "Failed to link userns data to '%s' in registry: %m", owner_fn); + goto fail; + } + + return 0; + +fail: + if (reg_fn) + (void) unlinkat(dir_fd, reg_fn, /* flags= */ 0); + if (link1_fn) + (void) unlinkat(dir_fd, link1_fn, /* flags= */ 0); + if (link2_fn) + (void) unlinkat(dir_fd, link2_fn, /* flags= */ 0); + if (owner_fn) + (void) unlinkat(dir_fd, owner_fn, /* flags= */ 0); + if (uid_fn) + (void) unlinkat(dir_fd, uid_fn, AT_REMOVEDIR); + + return r; +} + +int userns_registry_remove(int dir_fd, UserNamespaceInfo *info) { + _cleanup_close_ int registry_fd = -EBADF; + int ret = 0, r; + + assert(info); + + if (dir_fd < 0) { + registry_fd = userns_registry_open_fd(); + if (registry_fd < 0) + return registry_fd; + + dir_fd = registry_fd; + } + + _cleanup_free_ char *reg_fn = NULL; + if (asprintf(®_fn, "i%" PRIu64 ".userns", info->userns_inode) < 0) + return log_oom_debug(); + + ret = RET_NERRNO(unlinkat(dir_fd, reg_fn, 0)); + + _cleanup_free_ char *link1_fn = NULL; + link1_fn = strjoin("n", info->name, ".userns"); + if (!link1_fn) + return log_oom_debug(); + + RET_GATHER(ret, RET_NERRNO(unlinkat(dir_fd, link1_fn, 0))); + + if (uid_is_valid(info->start)) { + _cleanup_free_ char *link2_fn = NULL; + + if (asprintf(&link2_fn, "u" UID_FMT ".userns", info->start) < 0) + return log_oom_debug(); + + RET_GATHER(ret, RET_NERRNO(unlinkat(dir_fd, link2_fn, 0))); + } + + _cleanup_free_ char *uid_fn = NULL; + if (asprintf(&uid_fn, "o" UID_FMT ".owns", info->owner) < 0) + return log_oom_debug(); + + _cleanup_free_ char *owner_fn = NULL; + if (asprintf(&owner_fn, "%s/i%" PRIu64 ".userns", uid_fn, info->userns_inode) < 0) + return log_oom_debug(); + + RET_GATHER(ret, RET_NERRNO(unlinkat(dir_fd, owner_fn, 0))); + + r = RET_NERRNO(unlinkat(dir_fd, uid_fn, AT_REMOVEDIR)); + if (r != -ENOTEMPTY) + RET_GATHER(ret, r); + + return ret; +} + +bool userns_info_has_cgroup(UserNamespaceInfo *userns, uint64_t cgroup_id) { + assert(userns); + + FOREACH_ARRAY(i, userns->cgroups, userns->n_cgroups) + if (*i == cgroup_id) + return true; + + return false; +} + +int userns_info_add_cgroup(UserNamespaceInfo *userns, uint64_t cgroup_id) { + + if (userns_info_has_cgroup(userns, cgroup_id)) + return 0; + + if (!GREEDY_REALLOC(userns->cgroups, userns->n_cgroups+1)) + return -ENOMEM; + + userns->cgroups[userns->n_cgroups++] = cgroup_id; + return 1; +} + +static int userns_destroy_cgroup(uint64_t cgroup_id) { + _cleanup_close_ int cgroup_fd = -EBADF, parent_fd = -EBADF; + int r; + + cgroup_fd = cg_cgroupid_open(/* cgroupfsfd= */ -EBADF, cgroup_id); + if (cgroup_fd == -ESTALE) { + log_debug_errno(cgroup_fd, "Control group %" PRIu64 " already gone, ignoring: %m", cgroup_id); + return 0; + } + if (cgroup_fd < 0) + return log_debug_errno(errno, "Failed to open cgroup %" PRIu64 ", ignoring: %m", cgroup_id); + + _cleanup_free_ char *path = NULL; + r = fd_get_path(cgroup_fd, &path); + if (r < 0) + return log_debug_errno(r, "Failed to get path of cgroup %" PRIu64 ", ignoring: %m", cgroup_id); + + const char *e = path_startswith(path, "/sys/fs/cgroup/"); + if (!e) + return log_debug_errno(SYNTHETIC_ERRNO(EPERM), "Got cgroup path that doesn't start with /sys/fs/cgroup/, refusing: %s", path); + if (isempty(e)) + return log_debug_errno(SYNTHETIC_ERRNO(EPERM), "Got root cgroup path, which can't be right, refusing."); + + log_debug("Path of cgroup %" PRIu64 " is: %s", cgroup_id, path); + + _cleanup_free_ char *fname = NULL; + r = path_extract_filename(path, &fname); + if (r < 0) + return log_debug_errno(r, "Failed to extract name of cgroup %" PRIu64 ", ignoring: %m", cgroup_id); + + parent_fd = openat(cgroup_fd, "..", O_CLOEXEC|O_DIRECTORY); + if (parent_fd < 0) + return log_debug_errno(errno, "Failed to open parent cgroup of %" PRIu64 ", ignoring: %m", cgroup_id); + + /* Safety check, never leave cgroupfs */ + r = fd_is_fs_type(parent_fd, CGROUP2_SUPER_MAGIC); + if (r < 0) + return log_debug_errno(r, "Failed to determine if parent directory of cgroup %" PRIu64 " is still a cgroup, ignoring: %m", cgroup_id); + if (!r) + return log_debug_errno(SYNTHETIC_ERRNO(EPERM), "Parent directory of cgroup %" PRIu64 " is not a cgroup, refusing.", cgroup_id); + + cgroup_fd = safe_close(cgroup_fd); + + r = rm_rf_child(parent_fd, fname, REMOVE_ONLY_DIRECTORIES|REMOVE_PHYSICAL|REMOVE_CHMOD); + if (r < 0) + log_debug_errno(r, "Failed to remove delegated cgroup %" PRIu64 ", ignoring: %m", cgroup_id); + + return 0; +} + +int userns_info_remove_cgroups(UserNamespaceInfo *userns) { + int ret = 0; + + assert(userns); + + FOREACH_ARRAY(c, userns->cgroups, userns->n_cgroups) + RET_GATHER(ret, userns_destroy_cgroup(*c)); + + userns->cgroups = mfree(userns->cgroups); + userns->n_cgroups = 0; + + return ret; +} + +bool userns_name_is_valid(const char *name) { + + /* Checks if the specified string is suitable as user namespace name. */ + + if (strlen(name) > NAME_MAX) /* before we use alloca(), let's check for size */ + return false; + + const char *f = strjoina("n", name, ".userns"); /* Make sure we can name our lookup symlink with this name */ + if (!filename_is_valid(f)) + return false; + + const char *u = strjoina("ns-", name, "-65535"); /* Make sure we can turn this into valid user names */ + if (!valid_user_group_name(u, 0)) + return false; + + return true; +} + +int userns_registry_per_uid(int dir_fd, uid_t owner) { + _cleanup_close_ int registry_fd = -EBADF; + int n = 0, r; + + if (dir_fd < 0) { + registry_fd = userns_registry_open_fd(); + if (registry_fd < 0) + return registry_fd; + + dir_fd = registry_fd; + } + + _cleanup_free_ char *uid_fn = NULL; + if (asprintf(&uid_fn, "o" UID_FMT ".owns", owner) < 0) + return log_oom_debug(); + + _cleanup_free_ DirectoryEntries *de = NULL; + + r = readdir_all_at(dir_fd, uid_fn, RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &de); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_debug_errno(r, "Failed to enumerate contents of '%s' sub-directory: %m", uid_fn); + + FOREACH_ARRAY(i, de->entries, de->n_entries) { + struct dirent *e = *i; + + if (e->d_type != DT_REG) + continue; + + if (!startswith(e->d_name, "i") || !endswith(e->d_name, ".userns")) + continue; + + n++; + + if (n == INT_MAX) /* overflow safety check, just in case */ + break; + } + + return n; +} diff --git a/src/nsresourced/userns-registry.h b/src/nsresourced/userns-registry.h new file mode 100644 index 0000000000..9e66a6feec --- /dev/null +++ b/src/nsresourced/userns-registry.h @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#define USER_NAMESPACE_CGROUPS_DELEGATE_MAX 16 + +typedef struct UserNamespaceInfo { + uid_t owner; + char *name; + uint64_t userns_inode; + uid_t start; + uint32_t size; + uid_t target; + uint64_t *cgroups; + size_t n_cgroups; +} UserNamespaceInfo; + +UserNamespaceInfo* userns_info_new(void); +UserNamespaceInfo* userns_info_free(UserNamespaceInfo *userns); + +DEFINE_TRIVIAL_CLEANUP_FUNC(UserNamespaceInfo*, userns_info_free); + +bool userns_info_has_cgroup(UserNamespaceInfo *userns, uint64_t cgroup_id); +int userns_info_add_cgroup(UserNamespaceInfo *userns, uint64_t cgroup_id); +int userns_info_remove_cgroups(UserNamespaceInfo *userns); + +bool userns_name_is_valid(const char *name); + +int userns_registry_open_fd(void); +int userns_registry_lock(int dir_fd); + +int userns_registry_load_by_start_uid(int dir_fd, uid_t start, UserNamespaceInfo **ret); +int userns_registry_load_by_userns_inode(int dir_fd, uint64_t userns, UserNamespaceInfo **ret); +int userns_registry_load_by_name(int dir_fd, const char *name, UserNamespaceInfo **ret); + +int userns_registry_store(int dir_fd, UserNamespaceInfo *info); +int userns_registry_remove(int dir_fd, UserNamespaceInfo *info); + +int userns_registry_inode_exists(int dir_fd, uint64_t inode); +int userns_registry_name_exists(int dir_fd, const char *name); +int userns_registry_uid_exists(int dir_fd, uid_t start); + +int userns_registry_per_uid(int dir_fd, uid_t owner); diff --git a/src/nsresourced/userns-restrict.c b/src/nsresourced/userns-restrict.c new file mode 100644 index 0000000000..4e917fd758 --- /dev/null +++ b/src/nsresourced/userns-restrict.c @@ -0,0 +1,346 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "userns-restrict.h" + +#if HAVE_VMLINUX_H + +#include + +#include "bpf-dlopen.h" +#include "bpf-link.h" +#include "fd-util.h" +#include "fs-util.h" +#include "lsm-util.h" +#include "missing_mount.h" +#include "mkdir.h" +#include "mount-util.h" +#include "mountpoint-util.h" +#include "namespace-util.h" +#include "path-util.h" + +#define USERNS_MAX (16U*1024U) +#define MOUNTS_MAX 4096U + +#define PROGRAM_LINK_PREFIX "/sys/fs/bpf/systemd/userns-restrict/programs" +#define MAP_LINK_PREFIX "/sys/fs/bpf/systemd/userns-restrict/maps" + +struct userns_restrict_bpf *userns_restrict_bpf_free(struct userns_restrict_bpf *obj) { + (void) userns_restrict_bpf__destroy(obj); /* this call is fine with NULL */ + return NULL; +} + +static int make_inner_hash_map(void) { + int fd; + + fd = compat_bpf_map_create( + BPF_MAP_TYPE_HASH, + NULL, + sizeof(int), + sizeof(uint32_t), + MOUNTS_MAX, + NULL); + if (fd < 0) + return log_debug_errno(errno, "Failed allocate inner BPF map: %m"); + + return fd; +} + +int userns_restrict_install( + bool pin, + struct userns_restrict_bpf **ret) { + + _cleanup_(userns_restrict_bpf_freep) struct userns_restrict_bpf *obj = NULL; + _cleanup_close_ int dummy_mnt_id_hash_fd = -EBADF; + int r; + + r = lsm_supported("bpf"); + if (r < 0) + return r; + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "bpf-lsm not supported, can't lock down user namespace."); + + r = dlopen_bpf(); + if (r < 0) + return r; + + /* bpf_object__next_map() is not available in libbpf pre-0.7.0, and we want to use it. */ + if (!sym_bpf_object__next_map) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "libbpf too old for locking down user namespace."); + + obj = userns_restrict_bpf__open(); + if (!obj) + return log_error_errno(errno, "Failed to open userns_restrict BPF object: %m"); + + if (pin) { + struct bpf_map *map; + + /* libbpf will only create one level of dirs. Let's create the rest */ + (void) mkdir_p(MAP_LINK_PREFIX, 0755); + (void) mkdir_p(PROGRAM_LINK_PREFIX, 0755); + + map = sym_bpf_object__next_map(obj->obj, NULL); + while (map) { + _cleanup_free_ char *fn = NULL; + + fn = path_join(MAP_LINK_PREFIX, sym_bpf_map__name(map)); + if (!fn) + return log_oom(); + + r = sym_bpf_map__set_pin_path(map, fn); + if (r < 0) + return log_error_errno(r, "Failed to set pin path to '%s': %m", fn); + + map = sym_bpf_object__next_map(obj->obj, map); + } + } + + r = sym_bpf_map__set_max_entries(obj->maps.userns_mnt_id_hash, USERNS_MAX); + if (r < 0) + return log_error_errno(r, "Failed to size userns/mnt_id hash table: %m"); + + r = sym_bpf_map__set_max_entries(obj->maps.userns_ringbuf, USERNS_MAX * sizeof(unsigned int)); + if (r < 0) + return log_error_errno(r, "Failed to size userns ring buffer: %m"); + + /* Dummy map to satisfy the verifier */ + dummy_mnt_id_hash_fd = make_inner_hash_map(); + if (dummy_mnt_id_hash_fd < 0) + return dummy_mnt_id_hash_fd; + + r = sym_bpf_map__set_inner_map_fd(obj->maps.userns_mnt_id_hash, dummy_mnt_id_hash_fd); + if (r < 0) + return log_error_errno(r, "Failed to set inner BPF map: %m"); + + r = userns_restrict_bpf__load(obj); + if (r < 0) + return log_error_errno(r, "Failed to load BPF object: %m"); + + for (int i = 0; i < obj->skeleton->prog_cnt; i++) { + _cleanup_(bpf_link_freep) struct bpf_link *link = NULL; + struct bpf_prog_skeleton *ps = obj->skeleton->progs + i; + _cleanup_free_ char *fn = NULL; + bool linked = false; + const char *e; + + e = startswith(ps->name, "userns_restrict_"); + assert(e); + + if (pin) { + fn = path_join(PROGRAM_LINK_PREFIX, e); + if (!fn) + return log_oom(); + + link = sym_bpf_link__open(fn); + r = sym_libbpf_get_error(link); + if (r < 0) { + if (r != -ENOENT) + return log_error_errno(r, "Unable to open pinned program link: %m"); + link = NULL; + } else { + linked = true; + log_info("userns-restrict BPF-LSM program %s already attached.", ps->name); + } + } + + if (!link) { + link = sym_bpf_program__attach(*ps->prog); + r = sym_libbpf_get_error(link); + if (r < 0) + return log_error_errno(r, "Failed to attach LSM BPF program: %m"); + + log_info("userns-restrict BPF-LSM program %s now attached.", ps->name); + } + + if (pin && !linked) { + assert(fn); + + r = sym_bpf_link__pin(link, fn); + if (r < 0) + return log_error_errno(r, "Failed to pin LSM attachment: %m"); + } + + *ps->link = TAKE_PTR(link); + } + + if (pin) { + r = sym_bpf_object__pin_maps(obj->obj, NULL); + if (r < 0) + return log_error_errno(r, "Failed to pin BPF maps: %m"); + } + + if (ret) + *ret = TAKE_PTR(obj); + + return 0; +} + +int userns_restrict_put_by_inode( + struct userns_restrict_bpf *obj, + uint64_t userns_inode, + bool replace, + const int mount_fds[], + size_t n_mount_fds) { + + _cleanup_close_ int inner_map_fd = -EBADF; + _cleanup_free_ int *mnt_ids = NULL; + uint64_t ino = userns_inode; + int r, outer_map_fd; + + assert(obj); + assert(userns_inode != 0); + assert(n_mount_fds == 0 || mount_fds); + + /* The BPF map type BPF_MAP_TYPE_HASH_OF_MAPS only supports 32bit keys, and user namespace inode + * numbers are 32bit too, even though ino_t is 64bit these days. Should we ever run into a 64bit + * inode let's refuse early, we can't support this with the current BPF code for now. */ + if (userns_inode > UINT32_MAX) + return -EINVAL; + + mnt_ids = new(int, n_mount_fds); + if (!mnt_ids) + return -ENOMEM; + + for (size_t i = 0; i < n_mount_fds; i++) { + r = path_get_mnt_id_at(mount_fds[i], "", mnt_ids + i); + if (r < 0) + return log_debug_errno(r, "Failed to get mount ID: %m"); + } + + outer_map_fd = sym_bpf_map__fd(obj->maps.userns_mnt_id_hash); + if (outer_map_fd < 0) + return log_debug_errno(outer_map_fd, "Failed to get outer BPF map fd: %m"); + + if (replace) { + /* Add if missing, replace if already exists */ + inner_map_fd = make_inner_hash_map(); + if (inner_map_fd < 0) + return inner_map_fd; + + r = sym_bpf_map_update_elem(outer_map_fd, &ino, &inner_map_fd, BPF_ANY); + if (r < 0) + return log_debug_errno(errno, "Failed to replace map in inode hash: %m"); + } else { + /* Let's add an entry for this userns inode if missing. If it exists just extend the existing map. We + * might race against each other, hence we try a couple of times */ + for (size_t n_try = 10;; n_try--) { + uint32_t innermap_id; + + if (n_try == 0) + return log_debug_errno(SYNTHETIC_ERRNO(EEXIST), + "Stillcan't create inode entry in BPF map after 10 tries."); + + r = sym_bpf_map_lookup_elem(outer_map_fd, &ino, &innermap_id); + if (r >= 0) { + inner_map_fd = sym_bpf_map_get_fd_by_id(innermap_id); + if (inner_map_fd < 0) + return log_debug_errno(inner_map_fd, "Failed to get file descriptor for inner map: %m"); + + break; + } + if (errno != ENOENT) + return log_debug_errno(errno, "Failed to look up inode hash entry: %m"); + + /* No entry for this user namespace yet. Let's create one */ + inner_map_fd = make_inner_hash_map(); + if (inner_map_fd < 0) + return inner_map_fd; + + r = sym_bpf_map_update_elem(outer_map_fd, &ino, &inner_map_fd, BPF_NOEXIST); + if (r >= 0) + break; + if (errno != EEXIST) + return log_debug_errno(errno, "Failed to add mount ID list to inode hash: %m"); + } + } + + FOREACH_ARRAY(mntid, mnt_ids, n_mount_fds) { + uint32_t dummy_value = 1; + + r = sym_bpf_map_update_elem(inner_map_fd, mntid, &dummy_value, BPF_ANY); + if (r < 0) + return log_debug_errno(errno, "Failed to add mount ID to map: %m"); + + log_debug("Allowing mount %i on userns inode %" PRIu64, *mntid, ino); + } + + return 0; +} + +int userns_restrict_put_by_fd( + struct userns_restrict_bpf *obj, + int userns_fd, + bool replace, + const int mount_fds[], + size_t n_mount_fds) { + + struct stat st; + int r; + + assert(obj); + assert(userns_fd >= 0); + assert(n_mount_fds == 0 || mount_fds); + + r = fd_is_ns(userns_fd, CLONE_NEWUSER); + if (r < 0) + return log_debug_errno(r, "Failed to determine if file descriptor is user namespace: %m"); + if (r == 0) + return log_debug_errno(SYNTHETIC_ERRNO(EBADF), "User namespace fd is not actually a user namespace fd."); + + if (fstat(userns_fd, &st) < 0) + return log_debug_errno(errno, "Failed to fstat() user namespace: %m"); + + return userns_restrict_put_by_inode( + obj, + st.st_ino, + replace, + mount_fds, + n_mount_fds); +} + +int userns_restrict_reset_by_inode( + struct userns_restrict_bpf *obj, + uint64_t ino) { + + int r, outer_map_fd; + unsigned u; + + assert(obj); + assert(ino != 0); + + if (ino > UINT32_MAX) /* inodes larger than 32bit are definitely not included in our map, exit early */ + return 0; + + outer_map_fd = sym_bpf_map__fd(obj->maps.userns_mnt_id_hash); + if (outer_map_fd < 0) + return log_debug_errno(outer_map_fd, "Failed to get outer BPF map fd: %m"); + + u = (uint32_t) ino; + + r = sym_bpf_map_delete_elem(outer_map_fd, &u); + if (r < 0) + return log_debug_errno(outer_map_fd, "Failed to remove entry for inode %" PRIu64 " from outer map: %m", ino); + + return 0; +} + +#else +int userns_restrict_install(bool pin, struct userns_restrict_bpf **ret) { + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "User Namespace Restriction BPF support disabled."); +} + +struct userns_restrict_bpf *userns_restrict_bpf_free(struct userns_restrict_bpf *obj) { + return NULL; +} + +int userns_restrict_put_by_fd(struct userns_restrict_bpf *obj, int userns_fd, bool replace, const int mount_fds[], size_t n_mount_fds) { + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "User Namespace Restriction BPF support disabled."); +} + +int userns_restrict_put_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode, bool replace, const int mount_fds[], size_t n_mount_fds) { + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "User Namespace Restriction BPF support disabled."); +} + +int userns_restrict_reset_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode) { + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "User Namespace Restriction BPF support disabled."); +} +#endif diff --git a/src/nsresourced/userns-restrict.h b/src/nsresourced/userns-restrict.h new file mode 100644 index 0000000000..37aed7b350 --- /dev/null +++ b/src/nsresourced/userns-restrict.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#include "macro.h" + +#if HAVE_VMLINUX_H +#include "bpf/userns_restrict/userns-restrict-skel.h" +#else +struct userns_restrict_bpf; +#endif + +int userns_restrict_install(bool pin, struct userns_restrict_bpf **ret); +struct userns_restrict_bpf *userns_restrict_bpf_free(struct userns_restrict_bpf *obj); + +int userns_restrict_put_by_fd(struct userns_restrict_bpf *obj, int userns_fd, bool replace, const int mount_fds[], size_t n_mount_fds); +int userns_restrict_put_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode, bool replace, const int mount_fds[], size_t n_mount_fds); + +int userns_restrict_reset_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode); + +DEFINE_TRIVIAL_CLEANUP_FUNC(struct userns_restrict_bpf*, userns_restrict_bpf_free); diff --git a/src/shared/meson.build b/src/shared/meson.build index 71db6d8694..3c705c4c72 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -180,6 +180,7 @@ shared_sources = files( 'varlink-io.systemd.Hostname.c', 'varlink-io.systemd.Journal.c', 'varlink-io.systemd.ManagedOOM.c', + 'varlink-io.systemd.NamespaceResource.c', 'varlink-io.systemd.Network.c', 'varlink-io.systemd.PCRExtend.c', 'varlink-io.systemd.PCRLock.c', diff --git a/src/shared/varlink-io.systemd.NamespaceResource.c b/src/shared/varlink-io.systemd.NamespaceResource.c new file mode 100644 index 0000000000..e98c6c6663 --- /dev/null +++ b/src/shared/varlink-io.systemd.NamespaceResource.c @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.NamespaceResource.h" + +static VARLINK_DEFINE_METHOD( + AllocateUserRange, + VARLINK_DEFINE_INPUT(name, VARLINK_STRING, 0), + VARLINK_DEFINE_INPUT(size, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(target, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, VARLINK_INT, 0)); + +static VARLINK_DEFINE_METHOD( + RegisterUserNamespace, + VARLINK_DEFINE_INPUT(name, VARLINK_STRING, 0), + VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, VARLINK_INT, 0)); + +static VARLINK_DEFINE_METHOD( + AddMountToUserNamespace, + VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(mountFileDescriptor, VARLINK_INT, 0)); + +static VARLINK_DEFINE_METHOD( + AddControlGroupToUserNamespace, + VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(controlGroupFileDescriptor, VARLINK_INT, 0)); + +static VARLINK_DEFINE_METHOD( + AddNetworkToUserNamespace, + VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(networkNamespaceFileDescriptor, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(namespaceInterfaceName, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(mode, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(hostInterfaceName, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(namespaceInterfaceName, VARLINK_STRING, 0)); + +static VARLINK_DEFINE_ERROR(UserNamespaceInterfaceNotSupported); +static VARLINK_DEFINE_ERROR(NameExists); +static VARLINK_DEFINE_ERROR(UserNamespaceExists); +static VARLINK_DEFINE_ERROR(DynamicRangeUnavailable); +static VARLINK_DEFINE_ERROR(NoDynamicRange); +static VARLINK_DEFINE_ERROR(UserNamespaceNotRegistered); +static VARLINK_DEFINE_ERROR(UserNamespaceWithoutUserRange); +static VARLINK_DEFINE_ERROR(TooManyControlGroups); +static VARLINK_DEFINE_ERROR(ControlGroupAlreadyAdded); + +VARLINK_DEFINE_INTERFACE( + io_systemd_NamespaceResource, + "io.systemd.NamespaceResource", + &vl_method_AllocateUserRange, + &vl_method_RegisterUserNamespace, + &vl_method_AddMountToUserNamespace, + &vl_method_AddControlGroupToUserNamespace, + &vl_method_AddNetworkToUserNamespace, + &vl_error_UserNamespaceInterfaceNotSupported, + &vl_error_NameExists, + &vl_error_UserNamespaceExists, + &vl_error_DynamicRangeUnavailable, + &vl_error_NoDynamicRange, + &vl_error_UserNamespaceNotRegistered, + &vl_error_UserNamespaceWithoutUserRange, + &vl_error_TooManyControlGroups, + &vl_error_ControlGroupAlreadyAdded); diff --git a/src/shared/varlink-io.systemd.NamespaceResource.h b/src/shared/varlink-io.systemd.NamespaceResource.h new file mode 100644 index 0000000000..443cb97e48 --- /dev/null +++ b/src/shared/varlink-io.systemd.NamespaceResource.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd_NamespaceResource; diff --git a/src/test/test-varlink-idl.c b/src/test/test-varlink-idl.c index 3ca36a07a4..d64c6e9d3e 100644 --- a/src/test/test-varlink-idl.c +++ b/src/test/test-varlink-idl.c @@ -12,11 +12,12 @@ #include "varlink-io.systemd.Credentials.h" #include "varlink-io.systemd.Journal.h" #include "varlink-io.systemd.ManagedOOM.h" +#include "varlink-io.systemd.NamespaceResource.h" #include "varlink-io.systemd.Network.h" #include "varlink-io.systemd.PCRExtend.h" #include "varlink-io.systemd.PCRLock.h" -#include "varlink-io.systemd.Resolve.Monitor.h" #include "varlink-io.systemd.Resolve.h" +#include "varlink-io.systemd.Resolve.Monitor.h" #include "varlink-io.systemd.UserDatabase.h" #include "varlink-io.systemd.oom.h" #include "varlink-io.systemd.service.h" @@ -129,6 +130,8 @@ TEST(parse_format) { print_separator(); test_parse_format_one(&vl_interface_io_systemd_UserDatabase); print_separator(); + test_parse_format_one(&vl_interface_io_systemd_NamespaceResource); + print_separator(); test_parse_format_one(&vl_interface_io_systemd_Journal); print_separator(); test_parse_format_one(&vl_interface_io_systemd_Resolve); diff --git a/units/meson.build b/units/meson.build index 16a5564086..76510a08f9 100644 --- a/units/meson.build +++ b/units/meson.build @@ -732,6 +732,14 @@ units = [ 'file' : 'systemd-userdbd.socket', 'conditions' : ['ENABLE_USERDB'], }, + { + 'file' : 'systemd-nsresourced.service.in', + 'conditions' : ['ENABLE_NSRESOURCED'], + }, + { + 'file' : 'systemd-nsresourced.socket', + 'conditions' : ['ENABLE_NSRESOURCED'], + }, { 'file' : 'systemd-vconsole-setup.service.in', 'conditions' : ['ENABLE_VCONSOLE'], diff --git a/units/systemd-nsresourced.service.in b/units/systemd-nsresourced.service.in new file mode 100644 index 0000000000..3c927059e8 --- /dev/null +++ b/units/systemd-nsresourced.service.in @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Namespace Resource Manager +Documentation=man:systemd-nsresourced.service(8) +Requires=systemd-nsresourced.socket +After=systemd-nsresourced.socket +Conflicts=shutdown.target +Before=sysinit.target shutdown.target +DefaultDependencies=no + +[Service] +CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_SYS_RESOURCE CAP_BPF CAP_PERFMON CAP_SETGID CAP_SETUID CAP_SYS_ADMIN CAP_CHOWN CAP_FOWNER +ExecStart={{LIBEXECDIR}}/systemd-nsresourced +IPAddressDeny=any +LimitNOFILE={{HIGH_RLIMIT_NOFILE}} +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +PrivateDevices=yes +ProtectProc=invisible +ProtectControlGroups=yes +ProtectHome=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectSystem=strict +RestrictAddressFamilies=AF_UNIX AF_NETLINK +RestrictRealtime=yes +RestrictSUIDSGID=yes +SystemCallArchitectures=native +SystemCallErrorNumber=EPERM +SystemCallFilter=@system-service bpf perf_event_open open_by_handle_at +Type=notify +NotifyAccess=all +FileDescriptorStoreMax=4096 +{{SERVICE_WATCHDOG}} + +[Install] +Also=systemd-nsresourced.socket diff --git a/units/systemd-nsresourced.socket b/units/systemd-nsresourced.socket new file mode 100644 index 0000000000..2e3c8e9baa --- /dev/null +++ b/units/systemd-nsresourced.socket @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Namespace Resource Manager Socket +Documentation=man:systemd-nsresourced.service(8) +DefaultDependencies=no +Conflicts=shutdown.target +Before=sockets.target shutdown.target + +[Socket] +ListenStream=/run/systemd/io.systemd.NamespaceResource +Symlinks=/run/systemd/userdb/io.systemd.NamespaceResource +SocketMode=0666 + +[Install] +WantedBy=sockets.target From 54452c7b2aa03536331fc1ec04439c89d0320b57 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 4 Dec 2023 17:57:22 +0100 Subject: [PATCH 24/31] nsresourced: add client-side helpers around nsresourced APIs This adds simple functions that wrap the Varlink IPC calls. --- src/shared/meson.build | 1 + src/shared/nsresource.c | 329 ++++++++++++++++++++++++++++++++++++++++ src/shared/nsresource.h | 10 ++ 3 files changed, 340 insertions(+) create mode 100644 src/shared/nsresource.c create mode 100644 src/shared/nsresource.h diff --git a/src/shared/meson.build b/src/shared/meson.build index 3c705c4c72..eb92433e6d 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -126,6 +126,7 @@ shared_sources = files( 'netif-sriov.c', 'netif-util.c', 'nsflags.c', + 'nsresource.c', 'numa-util.c', 'open-file.c', 'openssl-util.c', diff --git a/src/shared/nsresource.c b/src/shared/nsresource.c new file mode 100644 index 0000000000..4419f2fdde --- /dev/null +++ b/src/shared/nsresource.c @@ -0,0 +1,329 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "fd-util.h" +#include "format-util.h" +#include "missing_sched.h" +#include "namespace-util.h" +#include "nsresource.h" +#include "process-util.h" +#include "varlink.h" + +static int make_pid_name(char **ret) { + char comm[TASK_COMM_LEN]; + + assert(ret); + + if (prctl(PR_GET_NAME, comm) < 0) + return -errno; + + /* So the namespace name should be 16 chars at max (because we want that it is usable in usernames, + * which have a limit of 31 chars effectively, and the nsresourced service wants to prefix/suffix + * some bits). But it also should be unique if we are called multiple times in a row. Hence we take + * the "comm" name (which is 15 chars), and suffix it with the UID, possibly overriding the end. */ + assert_cc(TASK_COMM_LEN == 15 + 1); + + char spid[DECIMAL_STR_MAX(pid_t)]; + xsprintf(spid, PID_FMT, getpid_cached()); + + assert(strlen(spid) <= 16); + strshorten(comm, 16 - strlen(spid)); + + _cleanup_free_ char *s = strjoin(comm, spid); + if (!s) + return -ENOMEM; + + *ret = TAKE_PTR(s); + return 0; +} + +int nsresource_allocate_userns(const char *name, uint64_t size) { + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + _cleanup_close_ int userns_fd = -EBADF; + _cleanup_free_ char *_name = NULL; + const char *error_id; + int r, userns_fd_idx; + + /* Allocate a new dynamic user namespace via the userdb registry logic */ + + if (!name) { + r = make_pid_name(&_name); + if (r < 0) + return r; + + name = _name; + } + + if (size <= 0 || size > UINT64_C(0x100000000)) /* Note: the server actually only allows allocating 1 or 64K right now */ + return -EINVAL; + + r = varlink_connect_address(&vl, "/run/systemd/io.systemd.NamespaceResource"); + if (r < 0) + return log_debug_errno(r, "Failed to connect to namespace resource manager: %m"); + + r = varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_debug_errno(r, "Failed to enable varlink fd passing for write: %m"); + + userns_fd = userns_acquire_empty(); + if (userns_fd < 0) + return log_debug_errno(userns_fd, "Failed to acquire empty user namespace: %m"); + + userns_fd_idx = varlink_push_dup_fd(vl, userns_fd); + if (userns_fd_idx < 0) + return log_debug_errno(userns_fd_idx, "Failed to push userns fd into varlink connection: %m"); + + JsonVariant *reply = NULL; + r = varlink_callb(vl, + "io.systemd.NamespaceResource.AllocateUserRange", + &reply, + &error_id, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("name", JSON_BUILD_STRING(name)), + JSON_BUILD_PAIR("size", JSON_BUILD_UNSIGNED(size)), + JSON_BUILD_PAIR("userNamespaceFileDescriptor", JSON_BUILD_UNSIGNED(userns_fd_idx)))); + if (r < 0) + return log_debug_errno(r, "Failed to call AllocateUserRange() varlink call: %m"); + if (error_id) + return log_debug_errno(varlink_error_to_errno(error_id, reply), "Failed to allocate user namespace with %" PRIu64 " users: %s", size, error_id); + + return TAKE_FD(userns_fd); +} + +int nsresource_register_userns(const char *name, int userns_fd) { + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + _cleanup_close_ int _userns_fd = -EBADF; + _cleanup_free_ char *_name = NULL; + const char *error_id; + int r, userns_fd_idx; + + /* Register the specified user namespace with userbd. */ + + if (!name) { + r = make_pid_name(&_name); + if (r < 0) + return r; + + name = _name; + } + + if (userns_fd < 0) { + _userns_fd = namespace_open_by_type(NAMESPACE_USER); + if (_userns_fd < 0) + return -errno; + + userns_fd = _userns_fd; + } + + r = varlink_connect_address(&vl, "/run/systemd/io.systemd.NamespaceResource"); + if (r < 0) + return log_debug_errno(r, "Failed to connect to namespace resource manager: %m"); + + r = varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_debug_errno(r, "Failed to enable varlink fd passing for write: %m"); + + userns_fd_idx = varlink_push_dup_fd(vl, userns_fd); + if (userns_fd_idx < 0) + return log_debug_errno(userns_fd_idx, "Failed to push userns fd into varlink connection: %m"); + + JsonVariant *reply = NULL; + r = varlink_callb(vl, + "io.systemd.NamespaceResource.RegisterUserNamespace", + &reply, + &error_id, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("name", JSON_BUILD_STRING(name)), + JSON_BUILD_PAIR("userNamespaceFileDescriptor", JSON_BUILD_UNSIGNED(userns_fd_idx)))); + if (r < 0) + return log_debug_errno(r, "Failed to call RegisterUserNamespace() varlink call: %m"); + if (error_id) + return log_debug_errno(varlink_error_to_errno(error_id, reply), "Failed to register user namespace: %s", error_id); + + return 0; +} + +int nsresource_add_mount(int userns_fd, int mount_fd) { + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + int r, userns_fd_idx, mount_fd_idx; + const char *error_id; + + assert(mount_fd >= 0); + + if (userns_fd < 0) { + int _userns_fd = namespace_open_by_type(NAMESPACE_USER); + if (_userns_fd < 0) + return -errno; + + userns_fd = _userns_fd; + } + + r = varlink_connect_address(&vl, "/run/systemd/io.systemd.NamespaceResource"); + if (r < 0) + return log_error_errno(r, "Failed to connect to namespace resource manager: %m"); + + r = varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_error_errno(r, "Failed to enable varlink fd passing for write: %m"); + + userns_fd_idx = varlink_push_dup_fd(vl, userns_fd); + if (userns_fd_idx < 0) + return log_error_errno(userns_fd_idx, "Failed to push userns fd into varlink connection: %m"); + + mount_fd_idx = varlink_push_dup_fd(vl, mount_fd); + if (mount_fd_idx < 0) + return log_error_errno(mount_fd_idx, "Failed to push mount fd into varlink connection: %m"); + + JsonVariant *reply = NULL; + r = varlink_callb(vl, + "io.systemd.NamespaceResource.AddMountToUserNamespace", + &reply, + &error_id, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userNamespaceFileDescriptor", JSON_BUILD_UNSIGNED(userns_fd_idx)), + JSON_BUILD_PAIR("mountFileDescriptor", JSON_BUILD_UNSIGNED(mount_fd_idx)))); + if (r < 0) + return log_error_errno(r, "Failed to call AddMountToUserNamespace() varlink call: %m"); + if (streq_ptr(error_id, "io.systemd.NamespaceResource.UserNamespaceNotRegistered")) { + log_notice("User namespace has not been allocated via namespace resource registry, not adding mount to registration."); + return 0; + } + if (error_id) + return log_error_errno(varlink_error_to_errno(error_id, reply), "Failed to mount image: %s", error_id); + + return 1; +} + +int nsresource_add_cgroup(int userns_fd, int cgroup_fd) { + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + _cleanup_close_ int _userns_fd = -EBADF; + int r, userns_fd_idx, cgroup_fd_idx; + const char *error_id; + + assert(cgroup_fd >= 0); + + if (userns_fd < 0) { + _userns_fd = namespace_open_by_type(NAMESPACE_USER); + if (_userns_fd < 0) + return -errno; + + userns_fd = _userns_fd; + } + + r = varlink_connect_address(&vl, "/run/systemd/io.systemd.NamespaceResource"); + if (r < 0) + return log_debug_errno(r, "Failed to connect to namespace resource manager: %m"); + + r = varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_debug_errno(r, "Failed to enable varlink fd passing for write: %m"); + + userns_fd_idx = varlink_push_dup_fd(vl, userns_fd); + if (userns_fd_idx < 0) + return log_debug_errno(userns_fd_idx, "Failed to push userns fd into varlink connection: %m"); + + cgroup_fd_idx = varlink_push_dup_fd(vl, cgroup_fd); + if (cgroup_fd_idx < 0) + return log_debug_errno(userns_fd_idx, "Failed to push cgroup fd into varlink connection: %m"); + + JsonVariant *reply = NULL; + r = varlink_callb(vl, + "io.systemd.NamespaceResource.AddControlGroupToUserNamespace", + &reply, + &error_id, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userNamespaceFileDescriptor", JSON_BUILD_UNSIGNED(userns_fd_idx)), + JSON_BUILD_PAIR("controlGroupFileDescriptor", JSON_BUILD_UNSIGNED(cgroup_fd_idx)))); + if (r < 0) + return log_debug_errno(r, "Failed to call AddControlGroupToUserNamespace() varlink call: %m"); + if (streq_ptr(error_id, "io.systemd.NamespaceResource.UserNamespaceNotRegistered")) { + log_notice("User namespace has not been allocated via namespace resource registry, not adding cgroup to registration."); + return 0; + } + if (error_id) + return log_debug_errno(varlink_error_to_errno(error_id, reply), "Failed to add cgroup to user namespace: %s", error_id); + + return 1; +} + +int nsresource_add_netif( + int userns_fd, + int netns_fd, + const char *namespace_ifname, + char **ret_host_ifname, + char **ret_namespace_ifname) { + + _cleanup_close_ int _userns_fd = -EBADF, _netns_fd = -EBADF; + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + int r, userns_fd_idx, netns_fd_idx; + const char *error_id; + + if (userns_fd < 0) { + _userns_fd = namespace_open_by_type(NAMESPACE_USER); + if (_userns_fd < 0) + return -errno; + + userns_fd = _userns_fd; + } + + if (netns_fd < 0) { + _netns_fd = namespace_open_by_type(NAMESPACE_NET); + if (_netns_fd < 0) + return -errno; + + netns_fd = _netns_fd; + } + + r = varlink_connect_address(&vl, "/run/systemd/io.systemd.NamespaceResource"); + if (r < 0) + return log_debug_errno(r, "Failed to connect to namespace resource manager: %m"); + + r = varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_debug_errno(r, "Failed to enable varlink fd passing for write: %m"); + + userns_fd_idx = varlink_push_dup_fd(vl, userns_fd); + if (userns_fd_idx < 0) + return log_debug_errno(userns_fd_idx, "Failed to push userns fd into varlink connection: %m"); + + netns_fd_idx = varlink_push_dup_fd(vl, netns_fd); + if (netns_fd_idx < 0) + return log_debug_errno(netns_fd_idx, "Failed to push netns fd into varlink connection: %m"); + + JsonVariant *reply = NULL; + r = varlink_callb(vl, + "io.systemd.NamespaceResource.AddNetworkToUserNamespace", + &reply, + &error_id, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userNamespaceFileDescriptor", JSON_BUILD_UNSIGNED(userns_fd_idx)), + JSON_BUILD_PAIR("networkNamespaceFileDescriptor", JSON_BUILD_UNSIGNED(netns_fd_idx)), + JSON_BUILD_PAIR("mode", JSON_BUILD_CONST_STRING("veth")), + JSON_BUILD_PAIR_CONDITION(namespace_ifname, "namespaceInterfaceName", JSON_BUILD_STRING(namespace_ifname)))); + if (r < 0) + return log_debug_errno(r, "Failed to call AddNetworkToUserNamespace() varlink call: %m"); + if (streq_ptr(error_id, "io.systemd.NamespaceResource.UserNamespaceNotRegistered")) { + log_notice("User namespace has not been allocated via namespace resource registry, not adding network to registration."); + return 0; + } + if (error_id) + return log_debug_errno(varlink_error_to_errno(error_id, reply), "Failed to add network to user namespace: %s", error_id); + + _cleanup_free_ char *host_interface_name = NULL, *namespace_interface_name = NULL; + r = json_dispatch( + reply, + (const JsonDispatch[]) { + { "hostInterfaceName", JSON_VARIANT_STRING, json_dispatch_string, PTR_TO_SIZE(&host_interface_name) }, + { "namespaceInterfaceName", JSON_VARIANT_STRING, json_dispatch_string, PTR_TO_SIZE(&namespace_interface_name) }, + }, + JSON_ALLOW_EXTENSIONS, + /* userdata= */ NULL); + + if (ret_host_ifname) + *ret_host_ifname = TAKE_PTR(host_interface_name); + if (ret_namespace_ifname) + *ret_namespace_ifname = TAKE_PTR(namespace_interface_name); + + return 1; +} diff --git a/src/shared/nsresource.h b/src/shared/nsresource.h new file mode 100644 index 0000000000..6b807b327b --- /dev/null +++ b/src/shared/nsresource.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +int nsresource_allocate_userns(const char *name, uint64_t size); +int nsresource_register_userns(const char *name, int userns_fd); +int nsresource_add_mount(int userns_fd, int mount_fd); +int nsresource_add_cgroup(int userns_fd, int cgroup_fd); +int nsresource_add_netif(int userns_fd, int netns_fd, const char *namespace_ifname, char **ret_host_ifname, char **ret_namespace_ifname); From 702a52f4b5d49cce11e2adbc740deb3b644e2de0 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 9 Mar 2023 12:27:29 +0100 Subject: [PATCH 25/31] mountfsd: add new systemd-mountfsd component --- docs/ENVIRONMENT.md | 18 + man/rules/meson.build | 1 + man/systemd-mountfsd.service.xml | 70 ++ meson.build | 5 + meson_options.txt | 5 + presets/90-systemd.preset | 3 +- .../io.systemd.mount-file-system.policy | 70 ++ src/mountfsd/meson.build | 28 + src/mountfsd/mountfsd-manager.c | 277 +++++++ src/mountfsd/mountfsd-manager.h | 30 + src/mountfsd/mountfsd.c | 43 ++ src/mountfsd/mountwork.c | 703 ++++++++++++++++++ src/shared/meson.build | 1 + .../varlink-io.systemd.MountFileSystem.c | 69 ++ .../varlink-io.systemd.MountFileSystem.h | 6 + src/test/test-varlink-idl.c | 3 + units/meson.build | 8 + units/systemd-mountfsd.service.in | 46 ++ units/systemd-mountfsd.socket | 22 + 19 files changed, 1407 insertions(+), 1 deletion(-) create mode 100644 man/systemd-mountfsd.service.xml create mode 100644 src/mountfsd/io.systemd.mount-file-system.policy create mode 100644 src/mountfsd/meson.build create mode 100644 src/mountfsd/mountfsd-manager.c create mode 100644 src/mountfsd/mountfsd-manager.h create mode 100644 src/mountfsd/mountfsd.c create mode 100644 src/mountfsd/mountwork.c create mode 100644 src/shared/varlink-io.systemd.MountFileSystem.c create mode 100644 src/shared/varlink-io.systemd.MountFileSystem.h create mode 100644 units/systemd-mountfsd.service.in create mode 100644 units/systemd-mountfsd.socket diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 449e284a91..3585ab0123 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -676,3 +676,21 @@ Tools using the Varlink protocol (such as `varlinkctl`) or sd-bus (such as service. Takes a file system path: if specified the tool will listen on an `AF_UNIX` stream socket on the specified path in addition to whatever else it would listen on. + +`systemd-mountfsd`: + +* `$SYSTEMD_MOUNTFSD_TRUSTED_DIRECTORIES` – takes a boolean argument. If true + disk images from the usual disk image directories (`/var/lib/machines/`, + `/var/lib/confexts/`, …) will be considered "trusted", i.e. are validated + with a more relaxed image policy (typically not requiring Verity signature + checking) than those from other directories (where Verity signature checks + are mandatory). If false all images are treated the same, regardless if + placed in the usual disk image directories or elsewhere. If not set defaults + to a compile time setting. + +* `$SYSTEMD_MOUNTFSD_IMAGE_POLICY_TRUSTED`, + `$SYSTEMD_MOUNTFSD_IMAGE_POLICY_UNTRUSTED` – the default image policy to + apply to trusted and untrusted disk images. An image is considered trusted if + placed in a trusted disk image directory (see above), or if suitable polkit + authentication was acquired. See `systemd.image-policy(7)` for the valid + syntax for image policy strings. diff --git a/man/rules/meson.build b/man/rules/meson.build index e6c0ac9b52..cf2be55bbd 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -999,6 +999,7 @@ manpages = [ ['systemd-measure', '1', [], 'HAVE_TPM2 HAVE_BLKID HAVE_OPENSSL'], ['systemd-modules-load.service', '8', ['systemd-modules-load'], 'HAVE_KMOD'], ['systemd-mount', '1', ['systemd-umount'], ''], + ['systemd-mountfsd.service', '8', ['systemd-mountfsd'], 'ENABLE_MOUNTFSD'], ['systemd-network-generator.service', '8', ['systemd-network-generator'], ''], ['systemd-networkd-wait-online.service', '8', diff --git a/man/systemd-mountfsd.service.xml b/man/systemd-mountfsd.service.xml new file mode 100644 index 0000000000..25c1568f33 --- /dev/null +++ b/man/systemd-mountfsd.service.xml @@ -0,0 +1,70 @@ + + + + + + + + systemd-mountfsd.service + systemd + + + + systemd-mountfsd.service + 8 + + + + systemd-mountfsd.service + systemd-mountfsd + Disk Image File System Mount Service + + + + systemd-mountfsd.service + /usr/lib/systemd/systemd-mountfsd + + + + Description + + systemd-mountfsd is a system service that dissects disk images, and returns mount + file descriptors for the file systems contained therein to clients, via a Varlink IPC API. + + The disk images provided must contain a raw file system image or must follow the Discoverable + Partitions Specification. Before mounting any file systems authenticity of the disk image is + established in one or a combination of the following ways: + + + If the disk image is located in a regular file in one of the directories + /var/lib/machines/, /var/lib/portables/, + /var/lib/extensions/, /var/lib/confexts/ or their + counterparts in the /etc/, /run/, + /usr/lib/ it is assumed to be trusted. + + If the disk image contains a Verity enabled disk image, along with a signature + partition with a key in the kernel keyring or in /etc/verity.d/ (and related + directories) the disk image is considered trusted. + + + This service provides one Varlink service: + io.systemd.MountFileSystem which accepts a file descriptor to a regular file or + block device, and returns a number of file descriptors referring to an fsmount() + file descriptor the client may then attach to a path of their choice. + + The returned mounts are automatically allowlisted in the per-user-namespace allowlist maintained by + systemd-nsresourced.service8. + + The file systems are automatically fsck'ed before mounting. + + + + See Also + + systemd1, + systemd-nsresourced.service8 + + + diff --git a/meson.build b/meson.build index 8fd6774799..1a160960cc 100644 --- a/meson.build +++ b/meson.build @@ -272,6 +272,7 @@ conf.set_quoted('SYSTEMD_TEST_DATA', testdata_dir) conf.set_quoted('SYSTEMD_TTY_ASK_PASSWORD_AGENT_BINARY_PATH', bindir / 'systemd-tty-ask-password-agent') conf.set_quoted('SYSTEMD_UPDATE_HELPER_PATH', libexecdir / 'systemd-update-helper') conf.set_quoted('SYSTEMD_USERWORK_PATH', libexecdir / 'systemd-userwork') +conf.set_quoted('SYSTEMD_MOUNTWORK_PATH', libexecdir / 'systemd-mountwork') conf.set_quoted('SYSTEMD_NSRESOURCEWORK_PATH', libexecdir / 'systemd-nsresourcework') conf.set_quoted('SYSTEMD_VERITYSETUP_PATH', libexecdir / 'systemd-veritysetup') conf.set_quoted('SYSTEM_CONFIG_UNIT_DIR', pkgsysconfdir / 'system') @@ -1598,6 +1599,8 @@ conf.set10('ENABLE_REMOTE', have) feature = get_option('vmspawn').disable_auto_if(conf.get('BUILD_MODE_DEVELOPER') == 0) conf.set10('ENABLE_VMSPAWN', feature.allowed()) +conf.set10('DEFAULT_MOUNTFSD_TRUSTED_DIRECTORIES', get_option('default-mountfsd-trusted-directories')) + foreach term : ['analyze', 'backlight', 'binfmt', @@ -1618,6 +1621,7 @@ foreach term : ['analyze', 'localed', 'logind', 'machined', + 'mountfsd', 'networkd', 'nscd', 'nsresourced', @@ -2276,6 +2280,7 @@ subdir('src/locale') subdir('src/login') subdir('src/machine') subdir('src/machine-id-setup') +subdir('src/mountfsd') subdir('src/modules-load') subdir('src/mount') subdir('src/network') diff --git a/meson_options.txt b/meson_options.txt index b75f074600..41a524b0dc 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -124,6 +124,8 @@ option('portabled', type : 'boolean', description : 'install the systemd-portabled stack') option('sysext', type : 'boolean', description : 'install the systemd-sysext stack') +option('mountfsd', type : 'boolean', + description : 'install the systemd-mountfsd stack') option('userdb', type : 'boolean', description : 'install the systemd-userdbd stack') option('homed', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' }, @@ -528,3 +530,6 @@ option('vmlinux-h', type : 'combo', choices : ['auto', 'provided', 'generated', description : 'which vmlinux.h to use') option('vmlinux-h-path', type : 'string', value : '', description : 'path to vmlinux.h to use') + +option('default-mountfsd-trusted-directories', type : 'boolean', value: false, + description : 'controls whether mountfsd should apply a relaxed policy on DDIs in system DDI directories') diff --git a/presets/90-systemd.preset b/presets/90-systemd.preset index f896ade2a4..7b7c20a020 100644 --- a/presets/90-systemd.preset +++ b/presets/90-systemd.preset @@ -23,9 +23,10 @@ enable systemd-homed.service enable systemd-homed-activate.service enable systemd-homed-firstboot.service enable systemd-journald-audit.socket +enable systemd-mountfsd.socket enable systemd-network-generator.service -enable systemd-networkd-wait-online.service enable systemd-networkd.service +enable systemd-networkd-wait-online.service enable systemd-nsresourced.socket enable systemd-pstore.service enable systemd-resolved.service diff --git a/src/mountfsd/io.systemd.mount-file-system.policy b/src/mountfsd/io.systemd.mount-file-system.policy new file mode 100644 index 0000000000..6a151eb437 --- /dev/null +++ b/src/mountfsd/io.systemd.mount-file-system.policy @@ -0,0 +1,70 @@ + + + + + + + + The systemd Project + https://systemd.io + + + + + Allow mounting of file system image + Authentication is required for an application to mount a file system image. + + auth_admin_keep + auth_admin_keep + yes + + + + + + Allow mounting of untrusted file system image + Authentication is required for an application to mount a cryptographically unsigned file system image or an image whose cryptographic signature is not recognized. + + auth_admin + auth_admin + auth_admin + + + io.systemd.mount-file-system.mount-image + + + + + Allow private mounting of trusted file system image + Authentication is required for an application to privately mount a file system image or an image whose cryptographic signature is recognized. + + yes + yes + yes + + + + + Allow private mounting of untrusted file system image + Authentication is required for an application to privately mount a cryptographically unsigned file system image or an image whose cryptographic signature is not recognized. + + auth_admin + auth_admin + auth_admin + + + io.systemd.mount-file-system.mount-image-privately + + diff --git a/src/mountfsd/meson.build b/src/mountfsd/meson.build new file mode 100644 index 0000000000..3689d2af27 --- /dev/null +++ b/src/mountfsd/meson.build @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +systemd_mountwork_sources = files( + 'mountwork.c', +) + +systemd_mountfsd_sources = files( + 'mountfsd.c', + 'mountfsd-manager.c', +) + +executables += [ + libexec_template + { + 'name' : 'systemd-mountfsd', + 'conditions' : ['ENABLE_MOUNTFSD'], + 'sources' : systemd_mountfsd_sources, + }, + libexec_template + { + 'name' : 'systemd-mountwork', + 'conditions' : ['ENABLE_MOUNTFSD'], + 'sources' : systemd_mountwork_sources, + 'link_with' : common_libs, + 'dependencies' : common_deps, + }, +] + +install_data('io.systemd.mount-file-system.policy', + install_dir : polkitpolicydir) diff --git a/src/mountfsd/mountfsd-manager.c b/src/mountfsd/mountfsd-manager.c new file mode 100644 index 0000000000..b05c6e8493 --- /dev/null +++ b/src/mountfsd/mountfsd-manager.c @@ -0,0 +1,277 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include + +#include "sd-daemon.h" + +#include "build-path.h" +#include "common-signal.h" +#include "env-util.h" +#include "fd-util.h" +#include "fs-util.h" +#include "mkdir.h" +#include "mountfsd-manager.h" +#include "process-util.h" +#include "set.h" +#include "signal-util.h" +#include "socket-util.h" +#include "stdio-util.h" +#include "umask-util.h" + +#define LISTEN_TIMEOUT_USEC (25 * USEC_PER_SEC) + +static int start_workers(Manager *m, bool explicit_request); + +static size_t manager_current_workers(Manager *m) { + assert(m); + + return set_size(m->workers_fixed) + set_size(m->workers_dynamic); +} + +static int on_worker_exit(sd_event_source *s, const siginfo_t *si, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + assert(s); + + assert_se(!set_remove(m->workers_dynamic, s) != !set_remove(m->workers_fixed, s)); + sd_event_source_disable_unref(s); + + if (si->si_code == CLD_EXITED) { + if (si->si_status == EXIT_SUCCESS) + log_debug("Worker " PID_FMT " exited successfully.", si->si_pid); + else + log_warning("Worker " PID_FMT " died with a failure exit status %i, ignoring.", si->si_pid, si->si_status); + } else if (si->si_code == CLD_KILLED) + log_warning("Worker " PID_FMT " was killed by signal %s, ignoring.", si->si_pid, signal_to_string(si->si_status)); + else if (si->si_code == CLD_DUMPED) + log_warning("Worker " PID_FMT " dumped core by signal %s, ignoring.", si->si_pid, signal_to_string(si->si_status)); + else + log_warning("Got unexpected exit code via SIGCHLD, ignoring."); + + (void) start_workers(m, /* explicit_request= */ false); /* Fill up workers again if we fell below the low watermark */ + return 0; +} + +static int on_sigusr2(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + (void) start_workers(m, /* explicit_request= */ true); /* Workers told us there's more work, let's add one more worker as long as we are below the high watermark */ + return 0; +} + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + event_source_hash_ops, + sd_event_source, + (void (*)(const sd_event_source*, struct siphash*)) trivial_hash_func, + (int (*)(const sd_event_source*, const sd_event_source*)) trivial_compare_func, + sd_event_source_disable_unref); + +int manager_new(Manager **ret) { + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + m = new(Manager, 1); + if (!m) + return -ENOMEM; + + *m = (Manager) { + .listen_fd = -EBADF, + .worker_ratelimit = { + .interval = 5 * USEC_PER_SEC, + .burst = 50, + }, + }; + + r = sd_event_new(&m->event); + if (r < 0) + return r; + + r = sd_event_set_signal_exit(m->event, true); + if (r < 0) + return r; + + r = sd_event_add_signal(m->event, NULL, (SIGRTMIN+18)|SD_EVENT_SIGNAL_PROCMASK, sigrtmin18_handler, NULL); + if (r < 0) + return r; + + r = sd_event_add_memory_pressure(m->event, NULL, NULL, NULL); + if (r < 0) + log_debug_errno(r, "Failed allocate memory pressure event source, ignoring: %m"); + + r = sd_event_set_watchdog(m->event, true); + if (r < 0) + log_debug_errno(r, "Failed to enable watchdog handling, ignoring: %m"); + + r = sd_event_add_signal(m->event, NULL, SIGUSR2|SD_EVENT_SIGNAL_PROCMASK, on_sigusr2, m); + if (r < 0) + return r; + + *ret = TAKE_PTR(m); + return 0; +} + +Manager* manager_free(Manager *m) { + if (!m) + return NULL; + + set_free(m->workers_fixed); + set_free(m->workers_dynamic); + + /* Note: we rely on PR_DEATHSIG to kill the workers for us */ + + sd_event_unref(m->event); + + return mfree(m); +} + +static int start_one_worker(Manager *m) { + _cleanup_(sd_event_source_disable_unrefp) sd_event_source *source = NULL; + bool fixed; + pid_t pid; + int r; + + assert(m); + + fixed = set_size(m->workers_fixed) < MOUNTFS_WORKERS_MIN; + + r = safe_fork_full( + "(sd-worker)", + /* stdio_fds= */ NULL, + &m->listen_fd, 1, + FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_REOPEN_LOG|FORK_LOG|FORK_CLOSE_ALL_FDS, + &pid); + if (r < 0) + return log_error_errno(r, "Failed to fork new worker child: %m"); + if (r == 0) { + char pids[DECIMAL_STR_MAX(pid_t)]; + /* Child */ + + if (m->listen_fd == 3) { + r = fd_cloexec(3, false); + if (r < 0) { + log_error_errno(r, "Failed to turn off O_CLOEXEC for fd 3: %m"); + _exit(EXIT_FAILURE); + } + } else { + if (dup2(m->listen_fd, 3) < 0) { /* dup2() creates with O_CLOEXEC off */ + log_error_errno(errno, "Failed to move listen fd to 3: %m"); + _exit(EXIT_FAILURE); + } + + safe_close(m->listen_fd); + } + + xsprintf(pids, PID_FMT, pid); + if (setenv("LISTEN_PID", pids, 1) < 0) { + log_error_errno(errno, "Failed to set $LISTEN_PID: %m"); + _exit(EXIT_FAILURE); + } + + if (setenv("LISTEN_FDS", "1", 1) < 0) { + log_error_errno(errno, "Failed to set $LISTEN_FDS: %m"); + _exit(EXIT_FAILURE); + } + + if (setenv("MOUNTFS_FIXED_WORKER", one_zero(fixed), 1) < 0) { + log_error_errno(errno, "Failed to set $MOUNTFS_FIXED_WORKER: %m"); + _exit(EXIT_FAILURE); + } + + r = setenv_systemd_log_level(); + if (r < 0) { + log_error_errno(r, "Failed to set $SYSTEMD_LOG_LEVEL: %m"); + _exit(EXIT_FAILURE); + } + + r = invoke_callout_binary(SYSTEMD_MOUNTWORK_PATH, STRV_MAKE("systemd-mountwork", "xxxxxxxxxxxxxxxx")); /* With some extra space rename_process() can make use of */ + log_error_errno(r, "Failed start worker process: %m"); + _exit(EXIT_FAILURE); + } + + r = sd_event_add_child(m->event, &source, pid, WEXITED, on_worker_exit, m); + if (r < 0) + return log_error_errno(r, "Failed to watch child " PID_FMT ": %m", pid); + + r = set_ensure_put( + fixed ? &m->workers_fixed : &m->workers_dynamic, + &event_source_hash_ops, + source); + if (r < 0) + return log_error_errno(r, "Failed to add child process to set: %m"); + + TAKE_PTR(source); + + return 0; +} + +static int start_workers(Manager *m, bool explicit_request) { + int r; + + assert(m); + + for (;;) { + size_t n; + + n = manager_current_workers(m); + + log_debug("%zu workers running.", n); + + if (n >= MOUNTFS_WORKERS_MIN && (!explicit_request || n >= MOUNTFS_WORKERS_MAX)) + break; + + if (!ratelimit_below(&m->worker_ratelimit)) { + /* If we keep starting workers too often, let's fail the whole daemon, something is wrong */ + sd_event_exit(m->event, EXIT_FAILURE); + + return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "Worker threads requested too frequently, something is wrong."); + } + + r = start_one_worker(m); + if (r < 0) + return r; + + explicit_request = false; + } + + return 0; +} + +int manager_startup(Manager *m) { + int n; + + assert(m); + assert(m->listen_fd < 0); + + n = sd_listen_fds(false); + if (n < 0) + return log_error_errno(n, "Failed to determine number of passed file descriptors: %m"); + if (n > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected one listening fd, got %i.", n); + if (n == 1) + m->listen_fd = SD_LISTEN_FDS_START; + else { + static const union sockaddr_union sockaddr = { + .un.sun_family = AF_UNIX, + .un.sun_path = "/run/systemd/io.systemd.MountFileSystem", + }; + + m->listen_fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0); + if (m->listen_fd < 0) + return log_error_errno(errno, "Failed to bind on socket: %m"); + + (void) sockaddr_un_unlink(&sockaddr.un); + + WITH_UMASK(0000) + if (bind(m->listen_fd, &sockaddr.sa, SOCKADDR_UN_LEN(sockaddr.un)) < 0) + return log_error_errno(errno, "Failed to bind socket: %m"); + + if (listen(m->listen_fd, SOMAXCONN) < 0) + return log_error_errno(errno, "Failed to listen on socket: %m"); + } + + /* Let's make sure every accept() call on this socket times out after 25s. This allows workers to be + * GC'ed on idle */ + if (setsockopt(m->listen_fd, SOL_SOCKET, SO_RCVTIMEO, TIMEVAL_STORE(LISTEN_TIMEOUT_USEC), sizeof(struct timeval)) < 0) + return log_error_errno(errno, "Failed to se SO_RCVTIMEO: %m"); + + return start_workers(m, /* explicit_request= */ false); +} diff --git a/src/mountfsd/mountfsd-manager.h b/src/mountfsd/mountfsd-manager.h new file mode 100644 index 0000000000..6bfbddcd17 --- /dev/null +++ b/src/mountfsd/mountfsd-manager.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-bus.h" +#include "sd-event.h" + +typedef struct Manager Manager; + +#include "hashmap.h" +#include "ratelimit.h" + +#define MOUNTFS_WORKERS_MIN 3 +#define MOUNTFS_WORKERS_MAX 4096 + +struct Manager { + sd_event *event; + + Set *workers_fixed; /* Workers 0…MOUNTFS_WORKERS_MIN */ + Set *workers_dynamic; /* Workers MOUNTFS_WORKERS_MIN+1…MOUNTFS_WORKERS_MAX */ + + int listen_fd; + + RateLimit worker_ratelimit; +}; + +int manager_new(Manager **ret); +Manager* manager_free(Manager *m); +DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); + +int manager_startup(Manager *m); diff --git a/src/mountfsd/mountfsd.c b/src/mountfsd/mountfsd.c new file mode 100644 index 0000000000..6073bd548e --- /dev/null +++ b/src/mountfsd/mountfsd.c @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "daemon-util.h" +#include "log.h" +#include "main-func.h" +#include "mountfsd-manager.h" +#include "signal-util.h" + +static int run(int argc, char *argv[]) { + _unused_ _cleanup_(notify_on_cleanup) const char *notify_stop = NULL; + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + log_setup(); + + umask(0022); + + if (argc != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments."); + + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD) >= 0); + + r = manager_new(&m); + if (r < 0) + return log_error_errno(r, "Could not create manager: %m"); + + r = manager_startup(m); + if (r < 0) + return log_error_errno(r, "Failed to start up daemon: %m"); + + notify_stop = notify_start(NOTIFY_READY, NOTIFY_STOPPING); + + r = sd_event_loop(m->event); + if (r < 0) + return log_error_errno(r, "Event loop failed: %m"); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c new file mode 100644 index 0000000000..f598596215 --- /dev/null +++ b/src/mountfsd/mountwork.c @@ -0,0 +1,703 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-daemon.h" + +#include "argv-util.h" +#include "bus-polkit.h" +#include "chase.h" +#include "discover-image.h" +#include "dissect-image.h" +#include "env-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "io-util.h" +#include "main-func.h" +#include "missing_loop.h" +#include "namespace-util.h" +#include "nsresource.h" +#include "nulstr-util.h" +#include "os-util.h" +#include "process-util.h" +#include "stat-util.h" +#include "user-util.h" +#include "varlink.h" +#include "varlink-io.systemd.MountFileSystem.h" + +#define ITERATIONS_MAX 64U +#define RUNTIME_MAX_USEC (5 * USEC_PER_MINUTE) +#define PRESSURE_SLEEP_TIME_USEC (50 * USEC_PER_MSEC) +#define LISTEN_IDLE_USEC (90 * USEC_PER_SEC) + +static const ImagePolicy image_policy_untrusted = { + .n_policies = 2, + .policies = { + { PARTITION_ROOT, PARTITION_POLICY_SIGNED|PARTITION_POLICY_ABSENT }, + { PARTITION_USR, PARTITION_POLICY_SIGNED|PARTITION_POLICY_ABSENT }, + }, + .default_flags = PARTITION_POLICY_IGNORE, +}; + +static int json_dispatch_image_policy(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + _cleanup_(image_policy_freep) ImagePolicy *q = NULL; + ImagePolicy **p = ASSERT_PTR(userdata); + int r; + + assert(p); + + if (json_variant_is_null(variant)) { + *p = image_policy_free(*p); + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + r = image_policy_from_string(json_variant_string(variant), &q); + if (r < 0) + return json_log(variant, flags, r, "JSON field '%s' is not a valid image policy.", strna(name)); + + image_policy_free(*p); + *p = TAKE_PTR(q); + return 0; +} + +typedef struct MountImageParameters { + unsigned image_fd_idx; + unsigned userns_fd_idx; + int read_only; + int growfs; + char *password; + ImagePolicy *image_policy; +} MountImageParameters; + +static void mount_image_parameters_done(MountImageParameters *p) { + assert(p); + + p->password = erase_and_free(p->password); + p->image_policy = image_policy_free(p->image_policy); +} + +static int validate_image_fd(int fd, MountImageParameters *p) { + int r, fl; + + assert(fd >= 0); + assert(p); + + r = fd_verify_regular(fd); + if (r < 0) + return r; + + fl = fd_verify_safe_flags(fd); + if (fl < 0) + return log_debug_errno(fl, "Image file descriptor has unsafe flags set: %m"); + + switch (fl & O_ACCMODE) { + + case O_RDONLY: + p->read_only = true; + break; + + case O_RDWR: + break; + + default: + return -EBADF; + } + + return 0; +} + +static int verify_trusted_image_fd_by_path(int fd) { + _cleanup_free_ char *p = NULL; + struct stat sta; + int r; + + assert(fd >= 0); + + r = secure_getenv_bool("SYSTEMD_MOUNTFSD_TRUSTED_DIRECTORIES"); + if (r == -ENXIO) { + if (!DEFAULT_MOUNTFSD_TRUSTED_DIRECTORIES) { + log_debug("Trusted directory mechanism disabled at compile time."); + return false; + } + } else if (r < 0) { + log_debug_errno(r, "Failed to parse $SYSTEMD_MOUNTFSD_TRUSTED_DIRECTORIES environment variable, not trusting any image."); + return false; + } else if (!r) { + log_debug("Trusted directory mechanism disabled via $SYSTEMD_MOUNTFSD_TRUSTED_DIRECTORIES environment variable."); + return false; + } + + r = fd_get_path(fd, &p); + if (r < 0) + return log_debug_errno(r, "Failed to get path of passed image file descriptor: %m"); + if (fstat(fd, &sta) < 0) + return log_debug_errno(errno, "Failed to stat() passed image file descriptor: %m"); + + log_debug("Checking if image '%s' is in trusted directories.", p); + + for (ImageClass c = 0; c < _IMAGE_CLASS_MAX; c++) + NULSTR_FOREACH(s, image_search_path[c]) { + _cleanup_close_ int dir_fd = -EBADF, inode_fd = -EBADF; + _cleanup_free_ char *q = NULL; + struct stat stb; + const char *e; + + r = chase(s, NULL, CHASE_SAFE, &q, &dir_fd); + if (r == -ENOENT) + continue; + if (r < 0) { + log_warning_errno(r, "Failed to resolve search path '%s', ignoring: %m", s); + continue; + } + + /* Check that the inode refers to a file immediately inside the image directory, + * i.e. not the image directory itself, and nothing further down the tree */ + e = path_startswith(p, q); + if (isempty(e)) + continue; + + e += strspn(e, "/"); + if (!filename_is_valid(e)) + continue; + + r = chaseat(dir_fd, e, CHASE_SAFE, NULL, &inode_fd); + if (r < 0) + return log_error_errno(r, "Couldn't verify that specified image '%s' is in search path '%s': %m", p, s); + + if (fstat(inode_fd, &stb) < 0) + return log_error_errno(errno, "Failed to stat image file '%s/%s': %m", q, e); + + if (stat_inode_same(&sta, &stb)) { + log_debug("Image '%s' is *in* trusted directories.", p); + return true; /* Yay */ + } + } + + log_debug("Image '%s' is *not* in trusted directories.", p); + return false; +} + +static int determine_image_policy( + int image_fd, + bool trusted, + ImagePolicy *client_policy, + ImagePolicy **ret) { + + _cleanup_(image_policy_freep) ImagePolicy *envvar_policy = NULL; + const ImagePolicy *default_policy; + const char *envvar, *e; + int r; + + assert(image_fd >= 0); + assert(ret); + + if (trusted) { + envvar = "SYSTEMD_MOUNTFSD_IMAGE_POLICY_TRUSTED"; + default_policy = &image_policy_allow; + } else { + envvar = "SYSTEMD_MOUNTFSD_IMAGE_POLICY_UNTRUSTED"; + default_policy = &image_policy_untrusted; + } + + e = secure_getenv(envvar); + if (e) { + r = image_policy_from_string(e, &envvar_policy); + if (r < 0) + return log_error_errno(r, "Failed to parse image policy supplied via $%s: %m", envvar); + + default_policy = envvar_policy; + } + + return image_policy_intersect(default_policy, client_policy, ret); +} + +static int validate_userns(Varlink *link, int *userns_fd) { + int r; + + assert(link); + assert(userns_fd); + + if (*userns_fd < 0) + return 0; + + r = fd_verify_safe_flags(*userns_fd); + if (r < 0) + return log_debug_errno(r, "User namespace file descriptor has unsafe flags set: %m"); + + r = fd_is_ns(*userns_fd, CLONE_NEWUSER); + if (r < 0) + return r; + if (r == 0) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + /* Our own host user namespace? Then close the fd, and handle it as if none was specified. */ + r = is_our_namespace(*userns_fd, NAMESPACE_USER); + if (r < 0) + return log_debug_errno(r, "Failed to determine if user namespace provided by client is our own."); + if (r > 0) { + log_debug("User namespace provided by client is our own."); + *userns_fd = safe_close(*userns_fd); + } + + return 0; +} + +static int vl_method_mount_image( + Varlink *link, + JsonVariant *parameters, + VarlinkMethodFlags flags, + void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "imageFileDescriptor", JSON_VARIANT_UNSIGNED, json_dispatch_uint, offsetof(MountImageParameters, image_fd_idx), JSON_MANDATORY }, + { "userNamespaceFileDescriptor", JSON_VARIANT_UNSIGNED, json_dispatch_uint, offsetof(MountImageParameters, userns_fd_idx), 0 }, + { "readOnly", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(MountImageParameters, read_only), 0 }, + { "growFileSystems", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(MountImageParameters, growfs), 0 }, + { "password", JSON_VARIANT_STRING, json_dispatch_string, offsetof(MountImageParameters, password), 0 }, + { "imagePolicy", JSON_VARIANT_STRING, json_dispatch_image_policy, offsetof(MountImageParameters, image_policy), 0 }, + VARLINK_DISPATCH_POLKIT_FIELD, + {} + }; + + _cleanup_(verity_settings_done) VeritySettings verity = VERITY_SETTINGS_DEFAULT; + _cleanup_(mount_image_parameters_done) MountImageParameters p = { + .image_fd_idx = UINT_MAX, + .userns_fd_idx = UINT_MAX, + .read_only = -1, + .growfs = -1, + }; + _cleanup_(dissected_image_unrefp) DissectedImage *di = NULL; + _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *aj = NULL; + _cleanup_close_ int image_fd = -EBADF, userns_fd = -EBADF; + _cleanup_(image_policy_freep) ImagePolicy *use_policy = NULL; + Hashmap **polkit_registry = ASSERT_PTR(userdata); + _cleanup_free_ char *ps = NULL; + bool image_is_trusted = false; + uid_t peer_uid; + int r; + + assert(link); + assert(parameters); + + json_variant_sensitive(parameters); /* might contain passwords */ + + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return log_debug_errno(r, "Failed to get client UID: %m"); + + r = varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + if (p.image_fd_idx != UINT_MAX) { + image_fd = varlink_peek_dup_fd(link, p.image_fd_idx); + if (image_fd < 0) + return log_debug_errno(image_fd, "Failed to peek image fd from client: %m"); + } + + if (p.userns_fd_idx != UINT_MAX) { + userns_fd = varlink_peek_dup_fd(link, p.userns_fd_idx); + if (userns_fd < 0) + return log_debug_errno(userns_fd, "Failed to peek user namespace fd from client: %m"); + } + + r = validate_image_fd(image_fd, &p); + if (r < 0) + return r; + + r = validate_userns(link, &userns_fd); + if (r != 0) + return r; + + r = verify_trusted_image_fd_by_path(image_fd); + if (r < 0) + return r; + image_is_trusted = r; + + const char *polkit_details[] = { + "read_only", one_zero(p.read_only > 0), + NULL, + }; + + const char *polkit_action, *polkit_untrusted_action; + PolkitFlags polkit_flags; + if (userns_fd < 0) { + /* Mount into the host user namespace */ + polkit_action = "io.systemd.mount-file-system.mount-image"; + polkit_untrusted_action = "io.systemd.mount-file-system.mount-untrusted-image"; + polkit_flags = 0; + } else { + /* Mount into a private user namespace */ + polkit_action = "io.systemd.mount-file-system.mount-image-privately"; + polkit_untrusted_action = "io.systemd.mount-file-system.mount-untrusted-image-privately"; + + /* If polkit is not around, let's allow mounting authenticated images by default */ + polkit_flags = POLKIT_DEFAULT_ALLOW; + } + + /* Let's definitely acquire the regular action privilege, for mounting properly signed images */ + r = varlink_verify_polkit_async_full( + link, + /* bus= */ NULL, + polkit_action, + polkit_details, + /* good_user= */ UID_INVALID, + polkit_flags, + polkit_registry); + if (r <= 0) + return r; + + /* Generate the commmon dissection directory here. We are not going to use it, but the clients might, + * and they likely are unprivileged, hence cannot create it themselves. Hence let's jsut create it + * here, if it is missing. */ + r = get_common_dissect_directory(NULL); + if (r < 0) + return r; + + r = loop_device_make( + image_fd, + p.read_only == 0 ? O_RDONLY : O_RDWR, + 0, + UINT64_MAX, + UINT32_MAX, + LO_FLAGS_PARTSCAN, + LOCK_EX, + &loop); + if (r < 0) + return r; + + DissectImageFlags dissect_flags = + (p.read_only == 0 ? DISSECT_IMAGE_READ_ONLY : 0) | + (p.growfs != 0 ? DISSECT_IMAGE_GROWFS : 0) | + DISSECT_IMAGE_DISCARD_ANY | + DISSECT_IMAGE_FSCK | + DISSECT_IMAGE_ADD_PARTITION_DEVICES | + DISSECT_IMAGE_PIN_PARTITION_DEVICES | + DISSECT_IMAGE_ALLOW_USERSPACE_VERITY; + + /* Let's see if we have acquired the privilege to mount untrusted images already */ + bool polkit_have_untrusted_action = + varlink_has_polkit_action(link, polkit_untrusted_action, polkit_details, polkit_registry); + + for (;;) { + use_policy = image_policy_free(use_policy); + ps = mfree(ps); + + /* We use the image policy for trusted images if either the path is below a trusted + * directory, or if we have already acquired a PK authentication that tells us that untrusted + * images are OK */ + bool use_trusted_policy = + image_is_trusted || + polkit_have_untrusted_action; + + r = determine_image_policy( + image_fd, + use_trusted_policy, + p.image_policy, + &use_policy); + if (r < 0) + return r; + + r = image_policy_to_string(use_policy, /* simplify= */ true, &ps); + if (r < 0) + return r; + + log_debug("Using image policy: %s", ps); + + r = dissect_loop_device( + loop, + &verity, + /* mount_options= */ NULL, + use_policy, + dissect_flags, + &di); + if (r == -ENOPKG) + return varlink_error(link, "io.systemd.MountFileSystem.IncompatibleImage", NULL); + if (r == -ENOTUNIQ) + return varlink_error(link, "io.systemd.MountFileSystem.MultipleRootPartitionsFound", NULL); + if (r == -ENXIO) + return varlink_error(link, "io.systemd.MountFileSystem.RootPartitionNotFound", NULL); + if (r == -ERFKILL) { + /* The image policy refused this, let's retry after trying to get PolicyKit */ + + if (!polkit_have_untrusted_action) { + log_debug("Denied by image policy. Trying a stronger polkit authentication before continuing."); + r = varlink_verify_polkit_async_full( + link, + /* bus= */ NULL, + polkit_untrusted_action, + polkit_details, + /* good_user= */ UID_INVALID, + /* flags= */ 0, /* NB: the image cannot be authenticated, hence unless PK is around to allow this anyway, fail! */ + polkit_registry); + if (r <= 0 && !ERRNO_IS_NEG_PRIVILEGE(r)) + return r; + if (r > 0) { + /* Try again, now that we know the client has enough privileges. */ + log_debug("Denied by image policy, retrying after polkit authentication."); + polkit_have_untrusted_action = true; + continue; + } + } + + return varlink_error(link, "io.systemd.MountFileSystem.DeniedByImagePolicy", NULL); + } + if (r < 0) + return r; + + /* Success */ + break; + } + + r = dissected_image_load_verity_sig_partition( + di, + loop->fd, + &verity); + if (r < 0) + return r; + + r = dissected_image_decrypt( + di, + p.password, + &verity, + dissect_flags); + if (r == -ENOKEY) /* new dm-verity userspace returns ENOKEY if the dm-verity signature key is not in + * key chain. That's great. */ + return varlink_error(link, "io.systemd.MountFileSystem.KeyNotFound", NULL); + if (r == -EBUSY) /* DM kernel subsystem is shit with returning useful errors hence we keep retrying + * under the assumption that some errors are transitional. Which the errors might + * not actually be. After all retries failed we return EBUSY. Let's turn that into a + * generic Verity error. It's not very helpful, could mean anything, but at least it + * gives client a clear idea that this has to do with Verity. */ + return varlink_error(link, "io.systemd.MountFileSystem.VerityFailure", NULL); + if (r < 0) + return r; + + r = dissected_image_mount( + di, + /* where= */ NULL, + /* uid_shift= */ UID_INVALID, + /* uid_range= */ UID_INVALID, + userns_fd, + dissect_flags); + if (r < 0) + return r; + + for (PartitionDesignator d = 0; d < _PARTITION_DESIGNATOR_MAX; d++) { + _cleanup_(json_variant_unrefp) JsonVariant *pj = NULL; + DissectedPartition *pp = di->partitions + d; + int fd_idx; + + if (!pp->found) + continue; + + if (pp->fsmount_fd < 0) + continue; + + if (userns_fd >= 0) { + r = nsresource_add_mount(userns_fd, pp->fsmount_fd); + if (r < 0) + return r; + } + + fd_idx = varlink_push_fd(link, pp->fsmount_fd); + if (fd_idx < 0) + return fd_idx; + + TAKE_FD(pp->fsmount_fd); + + r = json_build(&pj, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("designator", JSON_BUILD_STRING(partition_designator_to_string(d))), + JSON_BUILD_PAIR("writable", JSON_BUILD_BOOLEAN(pp->rw)), + JSON_BUILD_PAIR("growFileSystem", JSON_BUILD_BOOLEAN(pp->growfs)), + JSON_BUILD_PAIR_CONDITION(pp->partno > 0, "partitionNumber", JSON_BUILD_INTEGER(pp->partno)), + JSON_BUILD_PAIR_CONDITION(pp->architecture > 0, "architecture", JSON_BUILD_STRING(architecture_to_string(pp->architecture))), + JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(pp->uuid), "partitionUuid", JSON_BUILD_UUID(pp->uuid)), + JSON_BUILD_PAIR("fileSystemType", JSON_BUILD_STRING(dissected_partition_fstype(pp))), + JSON_BUILD_PAIR_CONDITION(pp->label, "partitionLabel", JSON_BUILD_STRING(pp->label)), + JSON_BUILD_PAIR("size", JSON_BUILD_INTEGER(pp->size)), + JSON_BUILD_PAIR("offset", JSON_BUILD_INTEGER(pp->offset)), + JSON_BUILD_PAIR("mountFileDescriptor", JSON_BUILD_INTEGER(fd_idx)))); + if (r < 0) + return r; + + r = json_variant_append_array(&aj, pj); + if (r < 0) + return r; + } + + loop_device_relinquish(loop); + + r = varlink_replyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("partitions", JSON_BUILD_VARIANT(aj)), + JSON_BUILD_PAIR("imagePolicy", JSON_BUILD_STRING(ps)), + JSON_BUILD_PAIR("imageSize", JSON_BUILD_INTEGER(di->image_size)), + JSON_BUILD_PAIR("sectorSize", JSON_BUILD_INTEGER(di->sector_size)), + JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(di->image_uuid), "imageUuid", JSON_BUILD_UUID(di->image_uuid)))); + if (r < 0) + return r; + + return r; +} + +static int process_connection(VarlinkServer *server, int _fd) { + _cleanup_close_ int fd = TAKE_FD(_fd); /* always take possesion */ + _cleanup_(varlink_close_unrefp) Varlink *vl = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + int r; + + r = sd_event_new(&event); + if (r < 0) + return r; + + r = varlink_server_attach_event(server, event, 0); + if (r < 0) + return log_error_errno(r, "Failed to attach Varlink server to event loop: %m"); + + r = varlink_server_add_connection(server, fd, &vl); + if (r < 0) + return log_error_errno(r, "Failed to add connection: %m"); + + TAKE_FD(fd); + vl = varlink_ref(vl); + + r = varlink_set_allow_fd_passing_input(vl, true); + if (r < 0) + return log_error_errno(r, "Failed to enable fd passing for read: %m"); + + r = varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_error_errno(r, "Failed to enable fd passing for write: %m"); + + r = sd_event_loop(event); + if (r < 0) + return log_error_errno(r, "Failed to run event loop: %m"); + + r = varlink_server_detach_event(server); + if (r < 0) + return log_error_errno(r, "Failed to detach Varlink server from event loop: %m"); + + return 0; +} + +static int run(int argc, char *argv[]) { + usec_t start_time, listen_idle_usec, last_busy_usec = USEC_INFINITY; + _cleanup_(varlink_server_unrefp) VarlinkServer *server = NULL; + _cleanup_(hashmap_freep) Hashmap *polkit_registry = NULL; + _cleanup_(pidref_done) PidRef parent = PIDREF_NULL; + unsigned n_iterations = 0; + int m, listen_fd, r; + + log_setup(); + + m = sd_listen_fds(false); + if (m < 0) + return log_error_errno(m, "Failed to determine number of listening fds: %m"); + if (m == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No socket to listen on received."); + if (m > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Worker can only listen on a single socket at a time."); + + listen_fd = SD_LISTEN_FDS_START; + + r = fd_nonblock(listen_fd, false); + if (r < 0) + return log_error_errno(r, "Failed to turn off non-blocking mode for listening socket: %m"); + + r = varlink_server_new(&server, VARLINK_SERVER_INHERIT_USERDATA); + if (r < 0) + return log_error_errno(r, "Failed to allocate server: %m"); + + r = varlink_server_add_interface(server, &vl_interface_io_systemd_MountFileSystem); + if (r < 0) + return log_error_errno(r, "Failed to add MountFileSystem interface to varlink server: %m"); + + r = varlink_server_bind_method_many( + server, + "io.systemd.MountFileSystem.MountImage",vl_method_mount_image); + if (r < 0) + return log_error_errno(r, "Failed to bind methods: %m"); + + varlink_server_set_userdata(server, &polkit_registry); + + r = varlink_server_set_exit_on_idle(server, true); + if (r < 0) + return log_error_errno(r, "Failed to enable exit-on-idle mode: %m"); + + r = getenv_bool("MOUNTFS_FIXED_WORKER"); + if (r < 0) + return log_error_errno(r, "Failed to parse MOUNTFSD_FIXED_WORKER: %m"); + listen_idle_usec = r ? USEC_INFINITY : LISTEN_IDLE_USEC; + + r = pidref_set_parent(&parent); + if (r < 0) + return log_error_errno(r, "Failed to acquire pidfd of parent process: %m"); + + start_time = now(CLOCK_MONOTONIC); + + for (;;) { + _cleanup_close_ int fd = -EBADF; + usec_t n; + + /* Exit the worker in regular intervals, to flush out all memory use */ + if (n_iterations++ > ITERATIONS_MAX) { + log_debug("Exiting worker, processed %u iterations, that's enough.", n_iterations); + break; + } + + n = now(CLOCK_MONOTONIC); + if (n >= usec_add(start_time, RUNTIME_MAX_USEC)) { + log_debug("Exiting worker, ran for %s, that's enough.", + FORMAT_TIMESPAN(usec_sub_unsigned(n, start_time), 0)); + break; + } + + if (last_busy_usec == USEC_INFINITY) + last_busy_usec = n; + else if (listen_idle_usec != USEC_INFINITY && n >= usec_add(last_busy_usec, listen_idle_usec)) { + log_debug("Exiting worker, been idle for %s.", + FORMAT_TIMESPAN(usec_sub_unsigned(n, last_busy_usec), 0)); + break; + } + + (void) rename_process("systemd-mountwork: waiting..."); + fd = RET_NERRNO(accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK|SOCK_CLOEXEC)); + (void) rename_process("systemd-mountwork: processing..."); + + if (fd == -EAGAIN) + continue; /* The listening socket has SO_RECVTIMEO set, hence a timeout is expected + * after a while, let's check if it's time to exit though. */ + if (fd == -EINTR) + continue; /* Might be that somebody attached via strace, let's just continue in that + * case */ + if (fd < 0) + return log_error_errno(fd, "Failed to accept() from listening socket: %m"); + + if (now(CLOCK_MONOTONIC) <= usec_add(n, PRESSURE_SLEEP_TIME_USEC)) { + /* We only slept a very short time? If so, let's see if there are more sockets + * pending, and if so, let's ask our parent for more workers */ + + r = fd_wait_for_event(listen_fd, POLLIN, 0); + if (r < 0) + return log_error_errno(r, "Failed to test for POLLIN on listening socket: %m"); + + if (FLAGS_SET(r, POLLIN)) { + r = pidref_kill(&parent, SIGUSR2); + if (r == -ESRCH) + return log_error_errno(r, "Parent already died?"); + if (r < 0) + return log_error_errno(r, "Failed to send SIGUSR2 signal to parent. %m"); + } + } + + (void) process_connection(server, TAKE_FD(fd)); + last_busy_usec = USEC_INFINITY; + } + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/shared/meson.build b/src/shared/meson.build index eb92433e6d..17313aefed 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -181,6 +181,7 @@ shared_sources = files( 'varlink-io.systemd.Hostname.c', 'varlink-io.systemd.Journal.c', 'varlink-io.systemd.ManagedOOM.c', + 'varlink-io.systemd.MountFileSystem.c', 'varlink-io.systemd.NamespaceResource.c', 'varlink-io.systemd.Network.c', 'varlink-io.systemd.PCRExtend.c', diff --git a/src/shared/varlink-io.systemd.MountFileSystem.c b/src/shared/varlink-io.systemd.MountFileSystem.c new file mode 100644 index 0000000000..4a33578a10 --- /dev/null +++ b/src/shared/varlink-io.systemd.MountFileSystem.c @@ -0,0 +1,69 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.MountFileSystem.h" + +static VARLINK_DEFINE_ENUM_TYPE( + PartitionDesignator, + VARLINK_DEFINE_ENUM_VALUE(root), + VARLINK_DEFINE_ENUM_VALUE(usr), + VARLINK_DEFINE_ENUM_VALUE(home), + VARLINK_DEFINE_ENUM_VALUE(srv), + VARLINK_DEFINE_ENUM_VALUE(esp), + VARLINK_DEFINE_ENUM_VALUE(xbootldr), + VARLINK_DEFINE_ENUM_VALUE(swap), + VARLINK_DEFINE_ENUM_VALUE(root_verity), + VARLINK_DEFINE_ENUM_VALUE(usr_verity), + VARLINK_DEFINE_ENUM_VALUE(root_verity_sig), + VARLINK_DEFINE_ENUM_VALUE(usr_verity_sig), + VARLINK_DEFINE_ENUM_VALUE(tmp), + VARLINK_DEFINE_ENUM_VALUE(var)); + +static VARLINK_DEFINE_STRUCT_TYPE( + PartitionInfo, + VARLINK_DEFINE_FIELD(designator, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(writable, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD(growFileSystem, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD(partitionNumber, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(architecture, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(partitionUuid, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(fileSystemType, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(partitionLabel, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(size, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(offset, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(mountFileDescriptor, VARLINK_INT, 0)); + +static VARLINK_DEFINE_METHOD( + MountImage, + VARLINK_DEFINE_INPUT(imageFileDescriptor, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(readOnly, VARLINK_BOOL, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(growFileSystems, VARLINK_BOOL, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(password, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(imagePolicy, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(allowInteractiveAuthentication, VARLINK_BOOL, VARLINK_NULLABLE), + VARLINK_DEFINE_OUTPUT_BY_TYPE(partitions, PartitionInfo, VARLINK_ARRAY), + VARLINK_DEFINE_OUTPUT(imagePolicy, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(imageSize, VARLINK_INT, 0), + VARLINK_DEFINE_OUTPUT(sectorSize, VARLINK_INT, 0), + VARLINK_DEFINE_OUTPUT(imageName, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_OUTPUT(imageUuid, VARLINK_STRING, VARLINK_NULLABLE)); + +static VARLINK_DEFINE_ERROR(IncompatibleImage); +static VARLINK_DEFINE_ERROR(MultipleRootPartitionsFound); +static VARLINK_DEFINE_ERROR(RootPartitionNotFound); +static VARLINK_DEFINE_ERROR(DeniedByImagePolicy); +static VARLINK_DEFINE_ERROR(KeyNotFound); +static VARLINK_DEFINE_ERROR(VerityFailure); + +VARLINK_DEFINE_INTERFACE( + io_systemd_MountFileSystem, + "io.systemd.MountFileSystem", + &vl_type_PartitionDesignator, + &vl_type_PartitionInfo, + &vl_method_MountImage, + &vl_error_IncompatibleImage, + &vl_error_MultipleRootPartitionsFound, + &vl_error_RootPartitionNotFound, + &vl_error_DeniedByImagePolicy, + &vl_error_KeyNotFound, + &vl_error_VerityFailure); diff --git a/src/shared/varlink-io.systemd.MountFileSystem.h b/src/shared/varlink-io.systemd.MountFileSystem.h new file mode 100644 index 0000000000..dc75957df4 --- /dev/null +++ b/src/shared/varlink-io.systemd.MountFileSystem.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd_MountFileSystem; diff --git a/src/test/test-varlink-idl.c b/src/test/test-varlink-idl.c index d64c6e9d3e..3323810368 100644 --- a/src/test/test-varlink-idl.c +++ b/src/test/test-varlink-idl.c @@ -12,6 +12,7 @@ #include "varlink-io.systemd.Credentials.h" #include "varlink-io.systemd.Journal.h" #include "varlink-io.systemd.ManagedOOM.h" +#include "varlink-io.systemd.MountFileSystem.h" #include "varlink-io.systemd.NamespaceResource.h" #include "varlink-io.systemd.Network.h" #include "varlink-io.systemd.PCRExtend.h" @@ -140,6 +141,8 @@ TEST(parse_format) { print_separator(); test_parse_format_one(&vl_interface_io_systemd_ManagedOOM); print_separator(); + test_parse_format_one(&vl_interface_io_systemd_MountFileSystem); + print_separator(); test_parse_format_one(&vl_interface_io_systemd_Network); print_separator(); test_parse_format_one(&vl_interface_io_systemd_oom); diff --git a/units/meson.build b/units/meson.build index 76510a08f9..2f972901d4 100644 --- a/units/meson.build +++ b/units/meson.build @@ -732,6 +732,14 @@ units = [ 'file' : 'systemd-userdbd.socket', 'conditions' : ['ENABLE_USERDB'], }, + { + 'file' : 'systemd-mountfsd.service.in', + 'conditions' : ['ENABLE_MOUNTFSD'], + }, + { + 'file' : 'systemd-mountfsd.socket', + 'conditions' : ['ENABLE_MOUNTFSD'], + }, { 'file' : 'systemd-nsresourced.service.in', 'conditions' : ['ENABLE_NSRESOURCED'], diff --git a/units/systemd-mountfsd.service.in b/units/systemd-mountfsd.service.in new file mode 100644 index 0000000000..20a9b425ab --- /dev/null +++ b/units/systemd-mountfsd.service.in @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=DDI File System Mounter +Documentation=man:systemd-mountfsd.service(8) +Requires=systemd-mountfsd.socket +After=systemd-mountfsd.socket +Conflicts=shutdown.target +Before=sysinit.target shutdown.target +DefaultDependencies=no + +[Service] +#CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_SYS_RESOURCE CAP_BPF CAP_PERFMON CAP_SETGID CAP_SETUID +ExecStart={{LIBEXECDIR}}/systemd-mountfsd +IPAddressDeny=any +LimitNOFILE={{HIGH_RLIMIT_NOFILE}} +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +ProtectProc=invisible +ProtectControlGroups=yes +ProtectHome=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectSystem=strict +RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6 +RestrictRealtime=yes +RestrictSUIDSGID=yes +SystemCallArchitectures=native +SystemCallErrorNumber=EPERM +SystemCallFilter=@system-service @mount +Type=notify +NotifyAccess=all +FileDescriptorStoreMax=4096 +{{SERVICE_WATCHDOG}} + +[Install] +Also=systemd-mountfsd.socket diff --git a/units/systemd-mountfsd.socket b/units/systemd-mountfsd.socket new file mode 100644 index 0000000000..cd88003af9 --- /dev/null +++ b/units/systemd-mountfsd.socket @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=DDI File System Mounter Socket +Documentation=man:systemd-mountfsd.service(8) +DefaultDependencies=no +Conflicts=shutdown.target +Before=sockets.target shutdown.target + +[Socket] +ListenStream=/run/systemd/io.systemd.MountFileSystem +SocketMode=0666 + +[Install] +WantedBy=sockets.target From fe7d8235e1b1c4ce4b39aaa3fbf29646f9a19571 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 4 Dec 2023 22:09:15 +0100 Subject: [PATCH 26/31] dissect-image: add a generic varlink client side for mountfsd --- src/shared/dissect-image.c | 208 +++++++++++++++++++++++++++++++++++++ src/shared/dissect-image.h | 3 + 2 files changed, 211 insertions(+) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index ad10f6bfdd..c5f35c7cb7 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -74,6 +74,7 @@ #include "tmpfile-util.h" #include "udev-util.h" #include "user-util.h" +#include "varlink.h" #include "xattr-util.h" /* how many times to wait for the device nodes to appear */ @@ -4156,3 +4157,210 @@ int get_common_dissect_directory(char **ret) { return 0; } + +#if HAVE_BLKID + +static JSON_DISPATCH_ENUM_DEFINE(dispatch_architecture, Architecture, architecture_from_string); +static JSON_DISPATCH_ENUM_DEFINE(dispatch_partition_designator, PartitionDesignator, partition_designator_from_string); + +typedef struct PartitionFields { + PartitionDesignator designator; + bool rw; + bool growfs; + unsigned partno; + Architecture architecture; + sd_id128_t uuid; + char *fstype; + char *label; + uint64_t size; + uint64_t offset; + unsigned fsmount_fd_idx; +} PartitionFields; + +static void partition_fields_done(PartitionFields *f) { + assert(f); + + f->fstype = mfree(f->fstype); + f->label = mfree(f->label); +} + +typedef struct ReplyParameters { + JsonVariant *partitions; + char *image_policy; + uint64_t image_size; + uint32_t sector_size; + sd_id128_t image_uuid; +} ReplyParameters; + +static void reply_parameters_done(ReplyParameters *p) { + assert(p); + + p->image_policy = mfree(p->image_policy); + p->partitions = json_variant_unref(p->partitions); +} + +#endif + +int mountfsd_mount_image( + const char *path, + int userns_fd, + const ImagePolicy *image_policy, + DissectImageFlags flags, + DissectedImage **ret) { + +#if HAVE_BLKID + _cleanup_(reply_parameters_done) ReplyParameters p = {}; + + static const JsonDispatch dispatch_table[] = { + { "partitions", JSON_VARIANT_ARRAY, json_dispatch_variant, offsetof(struct ReplyParameters, partitions), JSON_MANDATORY }, + { "imagePolicy", JSON_VARIANT_STRING, json_dispatch_string, offsetof(struct ReplyParameters, image_policy), 0 }, + { "imageSize", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(struct ReplyParameters, image_size), JSON_MANDATORY }, + { "sectorSize", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint32, offsetof(struct ReplyParameters, sector_size), JSON_MANDATORY }, + { "imageUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(struct ReplyParameters, image_uuid), 0 }, + {} + }; + + _cleanup_(dissected_image_unrefp) DissectedImage *di = NULL; + _cleanup_close_ int image_fd = -EBADF; + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + _cleanup_free_ char *ps = NULL; + unsigned max_fd = UINT_MAX; + const char *error_id; + int r; + + assert(path); + assert(ret); + + r = varlink_connect_address(&vl, "/run/systemd/io.systemd.MountFileSystem"); + if (r < 0) + return log_error_errno(r, "Failed to connect to mountfsd: %m"); + + r = varlink_set_allow_fd_passing_input(vl, true); + if (r < 0) + return log_error_errno(r, "Failed to enable varlink fd passing for read: %m"); + + r = varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_error_errno(r, "Failed to enable varlink fd passing for write: %m"); + + image_fd = open(path, O_RDONLY|O_CLOEXEC); + if (image_fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", path); + + r = varlink_push_dup_fd(vl, image_fd); + if (r < 0) + return log_error_errno(r, "Failed to push image fd into varlink connection: %m"); + + if (userns_fd >= 0) { + r = varlink_push_dup_fd(vl, userns_fd); + if (r < 0) + return log_error_errno(r, "Failed to push image fd into varlink connection: %m"); + } + + if (image_policy) { + r = image_policy_to_string(image_policy, /* simplify= */ false, &ps); + if (r < 0) + return log_error_errno(r, "Failed format image policy to string: %m"); + } + + JsonVariant *reply = NULL; + r = varlink_callb( + vl, + "io.systemd.MountFileSystem.MountImage", + &reply, + &error_id, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("imageFileDescriptor", JSON_BUILD_UNSIGNED(0)), + JSON_BUILD_PAIR_CONDITION(userns_fd >= 0, "userNamespaceFileDescriptor", JSON_BUILD_UNSIGNED(1)), + JSON_BUILD_PAIR("readOnly", JSON_BUILD_BOOLEAN(FLAGS_SET(flags, DISSECT_IMAGE_MOUNT_READ_ONLY))), + JSON_BUILD_PAIR("growFileSystems", JSON_BUILD_BOOLEAN(FLAGS_SET(flags, DISSECT_IMAGE_GROWFS))), + JSON_BUILD_PAIR_CONDITION(ps, "imagePolicy", JSON_BUILD_STRING(ps)), + JSON_BUILD_PAIR("allowInteractiveAuthentication", JSON_BUILD_BOOLEAN(FLAGS_SET(flags, DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH))))); + if (r < 0) + return log_error_errno(r, "Failed to call MountImage() varlink call: %m"); + if (!isempty(error_id)) + return log_error_errno(varlink_error_to_errno(error_id, reply), "Failed to call MountImage() varlink call: %s", error_id); + + r = json_dispatch(reply, dispatch_table, JSON_ALLOW_EXTENSIONS, &p); + if (r < 0) + return log_error_errno(r, "Failed to parse MountImage() reply: %m"); + + log_debug("Effective image policy: %s", p.image_policy); + + JsonVariant *i; + JSON_VARIANT_ARRAY_FOREACH(i, p.partitions) { + _cleanup_close_ int fsmount_fd = -EBADF; + + _cleanup_(partition_fields_done) PartitionFields pp = { + .designator = _PARTITION_DESIGNATOR_INVALID, + .architecture = _ARCHITECTURE_INVALID, + .size = UINT64_MAX, + .offset = UINT64_MAX, + .fsmount_fd_idx = UINT_MAX, + }; + + static const JsonDispatch partition_dispatch_table[] = { + { "designator", JSON_VARIANT_STRING, dispatch_partition_designator, offsetof(struct PartitionFields, designator), JSON_MANDATORY }, + { "writable", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(struct PartitionFields, rw), JSON_MANDATORY }, + { "growFileSystem", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(struct PartitionFields, growfs), JSON_MANDATORY }, + { "partitionNumber", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(struct PartitionFields, partno), 0 }, + { "architecture", JSON_VARIANT_STRING, dispatch_architecture, offsetof(struct PartitionFields, architecture), 0 }, + { "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(struct PartitionFields, uuid), 0 }, + { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(struct PartitionFields, fstype), JSON_MANDATORY }, + { "partitionLabel", JSON_VARIANT_STRING, json_dispatch_string, offsetof(struct PartitionFields, label), 0 }, + { "size", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(struct PartitionFields, size), JSON_MANDATORY }, + { "offset", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(struct PartitionFields, offset), JSON_MANDATORY }, + { "mountFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(struct PartitionFields, fsmount_fd_idx), JSON_MANDATORY }, + {} + }; + + r = json_dispatch(i, partition_dispatch_table, JSON_ALLOW_EXTENSIONS, &pp); + if (r < 0) + return log_error_errno(r, "Failed to parse partition data: %m"); + + if (pp.fsmount_fd_idx != UINT_MAX) { + if (max_fd == UINT_MAX || pp.fsmount_fd_idx > max_fd) + max_fd = pp.fsmount_fd_idx; + + fsmount_fd = varlink_take_fd(vl, pp.fsmount_fd_idx); + if (fsmount_fd < 0) + return fsmount_fd; + } + + assert(pp.designator >= 0); + + if (!di) { + r = dissected_image_new(path, &di); + if (r < 0) + return log_error_errno(r, "Failed to allocated new dissected image structure: %m"); + } + + if (di->partitions[pp.designator].found) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Duplicate partition data for '%s'.", partition_designator_to_string(pp.designator)); + + di->partitions[pp.designator] = (DissectedPartition) { + .found = true, + .rw = pp.rw, + .growfs = pp.growfs, + .partno = pp.partno, + .architecture = pp.architecture, + .uuid = pp.uuid, + .fstype = TAKE_PTR(pp.fstype), + .label = TAKE_PTR(pp.label), + .mount_node_fd = -EBADF, + .size = pp.size, + .offset = pp.offset, + .fsmount_fd = TAKE_FD(fsmount_fd), + }; + } + + di->image_size = p.image_size; + di->sector_size = p.sector_size; + di->image_uuid = p.image_uuid; + + *ret = TAKE_PTR(di); + return 0; +#else + return -EOPNOTSUPP; +#endif +} diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index 7623c1181e..e31fd54f84 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -88,6 +88,7 @@ typedef enum DissectImageFlags { DISSECT_IMAGE_ALLOW_EMPTY = 1 << 24, /* Allow that no usable partitions is present */ DISSECT_IMAGE_TRY_ATOMIC_MOUNT_EXCHANGE = 1 << 25, /* Try to mount the image beneath the specified mountpoint, rather than on top of it, and then umount the top */ DISSECT_IMAGE_ALLOW_USERSPACE_VERITY = 1 << 26, /* Allow userspace verity keyring in /etc/verity.d/ and related dirs */ + DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH = 1 << 27, /* Allow interactive authorization when going through mountfsd */ } DissectImageFlags; struct DissectedImage { @@ -241,3 +242,5 @@ static inline const char *dissected_partition_fstype(const DissectedPartition *m } int get_common_dissect_directory(char **ret); + +int mountfsd_mount_image(const char *path, int userns_fd, const ImagePolicy *image_policy, DissectImageFlags flags, DissectedImage **ret); From fdec6d1560896cdce6f7c42c2e6d811c0c12aefc Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Apr 2023 19:07:10 +0200 Subject: [PATCH 27/31] dissect-tool: allow systemd-dissect to talk to mountfsd --- src/dissect/dissect.c | 198 ++++++++++++++++++++++++++++-------------- 1 file changed, 135 insertions(+), 63 deletions(-) diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index d6939152e0..d68c29cbf1 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -31,10 +31,12 @@ #include "log.h" #include "loop-util.h" #include "main-func.h" +#include "missing_syscall.h" #include "mkdir.h" #include "mount-util.h" #include "mountpoint-util.h" #include "namespace-util.h" +#include "nsresource.h" #include "parse-argument.h" #include "parse-util.h" #include "path-util.h" @@ -91,6 +93,7 @@ static char **arg_argv = NULL; static char *arg_loop_ref = NULL; static ImagePolicy *arg_image_policy = NULL; static bool arg_mtree_hash = true; +static bool arg_via_service = false; STATIC_DESTRUCTOR_REGISTER(arg_image, freep); STATIC_DESTRUCTOR_REGISTER(arg_root, freep); @@ -704,6 +707,18 @@ static int parse_argv(int argc, char *argv[]) { assert_not_reached(); } + r = getenv_bool("SYSTEMD_USE_MOUNTFSD"); + if (r < 0) { + if (r != -ENXIO) + return log_error_errno(r, "Failed to parse $SYSTEMD_USE_MOUNTFSD: %m"); + } else + arg_via_service = r; + + if (!IN_SET(arg_action, ACTION_DISSECT, ACTION_LIST, ACTION_MTREE, ACTION_COPY_FROM, ACTION_COPY_TO, ACTION_DISCOVER, ACTION_VALIDATE) && geteuid() != 0) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be root."); + + SET_FLAG(arg_flags, DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH, isatty(STDIN_FILENO)); + return 1; } @@ -841,7 +856,11 @@ static int get_extension_scopes(DissectedImage *m, ImageClass class, char ***ret return 1; } -static int action_dissect(DissectedImage *m, LoopDevice *d) { +static int action_dissect( + DissectedImage *m, + LoopDevice *d, + int userns_fd) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; _cleanup_(table_unrefp) Table *t = NULL; _cleanup_free_ char *bn = NULL; @@ -849,7 +868,6 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { int r; assert(m); - assert(d); r = path_extract_filename(arg_image, &bn); if (r < 0) @@ -875,7 +893,7 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { fflush(stdout); } - r = dissected_image_acquire_metadata(m, /* userns_fd= */ -EBADF, /* extra_flags= */ 0); + r = dissected_image_acquire_metadata(m, userns_fd, /* extra_flags= */ 0); if (r == -ENXIO) return log_error_errno(r, "No root partition discovered."); if (r == -EUCLEAN) @@ -1087,7 +1105,6 @@ static int action_mount(DissectedImage *m, LoopDevice *d) { int r; assert(m); - assert(d); assert(arg_action == ACTION_MOUNT); r = dissected_image_mount_and_warn( @@ -1100,9 +1117,11 @@ static int action_mount(DissectedImage *m, LoopDevice *d) { if (r < 0) return r; - r = loop_device_flock(d, LOCK_UN); - if (r < 0) - return log_error_errno(r, "Failed to unlock loopback block device: %m"); + if (d) { + r = loop_device_flock(d, LOCK_UN); + if (r < 0) + return log_error_errno(r, "Failed to unlock loopback block device: %m"); + } r = dissected_image_relinquish(m); if (r < 0) @@ -1390,8 +1409,8 @@ static int archive_item( } #endif -static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopDevice *d) { - _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; +static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopDevice *d, int userns_fd) { + _cleanup_(umount_and_freep) char *mounted_dir = NULL; _cleanup_free_ char *t = NULL; const char *root; int r; @@ -1400,12 +1419,16 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD if (arg_image) { assert(m); - assert(d); - r = detach_mount_namespace(); + if (userns_fd < 0) + r = detach_mount_namespace_harder(0, 0); + else + r = detach_mount_namespace_userns(userns_fd); if (r < 0) return log_error_errno(r, "Failed to detach mount namespace: %m"); + /* Create a place we can mount things onto soon. We use a fixed path shared by all invocations. Given + * the mounts are done in a mount namespace there's not going to be a collision here */ r = get_common_dissect_directory(&t); if (r < 0) return log_error_errno(r, "Failed generate private mount directory: %m"); @@ -1422,9 +1445,11 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD mounted_dir = TAKE_PTR(t); - r = loop_device_flock(d, LOCK_UN); - if (r < 0) - return log_error_errno(r, "Failed to unlock loopback block device: %m"); + if (d) { + r = loop_device_flock(d, LOCK_UN); + if (r < 0) + return log_error_errno(r, "Failed to unlock loopback block device: %m"); + } r = dissected_image_relinquish(m); if (r < 0) @@ -1433,6 +1458,8 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD root = mounted_dir ?: arg_root; + dissected_image_close(m); + switch (arg_action) { case ACTION_COPY_FROM: { @@ -1716,7 +1743,6 @@ static int action_with(DissectedImage *m, LoopDevice *d) { int r, rcode; assert(m); - assert(d); assert(arg_action == ACTION_WITH); r = tempfn_random_child(NULL, program_invocation_short_name, &temp); @@ -1745,9 +1771,11 @@ static int action_with(DissectedImage *m, LoopDevice *d) { if (r < 0) return log_error_errno(r, "Failed to relinquish DM and loopback block devices: %m"); - r = loop_device_flock(d, LOCK_UN); - if (r < 0) - return log_error_errno(r, "Failed to unlock loopback block device: %m"); + if (d) { + r = loop_device_flock(d, LOCK_UN); + if (r < 0) + return log_error_errno(r, "Failed to unlock loopback block device: %m"); + } rcode = safe_fork("(with)", FORK_CLOSE_ALL_FDS|FORK_LOG|FORK_WAIT, NULL); if (rcode == 0) { @@ -1788,14 +1816,16 @@ static int action_with(DissectedImage *m, LoopDevice *d) { } /* Let's manually detach everything, to make things synchronous */ - r = loop_device_flock(d, LOCK_SH); - if (r < 0) - log_warning_errno(r, "Failed to lock loopback block device, ignoring: %m"); + if (d) { + r = loop_device_flock(d, LOCK_SH); + if (r < 0) + log_warning_errno(r, "Failed to lock loopback block device, ignoring: %m"); + } r = umount_recursive(mounted_dir, 0); if (r < 0) log_warning_errno(r, "Failed to unmount '%s', ignoring: %m", mounted_dir); - else + else if (d) loop_device_unrelinquish(d); /* Let's try to destroy the loopback device */ created_dir = TAKE_PTR(mounted_dir); @@ -1982,8 +2012,8 @@ static int action_validate(void) { static int run(int argc, char *argv[]) { _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL; _cleanup_(loop_device_unrefp) LoopDevice *d = NULL; - uint32_t loop_flags; - int open_flags, r; + _cleanup_close_ int userns_fd = -EBADF; + int r; log_setup(); @@ -2015,7 +2045,7 @@ static int run(int argc, char *argv[]) { return action_discover(); default: - /* All other actions need the image dissected */ + /* All other actions need the image dissected (except for ACTION_VALIDATE, see below) */ break; } @@ -2031,50 +2061,92 @@ static int run(int argc, char *argv[]) { * hence if there's external Verity data * available we turn off partition table * support */ + } - if (arg_action == ACTION_VALIDATE) - return action_validate(); + if (arg_action == ACTION_VALIDATE) + return action_validate(); - open_flags = FLAGS_SET(arg_flags, DISSECT_IMAGE_DEVICE_READ_ONLY) ? O_RDONLY : O_RDWR; - loop_flags = FLAGS_SET(arg_flags, DISSECT_IMAGE_NO_PARTITION_TABLE) ? 0 : LO_FLAGS_PARTSCAN; - if (arg_in_memory) - r = loop_device_make_by_path_memory(arg_image, open_flags, /* sector_size= */ UINT32_MAX, loop_flags, LOCK_SH, &d); - else - r = loop_device_make_by_path(arg_image, open_flags, /* sector_size= */ UINT32_MAX, loop_flags, LOCK_SH, &d); - if (r < 0) - return log_error_errno(r, "Failed to set up loopback device for %s: %m", arg_image); + if (arg_image) { + /* First try locally, if we are allowed to */ + if (!arg_via_service) { + uint32_t loop_flags; + int open_flags; - if (arg_loop_ref) { - r = loop_device_set_filename(d, arg_loop_ref); - if (r < 0) - log_warning_errno(r, "Failed to set loop reference string to '%s', ignoring: %m", arg_loop_ref); + open_flags = FLAGS_SET(arg_flags, DISSECT_IMAGE_DEVICE_READ_ONLY) ? O_RDONLY : O_RDWR; + loop_flags = FLAGS_SET(arg_flags, DISSECT_IMAGE_NO_PARTITION_TABLE) ? 0 : LO_FLAGS_PARTSCAN; + + if (arg_in_memory) + r = loop_device_make_by_path_memory(arg_image, open_flags, /* sector_size= */ UINT32_MAX, loop_flags, LOCK_SH, &d); + else + r = loop_device_make_by_path(arg_image, open_flags, /* sector_size= */ UINT32_MAX, loop_flags, LOCK_SH, &d); + if (r < 0) { + if (!ERRNO_IS_PRIVILEGE(r) || !IN_SET(arg_action, ACTION_DISSECT, ACTION_LIST, ACTION_MTREE, ACTION_COPY_FROM, ACTION_COPY_TO)) + return log_error_errno(r, "Failed to set up loopback device for %s: %m", arg_image); + + log_debug_errno(r, "Lacking permissions to set up loopback block device for %s, using service: %m", arg_image); + arg_via_service = true; + } else { + if (arg_loop_ref) { + r = loop_device_set_filename(d, arg_loop_ref); + if (r < 0) + log_warning_errno(r, "Failed to set loop reference string to '%s', ignoring: %m", arg_loop_ref); + } + + r = dissect_loop_device_and_warn( + d, + &arg_verity_settings, + /* mount_options= */ NULL, + arg_image_policy, + arg_flags, + &m); + if (r < 0) + return r; + + if (arg_action == ACTION_ATTACH) + return action_attach(m, d); + + r = dissected_image_load_verity_sig_partition( + m, + d->fd, + &arg_verity_settings); + if (r < 0) + return log_error_errno(r, "Failed to load verity signature partition: %m"); + + if (arg_action != ACTION_DISSECT) { + r = dissected_image_decrypt_interactively( + m, NULL, + &arg_verity_settings, + arg_flags); + if (r < 0) + return r; + } + } } - r = dissect_loop_device_and_warn( - d, - &arg_verity_settings, - /* mount_options= */ NULL, - arg_image_policy, - arg_flags, - &m); - if (r < 0) - return r; + /* Try via service */ + if (arg_via_service) { + if (arg_in_memory) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--in-memory= not supported when operating via systemd-mountfsd."); - if (arg_action == ACTION_ATTACH) - return action_attach(m, d); + if (arg_loop_ref) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--loop-ref= not supported when operating via systemd-mountfsd."); - r = dissected_image_load_verity_sig_partition( - m, - d->fd, - &arg_verity_settings); - if (r < 0) - return log_error_errno(r, "Failed to load verity signature partition: %m"); + if (verity_settings_set(&arg_verity_settings)) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Externally configured verity settings not supported when operating via systemd-mountfsd."); - if (arg_action != ACTION_DISSECT) { - r = dissected_image_decrypt_interactively( - m, NULL, - &arg_verity_settings, - arg_flags); + /* Don't run things in private userns, if the mount shall be attached to the host */ + if (!IN_SET(arg_action, ACTION_MOUNT, ACTION_WITH)) { + userns_fd = nsresource_allocate_userns(/* name= */ NULL, UINT64_C(0x10000)); /* allocate 64K users by default */ + if (userns_fd < 0) + return log_error_errno(userns_fd, "Failed to allocate user namespace with 64K users: %m"); + } + + r = mountfsd_mount_image( + arg_image, + userns_fd, + arg_image_policy, + arg_flags, + &m); if (r < 0) return r; } @@ -2083,7 +2155,7 @@ static int run(int argc, char *argv[]) { switch (arg_action) { case ACTION_DISSECT: - return action_dissect(m, d); + return action_dissect(m, d, userns_fd); case ACTION_MOUNT: return action_mount(m, d); @@ -2093,7 +2165,7 @@ static int run(int argc, char *argv[]) { case ACTION_COPY_FROM: case ACTION_COPY_TO: case ACTION_MAKE_ARCHIVE: - return action_list_or_mtree_or_copy_or_make_archive(m, d); + return action_list_or_mtree_or_copy_or_make_archive(m, d, userns_fd); case ACTION_WITH: return action_with(m, d); From 046a1487db00ca1a98b8cc3f5bcecb8b1f1a214b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 6 Dec 2023 16:39:39 +0100 Subject: [PATCH 28/31] core: implement RootImage= via mountfsd in unprivileged environments --- src/core/namespace.c | 90 +++++++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 35 deletions(-) diff --git a/src/core/namespace.c b/src/core/namespace.c index 855ad0de89..3ade7913c7 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -2114,6 +2114,7 @@ int setup_namespace(const NamespaceParameters *p, char **error_path) { _cleanup_(dissected_image_unrefp) DissectedImage *dissected_image = NULL; _cleanup_strv_free_ char **hierarchies = NULL; _cleanup_(mount_list_done) MountList ml = {}; + _cleanup_close_ int userns_fd = -EBADF; bool require_prefix = false; const char *root; DissectImageFlags dissect_image_flags = @@ -2150,40 +2151,57 @@ int setup_namespace(const NamespaceParameters *p, char **error_path) { SET_FLAG(dissect_image_flags, DISSECT_IMAGE_NO_PARTITION_TABLE, p->verity && p->verity->data_path); - r = loop_device_make_by_path( - p->root_image, - FLAGS_SET(dissect_image_flags, DISSECT_IMAGE_DEVICE_READ_ONLY) ? O_RDONLY : -1 /* < 0 means writable if possible, read-only as fallback */, - /* sector_size= */ UINT32_MAX, - FLAGS_SET(dissect_image_flags, DISSECT_IMAGE_NO_PARTITION_TABLE) ? 0 : LO_FLAGS_PARTSCAN, - LOCK_SH, - &loop_device); - if (r < 0) - return log_debug_errno(r, "Failed to create loop device for root image: %m"); + if (p->runtime_scope == RUNTIME_SCOPE_SYSTEM) { + /* In system mode we mount directly */ - r = dissect_loop_device( - loop_device, - p->verity, - p->root_image_options, - p->root_image_policy, - dissect_image_flags, - &dissected_image); - if (r < 0) - return log_debug_errno(r, "Failed to dissect image: %m"); + r = loop_device_make_by_path( + p->root_image, + FLAGS_SET(dissect_image_flags, DISSECT_IMAGE_DEVICE_READ_ONLY) ? O_RDONLY : -1 /* < 0 means writable if possible, read-only as fallback */, + /* sector_size= */ UINT32_MAX, + FLAGS_SET(dissect_image_flags, DISSECT_IMAGE_NO_PARTITION_TABLE) ? 0 : LO_FLAGS_PARTSCAN, + LOCK_SH, + &loop_device); + if (r < 0) + return log_debug_errno(r, "Failed to create loop device for root image: %m"); - r = dissected_image_load_verity_sig_partition( - dissected_image, - loop_device->fd, - p->verity); - if (r < 0) - return r; + r = dissect_loop_device( + loop_device, + p->verity, + p->root_image_options, + p->root_image_policy, + dissect_image_flags, + &dissected_image); + if (r < 0) + return log_debug_errno(r, "Failed to dissect image: %m"); - r = dissected_image_decrypt( - dissected_image, - NULL, - p->verity, - dissect_image_flags); - if (r < 0) - return log_debug_errno(r, "Failed to decrypt dissected image: %m"); + r = dissected_image_load_verity_sig_partition( + dissected_image, + loop_device->fd, + p->verity); + if (r < 0) + return r; + + r = dissected_image_decrypt( + dissected_image, + NULL, + p->verity, + dissect_image_flags); + if (r < 0) + return log_debug_errno(r, "Failed to decrypt dissected image: %m"); + } else { + userns_fd = namespace_open_by_type(NAMESPACE_USER); + if (userns_fd < 0) + return log_debug_errno(userns_fd, "Failed to open our own user namespace: %m"); + + r = mountfsd_mount_image( + p->root_image, + userns_fd, + p->root_image_policy, + dissect_image_flags, + &dissected_image); + if (r < 0) + return r; + } } if (p->root_directory) @@ -2547,16 +2565,18 @@ int setup_namespace(const NamespaceParameters *p, char **error_path) { root, /* uid_shift= */ UID_INVALID, /* uid_range= */ UID_INVALID, - /* userns_fd= */ -EBADF, + userns_fd, dissect_image_flags); if (r < 0) return log_debug_errno(r, "Failed to mount root image: %m"); /* Now release the block device lock, so that udevd is free to call BLKRRPART on the device * if it likes. */ - r = loop_device_flock(loop_device, LOCK_UN); - if (r < 0) - return log_debug_errno(r, "Failed to release lock on loopback block device: %m"); + if (loop_device) { + r = loop_device_flock(loop_device, LOCK_UN); + if (r < 0) + return log_debug_errno(r, "Failed to release lock on loopback block device: %m"); + } r = dissected_image_relinquish(dissected_image); if (r < 0) From 0af7e294343b29d769c1bae6a8d390236560ec1a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 12 Dec 2023 11:00:19 +0100 Subject: [PATCH 29/31] nspawn: make nspawn work without privileges --- man/systemd-nspawn.xml | 24 ++ src/nspawn/nspawn-cgroup.c | 87 +++--- src/nspawn/nspawn-cgroup.h | 3 +- src/nspawn/nspawn-mount.c | 70 +++-- src/nspawn/nspawn-mount.h | 1 + src/nspawn/nspawn.c | 577 +++++++++++++++++++++++++------------ 6 files changed, 520 insertions(+), 242 deletions(-) diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml index 4b9c252784..c7359f298a 100644 --- a/man/systemd-nspawn.xml +++ b/man/systemd-nspawn.xml @@ -118,6 +118,28 @@ keeps track of running containers, and provides programming interfaces to interact with them. + + Unprivileged Operation + + systemd-nspawn may be invoked with or without privileges. The full functionality + is currently only available when invoked with privileges. When invoked without privileges, various + limitations apply, including, but not limited to: + + + Only disk image based containers are supported (i.e. ). + Directory based ones (i.e. ) are not supported. + + Machine registration via is not supported. + + Only and networking modes are supported. + + + When running in unprivileged mode, some needed functionality is provided via + systemd-mountfsd.service8 + and + systemd-nsresourced.service8 + + Options @@ -1910,6 +1932,8 @@ After=sys-subsystem-net-devices-ens1.device systemd.slice5 machinectl1 importctl1 + systemd-mountfsd.service8 + systemd-nsresourced.service8 btrfs8 diff --git a/src/nspawn/nspawn-cgroup.c b/src/nspawn/nspawn-cgroup.c index c4a784fd64..4f28b4a225 100644 --- a/src/nspawn/nspawn-cgroup.c +++ b/src/nspawn/nspawn-cgroup.c @@ -13,6 +13,7 @@ #include "mountpoint-util.h" #include "nspawn-cgroup.h" #include "nspawn-mount.h" +#include "nsresource.h" #include "path-util.h" #include "rm-rf.h" #include "string-util.h" @@ -46,38 +47,6 @@ static int chown_cgroup_path(const char *path, uid_t uid_shift) { return 0; } -int chown_cgroup(pid_t pid, CGroupUnified unified_requested, uid_t uid_shift) { - _cleanup_free_ char *path = NULL, *fs = NULL; - int r; - - r = cg_pid_get_path(NULL, pid, &path); - if (r < 0) - return log_error_errno(r, "Failed to get container cgroup path: %m"); - - r = cg_get_path(SYSTEMD_CGROUP_CONTROLLER, path, NULL, &fs); - if (r < 0) - return log_error_errno(r, "Failed to get file system path for container cgroup: %m"); - - r = chown_cgroup_path(fs, uid_shift); - if (r < 0) - return log_error_errno(r, "Failed to chown() cgroup %s: %m", fs); - - if (unified_requested == CGROUP_UNIFIED_SYSTEMD || (unified_requested == CGROUP_UNIFIED_NONE && cg_unified_controller(SYSTEMD_CGROUP_CONTROLLER) > 0)) { - _cleanup_free_ char *lfs = NULL; - /* Always propagate access rights from unified to legacy controller */ - - r = cg_get_path(SYSTEMD_CGROUP_CONTROLLER_LEGACY, path, NULL, &lfs); - if (r < 0) - return log_error_errno(r, "Failed to get file system path for container cgroup: %m"); - - r = chown_cgroup_path(lfs, uid_shift); - if (r < 0) - return log_error_errno(r, "Failed to chown() cgroup %s: %m", lfs); - } - - return 0; -} - int sync_cgroup(pid_t pid, CGroupUnified unified_requested, uid_t uid_shift) { _cleanup_free_ char *cgroup = NULL; char tree[] = "/tmp/unifiedXXXXXX", pid_string[DECIMAL_STR_MAX(pid) + 1]; @@ -142,7 +111,14 @@ finish: return r; } -int create_subcgroup(pid_t pid, bool keep_unit, CGroupUnified unified_requested) { +int create_subcgroup( + pid_t pid, + bool keep_unit, + CGroupUnified unified_requested, + uid_t uid_shift, + int userns_fd, + bool privileged) { + _cleanup_free_ char *cgroup = NULL, *payload = NULL; CGroupMask supported; char *e; @@ -185,13 +161,54 @@ int create_subcgroup(pid_t pid, bool keep_unit, CGroupUnified unified_requested) if (!payload) return log_oom(); - r = cg_create_and_attach(SYSTEMD_CGROUP_CONTROLLER, payload, pid); + if (privileged) + r = cg_create_and_attach(SYSTEMD_CGROUP_CONTROLLER, payload, pid); + else + r = cg_create(SYSTEMD_CGROUP_CONTROLLER, payload); if (r < 0) return log_error_errno(r, "Failed to create %s subcgroup: %m", payload); + if (privileged) { + _cleanup_free_ char *fs = NULL; + r = cg_get_path(SYSTEMD_CGROUP_CONTROLLER, payload, NULL, &fs); + if (r < 0) + return log_error_errno(r, "Failed to get file system path for container cgroup: %m"); + + r = chown_cgroup_path(fs, uid_shift); + if (r < 0) + return log_error_errno(r, "Failed to chown() cgroup %s: %m", fs); + + } else if (userns_fd >= 0) { + _cleanup_close_ int cgroup_fd = -EBADF; + + cgroup_fd = cg_path_open(SYSTEMD_CGROUP_CONTROLLER, payload); + if (cgroup_fd < 0) + return log_error_errno(cgroup_fd, "Failed to open cgroup %s: %m", payload); + + r = cg_fd_attach(cgroup_fd, pid); + if (r < 0) + return log_error_errno(r, "Failed to add process " PID_FMT " to cgroup %s: %m", pid, payload); + + r = nsresource_add_cgroup(userns_fd, cgroup_fd); + if (r < 0) + return log_error_errno(r, "Failed to add cgroup %s to userns: %m", payload); + } + + if (unified_requested == CGROUP_UNIFIED_SYSTEMD || (unified_requested == CGROUP_UNIFIED_NONE && cg_unified_controller(SYSTEMD_CGROUP_CONTROLLER) > 0)) { + _cleanup_free_ char *lfs = NULL; + /* Always propagate access rights from unified to legacy controller */ + + r = cg_get_path(SYSTEMD_CGROUP_CONTROLLER_LEGACY, payload, NULL, &lfs); + if (r < 0) + return log_error_errno(r, "Failed to get file system path for container cgroup: %m"); + + r = chown_cgroup_path(lfs, uid_shift); + if (r < 0) + return log_error_errno(r, "Failed to chown() cgroup %s: %m", lfs); + } + if (keep_unit) { _cleanup_free_ char *supervisor = NULL; - supervisor = path_join(cgroup, "supervisor"); if (!supervisor) return log_oom(); diff --git a/src/nspawn/nspawn-cgroup.h b/src/nspawn/nspawn-cgroup.h index 3f5ba622d8..7e2cd53ddc 100644 --- a/src/nspawn/nspawn-cgroup.h +++ b/src/nspawn/nspawn-cgroup.h @@ -6,9 +6,8 @@ #include "cgroup-util.h" -int chown_cgroup(pid_t pid, CGroupUnified unified_requested, uid_t uid_shift); int sync_cgroup(pid_t pid, CGroupUnified unified_requested, uid_t uid_shift); -int create_subcgroup(pid_t pid, bool keep_unit, CGroupUnified unified_requested); +int create_subcgroup(pid_t pid, bool keep_unit, CGroupUnified unified_requested, uid_t uid_shift, int userns_fd, bool privileged); int mount_cgroups(const char *dest, CGroupUnified unified_requested, bool userns, uid_t uid_shift, uid_t uid_range, const char *selinux_apifs_context, bool use_cgns); int mount_systemd_cgroup_writable(const char *dest, CGroupUnified unified_requested); diff --git a/src/nspawn/nspawn-mount.c b/src/nspawn/nspawn-mount.c index e94ffd799e..c2bd4f6c30 100644 --- a/src/nspawn/nspawn-mount.c +++ b/src/nspawn/nspawn-mount.c @@ -444,22 +444,38 @@ int tmpfs_patch_options( } int mount_sysfs(const char *dest, MountSettingsMask mount_settings) { - const char *full, *top; - int r; + _cleanup_free_ char *top = NULL, *full = NULL;; unsigned long extra_flags = 0; + int r; - top = prefix_roota(dest, "/sys"); - r = path_is_fs_type(top, SYSFS_MAGIC); + top = path_join(dest, "/sys"); + if (!top) + return log_oom(); + + r = path_is_mount_point(top); if (r < 0) - return log_error_errno(r, "Failed to determine filesystem type of %s: %m", top); - /* /sys might already be mounted as sysfs by the outer child in the - * !netns case. In this case, it's all good. Don't touch it because we - * don't have the right to do so, see https://github.com/systemd/systemd/issues/1555. - */ - if (r > 0) - return 0; + return log_error_errno(r, "Failed to determine if '%s' is a mountpoint: %m", top); + if (r == 0) { + /* If this is not a mount point yet, then mount a tmpfs there */ + r = mount_nofollow_verbose(LOG_ERR, "tmpfs", top, "tmpfs", MS_NOSUID|MS_NOEXEC|MS_NODEV, "mode=0555" TMPFS_LIMITS_SYS); + if (r < 0) + return r; + } else { + r = path_is_fs_type(top, SYSFS_MAGIC); + if (r < 0) + return log_error_errno(r, "Failed to determine filesystem type of %s: %m", top); - full = prefix_roota(top, "/full"); + /* /sys/ might already be mounted as sysfs by the outer child in the !netns case. In this case, it's + * all good. Don't touch it because we don't have the right to do so, see + * https://github.com/systemd/systemd/issues/1555. + */ + if (r > 0) + return 0; + } + + full = path_join(top, "/full"); + if (!full) + return log_oom(); (void) mkdir(full, 0755); @@ -501,10 +517,11 @@ int mount_sysfs(const char *dest, MountSettingsMask mount_settings) { if (rmdir(full) < 0) return log_error_errno(errno, "Failed to remove %s: %m", full); - /* Create mountpoint for cgroups. Otherwise we are not allowed since we - * remount /sys read-only. - */ - const char *x = prefix_roota(top, "/fs/cgroup"); + /* Create mountpoint for cgroups. Otherwise we are not allowed since we remount /sys/ read-only. */ + _cleanup_free_ char *x = path_join(top, "/fs/cgroup"); + if (!x) + return log_oom(); + (void) mkdir_p(x, 0755); return mount_nofollow_verbose(LOG_ERR, NULL, top, NULL, @@ -541,7 +558,7 @@ int mount_all(const char *dest, } MountPoint; static const MountPoint mount_table[] = { - /* First we list inner child mounts (i.e. mounts applied *after* entering user namespacing) */ + /* First we list inner child mounts (i.e. mounts applied *after* entering user namespacing when we are privileged) */ { "proc", "/proc", "proc", NULL, PROC_DEFAULT_MOUNT_FLAGS, MOUNT_FATAL|MOUNT_IN_USERNS|MOUNT_MKDIR|MOUNT_FOLLOW_SYMLINKS }, /* we follow symlinks here since not following them requires /proc/ already being mounted, which we don't have here. */ @@ -575,15 +592,15 @@ int mount_all(const char *dest, { "mqueue", "/dev/mqueue", "mqueue", NULL, MS_NOSUID|MS_NOEXEC|MS_NODEV, MOUNT_IN_USERNS|MOUNT_MKDIR }, - /* Then we list outer child mounts (i.e. mounts applied *before* entering user namespacing) */ + /* Then we list outer child mounts (i.e. mounts applied *before* entering user namespacing when we are privileged) */ { "tmpfs", "/tmp", "tmpfs", "mode=01777" NESTED_TMPFS_LIMITS, MS_NOSUID|MS_NODEV|MS_STRICTATIME, MOUNT_FATAL|MOUNT_APPLY_TMPFS_TMP|MOUNT_MKDIR }, { "tmpfs", "/sys", "tmpfs", "mode=0555" TMPFS_LIMITS_SYS, MS_NOSUID|MS_NOEXEC|MS_NODEV, - MOUNT_FATAL|MOUNT_APPLY_APIVFS_NETNS|MOUNT_MKDIR }, + MOUNT_FATAL|MOUNT_APPLY_APIVFS_NETNS|MOUNT_MKDIR|MOUNT_PRIVILEGED }, { "sysfs", "/sys", "sysfs", NULL, SYS_DEFAULT_MOUNT_FLAGS, - MOUNT_FATAL|MOUNT_APPLY_APIVFS_RO|MOUNT_MKDIR }, /* skipped if above was mounted */ + MOUNT_FATAL|MOUNT_APPLY_APIVFS_RO|MOUNT_MKDIR|MOUNT_PRIVILEGED }, /* skipped if above was mounted */ { "sysfs", "/sys", "sysfs", NULL, MS_NOSUID|MS_NOEXEC|MS_NODEV, - MOUNT_FATAL|MOUNT_MKDIR }, /* skipped if above was mounted */ + MOUNT_FATAL|MOUNT_MKDIR|MOUNT_PRIVILEGED }, /* skipped if above was mounted */ { "tmpfs", "/dev", "tmpfs", "mode=0755" TMPFS_LIMITS_PRIVATE_DEV, MS_NOSUID|MS_STRICTATIME, MOUNT_FATAL|MOUNT_MKDIR }, { "tmpfs", "/dev/shm", "tmpfs", "mode=01777" NESTED_TMPFS_LIMITS, MS_NOSUID|MS_NODEV|MS_STRICTATIME, @@ -604,11 +621,11 @@ int mount_all(const char *dest, MOUNT_FATAL|MOUNT_IN_USERNS }, #if HAVE_SELINUX { "/sys/fs/selinux", "/sys/fs/selinux", NULL, NULL, MS_BIND, - MOUNT_MKDIR }, /* Bind mount first (mkdir/chown the mount point in case /sys/ is mounted as minimal skeleton tmpfs) */ + MOUNT_MKDIR|MOUNT_PRIVILEGED }, /* Bind mount first (mkdir/chown the mount point in case /sys/ is mounted as minimal skeleton tmpfs) */ { NULL, "/sys/fs/selinux", NULL, NULL, MS_BIND|MS_RDONLY|MS_NOSUID|MS_NOEXEC|MS_NODEV|MS_REMOUNT, - 0 }, /* Then, make it r/o (don't mkdir/chown the mount point here, the previous entry already did that) */ + MOUNT_PRIVILEGED }, /* Then, make it r/o (don't mkdir/chown the mount point here, the previous entry already did that) */ { NULL, "/sys/fs/selinux", NULL, NULL, MS_PRIVATE, - 0 }, /* Turn off propagation (we only want that for the mount propagation tunnel dir) */ + MOUNT_PRIVILEGED }, /* Turn off propagation (we only want that for the mount propagation tunnel dir) */ #endif }; @@ -617,6 +634,7 @@ int mount_all(const char *dest, bool ro = FLAGS_SET(mount_settings, MOUNT_APPLY_APIVFS_RO); bool in_userns = FLAGS_SET(mount_settings, MOUNT_IN_USERNS); bool tmpfs_tmp = FLAGS_SET(mount_settings, MOUNT_APPLY_TMPFS_TMP); + bool privileged = FLAGS_SET(mount_settings, MOUNT_PRIVILEGED); int r; for (size_t k = 0; k < ELEMENTSOF(mount_table); k++) { @@ -624,6 +642,10 @@ int mount_all(const char *dest, bool fatal = FLAGS_SET(mount_table[k].mount_settings, MOUNT_FATAL); const char *o; + /* If we are not privileged but the entry is marked as privileged and to be mounted outside the user namespace, then skip it */ + if (!privileged && FLAGS_SET(mount_table[k].mount_settings, MOUNT_PRIVILEGED) && !FLAGS_SET(mount_table[k].mount_settings, MOUNT_IN_USERNS)) + continue; + if (in_userns != FLAGS_SET(mount_table[k].mount_settings, MOUNT_IN_USERNS)) continue; diff --git a/src/nspawn/nspawn-mount.h b/src/nspawn/nspawn-mount.h index bf5e47dce4..9112f24b94 100644 --- a/src/nspawn/nspawn-mount.h +++ b/src/nspawn/nspawn-mount.h @@ -20,6 +20,7 @@ typedef enum MountSettingsMask { MOUNT_TOUCH = 1 << 9, /* if set, touch file to mount over first */ MOUNT_PREFIX_ROOT = 1 << 10,/* if set, prefix the source path with the container's root directory */ MOUNT_FOLLOW_SYMLINKS = 1 << 11,/* if set, we'll follow symlinks for the mount target */ + MOUNT_PRIVILEGED = 1 << 12,/* if set, we'll only mount this in in the outer child if we are running in privileged mode */ } MountSettingsMask; typedef enum CustomMountType { diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 3d0a9a73e3..7edb45dd17 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -84,6 +84,7 @@ #include "nspawn-stub-pid1.h" #include "nspawn-util.h" #include "nspawn.h" +#include "nsresource.h" #include "nulstr-util.h" #include "os-util.h" #include "pager.h" @@ -237,6 +238,7 @@ static char *arg_settings_filename = NULL; static Architecture arg_architecture = _ARCHITECTURE_INVALID; static ImagePolicy *arg_image_policy = NULL; static char *arg_background = NULL; +static bool arg_privileged = false; STATIC_DESTRUCTOR_REGISTER(arg_directory, freep); STATIC_DESTRUCTOR_REGISTER(arg_template, freep); @@ -518,6 +520,12 @@ static int detect_unified_cgroup_hierarchy_from_environment(void) { static int detect_unified_cgroup_hierarchy_from_image(const char *directory) { int r; + if (!arg_privileged) { + /* We only support the unified mode when running unprivileged */ + arg_unified_cgroup_hierarchy = CGROUP_UNIFIED_ALL; + return 0; + } + /* Let's inherit the mode to use from the host system, but let's take into consideration what systemd * in the image actually supports. */ r = cg_all_unified(); @@ -619,7 +627,6 @@ static int parse_mount_settings_env(void) { e = getenv("SYSTEMD_NSPAWN_API_VFS_WRITABLE"); if (streq_ptr(e, "network")) arg_mount_settings |= MOUNT_APPLY_APIVFS_RO|MOUNT_APPLY_APIVFS_NETNS; - else if (e) { r = parse_boolean(e); if (r < 0) @@ -1653,6 +1660,21 @@ static int parse_argv(int argc, char *argv[]) { static int verify_arguments(void) { int r; + SET_FLAG(arg_mount_settings, MOUNT_PRIVILEGED, arg_privileged); + + if (!arg_privileged) { + /* machined is not accessible to unpriv clients */ + if (arg_register) { + log_notice("Automatically implying --register=no, since machined is not accessible to unprivileged clients."); + arg_register = false; + } + + if (!arg_private_network) { + log_notice("Automatically implying --private-network, since mounting /sys/ in an unprivileged user namespaces requires network namespacing."); + arg_private_network = true; + } + } + if (arg_start_mode == START_PID2 && arg_unified_cgroup_hierarchy == CGROUP_UNIFIED_UNKNOWN) { /* If we are running the stub init in the container, we don't need to look at what the init * in the container supports, because we are not using it. Let's immediately pick the right @@ -2692,6 +2714,9 @@ static int reset_audit_loginuid(void) { if ((arg_clone_ns_flags & CLONE_NEWPID) == 0) return 0; + if (!arg_privileged) + return 0; + r = read_one_line_file("/proc/self/loginuid", &p); if (r == -ENOENT) return 0; @@ -2721,6 +2746,11 @@ static int mount_tunnel_dig(const char *root) { const char *p, *q; int r; + if (!arg_privileged) { + log_debug("Not digging mount tunnel, because running unprivileged."); + return 0; + } + (void) mkdir_p("/run/systemd/nspawn/", 0755); (void) mkdir_p("/run/systemd/nspawn/propagate", 0600); p = strjoina("/run/systemd/nspawn/propagate/", arg_machine); @@ -2749,6 +2779,11 @@ static int mount_tunnel_dig(const char *root) { static int mount_tunnel_open(void) { int r; + if (!arg_privileged) { + log_debug("Not opening up mount tunnel, because running unprivileged."); + return 0; + } + r = mount_follow_verbose(LOG_ERR, NULL, NSPAWN_MOUNT_TUNNEL, NULL, MS_SLAVE, NULL); if (r < 0) return r; @@ -3270,20 +3305,32 @@ static int inner_child( return r; if (!arg_network_namespace_path && arg_private_network) { - r = unshare(CLONE_NEWNET); + _cleanup_close_ int netns_fd = -EBADF; + + if (arg_privileged) { + if (unshare(CLONE_NEWNET) < 0) + return log_error_errno(errno, "Failed to unshare network namespace: %m"); + } + + netns_fd = namespace_open_by_type(NAMESPACE_NET); + if (netns_fd < 0) + return log_error_errno(netns_fd, "Failed to open newly allocate network namespace: %m"); + + r = send_one_fd(fd_inner_socket, netns_fd, 0); if (r < 0) - return log_error_errno(errno, "Failed to unshare network namespace: %m"); + return log_error_errno(r, "Failed to send network namespace to supervisor: %m"); /* Tell the parent that it can setup network interfaces. */ (void) barrier_place(barrier); /* #3 */ } - r = mount_sysfs(NULL, arg_mount_settings); - if (r < 0) - return r; + if (arg_privileged) { + r = mount_sysfs(NULL, arg_mount_settings); + if (r < 0) + return r; + } - /* Wait until we are cgroup-ified, so that we - * can mount the right cgroup path writable */ + /* Wait until we are cgroup-ified, so that we can mount the right cgroup path writable */ if (!barrier_place_and_sync(barrier)) /* #4 */ return log_error_errno(SYNTHETIC_ERRNO(ESRCH), "Parent died too early"); @@ -3584,11 +3631,11 @@ static int inner_child( return log_error_errno(errno, "execv(%s) failed: %m", exec_target); } -static int setup_notify_child(void) { +static int setup_notify_child(const void *directory) { _cleanup_close_ int fd = -EBADF; - static const union sockaddr_union sa = { + _cleanup_free_ char *j = NULL; + union sockaddr_union sa = { .un.sun_family = AF_UNIX, - .un.sun_path = NSPAWN_NOTIFY_SOCKET_PATH, }; int r; @@ -3596,7 +3643,17 @@ static int setup_notify_child(void) { if (fd < 0) return log_error_errno(errno, "Failed to allocate notification socket: %m"); - (void) mkdir_parents(NSPAWN_NOTIFY_SOCKET_PATH, 0755); + if (directory) { + j = path_join(directory, NSPAWN_NOTIFY_SOCKET_PATH); + if (!j) + return log_oom(); + } + + r = sockaddr_un_set_path(&sa.un, j ?: NSPAWN_NOTIFY_SOCKET_PATH); + if (r < 0) + return log_error_errno(r, "Failed to set AF_UNIX path to %s: %m", j ?: NSPAWN_NOTIFY_SOCKET_PATH); + + (void) mkdir_parents(sa.un.sun_path, 0755); (void) sockaddr_un_unlink(&sa.un); WITH_UMASK(0577) { /* only set "w" bit, which is all that's necessary for connecting from the container */ @@ -3605,7 +3662,7 @@ static int setup_notify_child(void) { return log_error_errno(errno, "bind(" NSPAWN_NOTIFY_SOCKET_PATH ") failed: %m"); } - r = userns_lchown(NSPAWN_NOTIFY_SOCKET_PATH, 0, 0); + r = userns_lchown(sa.un.sun_path, 0, 0); if (r < 0) return log_error_errno(r, "Failed to chown " NSPAWN_NOTIFY_SOCKET_PATH ": %m"); @@ -3621,6 +3678,11 @@ static int setup_unix_export_dir_outside(char **ret) { assert(ret); + if (!arg_privileged) { + log_debug("Not digging socket tunnel, because running unprivileged."); + return 0; + } + _cleanup_free_ char *p = NULL; p = path_join("/run/systemd/nspawn/unix-export", arg_machine); if (!p) @@ -3672,6 +3734,10 @@ static int setup_unix_export_host_inside(const char *directory, const char *unix int r; assert(directory); + + if (!arg_privileged) + return 0; + assert(unix_export_path); r = make_run_host(directory); @@ -3714,10 +3780,16 @@ static int setup_unix_export_host_inside(const char *directory, const char *unix static DissectImageFlags determine_dissect_image_flags(void) { return + DISSECT_IMAGE_GENERIC_ROOT | + DISSECT_IMAGE_REQUIRE_ROOT | + DISSECT_IMAGE_RELAX_VAR_CHECK | DISSECT_IMAGE_USR_NO_ROOT | DISSECT_IMAGE_DISCARD_ON_LOOP | + DISSECT_IMAGE_ADD_PARTITION_DEVICES | + DISSECT_IMAGE_PIN_PARTITION_DEVICES | (arg_read_only ? DISSECT_IMAGE_READ_ONLY : DISSECT_IMAGE_FSCK|DISSECT_IMAGE_GROWFS) | - DISSECT_IMAGE_ALLOW_USERSPACE_VERITY; + DISSECT_IMAGE_ALLOW_USERSPACE_VERITY | + (arg_console_mode == CONSOLE_INTERACTIVE ? DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH : 0); } static int outer_child( @@ -4093,47 +4165,59 @@ static int outer_child( return r; } - /* Mark everything as shared so our mounts get propagated down. This is required to make new bind - * mounts available in systemd services inside the container that create a new mount namespace. See - * https://github.com/systemd/systemd/issues/3860 Further submounts (such as /dev) done after this - * will inherit the shared propagation mode. - * - * IMPORTANT: Do not overmount the root directory anymore from now on to enable moving the root - * directory mount to root later on. - * https://github.com/systemd/systemd/issues/3847#issuecomment-562735251 - */ - r = mount_switch_root(directory, MS_SHARED); - if (r < 0) - return log_error_errno(r, "Failed to move root directory: %m"); + /* We have different codepaths here for privileged and non-privileged mode. In privileged mode we'll + * now switch into the target directory, and then do the final setup from there. If a user namespace + * is then allocated for the container, the root mount and everything else will be out of reach for + * it. For unprivileged containers we cannot do that however, since we couldn't mount a sysfs and + * procfs then anymore, since that only works if there's an unobstructed instance currently + * visible. Hence there we do it the other way round: we first allocate a new set set of namespaces + * (and fork for it) for which we then mount sysfs/procfs, and only then switch root. */ - /* We finished setting up the rootfs which is a shared mount. The mount tunnel needs to be a - * dependent mount otherwise we can't MS_MOVE mounts that were propagated from the host into - * the container. */ - r = mount_tunnel_open(); - if (r < 0) - return r; + if (arg_privileged) { + /* Mark everything as shared so our mounts get propagated down. This is required to make new + * bind mounts available in systemd services inside the container that create a new mount + * namespace. See https://github.com/systemd/systemd/issues/3860 Further submounts (such as + * /dev/) done after this will inherit the shared propagation mode. + * + * IMPORTANT: Do not overmount the root directory anymore from now on to enable moving the root + * directory mount to root later on. + * https://github.com/systemd/systemd/issues/3847#issuecomment-562735251 + */ + r = mount_switch_root(directory, MS_SHARED); + if (r < 0) + return log_error_errno(r, "Failed to move root directory: %m"); - if (arg_userns_mode != USER_NAMESPACE_NO) { - /* In order to mount procfs and sysfs in an unprivileged container the kernel - * requires that a fully visible instance is already present in the target mount - * namespace. Mount one here so the inner child can mount its own instances. Later - * we umount the temporary instances created here before we actually exec the - * payload. Since the rootfs is shared the umount will propagate into the container. - * Note, the inner child wouldn't be able to unmount the instances on its own since - * it doesn't own the originating mount namespace. IOW, the outer child needs to do - * this. */ - r = pin_fully_visible_fs(); + /* We finished setting up the rootfs which is a shared mount. The mount tunnel needs to be a + * dependent mount otherwise we can't MS_MOVE mounts that were propagated from the host into + * the container. */ + r = mount_tunnel_open(); if (r < 0) return r; - } - fd = setup_notify_child(); + if (arg_userns_mode != USER_NAMESPACE_NO) { + /* In order to mount procfs and sysfs in an unprivileged container the kernel + * requires that a fully visible instance is already present in the target mount + * namespace. Mount one here so the inner child can mount its own instances. Later + * we umount the temporary instances created here before we actually exec the + * payload. Since the rootfs is shared the umount will propagate into the container. + * Note, the inner child wouldn't be able to unmount the instances on its own since + * it doesn't own the originating mount namespace. IOW, the outer child needs to do + * this. */ + r = pin_fully_visible_fs(); + if (r < 0) + return r; + } + + fd = setup_notify_child(NULL); + } else + fd = setup_notify_child(directory); if (fd < 0) return fd; pid = raw_clone(SIGCHLD|CLONE_NEWNS| arg_clone_ns_flags | - (arg_userns_mode != USER_NAMESPACE_NO ? CLONE_NEWUSER : 0)); + (arg_userns_mode != USER_NAMESPACE_NO ? CLONE_NEWUSER : 0) | + ((arg_private_network && !arg_privileged) ? CLONE_NEWNET : 0)); if (pid < 0) return log_error_errno(errno, "Failed to fork inner child: %m"); if (pid == 0) { @@ -4152,6 +4236,26 @@ static int outer_child( return log_error_errno(r, "Failed to join network namespace: %m"); } + if (!arg_privileged) { + /* In unprivileged operation, sysfs + procfs are special, we'll have to mount them + * inside the inner namespaces, but before we switch root. Hence do so here. */ + _cleanup_free_ char *j = path_join(directory, "/proc"); + if (!j) + return log_oom(); + + r = mount_follow_verbose(LOG_ERR, "proc", j, "proc", MS_NOSUID|MS_NOEXEC|MS_NODEV, NULL); + if (r < 0) + return r; + + r = mount_sysfs(directory, arg_mount_settings); + if (r < 0) + return r; + + r = mount_switch_root(directory, MS_SHARED); + if (r < 0) + return log_error_errno(r, "Failed to move root directory: %m"); + } + r = inner_child(barrier, fd_inner_socket, fds, os_release_pairs); if (r < 0) _exit(EXIT_FAILURE); @@ -4433,6 +4537,9 @@ static int nspawn_dispatch_notify_fd(sd_event_source *source, int fd, uint32_t r static int setup_notify_parent(sd_event *event, int fd, pid_t *inner_child_pid, sd_event_source **notify_event_source) { int r; + if (fd < 0) + return 0; + r = sd_event_add_io(event, notify_event_source, fd, EPOLLIN, nspawn_dispatch_notify_fd, inner_child_pid); if (r < 0) return log_error_errno(r, "Failed to allocate notify event source: %m"); @@ -4812,26 +4919,28 @@ static int load_settings(void) { return 0; /* We first look in the admin's directories in /etc and /run */ - FOREACH_STRING(i, "/etc/systemd/nspawn", "/run/systemd/nspawn") { - _cleanup_free_ char *j = NULL; + if (arg_privileged) { + FOREACH_STRING(i, "/etc/systemd/nspawn", "/run/systemd/nspawn") { + _cleanup_free_ char *j = NULL; - j = path_join(i, arg_settings_filename); - if (!j) - return log_oom(); + j = path_join(i, arg_settings_filename); + if (!j) + return log_oom(); - f = fopen(j, "re"); - if (f) { - p = TAKE_PTR(j); + f = fopen(j, "re"); + if (f) { + p = TAKE_PTR(j); - /* By default, we trust configuration from /etc and /run */ - if (arg_settings_trusted < 0) - arg_settings_trusted = true; + /* By default, we trust configuration from /etc and /run */ + if (arg_settings_trusted < 0) + arg_settings_trusted = true; - break; + break; + } + + if (errno != ENOENT) + return log_error_errno(errno, "Failed to open %s: %m", j); } - - if (errno != ENOENT) - return log_error_errno(errno, "Failed to open %s: %m", j); } if (!f) { @@ -4891,10 +5000,14 @@ static int load_oci_bundle(void) { static int run_container( DissectedImage *dissected_image, + int userns_fd, FDSet *fds, - char veth_name[IFNAMSIZ], bool *veth_created, + char veth_name[IFNAMSIZ], + bool *veth_created, struct ExposeArgs *expose_args, - int *master, pid_t *pid, int *ret) { + int *master, + pid_t *pid, + int *ret) { static const struct sigaction sa = { .sa_handler = nop_signal_handler, @@ -4979,11 +5092,44 @@ static int run_container( "Path %s doesn't refer to a network namespace, refusing.", arg_network_namespace_path); } - *pid = raw_clone(SIGCHLD|CLONE_NEWNS); - if (*pid < 0) - return log_error_errno(errno, "clone() failed%s: %m", - errno == EINVAL ? - ", do you have namespace support enabled in your kernel? (You need UTS, IPC, PID and NET namespacing built in)" : ""); + if (arg_privileged) { + assert(userns_fd < 0); + + /* If we have no user namespace then we'll clone and create a new mount namepsace right-away. */ + + *pid = raw_clone(SIGCHLD|CLONE_NEWNS); + if (*pid < 0) + return log_error_errno(errno, "clone() failed%s: %m", + errno == EINVAL ? + ", do you have namespace support enabled in your kernel? (You need UTS, IPC, PID and NET namespacing built in)" : ""); + } else { + assert(userns_fd >= 0); + + /* If we have a user namespace then we'll clone() first, and then join the user namespace, + * and then open the mount namespace, so that it is owned by the user namespace */ + + *pid = raw_clone(SIGCHLD); + if (*pid < 0) + return log_error_errno(errno, "clone() failed: %m"); + + if (*pid == 0) { + if (setns(userns_fd, CLONE_NEWUSER) < 0) { + log_error_errno(errno, "Failed to join allocate user namespace: %m"); + _exit(EXIT_FAILURE); + } + + r = reset_uid_gid(); + if (r < 0) { + log_error_errno(r, "Failed to reset UID/GID to root: %m"); + _exit(EXIT_FAILURE); + } + + if (unshare(CLONE_NEWNS) < 0) { + log_error_errno(errno, "Failed to unshare file system namespace: %m"); + _exit(EXIT_FAILURE); + } + } + } if (*pid == 0) { /* The outer child only has a file system namespace. */ @@ -5120,19 +5266,13 @@ static int run_container( /* Wait until the child has unshared its network namespace. */ if (!barrier_place_and_sync(&barrier)) /* #3 */ return log_error_errno(SYNTHETIC_ERRNO(ESRCH), "Child died too early"); - } - if (child_netns_fd < 0) { - /* Make sure we have an open file descriptor to the child's network - * namespace so it stays alive even if the child exits. */ - r = namespace_open(*pid, - /* ret_pidns_fd = */ NULL, - /* ret_mntns_fd = */ NULL, - &child_netns_fd, - /* ret_userns_fd = */ NULL, - /* ret_root_fd = */ NULL); - if (r < 0) - return log_error_errno(r, "Failed to open child network namespace: %m"); + /* Make sure we have an open file descriptor to the child's network namespace so it + * stays alive even if the child exits. */ + assert(child_netns_fd < 0); + child_netns_fd = receive_one_fd(fd_inner_socket_pair[0], 0); + if (child_netns_fd < 0) + return log_error_errno(r, "Failed to receive child network namespace: %m"); } r = move_network_interfaces(child_netns_fd, arg_network_interfaces); @@ -5140,12 +5280,29 @@ static int run_container( return r; if (arg_network_veth) { - r = setup_veth(arg_machine, *pid, veth_name, - arg_network_bridge || arg_network_zone, &arg_network_provided_mac); - if (r < 0) - return r; - else if (r > 0) - ifi = r; + if (arg_privileged) { + r = setup_veth(arg_machine, *pid, veth_name, + arg_network_bridge || arg_network_zone, &arg_network_provided_mac); + if (r < 0) + return r; + else if (r > 0) + ifi = r; + } else { + _cleanup_free_ char *host_ifname = NULL; + + r = nsresource_add_netif(userns_fd, child_netns_fd, /* namespace_ifname= */ NULL, &host_ifname, /* ret_namespace_ifname= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to add network interface to container: %m"); + + ifi = if_nametoindex(host_ifname); + if (ifi == 0) + return log_error_errno(errno, "Failed to resolve interface '%s': %m", host_ifname); + + if (strlen(host_ifname) >= IFNAMSIZ) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Host interface name too long?"); + + strcpy(veth_name, host_ifname); + } if (arg_network_bridge) { /* Add the interface to a bridge */ @@ -5184,9 +5341,12 @@ static int run_container( } if (arg_register || !arg_keep_unit) { - r = sd_bus_default_system(&bus); + if (arg_privileged) + r = sd_bus_default_system(&bus); + else + r = sd_bus_default_user(&bus); if (r < 0) - return log_error_errno(r, "Failed to open system bus: %m"); + return log_error_errno(r, "Failed to open bus: %m"); r = sd_bus_set_close_on_exit(bus, false); if (r < 0) @@ -5247,7 +5407,13 @@ static int run_container( } else if (arg_slice || arg_property) log_notice("Machine and scope registration turned off, --slice= and --property= settings will have no effect."); - r = create_subcgroup(*pid, arg_keep_unit, arg_unified_cgroup_hierarchy); + r = create_subcgroup( + *pid, + arg_keep_unit, + arg_unified_cgroup_hierarchy, + arg_uid_shift, + userns_fd, + arg_privileged); if (r < 0) return r; @@ -5255,14 +5421,8 @@ static int run_container( if (r < 0) return r; - r = chown_cgroup(*pid, arg_unified_cgroup_hierarchy, arg_uid_shift); - if (r < 0) - return r; - - /* Notify the child that the parent is ready with all - * its setup (including cgroup-ification), and that - * the child can now hand over control to the code to - * run inside the container. */ + /* Notify the child that the parent is ready with all its setup (including cgroup-ification), and + * that the child can now hand over control to the code to run inside the container. */ (void) barrier_place(&barrier); /* #4 */ /* Block SIGCHLD here, before notifying child. @@ -5428,7 +5588,7 @@ static int run_container( fd_kmsg_fifo = safe_close(fd_kmsg_fifo); - if (arg_private_network) { + if (arg_private_network && arg_privileged) { r = move_back_network_interfaces(child_netns_fd, arg_network_interfaces); if (r < 0) return r; @@ -5569,6 +5729,10 @@ static int cant_be_in_netns(void) { if (r == -ENOENT || ERRNO_IS_NEG_DISCONNECT(r)) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Sorry, but --image= requires access to the host's /run/ hierarchy, since we need access to udev."); + if (ERRNO_IS_NEG_PRIVILEGE(r)) { + log_debug_errno(r, "Can't connect to udev control socket, assuming we are in same netns."); + return 0; + } if (r < 0) return log_error_errno(r, "Failed to connect socket to udev control socket: %m"); @@ -5587,7 +5751,7 @@ static int cant_be_in_netns(void) { static int run(int argc, char *argv[]) { bool remove_directory = false, remove_image = false, veth_created = false, remove_tmprootdir = false; - _cleanup_close_ int master = -EBADF; + _cleanup_close_ int master = -EBADF, userns_fd = -EBADF; _cleanup_fdset_free_ FDSet *fds = NULL; int r, n_fd_passed, ret = EXIT_SUCCESS; char veth_name[IFNAMSIZ] = ""; @@ -5602,17 +5766,12 @@ static int run(int argc, char *argv[]) { log_parse_environment(); log_open(); + arg_privileged = getuid() == 0; + r = parse_argv(argc, argv); if (r <= 0) goto finish; - if (geteuid() != 0) { - r = log_warning_errno(SYNTHETIC_ERRNO(EPERM), - argc >= 2 ? "Need to be root." : - "Need to be root (and some arguments are usually required).\nHint: try --help"); - goto finish; - } - r = cant_be_in_netns(); if (r < 0) goto finish; @@ -5643,7 +5802,7 @@ static int run(int argc, char *argv[]) { if (!arg_private_network && arg_userns_mode != USER_NAMESPACE_NO && arg_uid_shift > 0) arg_caps_retain &= ~(UINT64_C(1) << CAP_NET_BIND_SERVICE); - r = cg_unified(); + r = cg_unified(); /* initialize cache early */ if (r < 0) { log_error_errno(r, "Failed to determine whether the unified cgroups hierarchy is used: %m"); goto finish; @@ -5664,6 +5823,16 @@ static int run(int argc, char *argv[]) { /* Reapply environment settings. */ (void) detect_unified_cgroup_hierarchy_from_environment(); + if (!arg_privileged) { + r = cg_all_unified(); + if (r < 0) { + log_error_errno(r, "Failed to determine if we are in unified cgroupv2 mode: %m"); + goto finish; + } + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unprivileged operation only supported in unified cgroupv2 mode."); + } + /* Ignore SIGPIPE here, because we use splice() on the ptyfwd stuff and that will generate SIGPIPE if * the result is closed. Note that the container payload child will reset signal mask+handler anyway, * so just turning this off here means we only turn it off in nspawn itself, not any children. */ @@ -5683,9 +5852,21 @@ static int run(int argc, char *argv[]) { * the child. Functions like copy_devnodes() change the umask temporarily. */ umask(0022); + if (arg_console_mode < 0) + arg_console_mode = isatty(STDIN_FILENO) && isatty(STDOUT_FILENO) ? + CONSOLE_INTERACTIVE : CONSOLE_READ_ONLY; + + if (arg_console_mode == CONSOLE_PIPE) /* if we pass STDERR on to the container, don't add our own logs into it too */ + arg_quiet = true; + if (arg_directory) { assert(!arg_image); + if (!arg_privileged) { + r = log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Invoking container from plain directory tree is currently not supported if called without privileges."); + goto finish; + } + /* Safety precaution: let's not allow running images from the live host OS image, as long as * /var from the host will propagate into container dynamically (because bad things happen if * two systems write to the same /var). Let's allow it for the special cases where /var is @@ -5722,7 +5903,11 @@ static int run(int argc, char *argv[]) { /* We take an exclusive lock on this image, since it's our private, ephemeral copy * only owned by us and no one else. */ - r = image_path_lock(np, LOCK_EX|LOCK_NB, &tree_global_lock, &tree_local_lock); + r = image_path_lock( + np, + LOCK_EX|LOCK_NB, + arg_privileged ? &tree_global_lock : NULL, + &tree_local_lock); if (r < 0) { log_error_errno(r, "Failed to lock %s: %m", np); goto finish; @@ -5754,7 +5939,11 @@ static int run(int argc, char *argv[]) { if (r < 0) goto finish; - r = image_path_lock(arg_directory, (arg_read_only ? LOCK_SH : LOCK_EX) | LOCK_NB, &tree_global_lock, &tree_local_lock); + r = image_path_lock( + arg_directory, + (arg_read_only ? LOCK_SH : LOCK_EX) | LOCK_NB, + arg_privileged ? &tree_global_lock : NULL, + &tree_local_lock); if (r == -EBUSY) { log_error_errno(r, "Directory tree %s is currently busy.", arg_directory); goto finish; @@ -5846,15 +6035,12 @@ static int run(int argc, char *argv[]) { } else { DissectImageFlags dissect_image_flags = - DISSECT_IMAGE_GENERIC_ROOT | - DISSECT_IMAGE_REQUIRE_ROOT | - DISSECT_IMAGE_RELAX_VAR_CHECK | - DISSECT_IMAGE_USR_NO_ROOT | - DISSECT_IMAGE_ADD_PARTITION_DEVICES | - DISSECT_IMAGE_PIN_PARTITION_DEVICES; + determine_dissect_image_flags(); + assert(arg_image); assert(!arg_template); + r = chase_and_update(&arg_image, 0); if (r < 0) goto finish; @@ -5869,7 +6055,11 @@ static int run(int argc, char *argv[]) { } /* Always take an exclusive lock on our own ephemeral copy. */ - r = image_path_lock(np, LOCK_EX|LOCK_NB, &tree_global_lock, &tree_local_lock); + r = image_path_lock( + np, + LOCK_EX|LOCK_NB, + arg_privileged ? &tree_global_lock : NULL, + &tree_local_lock); if (r < 0) { log_error_errno(r, "Failed to create image lock: %m"); goto finish; @@ -5894,7 +6084,11 @@ static int run(int argc, char *argv[]) { free_and_replace(arg_image, np); remove_image = true; } else { - r = image_path_lock(arg_image, (arg_read_only ? LOCK_SH : LOCK_EX) | LOCK_NB, &tree_global_lock, &tree_local_lock); + r = image_path_lock( + arg_image, + (arg_read_only ? LOCK_SH : LOCK_EX) | LOCK_NB, + arg_privileged ? &tree_global_lock : NULL, + &tree_local_lock); if (r == -EBUSY) { log_error_errno(r, "Disk image %s is currently busy.", arg_image); goto finish; @@ -5929,57 +6123,81 @@ static int run(int argc, char *argv[]) { goto finish; } - r = loop_device_make_by_path( - arg_image, - arg_read_only ? O_RDONLY : O_RDWR, - /* sector_size= */ UINT32_MAX, - FLAGS_SET(dissect_image_flags, DISSECT_IMAGE_NO_PARTITION_TABLE) ? 0 : LO_FLAGS_PARTSCAN, - LOCK_SH, - &loop); - if (r < 0) { - log_error_errno(r, "Failed to set up loopback block device: %m"); - goto finish; + if (arg_privileged) { + r = loop_device_make_by_path( + arg_image, + arg_read_only ? O_RDONLY : O_RDWR, + /* sector_size= */ UINT32_MAX, + FLAGS_SET(dissect_image_flags, DISSECT_IMAGE_NO_PARTITION_TABLE) ? 0 : LO_FLAGS_PARTSCAN, + LOCK_SH, + &loop); + if (r < 0) { + log_error_errno(r, "Failed to set up loopback block device: %m"); + goto finish; + } + + r = dissect_loop_device_and_warn( + loop, + &arg_verity_settings, + /* mount_options=*/ NULL, + arg_image_policy ?: &image_policy_container, + dissect_image_flags, + &dissected_image); + if (r == -ENOPKG) { + /* dissected_image_and_warn() already printed a brief error message. Extend on that with more details */ + log_notice("Note that the disk image needs to\n" + " a) either contain only a single MBR partition of type 0x83 that is marked bootable\n" + " b) or contain a single GPT partition of type 0FC63DAF-8483-4772-8E79-3D69D8477DE4\n" + " c) or follow https://uapi-group.org/specifications/specs/discoverable_partitions_specification\n" + " d) or contain a file system without a partition table\n" + "in order to be bootable with systemd-nspawn."); + goto finish; + } + if (r < 0) + goto finish; + + r = dissected_image_load_verity_sig_partition( + dissected_image, + loop->fd, + &arg_verity_settings); + if (r < 0) + goto finish; + + if (dissected_image->has_verity && !arg_verity_settings.root_hash && !dissected_image->has_verity_sig) + log_notice("Note: image %s contains verity information, but no root hash specified and no embedded " + "root hash signature found! Proceeding without integrity checking.", arg_image); + + r = dissected_image_decrypt_interactively( + dissected_image, + NULL, + &arg_verity_settings, + dissect_image_flags); + if (r < 0) + goto finish; + } else { + _cleanup_free_ char *userns_name = strjoin("nspawn-", arg_machine); + if (!userns_name) { + r = log_oom(); + goto finish; + } + + /* if we are unprivileged, let's allocate a 64K userns first */ + userns_fd = nsresource_allocate_userns(userns_name, UINT64_C(0x10000)); + if (userns_fd < 0) { + r = log_error_errno(userns_fd, "Failed to allocate user namespace with 64K users: %m"); + goto finish; + } + + r = mountfsd_mount_image( + arg_image, + userns_fd, + arg_image_policy, + dissect_image_flags, + &dissected_image); + if (r < 0) + goto finish; } - r = dissect_loop_device_and_warn( - loop, - &arg_verity_settings, - /* mount_options=*/ NULL, - arg_image_policy ?: &image_policy_container, - dissect_image_flags, - &dissected_image); - if (r == -ENOPKG) { - /* dissected_image_and_warn() already printed a brief error message. Extend on that with more details */ - log_notice("Note that the disk image needs to\n" - " a) either contain only a single MBR partition of type 0x83 that is marked bootable\n" - " b) or contain a single GPT partition of type 0FC63DAF-8483-4772-8E79-3D69D8477DE4\n" - " c) or follow https://uapi-group.org/specifications/specs/discoverable_partitions_specification\n" - " d) or contain a file system without a partition table\n" - "in order to be bootable with systemd-nspawn."); - goto finish; - } - if (r < 0) - goto finish; - - r = dissected_image_load_verity_sig_partition( - dissected_image, - loop->fd, - &arg_verity_settings); - if (r < 0) - goto finish; - - if (dissected_image->has_verity && !arg_verity_settings.root_hash && !dissected_image->has_verity_sig) - log_notice("Note: image %s contains verity information, but no root hash specified and no embedded " - "root hash signature found! Proceeding without integrity checking.", arg_image); - - r = dissected_image_decrypt_interactively( - dissected_image, - NULL, - &arg_verity_settings, - 0); - if (r < 0) - goto finish; - /* Now that we mounted the image, let's try to remove it again, if it is ephemeral */ if (remove_image && unlink(arg_image) >= 0) remove_image = false; @@ -5992,13 +6210,6 @@ static int run(int argc, char *argv[]) { if (r < 0) goto finish; - if (arg_console_mode < 0) - arg_console_mode = isatty(STDIN_FILENO) && isatty(STDOUT_FILENO) ? - CONSOLE_INTERACTIVE : CONSOLE_READ_ONLY; - - if (arg_console_mode == CONSOLE_PIPE) /* if we pass STDERR on to the container, don't add our own logs into it too */ - arg_quiet = true; - if (!arg_quiet) { const char *t = arg_image ?: arg_directory; _cleanup_free_ char *u = NULL; @@ -6029,11 +6240,13 @@ static int run(int argc, char *argv[]) { expose_args.fw_ctx = fw_ctx; } for (;;) { - r = run_container(dissected_image, - fds, - veth_name, &veth_created, - &expose_args, &master, - &pid, &ret); + r = run_container( + dissected_image, + userns_fd, + fds, + veth_name, &veth_created, + &expose_args, &master, + &pid, &ret); if (r <= 0) break; } @@ -6075,7 +6288,7 @@ finish: log_debug_errno(errno, "Can't remove temporary root directory '%s', ignoring: %m", tmprootdir); } - if (arg_machine) { + if (arg_machine && arg_privileged) { const char *p; p = strjoina("/run/systemd/nspawn/propagate/", arg_machine); @@ -6089,9 +6302,11 @@ finish: expose_port_flush(&fw_ctx, arg_expose_ports, AF_INET, &expose_args.address4); expose_port_flush(&fw_ctx, arg_expose_ports, AF_INET6, &expose_args.address6); - if (veth_created) - (void) remove_veth_links(veth_name, arg_network_veth_extra); - (void) remove_bridge(arg_network_zone); + if (arg_privileged) { + if (veth_created) + (void) remove_veth_links(veth_name, arg_network_veth_extra); + (void) remove_bridge(arg_network_zone); + } custom_mount_free_all(arg_custom_mounts, arg_n_custom_mounts); expose_port_free_all(arg_expose_ports); From 625646815b2bc7a8c9992804a9230d92d598adc2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 23 Jan 2024 16:20:10 +0100 Subject: [PATCH 30/31] test: add integration test for unpriv mountfsd/nsresourced --- test/TEST-50-DISSECT/test.sh | 9 ++++++ test/test-functions | 3 ++ test/units/testsuite-50.sh | 59 ++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/test/TEST-50-DISSECT/test.sh b/test/TEST-50-DISSECT/test.sh index 613bb086ce..b93697b73c 100755 --- a/test/TEST-50-DISSECT/test.sh +++ b/test/TEST-50-DISSECT/test.sh @@ -12,6 +12,15 @@ TEST_INSTALL_VERITY_MINIMAL=1 # shellcheck source=test/test-functions . "${TEST_BASE_DIR:?}/test-functions" +# On Ubuntu the BPF LSM is not enabled by default, so we need to do it via the +# kernel command line on boot +if [ "$LOOKS_LIKE_UBUNTU" = "yes" ]; then + KERNEL_OPTIONS=( + "lsm=lockdown,capability,landlock,yama,apparmor,bpf" + ) + KERNEL_APPEND+=" ${KERNEL_OPTIONS[*]}" +fi + test_require_bin mksquashfs veritysetup sfdisk test_append_files() { diff --git a/test/test-functions b/test/test-functions index a09092a5b0..c62cf3d521 100644 --- a/test/test-functions +++ b/test/test-functions @@ -39,6 +39,8 @@ os_release=$(test -e /etc/os-release && echo /etc/os-release || echo /usr/lib/os # shellcheck source=/dev/null source "$os_release" [[ "$ID" == "debian" || " $ID_LIKE " == *" debian "* ]] && LOOKS_LIKE_DEBIAN=yes || LOOKS_LIKE_DEBIAN=no +# shellcheck disable=SC2034 +[[ "$ID" == "ubuntu" ]] && LOOKS_LIKE_UBUNTU=yes || LOOKS_LIKE_UBUNTU=no [[ "$ID" == "arch" || " $ID_LIKE " == *" arch "* ]] && LOOKS_LIKE_ARCH=yes || LOOKS_LIKE_ARCH=no [[ "$ID" == "fedora" ]] && LOOKS_LIKE_FEDORA=yes || LOOKS_LIKE_FEDORA=no [[ " $ID_LIKE " == *" suse "* ]] && LOOKS_LIKE_SUSE=yes || LOOKS_LIKE_SUSE=no @@ -733,6 +735,7 @@ install_verity_minimal() { BASICTOOLS=( bash cat + echo grep mount sleep diff --git a/test/units/testsuite-50.sh b/test/units/testsuite-50.sh index cd2d6aa7f7..9c2e6f354c 100755 --- a/test/units/testsuite-50.sh +++ b/test/units/testsuite-50.sh @@ -6,6 +6,9 @@ set -eux set -o pipefail +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + export SYSTEMD_LOG_LEVEL=debug cleanup_image_dir() { @@ -798,6 +801,62 @@ if command -v mksquashfs >/dev/null 2>&1; then (! test -f /usr/waldo) fi +if test -f /usr/lib/systemd/system/systemd-mountfsd.socket -a -f /usr/lib/systemd/system/systemd-nsresourced.socket && \ + command -v command -v mksquashfs >/dev/null 2>&1 && \ + grep -q bpf /sys/kernel/security/lsm && \ + test "$(find /usr/lib* -name libbpf.so.1 2>/dev/null)" != "" ; then + + cleanunprivfiles() { + umount -R /tmp/unpriv/mount + rmdir /tmp/unpriv + rm -f /tmp/test-50-unpriv-privkey.key /tmp/test-50-unpriv-cert.crt /run/verity.d/test-50-unpriv-cert.crt + rm -f /var/tmp/unpriv.raw /tmp/unpriv.raw.mtree /tmp/unpriv2.raw.mtree + rm -f /tmp/unpriv.out /tmp/unpriv.out2 /tmp/unpriv.out3 + } + + trap cleanunprivfiles EXIT + + systemctl start systemd-mountfsd.socket systemd-nsresourced.socket + + openssl req -config "$OPENSSL_CONFIG" -subj="/CN=waldo" -x509 -sha256 -nodes -days 365 -newkey rsa:4096 -keyout /tmp/test-50-unpriv-privkey.key -out /tmp/test-50-unpriv-cert.crt + + systemd-dissect --mkdir --mount "${image}.raw" /tmp/unpriv/mount + SYSTEMD_REPART_OVERRIDE_FSTYPE=squashfs systemd-repart -P -s /tmp/unpriv/mount --certificate=/tmp/test-50-unpriv-cert.crt --private-key=/tmp/test-50-unpriv-privkey.key /var/tmp/unpriv.raw + systemd-dissect --rmdir --umount /tmp/unpriv/mount + + systemd-dissect --image-policy='root=unprotected:=absent+unused' /var/tmp/unpriv.raw + systemd-dissect --image-policy='root=unprotected:=absent+unused' --mtree /var/tmp/unpriv.raw | tee /tmp/unpriv.raw.mtree + + # Run unpriv, should fail due to lack of privs + (! runas testuser systemd-dissect /var/tmp/unpriv.raw ) + (! runas testuser systemd-dissect --mtree /var/tmp/unpriv.raw ) + + # Install key in keychain + cp /tmp/test-50-unpriv-cert.crt /run/verity.d + + # Now run unpriv again, should be OK now. + runas testuser systemd-dissect /var/tmp/unpriv.raw + runas testuser systemd-dissect --mtree /var/tmp/unpriv.raw | tee /tmp/unpriv2.raw.mtree + + # Check that unpriv and priv run yielded same results + cmp /tmp/unpriv.raw.mtree /tmp/unpriv2.raw.mtree + + # Make sure nspawn works unpriv, too (for now do not nest) + if ! systemd-detect-virt -c ; then + systemd-nspawn --pipe -i /var/tmp/unpriv.raw --read-only echo thisisatest > /tmp/unpriv.out + echo thisisatest | cmp /tmp/unpriv.out - + + # The unpriv user has no rights to lock the image or write to it. Let's + # turn off both for this test, so that we don't have to copy the image + # around. + systemd-run -M testuser@ --user --pipe -p Environment=SYSTEMD_NSPAWN_LOCK=0 -p Delegate=1 -p DelegateSubgroup=supervisor -p Environment=SYSTEMD_LOG_LEVEL=debug --wait systemd-nspawn --keep-unit -i /var/tmp/unpriv.raw --read-only --pipe echo thisisatest > /tmp/unpriv.out2 + echo thisisatest | cmp /tmp/unpriv.out2 - + fi + + systemd-run -M testuser@ --user --pipe -p RootImage=/var/tmp/unpriv.raw -p PrivateUsers=1 --wait echo thisisatest > /tmp/unpriv.out3 + echo thisisatest | cmp /tmp/unpriv.out3 - +fi + # Sneak in a couple of expected-to-fail invocations to cover # https://github.com/systemd/systemd/issues/29610 (! systemd-run -P -p MountImages="/this/should/definitely/not/exist.img:/run/img2\:3:nosuid" false) From 18098d7dec0e488ba78ebde1be840f7a31185ef7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 4 Dec 2023 17:57:06 +0100 Subject: [PATCH 31/31] update TODO --- TODO | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/TODO b/TODO index 29a1a8e7c4..7bff644109 100644 --- a/TODO +++ b/TODO @@ -268,6 +268,18 @@ Features: * use udev rule networkd ownership property to take ownership of network interfaces nspawn creates +* mountfsd/nsresourced + - userdb: maybe allow callers to map one uid to their own uid + - bpflsm: allow writes if resulting UID on disk would be userns' owner UID + - make encrypted DDIs work (password…) + - add API for creating a new file system from scratch (together with some + dm-integrity/HMAC key). Should probably work using systemd-repart (access + via varlink). + - add api to make an existing file "trusted" via dm-integry/HMAC key + - port: portabled + - port: tmpfiles, sysusers and similar + - lets see if we can make runtime bind mounts into unpriv nspawn work + * add a kernel cmdline switch (and cred?) for marking a system to be "headless", in which case we never open /dev/console for reading, only for writing. This would then mean: systemd-firstboot would process creds but not