From 4c701125df79faeefe3e8161c00708c650129d0a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 16 Feb 2026 09:34:37 +0100 Subject: [PATCH 01/36] basic: define Architecture typedef in basic-forward.h --- src/basic/architecture.h | 2 +- src/basic/basic-forward.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/basic/architecture.h b/src/basic/architecture.h index 467e5acbbf..ee91ea0d34 100644 --- a/src/basic/architecture.h +++ b/src/basic/architecture.h @@ -9,7 +9,7 @@ * processor features, models, generations or even ABIs. Hence we * focus on general family, and distinguish word width and endianness. */ -typedef enum { +typedef enum Architecture { ARCHITECTURE_ALPHA, ARCHITECTURE_ARC, ARCHITECTURE_ARC_BE, diff --git a/src/basic/basic-forward.h b/src/basic/basic-forward.h index adfe85eca0..ef616dc7a9 100644 --- a/src/basic/basic-forward.h +++ b/src/basic/basic-forward.h @@ -83,6 +83,7 @@ struct iovec_wrapper; union in_addr_union; union sockaddr_union; +typedef enum Architecture Architecture; typedef enum CGroupFlags CGroupFlags; typedef enum CGroupMask CGroupMask; typedef enum ChaseFlags ChaseFlags; From dbf13060e19e3a8f587ad9908a9bfeec23ba552c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 19 Feb 2026 15:02:48 +0100 Subject: [PATCH 02/36] uid-range: add uid_range_base() that returns the lowest entry --- src/basic/uid-range.c | 15 +++++++++++++++ src/basic/uid-range.h | 2 ++ 2 files changed, 17 insertions(+) diff --git a/src/basic/uid-range.c b/src/basic/uid-range.c index 81e736ef5a..c498b5afe1 100644 --- a/src/basic/uid-range.c +++ b/src/basic/uid-range.c @@ -379,3 +379,18 @@ int uid_map_search_root(pid_t pid, UIDRangeUsernsMode mode, uid_t *ret) { } } } + +uid_t uid_range_base(const UIDRange *range) { + + /* Returns the lowest UID in the range (notw that elements are sorted, hence we just need to look at + * the first one that is populated. */ + + if (uid_range_is_empty(range)) + return UID_INVALID; + + FOREACH_ARRAY(e, range->entries, range->n_entries) + if (e->nr > 0) + return e->start; + + return UID_INVALID; +} diff --git a/src/basic/uid-range.h b/src/basic/uid-range.h index b74ab9d332..c28b02fa7d 100644 --- a/src/basic/uid-range.h +++ b/src/basic/uid-range.h @@ -54,3 +54,5 @@ int uid_range_load_userns_by_fd(int userns_fd, UIDRangeUsernsMode mode, UIDRange bool uid_range_overlaps(const UIDRange *range, uid_t start, uid_t nr); int uid_map_search_root(pid_t pid, UIDRangeUsernsMode mode, uid_t *ret); + +uid_t uid_range_base(const UIDRange *range); From 41b0c68760082db809a203174d3dc3ce366d867e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 5 Nov 2025 16:47:59 +0100 Subject: [PATCH 03/36] pull-job: keep track of content type reported by server --- src/import/pull-job.c | 14 +++++++++++++- src/import/pull-job.h | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/import/pull-job.c b/src/import/pull-job.c index e495b56eab..dca995543c 100644 --- a/src/import/pull-job.c +++ b/src/import/pull-job.c @@ -60,6 +60,7 @@ PullJob* pull_job_unref(PullJob *j) { iovec_done(&j->payload); iovec_done(&j->checksum); iovec_done(&j->expected_checksum); + free(j->content_type); return mfree(j); } @@ -105,6 +106,7 @@ static int pull_job_restart(PullJob *j, const char *new_url) { iovec_done(&j->checksum); iovec_done(&j->expected_checksum); j->expected_content_length = UINT64_MAX; + j->content_type = mfree(j->content_type); curl_glue_remove_and_free(j->glue, j->curl); j->curl = NULL; @@ -546,7 +548,7 @@ fail: } static size_t pull_job_header_callback(void *contents, size_t size, size_t nmemb, void *userdata) { - _cleanup_free_ char *length = NULL, *last_modified = NULL, *etag = NULL; + _cleanup_free_ char *length = NULL, *last_modified = NULL, *etag = NULL, *ct = NULL; size_t sz = size * nmemb; PullJob *j = ASSERT_PTR(userdata); CURLcode code; @@ -630,6 +632,16 @@ static size_t pull_job_header_callback(void *contents, size_t size, size_t nmemb return sz; } + r = curl_header_strdup(contents, sz, "Content-Type:", &ct); + if (r < 0) { + log_oom(); + goto fail; + } + if (r > 0) { + free_and_replace(j->content_type, ct); + return sz; + } + if (j->on_header) { r = j->on_header(j, contents, sz); if (r < 0) diff --git a/src/import/pull-job.h b/src/import/pull-job.h index c4cda83a47..d81f666d7a 100644 --- a/src/import/pull-job.h +++ b/src/import/pull-job.h @@ -67,6 +67,7 @@ typedef struct PullJob { struct stat disk_stat; usec_t mtime; + char *content_type; ImportCompress compress; From 4a886d9ab4999370556dbecda2def1c4dc41da11 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 5 Nov 2025 16:48:46 +0100 Subject: [PATCH 04/36] pull-job: add interface for controlling Accept: header sent to http server --- src/import/pull-job.c | 54 ++++++++++++++++++++++++++++++++----------- src/import/pull-job.h | 3 +++ 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/import/pull-job.c b/src/import/pull-job.c index dca995543c..bd42118042 100644 --- a/src/import/pull-job.c +++ b/src/import/pull-job.c @@ -736,6 +736,27 @@ int pull_job_new( return 0; } +int pull_job_add_request_header(PullJob *j, const char *hdr) { + assert(j); + assert(hdr); + + if (j->request_header) { + struct curl_slist *l; + + l = curl_slist_append(j->request_header, hdr); + if (!l) + return -ENOMEM; + + j->request_header = l; + } else { + j->request_header = curl_slist_new(hdr, NULL); + if (!j->request_header) + return -ENOMEM; + } + + return 0; +} + int pull_job_begin(PullJob *j) { int r; @@ -759,19 +780,9 @@ int pull_job_begin(PullJob *j) { if (!hdr) return -ENOMEM; - if (!j->request_header) { - j->request_header = curl_slist_new(hdr, NULL); - if (!j->request_header) - return -ENOMEM; - } else { - struct curl_slist *l; - - l = curl_slist_append(j->request_header, hdr); - if (!l) - return -ENOMEM; - - j->request_header = l; - } + r = pull_job_add_request_header(j, hdr); + if (r < 0) + return r; } if (j->request_header) { @@ -808,3 +819,20 @@ int pull_job_begin(PullJob *j) { return 0; } + +int pull_job_set_accept(PullJob *j, char * const *l) { + assert(j); + + if (strv_isempty(l)) + return 0; + + _cleanup_free_ char *joined = strv_join(l, ", "); + if (!joined) + return -ENOMEM; + + _cleanup_free_ char *f = strjoin("Accept: ", joined); + if (!f) + return -ENOMEM; + + return pull_job_add_request_header(j, f); +} diff --git a/src/import/pull-job.h b/src/import/pull-job.h index d81f666d7a..41dc567428 100644 --- a/src/import/pull-job.h +++ b/src/import/pull-job.h @@ -94,4 +94,7 @@ void pull_job_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result); void pull_job_close_disk_fd(PullJob *j); +int pull_job_add_request_header(PullJob *j, const char *hdr); +int pull_job_set_accept(PullJob *j, char * const *l); + DEFINE_TRIVIAL_CLEANUP_FUNC(PullJob*, pull_job_unref); From 98b714e0cbece762bda82a19d0c034909bfc6a2b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 6 Nov 2025 09:32:56 +0100 Subject: [PATCH 05/36] pull-job: optionally free userdata when we destroy a PullJob --- src/import/pull-job.c | 3 +++ src/import/pull-job.h | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/import/pull-job.c b/src/import/pull-job.c index bd42118042..ef1da55af0 100644 --- a/src/import/pull-job.c +++ b/src/import/pull-job.c @@ -62,6 +62,9 @@ PullJob* pull_job_unref(PullJob *j) { iovec_done(&j->expected_checksum); free(j->content_type); + if (j->free_userdata) + j->free_userdata(j->userdata); + return mfree(j); } diff --git a/src/import/pull-job.h b/src/import/pull-job.h index 41dc567428..0805184600 100644 --- a/src/import/pull-job.h +++ b/src/import/pull-job.h @@ -36,6 +36,8 @@ typedef struct PullJob { char *url; void *userdata; + free_func_t free_userdata; + PullJobFinished on_finished; PullJobOpenDisk on_open_disk; PullJobHeader on_header; From 1f593d2b5ee872de545716609b9441ec8edd2dd1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 6 Nov 2025 10:46:07 +0100 Subject: [PATCH 06/36] pull-job: add 'description' field to PullJob This is shown in the output in place of the URL if non-NULL. This is useful for OCI's hash-based URLs, which alone are very opaque to read. --- src/import/pull-job.c | 19 +++++++++++++------ src/import/pull-job.h | 2 ++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/import/pull-job.c b/src/import/pull-job.c index ef1da55af0..12bdf4c994 100644 --- a/src/import/pull-job.c +++ b/src/import/pull-job.c @@ -64,10 +64,17 @@ PullJob* pull_job_unref(PullJob *j) { if (j->free_userdata) j->free_userdata(j->userdata); + free(j->description); return mfree(j); } +static const char* pull_job_description(PullJob *j) { + assert(j); + + return j->description ?: j->url; +} + static void pull_job_finish(PullJob *j, int ret) { assert(j); @@ -77,7 +84,7 @@ static void pull_job_finish(PullJob *j, int ret) { if (ret == 0) { j->state = PULL_JOB_DONE; j->progress_percent = 100; - log_info("Download of %s complete.", j->url); + log_info("Download of %s complete.", pull_job_description(j)); } else { j->state = PULL_JOB_FAILED; j->error = ret; @@ -272,7 +279,7 @@ void pull_job_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result) { goto finish; } - log_debug("%s of %s is %s.", EVP_MD_CTX_get0_name(j->checksum_ctx), j->url, h); + log_debug("%s of %s is %s.", EVP_MD_CTX_get0_name(j->checksum_ctx), pull_job_description(j), h); } if (iovec_is_set(&j->expected_checksum) && @@ -336,7 +343,7 @@ void pull_job_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result) { } } - log_info("Acquired %s.", FORMAT_BYTES(j->written_uncompressed)); + log_info("Acquired %s for %s.", FORMAT_BYTES(j->written_uncompressed), pull_job_description(j)); r = 0; @@ -619,7 +626,7 @@ static size_t pull_job_header_callback(void *contents, size_t size, size_t nmemb goto fail; } - log_info("Downloading %s for %s.", FORMAT_BYTES(j->content_length), j->url); + log_info("Downloading %s for %s.", FORMAT_BYTES(j->content_length), pull_job_description(j)); } return sz; @@ -681,11 +688,11 @@ static int pull_job_progress_callback(void *userdata, curl_off_t dltotal, curl_o log_info("Got %u%% of %s. %s left at %s/s.", percent, - j->url, + pull_job_description(j), FORMAT_TIMESPAN(left, USEC_PER_SEC), FORMAT_BYTES((uint64_t) ((double) dlnow / ((double) done / (double) USEC_PER_SEC)))); } else - log_info("Got %u%% of %s.", percent, j->url); + log_info("Got %u%% of %s.", percent, pull_job_description(j)); j->progress_percent = percent; j->last_status_usec = n; diff --git a/src/import/pull-job.h b/src/import/pull-job.h index 0805184600..7dd3a4cff7 100644 --- a/src/import/pull-job.h +++ b/src/import/pull-job.h @@ -38,6 +38,8 @@ typedef struct PullJob { void *userdata; free_func_t free_userdata; + char *description; + PullJobFinished on_finished; PullJobOpenDisk on_open_disk; PullJobHeader on_header; From 93915ec17abc484903af0f424b0a786a87a9674f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 7 Nov 2025 08:31:34 +0100 Subject: [PATCH 07/36] pull-job: add helpers to detect requests for authentication, and accept bearer tokens --- src/import/pull-job.c | 32 ++++++++++++++++++++++++++++++++ src/import/pull-job.h | 3 +++ 2 files changed, 35 insertions(+) diff --git a/src/import/pull-job.c b/src/import/pull-job.c index 12bdf4c994..80197d6c57 100644 --- a/src/import/pull-job.c +++ b/src/import/pull-job.c @@ -30,6 +30,10 @@ static int http_status_etag_exists(CURLcode status) { return status == 304; } +static int http_status_need_authentication(CURLcode status) { + return status == 401; +} + void pull_job_close_disk_fd(PullJob *j) { if (!j) return; @@ -65,6 +69,7 @@ PullJob* pull_job_unref(PullJob *j) { if (j->free_userdata) j->free_userdata(j->userdata); free(j->description); + free(j->authentication_challenge); return mfree(j); } @@ -204,6 +209,10 @@ void pull_job_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result) { j->etag_exists = true; r = 0; goto finish; + } else if (http_status_need_authentication(status)) { + log_info("Access to image requires authentication."); + r = -ENOKEY; + goto finish; } else if (status >= 300) { if (status == 404 && j->on_not_found) { @@ -580,6 +589,19 @@ static size_t pull_job_header_callback(void *contents, size_t size, size_t nmemb goto fail; } + if (http_status_need_authentication(status)) { + _cleanup_free_ char *challenge = NULL; + + r = curl_header_strdup(contents, sz, "WWW-Authenticate:", &challenge); + if (r < 0) { + log_oom(); + goto fail; + } + if (r > 0) + free_and_replace(j->authentication_challenge, challenge); + return sz; + } + if (http_status_ok(status) || http_status_etag_exists(status)) { /* Check Etag on OK and etag exists responses. */ @@ -846,3 +868,13 @@ int pull_job_set_accept(PullJob *j, char * const *l) { return pull_job_add_request_header(j, f); } + +int pull_job_set_bearer_token(PullJob *j, const char *token) { + assert(j); + + _cleanup_free_ char *f = strjoin("Authorization: Bearer ", token); + if (!f) + return -ENOMEM; + + return pull_job_add_request_header(j, f); +} diff --git a/src/import/pull-job.h b/src/import/pull-job.h index 7dd3a4cff7..bfbdb8cced 100644 --- a/src/import/pull-job.h +++ b/src/import/pull-job.h @@ -87,6 +87,8 @@ typedef struct PullJob { bool sync; bool force_memory; + + char *authentication_challenge; } PullJob; int pull_job_new(PullJob **ret, const char *url, CurlGlue *glue, void *userdata); @@ -100,5 +102,6 @@ void pull_job_close_disk_fd(PullJob *j); int pull_job_add_request_header(PullJob *j, const char *hdr); int pull_job_set_accept(PullJob *j, char * const *l); +int pull_job_set_bearer_token(PullJob *j, const char *token); DEFINE_TRIVIAL_CLEANUP_FUNC(PullJob*, pull_job_unref); From d66e3cc79a06cf02c972d76ba0f98cc2facf6ed2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 7 Nov 2025 08:32:39 +0100 Subject: [PATCH 08/36] pull-job: make sure pull_job_restart() can be used to fetch the same resource again, just with new headers Let's flush out all response state from the job, but let's keep the request data previously configured, in particular the headers set. This is useful to re-request a resource, just with a slightly modified or identical URL. --- src/import/pull-job.c | 25 +++++++++++++++---------- src/import/pull-job.h | 2 ++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/import/pull-job.c b/src/import/pull-job.c index 80197d6c57..dcdb1fe8fa 100644 --- a/src/import/pull-job.c +++ b/src/import/pull-job.c @@ -99,15 +99,19 @@ static void pull_job_finish(PullJob *j, int ret) { j->on_finished(j); } -static int pull_job_restart(PullJob *j, const char *new_url) { +int pull_job_restart(PullJob *j, const char *new_url) { int r; assert(j); - assert(new_url); - r = free_and_strdup(&j->url, new_url); - if (r < 0) - return r; + /* If an URL is specified we retry the same request, just towards a different URL. If the URL is NULL + * then we'll fire the same request again (which is useful if some parameters have been changed) */ + + if (new_url) { + r = free_and_strdup(&j->url, new_url); + if (r < 0) + return r; + } j->state = PULL_JOB_INIT; j->error = 0; @@ -119,16 +123,17 @@ static int pull_job_restart(PullJob *j, const char *new_url) { j->etag_exists = false; j->mtime = 0; iovec_done(&j->checksum); - iovec_done(&j->expected_checksum); - j->expected_content_length = UINT64_MAX; j->content_type = mfree(j->content_type); + if (new_url) { + /* Reset expectations if the URL changes */ + iovec_done(&j->expected_checksum); + j->expected_content_length = UINT64_MAX; + } + curl_glue_remove_and_free(j->glue, j->curl); j->curl = NULL; - curl_slist_free_all(j->request_header); - j->request_header = NULL; - import_compress_free(&j->compress); if (j->checksum_ctx) { diff --git a/src/import/pull-job.h b/src/import/pull-job.h index bfbdb8cced..ea58b62f3b 100644 --- a/src/import/pull-job.h +++ b/src/import/pull-job.h @@ -104,4 +104,6 @@ int pull_job_add_request_header(PullJob *j, const char *hdr); int pull_job_set_accept(PullJob *j, char * const *l); int pull_job_set_bearer_token(PullJob *j, const char *token); +int pull_job_restart(PullJob *j, const char *new_url); + DEFINE_TRIVIAL_CLEANUP_FUNC(PullJob*, pull_job_unref); From 6527861039bd57718df8978e57f91cb800dbf1ed Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 7 Nov 2025 08:33:32 +0100 Subject: [PATCH 09/36] tar-util: add support for extracting OCI compatible whiteouts, and turn them into overlayfs whiteouts --- src/shared/tar-util.c | 156 ++++++++++++++++++++++++++++++++++++------ src/shared/tar-util.h | 1 + 2 files changed, 137 insertions(+), 20 deletions(-) diff --git a/src/shared/tar-util.c b/src/shared/tar-util.c index 27f3212fb3..17c03e910b 100644 --- a/src/shared/tar-util.c +++ b/src/shared/tar-util.c @@ -147,8 +147,8 @@ static int open_inode_finalize(OpenInode *of) { /* We adjust the UID/GID right before the mode, since doing this might affect the mode (drops * suid/sgid bits). * - * We adjust the mode only when leaving a dir, because if we are unpriv we might lose the - * ability to enter it once we do this. */ + * We adjust the mode only when leaving a dir, because if we are unprivileged we might lose + * the ability to enter it once we do this. */ if (uid_is_valid(of->uid) || gid_is_valid(of->gid) || of->mode != MODE_INVALID) { k = fchmod_and_chown_with_fallback(of->fd, /* path= */ NULL, of->mode, of->uid, of->gid); @@ -277,6 +277,91 @@ static int archive_unpack_regular( return TAKE_FD(fd); } +static int overlayfs_fsetfattr( + const char *path, /* purely decorative, for log purposes */ + int fd, + const char *name, /* xattr key name */ + const char *value /* xattr value */) { + int r; + + assert(fd >= 0); + assert(path); + assert(name); + assert(value); + + /* overlayfs knows magic {user|trusted}.overlay.* xattrs for whiteouts and opaque directories. The + * 'user.overlay.*' ones are only checked if overlayfs is mounted with "userxattr". We only set that + * one because we want to operate unprivileged. Ideally, we'd set both here, to maximize the chance + * that things work both in privileged and unprivileged scenarios, but unfortunately this has the + * effect that the privileged ones are ignored (and visible in the overlayfs mount). */ + _cleanup_free_ char *n = strjoin("user.overlay.", name); + if (!n) + return log_oom(); + + r = xsetxattr(fd, /* path= */ NULL, AT_EMPTY_PATH, n, value); + if (r < 0) + return log_error_errno(r, "Failed to set '%s' xattr on file '%s': %m", n, path); + + return 0; +} + +static int archive_unpack_whiteout( + struct archive *a, + struct archive_entry *entry, + int parent_fd, + const char *parent_path, /* Full path of 'parent_fd', purely decorative for log purposes */ + const char *filename, /* Just the filename we are supposed to whiteout */ + const char *path /* Full path of the whiteout file, purely decorative for log purposes */) { + + int r; + + assert(a); + assert(entry); + assert(parent_fd >= 0); + assert(parent_path); + assert(filename); + assert(path); + + _cleanup_free_ char *tmp = NULL; + _cleanup_close_ int fd = open_tmpfile_linkable_at(parent_fd, filename, O_CLOEXEC|O_WRONLY, &tmp); + if (fd < 0) + return log_error_errno(fd, "Failed to create whiteout file for '%s': %m", path); + + CLEANUP_TMPFILE_AT(parent_fd, tmp); + + r = overlayfs_fsetfattr(path, fd, "whiteout", "y"); + if (r < 0) + return r; + + /* As per https://docs.kernel.org/filesystems/overlayfs.html also mark the parent */ + r = overlayfs_fsetfattr(parent_path, parent_fd, "opaque", "x"); + if (r < 0) + return r; + + r = link_tmpfile_at(fd, parent_fd, tmp, filename, LINK_TMPFILE_REPLACE); + if (r < 0) + return log_error_errno(r, "Failed to install regular file '%s': %m", path); + + tmp = mfree(tmp); /* disarm CLEANUP_TMPFILE_AT */ + return 0; /* we do not return an fd here, because this kills an inode, and doesn't synthesize one */ +} + +static int archive_unpack_opaque( + struct archive *a, + struct archive_entry *entry, + int parent_fd, + const char *parent_path) { + + assert(a); + assert(entry); + assert(parent_fd >= 0); + assert(parent_path); + + /* we do not return an fd here either */ + + return overlayfs_fsetfattr(parent_path, parent_fd, "opaque", "y"); +} + static int archive_unpack_directory( struct archive *a, struct archive_entry *entry, @@ -908,15 +993,45 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { switch (filetype) { case S_IFREG: + if (FLAGS_SET(flags, TAR_OCI_WHITEOUTS)) { + if (streq(e, ".wh..wh..opq")) { + r = archive_unpack_opaque(a, entry, parent_fd, empty_to_root(parent_path)); + if (r < 0) + return r; + + /* NB: this does not create an inode! */ + break; + } + + const char *w = startswith(e, ".wh."); + if (w) { + if (!filename_is_valid(w)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Invalid whiteout file entry '%s', refusing.", e); + + r = archive_unpack_whiteout(a, entry, parent_fd, empty_to_root(parent_path), w, j); + if (r < 0) + return r; + + /* NB: this does not create an inode! */ + break; + } + } + fd = archive_unpack_regular(a, entry, parent_fd, e, j, fflags); + if (fd < 0) + return fd; break; case S_IFDIR: fd = archive_unpack_directory(a, entry, parent_fd, e, j, fflags); + if (fd < 0) + return fd; break; case S_IFLNK: fd = archive_unpack_symlink(a, entry, parent_fd, e, j); + if (fd < 0) + return fd; break; case S_IFCHR: @@ -924,6 +1039,8 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { case S_IFIFO: case S_IFSOCK: fd = archive_unpack_special_inode(a, entry, parent_fd, e, j, filetype); + if (fd < 0) + return fd; break; default: @@ -931,9 +1048,6 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Unexpected file type %i of '%s', refusing.", (int) filetype, j); } - if (fd < 0) - return fd; - } else { /* This is some intermediary node in the path that we haven't opened yet. Create it with default attributes */ fd = open_mkdir_at(parent_fd, e, O_CLOEXEC, 0700); @@ -949,22 +1063,24 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { * fully done with the inode (i.e. after creating further inodes inside of dir inodes * for example), due to permission problems this might create or that the mtime * changes we do might still be affected by our changes. */ - open_inodes[n_open_inodes++] = (OpenInode) { - .fd = TAKE_FD(fd), - .path = TAKE_PTR(j), - .filetype = filetype, - .mode = mode, - .mtime = mtime, - .uid = uid, - .gid = gid, - .fflags = fflags, - .acl_access = TAKE_PTR(acl_access), - .acl_default = TAKE_PTR(acl_default), - .xattr = TAKE_PTR(xa), - .n_xattr = n_xa, - }; + if (fd >= 0) { + open_inodes[n_open_inodes++] = (OpenInode) { + .fd = TAKE_FD(fd), + .path = TAKE_PTR(j), + .filetype = filetype, + .mode = mode, + .mtime = mtime, + .uid = uid, + .gid = gid, + .fflags = fflags, + .acl_access = TAKE_PTR(acl_access), + .acl_default = TAKE_PTR(acl_default), + .xattr = TAKE_PTR(xa), + .n_xattr = n_xa, + }; - n_xa = 0; + n_xa = 0; + } } } diff --git a/src/shared/tar-util.h b/src/shared/tar-util.h index 10c60be8a8..4ce8c57377 100644 --- a/src/shared/tar-util.h +++ b/src/shared/tar-util.h @@ -4,6 +4,7 @@ typedef enum TarFlags { TAR_SELINUX = 1 << 0, /* Include SELinux xattr in tarball, or unpack it */ TAR_SQUASH_UIDS_ABOVE_64K = 1 << 1, /* Squash UIDs/GIDs above 64K when packing/unpacking to the nobody user */ + TAR_OCI_WHITEOUTS = 1 << 2, /* Turn OCI/aufs whiteout inodes into overlayfs whiteouts */ } TarFlags; int tar_x(int input_fd, int tree_fd, TarFlags flags); From 6d327e9fddbf048958f70d36081dc0fe70e03ee6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 17 Feb 2026 15:46:58 +0100 Subject: [PATCH 10/36] core: use exec_context_with_rootfs() at one more place --- src/core/execute.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/execute.c b/src/core/execute.c index 6d2291cadd..51d6996e72 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -356,7 +356,7 @@ const char* exec_get_private_notify_socket_path(const ExecContext *context, cons if (!needs_sandboxing) return NULL; - if (!context->root_directory && !context->root_image && !context->root_directory_as_fd) + if (!exec_context_with_rootfs(context)) return NULL; if (!exec_context_get_effective_mount_apivfs(context)) From 087f2ec34410061216a91a3a00987a73ec504e44 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 17 Feb 2026 15:46:45 +0100 Subject: [PATCH 11/36] core: introduce exec_context_with_rootfs_strict() as a stricter version of exec_context_with_rootfs() We have two very similar checks in place: in some contexts we want to know if *any* RootDirectory= is configured, in the other we want to suppress if it is configured to our regular root. Let's add a helper for both (even if we only need it once), to make the mirrored behaviour clear. --- src/core/exec-invoke.c | 4 +--- src/core/execute.c | 10 ++++++++++ src/core/execute.h | 1 + 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index a0b0a87a65..298fd0754e 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -4530,10 +4530,8 @@ static bool exec_needs_cap_sys_admin(const ExecContext *context, const ExecParam context->bind_log_sockets > 0 || context->n_bind_mounts > 0 || context->n_temporary_filesystems > 0 || - context->root_directory || - context->root_directory_as_fd || + exec_context_with_rootfs_strict(context) || !strv_isempty(context->extension_directories) || - context->root_image || context->n_mount_images > 0 || context->n_extension_images > 0 || context->protect_system != PROTECT_SYSTEM_NO || diff --git a/src/core/execute.c b/src/core/execute.c index 51d6996e72..8e5796b92a 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -2063,6 +2063,16 @@ bool exec_context_with_rootfs(const ExecContext *c) { return !empty_or_root(c->root_directory) || c->root_image || c->root_directory_as_fd; } +bool exec_context_with_rootfs_strict(const ExecContext *c) { + assert(c); + + /* just like exec_context_with_rootfs(), but doesn't suppress a root directory of "/", i.e. returns + * true in more cases: when a root directory is explicitly configured, even if it's our usual + * root. */ + + return c->root_directory || c->root_image || c->root_directory_as_fd; +} + int exec_context_has_vpicked_extensions(const ExecContext *context) { int r; diff --git a/src/core/execute.h b/src/core/execute.h index 6624c3a71c..6fe0d5e570 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -577,6 +577,7 @@ char** exec_context_get_restrict_filesystems(const ExecContext *c); bool exec_context_restrict_namespaces_set(const ExecContext *c); bool exec_context_restrict_filesystems_set(const ExecContext *c); bool exec_context_with_rootfs(const ExecContext *c); +bool exec_context_with_rootfs_strict(const ExecContext *c); int exec_context_has_vpicked_extensions(const ExecContext *context); From a9f6ba04969d6eb2e629e30299fab7538ef42a57 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 7 Nov 2025 08:35:59 +0100 Subject: [PATCH 12/36] pull: add OCI support --- meson_options.txt | 3 + src/import/import-common.h | 5 +- src/import/meson.build | 39 +- .../registry.docker.io.oci-registry | 4 + .../oci-registry/registry.fedora.oci-registry | 3 + src/import/oci-util.c | 401 +++++ src/import/oci-util.h | 31 + src/import/pull-oci.c | 1585 +++++++++++++++++ src/import/pull-oci.h | 16 + src/import/pull.c | 69 + src/import/test-oci-util.c | 22 + 11 files changed, 2172 insertions(+), 6 deletions(-) create mode 100644 src/import/oci-registry/registry.docker.io.oci-registry create mode 100644 src/import/oci-registry/registry.fedora.oci-registry create mode 100644 src/import/oci-util.c create mode 100644 src/import/oci-util.h create mode 100644 src/import/pull-oci.c create mode 100644 src/import/pull-oci.h create mode 100644 src/import/test-oci-util.c diff --git a/meson_options.txt b/meson_options.txt index 4d13d2866d..c1af7ce237 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -565,3 +565,6 @@ option('vmlinux-h-path', type : 'string', value : '', option('default-mountfsd-trusted-directories', type : 'boolean', value: false, description : 'controls whether mountfsd should apply a relaxed policy on DDIs in system DDI directories') + +option('default-oci-registry', type : 'string', value : 'docker.io', + description : 'Default OCI registry name') diff --git a/src/import/import-common.h b/src/import/import-common.h index b568906a8a..6b10f8c29d 100644 --- a/src/import/import-common.h +++ b/src/import/import-common.h @@ -23,13 +23,14 @@ typedef enum ImportFlags { IMPORT_PULL_ROOTHASH_SIGNATURE = 1 << 11, /* only for raw: download .roothash.p7s file for verity */ IMPORT_PULL_VERITY = 1 << 12, /* only for raw: download .verity file for verity */ - /* The supported flags for the tar and the raw importing */ + /* The supported flags for the tar and raw importing */ IMPORT_FLAGS_MASK_TAR = IMPORT_FORCE|IMPORT_READ_ONLY|IMPORT_BTRFS_SUBVOL|IMPORT_BTRFS_QUOTA|IMPORT_DIRECT|IMPORT_SYNC|IMPORT_FOREIGN_UID, IMPORT_FLAGS_MASK_RAW = IMPORT_FORCE|IMPORT_READ_ONLY|IMPORT_CONVERT_QCOW2|IMPORT_DIRECT|IMPORT_SYNC, - /* The supported flags for the tar and the raw pulling */ + /* The supported flags for the tar, raw, oci pulling */ IMPORT_PULL_FLAGS_MASK_TAR = IMPORT_FLAGS_MASK_TAR|IMPORT_PULL_KEEP_DOWNLOAD|IMPORT_PULL_SETTINGS, IMPORT_PULL_FLAGS_MASK_RAW = IMPORT_FLAGS_MASK_RAW|IMPORT_PULL_KEEP_DOWNLOAD|IMPORT_PULL_SETTINGS|IMPORT_PULL_ROOTHASH|IMPORT_PULL_ROOTHASH_SIGNATURE|IMPORT_PULL_VERITY, + IMPORT_PULL_FLAGS_MASK_OCI = IMPORT_FORCE|IMPORT_READ_ONLY|IMPORT_BTRFS_SUBVOL|IMPORT_BTRFS_QUOTA|IMPORT_SYNC|IMPORT_FOREIGN_UID|IMPORT_PULL_SETTINGS, _IMPORT_FLAGS_INVALID = -EINVAL, } ImportFlags; diff --git a/src/import/meson.build b/src/import/meson.build index 46333fb15f..7735db7ff5 100644 --- a/src/import/meson.build +++ b/src/import/meson.build @@ -20,6 +20,7 @@ executables += [ 'importd.c', ), 'extract' : files( + 'oci-util.c', 'import-common.c', 'import-compress.c', 'qcow2-util.c', @@ -30,12 +31,13 @@ executables += [ 'name' : 'systemd-pull', 'public' : true, 'sources' : files( + 'curl-util.c', 'pull.c', + 'pull-common.c', + 'pull-job.c', + 'pull-oci.c', 'pull-raw.c', 'pull-tar.c', - 'pull-job.c', - 'pull-common.c', - 'curl-util.c', ), 'objects' : ['systemd-importd'], 'dependencies' : common_deps + [ @@ -92,7 +94,11 @@ executables += [ 'dependencies' : common_deps, 'type' : 'manual', }, - + test_template + { + 'sources' : files('test-oci-util.c'), + 'objects': ['systemd-importd'], + 'dependencies' : common_deps, + }, ] install_data('org.freedesktop.import1.conf', @@ -108,3 +114,28 @@ install_data('org.freedesktop.import1.policy', install_data('import-pubring.pgp', install_dir : libexecdir) # TODO: shouldn't this be in pkgdatadir? + +oci_registry_files = [ + 'registry.docker.io.oci-registry', + 'registry.fedora.oci-registry', +] +oci_registry_symlinks = [ + [ 'default.oci-registry', 'registry.' + get_option('default-oci-registry') + '.oci-registry' ], + [ 'image.alpine.oci-registry', 'registry.docker.io.oci-registry' ], + [ 'image.archlinux.oci-registry', 'registry.docker.io.oci-registry' ], + [ 'image.debian.oci-registry', 'registry.docker.io.oci-registry' ], + [ 'image.fedora-minimal.oci-registry', 'registry.fedora.oci-registry' ], + [ 'image.fedora.oci-registry', 'registry.fedora.oci-registry' ], + [ 'image.ubuntu.oci-registry', 'registry.docker.io.oci-registry' ], +] + +foreach file : oci_registry_files + install_data('oci-registry' / file, + install_dir : libexecdir / 'oci-registry') +endforeach + +foreach tuple: oci_registry_symlinks + install_symlink(tuple[0], + pointing_to : tuple[1], + install_dir : libexecdir / 'oci-registry') +endforeach diff --git a/src/import/oci-registry/registry.docker.io.oci-registry b/src/import/oci-registry/registry.docker.io.oci-registry new file mode 100644 index 0000000000..a36b54eb90 --- /dev/null +++ b/src/import/oci-registry/registry.docker.io.oci-registry @@ -0,0 +1,4 @@ +{ + "shortImagePrefix" : "library/", + "overrideRegistry" : "registry-1.docker.io" +} diff --git a/src/import/oci-registry/registry.fedora.oci-registry b/src/import/oci-registry/registry.fedora.oci-registry new file mode 100644 index 0000000000..e416bf2853 --- /dev/null +++ b/src/import/oci-registry/registry.fedora.oci-registry @@ -0,0 +1,3 @@ +{ + "defaultRegistry" : "registry.fedoraproject.org" +} diff --git a/src/import/oci-util.c b/src/import/oci-util.c new file mode 100644 index 0000000000..37a617b28c --- /dev/null +++ b/src/import/oci-util.c @@ -0,0 +1,401 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "sd-json.h" + +#include "alloc-util.h" +#include "architecture.h" +#include "constants.h" +#include "dns-domain.h" +#include "fd-util.h" +#include "fileio.h" +#include "hexdecoct.h" +#include "log.h" +#include "oci-util.h" +#include "parse-util.h" +#include "string-table.h" +#include "string-util.h" + +bool oci_image_is_valid(const char *n) { + bool slash = true; + + /* The OCI spec suggests validating this regex: + * + * [a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)* + * + * We implement a generalization of this, i.e. do not insist on the single ".", "_", "__", "-", "+" + * separator, but allow any number of them. And we refuse leading dots, since if used in the fs this + * would make the files hidden, and we probably don't want that. + */ + + for (const char *p = n; *p; p++) { + if (*p == '/') { + if (slash) + return false; + + slash = true; + continue; + } + + if (!strchr(slash ? LOWERCASE_LETTERS DIGITS "_-+" : + "." LOWERCASE_LETTERS DIGITS "_-+", *p)) + return false; + + slash = false; + } + + return !slash; +} + +int oci_registry_is_valid(const char *n) { + int r; + + if (!n) + return false; + + const char *colon = strchr(n, ':'); + if (!colon) + return dns_name_is_valid(n); + + _cleanup_free_ char *s = strndup(n, colon - n); + if (!s) + return -ENOMEM; + + r = dns_name_is_valid(s); + if (r <= 0) + return r; + + uint16_t port; + return safe_atou16(s, &port) >= 0 && port != 0; +} + +bool oci_tag_is_valid(const char *n) { + if (!n) + return false; + + /* As per https://github.com/opencontainers/distribution-spec/blob/main/spec.md, accept the following regex: + * + * [a-zA-Z0-9_][a-zA-Z0-9._-]{0,127} + */ + + if (!strchr(LETTERS DIGITS "_", n[0])) + return false; + + size_t l = strspn(n + 1, LETTERS DIGITS "._-"); + if (l > 126) + return false; + if (n[1+l] != 0) + return false; + + return true; +} + +int oci_ref_parse( + const char *ref, + char **ret_registry, + char **ret_image, + char **ret_tag) { + + int r; + + assert(ref); + + _cleanup_free_ char *without_tag = NULL, *tag = NULL; + const char *t = strrchr(ref, ':'); + if (t) { + tag = strdup(t + 1); + if (!tag) + return -ENOMEM; + if (!oci_tag_is_valid(tag)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "OCI tag specification '%s' is not valid.", tag); + + without_tag = strndup(ref, t - ref); + if (!without_tag) + return -ENOMEM; + + ref = without_tag; + } + + _cleanup_free_ char *image = NULL, *registry = NULL; + t = strchr(ref, '/'); + if (t) { + registry = strndup(ref, t - ref); + if (!registry) + return -ENOMEM; + + r = oci_registry_is_valid(registry); + if (r < 0) + return r; + if (r == 0) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "OCI registry specification '%s' is not valid.", registry); + + image = strdup(t + 1); + } else + image = strdup(ref); + if (!image) + return -ENOMEM; + if (!oci_image_is_valid(image)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "OCI image specification '%s' is not valid.", registry); + + if (ret_registry) + *ret_registry = TAKE_PTR(registry); + if (ret_image) + *ret_image = TAKE_PTR(image); + if (ret_tag) + *ret_tag = TAKE_PTR(tag); + + return 0; +} + +int oci_ref_normalize(char **protocol, char **registry, char **image, char **tag) { + int r; + + assert(protocol); + assert(registry); + assert(image && *image); + assert(tag); + + /* OCI container reference are supposed to have the form /:. Except that it's + * all super messy, and for some registries the server name differs from the name people use in the + * references, and there are special rules for "short" container names (i.e. those which do not + * contain a "/"), and more. To deal with this, we devise a relatively simple scheme, to normalize + * such names. Specifically: + * + * If a registry is specified we look for + * /usr/lib/systemd/oci-registry/registry..oci-registry for registry-specific rules, to + * enforce on the reference. If no registry is specified, we look for an + * /usr/lib/systemd/oci-registry/image..oci-registry file, which contains image-specific rules + * instead. If this is not found we load /usr/lib/systemd/oci-registry/default.oci-registry + * instead. The files are encoded in JSON. + * + * The rules we apply are relatively simple: + * + * • defaultProtocol controls which protocol to use if none is known. This should always be https + * (since OCI images are authenticated purely via HTTPS), but for testing purposes "file" might be + * useful too. + * + * • overrideRegistry encodes which registry server to actually use, overriding what might have been + * specified. + * + * • overrideImage encodes which image name to actually use, overriding what might have been specified. + * + * • shortImagePrefix encodes a name prefix to prepend to "short" container names. This has no effect + * if overrideImage is set too. + * + * • defaultTag contains a tag to use as default, if none is specified. If not configured this + * defaults to "latest". + */ + + _cleanup_free_ char *fn = NULL; + if (*registry) { + /* If a registry is specified, we'll always respect it, and use it as only search key */ + _cleanup_free_ char *e = urlescape(*registry); + if (!e) + return -ENOMEM; + + fn = strjoin("registry.", e, ".oci-registry"); + } else { + /* If no registry is specified, let's go by image name */ + _cleanup_free_ char *e = urlescape(*image); + if (!e) + return -ENOMEM; + + fn = strjoin("image.", e, ".oci-registry"); + } + if (!fn) + return -ENOMEM; + + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *path = NULL; + r = search_and_fopen_nulstr(fn, "re", /* root= */ NULL, CONF_PATHS_NULSTR("systemd/oci-registry"), &f, &path); + if (r == -ENOENT) + r = search_and_fopen_nulstr("default.oci-registry", "re", /* root= */ NULL, CONF_PATHS_NULSTR("systemd/oci-registry"), &f, &path); + if (r < 0 && r != -ENOENT) + return log_debug_errno(r, "Failed to find suitable OCI registry file: %m"); + + /* if ENOENT is seen, we use the defaults below! */ + + struct { + const char *default_protocol; + const char *override_registry; + const char *default_registry; + const char *override_image; + const char *short_image_prefix; + const char *default_tag; + } data = { + .default_protocol = "https", + .default_tag = "latest", + }; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + if (f) { + unsigned line = 0, column = 0; + r = sd_json_parse_file(f, path, /* flags= */ 0, &v, &line, &column); + if (r < 0) + return log_debug_errno(r, "Parse failure at %s:%u:%u: %m", path, line, column); + + static const sd_json_dispatch_field table[] = { + { "defaultProtocol", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, default_protocol), 0 }, + { "overrideRegistry", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, override_registry), 0 }, + { "defaultRegistry", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, default_registry), 0 }, + { "overrideImage", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, override_image), 0 }, + { "shortImagePrefix", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, short_image_prefix), 0 }, + { "defaultTag", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, default_tag), 0 }, + {}, + }; + r = sd_json_dispatch(v, table, SD_JSON_ALLOW_EXTENSIONS, &data); + if (r < 0) + return r; + } + + _cleanup_free_ char *new_protocol = NULL; + if (data.default_protocol && isempty(*protocol)) { + new_protocol = strdup(data.default_protocol); + if (!new_protocol) + return -ENOMEM; + } + + _cleanup_free_ char *new_registry = NULL; + if (data.override_registry) { + if (!isempty(*registry)) + log_debug("Overriding registry to '%s' (was '%s') based on OCI registry database.", data.override_registry, *registry); + + new_registry = strdup(data.override_registry); + if (!new_registry) + return -ENOMEM; + } else if (data.default_registry && isempty(*registry)) { + new_registry = strdup(data.default_registry); + if (!new_registry) + return -ENOMEM; + } + + _cleanup_free_ char *new_image = NULL; + if (data.override_image) { + log_debug("Overriding image to '%s' (was '%s') based on OCI registry database.", data.override_registry, *image); + + new_image = strdup(data.override_image); + if (!new_image) + return -ENOMEM; + } else if (data.short_image_prefix && !strchr(*image, '/')) { + new_image = strjoin(data.short_image_prefix, *image); + if (!new_image) + return -ENOMEM; + } + + _cleanup_free_ char *new_tag = NULL; + if (data.default_tag && isempty(*tag)) { + new_tag = strdup(data.default_tag); + if (!new_tag) + return -ENOMEM; + } + + if (!new_registry && isempty(*registry)) + return log_debug_errno(SYNTHETIC_ERRNO(ENODATA), "No suitable registry found."); + + if (new_protocol) + free_and_replace(*protocol, new_protocol); + if (new_registry) + free_and_replace(*registry, new_registry); + if (new_image) + free_and_replace(*image, new_image); + if (new_tag) + free_and_replace(*tag, new_tag); + + return 0; +} + +char* oci_digest_string(const struct iovec *iovec) { + assert(iovec); + + _cleanup_free_ char *h = hexmem(iovec->iov_base, iovec->iov_len); + if (!h) + return NULL; + + return strjoin("sha256:", h); +} + +int oci_make_manifest_url( + const char *protocol, + const char *repository, + const char *image, + const char *tag, + char **ret) { + + assert(protocol); + assert(repository); + assert(image); + assert(tag); + assert(ret); + + _cleanup_free_ char *url = strjoin(protocol, "://", repository, "/v2/", image, "/manifests/", tag); + if (!url) + return -ENOMEM; + + *ret = TAKE_PTR(url); + return 0; +} + +int oci_make_blob_url( + const char *protocol, + const char *repository, + const char *image, + const struct iovec *digest, + char **ret) { + + assert(protocol); + assert(repository); + assert(image); + assert(digest); + assert(ret); + + _cleanup_free_ char *d = oci_digest_string(digest); + if (!d) + return -ENOMEM; + + _cleanup_free_ char *url = strjoin(protocol, "://", repository, "/v2/", image, "/blobs/", d); + if (!url) + return -ENOMEM; + + *ret = TAKE_PTR(url); + return 0; +} + +/* OCI uses the Go architecture IDs */ +static const char *const go_arch_table[_ARCHITECTURE_MAX] = { + [ARCHITECTURE_ARM] = "arm", + [ARCHITECTURE_ARM64] = "arm64", + [ARCHITECTURE_MIPS] = "mips", + [ARCHITECTURE_MIPS64] = "mips64", + [ARCHITECTURE_MIPS64_LE] = "mips64le", + [ARCHITECTURE_MIPS_LE] = "mipsle", + [ARCHITECTURE_PPC64] = "ppc64", + [ARCHITECTURE_PPC64_LE] = "ppc64le", + [ARCHITECTURE_S390X] = "s390x", + [ARCHITECTURE_X86] = "386", + [ARCHITECTURE_X86_64] = "amd64", +}; + +DEFINE_STRING_TABLE_LOOKUP_FROM_STRING(go_arch, Architecture); + +char* urlescape(const char *s) { + size_t l = strlen_ptr(s); + + _cleanup_free_ char *t = new(char, l * 3 + 1); + if (!t) + return NULL; + + char *p = t; + for (; s && *s; s++) { + if (strchr(LETTERS DIGITS ".-_", *s)) + *(p++) = *s; + else { + *(p++) = '%'; + *(p++) = hexchar((uint8_t) *s >> 4); + *(p++) = hexchar((uint8_t) *s & 15); + } + } + + *p = 0; + return TAKE_PTR(t); +} diff --git a/src/import/oci-util.h b/src/import/oci-util.h new file mode 100644 index 0000000000..72b6370057 --- /dev/null +++ b/src/import/oci-util.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "basic-forward.h" + +bool oci_image_is_valid(const char *n); +int oci_registry_is_valid(const char *n); +bool oci_tag_is_valid(const char *n); + +int oci_ref_parse(const char *ref, char **ret_registry, char **ret_image, char **ret_tag); + +static inline int oci_ref_valid(const char *ref) { + int r; + r = oci_ref_parse(ref, NULL, NULL, NULL); + if (r == -EINVAL) + return false; + if (r < 0) + return r; + return true; +} + +int oci_ref_normalize(char **protocol, char **registry, char **image, char **tag); + +char* oci_digest_string(const struct iovec *iovec); + +int oci_make_manifest_url(const char *protocol, const char *repository, const char *image, const char *tag, char **ret); +int oci_make_blob_url(const char *protocol, const char *repository, const char *image, const struct iovec *digest, char **ret); + +Architecture go_arch_from_string(const char *s); + +char* urlescape(const char *s); diff --git a/src/import/pull-oci.c b/src/import/pull-oci.c new file mode 100644 index 0000000000..d55bd43f57 --- /dev/null +++ b/src/import/pull-oci.c @@ -0,0 +1,1585 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-event.h" +#include "sd-json.h" +#include "sd-varlink.h" + +#include "alloc-util.h" +#include "architecture.h" +#include "btrfs-util.h" +#include "cleanup-util.h" +#include "curl-util.h" +#include "dissect-image.h" +#include "errno-util.h" +#include "escape.h" +#include "extract-word.h" +#include "fd-util.h" +#include "fs-util.h" +#include "hexdecoct.h" +#include "install-file.h" +#include "io-util.h" +#include "json-util.h" +#include "mkdir-label.h" +#include "oci-util.h" +#include "ordered-set.h" +#include "path-util.h" +#include "pidref.h" +#include "process-util.h" +#include "pull-common.h" +#include "pull-oci.h" +#include "rm-rf.h" +#include "set.h" +#include "signal-util.h" +#include "stat-util.h" +#include "string-util.h" +#include "strv.h" +#include "tmpfile-util.h" + +#define LAYER_MAX 4096U +#define ACTIVE_LAYERS_MAX 5U + +typedef enum OciProgress { + OCI_DOWNLOADING_MANIFEST, + OCI_DOWNLOADING_LAYERS, + OCI_FINALIZING, + OCI_COPYING, +} OciProgress; + +typedef struct OciPull { + sd_event *event; + CurlGlue *glue; + + ImportFlags flags; + char *image_root; + + char *protocol; + char *repository; + char *image; + char *tag; + + PullJob *manifest_job; + PullJob *bearer_token_job; + PullJob *config_job; + OrderedSet *queued_layer_jobs; + Set *active_layer_jobs, *done_layer_jobs; + + OciPullFinished on_finished; + void *userdata; + + bool refuse_index; /* if true, refuse processing an OCI index, because we already processed one */ + + char *bearer_token; + + char *local; + + struct iovec *layer_digests; + size_t n_layers; + + struct iovec config; + int userns_fd; +} OciPull; + +typedef struct OciLayerState { + OciPull *pull; + + PidRef tar_pid; + char *temp_path; + char *final_path; + + int tree_fd; +} OciLayerState; + +static void oci_pull_job_on_finished_layer(PullJob *j); +static void oci_pull_job_on_finished_manifest(PullJob *j); +static void oci_pull_job_on_finished_config(PullJob *j); +static int oci_pull_work(OciPull *i); + +static OciLayerState* oci_layer_state_free(OciLayerState *st) { + if (!st) + return NULL; + + pidref_done_sigkill_wait(&st->tar_pid); + + if (st->temp_path) { + import_remove_tree(st->temp_path, st->pull ? &st->pull->userns_fd : NULL, st->pull->flags); + free(st->temp_path); + } + free(st->final_path); + + safe_close(st->tree_fd); + + return mfree(st); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(OciLayerState*, oci_layer_state_free); + +OciPull* oci_pull_unref(OciPull *i) { + if (!i) + return NULL; + + pull_job_unref(i->manifest_job); + pull_job_unref(i->bearer_token_job); + pull_job_unref(i->config_job); + ordered_set_free(i->queued_layer_jobs); + set_free(i->active_layer_jobs); + set_free(i->done_layer_jobs); + + curl_glue_unref(i->glue); + sd_event_unref(i->event); + + free(i->protocol); + free(i->repository); + free(i->image); + free(i->tag); + + free(i->image_root); + free(i->local); + free(i->bearer_token); + + iovec_done(&i->config); + + iovec_done_many_and_free(i->layer_digests, i->n_layers); + + safe_close(i->userns_fd); + + return mfree(i); +} + +int oci_pull_new( + OciPull **ret, + sd_event *event, + const char *image_root, + OciPullFinished on_finished, + void *userdata) { + + int r; + + assert(image_root); + assert(ret); + + _cleanup_free_ char *root = strdup(image_root); + if (!root) + return -ENOMEM; + + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + if (event) + e = sd_event_ref(event); + else { + r = sd_event_default(&e); + if (r < 0) + return r; + } + + _cleanup_(curl_glue_unrefp) CurlGlue *g = NULL; + r = curl_glue_new(&g, e); + if (r < 0) + return r; + + _cleanup_(oci_pull_unrefp) OciPull *i = NULL; + i = new(OciPull, 1); + if (!i) + return -ENOMEM; + + *i = (OciPull) { + .on_finished = on_finished, + .userdata = userdata, + .image_root = TAKE_PTR(root), + .event = TAKE_PTR(e), + .glue = TAKE_PTR(g), + .userns_fd = -EBADF, + }; + + i->glue->on_finished = pull_job_curl_on_finished; + i->glue->userdata = i; + + *ret = TAKE_PTR(i); + + return 0; +} + +static int pull_job_payload_as_json(PullJob *j, sd_json_variant **ret) { + int r; + + assert(j); + assert(ret); + + /* The PullJob logic implicitly NUL terminates */ + assert(((char*) j->payload.iov_base)[j->payload.iov_len] == 0); + + if (memchr(j->payload.iov_base, 0, j->payload.iov_len)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Embedded NUL byte in JSON data, refusing."); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + unsigned line = 0, column = 0; + r = sd_json_parse((char*) j->payload.iov_base, /* flags= */ 0, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON at position %u:%u: %m", line, column); + + if (DEBUG_LOGGING) + sd_json_variant_dump(v, SD_JSON_FORMAT_COLOR_AUTO|SD_JSON_FORMAT_PRETTY_AUTO, /* f= */ NULL, /* prefix= */ NULL); + + *ret = TAKE_PTR(v); + return 0; +} + +typedef struct OciIndexEntry { + char *media_type; + struct iovec digest; + uint64_t size; + char **url; + char *os; + char *architecture; + char *variant; +} OciIndexEntry; + +static void oci_index_entry_done(OciIndexEntry *entry) { + assert(entry); + + entry->media_type = mfree(entry->media_type); + iovec_done(&entry->digest); + entry->url = strv_free(entry->url); + entry->os = mfree(entry->os); + entry->architecture = mfree(entry->architecture); + entry->variant = mfree(entry->variant); +} + +static bool oci_index_entry_match(OciIndexEntry *entry) { + assert(entry); + + if (entry->os && !streq(entry->os, "linux")) + return false; + + if (entry->architecture && go_arch_from_string(entry->architecture) != native_architecture()) + return false; + + return true; +} + +static int json_dispatch_oci_digest(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + struct iovec *field = ASSERT_PTR(userdata); + int r; + + assert(variant); + + const char *s = NULL; + r = sd_json_dispatch_const_string(name, variant, flags, &s); + if (r < 0) + return r; + + const char *h = startswith(s, "sha256:"); + if (!h) + return json_log(variant, flags, SYNTHETIC_ERRNO(EOPNOTSUPP), "Unsupported hash algorithm used in '%s', refusing.", s); + + _cleanup_(iovec_done) struct iovec d = {}; + r = unhexmem(h, &d.iov_base, &d.iov_len); + if (r < 0) + return json_log(variant, flags, r, "Failed to decode hash '%s', refusing.", s); + if (d.iov_len != SHA256_DIGEST_SIZE) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Hash '%s' has wrong size, refusing.", s); + + iovec_done(field); + *field = TAKE_STRUCT(d); + + return 0; +} + +static int json_dispatch_oci_platform(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + OciIndexEntry *entry = ASSERT_PTR(userdata); + + assert(variant); + + static const struct sd_json_dispatch_field table[] = { + { "os", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(OciIndexEntry, os), 0 }, + { "architecture", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(OciIndexEntry, architecture), 0 }, + { "variant", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(OciIndexEntry, variant), 0 }, + {} + }; + + return sd_json_dispatch(variant, table, flags|SD_JSON_ALLOW_EXTENSIONS, entry); +} + +static int json_dispatch_oci_index_entry(sd_json_variant *v, OciIndexEntry *entry) { + int r; + + assert(v); + assert(entry); + + static const struct sd_json_dispatch_field dispatch_table[] = { + { "mediaType", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(OciIndexEntry, media_type), SD_JSON_MANDATORY }, + { "digest", SD_JSON_VARIANT_STRING, json_dispatch_oci_digest, offsetof(OciIndexEntry, digest), SD_JSON_MANDATORY }, + { "size", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(OciIndexEntry, size), SD_JSON_MANDATORY }, + { "urls", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(OciIndexEntry, url), 0 }, + { "platform", SD_JSON_VARIANT_OBJECT, json_dispatch_oci_platform, 0, 0 }, + {} + }; + + r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG, entry); + if (r < 0) + return r; + + if (!streq_ptr(entry->media_type, "application/vnd.oci.image.manifest.v1+json")) { + json_log(v, SD_JSON_LOG, /* error= */ 0, "Unexpected manifest index entry with media type '%s', skipping.", entry->media_type); + return 0; + } + + return 1; +} + +static int oci_pull_redirect_manifest(OciPull *i, const OciIndexEntry *entry) { + int r; + + assert(i); + assert(entry); + + /* We acquired an index already and found the right manifest to select, hence let's acquire that one + * now */ + + _cleanup_free_ char *p = oci_digest_string(&entry->digest); + if (!p) + return -ENOMEM; + + _cleanup_free_ char *url = NULL; + r = oci_make_manifest_url(i->protocol, i->repository, i->image, p, &url); + if (r < 0) + return r; + + _cleanup_(pull_job_unrefp) PullJob *j = NULL; + r = pull_job_new(&j, url, i->glue, i); + if (r < 0) + return r; + + r = pull_job_set_accept( + j, + STRV_MAKE("application/vnd.oci.image.manifest.v1+json")); + if (r < 0) + return r; + + if (i->bearer_token) { + r = pull_job_set_bearer_token(j, i->bearer_token); + if (r < 0) + return r; + } + + j->on_finished = oci_pull_job_on_finished_manifest; + j->calc_checksum = true; + if (!iovec_memdup(&entry->digest, &j->checksum)) + return -ENOMEM; + + j->description = strjoin("Image Manifest (", url, ")"); + if (!j->description) + return -ENOMEM; + + r = pull_job_begin(j); + if (r < 0) + return r; + + free_and_replace_full(i->manifest_job, j, pull_job_unref); + return 0; +} + +static int oci_pull_process_index(OciPull *i, PullJob *j) { + int r; + + assert(i); + assert(j); + + /* Processes a just downloaded OCI image index, as per: + * + * https://github.com/opencontainers/image-spec/blob/main/image-index.md */ + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + r = pull_job_payload_as_json(j, &v); + if (r < 0) + return r; + + struct { + unsigned schema_version; + const char *media_type; + sd_json_variant *manifests; + } index_data = { + .schema_version = UINT_MAX, + }; + + static const struct sd_json_dispatch_field dispatch_table[] = { + { "schemaVersion", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_uint, voffsetof(index_data, schema_version), SD_JSON_MANDATORY }, + { "mediaType", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(index_data, media_type), 0 }, + { "manifests", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_variant_noref, voffsetof(index_data, manifests), SD_JSON_MANDATORY }, + {} + }; + + r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG, &index_data); + if (r < 0) + return r; + + if (index_data.schema_version != 2) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "OCI image index has unsupported schema version %u, refusing.", index_data.schema_version); + if (index_data.media_type && !streq(index_data.media_type, "application/vnd.oci.image.index.v1+json")) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "OCI image index has unexpected media type '%s', refusing.", index_data.media_type); + + sd_json_variant *m; + JSON_VARIANT_ARRAY_FOREACH(m, index_data.manifests) { + _cleanup_(oci_index_entry_done) OciIndexEntry entry = { + .size = UINT64_MAX, + }; + + r = json_dispatch_oci_index_entry(m, &entry); + if (r < 0) + return r; + if (r == 0) /* skip? */ + continue; + + if (!oci_index_entry_match(&entry)) + continue; + + r = oci_pull_redirect_manifest(i, &entry); + if (r < 0) + return r; + + return 1; /* continue */ + } + + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No suitable OCI image manifest found for local system."); +} + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(pull_job_hash_ops, void, trivial_hash_func, trivial_compare_func, PullJob, pull_job_unref); + +static int oci_pull_job_on_open_disk(PullJob *j) { + int r; + + assert(j); + + OciLayerState *st = ASSERT_PTR(j->userdata); + OciPull *i = st->pull; + + if (!st->temp_path) { + r = tempfn_random_child(i->image_root, "oci", &st->temp_path); + if (r < 0) + return log_oom(); + } + + (void) mkdir_parents_label(st->temp_path, 0700); + + if (FLAGS_SET(i->flags, IMPORT_FOREIGN_UID)) { + r = import_make_foreign_userns(&i->userns_fd); + if (r < 0) + return r; + + _cleanup_close_ int directory_fd = -EBADF; + r = mountfsd_make_directory(st->temp_path, MODE_INVALID, /* flags= */ 0, &directory_fd); + if (r < 0) + return log_error_errno(r, "Failed to make directory via mountfsd: %m"); + + r = mountfsd_mount_directory_fd(directory_fd, i->userns_fd, DISSECT_IMAGE_FOREIGN_UID, &st->tree_fd); + if (r < 0) + return log_error_errno(r, "Failed to mount directory via mountsd: %m"); + } else { + if (i->flags & IMPORT_BTRFS_SUBVOL) + r = btrfs_subvol_make_fallback(AT_FDCWD, st->temp_path, 0755); + else + r = RET_NERRNO(mkdir(st->temp_path, 0755)); + if (r < 0) + return log_error_errno(r, "Failed to create directory/subvolume %s: %m", st->temp_path); + + if (r > 0 && (i->flags & IMPORT_BTRFS_QUOTA)) { /* actually btrfs subvol */ + (void) import_assign_pool_quota_and_warn(i->image_root); + (void) import_assign_pool_quota_and_warn(st->temp_path); + } + + st->tree_fd = open(st->temp_path, O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); + if (st->tree_fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", st->temp_path); + } + + j->disk_fd = import_fork_tar_x(st->tree_fd, i->userns_fd, &st->tar_pid); + if (j->disk_fd < 0) + return j->disk_fd; + + return 0; +} + +static void oci_layer_state_free_wrapper(void *p) { + oci_layer_state_free(p); +} + +typedef struct OciManifestLayer { + char *media_type; + struct iovec digest; + uint64_t size; +} OciManifestLayer; + +static void oci_manifest_layer_done(OciManifestLayer *layer) { + assert(layer); + + layer->media_type = mfree(layer->media_type); + iovec_done(&layer->digest); +} + +static int oci_layer_dirname_for_digest(struct iovec *iovec, char **ret) { + assert(iovec); + assert(ret); + + _cleanup_free_ char *h = oci_digest_string(iovec); + if (!h) + return log_oom(); + + _cleanup_free_ char *fn = strjoin(".oci-", h); + if (!fn) + return log_oom(); + + *ret = TAKE_PTR(fn); + return 0; +} + +static int oci_pull_queue_layer(OciPull *i, OciManifestLayer *layer) { + int r; + + assert(i); + assert(layer); + + _cleanup_free_ char *url = NULL; + r = oci_make_blob_url(i->protocol, i->repository, i->image, &layer->digest, &url); + if (r < 0) + return r; + + _cleanup_free_ char *fn = NULL; + r = oci_layer_dirname_for_digest(&layer->digest, &fn); + if (r < 0) + return r; + + _cleanup_free_ char *final_path = path_join(i->image_root, fn); + if (!final_path) + return log_oom(); + + r = is_dir(final_path, /* follow= */ true); + if (r < 0) { + if (r != -ENOENT) + return log_error_errno(r, "Failed to determine if directory '%s' exists: %m", final_path); + } else { + log_debug("Layer '%s' already exists, skipping download.", final_path); + return 0; + } + + _cleanup_(oci_layer_state_freep) OciLayerState *st = new(OciLayerState, 1); + if (!st) + return -ENOMEM; + + *st = (OciLayerState) { + .pull = i, + .tar_pid = PIDREF_NULL, + .tree_fd = -EBADF, + .final_path = TAKE_PTR(final_path), + }; + + /* Set up */ + _cleanup_(pull_job_unrefp) PullJob *j = NULL; + r = pull_job_new(&j, url, i->glue, st); + if (r < 0) + return r; + + j->free_userdata = oci_layer_state_free_wrapper; + TAKE_PTR(st); + + r = pull_job_set_accept(j, STRV_MAKE(layer->media_type)); + if (r < 0) + return r; + + if (i->bearer_token) { + r = pull_job_set_bearer_token(j, i->bearer_token); + if (r < 0) + return r; + } + + j->on_finished = oci_pull_job_on_finished_layer; + j->on_open_disk = oci_pull_job_on_open_disk; + j->expected_content_length = layer->size; + j->calc_checksum = true; + if (!iovec_memdup(&layer->digest, &j->expected_checksum)) + return -ENOMEM; + + r = ordered_set_ensure_put(&i->queued_layer_jobs, &pull_job_hash_ops, j); + if (r < 0) + return r; + + TAKE_PTR(j); + return 0; +} + +static int json_dispatch_oci_manifest_layer(sd_json_variant *v, OciManifestLayer *layer) { + int r; + + assert(v); + assert(layer); + + static const struct sd_json_dispatch_field dispatch_table[] = { + { "mediaType", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(OciManifestLayer, media_type), SD_JSON_MANDATORY }, + { "digest", SD_JSON_VARIANT_STRING, json_dispatch_oci_digest, offsetof(OciManifestLayer, digest), SD_JSON_MANDATORY }, + { "size", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(OciManifestLayer, size), SD_JSON_MANDATORY }, + {} + }; + + r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG, layer); + if (r < 0) + return r; + + if (!STR_IN_SET(layer->media_type, + "application/vnd.oci.image.layer.v1.tar", + "application/vnd.oci.image.layer.v1.tar+gzip", + "application/vnd.oci.image.layer.v1.tar+zstd")) { + json_log(v, SD_JSON_LOG, /* error= */ 0, "Unexpected manifest layer with media type '%s', skipping.", layer->media_type); + return 0; + } + + return 1; +} + +typedef struct OciManifestConfig { + char *media_type; + struct iovec digest; + uint64_t size; + struct iovec data; +} OciManifestConfig; + +static void oci_manifest_config_done(OciManifestConfig *config) { + assert(config); + + config->media_type = mfree(config->media_type); + iovec_done(&config->digest); + iovec_done(&config->data); +} + +static int oci_pull_fetch_config(OciPull *i, OciManifestConfig *config) { + int r; + + assert(i); + assert(config); + + if (i->config_job) + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Two configs requested, refusing."); + + _cleanup_free_ char *url = NULL; + r = oci_make_blob_url(i->protocol, i->repository, i->image, &config->digest, &url); + if (r < 0) + return r; + + _cleanup_free_ char *h = oci_digest_string(&config->digest); + if (!h) + return log_oom(); + + r = pull_job_new(&i->config_job, url, i->glue, i); + if (r < 0) + return r; + + r = pull_job_set_accept(i->config_job, STRV_MAKE(config->media_type)); + if (r < 0) + return r; + + if (i->bearer_token) { + r = pull_job_set_bearer_token(i->config_job, i->bearer_token); + if (r < 0) + return r; + } + + i->config_job->on_finished = oci_pull_job_on_finished_config; + i->config_job->expected_content_length = config->size; + i->config_job->calc_checksum = true; + if (!iovec_memdup(&config->digest, &i->config_job->expected_checksum)) + return log_oom(); + + i->config_job->description = strjoin("Image configuration (", url, ")"); + + r = pull_job_begin(i->config_job); + if (r < 0) + return r; + + return 0; +} + +static int json_dispatch_oci_manifest_config(sd_json_variant *v, OciManifestConfig *config) { + int r; + + assert(v); + assert(config); + + static const struct sd_json_dispatch_field dispatch_table[] = { + { "mediaType", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(OciManifestConfig, media_type), SD_JSON_MANDATORY }, + { "digest", SD_JSON_VARIANT_STRING, json_dispatch_oci_digest, offsetof(OciManifestConfig, digest), SD_JSON_MANDATORY }, + { "size", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(OciManifestConfig, size), SD_JSON_MANDATORY }, + { "data", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(OciManifestConfig, data), 0 }, + {} + }; + + r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG, config); + if (r < 0) + return r; + + if (!STR_IN_SET(config->media_type, "application/vnd.oci.image.config.v1+json")) { + json_log(v, SD_JSON_LOG, /* error= */ 0, "Unexpected manifest config with media type '%s', skipping.", config->media_type); + return 0; + } + + if (iovec_is_set(&config->data)) { + if (config->data.iov_len != config->size) + return json_log(v, SD_JSON_LOG, SYNTHETIC_ERRNO(EBADMSG), "Manifest config size mismatch."); + + uint8_t h[SHA256_DIGEST_SIZE]; + if (memcmp_nn(sha256_direct(config->data.iov_base, config->data.iov_len, h), SHA256_DIGEST_SIZE, + config->digest.iov_base, config->digest.iov_len) != 0) + return json_log(v, SD_JSON_LOG, SYNTHETIC_ERRNO(EBADMSG), "Manifest data size mismatch."); + } + + return 1; +} + +static int oci_pull_fetch_layers(OciPull *i) { + int r; + + assert(i); + + while (set_size(i->active_layer_jobs) < ACTIVE_LAYERS_MAX) { + _cleanup_(pull_job_unrefp) PullJob *j = ordered_set_steal_first(i->queued_layer_jobs); + if (!j) + return 0; + + r = pull_job_begin(j); + if (r < 0) + return r; + + r = set_ensure_put(&i->active_layer_jobs, &pull_job_hash_ops, j); + if (r < 0) + return r; + + TAKE_PTR(j); + } + + return 0; +} + +static int oci_pull_process_manifest(OciPull *i, PullJob *j) { + int r; + + assert(i); + assert(j); + + /* Processes a just downloaded OCI image manifest, as per: + * + * https://github.com/opencontainers/image-spec/blob/main/manifest.md */ + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + r = pull_job_payload_as_json(j, &v); + if (r < 0) + return r; + + struct { + unsigned schema_version; + const char *media_type; + sd_json_variant *config; + sd_json_variant *layers; + } manifest_data = { + .schema_version = UINT_MAX, + }; + + static const struct sd_json_dispatch_field dispatch_table[] = { + { "schemaVersion", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_uint, voffsetof(manifest_data, schema_version), SD_JSON_MANDATORY }, + { "mediaType", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(manifest_data, media_type), 0 }, + { "config", SD_JSON_VARIANT_OBJECT, sd_json_dispatch_variant_noref, voffsetof(manifest_data, config), 0 }, + { "layers", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_variant_noref, voffsetof(manifest_data, layers), SD_JSON_MANDATORY }, + {} + }; + + r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG, &manifest_data); + if (r < 0) + return r; + + if (manifest_data.schema_version != 2) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "OCI image manifest has unsupported schema version %u, refusing.", manifest_data.schema_version); + if (manifest_data.media_type && !streq(manifest_data.media_type, "application/vnd.oci.image.manifest.v1+json")) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "OCI image manifest has unexpected media type '%s', refusing.", manifest_data.media_type); + + if (manifest_data.config) { + _cleanup_(oci_manifest_config_done) OciManifestConfig config = { + .size = UINT64_MAX, + }; + + r = json_dispatch_oci_manifest_config(manifest_data.config, &config); + if (r < 0) + return r; + + if (iovec_is_set(&config.data)) { + iovec_done(&i->config); + i->config = TAKE_STRUCT(config.data); + } else { + r = oci_pull_fetch_config(i, &config); + if (r < 0) + return r; + } + } + + sd_json_variant *m; + JSON_VARIANT_ARRAY_FOREACH(m, manifest_data.layers) { + _cleanup_(oci_manifest_layer_done) OciManifestLayer layer = { + .size = UINT64_MAX, + }; + + r = json_dispatch_oci_manifest_layer(m, &layer); + if (r < 0) + return r; + if (r == 0) /* skip? */ + continue; + + if (i->n_layers >= LAYER_MAX) + return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Too many layers in manifest (> %zu), refusing.", i->n_layers); + + if (!GREEDY_REALLOC(i->layer_digests, i->n_layers + 1)) + return log_oom(); + + if (!iovec_memdup(&layer.digest, i->layer_digests + i->n_layers)) + return log_oom(); + + i->n_layers++; + + r = oci_pull_queue_layer(i, &layer); + if (r < 0) + return r; + } + + if (i->n_layers == 0) + return log_error_errno(SYNTHETIC_ERRNO(EPROTO), "Manifest has no recognized file system layers, refusing."); + + assert(i->n_layers >= ordered_set_size(i->queued_layer_jobs)); + size_t present_layers = i->n_layers - ordered_set_size(i->queued_layer_jobs); + + log_info("Image %s/%s:%s has %zu layers, %zu already present.", i->repository, i->image, i->tag, i->n_layers, present_layers); + + /* Assign nice descriptions that indicate which layer we are talking of here */ + PullJob *q; + size_t k = 0; + ORDERED_SET_FOREACH(q, i->queued_layer_jobs) { + _cleanup_free_ char *d = NULL; + + if (asprintf(&d, "Layer %zu/%zu (%s)", present_layers + k + 1, i->n_layers, q->url) < 0) + return log_oom(); + + free_and_replace(q->description, d); + k++; + } + + return oci_pull_work(i); +} + +static bool oci_pull_is_done(OciPull *i) { + assert(i); + assert(i->manifest_job); + + if (!PULL_JOB_IS_COMPLETE(i->manifest_job) || + (i->bearer_token_job && !PULL_JOB_IS_COMPLETE(i->bearer_token_job)) || + (i->config_job && !PULL_JOB_IS_COMPLETE(i->config_job))) + return false; + + return ordered_set_isempty(i->queued_layer_jobs) && + set_isempty(i->active_layer_jobs); +} + +typedef struct OciConfiguration { + char *user; + char **env; + char **entrypoint; + char **cmd; + char *working_dir; + int stop_signal; +} OciConfiguration; + +static void oci_configuration_done(OciConfiguration *c) { + assert(c); + + c->user = mfree(c->user); + c->env = strv_free(c->env); + c->entrypoint = strv_free(c->entrypoint); + c->cmd = strv_free(c->cmd); + c->working_dir = mfree(c->working_dir); +} + +static int print_pair_escaped(FILE *f, const char *field, const char *value) { + assert(f); + assert(field); + assert(value); + + _cleanup_free_ char *escaped = cescape(value); + if (!escaped) + return log_oom(); + + fputs(field, f); + fputc('=', f); + fputs(escaped, f); + fputc('\n', f); + + return 0; +} + +static int dispatch_unsupported(const char *name, sd_json_variant *v, sd_json_dispatch_flags_t flags, void *userdata) { + assert(v); + json_log(v, flags|SD_JSON_WARNING, /* error= */ 0, "OCI image specification field '%s' cannot be be translated to any corresponding .nspawn setting, skipping.", name); + return 0; +} + +static int oci_pull_save_nspawn_settings(OciPull *i) { + int r; + + assert(i); + assert(iovec_is_set(&i->config)); + + /* This translates the image spec "configuration" object into native .nspawn files for consumption by + * systemd-nspawn. It's vaguely related to + * https://github.com/opencontainers/image-spec/blob/v1.1.1/conversion.md except of course we don't + * translate to the OCI runtime spec here but rather to .nspawn knobs. */ + + /* The PullJob logic implicitly NUL terminates */ + assert(((char*) i->config.iov_base)[i->config.iov_len] == 0); + + if (memchr(i->config.iov_base, 0, i->config.iov_len)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Embedded NUL by in JSON config data, refusing."); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + unsigned line = 0, column = 0; + r = sd_json_parse((char*) i->config.iov_base, /* flags= */ 0, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON config data at position %u:%u: %m", line, column); + + sd_json_variant *config = sd_json_variant_by_key(v, "config"); + + _cleanup_(oci_configuration_done) OciConfiguration config_data = { + .stop_signal = SIGTERM, + }; + static const struct sd_json_dispatch_field dispatch_table[] = { + { "User", SD_JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(OciConfiguration, user), 0 }, + { "Env", SD_JSON_VARIANT_ARRAY, json_dispatch_strv_environment, offsetof(OciConfiguration, env), 0 }, + { "Entrypoint", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(OciConfiguration, entrypoint), 0 }, + { "Cmd", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(OciConfiguration, cmd), 0 }, + { "WorkingDir", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(OciConfiguration, working_dir), 0 }, + { "StopSignal", SD_JSON_VARIANT_STRING, sd_json_dispatch_signal, offsetof(OciConfiguration, stop_signal), 0 }, + + /* Currently unsupported fields that the OCI image spec defines but we cannot convert into + * .nspawn settings just yet. */ + { "ExposedPorts", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_unsupported, 0, 0 }, + { "Volumes", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_unsupported, 0, 0 }, + { "Labels", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_unsupported, 0, 0 }, + { "ArgsEscaped", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_unsupported, 0, 0 }, + { "Memory", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_unsupported, 0, 0 }, + { "MemorySwap", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_unsupported, 0, 0 }, + { "CpuShares", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_unsupported, 0, 0 }, + { "Healthcheck", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_unsupported, 0, 0 }, + {} + }; + + r = sd_json_dispatch(config, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG, &config_data); + if (r < 0) + return r; + + _cleanup_free_ char *fn = strjoin(i->local, ".nspawn"); + if (!fn) + return log_oom(); + + _cleanup_free_ char *j = path_join(i->image_root, fn); + if (!fn) + return log_oom(); + + _cleanup_fclose_ FILE *f = NULL; + _cleanup_(unlink_and_freep) char *tmpfile = NULL; + r = fopen_tmpfile_linkable(j, O_WRONLY|O_CLOEXEC, &tmpfile, &f); + if (r < 0) + return log_error_errno(r, "Failed to create '%s': %m", j); + + fprintf(f, + "# Generated from OCI configuration object\n" + "[Exec]\n" + "Boot=no\n" + "KillSignal=%s\n", + signal_to_string(config_data.stop_signal)); + + if (config_data.user && !STR_IN_SET(config_data.user, "root", "0")) { + r = print_pair_escaped(f, "User", config_data.user); + if (r < 0) + return r; + } + + if (config_data.working_dir && !path_equal(config_data.working_dir, "/")) { + r = print_pair_escaped(f, "WorkingDirectory", config_data.working_dir); + if (r < 0) + return r; + } + + STRV_FOREACH(e, config_data.env) { + r = print_pair_escaped(f, "Environment", *e); + if (r < 0) + return r; + } + + _cleanup_strv_free_ char **cmdline = strv_copy(config_data.entrypoint); + if (!cmdline) + return log_oom(); + if (strv_extend_strv(&cmdline, config_data.cmd, /* filter_duplicates= */ false) < 0) + return log_oom(); + + if (!strv_isempty(cmdline)) { + _cleanup_free_ char *ej = NULL; + + STRV_FOREACH(z, cmdline) { + _cleanup_free_ char *q = NULL; + const char *a; + + if (isempty(*z)) + a = "\"\""; + else if (!in_charset(*z, ALPHANUMERICAL "=+-_/;.:,;")) { + _cleanup_free_ char *e = shell_escape(*z, "\""); + if (!e) + return log_oom(); + + q = strjoin("\"", e, "\""); + if (!q) + return log_oom(); + + a = q; + } else + a = *z; + + if (!strextend_with_separator(&ej, " ", a)) + return log_oom(); + } + + fprintf(f, "Parameters=%s\n", ej); + } + + r = flink_tmpfile(f, tmpfile, j, LINK_TMPFILE_REPLACE); + if (r < 0) + return log_error_errno(r, "Failed to move '%s' into place: %m", j); + + log_info("systemd-nspawn settings file written to '%s'.", j); + return 0; +} + +static int oci_pull_save_oci_config(OciPull *i) { + int r; + + assert(i); + assert(iovec_is_set(&i->config)); + + _cleanup_free_ char *fn = strjoin(i->local, ".oci-config"); + if (!fn) + return log_oom(); + + _cleanup_free_ char *j = path_join(i->image_root, fn); + if (!fn) + return log_oom(); + + _cleanup_close_ int fd = -EBADF; + _cleanup_(unlink_and_freep) char *tmpfile = NULL; + fd = open_tmpfile_linkable(j, O_WRONLY|O_CLOEXEC, &tmpfile); + if (fd < 0) + return log_error_errno(fd, "Failed to create '%s': %m", j); + + r = loop_write(fd, i->config.iov_base, i->config.iov_len); + if (r < 0) + return log_error_errno(r, "Failed to write '%s': %m", j); + + r = link_tmpfile(fd, tmpfile, j, LINK_TMPFILE_REPLACE); + if (r < 0) + return log_error_errno(r, "Failed to move '%s' into place: %m", j); + + log_info("OCI config written to '%s'.", j); + return 0; +} + +static int oci_pull_save_mstack(OciPull *i) { + int r; + + assert(i); + + _cleanup_free_ char *dn = strjoin(i->local, ".mstack"); + if (!dn) + return log_oom(); + + _cleanup_free_ char *j = path_join(i->image_root, dn); + if (!dn) + return log_oom(); + + log_notice("Creating '%s'", j); + + _cleanup_free_ char *_jt = NULL; + r = tempfn_random(j, "oci", &_jt); + if (r < 0) + return log_oom(); + + _cleanup_close_ int dir_fd = xopenat(AT_FDCWD, _jt, O_DIRECTORY|O_CREAT|O_CLOEXEC); + if (dir_fd < 0) + return log_error_errno(dir_fd, "Failed to create '%s': %m", j); + + _cleanup_(rm_rf_physical_and_freep) char *jt = TAKE_PTR(_jt); + for (size_t k = 0; k < i->n_layers; k++) { + _cleanup_free_ char *sl = NULL; + if (asprintf(&sl, "layer@%zu", k) < 0) + return log_oom(); + + _cleanup_free_ char *ldn = NULL; + if (oci_layer_dirname_for_digest(i->layer_digests + k, &ldn) < 0) + return log_oom(); + + _cleanup_free_ char *with_dot_dot = path_join("..", ldn); + if (!with_dot_dot) + return log_oom(); + + if (symlinkat(with_dot_dot, dir_fd, sl) < 0) + return log_error_errno(errno, "Failed to make symlink for layer '%s': %m", sl); + } + + if (!FLAGS_SET(i->flags, IMPORT_READ_ONLY)) { + /* If this shall be a mutable container let's also create an "rw" layer, that will become + * the upper layer in the overlayfs stack. */ + + if (FLAGS_SET(i->flags, IMPORT_FOREIGN_UID)) { + r = import_make_foreign_userns(&i->userns_fd); + if (r < 0) + return r; + + r = mountfsd_make_directory_fd( + dir_fd, + "rw", + 0755, + /* flags= */ 0, + /* ret_directory_fd= */ NULL); + if (r < 0) + return r; + } else { + _cleanup_close_ int rw_fd = open_mkdir_at(dir_fd, "rw", O_EXCL|O_CLOEXEC, 0755); + if (rw_fd < 0) + return log_error_errno(rw_fd, "Failed to create 'rw' layer: %m"); + } + } + + if (rename(jt, j) < 0) + return log_error_errno(errno, "Failed to move '%s' into place: %m", j); + + jt = mfree(jt); /* Disarm rm_rf_physical_and_free() */ + + log_info("Mount stack written to '%s'.", j); + return 0; +} + +static int oci_pull_make_local(OciPull *i) { + int r; + + assert(i); + assert(oci_pull_is_done(i)); + + r = oci_pull_save_mstack(i); + if (r < 0) + return r; + + if (!iovec_is_set(&i->config) || + iovec_memcmp(&i->config, &IOVEC_MAKE_STRING("{}")) == 0) + log_info("Image has no configuration, not saving."); + else { + r = oci_pull_save_nspawn_settings(i); + if (r < 0) + return r; + + r = oci_pull_save_oci_config(i); + if (r < 0) + return r; + } + + return 0; +} + +static int oci_pull_work(OciPull *i) { + int r; + + assert(i); + + r = oci_pull_fetch_layers(i); + if (r < 0) + return r; + + if (!oci_pull_is_done(i)) + return 1; /* continue */ + + r = oci_pull_make_local(i); + if (r < 0) + return r; + + log_info("Everything done."); + return 0; /* done */ +} + +static void oci_pull_finish(OciPull *i, int r) { + assert(i); + + if (i->on_finished) + i->on_finished(i, r, i->userdata); + else + sd_event_exit(i->event, r); +} + +static void oci_pull_job_on_finished_layer(PullJob *j) { + int r; + + assert(j); + + OciLayerState *st = ASSERT_PTR(j->userdata); + OciPull *i = st->pull; + + if (j->error != 0) { + r = log_error_errno(j->error, "Failed to retrieve layer file: %m"); + goto finish; + } + + pull_job_close_disk_fd(j); + + if (pidref_is_set(&st->tar_pid)) { + r = pidref_wait_for_terminate_and_check("tar", &st->tar_pid, WAIT_LOG); + if (r < 0) + goto finish; + + pidref_done(&st->tar_pid); + if (r != EXIT_SUCCESS) { + r = -EIO; + goto finish; + } + } + + assert(st->temp_path); + assert(st->final_path); + + r = install_file(AT_FDCWD, st->temp_path, + AT_FDCWD, st->final_path, + INSTALL_READ_ONLY|INSTALL_GRACEFUL); + if (r < 0) { + log_error_errno(r, "Failed to rename final image name to %s: %m", st->final_path); + goto finish; + } + + st->temp_path = mfree(st->temp_path); + + r = set_ensure_put(&i->done_layer_jobs, &pull_job_hash_ops, j); + if (r < 0) { + log_oom(); + goto finish; + } + + assert(set_remove(i->active_layer_jobs, j) == j); + + r = oci_pull_work(i); + if (r <= 0) + goto finish; + + return; + +finish: + oci_pull_finish(i, r); +} + +static void oci_pull_job_on_finished_config(PullJob *j) { + int r; + + assert(j); + OciPull *i = ASSERT_PTR(j->userdata); + assert(i->config_job == j); + + if (j->error != 0) { + if (j->error == -ENOMEDIUM) /* HTTP 404 */ + r = log_error_errno(j->error, "Failed to retrieve config object. (Wrong URL?)"); + else + r = log_error_errno(j->error, "Failed to retrieve config object: %m"); + goto finish; + } + + iovec_done(&i->config); + i->config = TAKE_STRUCT(j->payload); + + r = oci_pull_work(i); + if (r <= 0) + goto finish; + + return; + +finish: + oci_pull_finish(i, r); +} + +static void oci_pull_job_on_finished_bearer_token(PullJob *j) { + int r; + + assert(j); + OciPull *i = ASSERT_PTR(j->userdata); + assert(i->bearer_token_job == j); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + + if (j->error != 0) { + if (j->error == -ENOMEDIUM) /* HTTP 404 */ + r = log_error_errno(j->error, "Failed to retrieve bearer token. (Wrong URL?)"); + else + r = log_error_errno(j->error, "Failed to retrieve bearer token: %m"); + goto finish; + } + + r = pull_job_payload_as_json(j, &v); + if (r < 0) + goto finish; + + sd_json_variant *tv = sd_json_variant_by_key(v, "token"); + if (!tv) { + r = log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Bearer token lacks 'token' field."); + goto finish; + } + + if (!sd_json_variant_is_string(tv)) { + r = log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'token' field of bearer token is not a string."); + goto finish; + } + + r = free_and_strdup_warn(&i->bearer_token, sd_json_variant_string(tv)); + if (r < 0) + goto finish; + + assert(i->manifest_job); + r = pull_job_set_bearer_token(i->manifest_job, i->bearer_token); + if (r < 0) { + log_error_errno(r, "Failed to set bearer token on manifest job: %m"); + goto finish; + } + + r = pull_job_restart(i->manifest_job, /* new_url= */ NULL); + if (r < 0) + goto finish; + + return; + +finish: + oci_pull_finish(i, r); +} + +static int make_bearer_token_url(const char *realm, const char *service, const char *scope, char **ret) { + assert(realm); + assert(service); + assert(scope); + assert(ret); + + _cleanup_free_ char *rs = urlescape(service); + if (!rs) + return -ENOMEM; + + _cleanup_free_ char *ss = urlescape(scope); + if (!ss) + return -ENOMEM; + + _cleanup_free_ char *url = strjoin(realm, "?service=", rs, "&scope=", ss); + if (!url) + return -ENOMEM; + + *ret = TAKE_PTR(url); + return 0; +} + +static int oci_pull_process_authentication_challenge(OciPull *i, const char *challenge) { + int r; + + assert(i); + assert(challenge); + + /* We only know what to do with the bearer token challenge */ + const char *e = startswith_no_case(challenge, "bearer "); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Authentication mechanism not recognized, cannot authenticate."); + + if (i->bearer_token_job) + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Two bearer token challenges requested, refusing."); + + e += strspn(e, WHITESPACE); + + _cleanup_strv_free_ char **l = NULL; + r = strv_split_full(&l, e, ",", EXTRACT_KEEP_QUOTE); + if (r < 0) + return log_error_errno(r, "Failed to split bearer token paramaters: %m"); + + _cleanup_free_ char *realm = NULL, *scope = NULL, *service = NULL; + struct { + const char *name; + char **value; + } fields[] = { + { "realm=\"", &realm }, + { "scope=\"", &scope }, + { "service=\"", &service }, + }; + + STRV_FOREACH(k, l) { + bool found = false; + + FOREACH_ELEMENT(f, fields) { + const char *v = startswith_no_case(*k, f->name); + if (!v) + continue; + + const char *q = endswith(v,"\""); + if (!q) { + log_warning("Field isn't quoted properly: %s", *k); + continue; + } + + _cleanup_free_ char *s = strndup(v, q - v); + if (!s) + return log_oom(); + + free_and_replace(*f->value, s); + found = true; + break; + } + + if (!found) + log_debug("Ignoring bearer token challenge field: %s", *k); + } + + if (!realm || !service || !scope) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Incomplete bearer token fields"); + + if (!startswith_no_case(realm, "https://")) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Authentication realm is not an URL, don't know how to proceed."); + + _cleanup_free_ char *url = NULL; + r = make_bearer_token_url(realm, service, scope, &url); + if (r < 0) + return log_error_errno(r, "Failed to make bearer token URL: %m"); + + r = pull_job_new(&i->bearer_token_job, url, i->glue, i); + if (r < 0) + return r; + + i->bearer_token_job->on_finished = oci_pull_job_on_finished_bearer_token; + i->bearer_token_job->description = strjoin("Bearer token (", url, ")"); + + r = pull_job_begin(i->bearer_token_job); + if (r < 0) + return r; + + return 1; /* continue running */ +} + +static void oci_pull_job_on_finished_manifest(PullJob *j) { + int r; + + assert(j); + OciPull *i = ASSERT_PTR(j->userdata); + assert(i->manifest_job == j); + + if (j->error != 0) { + if (j->error == -ENOMEDIUM) /* HTTP 404 */ + r = log_error_errno(j->error, "Failed to retrieve manifest/index file. (Wrong URL?)"); + else if (j->error == -ENOKEY) { /* HTTP 401 - Need authentication */ + + if (j->authentication_challenge) { + r = oci_pull_process_authentication_challenge(i, j->authentication_challenge); + if (r > 0) + return; + + goto finish; + } else + r = log_error_errno(j->error, "Failed to retrieve manifest/index file. (Needing authentication.)"); + } else + r = log_error_errno(j->error, "Failed to retrieve manifest/index file: %m"); + goto finish; + } + + /* If we have no content type, let's assume it's a manifest, not an index. This can happen in case we + * operate with file:// URLs, which in turn is really useful for testing purposes, to mock an OCI + * registry without having to set up an HTTP service. */ + if (streq_ptr(j->content_type, "application/vnd.oci.image.manifest.v1+json") || + !j->content_type) { + r = oci_pull_process_manifest(i, j); + if (r <= 0) + goto finish; + + return; + } + + if (streq_ptr(j->content_type, "application/vnd.oci.image.index.v1+json")) { + + if (i->refuse_index) { + r = log_error_errno(SYNTHETIC_ERRNO(EPROTO), "Already processed an OCI index, refusing to process another one."); + goto finish; + } + + /* For now do not allow nested indexes */ + i->refuse_index = true; + + r = oci_pull_process_index(i, j); + if (r <= 0) + goto finish; + + return; + } + + r = log_error_errno(SYNTHETIC_ERRNO(EPROTO), "Unexpected content type '%s', refusing.", strna(j->content_type)); + +finish: + oci_pull_finish(i, r); +} + +int oci_pull_start( + OciPull *i, + const char *ref, + const char *local, + ImportFlags flags) { + + int r; + + assert(i); + assert(ref); + + r = oci_ref_parse(ref, &i->repository, &i->image, &i->tag); + if (r < 0) + return r; + + r = oci_ref_normalize(&i->protocol, &i->repository, &i->image, &i->tag); + if (r < 0) + return r; + + if (local && !pull_validate_local(local, flags)) + return -EINVAL; + + if (i->manifest_job) + return -EBUSY; + + r = free_and_strdup(&i->local, local); + if (r < 0) + return r; + + i->flags = flags; + + _cleanup_free_ char *url = NULL; + r = oci_make_manifest_url(i->protocol, i->repository, i->image, i->tag, &url); + if (r < 0) + return r; + + /* Set up */ + r = pull_job_new(&i->manifest_job, url, i->glue, i); + if (r < 0) + return r; + + if (i->bearer_token) { + r = pull_job_set_bearer_token(i->manifest_job, i->bearer_token); + if (r < 0) + return r; + } + + r = pull_job_set_accept( + i->manifest_job, + STRV_MAKE("application/vnd.oci.image.manifest.v1+json", + "application/vnd.oci.image.index.v1+json")); + if (r < 0) + return r; + + i->manifest_job->on_finished = oci_pull_job_on_finished_manifest; + i->manifest_job->description = strjoin("Image Index (", url, ")"); + if (!i->manifest_job->description) + return -ENOMEM; + + return pull_job_begin(i->manifest_job); +} diff --git a/src/import/pull-oci.h b/src/import/pull-oci.h new file mode 100644 index 0000000000..86a37c33ec --- /dev/null +++ b/src/import/pull-oci.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "shared-forward.h" +#include "import-common.h" + +typedef struct OciPull OciPull; + +typedef void (*OciPullFinished)(OciPull *pull, int error, void *userdata); + +int oci_pull_new(OciPull **ret, sd_event *event, const char *image_root, OciPullFinished on_finished, void *userdata); +OciPull* oci_pull_unref(OciPull *i); + +DEFINE_TRIVIAL_CLEANUP_FUNC(OciPull*, oci_pull_unref); + +int oci_pull_start(OciPull *i, const char *ref, const char *local, ImportFlags flags); diff --git a/src/import/pull.c b/src/import/pull.c index a57c1c039d..e2f8161526 100644 --- a/src/import/pull.c +++ b/src/import/pull.c @@ -18,9 +18,11 @@ #include "iovec-util.h" #include "log.h" #include "main-func.h" +#include "oci-util.h" #include "parse-argument.h" #include "parse-util.h" #include "path-util.h" +#include "pull-oci.h" #include "pull-raw.h" #include "pull-tar.h" #include "runtime-scope.h" @@ -244,6 +246,71 @@ static int pull_raw(int argc, char *argv[], void *userdata) { return -r; } +static void on_oci_finished(OciPull *pull, int error, void *userdata) { + sd_event *event = userdata; + assert(pull); + + if (error == 0) + log_info("Operation completed successfully."); + + sd_event_exit(event, ABS(error)); +} + +static int pull_oci(int argc, char *argv[], void *userdata) { + int r; + + const char *ref = argv[1]; + + _cleanup_free_ char *image = NULL; + r = oci_ref_parse(ref, /* ret_registry= */ NULL, &image, /* ret_tag= */ NULL); + if (r == -EINVAL) + return log_error_errno(r, "OCI ref '%s' is invalid.", ref); + if (r < 0) + return log_error_errno(r, "Failed to check of OCI ref '%s' is valid: %m", ref); + + _cleanup_free_ char *l = NULL; + const char *local; + if (argc >= 3) + local = empty_or_dash_to_null(argv[2]); + else { + r = path_extract_filename(image, &l); + if (r < 0) + return log_error_errno(r, "Failed to get extract final component of '%s': %m", image); + + local = l; + } + + _cleanup_free_ char *normalized = NULL; + r = normalize_local(local, ref, &normalized); + if (r < 0) + return r; + + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + r = import_allocate_event_with_signals(&event); + if (r < 0) + return r; + + _cleanup_(oci_pull_unrefp) OciPull *pull = NULL; + r = oci_pull_new(&pull, event, arg_image_root, on_oci_finished, event); + if (r < 0) + return log_error_errno(r, "Failed to allocate puller: %m"); + + r = oci_pull_start( + pull, + ref, + normalized, + arg_import_flags & IMPORT_PULL_FLAGS_MASK_OCI); + if (r < 0) + return log_error_errno(r, "Failed to pull image: %m"); + + r = sd_event_loop(event); + if (r < 0) + return log_error_errno(r, "Failed to run event loop: %m"); + + log_info("Exiting."); + return -r; +} + static int help(int argc, char *argv[], void *userdata) { printf("%1$s [OPTIONS...] {COMMAND} ...\n" @@ -251,6 +318,7 @@ static int help(int argc, char *argv[], void *userdata) { "\n%2$sCommands:%3$s\n" " tar URL [NAME] Download a TAR image\n" " raw URL [NAME] Download a RAW image\n" + " oci REF [NAME] Download an OCI image\n" "\n%2$sOptions:%3$s\n" " -h --help Show this help\n" " --version Show package version\n" @@ -595,6 +663,7 @@ static int pull_main(int argc, char *argv[]) { { "help", VERB_ANY, VERB_ANY, 0, help }, { "tar", 2, 3, 0, pull_tar }, { "raw", 2, 3, 0, pull_raw }, + { "oci", 2, 3, 0, pull_oci }, {} }; diff --git a/src/import/test-oci-util.c b/src/import/test-oci-util.c new file mode 100644 index 0000000000..395b622b42 --- /dev/null +++ b/src/import/test-oci-util.c @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "tests.h" +#include "oci-util.h" + +static void test_urlescape_one(const char *s, const char *expected) { + _cleanup_free_ char *t = ASSERT_PTR(urlescape(s)); + + ASSERT_STREQ(t, expected); +} + +TEST(urlescape) { + test_urlescape_one(NULL, ""); + test_urlescape_one("", ""); + test_urlescape_one("a", "a"); + test_urlescape_one(" ", "%20"); + test_urlescape_one(" ", "%20%20%20%20%20"); + test_urlescape_one("foo\tfoo\aqux", "foo%09foo%07qux"); + test_urlescape_one("müffel", "m%c3%bcffel"); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); From 8343032a86b62f62780de85a696ab8f9d2632244 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sun, 9 Nov 2025 21:16:44 +0100 Subject: [PATCH 13/36] mstack: introduce "mstack" concept --- README | 3 +- src/shared/meson.build | 1 + src/shared/mstack.c | 1187 +++++++++++++++++++++++++++++++++++ src/shared/mstack.h | 64 ++ src/shared/shared-forward.h | 2 + src/test/meson.build | 1 + src/test/test-mstack.c | 131 ++++ 7 files changed, 1388 insertions(+), 1 deletion(-) create mode 100644 src/shared/mstack.c create mode 100644 src/shared/mstack.h create mode 100644 src/test/test-mstack.c diff --git a/README b/README index d85295b10e..0b2d53de1c 100644 --- a/README +++ b/README @@ -77,7 +77,8 @@ REQUIREMENTS: ≥ 6.10 for fcntl(F_DUPFD_QUERY), unprivileged linkat(AT_EMPTY_PATH), and block device 'partscan' sysfs attribute ≥ 6.12 for AT_HANDLE_MNT_ID_UNIQUE - ≥ 6.13 for PIDFD_GET_INFO and {set,remove}xattrat() + ≥ 6.13 for PIDFD_GET_INFO and {set,remove}xattrat() and + FSCONFIG_SET_FD support for overlayfs layers ≥ 6.16 for coredump pattern '%F' (pidfd) specifier and SO_PASSRIGHTS ✅ systemd utilizes several new kernel APIs, but will fall back gracefully diff --git a/src/shared/meson.build b/src/shared/meson.build index 8b49ad9a52..5becbf3b4f 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -132,6 +132,7 @@ shared_sources = files( 'module-util.c', 'mount-setup.c', 'mount-util.c', + 'mstack.c', 'net-condition.c', 'netif-naming-scheme.c', 'netif-sriov.c', diff --git a/src/shared/mstack.c b/src/shared/mstack.c new file mode 100644 index 0000000000..8b0beb21ad --- /dev/null +++ b/src/shared/mstack.c @@ -0,0 +1,1187 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include + +#include "alloc-util.h" +#include "chase.h" +#include "dissect-image.h" +#include "errno-util.h" +#include "fd-util.h" +#include "fs-util.h" +#include "log.h" +#include "loop-util.h" +#include "macro.h" +#include "mount-util.h" +#include "mstack.h" +#include "path-util.h" +#include "process-util.h" +#include "recurse-dir.h" +#include "rm-rf.h" +#include "sort-util.h" +#include "stat-util.h" +#include "string-table.h" +#include "string-util.h" +#include "tmpfile-util.h" +#include "uid-classification.h" +#include "unit-name.h" +#include "vpick.h" + +static void mstack_mount_done(MStackMount *m) { + assert(m); + + m->where = mfree(m->where); + m->what = mfree(m->what); + m->what_fd = safe_close(m->what_fd); + m->mount_fd = safe_close(m->mount_fd); + m->sort_key = mfree(m->sort_key); + m->dissected_image = dissected_image_unref(m->dissected_image); +} + +static void mstack_done(MStack *mstack) { + assert(mstack); + + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) + mstack_mount_done(m); + + mstack->mounts = mfree(mstack->mounts); + mstack->n_mounts = 0; + mstack->root_mount = NULL; + mstack->has_tmpfs_root = mstack->has_overlayfs = false; + mstack->path = mfree(mstack->path); + safe_close(mstack->root_mount_fd); + safe_close(mstack->usr_mount_fd); +} + +MStack *mstack_free(MStack *mstack) { + if (!mstack) + return NULL; + + mstack_done(mstack); + + return mfree(mstack); +} + +static int validate_prefix_name(const char *name, const char *prefix, char **ret_parameter) { + _cleanup_free_ char *p = NULL; + + assert(name); + assert(prefix); + + const char *a = startswith(name, prefix); + if (isempty(a)) { + if (ret_parameter) + *ret_parameter = NULL; + + return false; + } + + p = strdup(a); + if (!p) + return -ENOMEM; + + if (ret_parameter) + *ret_parameter = TAKE_PTR(p); + + return true; +} + +static MStackMount *mstack_find(MStack *mstack, MStackMountType t, const char *sort_key, const char *where) { + assert(mstack); + + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { + + if (t >= 0 && m->mount_type != t) + continue; + + if (sort_key && !streq_ptr(m->sort_key, sort_key)) + continue; + + if (where && !path_equal(m->where, where)) + continue; + + return m; + } + + return NULL; +} + +static int mstack_load_one(MStack *mstack, const char *dir, int dir_fd, const char *fname) { + int r; + + assert(mstack); + assert(dir_fd >= 0); + assert(fname); + + _cleanup_close_ int what_fd = openat(dir_fd, fname, O_PATH|O_CLOEXEC); + if (what_fd < 0) + return log_debug_errno(errno, "Failed to open %s/%s: %m", dir, fname); + + struct stat st; + if (fstat(what_fd, &st) < 0) + return log_debug_errno(errno, "Failed to stat %s/%s: %m", dir, fname); + + ImageType image_type = _IMAGE_TYPE_INVALID; + _cleanup_free_ char *what = NULL, *unsuffixed = NULL; + if (S_ISDIR(st.st_mode)) { + + const char *dotv = endswith(fname, ".v"); + if (dotv) { + const char *dotrawv = endswith(fname, ".raw.v"); + + PickFilter filter = { + .type_mask = dotrawv ? (1U << DT_REG) : ((1U << DT_DIR) | (1U << DT_BLK)), + .suffix = dotrawv ? ".raw" : NULL, + .architecture = _ARCHITECTURE_INVALID, + }; + + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + r = path_pick(dir, dir_fd, fname, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE, &result); + if (r < 0) + return log_debug_errno(r, "Failed to resolve '%s' directory: %m", fname); + if (r == 0) + return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "Found no suitable entry in '%s': %m", fname); + + what = TAKE_PTR(result.path); + close_and_replace(what_fd, result.fd); + st = result.st; + + unsuffixed = strndup(fname, (dotrawv ?: dotv) - fname); + if (!unsuffixed) + return log_oom(); + + image_type = S_ISDIR(st.st_mode) ? IMAGE_DIRECTORY : + S_ISREG(st.st_mode) ? IMAGE_RAW : + S_ISBLK(st.st_mode) ? IMAGE_BLOCK : _IMAGE_TYPE_INVALID; + + assert(image_type >= 0); + } else + image_type = IMAGE_DIRECTORY; + + } else if (S_ISREG(st.st_mode)) { + const char *e = endswith(fname, ".raw"); + if (!e) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Unexpected suffix of '%s/%s', refusing.", dir, fname); + + unsuffixed = strndup(fname, e - fname); + if (!unsuffixed) + return -ENOMEM; + + image_type = IMAGE_RAW; + + } else if (S_ISBLK(st.st_mode)) + image_type = IMAGE_BLOCK; + else + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Unexpected inode type of '%s/%s', refusing.", dir, fname); + + if (!what) { + what = strdup(fname); + if (!what) + return -ENOMEM; + } + + if (!unsuffixed) { + unsuffixed = strdup(what); + if (!unsuffixed) + return -ENOMEM; + } + + if (!GREEDY_REALLOC(mstack->mounts, mstack->n_mounts+1)) + return -ENOMEM; + + MStackMount *m = mstack->mounts + mstack->n_mounts; + + _cleanup_free_ char *parameter = NULL; + r = validate_prefix_name(unsuffixed, "layer@", ¶meter); + if (r < 0) + return log_debug_errno(r, "Failed to check prefix of %s/%s: %m", dir, fname); + if (r > 0) { + /* Paranoia: let's refuse two layers that have the same sort key. Howe can that happen? + * People might have a .raw layer and one dir layer with the same name. Or one with .v and + * one without. */ + if (mstack_find(mstack, MSTACK_LAYER, parameter, /* where= */ NULL)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Duplicate layer '%s', refusing.", parameter); + + *m = (MStackMount) { + .mount_type = MSTACK_LAYER, + .what = TAKE_PTR(what), + .what_fd = TAKE_FD(what_fd), + .mount_fd = -EBADF, + .sort_key = TAKE_PTR(parameter), + .image_type = image_type, + }; + + mstack->n_mounts++; + log_debug("Found mstack layer '%s' ('%s', owned by UID " UID_FMT ")", m->sort_key, m->what, st.st_uid); + return 0; + } + + if (streq(unsuffixed, "rw")) { + if (mstack_find(mstack, MSTACK_RW, /* sort_key= */ NULL, /* where= */ NULL)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Duplicate rw entry, refusing."); + + *m = (MStackMount) { + .mount_type = MSTACK_RW, + .what = TAKE_PTR(what), + .what_fd = TAKE_FD(what_fd), + .mount_fd = -EBADF, + .image_type = image_type, + }; + + mstack->n_mounts++; + log_debug("Found mstack rw layer ('%s')", m->what); + return 0; + } + + MStackMountType bind_type = _MSTACK_MOUNT_TYPE_INVALID; + r = validate_prefix_name(unsuffixed, "bind@", ¶meter); + if (r < 0) + return log_debug_errno(r, "Failed to check prefix of %s/%s: %m", dir, fname); + if (r > 0) + bind_type = MSTACK_BIND; + else { + r = validate_prefix_name(unsuffixed, "robind@", ¶meter); + if (r < 0) + return log_debug_errno(r, "Failed to check prefix of %s/%s: %m", dir, fname); + if (r > 0) + bind_type = MSTACK_ROBIND; + } + if (bind_type >= 0) { + _cleanup_free_ char *where = NULL; + r = unit_name_path_unescape(parameter, &where); + if (r < 0) + return log_debug_errno(r, "Cannot unescape path '%s' of '%s/%s'", parameter, dir, fname); + + if (mstack_find(mstack, MSTACK_BIND, /* sort_key= */ NULL, /* where= */ where) || + mstack_find(mstack, MSTACK_ROBIND, /* sort_key= */ NULL, /* where= */ where)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Duplicate bind entry, refusing"); + + *m = (MStackMount) { + .mount_type = bind_type, + .what = TAKE_PTR(what), + .what_fd = TAKE_FD(what_fd), + .mount_fd = -EBADF, + .where = TAKE_PTR(where), + .image_type = image_type, + }; + + mstack->n_mounts++; + log_debug("Found mstack bind layer '%s' ('%s')", empty_to_root(m->where), m->what); + return 0; + } + + if (streq(unsuffixed, "root")) { + if (mstack_find(mstack, MSTACK_ROOT, /* sort_key= */ NULL, /* where= */ NULL)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Duplicate root entry, refusing"); + + *m = (MStackMount) { + .mount_type = MSTACK_ROOT, + .what = TAKE_PTR(what), + .what_fd = TAKE_FD(what_fd), + .mount_fd = -EBADF, + .image_type = image_type, + }; + + mstack->n_mounts++; + log_debug("Found mstack root layer ('%s')", m->what); + return 0; + } + + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Unrecognized entry '%s/%s', refusing", dir, fname); +} + +static int mount_compare_func(const MStackMount *a, const MStackMount *b) { + int r; + + assert(a); + assert(b); + + /* If we apply this mstack in read-only mode then we'll convert the 'rw' layer which normally is an + * upperdir into the topmost lowerdir. When sorting the mstack it is hence essential, that the "rw" + * layer ends up *after* the regular layers. Enforce this here via a compile-time check. */ + assert_cc(MSTACK_RW > MSTACK_LAYER); + + r = CMP(a->mount_type, b->mount_type); + if (r != 0) + return r; + + r = path_compare(a->where, b->where); + if (r != 0) + return r; + + r = strverscmp_improved(a->sort_key, b->sort_key); + if (r != 0) + return r; + + return 0; +} + +static void mstack_remove(MStack *mstack, MStackMountType t) { + assert(mstack); + + size_t z = 0; + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { + if (m->mount_type == t) + mstack_mount_done(m); + else + mstack->mounts[z++] = *m; + } + + mstack->n_mounts = z; +} + +static int mstack_normalize(MStack *mstack) { + int r; + + assert(mstack); + + typesafe_qsort(mstack->mounts, mstack->n_mounts, mount_compare_func); + + size_t n_layers = 0; + bool has_rw = false, has_root_bind = false, has_usr_bind = false, has_root = false; + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { + switch (m->mount_type) { + case MSTACK_LAYER: + n_layers++; + break; + + case MSTACK_RW: + assert(!has_rw); + has_rw = true; + break; + + case MSTACK_BIND: + case MSTACK_ROBIND: + if (empty_or_root(m->where)) + has_root_bind = true; + else if (path_equal(m->where, "/usr")) + has_usr_bind = true; + break; + + case MSTACK_ROOT: + assert(!has_root); + has_root = true; + break; + + default: + assert_not_reached(); + } + } + + /* If the overlayfs stack is fully obstructed, kill it */ + if (has_root_bind || (has_root && has_usr_bind)) { + mstack_remove(mstack, MSTACK_LAYER); + mstack_remove(mstack, MSTACK_RW); + + n_layers = 0; + has_rw = false; + } + + /* Only a single read-only or read-write layer? Turn into bind mount! */ + if (n_layers + has_rw == 1) { + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { + if (m->mount_type == MSTACK_LAYER) + m->mount_type = MSTACK_ROBIND; + else if (m->mount_type == MSTACK_RW) + m->mount_type = MSTACK_BIND; + else + continue; + + if (has_root) { + /* If there's a root dir, let's only bind mount the /usr/ subdir */ + _cleanup_close_ int usr_fd = openat(m->what_fd, "usr", O_CLOEXEC|O_PATH|O_NOFOLLOW|O_DIRECTORY); + if (usr_fd < 0) + return log_debug_errno(errno, "Failed to open /usr/ subdir: %m"); + + _cleanup_free_ char *usr = path_join(m->what, "usr"); + if (!usr) + return log_oom(); + + r = free_and_strdup_warn(&m->where, "/usr"); + if (r < 0) + return r; + + close_and_replace(m->what_fd, usr_fd); + free_and_replace(m->what, usr); + } else { + r = free_and_strdup_warn(&m->where, "/"); + if (r < 0) + return r; + + has_root_bind = true; + } + } + + n_layers = 0; + has_rw = false; + } + + /* If the root dir is overmounted, we can drop the original root */ + if (has_root_bind) { + mstack_remove(mstack, MSTACK_ROOT); + has_root = false; + } + + /* After converting, let's sort things again */ + typesafe_qsort(mstack->mounts, mstack->n_mounts, mount_compare_func); + + /* Find root mount (unless it's the overlayfs stack) */ + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) + if ((m->mount_type == MSTACK_ROOT) || + (IN_SET(m->mount_type, MSTACK_BIND, MSTACK_ROBIND) && empty_or_root(m->where))) { + assert(!mstack->root_mount); + mstack->root_mount = m; + } + assert((has_root || has_root_bind) == !!mstack->root_mount); + + mstack->has_tmpfs_root = n_layers == 0 && !has_rw && !has_root_bind && !has_root; + mstack->has_overlayfs = n_layers > 0 || has_rw; + return 0; +} + +static int mstack_load_now(MStack *mstack, const char *dir, int dir_fd, MStackFlags flags) { + _cleanup_close_ int _dir_fd = -EBADF; + int r; + + assert(mstack); + + r = free_and_strdup_warn(&mstack->path, dir); + if (r < 0) + return r; + + /* Expects dir_fd already opened. If not, then we'll open it based on 'dir' */ + if (dir_fd < 0) { + _dir_fd = openat(AT_FDCWD, isempty(dir) ? "." : dir, O_DIRECTORY|O_CLOEXEC); + if (_dir_fd < 0) + return log_debug_errno(errno, "Failed to to open '%s': %m", dir); + + dir_fd = _dir_fd; + } else { + /* Possibly convert an O_PATH fd to a real one */ + dir_fd = fd_reopen_condition(dir_fd, O_DIRECTORY|O_CLOEXEC, O_PATH|O_DIRECTORY, &_dir_fd); + if (dir_fd < 0) + return log_debug_errno(dir_fd, "Failed to reopen '%s': %m", dir); + } + + _cleanup_free_ DirectoryEntries *de = NULL; + r = readdir_all(dir_fd, RECURSE_DIR_IGNORE_DOT, &de); + if (r < 0) + return r; + + FOREACH_ARRAY(i, de->entries, de->n_entries) { + r = mstack_load_one(mstack, dir, dir_fd, (*i)->d_name); + if (r < 0) + return r; + } + + return mstack_normalize(mstack); +} + +static int mount_get_fd(MStackMount *m) { + assert(m); + + if (m->dissected_image) { + assert(m->dissected_image->partitions[PARTITION_ROOT].found); + return ASSERT_FD(m->dissected_image->partitions[PARTITION_ROOT].fsmount_fd); + } + + if (m->mount_fd >= 0) + return m->mount_fd; + + return m->what_fd; +} + +static bool mount_is_ro(MStackMount *m, MStackFlags flags) { + assert(m); + + return FLAGS_SET(flags, MSTACK_RDONLY) || + IN_SET(m->mount_type, MSTACK_LAYER, MSTACK_ROBIND); +} + +static const char *mount_name(MStackMount *m) { + assert(m); + + /* Returns some vaguely useful identifier for this layer, for showing in debug output */ + + if (m->sort_key) + return m->sort_key; + + if (m->where) + return m->where; + + return mstack_mount_type_to_string(m->mount_type); +} + +int mstack_open_images( + MStack *mstack, + int userns_fd, + const ImagePolicy *image_policy, + const ImageFilter *image_filter, + MStackFlags flags) { + + int r; + + assert(mstack); + + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { + + DissectImageFlags dissect_image_flags = + DISSECT_IMAGE_DISCARD| + DISSECT_IMAGE_GENERIC_ROOT| + DISSECT_IMAGE_REQUIRE_ROOT| + DISSECT_IMAGE_MOUNT_ROOT_ONLY| + DISSECT_IMAGE_FSCK| + DISSECT_IMAGE_USR_NO_ROOT| + DISSECT_IMAGE_GROWFS| + DISSECT_IMAGE_ADD_PARTITION_DEVICES| + DISSECT_IMAGE_PIN_PARTITION_DEVICES| + DISSECT_IMAGE_ALLOW_USERSPACE_VERITY; + + SET_FLAG(dissect_image_flags, DISSECT_IMAGE_READ_ONLY, mount_is_ro(m, flags)); + SET_FLAG(dissect_image_flags, DISSECT_IMAGE_FOREIGN_UID, userns_fd >= 0); + + switch (m->image_type) { + + case IMAGE_RAW: + case IMAGE_BLOCK: + assert(!m->dissected_image); + + if (userns_fd >= 0) { + r = mountfsd_mount_image_fd( + m->what_fd, + userns_fd, + /* options= */ NULL, + image_policy, + /* verity= */ NULL, + dissect_image_flags, + &m->dissected_image); + if (r < 0) + return r; + } else { + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(dissected_image_unrefp) DissectedImage *dissected_image = NULL; + + r = loop_device_make( + m->what_fd, + FLAGS_SET(flags, MSTACK_RDONLY) ? O_RDONLY : O_RDWR, + /* offset= */ 0, + /* size= */ UINT64_MAX, + /* sector_size= */ UINT32_MAX, + LO_FLAGS_PARTSCAN, + LOCK_SH, + &loop_device); + if (r < 0) + return log_debug_errno(r, "Failed to allocate loopback device for '%s': %m", m->what); + + _cleanup_(verity_settings_done) VeritySettings verity = VERITY_SETTINGS_DEFAULT; + r = dissect_loop_device_and_warn( + loop_device, + &verity, + /* mount_options= */ NULL, + image_policy, + image_filter, + dissect_image_flags, + &dissected_image); + if (r < 0) + return r; + + if (!dissected_image->partitions[PARTITION_ROOT].found) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Currently images withot root partition are not supported: %m"); + + r = dissected_image_load_verity_sig_partition( + dissected_image, + loop_device->fd, + &verity); + if (r < 0) + return log_debug_errno(r, "Failed to load Verity signature partition of '%s': %m", m->what); + + r = dissected_image_guess_verity_roothash( + dissected_image, + &verity); + if (r < 0) + return log_debug_errno(r, "Failed to guess Verity root hash of '%s': %m", m->what); + + r = dissected_image_decrypt( + dissected_image, + /* root= */ NULL, + /* passphrase= */ NULL, + &verity, + image_policy, + dissect_image_flags); + if (r < 0) + return log_debug_errno(r, "Failed to decrypt image '%s': %m", m->what); + + r = dissected_image_mount( + dissected_image, + /* where= */ NULL, /* allocate as mount fds, do not attach anywhere */ + /* uid_shift= */ UID_INVALID, + /* uid_range= */ UID_INVALID, + /* userns_fd = */ -EBADF, + dissect_image_flags); + if (r < 0) + return log_debug_errno(r, "Failed to mount image '%s': %m", m->what); + + r = loop_device_flock(loop_device, LOCK_UN); + if (r < 0) + return log_debug_errno(r, "Failed to unlock loopback block device: %m"); + + r = dissected_image_relinquish(dissected_image); + if (r < 0) + return log_debug_errno(r, "Failed to relinquish DM and loopback block devices: %m"); + + m->dissected_image = TAKE_PTR(dissected_image); + } + + log_debug("Acquired mstack DDI layer '%s'", mount_name(m)); + break; + + case IMAGE_DIRECTORY: + case IMAGE_SUBVOLUME: + assert(m->mount_fd < 0); + + if (userns_fd >= 0) { + r = mountfsd_mount_directory_fd( + m->what_fd, + userns_fd, + dissect_image_flags, + &m->mount_fd); + if (r < 0) + return r; + } else { + m->mount_fd = open_tree_attr_with_fallback( + mount_get_fd(m), + /* path= */ "", + OPEN_TREE_CLONE|OPEN_TREE_CLOEXEC|AT_EMPTY_PATH, + &(struct mount_attr) { + .attr_set = mount_is_ro(m, flags) ? MOUNT_ATTR_RDONLY : 0, + .attr_clr = mount_is_ro(m, flags) ? 0 : MOUNT_ATTR_RDONLY, + .propagation = MS_PRIVATE, /* disconnect us from bind mount source */ + }); + if (m->mount_fd < 0) + return log_debug_errno(m->mount_fd, "Failed to create bind mount inode '%s': %m", m->where); + } + + log_debug("Acquired bind mount for layer '%s'.", mount_name(m)); + break; + + default: + assert_not_reached(); + } + } + + return 0; +} + +static int mstack_has_writable_layers(MStack *mstack, MStackFlags flags) { + assert(mstack); + + if (FLAGS_SET(flags, MSTACK_RDONLY)) + return false; + + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) + if (m->mount_type == MSTACK_RW) + return true; + + return false; +} + +static int fsconfig_add_layer(int sb_fd, const char *key, int layer_fd) { + int r; + + assert(sb_fd >= 0); + assert(key); + assert(layer_fd >= 0); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *pretty = NULL; + (void) fd_get_path(layer_fd, &pretty); + log_debug("Adding '%s' as layer '%s' to overlayfs.", key, pretty); + } + + r = RET_NERRNO(fsconfig(sb_fd, FSCONFIG_SET_FD, key, /* value= */ NULL, layer_fd)); + if (r != -EBADF && !ERRNO_IS_NEG_NOT_SUPPORTED(r)) + return r; + + /* overlayfs learnt support for FSCONFIG_SET_FD only with linux 6.13, hence provide a fallback here via /proc/self/fd/ */ + + // FIXME: This compatibility code path shall be removed once kernel 6.13 + // becomes the new minimal baseline + + const char *layer_path = FORMAT_PROC_FD_PATH(layer_fd); + log_debug_errno(r, "FSCONFIG_SET_FD for layer '%s' failed, falling back to FSCONFIG_SET with '%s': %m", key, layer_path); + return RET_NERRNO(fsconfig(sb_fd, FSCONFIG_SET_STRING, key, layer_path, /* aux= */ 0)); +} + +static int mstack_make_overlayfs( + MStack *mstack, + const char *temp_mount_dir, + MStackFlags flags, + int *ret_overlayfs_mnt_fd) { + + int r; + + assert(mstack); + assert(temp_mount_dir); + assert(ret_overlayfs_mnt_fd); + + if (!mstack->has_overlayfs) { + *ret_overlayfs_mnt_fd = -EBADF; + return 0; + } + + bool writable = mstack_has_writable_layers(mstack, flags); + + _cleanup_close_ int sb_fd = fsopen("overlay", FSOPEN_CLOEXEC); + if (sb_fd < 0) + return log_debug_errno(errno, "Failed to create overlayfs: %m"); + + _cleanup_close_pair_ int errno_pipe_fds[2] = EBADF_PAIR; + if (pipe2(errno_pipe_fds, O_CLOEXEC) < 0) + return log_debug_errno(errno, "Failed to open pipe: %m"); + + /* If we operate unpriv, we have to attach the layers to a place in the fs, before we can pass them + * to overlayfs (see comments below), hence fork off a child with a private mount namespace, so that + * noone else sees that. */ + r = pidref_safe_fork("(layerfd)", + FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_REOPEN_LOG|FORK_WAIT|FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE, + /* ret= */ NULL); + if (r < 0) { + errno_pipe_fds[1] = safe_close(errno_pipe_fds[1]); + + int q = read_errno(errno_pipe_fds[0]); + if (q < 0 && q != -EIO) + return q; + + return r; + } + if (r == 0) { + /* child */ + + /* Kernel expects the stack in reverse order, hence go from back to front */ + for (size_t i = mstack->n_mounts; i > 0; i--) { + MStackMount *m = mstack->mounts + i - 1; + + if (!IN_SET(m->mount_type, MSTACK_RW, MSTACK_LAYER)) + continue; + + /* overlayfs refuses to work with layers on mounts not owned by our userns, hence create a + * clone that is owned by our userns */ + _cleanup_close_ int cloned_fd = mount_fd_clone(ASSERT_FD(mount_get_fd(m)), /* recursive= */ false, /* replacement_fd= */ NULL); + if (cloned_fd < 0) + report_errno_and_exit(errno_pipe_fds[1], cloned_fd); + + /* When working with detached mounts overlayfs (which requires kernel 6.14) currently + * insists on upperdir being the root inode of the mount. But that collides with the + * requirement that upperdir/workdir are on the same mount and siblings. Bummer. To + * work around this we'll temporarily attach the thing, which relaxes the rules + * sufficiently. */ + if (move_mount(cloned_fd, "", -EBADF, temp_mount_dir, MOVE_MOUNT_F_EMPTY_PATH) < 0) + report_errno_and_exit(errno_pipe_fds[1], -errno); + + /* Open the layer immediately after attaching */ + _cleanup_close_ int temp_fd = open(temp_mount_dir, O_PATH|O_CLOEXEC); + if (temp_fd < 0) + report_errno_and_exit(errno_pipe_fds[1], -errno); + + switch (m->mount_type) { + + case MSTACK_RW: { + if (mount_is_ro(m, flags)) { + /* If invoked in read-only mode we'll not create the data dir, but use it if it exists */ + _cleanup_close_ int data_fd = openat(temp_fd, "data", O_CLOEXEC|O_NOFOLLOW|O_DIRECTORY); + if (data_fd < 0) { + if (errno == ENOENT) /* If the 'data' dir doesn't exist, just skip + * over it, it apparently was never created, but + * that's fine for a read-only invocation */ + break; + + log_debug_errno(errno, "Failed to open 'data' directory below 'rw' layer: %m"); + report_errno_and_exit(errno_pipe_fds[1], -errno); + } + + /* Downgrade to regular lowerdir if read-only is requested */ + r = fsconfig_add_layer(sb_fd, "lowerdir+", data_fd); + if (r < 0) { + log_debug_errno(r, "Failed to set mount layer lowerdir+=%s/data: %m", m->what); + report_errno_and_exit(errno_pipe_fds[1], r); + } + } else { + /* If invoked in writable mode, let's create the data dir if it is missing */ + _cleanup_close_ int data_fd = open_mkdir_at(temp_fd, "data", O_CLOEXEC|O_NOFOLLOW, 0755); + if (data_fd < 0) { + log_debug_errno(data_fd, "Failed to open 'data' directory below 'rw' layer: %m"); + report_errno_and_exit(errno_pipe_fds[1], data_fd); + } + + r = fsconfig_add_layer(sb_fd, "upperdir", data_fd); + if (r < 0) { + log_debug_errno(r, "Failed to set mount layer upperdir=%s/data: %m", m->what); + report_errno_and_exit(errno_pipe_fds[1], r); + } + + /* Similar, create the work directory */ + _cleanup_close_ int work_fd = open_mkdir_at(temp_fd, "work", O_CLOEXEC|O_NOFOLLOW, 0755); + if (work_fd < 0) { + log_debug_errno(work_fd, "Failed to open 'work' directory below 'rw' layer: %m"); + report_errno_and_exit(errno_pipe_fds[1], work_fd); + } + + /* rm_rf_children() takes possession of the fd no matter what, let's dup it here */ + int dup_fd = fcntl(work_fd, F_DUPFD_CLOEXEC, 3); + if (dup_fd < 0) { + log_debug_errno(errno, "Failed to duplicate work fd: %m"); + report_errno_and_exit(errno_pipe_fds[1], -errno); + } + + /* Empty the work directory, just in case it existed before. It's supposed to be empty. */ + r = rm_rf_children(dup_fd, REMOVE_PHYSICAL, /* root_dev= */ NULL); + if (r < 0) + log_debug_errno(r, "Failed to empty 'work' directory below 'rw' layer, ignoring: %m"); + + r = fsconfig_add_layer(sb_fd, "workdir", work_fd); + if (r < 0) { + log_debug_errno(r, "Failed to set mount layer workdir=%s/work: %m", m->what); + report_errno_and_exit(errno_pipe_fds[1], r); + } + + break; + } + break; + } + + case MSTACK_LAYER: + r = fsconfig_add_layer(sb_fd, "lowerdir+", temp_fd); + if (r < 0) { + log_debug_errno(r, "Failed to set mount layer lowerdir+=%s: %m", m->what); + report_errno_and_exit(errno_pipe_fds[1], r); + } + + break; + + default: + break; + } + } + + if (!writable && fsconfig(sb_fd, FSCONFIG_SET_FLAG, "ro", /* value= */ NULL, /* aux= */ 0) < 0) { + log_debug_errno(errno, "Failed to set read-only mount flag: %m"); + report_errno_and_exit(errno_pipe_fds[1], -errno); + } + + if (fsconfig(sb_fd, FSCONFIG_SET_FLAG, "userxattr", /* value= */ NULL, /* aux= */ 0) < 0) { + log_debug_errno(errno, "Failed to set userxattr mount flag: %m"); + report_errno_and_exit(errno_pipe_fds[1], -errno); + } + + if (fsconfig(sb_fd, FSCONFIG_SET_STRING, "source", mstack->path, /* aux= */ 0) < 0) { + log_debug_errno(errno, "Failed to set mount source: %m"); + report_errno_and_exit(errno_pipe_fds[1], -errno); + } + + /* This is where the superblock is materialized. It must be called from the child's + * namespace, where the mounts are attached as described above, otherwise overlayfs is + * unhappy and will refuse the superblock to be created. */ + if (fsconfig(sb_fd, FSCONFIG_CMD_CREATE, /* key= */ NULL, /* value= */ NULL, /* aux= */ 0) < 0) { + log_debug_errno(errno, "Failed to realize overlayfs: %m"); + report_errno_and_exit(errno_pipe_fds[1], -errno); + } + + report_errno_and_exit(errno_pipe_fds[1], 0); + } + + _cleanup_close_ int overlayfs_mnt_fd = fsmount(sb_fd, FSMOUNT_CLOEXEC, 0); + if (overlayfs_mnt_fd < 0) + return log_debug_errno(errno, "Failed to create mount fd: %m"); + + if (mount_setattr(overlayfs_mnt_fd, "", AT_EMPTY_PATH, + &(struct mount_attr) { + .attr_set = writable ? 0 : MOUNT_ATTR_RDONLY, + .attr_clr = writable ? MOUNT_ATTR_RDONLY : 0, + }, sizeof(struct mount_attr)) < 0) + return log_debug_errno(errno, "Failed to mark root bind mount read-only: %m"); + + *ret_overlayfs_mnt_fd = TAKE_FD(overlayfs_mnt_fd); + return 1; +} + +int mstack_make_mounts( + MStack *mstack, + const char *temp_mount_dir, + MStackFlags flags) { + + int r; + + assert(mstack); + assert(temp_mount_dir); + + _cleanup_close_ int overlayfs_mnt_fd = -EBADF; + r = mstack_make_overlayfs(mstack, temp_mount_dir, flags, &overlayfs_mnt_fd); + if (r < 0) + return r; + if (r > 0) + log_debug("Acquired mstack overlayfs mount."); + + assert(mstack->root_mount_fd < 0); + if (mstack->root_mount) { + assert(!mstack->has_tmpfs_root); + + mstack->root_mount_fd = fcntl(mount_get_fd(mstack->root_mount), F_DUPFD_CLOEXEC, 3); + if (mstack->root_mount_fd < 0) + return log_debug_errno(errno, "Failed to create root bind mount: %m"); + + log_debug("Acquired mstack root bind mount."); + + } else if (mstack->has_tmpfs_root) { + _cleanup_close_ int sb_fd = fsopen("tmpfs", FSOPEN_CLOEXEC); + if (sb_fd < 0) + return log_debug_errno(errno, "Failed to create tmpfs: %m"); + + if (fsconfig(sb_fd, FSCONFIG_SET_STRING, "source", mstack->path, 0) < 0) + return log_debug_errno(errno, "Failed to set mount source: %m"); + + if (fsconfig(sb_fd, FSCONFIG_SET_STRING, "mode", "0755", 0) < 0) + return log_debug_errno(errno, "Failed to set mount source: %m"); + + if (fsconfig(sb_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0) < 0) + return log_debug_errno(errno, "Failed to realize tmpfs: %m"); + + mstack->root_mount_fd = fsmount(sb_fd, FSMOUNT_CLOEXEC, 0); + if (mstack->root_mount_fd < 0) + return log_debug_errno(errno, "Failed to create mount fd: %m"); + + log_debug("Acquired root tmpfs mount."); + } + + if (mstack->root_mount_fd >= 0 && overlayfs_mnt_fd >= 0) { + /* If we have an overlayfs and a root fs, then the overlayfs should be placed on /usr/. */ + mstack->usr_mount_fd = open_tree(overlayfs_mnt_fd, "usr", OPEN_TREE_CLONE|OPEN_TREE_CLOEXEC|AT_SYMLINK_NOFOLLOW); + if (mstack->usr_mount_fd < 0) + return log_debug_errno(errno, "Failed to create bind mount inode '/usr/': %m"); + + if (mount_setattr(mstack->usr_mount_fd, "", AT_EMPTY_PATH, + &(struct mount_attr) { + .attr_set = mount_is_ro(mstack->root_mount, flags) ? MOUNT_ATTR_RDONLY : 0, + .attr_clr = mount_is_ro(mstack->root_mount, flags) ? 0 : MOUNT_ATTR_RDONLY, + .propagation = MS_PRIVATE, /* disconnect us from bind mount source */ + }, sizeof(struct mount_attr)) < 0) + return log_debug_errno(errno, "Failed to mark usr bind mount read-only: %m"); + + log_debug("Acquired mstack overlayfs '/usr/' submount."); + } + + /* If we acquired no other root fs, then the overlayfs is our root */ + if (mstack->root_mount_fd < 0) + mstack->root_mount_fd = TAKE_FD(overlayfs_mnt_fd); + + return 0; +} + +int mstack_bind_mounts( + MStack *mstack, + const char *where, + int where_fd, + MStackFlags flags, + int *ret_root_fd) { + + int r; + + assert(mstack); + + _cleanup_close_ int _where_fd = -EBADF; + if (where_fd == AT_FDCWD) { + _where_fd = open(".", O_CLOEXEC|O_PATH|O_DIRECTORY); + if (_where_fd < 0) + return log_debug_errno(errno, "Failed to open current working directory: %m"); + where_fd = _where_fd; + } else if (where_fd < 0) { + r = chase(where, + /* root= */ NULL, + (FLAGS_SET(flags, MSTACK_MKDIR) ? CHASE_MKDIR_0755 : 0)|CHASE_MUST_BE_DIRECTORY, + /* ret_path= */ NULL, + &_where_fd); + if (r < 0) + return log_debug_errno(r, "Failed to open '%s': %m", where); + + where_fd = _where_fd; + } + + assert(mstack->root_mount_fd >= 0); + if (move_mount(mstack->root_mount_fd, "", where_fd, "", MOVE_MOUNT_F_EMPTY_PATH|MOVE_MOUNT_T_EMPTY_PATH) < 0) + return log_debug_errno(errno, "Failed to attach mstack root mount to '%s': %m", where); + + log_debug("Attached mstack root mount to '%s'.", where); + + _cleanup_close_ int root_fd = open(where, O_CLOEXEC|O_PATH|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) + return log_debug_errno(errno, "Failed to mount root mount '%s': %m", where); + + if (mstack->usr_mount_fd >= 0) { + _cleanup_close_ int subdir_fd = -EBADF; + r = chaseat(root_fd, "usr", CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, /* ret_path= */ NULL, &subdir_fd); + if (r < 0) + return log_debug_errno(r, "Failed to open mount point inode '%s': %m", where); + + if (move_mount(mstack->usr_mount_fd, "", subdir_fd, "", MOVE_MOUNT_F_EMPTY_PATH|MOVE_MOUNT_T_EMPTY_PATH) < 0) + return log_debug_errno(errno, "Failed to attach bind mount to '/usr/' subdir: %m"); + + log_debug("Attached mstack '/usr/' mount to '%s/usr/'.", where); + } + + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { + + if (!IN_SET(m->mount_type, MSTACK_BIND, MSTACK_ROBIND) || + m == mstack->root_mount) + continue; + + assert(m->mount_fd >= 0); + + _cleanup_close_ int subdir_fd = -EBADF; + r = chaseat(root_fd, m->where, CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, /* ret_path= */ NULL, &subdir_fd); + if (r < 0) + return log_debug_errno(r, "Failed to open mount point inode '%s': %m", m->where); + + if (move_mount(m->mount_fd, "", subdir_fd, "", MOVE_MOUNT_F_EMPTY_PATH|MOVE_MOUNT_T_EMPTY_PATH) < 0) + return log_debug_errno(errno, "Failed to attach bind mount to '%s' subdir: %m", m->where); + + log_debug("Attached mstack '%s/' mount to '%s/%s/'.", m->where, where, m->where); + } + + /* If we have a tmpfs root, the above might have created mount point inodes. Hence we left the tmpfs + * writable for that. Let's fix that now. Also, let's enable propagation for the future. (Reminder: + * we disconnect propagation from the host, but we *want* propagation by default for everything + * created further down the tree. Hence we'll set MS_SHARED here right-away.) */ + if (mount_setattr(root_fd, "", AT_EMPTY_PATH|AT_RECURSIVE, + &(struct mount_attr) { + .attr_set = FLAGS_SET(flags, MSTACK_RDONLY) ? MOUNT_ATTR_RDONLY : 0, + .attr_clr = FLAGS_SET(flags, MSTACK_RDONLY) ? 0 : MOUNT_ATTR_RDONLY, + .propagation = MS_SHARED, + }, sizeof(struct mount_attr)) < 0) + return log_debug_errno(errno, "Failed to mark root bind mount read-only: %m"); + + if (ret_root_fd) + *ret_root_fd = TAKE_FD(root_fd); + + return 0; +} + +int mstack_apply( + const char *dir, + int dir_fd, + const char *where, + const char *temp_mount_dir, + int userns_fd, + const ImagePolicy *image_policy, + const ImageFilter *image_filter, + MStackFlags flags, + int *ret_root_fd) { + int r; + + assert(where); + + _cleanup_(mstack_done) MStack mstack = MSTACK_INIT; + r = mstack_load_now(&mstack, dir, dir_fd, flags); + if (r < 0) + return r; + + r = mstack_open_images(&mstack, userns_fd, image_policy, image_filter, flags); + if (r < 0) + return r; + + _cleanup_(rmdir_and_freep) char *t = NULL; + if (!temp_mount_dir) { + r = mkdtemp_malloc("/tmp/mstack-temporary-XXXXXX", &t); + if (r < 0) + return r; + + temp_mount_dir = t; + } + + r = mstack_make_mounts(&mstack, temp_mount_dir, flags); + if (r < 0) + return r; + + return mstack_bind_mounts(&mstack, where, /* where_fd= */ -EBADF, flags, ret_root_fd); +} + +int mstack_load(const char *dir, + int dir_fd, + MStack **ret) { + + int r; + + /* Well-known errors: + * + * -ENOTUNIQ → Multiple conflicting layers for the same path defined + * -EBADMSG → Bad file suffix, inode type for layer, or unrecognized entry + */ + + MStack *mstack = new(MStack, 1); + if (!mstack) + return -ENOMEM; + + *mstack = MSTACK_INIT; + + r = mstack_load_now(mstack, dir, dir_fd, /* flags= */ 0); + if (r < 0) + return r; + + if (ret) + *ret = TAKE_PTR(mstack); + + return 0; +} + +int mstack_is_read_only(MStack *mstack) { + assert(mstack); + + /* Checks if the mstack consists of only read-only layers and bind mounts */ + + if (mstack->has_tmpfs_root) + return false; + + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) + if (IN_SET(m->mount_type, MSTACK_ROOT, MSTACK_RW, MSTACK_BIND)) + return false; + + return true; +} + +int mstack_is_foreign_uid_owned(MStack *mstack) { + int r; + + assert(mstack); + + /* Checks if any of the layers are owned by the host's foreign UID range */ + + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { + + if (!IN_SET(m->image_type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME)) + continue; + + assert(m->what_fd >= 0); + + struct stat st; + if (fstat(m->what_fd, &st) < 0) + return -errno; + + r = stat_verify_directory(&st); + if (r < 0) + return r; + + if (uid_is_foreign(st.st_uid)) + return true; + } + + return false; +} + +static const char *const mstack_mount_type_table[] = { + [MSTACK_ROOT] = "root", + [MSTACK_LAYER] = "layer", + [MSTACK_RW] = "rw", + [MSTACK_BIND] = "bind", + [MSTACK_ROBIND] = "robind", +}; + +DEFINE_STRING_TABLE_LOOKUP_TO_STRING(mstack_mount_type, MStackMountType); diff --git a/src/shared/mstack.h b/src/shared/mstack.h new file mode 100644 index 0000000000..e526f17d98 --- /dev/null +++ b/src/shared/mstack.h @@ -0,0 +1,64 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "discover-image.h" +#include "shared-forward.h" + +typedef enum MStackFlags { + MSTACK_MKDIR = 1 << 0, /* when mounting, create top-level inode to mount on top */ + MSTACK_RDONLY = 1 << 1, +} MStackFlags; + +typedef enum MStackMountType { + MSTACK_ROOT, /* optional "root" entry used as root, with the layer@/rw layers only used for /usr/ */ + MSTACK_LAYER, /* "layer@…" entries that are the lower (read-only) layers of an overlayfs stack */ + MSTACK_RW, /* "rw" entry that is the upper (writable) layer of an overlayfs stack (contains two subdirs: 'data' + 'work') */ + MSTACK_BIND, /* "bind@…" entries that are (writable) bind mounted on top of the overlayfs */ + MSTACK_ROBIND, /* "robind@…" similar, but read-only */ + _MSTACK_MOUNT_TYPE_MAX, + _MSTACK_MOUNT_TYPE_INVALID = -EINVAL, +} MStackMountType; + +typedef struct MStackMount { + MStackMountType mount_type; + char *what; + int what_fd; + int mount_fd; + char *sort_key; + char *where; + ImageType image_type; + DissectedImage *dissected_image; +} MStackMount; + +typedef struct MStack { + char *path; + MStackMount *mounts; + size_t n_mounts; + bool has_tmpfs_root; /* If true, we need a throw-away tmpfs as root */ + bool has_overlayfs; /* Indicates whether we need overlayfs (i.e. if there are more than a single layer */ + MStackMount *root_mount; /* If there's a MOUNT_BIND/MOUNT_ROBIND/MOUNT_ROOT mount, this points to it */ + int root_mount_fd; + int usr_mount_fd; +} MStack; + +#define MSTACK_INIT \ + (MStack) { \ + .root_mount_fd = -EBADF, \ + .usr_mount_fd = -EBADF, \ + } + +MStack *mstack_free(MStack *mstack); +DEFINE_TRIVIAL_CLEANUP_FUNC(MStack*, mstack_free); + +int mstack_load(const char *dir, int dir_fd, MStack **ret); +int mstack_open_images(MStack *mstack, int userns_fd, const ImagePolicy *image_policy, const ImageFilter *image_filter, MStackFlags flags); +int mstack_make_mounts(MStack *mstack, const char *temp_mount_dir, MStackFlags flags); +int mstack_bind_mounts(MStack *mstack, const char *where, int where_fd, MStackFlags flags, int *ret_root_fd); + +/* The four calls above in one */ +int mstack_apply(const char *dir, int dir_fd, const char *where, const char *temp_mount_dir, int userns_fd, const ImagePolicy *image_policy, const ImageFilter *image_filter, MStackFlags flags, int *ret_root_fd); + +int mstack_is_read_only(MStack *mstack); +int mstack_is_foreign_uid_owned(MStack *mstack); + +DECLARE_STRING_TABLE_LOOKUP_TO_STRING(mstack_mount_type, MStackMountType); diff --git a/src/shared/shared-forward.h b/src/shared/shared-forward.h index f35287746b..82bdf86330 100644 --- a/src/shared/shared-forward.h +++ b/src/shared/shared-forward.h @@ -53,6 +53,7 @@ typedef struct Condition Condition; typedef struct ConfigSection ConfigSection; typedef struct ConfigTableItem ConfigTableItem; typedef struct CPUSet CPUSet; +typedef struct DissectedImage DissectedImage; typedef struct DnsAnswer DnsAnswer; typedef struct DnsPacket DnsPacket; typedef struct DnsQuestion DnsQuestion; @@ -64,6 +65,7 @@ typedef struct FDSet FDSet; typedef struct Fido2HmacSalt Fido2HmacSalt; typedef struct GroupRecord GroupRecord; typedef struct Image Image; +typedef struct ImageFilter ImageFilter; typedef struct ImagePolicy ImagePolicy; typedef struct InstallChange InstallChange; typedef struct InstallInfo InstallInfo; diff --git a/src/test/meson.build b/src/test/meson.build index 3ca614db36..adbcd3c0d4 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -149,6 +149,7 @@ simple_tests += files( 'test-mkdir.c', 'test-modhex.c', 'test-mountpoint-util.c', + 'test-mstack.c', 'test-namespace-util.c', 'test-net-naming-scheme.c', 'test-notify-recv.c', diff --git a/src/test/test-mstack.c b/src/test/test-mstack.c new file mode 100644 index 0000000000..15400af511 --- /dev/null +++ b/src/test/test-mstack.c @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "capability-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "fs-util.h" +#include "mountpoint-util.h" +#include "mstack.h" +#include "path-util.h" +#include "process-util.h" +#include "rm-rf.h" +#include "tests.h" +#include "tmpfile-util.h" +#include "virt.h" + +static bool overlayfs_set_fd_lowerdir_plus_supported(void) { + int r; + + _cleanup_close_ int sb_fd = fsopen("overlay", FSOPEN_CLOEXEC); + if (sb_fd < 0 && (ERRNO_IS_NOT_SUPPORTED(errno) || errno == ENODEV)) + return false; + ASSERT_OK_ERRNO(sb_fd); + + _cleanup_close_ int layer_fd = open("/", O_DIRECTORY|O_CLOEXEC); + ASSERT_OK_ERRNO(layer_fd); + + r = RET_NERRNO(fsconfig(sb_fd, FSCONFIG_SET_FD, "lowerdir+", /* value= */ NULL, layer_fd)); + if (r < 0 && (ERRNO_IS_NEG_NOT_SUPPORTED(r) || r == -EINVAL)) + return false; + + ASSERT_OK_ERRNO(r); + return true; +} + +TEST(mstack) { + _cleanup_(rm_rf_physical_and_freep) char *t = NULL; + _cleanup_close_ int tfd = -EBADF; + int r; + + tfd = mkdtemp_open("/tmp/mstack-what-XXXXXX", O_PATH, &t); + ASSERT_OK(tfd); + + ASSERT_OK_ERRNO(mkdirat(tfd, "rw", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "rw/data", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "rw/data/check1", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "layer@0", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "layer@0/check2", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "layer@0/zzz", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "layer@1", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "layer@1/check3", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "layer@0/yyy", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "bind@zzz", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "bind@zzz/check4", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "robind@yyy", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "robind@yyy/check5", 0755)); + + _cleanup_(mstack_freep) MStack *mstack = NULL; + ASSERT_OK(mstack_load(t, tfd, &mstack)); + + ASSERT_OK_ZERO(mstack_is_read_only(mstack)); + ASSERT_OK_ZERO(mstack_is_foreign_uid_owned(mstack)); + + if (!have_effective_cap(CAP_SYS_ADMIN)) + return (void) log_tests_skipped("not attaching mstack, lacking privs"); + if (!mount_new_api_supported()) + return (void) log_tests_skipped("kernel does not support new mount API, skipping mstack attachment test."); + if (!overlayfs_set_fd_lowerdir_plus_supported()) + return (void) log_tests_skipped("overlayfs does not support FSCONFIG_SET_FD with lowerdir+, skipping mstack attachment test."); + if (running_in_chroot() > 0) /* we cannot disable mount prop if we are in a chroot without the root inode being a proper mount point */ + return (void) log_tests_skipped("running in chroot(), skipping mstack attachment test."); + + mstack = mstack_free(mstack); + + /* For with a new mountns */ + r = pidref_safe_fork("(mstack-test", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT|FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE, /* ret= */ NULL); + ASSERT_OK(r); + + if (r == 0) { + MStackFlags flags = 0; + + /* Close the original temporary fd, it still points to an inode of the original mountns, + * which we cannot use to generate mounts from */ + tfd = safe_close(tfd); + + { + ASSERT_OK(mstack_load(t, -EBADF, &mstack)); + + ASSERT_OK(mstack_open_images( + mstack, + /* userns_fd= */ -EBADF, + /* image_policy= */ NULL, + /* image_filter= */ NULL, + flags)); + + _cleanup_(rmdir_and_freep) char *m = NULL; + ASSERT_OK(mkdtemp_malloc("/tmp/mstack-temporary-XXXXXX", &m)); + + ASSERT_OK(mstack_make_mounts(mstack, m, flags)); + + _cleanup_(rmdir_and_freep) char *w = NULL; + ASSERT_OK(mkdtemp_malloc("/tmp/mstack-where-XXXXXX", &w)); + + _cleanup_close_ int rfd = -EBADF; + ASSERT_OK(mstack_bind_mounts(mstack, w, /* where_fd= */ -EBADF, flags, &rfd)); + + _cleanup_close_ int ofd = open(w, O_PATH|O_CLOEXEC); + ASSERT_OK_ERRNO(ofd); + + ASSERT_OK_ERRNO(faccessat(ofd, "check1", F_OK, AT_SYMLINK_NOFOLLOW)); + ASSERT_OK_ERRNO(faccessat(ofd, "check2/", F_OK, AT_SYMLINK_NOFOLLOW)); + ASSERT_OK_ERRNO(faccessat(ofd, "check3/", F_OK, AT_SYMLINK_NOFOLLOW)); + ASSERT_OK_ERRNO(faccessat(ofd, "zzz/check4/", F_OK, AT_SYMLINK_NOFOLLOW)); + ASSERT_OK_ERRNO(faccessat(ofd, "yyy/check5/", F_OK, AT_SYMLINK_NOFOLLOW)); + + _cleanup_free_ char *j = ASSERT_PTR(path_join(w, "zzz")); + ASSERT_OK_ERRNO(umount2(j, MNT_DETACH)); + _cleanup_free_ char *jj = ASSERT_PTR(path_join(w, "yyy")); + ASSERT_OK_ERRNO(umount2(jj, MNT_DETACH)); + ASSERT_OK_ERRNO(umount2(w, MNT_DETACH)); + } + + mstack = mstack_free(mstack); + + _exit(EXIT_SUCCESS); + } +} + +DEFINE_TEST_MAIN(LOG_INFO); From 944161d0d4cb4b9fec0de323183448dcf89b0cec Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 18 Nov 2025 22:33:59 +0100 Subject: [PATCH 14/36] vpick: add generic definition for mstack image pick filters --- src/shared/vpick.c | 8 ++++++++ src/shared/vpick.h | 2 ++ 2 files changed, 10 insertions(+) diff --git a/src/shared/vpick.c b/src/shared/vpick.c index 69e8be1b16..38ceb225cd 100644 --- a/src/shared/vpick.c +++ b/src/shared/vpick.c @@ -798,3 +798,11 @@ const PickFilter pick_filter_image_dir[1] = { .architecture = _ARCHITECTURE_INVALID, }, }; + +const PickFilter pick_filter_image_mstack[1] = { + { + .type_mask = UINT32_C(1) << DT_DIR, + .architecture = _ARCHITECTURE_INVALID, + .suffix = ".mstack", + }, +}; diff --git a/src/shared/vpick.h b/src/shared/vpick.h index 1ebcd5c8ab..55596d24df 100644 --- a/src/shared/vpick.h +++ b/src/shared/vpick.h @@ -64,8 +64,10 @@ int path_uses_vpick(const char *path); extern const PickFilter pick_filter_image_raw[1]; extern const PickFilter pick_filter_image_dir[1]; +extern const PickFilter pick_filter_image_mstack[1]; #define pick_filter_image_any (const PickFilter[]) { \ pick_filter_image_raw[0], \ + pick_filter_image_mstack[0], \ pick_filter_image_dir[0], \ } From 8187cd18d61c9459f2fdb7591c9eb7c73afea24d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 10 Nov 2025 12:13:11 +0100 Subject: [PATCH 15/36] add mstack tool for accessing mstacks from the command line --- meson.build | 1 + src/mstack/meson.build | 13 ++ src/mstack/mstack-tool.c | 436 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 450 insertions(+) create mode 100644 src/mstack/meson.build create mode 100644 src/mstack/mstack-tool.c diff --git a/meson.build b/meson.build index bf419b7f60..61a2c253ee 100644 --- a/meson.build +++ b/meson.build @@ -2379,6 +2379,7 @@ subdir('src/measure') subdir('src/modules-load') subdir('src/mount') subdir('src/mountfsd') +subdir('src/mstack') subdir('src/mute-console') subdir('src/network') subdir('src/notify') diff --git a/src/mstack/meson.build b/src/mstack/meson.build new file mode 100644 index 0000000000..27a618f371 --- /dev/null +++ b/src/mstack/meson.build @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +executables += [ + executable_template + { + 'name' : 'systemd-mstack', + 'public' : true, + 'sources' : files('mstack-tool.c'), + }, +] + +install_symlink('mount.mstack', + pointing_to : sbin_to_bin + 'systemd-mstack', + install_dir : sbindir) diff --git a/src/mstack/mstack-tool.c b/src/mstack/mstack-tool.c new file mode 100644 index 0000000000..01a71d6e56 --- /dev/null +++ b/src/mstack/mstack-tool.c @@ -0,0 +1,436 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include + +#include "argv-util.h" +#include "build.h" +#include "chase.h" +#include "errno-util.h" +#include "extract-word.h" +#include "fd-util.h" +#include "format-table.h" +#include "image-policy.h" +#include "main-func.h" +#include "mount-util.h" +#include "mountpoint-util.h" +#include "mstack.h" +#include "parse-argument.h" +#include "pretty-print.h" +#include "string-util.h" + +static enum { + ACTION_INSPECT, + ACTION_MOUNT, + ACTION_UMOUNT, +} arg_action = ACTION_INSPECT; +static char *arg_what = NULL; +static char *arg_where = NULL; +static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF; +static PagerFlags arg_pager_flags = 0; +static int arg_legend = true; +static MStackFlags arg_mstack_flags = 0; +static bool arg_rmdir = false; +static ImagePolicy *arg_image_policy = NULL; +static ImageFilter *arg_image_filter = NULL; + +STATIC_DESTRUCTOR_REGISTER(arg_what, freep); +STATIC_DESTRUCTOR_REGISTER(arg_where, freep); +STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); +STATIC_DESTRUCTOR_REGISTER(arg_image_filter, image_filter_freep); + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-mstack", "1", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] WHAT\n" + "%1$s [OPTIONS...] --mount WHAT WHERE\n" + "%1$s [OPTIONS...] --umount WHERE\n" + "\n%5$sInspect or apply mount stack.%6$s\n\n" + "%3$sOptions:%4$s\n" + " --no-pager Do not pipe output into a pager\n" + " --no-legend Do not print the column headers\n" + " --json=pretty|short|off Generate JSON output\n" + " -r --read-only Mount read-only\n" + " --mkdir Make mount directory before mounting, if missing\n" + " --rmdir Remove mount directory after unmounting\n" + " --image-policy=POLICY\n" + " Specify image dissection policy\n" + " --image-filter=FILTER\n" + " Specify image dissection filter\n" + "\n%3$sCommands:%4$s\n" + " -h --help Show this help\n" + " --version Show package version\n" + " -m --mount Mount the mstack to the specified directory\n" + " -M Shortcut for --mount --mkdir\n" + " -u --umount Unmount the image from the specified directory\n" + " -U Shortcut for --umount --rmdir\n" + "\nSee the %2$s for details.\n", + program_invocation_short_name, + link, + ansi_underline(), ansi_normal(), + ansi_highlight(), ansi_normal()); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_NO_PAGER, + ARG_NO_LEGEND, + ARG_JSON, + ARG_MKDIR, + ARG_RMDIR, + ARG_IMAGE_POLICY, + ARG_IMAGE_FILTER, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, + { "mount", no_argument, NULL, 'm' }, + { "umount", no_argument, NULL, 'u' }, + { "json", required_argument, NULL, ARG_JSON }, + { "read-only", no_argument, NULL, 'r' }, + { "rmdir", no_argument, NULL, ARG_RMDIR }, + { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, + { "image-filter", required_argument, NULL, ARG_IMAGE_FILTER }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hmMuUr", options, NULL)) >= 0) { + + switch (c) { + + case 'h': + return help(); + + case ARG_VERSION: + return version(); + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_NO_LEGEND: + arg_legend = false; + break; + + case ARG_JSON: + r = parse_json_argument(optarg, &arg_json_format_flags); + if (r <= 0) + return r; + + break; + + case 'r': + arg_mstack_flags |= MSTACK_RDONLY; + break; + + case ARG_IMAGE_POLICY: + r = parse_image_policy_argument(optarg, &arg_image_policy); + if (r < 0) + return r; + break; + + case ARG_IMAGE_FILTER: { + _cleanup_(image_filter_freep) ImageFilter *f = NULL; + r = image_filter_parse(optarg, &f); + if (r < 0) + return log_error_errno(r, "Failed to parse image filter expression: %s", optarg); + + image_filter_free(arg_image_filter); + arg_image_filter = TAKE_PTR(f); + break; + } + + case ARG_MKDIR: + arg_mstack_flags |= MSTACK_MKDIR; + break; + + case ARG_RMDIR: + arg_rmdir = true; + break; + + case 'm': + arg_action = ACTION_MOUNT; + break; + + case 'M': + /* Shortcut combination of --mkdir + --mount */ + arg_action = ACTION_MOUNT; + arg_mstack_flags |= MSTACK_MKDIR; + break; + + case 'u': + arg_action = ACTION_UMOUNT; + break; + + case 'U': + /* Shortcut combination of --rmdir + --umount */ + arg_action = ACTION_UMOUNT; + arg_rmdir = true; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + } + + switch (arg_action) { + + case ACTION_INSPECT: + if (optind + 1 != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected one argument."); + + r = parse_path_argument(argv[optind], /* suppress_root= */ false, &arg_what); + if (r < 0) + return r; + + break; + + case ACTION_MOUNT: + if (optind + 2 != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected two arguments."); + + r = parse_path_argument(argv[optind], /* suppress_root= */ false, &arg_what); + if (r < 0) + return r; + + r = parse_path_argument(argv[optind+1], /* suppress_root= */ false, &arg_where); + if (r < 0) + return r; + + break; + + case ACTION_UMOUNT: + if (optind + 1 != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected one argument."); + + r = parse_path_argument(argv[optind], /* suppress_root= */ false, &arg_where); + if (r < 0) + return r; + + break; + + default: + assert_not_reached(); + } + + return 1; +} + +static int parse_argv_as_mount_helper(int argc, char *argv[]) { + const char *options = NULL; + bool fake = false; + int c, r; + + /* Implements util-linux "external helper" command line interface, as per mount(8) man page. */ + + while ((c = getopt(argc, argv, "sfnvN:o:t:")) >= 0) { + switch (c) { + + case 'f': + fake = true; + break; + + case 'o': + options = optarg; + break; + + case 't': + if (!streq(optarg, "mstack")) + log_debug("Unexpected file system type '%s', ignoring.", optarg); + break; + + case 's': /* sloppy mount options */ + case 'n': /* aka --no-mtab */ + case 'v': /* aka --verbose */ + log_debug("Ignoring option -%c, not implemented.", c); + break; + + case 'N': /* aka --namespace= */ + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Option -%c is not implemented, refusing.", c); + + case '?': + return -EINVAL; + } + } + + if (optind + 2 != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Expected an image file path and target directory as only argument."); + + for (const char *p = options;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&p, &word, ",", EXTRACT_KEEP_QUOTE); + if (r < 0) + return log_error_errno(r, "Failed to extract mount option: %m"); + if (r == 0) + break; + + if (streq(word, "ro")) + SET_FLAG(arg_mstack_flags, MSTACK_RDONLY, true); + else if (streq(word, "rw")) + SET_FLAG(arg_mstack_flags, MSTACK_RDONLY, false); + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unknown mount option '%s'.", word); + } + + if (fake) + return 0; + + r = parse_path_argument(argv[optind], /* suppress_root= */ false, &arg_what); + if (r < 0) + return r; + + r = parse_path_argument(argv[optind+1], /* suppress_root= */ false, &arg_where); + if (r < 0) + return r; + + arg_action = ACTION_MOUNT; + return 1; +} + +static int inspect_mstack(void) { + _cleanup_(mstack_freep) MStack *mstack = NULL; + int r; + + assert(arg_what); + + r = mstack_load(arg_what, /* dir_fd= */ -EBADF, &mstack); + if (r < 0) + return log_debug_errno(r, "Failed to load .mstack/ directory '%s': %m", arg_what); + + _cleanup_(table_unrefp) Table *t = NULL; + + t = table_new("type", "name", "image", "what", "where", "sort"); + if (!t) + return log_oom(); + + table_set_ersatz_string(t, TABLE_ERSATZ_DASH); + + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { + _cleanup_free_ char *w = NULL; + r = fd_get_path(m->what_fd, &w); + if (r < 0) + return log_error_errno(r, "Failed to get path of what file descriptor: %m"); + + r = table_add_many( + t, + TABLE_STRING, mstack_mount_type_to_string(m->mount_type), + TABLE_STRING, m->what, + TABLE_STRING, image_type_to_string(m->image_type), + TABLE_PATH, w, + TABLE_PATH, m->where ?: ((mstack->root_mount && mstack->root_mount != m) ? "/usr" : "/"), + TABLE_STRING, m->sort_key); + if (r < 0) + return table_log_add_error(r); + } + + return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend); +} + +static int mount_mstack(void) { + int r; + + assert(arg_what); + assert(arg_where); + + r = mstack_apply( + arg_what, + /* dir_fd= */ -EBADF, + arg_where, + /* temp_mount_dir= */ NULL, /* auto-create temporary directory */ + /* userns_fd= */ -EBADF, + arg_image_policy, + arg_image_filter, + arg_mstack_flags, + /* ret_root_fd= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to apply .mstack/ directory '%s': %m", arg_what); + + return 0; +} + +static int umount_mstack(void) { + int r; + + assert(arg_where); + + _cleanup_free_ char *canonical = NULL; + _cleanup_close_ int fd = chase_and_open(arg_where, /* root= */ NULL, /* chase_flags= */ 0, O_DIRECTORY, &canonical); + if (fd == -ENOTDIR) + return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "'%s' is not a directory", arg_where); + if (fd < 0) + return log_error_errno(fd, "Failed to resolve path '%s': %m", arg_where); + + r = is_mount_point_at(fd, /* path= */ NULL, /* flags= */ 0); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "'%s' is not a mount point", canonical); + if (r < 0) + return log_error_errno(r, "Failed to determine whether '%s' is a mount point: %m", canonical); + + fd = safe_close(fd); + + r = umount_recursive(canonical, 0); + if (r < 0) + return log_error_errno(r, "Failed to unmount '%s': %m", canonical); + + if (arg_rmdir) { + r = RET_NERRNO(rmdir(canonical)); + if (r < 0) + return log_error_errno(r, "Failed to remove mount directory '%s': %m", canonical); + } + + return 0; +} + +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + + if (invoked_as(argv, "mount.mstack")) + r = parse_argv_as_mount_helper(argc, argv); + else + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + switch (arg_action) { + + case ACTION_INSPECT: + return inspect_mstack(); + + case ACTION_MOUNT: + return mount_mstack(); + + case ACTION_UMOUNT: + return umount_mstack(); + + default: + assert_not_reached(); + } +} + +DEFINE_MAIN_FUNCTION(run); From 8cd29712cb86049a648dd5e2ccd2bb6bb007b047 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 12 Nov 2025 16:47:57 +0100 Subject: [PATCH 16/36] discover-image: add support for discovering mstack images --- src/shared/discover-image.c | 75 ++++++++++++++++++++++++++++++++++--- src/shared/discover-image.h | 1 + 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index 8352e5a821..064a3e8002 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -36,6 +36,7 @@ #include "log.h" #include "loop-util.h" #include "mountpoint-util.h" +#include "mstack.h" #include "namespace-util.h" #include "nsresource.h" #include "nulstr-util.h" @@ -478,12 +479,67 @@ static int image_make( on_mount_id = _mnt_id; if (S_ISDIR(st->st_mode)) { - unsigned file_attr = 0; - usec_t crtime = 0; if (!ret) return 0; + if (endswith(path, ".mstack")) { + usec_t crtime = 0; + r = fd_getcrtime(fd, &crtime); + if (r < 0) + log_debug_errno(r, "Unable to read creation time of '%s', ignoring: %m", path); + + if (!pretty) { + r = extract_image_basename( + path, + image_class_suffix_to_string(c), + STRV_MAKE(".mstack"), + &pretty_buffer, + /* ret_suffix= */ NULL); + if (r < 0) + return r; + + pretty = pretty_buffer; + } + + _cleanup_(mstack_freep) MStack *mstack = NULL; + r = mstack_load(path, fd, &mstack); + if (r < 0) { + log_debug_errno(r, "Failed to load mstack '%s', ignoring: %m", path); + read_only = true; + } else if (!read_only) { + r = mstack_is_read_only(mstack); + if (r < 0) + log_debug_errno(r, "Failed to determine if mstack '%s' is read-only, assuming it is: %m", path); + + read_only = r != 0; + } + + r = image_new(IMAGE_MSTACK, + c, + pretty, + path, + read_only, + crtime, + /* mtime= */ 0, + fh, + on_mount_id, + (uint64_t) st->st_ino, + ret); + if (r < 0) + return r; + + if (mstack) { + r = mstack_is_foreign_uid_owned(mstack); + if (r < 0) + log_debug_errno(r, "Failed to determine if mstack '%s' is foreign UID owned, assuming it is not: %m", path); + if (r > 0) + (*ret)->foreign_uid_owned = true; + } + + return 0; + } + if (!pretty) { r = extract_image_basename( path, @@ -532,10 +588,12 @@ static int image_make( } /* Get directory creation time (not available everywhere, but that's OK */ + usec_t crtime = 0; (void) fd_getcrtime(fd, &crtime); /* If the IMMUTABLE bit is set, we consider the directory read-only. Since the ioctl is not * supported everywhere we ignore failures. */ + unsigned file_attr = 0; (void) read_attr_fd(fd, &file_attr); /* It's just a normal directory. */ @@ -767,7 +825,7 @@ static char** make_possible_filenames(ImageClass class, const char *image_name) assert(image_name); FOREACH_STRING(v_suffix, "", ".v") - FOREACH_STRING(format_suffix, "", ".raw") { + FOREACH_STRING(format_suffix, "", ".raw", ".mstack") { _cleanup_free_ char *j = NULL; const char *class_suffix; @@ -864,6 +922,12 @@ int image_find(RuntimeScope scope, log_debug("Ignoring non-regular file '%s' with .raw suffix.", fname); continue; } + } else if (endswith(fname, ".mstack")) { + + if (!S_ISDIR(st.st_mode)) { + log_debug("Ignoring non-directory '%s' with .mstack suffix.", fname); + continue; + } } else if (endswith(fname, ".v")) { @@ -1088,7 +1152,7 @@ int image_discover( r = extract_image_basename( nov, image_class_suffix_to_string(class), - STRV_MAKE(".raw", ""), + STRV_MAKE(".raw", ".mstack", ""), &pretty, &suffix); if (r < 0) { @@ -1139,7 +1203,7 @@ int image_discover( r = extract_image_basename( fname, image_class_suffix_to_string(class), - /* format_suffixes= */ NULL, + STRV_MAKE(".mstack", ""), &pretty, /* ret_suffix= */ NULL); if (r < 0) { @@ -2367,6 +2431,7 @@ static const char* const image_type_table[_IMAGE_TYPE_MAX] = { [IMAGE_SUBVOLUME] = "subvolume", [IMAGE_RAW] = "raw", [IMAGE_BLOCK] = "block", + [IMAGE_MSTACK] = "mstack", }; DEFINE_STRING_TABLE_LOOKUP(image_type, ImageType); diff --git a/src/shared/discover-image.h b/src/shared/discover-image.h index c2c1a5e93d..5dac81b9c5 100644 --- a/src/shared/discover-image.h +++ b/src/shared/discover-image.h @@ -11,6 +11,7 @@ typedef enum ImageType { IMAGE_SUBVOLUME, IMAGE_RAW, IMAGE_BLOCK, + IMAGE_MSTACK, _IMAGE_TYPE_MAX, _IMAGE_TYPE_INVALID = -EINVAL, } ImageType; From 1bc237dd7a1bb3762a2cfc60ce959d4dbf520b85 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 12 Nov 2025 16:46:59 +0100 Subject: [PATCH 17/36] nspawn: add support for running mstack container images --- src/nspawn/nspawn.c | 177 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 152 insertions(+), 25 deletions(-) diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 296b12bb90..623de6d932 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -69,6 +69,7 @@ #include "mkdir.h" #include "mount-util.h" #include "mountpoint-util.h" +#include "mstack.h" #include "namespace-util.h" #include "netlink-internal.h" #include "notify-recv.h" @@ -204,6 +205,7 @@ struct ether_addr arg_network_provided_mac = {}; static PagerFlags arg_pager_flags = 0; static unsigned long arg_personality = PERSONALITY_INVALID; static char *arg_image = NULL; +static char *arg_mstack = NULL; static char *arg_oci_bundle = NULL; static VolatileMode arg_volatile_mode = VOLATILE_NO; static ExposePort *arg_expose_ports = NULL; @@ -272,6 +274,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_network_bridge, freep); STATIC_DESTRUCTOR_REGISTER(arg_network_zone, freep); STATIC_DESTRUCTOR_REGISTER(arg_network_namespace_path, freep); STATIC_DESTRUCTOR_REGISTER(arg_image, freep); +STATIC_DESTRUCTOR_REGISTER(arg_mstack, freep); STATIC_DESTRUCTOR_REGISTER(arg_oci_bundle, freep); STATIC_DESTRUCTOR_REGISTER(arg_property, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_property_message, sd_bus_message_unrefp); @@ -738,6 +741,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_BACKGROUND, ARG_CLEANUP, ARG_NO_ASK_PASSWORD, + ARG_MSTACK, }; static const struct option options[] = { @@ -817,6 +821,7 @@ static int parse_argv(int argc, char *argv[]) { { "background", required_argument, NULL, ARG_BACKGROUND }, { "cleanup", no_argument, NULL, ARG_CLEANUP }, { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD }, + { "mstack", required_argument, NULL, ARG_MSTACK }, {} }; @@ -863,6 +868,14 @@ static int parse_argv(int argc, char *argv[]) { arg_settings_mask |= SETTING_DIRECTORY; break; + case ARG_MSTACK: + r = parse_path_argument(optarg, false, &arg_mstack); + if (r < 0) + return r; + + arg_settings_mask |= SETTING_DIRECTORY; + break; + case ARG_OCI_BUNDLE: r = parse_path_argument(optarg, false, &arg_oci_bundle); if (r < 0) @@ -1654,11 +1667,11 @@ static int verify_arguments(void) { if (has_custom_root_mount(arg_custom_mounts, arg_n_custom_mounts)) arg_read_only = true; - if (arg_directory && arg_image) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--directory= and --image= may not be combined."); + if (!!arg_directory + !!arg_image + !!arg_mstack > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--directory=, --image= --mstack= may not be combined."); - if (arg_template && arg_image) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--template= and --image= may not be combined."); + if (arg_template && (arg_image || arg_mstack)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--template= and --image=/--mstack= may not be combined."); if (arg_template && !(arg_directory || arg_machine)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--template= needs --directory= or --machine=."); @@ -1666,6 +1679,9 @@ static int verify_arguments(void) { if (arg_ephemeral && arg_template) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--ephemeral and --template= may not be combined."); + if (arg_ephemeral && arg_mstack) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--ephemeral and --mstack= may not be combined."); + /* Permit --ephemeral with --link-journal=try-* to satisfy principle of the least astonishment * (by common sense, "try" means "do not fail if not possible") */ if (arg_ephemeral && !IN_SET(arg_link_journal, LINK_NO, LINK_AUTO) && !arg_link_journal_try) @@ -3054,6 +3070,24 @@ static int pick_paths(void) { arg_architecture = result.architecture; } + if (arg_mstack) { + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + PickFilter filter = *pick_filter_image_mstack; + + filter.architecture = arg_architecture; + + r = path_pick_update_warn( + &arg_mstack, + &filter, + /* n_filters= */ 1, + PICK_ARCHITECTURE|PICK_TRIES, + &result); + if (r < 0) + return r; + + arg_architecture = result.architecture; + } + if (arg_template) { _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; PickFilter filter = *pick_filter_image_dir; @@ -3088,7 +3122,7 @@ static int determine_names(void) { return log_oom(); } - if (!arg_image && !arg_directory) { + if (!arg_image && !arg_directory && !arg_mstack) { if (arg_machine) { _cleanup_(image_unrefp) Image *i = NULL; @@ -3099,10 +3133,24 @@ static int determine_names(void) { if (r < 0) return log_error_errno(r, "Failed to find image for machine '%s': %m", arg_machine); - if (IN_SET(i->type, IMAGE_RAW, IMAGE_BLOCK)) + switch (i->type) { + case IMAGE_RAW: + case IMAGE_BLOCK: r = free_and_strdup(&arg_image, i->path); - else + break; + + case IMAGE_DIRECTORY: + case IMAGE_SUBVOLUME: r = free_and_strdup(&arg_directory, i->path); + break; + + case IMAGE_MSTACK: + r = free_and_strdup(&arg_mstack, i->path); + break; + + default: + assert_not_reached(); + } if (r < 0) return log_oom(); @@ -3114,31 +3162,40 @@ static int determine_names(void) { return log_error_errno(r, "Failed to determine current directory: %m"); } - if (!arg_directory && !arg_image) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine path, please use -D or -i."); + if (!arg_directory && !arg_image && !arg_mstack) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine path, please use --directory=, --image= or --mstack=."); } if (!arg_machine) { - if (arg_directory && path_equal(arg_directory, "/")) { - arg_machine = gethostname_malloc(); - if (!arg_machine) - return log_oom(); + if (arg_directory) { + if (path_equal(arg_directory, "/")) { + arg_machine = gethostname_malloc(); + if (!arg_machine) + return log_oom(); + } else { + r = path_extract_filename(arg_directory, &arg_machine); + if (r < 0) + return log_error_errno(r, "Failed to extract file name from '%s': %m", arg_directory); + } } else if (arg_image) { - char *e; - r = path_extract_filename(arg_image, &arg_machine); if (r < 0) return log_error_errno(r, "Failed to extract file name from '%s': %m", arg_image); /* Truncate suffix if there is one */ - e = endswith(arg_machine, ".raw"); + char *e = endswith(arg_machine, ".raw"); if (e) *e = 0; - } else { - r = path_extract_filename(arg_directory, &arg_machine); + } else if (arg_mstack) { + r = path_extract_filename(arg_mstack, &arg_machine); if (r < 0) - return log_error_errno(r, "Failed to extract file name from '%s': %m", arg_directory); - } + return log_error_errno(r, "Failed to extract file name from '%s': %m", arg_mstack); + + char *e = endswith(arg_machine, ".mstack"); + if (e) + *e = 0; + } else + assert_not_reached(); hostname_cleanup(arg_machine); if (!hostname_is_valid(arg_machine, 0)) @@ -3873,6 +3930,7 @@ static int outer_child( const char *directory, int mount_fd, DissectedImage *dissected_image, + MStack *mstack, int fd_outer_socket, int fd_inner_socket, FDSet *fds, @@ -3928,6 +3986,7 @@ static int outer_child( if (mount_fd >= 0) { assert(arg_directory); assert(!arg_image); + assert(!arg_mstack); if (move_mount(mount_fd, "", AT_FDCWD, directory, MOVE_MOUNT_F_EMPTY_PATH) < 0) return log_error_errno(errno, "Failed to attach root directory: %m"); @@ -3938,6 +3997,7 @@ static int outer_child( } else if (dissected_image) { assert(!arg_directory); assert(arg_image); + assert(!arg_mstack); /* If we are operating on a disk image, then mount its root directory now, but leave out the * rest. We can read the UID shift from it if we need to. Further down we'll mount the rest, @@ -3955,9 +4015,38 @@ static int outer_child( (arg_start_mode == START_BOOT ? DISSECT_IMAGE_VALIDATE_OS : 0)); if (r < 0) return r; + + } else if (arg_mstack) { + assert(!arg_directory); + assert(!arg_image); + assert(arg_mstack); + + MStackFlags mstack_flags = arg_read_only ? MSTACK_RDONLY : 0; + + /* This creates the needed overlayfs or tmpfs, owned by our target userns. Note that we pass + * the target mount dir as temporary mount dir here. We after all just need some dir here + * that definitely exists, and the temporary mounts on it are not going to be visible + * outside. */ + r = mstack_make_mounts( + mstack, + /* temp_mount_dir= */ directory, /* !! */ + mstack_flags); + if (r < 0) + return log_error_errno(r, "Failed to make .mstack/ mounts: %m"); + + /* And then attaches all mounts to the directory */ + r = mstack_bind_mounts( + mstack, + directory, + /* where_fd= */ -EBADF, + mstack_flags, + /* ret_root_fd= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed bind mount .mstack/ mounts: %m"); } else { assert(arg_directory); assert(!arg_image); + assert(!arg_mstack); r = mount_nofollow_verbose(LOG_ERR, arg_directory, directory, /* fstype= */ NULL, MS_BIND|MS_REC, /* options= */ NULL); if (r < 0) @@ -5023,6 +5112,10 @@ static int load_settings(void) { r = file_in_same_dir(arg_directory, arg_settings_filename, &p); if (r < 0 && r != -EADDRNOTAVAIL) /* if directory is root fs, don't complain */ return log_error_errno(r, "Failed to generate settings path from directory path: %m"); + } else if (arg_mstack) { + r = file_in_same_dir(arg_mstack, arg_settings_filename, &p); + if (r < 0) + return log_error_errno(r, "Failed to generate settings path from mstack path: %m"); } if (p) { @@ -5073,6 +5166,7 @@ static int run_container( const char *directory, int mount_fd, DissectedImage *dissected_image, + MStack *mstack, int userns_fd, FDSet *fds, char veth_name[IFNAMSIZ], @@ -5223,6 +5317,7 @@ static int run_container( directory, mount_fd, dissected_image, + mstack, fd_outer_socket_pair[1], fd_inner_socket_pair[1], fds, @@ -5932,6 +6027,7 @@ static int run(int argc, char *argv[]) { _cleanup_(rm_rf_subvolume_and_freep) char *snapshot_dir = NULL; _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL; _cleanup_(dissected_image_unrefp) DissectedImage *dissected_image = NULL; + _cleanup_(mstack_freep) MStack *mstack = NULL; _cleanup_(sd_netlink_unrefp) sd_netlink *nfnl = NULL; _cleanup_(pidref_done) PidRef pid = PIDREF_NULL; @@ -6195,7 +6291,7 @@ static int run(int argc, char *argv[]) { } } - if (arg_userns_mode == USER_NAMESPACE_MANAGED) { + if (userns_fd >= 0) { r = mountfsd_mount_directory( arg_directory, userns_fd, @@ -6204,11 +6300,11 @@ static int run(int argc, char *argv[]) { if (r < 0) goto finish; } - } else { + + } else if (arg_image) { DissectImageFlags dissect_image_flags = determine_dissect_image_flags(); - assert(arg_image); assert(!arg_template); r = chase_and_update(&arg_image, 0); @@ -6362,7 +6458,37 @@ static int run(int argc, char *argv[]) { if (arg_architecture < 0) arg_architecture = dissected_image_architecture(dissected_image); - } + + } else if (arg_mstack) { + assert(!arg_template); + assert(!arg_ephemeral); + + r = chase_and_update(&arg_mstack, CHASE_MUST_BE_DIRECTORY); + if (r < 0) + goto finish; + + if (!IN_SET(arg_userns_mode, USER_NAMESPACE_NO, USER_NAMESPACE_MANAGED)) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--mstack= requires managed user namespacing, or user namespace turned off."); + + MStackFlags mstack_flags = arg_read_only ? MSTACK_RDONLY : 0; + r = mstack_load(arg_mstack, + /* dir_fd= */ -EBADF, + &mstack); + if (r < 0) + goto finish; + + r = mstack_open_images( + mstack, + userns_fd, + arg_image_policy, + /* image_filter= */ NULL, + mstack_flags); + if (r < 0) { + log_error_errno(r, "Failed to open .mstack/ layer images '%s': %m", arg_mstack); + goto finish; + } + } else + assert_not_reached(); /* Create a temporary place to mount stuff. */ r = mkdtemp_malloc("/tmp/nspawn-root-XXXXXX", &rootdir); @@ -6376,7 +6502,7 @@ static int run(int argc, char *argv[]) { goto finish; if (!arg_quiet) { - const char *t = arg_image ?: arg_directory; + const char *t = arg_mstack ?: arg_image ?: arg_directory; _cleanup_free_ char *u = NULL; (void) terminal_urlify_path(t, t, &u); @@ -6412,6 +6538,7 @@ static int run(int argc, char *argv[]) { rootdir, mount_fd, dissected_image, + mstack, userns_fd, fds, veth_name, &veth_created, From 116394f7ceff8cf48b3f7578bbd5150edbeaafa5 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 16 Feb 2026 09:37:58 +0100 Subject: [PATCH 18/36] tree-wide: move logging from varlink clients in nsresource.c/dissect-image.c into callers These calls are "library-like", hence better should only debug log on their own, not more. --- src/dissect/dissect.c | 2 +- src/import/export-tar.c | 2 +- src/import/import-tar.c | 4 ++-- src/import/pull-tar.c | 10 ++++---- src/nspawn/nspawn.c | 12 +++++++--- src/portable/portable.c | 2 +- src/shared/dissect-image.c | 48 +++++++++++++++++++------------------- src/shared/nsresource.c | 14 +++++------ 8 files changed, 50 insertions(+), 44 deletions(-) diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index a699c007bf..7303cfc3bd 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -2261,7 +2261,7 @@ static int run(int argc, char *argv[]) { arg_flags, &m); if (r < 0) - return r; + return log_error_errno(r, "Failed to mount image via mountfsd: %m"); } } diff --git a/src/import/export-tar.c b/src/import/export-tar.c index e945abe8c3..81fab3714b 100644 --- a/src/import/export-tar.c +++ b/src/import/export-tar.c @@ -365,7 +365,7 @@ int tar_export_start( _cleanup_close_ int mapped_fd = -EBADF; r = mountfsd_mount_directory_fd(directory_fd, e->userns_fd, DISSECT_IMAGE_FOREIGN_UID, &mapped_fd); if (r < 0) - return r; + return log_error_errno(r, "Failed to mount directory via mountfsd: %m"); /* Drop O_PATH */ e->tree_fd = fd_reopen(mapped_fd, O_DIRECTORY|O_CLOEXEC); diff --git a/src/import/import-tar.c b/src/import/import-tar.c index ed96e27222..b3d3fc6104 100644 --- a/src/import/import-tar.c +++ b/src/import/import-tar.c @@ -259,11 +259,11 @@ static int tar_import_fork_tar(TarImport *i) { _cleanup_close_ int directory_fd = -EBADF; r = mountfsd_make_directory(d, MODE_INVALID, /* flags= */ 0, &directory_fd); if (r < 0) - return r; + return log_error_errno(r, "Failed to make directory via mountfsd: %m"); r = mountfsd_mount_directory_fd(directory_fd, i->userns_fd, DISSECT_IMAGE_FOREIGN_UID, &i->tree_fd); if (r < 0) - return r; + return log_error_errno(r, "Failed mount directory via mountfsd: %m"); } else { if (i->flags & IMPORT_BTRFS_SUBVOL) r = btrfs_subvol_make_fallback(AT_FDCWD, d, 0755); diff --git a/src/import/pull-tar.c b/src/import/pull-tar.c index b0cafd95f4..b3d8092106 100644 --- a/src/import/pull-tar.c +++ b/src/import/pull-tar.c @@ -296,18 +296,18 @@ static int tar_pull_make_local_copy(TarPull *p) { r = mountfsd_mount_directory_fd(directory_fd, p->userns_fd, DISSECT_IMAGE_FOREIGN_UID, &p->tree_fd); if (r < 0) - return r; + return log_error_errno(r, "Failed to mount directory via mountfsd: %m"); } _cleanup_close_ int directory_fd = -EBADF; r = mountfsd_make_directory(t, MODE_INVALID, /* flags= */ 0, &directory_fd); if (r < 0) - return r; + return log_error_errno(r, "Failed to make directory via mountfsd: %m"); _cleanup_close_ int copy_fd = -EBADF; r = mountfsd_mount_directory_fd(directory_fd, p->userns_fd, DISSECT_IMAGE_FOREIGN_UID, ©_fd); if (r < 0) - return r; + return log_error_errno(r, "Failed to mount directory via mountfsd: %m"); r = copy_tree_at_foreign(p->tree_fd, copy_fd, p->userns_fd); if (r < 0) @@ -614,11 +614,11 @@ static int tar_pull_job_on_open_disk_tar(PullJob *j) { _cleanup_close_ int directory_fd = -EBADF; r = mountfsd_make_directory(where, MODE_INVALID, /* flags= */ 0, &directory_fd); if (r < 0) - return r; + return log_error_errno(r, "Failed to make directory via mountfsd: %m"); r = mountfsd_mount_directory_fd(directory_fd, p->userns_fd, DISSECT_IMAGE_FOREIGN_UID, &p->tree_fd); if (r < 0) - return r; + return log_error_errno(r, "Failed to mount directory via mountfsd: %m"); } else { if (p->flags & IMPORT_BTRFS_SUBVOL) r = btrfs_subvol_make_fallback(AT_FDCWD, where, 0755); diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 623de6d932..47300ff7e3 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -6297,8 +6297,10 @@ static int run(int argc, char *argv[]) { userns_fd, determine_dissect_image_flags(), &mount_fd); - if (r < 0) + if (r < 0) { + log_error_errno(r, "Failed to mount directory via mountfsd: %m"); goto finish; + } } } else if (arg_image) { @@ -6448,8 +6450,10 @@ static int run(int argc, char *argv[]) { &arg_verity_settings, dissect_image_flags, &dissected_image); - if (r < 0) + if (r < 0) { + log_error_errno(r, "Failed to mount image via mountfsd: %m"); goto finish; + } } /* Now that we mounted the image, let's try to remove it again, if it is ephemeral */ @@ -6474,8 +6478,10 @@ static int run(int argc, char *argv[]) { r = mstack_load(arg_mstack, /* dir_fd= */ -EBADF, &mstack); - if (r < 0) + if (r < 0) { + log_error_errno(r, "Failed to load .mstack/ directory '%s': %m", arg_mstack); goto finish; + } r = mstack_open_images( mstack, diff --git a/src/portable/portable.c b/src/portable/portable.c index 403a05005c..ed249fed9b 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -490,7 +490,7 @@ static int portable_extract_by_path( _cleanup_close_ int mfd = -EBADF; r = mountfsd_mount_directory_fd(rfd, userns_fd, DISSECT_IMAGE_FOREIGN_UID, &mfd); if (r < 0) - return log_debug_errno(r, "Failed to open '%s' via mountfsd: %m", path); + return r; _cleanup_close_pair_ int seq[2] = EBADF_PAIR; if (socketpair(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC, 0, seq) < 0) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 4156903774..0c0274fbc5 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -5173,32 +5173,32 @@ int mountfsd_mount_image_fd( image_fd = fd_reopen_condition(image_fd, O_CLOEXEC|O_NOCTTY|O_NONBLOCK|(FLAGS_SET(flags, DISSECT_IMAGE_MOUNT_READ_ONLY) ? O_RDONLY : O_RDWR), O_PATH, &reopened_fd); if (image_fd < 0) - return log_error_errno(image_fd, "Failed to reopen fd: %m"); + return log_debug_errno(image_fd, "Failed to reopen fd: %m"); r = sd_varlink_push_dup_fd(vl, image_fd); if (r < 0) - return log_error_errno(r, "Failed to push image fd into varlink connection: %m"); + return log_debug_errno(r, "Failed to push image fd into varlink connection: %m"); if (userns_fd >= 0) { r = sd_varlink_push_dup_fd(vl, userns_fd); if (r < 0) - return log_error_errno(r, "Failed to push image fd into varlink connection: %m"); + return log_debug_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 to format image policy to string: %m"); + return log_debug_errno(r, "Failed to format image policy to string: %m"); } if (verity && verity->data_path) { verity_data_fd = open(verity->data_path, O_RDONLY|O_CLOEXEC); if (verity_data_fd < 0) - return log_error_errno(errno, "Failed to open verity data file '%s': %m", verity->data_path); + return log_debug_errno(errno, "Failed to open verity data file '%s': %m", verity->data_path); r = sd_varlink_push_dup_fd(vl, verity_data_fd); if (r < 0) - return log_error_errno(r, "Failed to push verity data fd into varlink connection: %m"); + return log_debug_errno(r, "Failed to push verity data fd into varlink connection: %m"); } _cleanup_(sd_json_variant_unrefp) sd_json_variant *mount_options = NULL; @@ -5220,7 +5220,7 @@ int mountfsd_mount_image_fd( /* ret_values= */ NULL, &filtered); if (r < 0) - return log_error_errno(r, "Failed to filter mount options: %m"); + return log_debug_errno(r, "Failed to filter mount options: %m"); if (isempty(filtered)) continue; @@ -5230,7 +5230,7 @@ int mountfsd_mount_image_fd( &mount_options, SD_JSON_BUILD_PAIR_STRING(partition_designator_to_string(i), filtered ?: o)); if (r < 0) - return log_error_errno(r, "Failed to build mount options array: %m"); + return log_debug_errno(r, "Failed to build mount options array: %m"); } sd_json_variant *reply = NULL; @@ -5256,7 +5256,7 @@ int mountfsd_mount_image_fd( r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); if (r < 0) - return log_error_errno(r, "Failed to parse MountImage() reply: %m"); + return log_debug_errno(r, "Failed to parse MountImage() reply: %m"); log_debug("Effective image policy: %s", p.image_policy); @@ -5289,7 +5289,7 @@ int mountfsd_mount_image_fd( r = sd_json_dispatch(i, partition_dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &pp); if (r < 0) - return log_error_errno(r, "Failed to parse partition data: %m"); + return log_debug_errno(r, "Failed to parse partition data: %m"); if (pp.fsmount_fd_idx != UINT_MAX) { fsmount_fd = sd_varlink_take_fd(vl, pp.fsmount_fd_idx); @@ -5302,11 +5302,11 @@ int mountfsd_mount_image_fd( if (!di) { r = dissected_image_new(/* path= */ NULL, &di); if (r < 0) - return log_error_errno(r, "Failed to allocated new dissected image structure: %m"); + return log_debug_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)); + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Duplicate partition data for '%s'.", partition_designator_to_string(pp.designator)); di->partitions[pp.designator] = (DissectedPartition) { .found = true, @@ -5352,7 +5352,7 @@ int mountfsd_mount_image( _cleanup_close_ int image_fd = open(path, O_RDONLY|O_CLOEXEC); if (image_fd < 0) - return log_error_errno(errno, "Failed to open '%s': %m", path); + return log_debug_errno(errno, "Failed to open '%s': %m", path); _cleanup_(dissected_image_unrefp) DissectedImage *di = NULL; r = mountfsd_mount_image_fd(image_fd, userns_fd, options, image_policy, verity, flags, &di); @@ -5398,12 +5398,12 @@ int mountfsd_mount_directory_fd( r = sd_varlink_push_dup_fd(vl, directory_fd); if (r < 0) - return log_error_errno(r, "Failed to push directory fd into varlink connection: %m"); + return log_debug_errno(r, "Failed to push directory fd into varlink connection: %m"); if (userns_fd >= 0) { r = sd_varlink_push_dup_fd(vl, userns_fd); if (r < 0) - return log_error_errno(r, "Failed to push user namespace fd into varlink connection: %m"); + return log_debug_errno(r, "Failed to push user namespace fd into varlink connection: %m"); } sd_json_variant *reply = NULL; @@ -5430,11 +5430,11 @@ int mountfsd_mount_directory_fd( unsigned fsmount_fd_idx = UINT_MAX; r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &fsmount_fd_idx); if (r < 0) - return log_error_errno(r, "Failed to parse MountImage() reply: %m"); + return log_debug_errno(r, "Failed to parse MountImage() reply: %m"); _cleanup_close_ int fsmount_fd = sd_varlink_take_fd(vl, fsmount_fd_idx); if (fsmount_fd < 0) - return log_error_errno(fsmount_fd, "Failed to take mount fd from Varlink connection: %m"); + return log_debug_errno(fsmount_fd, "Failed to take mount fd from Varlink connection: %m"); *ret_mount_fd = TAKE_FD(fsmount_fd); return 0; @@ -5451,7 +5451,7 @@ int mountfsd_mount_directory( _cleanup_close_ int directory_fd = open(path, O_DIRECTORY|O_RDONLY|O_CLOEXEC|O_PATH); if (directory_fd < 0) - return log_error_errno(errno, "Failed to open '%s': %m", path); + return log_debug_errno(errno, "Failed to open '%s': %m", path); return mountfsd_mount_directory_fd(directory_fd, userns_fd, flags, ret_mount_fd); } @@ -5483,7 +5483,7 @@ int mountfsd_make_directory_fd( r = sd_varlink_push_dup_fd(vl, parent_fd); if (r < 0) - return log_error_errno(r, "Failed to push parent fd into varlink connection: %m"); + return log_debug_errno(r, "Failed to push parent fd into varlink connection: %m"); sd_json_variant *reply = NULL; const char *error_id = NULL; @@ -5507,11 +5507,11 @@ int mountfsd_make_directory_fd( unsigned directory_fd_idx = UINT_MAX; r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &directory_fd_idx); if (r < 0) - return log_error_errno(r, "Failed to parse MountImage() reply: %m"); + return log_debug_errno(r, "Failed to parse MountImage() reply: %m"); _cleanup_close_ int directory_fd = sd_varlink_take_fd(vl, directory_fd_idx); if (directory_fd < 0) - return log_error_errno(directory_fd, "Failed to take directory fd from Varlink connection: %m"); + return log_debug_errno(directory_fd, "Failed to take directory fd from Varlink connection: %m"); if (ret_directory_fd) *ret_directory_fd = TAKE_FD(directory_fd); @@ -5529,16 +5529,16 @@ int mountfsd_make_directory( _cleanup_free_ char *parent = NULL; r = path_extract_directory(path, &parent); if (r < 0) - return log_error_errno(r, "Failed to extract parent directory from '%s': %m", path); + return log_debug_errno(r, "Failed to extract parent directory from '%s': %m", path); _cleanup_free_ char *dirname = NULL; r = path_extract_filename(path, &dirname); if (r < 0) - return log_error_errno(r, "Failed to extract directory name from '%s': %m", path); + return log_debug_errno(r, "Failed to extract directory name from '%s': %m", path); _cleanup_close_ int fd = open(parent, O_DIRECTORY|O_CLOEXEC); if (fd < 0) - return log_error_errno(r, "Failed to open '%s': %m", parent); + return log_debug_errno(r, "Failed to open '%s': %m", parent); return mountfsd_make_directory_fd(fd, dirname, mode, flags, ret_directory_fd); } diff --git a/src/shared/nsresource.c b/src/shared/nsresource.c index 31d96bad7c..651d9bdf4d 100644 --- a/src/shared/nsresource.c +++ b/src/shared/nsresource.c @@ -195,11 +195,11 @@ int nsresource_add_mount(int userns_fd, int mount_fd) { userns_fd_idx = sd_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"); + return log_debug_errno(userns_fd_idx, "Failed to push userns fd into varlink connection: %m"); mount_fd_idx = sd_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"); + return log_debug_errno(mount_fd_idx, "Failed to push mount fd into varlink connection: %m"); sd_json_variant *reply = NULL; r = sd_varlink_callbo( @@ -210,13 +210,13 @@ int nsresource_add_mount(int userns_fd, int mount_fd) { SD_JSON_BUILD_PAIR_UNSIGNED("userNamespaceFileDescriptor", userns_fd_idx), SD_JSON_BUILD_PAIR_UNSIGNED("mountFileDescriptor", mount_fd_idx)); if (r < 0) - return log_error_errno(r, "Failed to call AddMountToUserNamespace() varlink call: %m"); + return log_debug_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."); + log_debug("User namespace has not been allocated via namespace resource registry, not adding mount to registration."); return 0; } if (error_id) - return log_error_errno(sd_varlink_error_to_errno(error_id, reply), "Failed to mount image: %s", error_id); + return log_debug_errno(sd_varlink_error_to_errno(error_id, reply), "Failed to mount image: %s", error_id); return 1; } @@ -264,7 +264,7 @@ int nsresource_add_cgroup(int userns_fd, int cgroup_fd) { 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."); + log_debug("User namespace has not been allocated via namespace resource registry, not adding cgroup to registration."); return 0; } if (error_id) @@ -343,7 +343,7 @@ int nsresource_add_netif_veth( 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."); + log_debug("User namespace has not been allocated via namespace resource registry, not adding network to registration."); return 0; } if (error_id) From 32b88dd25a5c726d1d8dd0b8472455109cb8b215 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 24 Nov 2025 22:23:41 +0100 Subject: [PATCH 19/36] pid1: introduce RootMStack= for using an mstack as root dir for a service --- man/org.freedesktop.systemd1.xml | 34 +++++++++++---- src/core/dbus-execute.c | 4 ++ src/core/execute-serialize.c | 12 ++++++ src/core/execute.c | 13 ++++-- src/core/execute.h | 5 ++- src/core/load-fragment-gperf.gperf.in | 1 + src/core/namespace.c | 59 +++++++++++++++++++++++---- src/core/namespace.h | 1 + src/core/service.c | 5 ++- src/core/unit.c | 15 +++++-- src/core/varlink-execute.c | 1 + src/shared/varlink-io.systemd.Unit.c | 2 + 12 files changed, 124 insertions(+), 28 deletions(-) diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index fbed12c4be..f0a7ccc6d6 100644 --- a/man/org.freedesktop.systemd1.xml +++ b/man/org.freedesktop.systemd1.xml @@ -3137,6 +3137,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly b RootEphemeral = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s RootMStack = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as ExtensionDirectories = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sba(ss)) ExtensionImages = [...]; @@ -4496,6 +4498,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + @@ -4938,6 +4942,7 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { MountImages ExtensionImages ExtensionDirectories + RootMStack see systemd.exec(5) for their meaning. MemoryAvailable takes into account unit's and parents' MemoryMax @@ -5392,6 +5397,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly b RootEphemeral = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s RootMStack = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as ExtensionDirectories = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sba(ss)) ExtensionImages = [...]; @@ -6741,6 +6748,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { + + @@ -7461,6 +7470,8 @@ node /org/freedesktop/systemd1/unit/home_2emount { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly b RootEphemeral = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s RootMStack = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as ExtensionDirectories = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sba(ss)) ExtensionImages = [...]; @@ -8642,6 +8653,8 @@ node /org/freedesktop/systemd1/unit/home_2emount { + + @@ -9495,6 +9508,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly b RootEphemeral = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s RootMStack = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as ExtensionDirectories = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sba(ss)) ExtensionImages = [...]; @@ -10640,6 +10655,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { + + @@ -12520,9 +12537,8 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ ManagedOOMKills, ExecReloadPost, and ExecReloadPostEx were added in version 259. - BindNetworkInterface, - MemoryTHP, and - RefreshOnReload were added in version 260. + BindNetworkInterface, MemoryTHP, + RefreshOnReload, and RootMStack were added in version 260. Socket Unit Objects @@ -12591,8 +12607,8 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ UserNamespacePath, OOMKills, and ManagedOOMKills were added in 259. - BindNetworkInterface, and - MemoryTHP were added in version 260. + BindNetworkInterface MemoryTHP, and + RootMStack were added in version 260. Mount Unit Objects @@ -12656,8 +12672,8 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ UserNamespacePath, OOMKills, and ManagedOOMKills were added in 259. - BindNetworkInterface, and - MemoryTHP were added in version 260. + BindNetworkInterface MemoryTHP, and + RootMStack were added in version 260. Swap Unit Objects @@ -12719,8 +12735,8 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ UserNamespacePath, OOMKills, and ManagedOOMKills were added in 259. - BindNetworkInterface, and - MemoryTHP were added in version 260. + BindNetworkInterface, MemoryTHP, and + RootMStack were added in version 260. Slice Unit Objects diff --git a/src/core/dbus-execute.c b/src/core/dbus-execute.c index 6fb9060415..2bd7b1c07e 100644 --- a/src/core/dbus-execute.c +++ b/src/core/dbus-execute.c @@ -1258,6 +1258,7 @@ const sd_bus_vtable bus_exec_vtable[] = { SD_BUS_PROPERTY("RootHashSignaturePath", "s", NULL, offsetof(ExecContext, root_hash_sig_path), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("RootVerity", "s", NULL, offsetof(ExecContext, root_verity), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("RootEphemeral", "b", bus_property_get_bool, offsetof(ExecContext, root_ephemeral), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("RootMStack", "s", NULL, offsetof(ExecContext, root_mstack), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("ExtensionDirectories", "as", NULL, offsetof(ExecContext, extension_directories), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("ExtensionImages", "a(sba(ss))", property_get_extension_images, 0, SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("MountImages", "a(ssba(ss))", property_get_mount_images, 0, SD_BUS_VTABLE_PROPERTY_CONST), @@ -1882,6 +1883,9 @@ int bus_exec_context_set_transient_property( if (streq(name, "RootImage")) return bus_set_transient_path(u, name, &c->root_image, message, flags, reterr_error); + if (streq(name, "RootMStack")) + return bus_set_transient_path(u, name, &c->root_mstack, message, flags, reterr_error); + if (streq(name, "RootImageOptions")) { _cleanup_(mount_options_free_allp) MountOptions *options = NULL; _cleanup_free_ char *format_str = NULL; diff --git a/src/core/execute-serialize.c b/src/core/execute-serialize.c index 3ffedf58db..560df95287 100644 --- a/src/core/execute-serialize.c +++ b/src/core/execute-serialize.c @@ -1633,6 +1633,10 @@ static int exec_context_serialize(const ExecContext *c, FILE *f) { if (r < 0) return r; + r = serialize_item_escaped(f, "exec-context-root-mstack", c->root_mstack); + if (r < 0) + return r; + r = serialize_item_format(f, "exec-context-umask", "%04o", c->umask); if (r < 0) return r; @@ -2568,6 +2572,14 @@ static int exec_context_deserialize(ExecContext *c, FILE *f) { if (r < 0) return r; c->root_ephemeral = r; + } else if ((val = startswith(l, "exec-context-root-mstack="))) { + ssize_t k; + char *p; + + k = cunescape(val, 0, &p); + if (k < 0) + return k; + free_and_replace(c->root_mstack, p); } else if ((val = startswith(l, "exec-context-umask="))) { r = parse_mode(val, &c->umask); if (r < 0) diff --git a/src/core/execute.c b/src/core/execute.c index 8e5796b92a..6a7fc65f20 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -263,7 +263,8 @@ bool exec_needs_mount_namespace( assert(context); - if (context->root_image) + if (context->root_image || + context->root_mstack) return true; if (context->root_directory_as_fd) @@ -684,6 +685,7 @@ void exec_context_done(ExecContext *c) { iovec_done(&c->root_hash_sig); c->root_hash_sig_path = mfree(c->root_hash_sig_path); c->root_verity = mfree(c->root_verity); + c->root_mstack = mfree(c->root_mstack); c->tty_path = mfree(c->tty_path); c->syslog_identifier = mfree(c->syslog_identifier); c->user = mfree(c->user); @@ -1203,6 +1205,9 @@ void exec_context_dump(const ExecContext *c, FILE* f, const char *prefix) { if (c->root_verity) fprintf(f, "%sRootVerity: %s\n", prefix, c->root_verity); + if (c->root_mstack) + fprintf(f, "%sRootMStack: %s\n", prefix, c->root_mstack); + STRV_FOREACH(e, c->environment) fprintf(f, "%sEnvironment: %s\n", prefix, *e); @@ -2058,9 +2063,9 @@ bool exec_context_restrict_filesystems_set(const ExecContext *c) { bool exec_context_with_rootfs(const ExecContext *c) { assert(c); - /* Checks if RootDirectory=, RootImage= or RootDirectoryFileDescriptor= are used */ + /* Checks if RootDirectory=, RootImage=, RootMStack= or RootDirectoryFileDescriptor= are used */ - return !empty_or_root(c->root_directory) || c->root_image || c->root_directory_as_fd; + return !empty_or_root(c->root_directory) || c->root_image || c->root_directory_as_fd || c->root_mstack; } bool exec_context_with_rootfs_strict(const ExecContext *c) { @@ -2070,7 +2075,7 @@ bool exec_context_with_rootfs_strict(const ExecContext *c) { * true in more cases: when a root directory is explicitly configured, even if it's our usual * root. */ - return c->root_directory || c->root_image || c->root_directory_as_fd; + return c->root_directory || c->root_image || c->root_directory_as_fd || c->root_mstack; } int exec_context_has_vpicked_extensions(const ExecContext *context) { diff --git a/src/core/execute.h b/src/core/execute.h index 6fe0d5e570..c5f3988371 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -199,9 +199,12 @@ typedef struct ExecContext { char **unset_environment; struct rlimit *rlimit[_RLIMIT_MAX]; - char *working_directory, *root_directory, *root_image, *root_verity, *root_hash_path, *root_hash_sig_path; + char *working_directory; + char *root_directory; + char *root_image, *root_verity, *root_hash_path, *root_hash_sig_path; struct iovec root_hash, root_hash_sig; MountOptions *root_image_options; + char *root_mstack; bool root_ephemeral; bool working_directory_missing_ok:1; bool working_directory_home:1; diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in index 5605445c34..60e616a03f 100644 --- a/src/core/load-fragment-gperf.gperf.in +++ b/src/core/load-fragment-gperf.gperf.in @@ -11,6 +11,7 @@ {{type}}.RootHashSignature, config_parse_exec_root_hash_sig, 0, offsetof({{type}}, exec_context) {{type}}.RootVerity, config_parse_unit_path_printf, true, offsetof({{type}}, exec_context.root_verity) {{type}}.RootEphemeral, config_parse_bool, 0, offsetof({{type}}, exec_context.root_ephemeral) +{{type}}.RootMStack, config_parse_unit_path_printf, true, offsetof({{type}}, exec_context.root_mstack) {{type}}.ExtensionDirectories, config_parse_namespace_path_strv, 0, offsetof({{type}}, exec_context.extension_directories) {{type}}.ExtensionImages, config_parse_extension_images, 0, offsetof({{type}}, exec_context) {{type}}.ExtensionImagePolicy, config_parse_image_policy, 0, offsetof({{type}}, exec_context.extension_image_policy) diff --git a/src/core/namespace.c b/src/core/namespace.c index 0ed686f9ec..6927845b9c 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -34,6 +34,7 @@ #include "mkdir-label.h" #include "mount-util.h" #include "mountpoint-util.h" +#include "mstack.h" #include "namespace.h" #include "namespace-util.h" #include "nsflags.h" @@ -1296,6 +1297,13 @@ static int create_temporary_mount_point(RuntimeScope scope, char **ret) { return 0; } +static bool namespace_with_rootfs(const NamespaceParameters *p) { + /* Returns true, if we have a root dir, root image or too mstack, and hence the root mount is + * changed */ + + return p->root_image || p->root_directory || p->root_directory_fd >= 0 || p->root_mstack; +} + static int mount_private_dev(const MountEntry *m, const NamespaceParameters *p) { static const char devnodes[] = "/dev/null\0" @@ -1360,7 +1368,7 @@ static int mount_private_dev(const MountEntry *m, const NamespaceParameters *p) /* We assume /run/systemd/journal/ is available if not changing root, which isn't entirely accurate * but shouldn't matter, as either way the user would get ENOENT when accessing /dev/log */ - if ((!p->root_image && !p->root_directory && p->root_directory_fd < 0) || p->bind_log_sockets) { + if (!namespace_with_rootfs(p) || p->bind_log_sockets) { const char *devlog = strjoina(temporary_mount, "/dev/log"); if (symlink("/run/systemd/journal/dev-log", devlog) < 0) log_debug_errno(errno, @@ -2497,10 +2505,22 @@ static bool home_read_only( return false; } +static bool namespace_read_only(const NamespaceParameters *p) { + assert(p); + + return root_read_only(p->read_only_paths, + p->protect_system) && + home_read_only(p->read_only_paths, p->inaccessible_paths, p->empty_directories, + p->bind_mounts, p->n_bind_mounts, p->temporary_filesystems, p->n_temporary_filesystems, + p->protect_home) && + strv_isempty(p->read_write_paths); +} + int setup_namespace(const NamespaceParameters *p, char **reterr_path) { _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; _cleanup_(dissected_image_unrefp) DissectedImage *dissected_image = NULL; + _cleanup_(mstack_freep) MStack *mstack = NULL; _cleanup_strv_free_ char **hierarchies = NULL; _cleanup_(mount_list_done) MountList ml = {}; _cleanup_close_ int userns_fd = -EBADF; @@ -2518,6 +2538,7 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { DISSECT_IMAGE_PIN_PARTITION_DEVICES | DISSECT_IMAGE_ALLOW_USERSPACE_VERITY | DISSECT_IMAGE_VERITY_SHARE; + MStackFlags mstack_flags = 0; int r; assert(p); @@ -2531,12 +2552,7 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { if (p->root_image) { /* Make the whole image read-only if we can determine that we only access it in a read-only fashion. */ - if (root_read_only(p->read_only_paths, - p->protect_system) && - home_read_only(p->read_only_paths, p->inaccessible_paths, p->empty_directories, - p->bind_mounts, p->n_bind_mounts, p->temporary_filesystems, p->n_temporary_filesystems, - p->protect_home) && - strv_isempty(p->read_write_paths)) + if (namespace_read_only(p)) dissect_image_flags |= DISSECT_IMAGE_READ_ONLY; SET_FLAG(dissect_image_flags, DISSECT_IMAGE_NO_PARTITION_TABLE, p->verity && p->verity->data_path); @@ -2620,6 +2636,24 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { return r; } } + + } else if (p->root_mstack) { + if (namespace_read_only(p)) + mstack_flags |= MSTACK_RDONLY; + + r = mstack_load(p->root_mstack, /* dir_fd= */ -EBADF, &mstack); + if (r < 0) + return r; + + if (p->runtime_scope != RUNTIME_SCOPE_SYSTEM) { + 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 = mstack_open_images(mstack, userns_fd, p->root_image_policy, /* image_filter= */ NULL, mstack_flags); + if (r < 0) + return r; } if (p->root_directory) @@ -3014,6 +3048,15 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { return r; } + } else if (p->root_mstack) { + r = mstack_make_mounts(mstack, root, mstack_flags); + if (r < 0) + return r; + + r = mstack_bind_mounts(mstack, root, /* where_fd= */ -EBADF, mstack_flags, /* ret_root_fd= */ NULL); + if (r < 0) + return r; + } else { /* Let's mount the main root directory to the root directory to use */ r = mount_nofollow_verbose(LOG_DEBUG, "/", root, NULL, MS_BIND|MS_REC, NULL); @@ -3022,7 +3065,7 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { } /* Try to set up the new root directory before mounting anything else there. */ - if (p->root_image || p->root_directory || p->root_directory_fd >= 0) + if (namespace_with_rootfs(p)) (void) base_filesystem_create(root, UID_INVALID, GID_INVALID); /* Now make the magic happen */ diff --git a/src/core/namespace.h b/src/core/namespace.h index b96e7b4372..4f5e6546bd 100644 --- a/src/core/namespace.h +++ b/src/core/namespace.h @@ -130,6 +130,7 @@ typedef struct NamespaceParameters { int root_directory_fd; const char *root_directory; const char *root_image; + const char *root_mstack; const MountOptions *root_image_options; const ImagePolicy *root_image_policy; diff --git a/src/core/service.c b/src/core/service.c index aa133a57db..aa3690e92a 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -5887,8 +5887,9 @@ int service_determine_exec_selinux_label(Service *s, char **ret) { if (s->exec_context.root_image || s->exec_context.n_extension_images > 0 || - !strv_isempty(s->exec_context.extension_directories)) /* We cannot chase paths through images */ - return log_unit_debug_errno(UNIT(s), SYNTHETIC_ERRNO(ENODATA), "Service with RootImage=, ExtensionImages= or ExtensionDirectories= set, cannot determine socket SELinux label before activation, ignoring."); + !strv_isempty(s->exec_context.extension_directories) || + s->exec_context.root_mstack) /* We cannot chase paths through images */ + return log_unit_debug_errno(UNIT(s), SYNTHETIC_ERRNO(ENODATA), "Service with RootImage=, ExtensionImages=, ExtensionDirectories= or RootMStack= set cannot determine socket SELinux label before activation, ignoring."); ExecCommand *c = s->exec_command[SERVICE_EXEC_START]; if (!c) diff --git a/src/core/unit.c b/src/core/unit.c index 0f6ea87756..d468f1303d 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -1266,6 +1266,12 @@ int unit_add_exec_dependencies(Unit *u, ExecContext *c) { return r; } + if (c->root_mstack) { + r = unit_add_mounts_for(u, c->root_mstack, UNIT_DEPENDENCY_FILE, UNIT_MOUNT_WANTS); + if (r < 0) + return r; + } + for (ExecDirectoryType dt = 0; dt < _EXEC_DIRECTORY_TYPE_MAX; dt++) { if (!u->manager->prefix[dt]) continue; @@ -1322,9 +1328,9 @@ int unit_add_exec_dependencies(Unit *u, ExecContext *c) { return r; } - if (c->root_image) { + if (c->root_image || c->root_mstack) { /* We need to wait for /dev/loopX to appear when doing RootImage=, hence let's add an - * implicit dependency on udev */ + * implicit dependency on udev. (And for RootMStack= we might need it) */ r = unit_add_dependency_by_name(u, UNIT_AFTER, SPECIAL_UDEVD_SERVICE, true, UNIT_DEPENDENCY_FILE); if (r < 0) @@ -4461,9 +4467,10 @@ int unit_patch_contexts(Unit *u) { /* Only add these if needed, as they imply that everything else is blocked. */ if (cgroup_context_has_device_policy(cc)) { - if (ec->root_image || ec->mount_images) { + if (ec->root_image || ec->mount_images || ec->root_mstack) { - /* When RootImage= or MountImages= is specified, the following devices are touched. */ + /* When RootImage= or MountImages= is specified, the following devices are + * touched. For RootMStack= there's the possibility the are touched. */ FOREACH_STRING(p, "/dev/loop-control", "/dev/mapper/control") { r = cgroup_context_add_device_allow(cc, p, CGROUP_DEVICE_READ|CGROUP_DEVICE_WRITE); if (r < 0) diff --git a/src/core/varlink-execute.c b/src/core/varlink-execute.c index 35ea1b1330..e6efd59895 100644 --- a/src/core/varlink-execute.c +++ b/src/core/varlink-execute.c @@ -794,6 +794,7 @@ int unit_exec_context_build_json(sd_json_variant **ret, const char *name, void * JSON_BUILD_PAIR_CALLBACK_NON_NULL("WorkingDirectory", working_directory_build_json, c), JSON_BUILD_PAIR_STRING_NON_EMPTY("RootDirectory", c->root_directory), JSON_BUILD_PAIR_STRING_NON_EMPTY("RootImage", c->root_image), + JSON_BUILD_PAIR_STRING_NON_EMPTY("RootMStack", c->root_mstack), JSON_BUILD_PAIR_CALLBACK_NON_NULL("RootImageOptions", root_image_options_build_json, c->root_image_options), SD_JSON_BUILD_PAIR_BOOLEAN("RootEphemeral", c->root_ephemeral), JSON_BUILD_PAIR_BASE64_NON_EMPTY("RootHash", c->root_hash.iov_base, c->root_hash.iov_len), diff --git a/src/shared/varlink-io.systemd.Unit.c b/src/shared/varlink-io.systemd.Unit.c index a7d56bb5e9..a008b506e9 100644 --- a/src/shared/varlink-io.systemd.Unit.c +++ b/src/shared/varlink-io.systemd.Unit.c @@ -409,6 +409,8 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE( SD_VARLINK_DEFINE_FIELD(RootDirectory, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man"PROJECT_VERSION_STR"systemd.exec.html#RootImage="), SD_VARLINK_DEFINE_FIELD(RootImage, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man"PROJECT_VERSION_STR"systemd.exec.html#RootMStack="), + SD_VARLINK_DEFINE_FIELD(RootMStack, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man"PROJECT_VERSION_STR"systemd.exec.html#RootImageOptions="), SD_VARLINK_DEFINE_FIELD_BY_TYPE(RootImageOptions, PartitionMountOptions, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man"PROJECT_VERSION_STR"systemd.exec.html#RootEphemeral="), From 38e65b6e4cf53e96f22031358ea3d73e17061517 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 25 Nov 2025 09:01:36 +0100 Subject: [PATCH 20/36] portable: support .mstack images --- src/portable/portable.c | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/portable/portable.c b/src/portable/portable.c index ed249fed9b..191125b9c5 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -1267,11 +1267,36 @@ void portable_changes_free(PortableChange *changes, size_t n_changes) { } static const char *root_setting_from_image(ImageType type) { - return IN_SET(type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME) ? "RootDirectory=" : "RootImage="; + switch (type) { + case IMAGE_DIRECTORY: + case IMAGE_SUBVOLUME: + return "RootDirectory="; + + case IMAGE_RAW: + case IMAGE_BLOCK: + return "RootImage="; + + case IMAGE_MSTACK: + return "RootMStack="; + + default: + return NULL; + } } static const char *extension_setting_from_image(ImageType type) { - return IN_SET(type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME) ? "ExtensionDirectories=" : "ExtensionImages="; + switch (type) { + case IMAGE_DIRECTORY: + case IMAGE_SUBVOLUME: + return "ExtensionDirectories="; + + case IMAGE_RAW: + case IMAGE_BLOCK: + return "ExtensionImages="; + + default: + return NULL; + } } static int make_marker_text(const char *image_path, OrderedHashmap *extension_images, char **ret_text) { @@ -1401,6 +1426,8 @@ static int install_chroot_dropin( Image *ext; root_type = root_setting_from_image(type); + if (!root_type) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Image type '%s' not supported as portable service.", image_type_to_string(type)); r = path_extract_filename(m->image_path ?: image_path, &base_name); if (r < 0) @@ -1453,15 +1480,19 @@ static int install_chroot_dropin( if (m->image_path && !path_equal(m->image_path, image_path)) ORDERED_HASHMAP_FOREACH(ext, extension_images) { - _cleanup_free_ char *extension_base_name = NULL; + const char *extension_setting = extension_setting_from_image(ext->type); + if (!extension_setting) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Image type '%s' not supported for extensions: %m", image_type_to_string(ext->type)); + + _cleanup_free_ char *extension_base_name = NULL; r = path_extract_filename(ext->path, &extension_base_name); if (r < 0) return log_debug_errno(r, "Failed to extract basename from '%s': %m", ext->path); if (!strextend(&text, "\n", - extension_setting_from_image(ext->type), + extension_setting, ext->path, /* With --force tell PID1 to avoid enforcing that the image and * extension-release. have to match. */ From fe1f8c1acdbcc098a6fbafcea02d24ddee1a8ccc Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 25 Nov 2025 09:01:58 +0100 Subject: [PATCH 21/36] run: support RootMStack= on the client side for systemd-run --- src/run/run.c | 2 +- src/shared/bus-unit-util.c | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/run/run.c b/src/run/run.c index 04e643b209..e614f27c28 100644 --- a/src/run/run.c +++ b/src/run/run.c @@ -3053,7 +3053,7 @@ static bool shall_make_executable_absolute(void) { if (!arg_root_directory && running_in_chroot() > 0) return false; - FOREACH_STRING(f, "RootDirectory=", "RootImage=", "ExecSearchPath=", "MountImages=", "ExtensionImages=") + FOREACH_STRING(f, "RootDirectory=", "RootImage=", "RootMStack=", "ExecSearchPath=", "MountImages=", "ExtensionImages=") if (strv_find_startswith(arg_property, f)) return false; diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index 057a89b8ab..84de3478a7 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -2468,6 +2468,7 @@ static const BusProperty execute_properties[] = { { "SELinuxContext", bus_append_string }, { "RootImage", bus_append_string }, { "RootVerity", bus_append_string }, + { "RootMStack", bus_append_string }, { "RuntimeDirectoryPreserve", bus_append_string }, { "Personality", bus_append_string }, { "KeyringMode", bus_append_string }, From 9628a5ced40311527b142c9b31de97210ce24e95 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 25 Nov 2025 15:33:36 +0100 Subject: [PATCH 22/36] importd: add bus/varlink api for downloading OCIs --- man/org.freedesktop.import1.xml | 22 ++++ src/import/importd.c | 166 +++++++++++++++--------- src/import/meson.build | 1 + src/import/org.freedesktop.import1.conf | 4 + src/shared/import-util.c | 1 + src/shared/import-util.h | 1 + 6 files changed, 134 insertions(+), 61 deletions(-) diff --git a/man/org.freedesktop.import1.xml b/man/org.freedesktop.import1.xml index cece4cad0b..95ecbd79e6 100644 --- a/man/org.freedesktop.import1.xml +++ b/man/org.freedesktop.import1.xml @@ -136,6 +136,12 @@ node /org/freedesktop/import1 { in t flags, out u transfer_id, out o transfer_path); + PullOci(in s ref, + in s local_name, + in s class, + in t flags, + out u transfer_id, + out o transfer_path); ListTransfers(out a(usssdo) transfers); ListTransfersEx(in s class, in t flags, @@ -191,6 +197,8 @@ node /org/freedesktop/import1 { + + @@ -290,6 +298,19 @@ node /org/freedesktop/import1 { export calls above, these calls return a pair of transfer identifier and object path for the ongoing download. + PullOci() is similar to PullTarEx() or + PullRawEx() and may be used to download and import an OCI container image from an + OCI registry. It takes an OCI container reference as argument. The second argument is a local name for + the image (which will be generated as + systemd.mstack7 + directory referencing the OCI layers). It should be suitable as a hostname, similarly to the matching + argument of the PullTar()/PullTarEx() and + PullRaw()/PullRawEx() methods above. The last argument is a + 64bit flags parameter, where bit 0 controls the force flag, bit 1 is a + read_only flag that controls whether the created image shall be marked + read-only. Like the pull calls above, this call return a pair of transfer identifier and object path + for the ongoing download. + ImportFileSystem()/ImportFileSystemEx() are similar to ImportTar()/ImportTarEx() but import a directory tree. The first argument must refer to a directory file descriptor for the source hierarchy to import. @@ -464,6 +485,7 @@ node /org/freedesktop/import1/transfer/_1 { ExportRawEx(), PullTarEx(), PullRawEx(), ListTransfersEx(), ListImages() were added in version 256. + PullOci() was added in version 260. Transfer Objects diff --git a/src/import/importd.c b/src/import/importd.c index 280aa88d46..d3363d446c 100644 --- a/src/import/importd.c +++ b/src/import/importd.c @@ -29,6 +29,7 @@ #include "json-util.h" #include "main-func.h" #include "notify-recv.h" +#include "oci-util.h" #include "os-util.h" #include "parse-util.h" #include "path-lookup.h" @@ -58,6 +59,7 @@ typedef enum TransferType { TRANSFER_EXPORT_RAW, TRANSFER_PULL_TAR, TRANSFER_PULL_RAW, + TRANSFER_PULL_OCI, _TRANSFER_TYPE_MAX, _TRANSFER_TYPE_INVALID = -EINVAL, } TransferType; @@ -127,6 +129,7 @@ static const char* const transfer_type_table[_TRANSFER_TYPE_MAX] = { [TRANSFER_EXPORT_RAW] = "export-raw", [TRANSFER_PULL_TAR] = "pull-tar", [TRANSFER_PULL_RAW] = "pull-raw", + [TRANSFER_PULL_OCI] = "pull-oci", }; DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(transfer_type, TransferType); @@ -497,6 +500,7 @@ static int transfer_start(Transfer *t) { case TRANSFER_PULL_TAR: case TRANSFER_PULL_RAW: + case TRANSFER_PULL_OCI: cmd[k++] = SYSTEMD_PULL_PATH; break; @@ -518,6 +522,10 @@ static int transfer_start(Transfer *t) { cmd[k++] = "raw"; break; + case TRANSFER_PULL_OCI: + cmd[k++] = "oci"; + break; + case TRANSFER_IMPORT_FS: cmd[k++] = "run"; break; @@ -1053,7 +1061,7 @@ static int method_export_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_ return 1; } -static int method_pull_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_error *error) { +static int method_pull_tar_or_raw_or_oci(sd_bus_message *msg, void *userdata, sd_bus_error *error) { _cleanup_(transfer_unrefp) Transfer *t = NULL; ImageClass class = _IMAGE_CLASS_INVALID; const char *remote, *local, *verify; @@ -1078,7 +1086,24 @@ static int method_pull_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_er return 1; /* Will call us back */ } - if (endswith(sd_bus_message_get_member(msg), "Ex")) { + if (streq(sd_bus_message_get_member(msg), "PullOci")) { + const char *sclass; + + r = sd_bus_message_read(msg, "ssst", &remote, &local, &sclass, &flags); + if (r < 0) + return r; + + class = image_class_from_string(sclass); + if (class < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, + "Image class '%s' not known", sclass); + + if (flags & ~(IMPORT_FORCE|IMPORT_READ_ONLY)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, + "Flags 0x%" PRIx64 " invalid", flags); + + verify = NULL; + } else if (endswith(sd_bus_message_get_member(msg), "Ex")) { const char *sclass; r = sd_bus_message_read(msg, "sssst", &remote, &local, &sclass, &verify, &flags); @@ -1106,9 +1131,21 @@ static int method_pull_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_er SET_FLAG(flags, IMPORT_FORCE, force); } - if (!http_url_is_valid(remote) && !file_url_is_valid(remote)) + type = startswith(sd_bus_message_get_member(msg), "PullTar") ? TRANSFER_PULL_TAR : + startswith(sd_bus_message_get_member(msg), "PullRaw") ? TRANSFER_PULL_RAW : + streq(sd_bus_message_get_member(msg), "PullOci") ? TRANSFER_PULL_OCI : _TRANSFER_TYPE_INVALID; + assert(type >= 0); + + if (type == TRANSFER_PULL_OCI) { + r = oci_ref_valid(remote); + if (r < 0) + return r; + if (r == 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, + "Reference '%s' is invalid", remote); + } else if (!http_url_is_valid(remote) && !file_url_is_valid(remote)) return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, - "URL %s is invalid", remote); + "URL '%s' is invalid", remote); if (isempty(local)) local = NULL; @@ -1116,13 +1153,16 @@ static int method_pull_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_er return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Local image name %s is invalid", local); - if (isempty(verify)) + if (type == TRANSFER_PULL_OCI) + v = _IMPORT_VERIFY_INVALID; + else if (isempty(verify)) v = IMPORT_VERIFY_SIGNATURE; - else + else { v = import_verify_from_string(verify); - if (v < 0) - return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, - "Unknown verification mode %s", verify); + if (v < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, + "Unknown verification mode %s", verify); + } if (class == IMAGE_MACHINE) { r = image_setup_pool(m->runtime_scope, class, m->use_btrfs_subvol, m->use_btrfs_quota); @@ -1130,9 +1170,6 @@ static int method_pull_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_er return sd_bus_error_set_errnof(error, r, "Failed to set up machine pool: %m"); } - type = startswith(sd_bus_message_get_member(msg), "PullTar") ? - TRANSFER_PULL_TAR : TRANSFER_PULL_RAW; - if (manager_find(m, type, remote)) return sd_bus_error_setf(error, BUS_ERROR_TRANSFER_IN_PROGRESS, "Transfer for %s already in progress.", remote); @@ -1613,52 +1650,53 @@ static const sd_bus_vtable manager_vtable[] = { SD_BUS_PARAM(transfer_path), method_export_tar_or_raw, SD_BUS_VTABLE_UNPRIVILEGED), - SD_BUS_METHOD_WITH_NAMES("PullTar", - "sssb", - SD_BUS_PARAM(url) - SD_BUS_PARAM(local_name) - SD_BUS_PARAM(verify_mode) - SD_BUS_PARAM(force), - "uo", - SD_BUS_PARAM(transfer_id) - SD_BUS_PARAM(transfer_path), - method_pull_tar_or_raw, - SD_BUS_VTABLE_UNPRIVILEGED), - SD_BUS_METHOD_WITH_NAMES("PullTarEx", - "sssst", - SD_BUS_PARAM(url) - SD_BUS_PARAM(local_name) - SD_BUS_PARAM(class) - SD_BUS_PARAM(verify_mode) - SD_BUS_PARAM(flags), - "uo", - SD_BUS_PARAM(transfer_id) - SD_BUS_PARAM(transfer_path), - method_pull_tar_or_raw, - SD_BUS_VTABLE_UNPRIVILEGED), - SD_BUS_METHOD_WITH_NAMES("PullRaw", - "sssb", - SD_BUS_PARAM(url) - SD_BUS_PARAM(local_name) - SD_BUS_PARAM(verify_mode) - SD_BUS_PARAM(force), - "uo", - SD_BUS_PARAM(transfer_id) - SD_BUS_PARAM(transfer_path), - method_pull_tar_or_raw, - SD_BUS_VTABLE_UNPRIVILEGED), - SD_BUS_METHOD_WITH_NAMES("PullRawEx", - "sssst", - SD_BUS_PARAM(url) - SD_BUS_PARAM(local_name) - SD_BUS_PARAM(class) - SD_BUS_PARAM(verify_mode) - SD_BUS_PARAM(flags), - "uo", - SD_BUS_PARAM(transfer_id) - SD_BUS_PARAM(transfer_path), - method_pull_tar_or_raw, - SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("PullTar", + SD_BUS_ARGS("s", url, + "s", local_name, + "s", verify_mode, + "b", force), + SD_BUS_RESULT("u", transfer_id, + "o", transfer_path), + method_pull_tar_or_raw_or_oci, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("PullTarEx", + SD_BUS_ARGS("s", url, + "s", local_name, + "s", class, + "s", verify_mode, + "t", flags), + SD_BUS_RESULT("u", transfer_id, + "o", transfer_path), + method_pull_tar_or_raw_or_oci, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("PullRaw", + SD_BUS_ARGS("s", url, + "s", local_name, + "s", verify_mode, + "b", force), + SD_BUS_RESULT("u", transfer_id, + "o", transfer_path), + method_pull_tar_or_raw_or_oci, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("PullRawEx", + SD_BUS_ARGS("s", url, + "s", local_name, + "s", class, + "s", verify_mode, + "t", flags), + SD_BUS_RESULT("u", transfer_id, + "o", transfer_path), + method_pull_tar_or_raw_or_oci, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("PullOci", + SD_BUS_ARGS("s", ref, + "s", local_name, + "s", class, + "t", flags), + SD_BUS_RESULT("u", transfer_id, + "o", transfer_path), + method_pull_tar_or_raw_or_oci, + SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD_WITH_NAMES("ListTransfers", NULL,, "a(usssdo)", @@ -1864,7 +1902,13 @@ static int vl_method_pull(sd_varlink *link, sd_json_variant *parameters, sd_varl if (r != 0) return r; - if (!http_url_is_valid(p.remote) && !file_url_is_valid(p.remote)) + if (p.type == IMPORT_OCI) { + r = oci_ref_valid(p.remote); + if (r < 0) + return r; + if (r == 0) + return sd_varlink_error_invalid_parameter_name(link, "remote"); + } else if (!http_url_is_valid(p.remote) && !file_url_is_valid(p.remote)) return sd_varlink_error_invalid_parameter_name(link, "remote"); if (p.local && !image_name_is_valid(p.local)) @@ -1874,8 +1918,8 @@ static int vl_method_pull(sd_varlink *link, sd_json_variant *parameters, sd_varl TransferType tt = p.type == IMPORT_TAR ? TRANSFER_PULL_TAR : - p.type == IMPORT_RAW ? TRANSFER_PULL_RAW : _TRANSFER_TYPE_INVALID; - + p.type == IMPORT_RAW ? TRANSFER_PULL_RAW : + p.type == IMPORT_OCI ? TRANSFER_PULL_OCI : _TRANSFER_TYPE_INVALID; assert(tt >= 0); if (manager_find(m, tt, p.remote)) diff --git a/src/import/meson.build b/src/import/meson.build index 7735db7ff5..334c05368d 100644 --- a/src/import/meson.build +++ b/src/import/meson.build @@ -18,6 +18,7 @@ executables += [ 'dbus' : true, 'sources' : files( 'importd.c', + 'oci-util.c', ), 'extract' : files( 'oci-util.c', diff --git a/src/import/org.freedesktop.import1.conf b/src/import/org.freedesktop.import1.conf index f99ec56c79..d8b82388d5 100644 --- a/src/import/org.freedesktop.import1.conf +++ b/src/import/org.freedesktop.import1.conf @@ -106,6 +106,10 @@ send_interface="org.freedesktop.import1.Manager" send_member="PullRawEx"/> + + diff --git a/src/shared/import-util.c b/src/shared/import-util.c index 2644f59fda..35079a6537 100644 --- a/src/shared/import-util.c +++ b/src/shared/import-util.c @@ -125,6 +125,7 @@ int import_url_change_suffix( static const char* const import_type_table[_IMPORT_TYPE_MAX] = { [IMPORT_RAW] = "raw", [IMPORT_TAR] = "tar", + [IMPORT_OCI] = "oci", }; DEFINE_STRING_TABLE_LOOKUP(import_type, ImportType); diff --git a/src/shared/import-util.h b/src/shared/import-util.h index 2a594e1e61..a982d159ca 100644 --- a/src/shared/import-util.h +++ b/src/shared/import-util.h @@ -6,6 +6,7 @@ typedef enum ImportType { IMPORT_RAW, IMPORT_TAR, + IMPORT_OCI, _IMPORT_TYPE_MAX, _IMPORT_TYPE_INVALID = -EINVAL, } ImportType; From fbaf05bfeb0a359d001a30920b97b6e53c25fb8c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 25 Nov 2025 15:32:23 +0100 Subject: [PATCH 23/36] importctl: add 'pull-oci' client API --- src/import/importctl.c | 58 ++++++++++++++++++++++++++++++++++++++++++ src/import/meson.build | 2 ++ 2 files changed, 60 insertions(+) diff --git a/src/import/importctl.c b/src/import/importctl.c index eddddb86d1..c2ddd08f27 100644 --- a/src/import/importctl.c +++ b/src/import/importctl.c @@ -19,6 +19,7 @@ #include "import-util.h" #include "log.h" #include "main-func.h" +#include "oci-util.h" #include "os-util.h" #include "pager.h" #include "parse-argument.h" @@ -769,6 +770,61 @@ static int pull_raw(int argc, char *argv[], void *userdata) { return transfer_image_common(bus, m); } +static int pull_oci(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_free_ char *l = NULL; + const char *local, *remote; + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + r = settle_image_class(); + if (r < 0) + return r; + + remote = argv[1]; + _cleanup_free_ char *image = NULL; + r = oci_ref_parse(remote, /* ret_registry= */ NULL, &image, /* ret_tag= */ NULL); + if (r == -EINVAL) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Ref '%s' is not valid.", remote); + if (r < 0) + return log_error_errno(r, "Failed to determine if ref '%s' is valid.", remote); + + if (argc >= 3) + local = argv[2]; + else { + r = path_extract_filename(image, &l); + if (r < 0) + return log_error_errno(r, "Failed to get final component of reference: %m"); + + local = l; + } + + local = empty_or_dash_to_null(local); + + if (local) { + if (!image_name_is_valid(local)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Local name %s is not a suitable image name.", + local); + } + + r = bus_message_new_method_call(bus, &m, bus_import_mgr, "PullOci"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append( + m, + "ssst", + remote, + local, + image_class_to_string(arg_image_class), + (uint64_t) arg_import_flags & (IMPORT_FORCE|IMPORT_READ_ONLY)); + if (r < 0) + return bus_log_create_error(r); + + return transfer_image_common(bus, m); +} + static int list_transfers(int argc, char *argv[], void *userdata) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; @@ -1007,6 +1063,7 @@ static int help(int argc, char *argv[], void *userdata) { "\n%3$sCommands:%4$s\n" " pull-tar URL [NAME] Download a TAR container image\n" " pull-raw URL [NAME] Download a RAW container or VM image\n" + " pull-oci REF [NAME] Download an OCI container image\n" " import-tar FILE [NAME] Import a local TAR container image\n" " import-raw FILE [NAME] Import a local RAW container or VM image\n" " import-fs DIRECTORY [NAME] Import a local directory container image\n" @@ -1245,6 +1302,7 @@ static int importctl_main(int argc, char *argv[], sd_bus *bus) { { "export-tar", 2, 3, 0, export_tar }, { "export-raw", 2, 3, 0, export_raw }, { "pull-tar", 2, 3, 0, pull_tar }, + { "pull-oci", 2, 3, 0, pull_oci }, { "pull-raw", 2, 3, 0, pull_raw }, { "list-transfers", VERB_ANY, 1, VERB_DEFAULT, list_transfers }, { "cancel-transfer", 2, VERB_ANY, 0, cancel_transfer }, diff --git a/src/import/meson.build b/src/import/meson.build index 334c05368d..c0589ddd4e 100644 --- a/src/import/meson.build +++ b/src/import/meson.build @@ -80,6 +80,8 @@ executables += [ 'name' : 'importctl', 'public' : true, 'sources' : files('importctl.c'), + 'objects': ['systemd-importd'], + 'dependencies' : common_deps, }, generator_template + { 'name' : 'systemd-import-generator', From 6b22ac31afcfab53dc9b51d6b5f7862e52607923 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 27 Nov 2025 08:58:26 +0100 Subject: [PATCH 24/36] core: add PrivateUsers=managed --- man/systemd.exec.xml | 42 +++++++++++++++++++++++++----------------- src/core/exec-invoke.c | 28 ++++++++++++++++++++++++---- src/core/namespace.c | 1 + src/core/namespace.h | 1 + src/core/unit.c | 3 +++ 5 files changed, 54 insertions(+), 21 deletions(-) diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index 0ba3e011f2..87bd3fd92a 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -2202,16 +2202,17 @@ BindReadOnlyPaths=/var/lib/systemd PrivateUsers= - Takes a boolean argument or one of self, identity, - or full. Defaults to false. If enabled, sets up a new user namespace for the - executed processes and configures a user and group mapping. If set to a true value or - self, a minimal user and group mapping is configured that maps the - root user and group as well as the unit's own user and group to themselves and - everything else to the nobody user and group. This is useful to securely detach - the user and group databases used by the unit from the rest of the system, and thus to create an - effective sandbox environment. All files, directories, processes, IPC objects and other resources - owned by users/groups not equaling root or the unit's own will stay visible from - within the unit but appear owned by the nobody user and group. + Takes a boolean argument or one of self, + identity, full or managed. Defaults to + false. If enabled, sets up a new user namespace for the executed processes and configures a user and + group mapping. If set to a true value or self, a minimal user and group mapping is + configured that maps the root user and group as well as the unit's own user and + group to themselves and everything else to the nobody user and group. This is + useful to securely detach the user and group databases used by the unit from the rest of the system, + and thus to create an effective sandbox environment. All files, directories, processes, IPC objects + and other resources owned by users/groups not equaling root or the unit's own will + stay visible from within the unit but appear owned by the nobody user and + group. If the parameter is identity, user namespacing is set up with an identity mapping for the first 65536 UIDs/GIDs. Any UIDs/GIDs above 65536 will be mapped to the @@ -2224,14 +2225,21 @@ BindReadOnlyPaths=/var/lib/systemd to call setgroups() system calls (by setting /proc/pid/setgroups to allow). Similar to identity, this does not provide UID/GID isolation, but it does provide - process capability isolation. + process capability isolation. If this mode is enabled, all unit processes are run without privileges + in the host user namespace (regardless of whether the unit's own user/group is + root or not). Specifically this means that the process will have zero process + capabilities on the host's user namespace, but full capabilities within the service's user + namespace. Settings such as CapabilityBoundingSet= will affect only the latter, + and there's no way to acquire additional capabilities in the host's user namespace. - If this mode is enabled, all unit processes are run without privileges in the host user - namespace (regardless of whether the unit's own user/group is root or not). Specifically - this means that the process will have zero process capabilities on the host's user namespace, but - full capabilities within the service's user namespace. Settings such as - CapabilityBoundingSet= will affect only the latter, and there's no way to acquire - additional capabilities in the host's user namespace. + If the paramater is managed a transient, dynamically allocated range of + 65536 UIDs/GIDs is allocated for the unit, and a UID/GID mapping is assigned to the unit's process + so the UID/GID 0 from inside the unit maps to the first UID/GID of the allocated mapping. Note that + in this mode the UID/GID the service process will run as is different depending if looking from the + host side (where it will be a high, dynamically assigned UID) or from inside the unit (where it will + be 0). Also note that this mode will enable file system UID mapping for the file systems this service + accesses, mapping the "foreign" UID range on disk to the selected dynamic UID range at + runtime. When this setting is set up by a per-user instance of the service manager, the mapping of the root user and group to itself is omitted (unless the user manager is root). diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 298fd0754e..48e3902633 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -57,6 +57,7 @@ #include "mountpoint-util.h" #include "namespace-util.h" #include "nsflags.h" +#include "nsresource.h" #include "open-file.h" #include "osc-context.h" #include "pam-util.h" @@ -2396,10 +2397,10 @@ static int setup_private_users_child(int unshare_ready_fd, const char *uid_map, static int setup_private_users( PrivateUsers private_users, - uid_t ouid, - gid_t ogid, - uid_t uid, - gid_t gid, + uid_t ouid, /* service manager uid */ + gid_t ogid, /* service manager gid */ + uid_t uid, /* unit uid */ + gid_t gid, /* unit gid */ bool allow_setgroups) { _cleanup_free_ char *uid_map = NULL, *gid_map = NULL; @@ -2425,6 +2426,25 @@ static int setup_private_users( case PRIVATE_USERS_NO: return 0; /* Early exit */ + case PRIVATE_USERS_MANAGED: { + if (uid != 0 || gid != 0) + return log_debug_errno(SYNTHETIC_ERRNO(EPERM), "When allocating dynamic user namespace range, target UID/GID must be root, refusing."); + + _cleanup_close_ int userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K); + if (userns_fd < 0) + return userns_fd; + + if (setns(userns_fd, CLONE_NEWUSER) < 0) + return log_debug_errno(errno, "Failed to join freshly allocated user namespace: %m"); + + /* In "managed" mode the originating UID is not mapped hence we need to explicitly become root in the new userns now. */ + r = reset_uid_gid(); + if (r < 0) + return log_debug_errno(r, "Failed to reset UID/GID to root: %m"); + + return 1; /* Early exit */ + } + case PRIVATE_USERS_IDENTITY: uid_map = strdup("0 0 65536\n"); if (!uid_map) diff --git a/src/core/namespace.c b/src/core/namespace.c index 6927845b9c..56fc268fef 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -4137,6 +4137,7 @@ static const char* const private_users_table[_PRIVATE_USERS_MAX] = { [PRIVATE_USERS_SELF] = "self", [PRIVATE_USERS_IDENTITY] = "identity", [PRIVATE_USERS_FULL] = "full", + [PRIVATE_USERS_MANAGED] = "managed", }; DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(private_users, PrivateUsers, PRIVATE_USERS_SELF); diff --git a/src/core/namespace.h b/src/core/namespace.h index 4f5e6546bd..4b62debf2f 100644 --- a/src/core/namespace.h +++ b/src/core/namespace.h @@ -70,6 +70,7 @@ typedef enum PrivateUsers { PRIVATE_USERS_SELF, PRIVATE_USERS_IDENTITY, PRIVATE_USERS_FULL, + PRIVATE_USERS_MANAGED, _PRIVATE_USERS_MAX, _PRIVATE_USERS_INVALID = -EINVAL, } PrivateUsers; diff --git a/src/core/unit.c b/src/core/unit.c index d468f1303d..b636e097cd 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -4328,6 +4328,9 @@ static int unit_verify_contexts(const Unit *u) { if (ec->pam_name && kc && !IN_SET(kc->kill_mode, KILL_CONTROL_GROUP, KILL_MIXED)) return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOEXEC), "Unit has PAM enabled. Kill mode must be set to 'control-group' or 'mixed'. Refusing."); + if ((ec->user || ec->dynamic_user) && ec->private_users == PRIVATE_USERS_MANAGED) + return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOEXEC), "PrivateUsers=managed may not be used in combination with User=/DynamicUser=, refusing."); + return 0; } From 051dfed9b6be64fdf93345b05c87d586e0670514 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 19 Feb 2026 15:03:50 +0100 Subject: [PATCH 25/36] private --- src/core/exec-invoke.c | 119 ++++++++++++++++++++++++++++++----------- src/core/unit.c | 4 +- 2 files changed, 90 insertions(+), 33 deletions(-) diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 48e3902633..06e6a3081f 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -82,6 +82,7 @@ #include "strv.h" #include "strxcpyx.h" #include "terminal-util.h" +#include "uid-range.h" #include "user-util.h" #include "utmp-wtmp.h" #include "vpick.h" @@ -2397,10 +2398,12 @@ static int setup_private_users_child(int unshare_ready_fd, const char *uid_map, static int setup_private_users( PrivateUsers private_users, - uid_t ouid, /* service manager uid */ - gid_t ogid, /* service manager gid */ - uid_t uid, /* unit uid */ - gid_t gid, /* unit gid */ + uid_t saved_uid, /* service manager uid */ + gid_t saved_gid, /* service manager gid */ + uid_t *uid, /* unit uid (seen from inside) [input+output] */ + gid_t *gid, /* unit gid (ditto) [input+output] */ + uid_t *outside_uid, /* uid seen from the outside (which is the same as *uid, except of userns is used) */ + gid_t *outside_gid, /* gid seen from the outside (similar) */ bool allow_setgroups) { _cleanup_free_ char *uid_map = NULL, *gid_map = NULL; @@ -2411,6 +2414,11 @@ static int setup_private_users( ssize_t n; int r; + assert(uid); + assert(gid); + assert(outside_uid); + assert(outside_gid); + /* Set up a user namespace and map the original UID/GID (IDs from before any user or group changes, i.e. * the IDs from the user or system manager(s)) to itself, the selected UID/GID to itself, and everything else to * nobody. In order to be able to write this mapping we need CAP_SETUID in the original user namespace, which @@ -2427,13 +2435,23 @@ static int setup_private_users( return 0; /* Early exit */ case PRIVATE_USERS_MANAGED: { - if (uid != 0 || gid != 0) + if (uid_is_valid(*uid) || uid_is_valid(*gid)) return log_debug_errno(SYNTHETIC_ERRNO(EPERM), "When allocating dynamic user namespace range, target UID/GID must be root, refusing."); _cleanup_close_ int userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K); if (userns_fd < 0) return userns_fd; + _cleanup_(uid_range_freep) UIDRange *uid_range = NULL; + r = uid_range_load_userns_by_fd(userns_fd, UID_RANGE_USERNS_OUTSIDE, &uid_range); + if (r < 0) + return log_debug_errno(r, "Failed to read outside userns UID range: %m"); + + _cleanup_(uid_range_freep) UIDRange *gid_range = NULL; + r = uid_range_load_userns_by_fd(userns_fd, GID_RANGE_USERNS_OUTSIDE, &gid_range); + if (r < 0) + return log_debug_errno(r, "Failed to read outside userns GID range: %m"); + if (setns(userns_fd, CLONE_NEWUSER) < 0) return log_debug_errno(errno, "Failed to join freshly allocated user namespace: %m"); @@ -2442,6 +2460,13 @@ static int setup_private_users( if (r < 0) return log_debug_errno(r, "Failed to reset UID/GID to root: %m"); + /* Now report are new UIDs/GIDS, both inside and outside */ + *uid = 0; + *gid = 0; + + *outside_uid = uid_range_base(uid_range); + *outside_gid = uid_range_base(gid_range); + return 1; /* Early exit */ } @@ -2465,15 +2490,15 @@ static int setup_private_users( case PRIVATE_USERS_SELF: /* Can only set up multiple mappings with CAP_SETUID. */ - if (uid_is_valid(uid) && uid != ouid && have_effective_cap(CAP_SETUID) > 0) + if (uid_is_valid(*uid) && *uid != saved_uid && have_effective_cap(CAP_SETUID) > 0) r = asprintf(&uid_map, UID_FMT " " UID_FMT " 1\n" /* Map $OUID → $OUID */ UID_FMT " " UID_FMT " 1\n", /* Map $UID → $UID */ - ouid, ouid, uid, uid); + saved_uid, saved_uid, *uid, *uid); else r = asprintf(&uid_map, UID_FMT " " UID_FMT " 1\n", /* Map $OUID → $OUID */ - ouid, ouid); + saved_uid, saved_uid); if (r < 0) return -ENOMEM; @@ -2499,15 +2524,15 @@ static int setup_private_users( case PRIVATE_USERS_SELF: /* Can only set up multiple mappings with CAP_SETGID. */ - if (gid_is_valid(gid) && gid != ogid && have_effective_cap(CAP_SETGID) > 0) + if (gid_is_valid(*gid) && *gid != saved_gid && have_effective_cap(CAP_SETGID) > 0) r = asprintf(&gid_map, GID_FMT " " GID_FMT " 1\n" /* Map $OGID → $OGID */ GID_FMT " " GID_FMT " 1\n", /* Map $GID → $GID */ - ogid, ogid, gid, gid); + saved_gid, saved_gid, *gid, *gid); else r = asprintf(&gid_map, GID_FMT " " GID_FMT " 1\n", /* Map $OGID -> $OGID */ - ogid, ogid); + saved_gid, saved_gid); if (r < 0) return -ENOMEM; break; @@ -5111,10 +5136,10 @@ int exec_invoke( #if HAVE_SECCOMP uint64_t saved_bset = 0; #endif - uid_t saved_uid = getuid(); - gid_t saved_gid = getgid(); - uid_t uid = UID_INVALID; - gid_t gid = GID_INVALID; + /* saved_uid → where we started from; uid → where we ended up in; outside_uid → where we ended up in, + * but seen from the outside (covers userns mappings) */ + uid_t uid = UID_INVALID, saved_uid = getuid(), outside_uid = UID_INVALID; + gid_t gid = GID_INVALID, saved_gid = getgid(), outside_gid = GID_INVALID; int secure_bits; _cleanup_free_ gid_t *gids = NULL, *gids_after_pam = NULL; int ngids = 0, ngids_after_pam = 0; @@ -5284,7 +5309,9 @@ int exec_invoke( return log_error_errno(errno, "Failed to update environment: %m"); } - if (context->dynamic_user && runtime->dynamic_creds) { + if (exec_context_get_effective_private_users(context, params) == PRIVATE_USERS_MANAGED) + log_debug("Running with a managed user namespace, not initializing UIDs/GIDs."); + else if (context->dynamic_user && runtime->dynamic_creds) { _cleanup_strv_free_ char **suggested_paths = NULL; /* On top of that, make sure we bypass our own NSS module nss-systemd comprehensively for any NSS @@ -5366,20 +5393,22 @@ int exec_invoke( } } - /* Initialize user supplementary groups and get SupplementaryGroups= ones */ - ngids = get_supplementary_groups(context, username, gid, &gids); - if (ngids < 0) { - *exit_status = EXIT_GROUP; - return log_error_errno(ngids, "Failed to determine supplementary groups: %m"); - } + if (exec_context_get_effective_private_users(context, params) != PRIVATE_USERS_MANAGED) { + /* Initialize user supplementary groups and get SupplementaryGroups= ones */ + ngids = get_supplementary_groups(context, username, gid, &gids); + if (ngids < 0) { + *exit_status = EXIT_GROUP; + return log_error_errno(ngids, "Failed to determine supplementary groups: %m"); + } - r = send_user_lookup(params->unit_id, params->user_lookup_fd, uid, gid); - if (r < 0) { - *exit_status = EXIT_USER; - return log_error_errno(r, "Failed to send user credentials to PID1: %m"); - } + r = send_user_lookup(params->unit_id, params->user_lookup_fd, uid, gid); + if (r < 0) { + *exit_status = EXIT_USER; + return log_error_errno(r, "Failed to send user credentials to PID1: %m"); + } - params->user_lookup_fd = safe_close(params->user_lookup_fd); + params->user_lookup_fd = safe_close(params->user_lookup_fd); + } r = acquire_home(context, &pwent_home, &home_buffer); if (r < 0) { @@ -5909,7 +5938,15 @@ int exec_invoke( /* The kernel requires /proc/pid/setgroups be set to "deny" prior to writing /proc/pid/gid_map in * unprivileged user namespaces. */ - r = setup_private_users(pu, saved_uid, saved_gid, uid, gid, /* allow_setgroups= */ false); + r = setup_private_users( + pu, + saved_uid, + saved_gid, + &uid, + &gid, + &outside_uid, + &outside_gid, + /* allow_setgroups= */ false); /* If it was requested explicitly and we can't set it up, fail early. Otherwise, continue and let * the actual requested operations fail (or silently continue). */ if (r < 0 && context->private_users != PRIVATE_USERS_NO) { @@ -5989,8 +6026,15 @@ int exec_invoke( } else if (needs_sandboxing && !userns_set_up) { PrivateUsers pu = exec_context_get_effective_private_users(context, params); - r = setup_private_users(pu, saved_uid, saved_gid, uid, gid, - /* allow_setgroups= */ pu == PRIVATE_USERS_FULL); + r = setup_private_users( + pu, + saved_uid, + saved_gid, + &uid, + &gid, + &outside_uid, + &outside_gid, + /* allow_setgroups= */ pu == PRIVATE_USERS_FULL); if (r < 0) { *exit_status = EXIT_USER; return log_error_errno(r, "Failed to set up user namespacing: %m"); @@ -5999,6 +6043,19 @@ int exec_invoke( log_debug("Set up privileged user namespace"); } + if (params->user_lookup_fd >= 0) { + /* If we haven't sent the UIDs/GIDs we settled on upstream yet, let's do so now, as we + * finally know our final UID/GID range */ + + r = send_user_lookup(params->unit_id, params->user_lookup_fd, outside_uid, outside_gid); + if (r < 0) { + *exit_status = EXIT_USER; + return log_error_errno(r, "Failed to send user credentials to PID1: %m"); + } + + params->user_lookup_fd = safe_close(params->user_lookup_fd); + } + /* Call setup_delegated_namespaces() the second time to unshare all delegated namespaces. */ r = setup_delegated_namespaces( context, diff --git a/src/core/unit.c b/src/core/unit.c index b636e097cd..78c67c9833 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -4328,8 +4328,8 @@ static int unit_verify_contexts(const Unit *u) { if (ec->pam_name && kc && !IN_SET(kc->kill_mode, KILL_CONTROL_GROUP, KILL_MIXED)) return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOEXEC), "Unit has PAM enabled. Kill mode must be set to 'control-group' or 'mixed'. Refusing."); - if ((ec->user || ec->dynamic_user) && ec->private_users == PRIVATE_USERS_MANAGED) - return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOEXEC), "PrivateUsers=managed may not be used in combination with User=/DynamicUser=, refusing."); + if ((ec->user || ec->dynamic_user || ec->group || ec->pam_name) && ec->private_users == PRIVATE_USERS_MANAGED) + return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOEXEC), "PrivateUsers=managed may not be used in combination with User=/DynamicUser=/Group=/PAMName=, refusing."); return 0; } From 26f80a9d337bac087889bc9244ea4fad8ffb294f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 28 Nov 2025 12:21:41 +0100 Subject: [PATCH 26/36] namespace: port mount_private_apivfs() to fsopen() and friends This is not just refactoring, but has the big benefit that it makes us indepdendent from a temporary directory we might not have enough access to create. (This matters with the new PrivateUsers=managed). --- src/core/namespace.c | 60 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/src/core/namespace.c b/src/core/namespace.c index 56fc268fef..680ee26285 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -1462,16 +1462,45 @@ static int mount_private_apivfs( assert(entry_path); assert(bind_source); + bool noprivs = false; + + /* First, check if we have enough privileges to mount a new instance. */ + _cleanup_close_ int mount_fd = make_fsmount( + LOG_DEBUG, + /* what= */ fstype, + fstype, + MS_NOSUID|MS_NOEXEC|MS_NODEV, + opts, + /* userns_fd= */ -EBADF); + if (ERRNO_IS_NEG_PRIVILEGE(mount_fd)) + noprivs = true; + else if (ERRNO_IS_NEG_NOT_SUPPORTED(mount_fd)) { + /* Fallback for kernels lacking mount_setattr() */ + + // FIXME: This compatibility code path shall be removed once kernel 5.12 + // becomes the new minimal baseline + + r = create_temporary_mount_point(scope, &temporary_mount); + if (r < 0) + return r; + + r = mount_nofollow_verbose( + LOG_DEBUG, + fstype, + temporary_mount, + fstype, + MS_NOSUID|MS_NOEXEC|MS_NODEV, + opts); + if (ERRNO_IS_NEG_PRIVILEGE(r)) + noprivs = true; + else if (r < 0) + return r; + } else if (mount_fd < 0) + return log_debug_errno(mount_fd, "Failed to make file system mount: %m"); + (void) mkdir_p_label(entry_path, 0755); - /* First, check if we have enough privileges to mount a new instance. Note, a new sysfs instance - * cannot be mounted on an already existing mount. Let's use a temporary place. */ - r = create_temporary_mount_point(scope, &temporary_mount); - if (r < 0) - return r; - - r = mount_nofollow_verbose(LOG_DEBUG, fstype, temporary_mount, fstype, MS_NOSUID|MS_NOEXEC|MS_NODEV, opts); - if (ERRNO_IS_NEG_PRIVILEGE(r)) { + if (noprivs) { /* When we do not have enough privileges to mount a new instance, fall back to use an * existing mount. */ @@ -1490,8 +1519,6 @@ static int mount_private_apivfs( return 1; } - if (r < 0) - return r; /* OK. We have a new mount instance. Let's clear an existing mount and its submounts. */ r = umount_recursive(entry_path, /* flags= */ 0); @@ -1499,9 +1526,16 @@ static int mount_private_apivfs( log_debug_errno(r, "Failed to unmount directories below '%s', ignoring: %m", entry_path); /* Then, move the new mount instance. */ - r = mount_nofollow_verbose(LOG_DEBUG, temporary_mount, entry_path, /* fstype= */ NULL, MS_MOVE, /* options= */ NULL); - if (r < 0) - return r; + if (mount_fd >= 0) { + r = RET_NERRNO(move_mount(mount_fd, "", -EBADF, entry_path, MOVE_MOUNT_F_EMPTY_PATH)); + if (r < 0) + return log_debug_errno(r, "Failed to attach '%s' to '%s': %m", fstype, entry_path); + } else if (temporary_mount) { + r = mount_nofollow_verbose(LOG_DEBUG, temporary_mount, entry_path, /* fstype= */ NULL, MS_MOVE, /* options= */ NULL); + if (r < 0) + return r; + } else + assert_not_reached(); /* We mounted a new instance now. Let's bind mount the children over now. This matters for nspawn * where a bunch of files are overmounted, in particular the boot id. */ From fe487d3670040a7e019b5e01260603f970f8af4c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 28 Nov 2025 16:18:07 +0100 Subject: [PATCH 27/36] namespace: extend bind mount ignore field to permission issues A later commit will add transient allocation of user namespaces with dynamic UID range assignment. That creates certain permission issues. Let's hence allow them to be handled gracefully in case the 'ignore' field is set for a mount. --- man/systemd.exec.xml | 20 ++++++++++---------- src/core/namespace.c | 18 +++++++++++++----- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index 87bd3fd92a..59b6919a75 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -450,16 +450,16 @@ BindPaths= BindReadOnlyPaths= - Configures unit-specific bind mounts. A bind mount makes a particular file or directory - available at an additional place in the unit's view of the file system. Any bind mounts created with this - option are specific to the unit, and are not visible in the host's mount table. This option expects a - whitespace separated list of bind mount definitions. Each definition consists of a colon-separated triple of - source path, destination path and option string, where the latter two are optional. If only a source path is - specified the source and destination is taken to be the same. The option string may be either - rbind or norbind for configuring a recursive or non-recursive bind - mount. If the destination path is omitted, the option string must be omitted too. - Each bind mount definition may be prefixed with -, in which case it will be ignored - when its source path does not exist. + Configures unit-specific bind mounts. A bind mount makes a particular file or + directory available at an additional place in the unit's view of the file system. Any bind mounts + created with this option are specific to the unit, and are not visible in the host's mount + table. This option expects a whitespace separated list of bind mount definitions. Each definition + consists of a colon-separated triple of source path, destination path and option string, where the + latter two are optional. If only a source path is specified the source and destination is taken to be + the same. The option string may be either rbind or norbind for + configuring a recursive or non-recursive bind mount. If the destination path is omitted, the option + string must be omitted too. Each bind mount definition may be prefixed with -, in + which case it will be ignored when its source path does not exist or is not accessible. BindPaths= creates regular writable bind mounts (unless the source file system mount is already marked read-only), while BindReadOnlyPaths= creates read-only bind mounts. These diff --git a/src/core/namespace.c b/src/core/namespace.c index 680ee26285..e348d26c43 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -2016,12 +2016,20 @@ static int apply_one_mount( } r = chase(mount_entry_source(m), NULL, CHASE_TRAIL_SLASH|CHASE_TRIGGER_AUTOFS, &chased, NULL); - if (r == -ENOENT && m->ignore) { - log_debug_errno(r, "Path %s does not exist, ignoring.", mount_entry_source(m)); - return 0; - } - if (r < 0) + if (r < 0) { + if (m->ignore) { + if (r == -ENOENT) { + log_debug_errno(r, "Path '%s' does not exist, ignoring.", mount_entry_source(m)); + return 0; + } + if (ERRNO_IS_NEG_PRIVILEGE(r)) { + log_debug_errno(r, "Path '%s' is not accessible, ignoring: %m", mount_entry_source(m)); + return 0; + } + } + return log_debug_errno(r, "Failed to follow symlinks on %s: %m", mount_entry_source(m)); + } log_debug("Followed source symlinks %s %s %s.", mount_entry_source(m), glyph(GLYPH_ARROW_RIGHT), chased); From 68ed4f3c669b3b175222eb6e3ba11cc34fa03b12 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 28 Nov 2025 16:28:42 +0100 Subject: [PATCH 28/36] mountfsd,nsresource: allow recycling mountfsd/nsresourced client connections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So far we opened a new Varlink connection for every mountfsd/nsresourced method call. Given each tool only does a very small number of calls (usually 1…5) on them and the connections are cheap this is not too wasteful. Nonetheless, let's do something about it, and allow reusing the connection for multiple calls. This not only makes things a bit more efficient, but has one more important benefit: Varlink connections pin the security context of the client when connecting. This means that varlink method calls done with a connection established while some code was privileged will still operate as privieged once privs are dropped, until the connection is closed. This pinning effect is really nice, as it gives us behaviour in a "capability system" like scheme. Later code is going to use that to continue doing certain priv userns ops even after unsharing userns and becoming fully unpriv. --- src/core/exec-invoke.c | 61 ++++++++++++++++---- src/core/namespace.c | 10 +++- src/core/namespace.h | 2 + src/dissect/dissect.c | 6 +- src/import/export-tar.c | 7 ++- src/import/import-common.c | 5 +- src/import/import-tar.c | 20 ++++++- src/import/pull-oci.c | 20 ++++++- src/import/pull-tar.c | 46 +++++++++++++-- src/mountfsd/mountwork.c | 12 +++- src/mstack/mstack-tool.c | 1 + src/nspawn/nspawn-cgroup.c | 5 +- src/nspawn/nspawn.c | 36 +++++++++++- src/portable/portable.c | 40 ++++++++++--- src/shared/discover-image.c | 22 +++++++- src/shared/dissect-image.c | 98 +++++++++++++++++++------------- src/shared/dissect-image.h | 22 ++++++-- src/shared/mstack.c | 18 +++++- src/shared/mstack.h | 4 +- src/shared/nsresource.c | 110 +++++++++++++++++++++--------------- src/shared/nsresource.h | 22 ++++++-- src/test/test-mstack.c | 1 + src/test/test-nsresource.c | 11 +++- src/vmspawn/vmspawn.c | 10 +++- 24 files changed, 444 insertions(+), 145 deletions(-) diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 06e6a3081f..888a2555fb 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -14,6 +14,7 @@ #include #include "sd-messages.h" +#include "sd-varlink.h" #include "apparmor-util.h" /* IWYU pragma: keep */ #include "argv-util.h" @@ -2397,6 +2398,7 @@ static int setup_private_users_child(int unshare_ready_fd, const char *uid_map, } static int setup_private_users( + sd_varlink *nsresource_link, PrivateUsers private_users, uid_t saved_uid, /* service manager uid */ gid_t saved_gid, /* service manager gid */ @@ -2438,7 +2440,10 @@ static int setup_private_users( if (uid_is_valid(*uid) || uid_is_valid(*gid)) return log_debug_errno(SYNTHETIC_ERRNO(EPERM), "When allocating dynamic user namespace range, target UID/GID must be root, refusing."); - _cleanup_close_ int userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K); + _cleanup_close_ int userns_fd = nsresource_allocate_userns( + nsresource_link, + /* name= */ NULL, + NSRESOURCE_UIDS_64K); if (userns_fd < 0) return userns_fd; @@ -3786,6 +3791,7 @@ static int apply_mount_namespace( PidRef *bpffs_pidref, int bpffs_socket_fd, int bpffs_errno_pipe, + sd_varlink *mountfsd_link, char **reterr_path) { _cleanup_(verity_settings_done) VeritySettings verity = VERITY_SETTINGS_DEFAULT; @@ -4005,6 +4011,8 @@ static int apply_mount_namespace( .bpffs_pidref = bpffs_pidref, .bpffs_socket_fd = bpffs_socket_fd, .bpffs_errno_pipe = bpffs_errno_pipe, + + .mountfsd_link = mountfsd_link, }; r = setup_namespace(¶meters, reterr_path); @@ -4659,6 +4667,7 @@ static int setup_delegated_namespaces( PidRef *bpffs_pidref, int bpffs_socket_fd, int bpffs_errno_pipe, + sd_varlink *mountfsd_link, int *reterr_exit_status) { int r; @@ -4773,18 +4782,20 @@ static int setup_delegated_namespaces( exec_namespace_is_delegated(context, params, have_cap_sys_admin, CLONE_NEWNS) == delegate) { _cleanup_free_ char *error_path = NULL; - r = apply_mount_namespace(command->flags, - context, - params, - runtime, + r = apply_mount_namespace( + command->flags, + context, + params, + runtime, memory_pressure_path, - needs_sandboxing, - uid, - gid, - bpffs_pidref, - bpffs_socket_fd, - bpffs_errno_pipe, - &error_path); + needs_sandboxing, + uid, + gid, + bpffs_pidref, + bpffs_socket_fd, + bpffs_errno_pipe, + mountfsd_link, + &error_path); if (r < 0) { *reterr_exit_status = EXIT_NAMESPACE; return log_error_errno(r, "Failed to set up mount namespacing%s%s: %m", @@ -5744,6 +5755,24 @@ int exec_invoke( } } + _cleanup_(sd_varlink_unrefp) sd_varlink *mountfsd_link = NULL, *nsresource_link = NULL; + if (needs_sandboxing && + exec_context_get_effective_private_users(context, params) == PRIVATE_USERS_MANAGED) { + + /* In managed mode we need to allocate a userns via nsresource, and then assign mounts to + * it. We must do so with our original privileges (since after creating the userns, we might + * simply not have the necessary privs for the IPC calls anymore), hence do this here, ahead + * of time. */ + + r = mountfsd_connect(&mountfsd_link); + if (r < 0) + return log_error_errno(r, "Failed to connect to mountfsd: %m"); + + r = nsresource_connect(&nsresource_link); + if (r < 0) + return log_error_errno(r, "Failed to connect to nsresourced: %m"); + } + needs_mount_namespace = exec_needs_mount_namespace(context, params, runtime); for (ExecDirectoryType dt = 0; dt < _EXEC_DIRECTORY_TYPE_MAX; dt++) { @@ -5939,6 +5968,7 @@ int exec_invoke( /* The kernel requires /proc/pid/setgroups be set to "deny" prior to writing /proc/pid/gid_map in * unprivileged user namespaces. */ r = setup_private_users( + nsresource_link, pu, saved_uid, saved_gid, @@ -5977,6 +6007,7 @@ int exec_invoke( &bpffs_pidref, bpffs_socket_fd, bpffs_errno_pipe, + mountfsd_link, exit_status); if (r < 0) return r; @@ -6027,6 +6058,7 @@ int exec_invoke( PrivateUsers pu = exec_context_get_effective_private_users(context, params); r = setup_private_users( + nsresource_link, pu, saved_uid, saved_gid, @@ -6071,10 +6103,15 @@ int exec_invoke( &bpffs_pidref, bpffs_socket_fd, bpffs_errno_pipe, + mountfsd_link, exit_status); if (r < 0) return r; + /* We are done now with the nsresourced/mountfsd shenanigans, let's close the connections */ + nsresource_link = sd_varlink_unref(nsresource_link); + mountfsd_link = sd_varlink_unref(mountfsd_link); + /* Kill unnecessary process, for the case that e.g. when the bpffs mount point is hidden. */ pidref_done_sigkill_wait(&bpffs_pidref); diff --git a/src/core/namespace.c b/src/core/namespace.c index e348d26c43..504f80cb63 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -2667,6 +2667,7 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { return log_debug_errno(userns_fd, "Failed to open our own user namespace: %m"); r = mountfsd_mount_image( + p->mountfsd_link, p->root_image, userns_fd, p->root_image_options, @@ -2678,7 +2679,6 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { return r; } } - } else if (p->root_mstack) { if (namespace_read_only(p)) mstack_flags |= MSTACK_RDONLY; @@ -2693,7 +2693,13 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { return log_debug_errno(userns_fd, "Failed to open our own user namespace: %m"); } - r = mstack_open_images(mstack, userns_fd, p->root_image_policy, /* image_filter= */ NULL, mstack_flags); + r = mstack_open_images( + mstack, + p->mountfsd_link, + userns_fd, + p->root_image_policy, + /* image_filter= */ NULL, + mstack_flags); if (r < 0) return r; } diff --git a/src/core/namespace.h b/src/core/namespace.h index 4b62debf2f..26b0bf8ff2 100644 --- a/src/core/namespace.h +++ b/src/core/namespace.h @@ -205,6 +205,8 @@ typedef struct NamespaceParameters { PidRef *bpffs_pidref; int bpffs_socket_fd; int bpffs_errno_pipe; + + sd_varlink *mountfsd_link; } NamespaceParameters; int setup_namespace(const NamespaceParameters *p, char **reterr_path); diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 7303cfc3bd..33ddb53d0a 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -2247,12 +2247,16 @@ static int run(int argc, char *argv[]) { /* Don't run things in private userns, if the mount shall be attached to the host * or if we're copying from/to the host. */ if (!IN_SET(arg_action, ACTION_MOUNT, ACTION_WITH, ACTION_COPY_FROM, ACTION_COPY_TO)) { - userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K); /* allocate 64K users by default */ + userns_fd = nsresource_allocate_userns( + /* vl= */ NULL, + /* name= */ NULL, + NSRESOURCE_UIDS_64K); /* 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( + /* vl= */ NULL, arg_image, userns_fd, /* options= */ NULL, diff --git a/src/import/export-tar.c b/src/import/export-tar.c index 81fab3714b..22f731de57 100644 --- a/src/import/export-tar.c +++ b/src/import/export-tar.c @@ -363,7 +363,12 @@ int tar_export_start( return log_error_errno(r, "Failed to open '%s': %m", p); _cleanup_close_ int mapped_fd = -EBADF; - r = mountfsd_mount_directory_fd(directory_fd, e->userns_fd, DISSECT_IMAGE_FOREIGN_UID, &mapped_fd); + r = mountfsd_mount_directory_fd( + /* vl= */ NULL, + directory_fd, + e->userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + &mapped_fd); if (r < 0) return log_error_errno(r, "Failed to mount directory via mountfsd: %m"); diff --git a/src/import/import-common.c b/src/import/import-common.c index 0c448355fd..0a5144f94e 100644 --- a/src/import/import-common.c +++ b/src/import/import-common.c @@ -375,7 +375,10 @@ int import_make_foreign_userns(int *userns_fd) { if (*userns_fd >= 0) return 0; - *userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K); /* allocate 64K users */ + *userns_fd = nsresource_allocate_userns( + /* vl= */ NULL, + /* name= */ NULL, + NSRESOURCE_UIDS_64K); /* allocate 64K users */ if (*userns_fd < 0) return log_error_errno(*userns_fd, "Failed to allocate transient user namespace: %m"); diff --git a/src/import/import-tar.c b/src/import/import-tar.c index b3d3fc6104..5e74de896e 100644 --- a/src/import/import-tar.c +++ b/src/import/import-tar.c @@ -4,6 +4,7 @@ #include "sd-daemon.h" #include "sd-event.h" +#include "sd-varlink.h" #include "alloc-util.h" #include "btrfs-util.h" @@ -256,12 +257,27 @@ static int tar_import_fork_tar(TarImport *i) { if (r < 0) return r; + _cleanup_(sd_varlink_unrefp) sd_varlink *mountfsd_link = NULL; + r = mountfsd_connect(&mountfsd_link); + if (r < 0) + return log_error_errno(r, "Failed to connect to mountfsd: %m"); + _cleanup_close_ int directory_fd = -EBADF; - r = mountfsd_make_directory(d, MODE_INVALID, /* flags= */ 0, &directory_fd); + r = mountfsd_make_directory( + mountfsd_link, + d, + MODE_INVALID, + /* flags= */ 0, + &directory_fd); if (r < 0) return log_error_errno(r, "Failed to make directory via mountfsd: %m"); - r = mountfsd_mount_directory_fd(directory_fd, i->userns_fd, DISSECT_IMAGE_FOREIGN_UID, &i->tree_fd); + r = mountfsd_mount_directory_fd( + mountfsd_link, + directory_fd, + i->userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + &i->tree_fd); if (r < 0) return log_error_errno(r, "Failed mount directory via mountfsd: %m"); } else { diff --git a/src/import/pull-oci.c b/src/import/pull-oci.c index d55bd43f57..3abb8777d1 100644 --- a/src/import/pull-oci.c +++ b/src/import/pull-oci.c @@ -464,12 +464,27 @@ static int oci_pull_job_on_open_disk(PullJob *j) { if (r < 0) return r; + _cleanup_(sd_varlink_unrefp) sd_varlink *mountfsd_link = NULL; + r = mountfsd_connect(&mountfsd_link); + if (r < 0) + return log_error_errno(r, "Failed to connect to mountfsd: %m"); + _cleanup_close_ int directory_fd = -EBADF; - r = mountfsd_make_directory(st->temp_path, MODE_INVALID, /* flags= */ 0, &directory_fd); + r = mountfsd_make_directory( + mountfsd_link, + st->temp_path, + MODE_INVALID, + /* flags= */ 0, + &directory_fd); if (r < 0) return log_error_errno(r, "Failed to make directory via mountfsd: %m"); - r = mountfsd_mount_directory_fd(directory_fd, i->userns_fd, DISSECT_IMAGE_FOREIGN_UID, &st->tree_fd); + r = mountfsd_mount_directory_fd( + mountfsd_link, + directory_fd, + i->userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + &st->tree_fd); if (r < 0) return log_error_errno(r, "Failed to mount directory via mountsd: %m"); } else { @@ -1140,6 +1155,7 @@ static int oci_pull_save_mstack(OciPull *i) { return r; r = mountfsd_make_directory_fd( + /* vl= */ NULL, dir_fd, "rw", 0755, diff --git a/src/import/pull-tar.c b/src/import/pull-tar.c index b3d8092106..ae763879b7 100644 --- a/src/import/pull-tar.c +++ b/src/import/pull-tar.c @@ -2,6 +2,7 @@ #include "sd-daemon.h" #include "sd-event.h" +#include "sd-varlink.h" #include "alloc-util.h" #include "btrfs-util.h" @@ -276,6 +277,11 @@ static int tar_pull_make_local_copy(TarPull *p) { if (r < 0) return r; + _cleanup_(sd_varlink_unrefp) sd_varlink *mountfsd_link = NULL; + r = mountfsd_connect(&mountfsd_link); + if (r < 0) + return log_error_errno(r, "Failed to connect to mountsd: %m"); + /* Usually, tar_pull_job_on_open_disk_tar() would allocate ->tree_fd for us, but if * already downloaded the image before, and are just making a copy of the original * download, we need to open ->tree_fd now */ @@ -294,18 +300,33 @@ static int tar_pull_make_local_copy(TarPull *p) { "Image tree '%s' is not owned by the foreign UID range, refusing.", p->final_path); - r = mountfsd_mount_directory_fd(directory_fd, p->userns_fd, DISSECT_IMAGE_FOREIGN_UID, &p->tree_fd); + r = mountfsd_mount_directory_fd( + mountfsd_link, + directory_fd, + p->userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + &p->tree_fd); if (r < 0) return log_error_errno(r, "Failed to mount directory via mountfsd: %m"); } _cleanup_close_ int directory_fd = -EBADF; - r = mountfsd_make_directory(t, MODE_INVALID, /* flags= */ 0, &directory_fd); + r = mountfsd_make_directory( + mountfsd_link, + t, + MODE_INVALID, + /* flags= */ 0, + &directory_fd); if (r < 0) return log_error_errno(r, "Failed to make directory via mountfsd: %m"); _cleanup_close_ int copy_fd = -EBADF; - r = mountfsd_mount_directory_fd(directory_fd, p->userns_fd, DISSECT_IMAGE_FOREIGN_UID, ©_fd); + r = mountfsd_mount_directory_fd( + mountfsd_link, + directory_fd, + p->userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + ©_fd); if (r < 0) return log_error_errno(r, "Failed to mount directory via mountfsd: %m"); @@ -611,12 +632,27 @@ static int tar_pull_job_on_open_disk_tar(PullJob *j) { if (r < 0) return r; + _cleanup_(sd_varlink_unrefp) sd_varlink *mountfsd_link = NULL; + r = mountfsd_connect(&mountfsd_link); + if (r < 0) + return log_error_errno(r, "Failed to connect to mountfsd: %m"); + _cleanup_close_ int directory_fd = -EBADF; - r = mountfsd_make_directory(where, MODE_INVALID, /* flags= */ 0, &directory_fd); + r = mountfsd_make_directory( + mountfsd_link, + where, + MODE_INVALID, + /* flags= */ 0, + &directory_fd); if (r < 0) return log_error_errno(r, "Failed to make directory via mountfsd: %m"); - r = mountfsd_mount_directory_fd(directory_fd, p->userns_fd, DISSECT_IMAGE_FOREIGN_UID, &p->tree_fd); + r = mountfsd_mount_directory_fd( + mountfsd_link, + directory_fd, + p->userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + &p->tree_fd); if (r < 0) return log_error_errno(r, "Failed to mount directory via mountfsd: %m"); } else { diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c index 7b8812f03a..25f1a84551 100644 --- a/src/mountfsd/mountwork.c +++ b/src/mountfsd/mountwork.c @@ -662,6 +662,7 @@ static int vl_method_mount_image( if (r < 0) return r; + _cleanup_(sd_varlink_unrefp) sd_varlink *nsresource_link = NULL; for (PartitionDesignator d = 0; d < _PARTITION_DESIGNATOR_MAX; d++) { DissectedPartition *pp = di->partitions + d; int fd_idx; @@ -673,7 +674,14 @@ static int vl_method_mount_image( continue; if (userns_fd >= 0) { - r = nsresource_add_mount(userns_fd, pp->fsmount_fd); + + if (!nsresource_link) { + r = nsresource_connect(&nsresource_link); + if (r < 0) + return r; + } + + r = nsresource_add_mount(nsresource_link, userns_fd, pp->fsmount_fd); if (r < 0) return r; } @@ -1206,7 +1214,7 @@ static int vl_method_mount_directory( } if (userns_fd >= 0) { - r = nsresource_add_mount(userns_fd, mount_fd); + r = nsresource_add_mount(/* vl= */ NULL, userns_fd, mount_fd); if (r < 0) return r; } diff --git a/src/mstack/mstack-tool.c b/src/mstack/mstack-tool.c index 01a71d6e56..bc86d8565a 100644 --- a/src/mstack/mstack-tool.c +++ b/src/mstack/mstack-tool.c @@ -361,6 +361,7 @@ static int mount_mstack(void) { /* dir_fd= */ -EBADF, arg_where, /* temp_mount_dir= */ NULL, /* auto-create temporary directory */ + /* mountfsd_link= */ NULL, /* userns_fd= */ -EBADF, arg_image_policy, arg_image_filter, diff --git a/src/nspawn/nspawn-cgroup.c b/src/nspawn/nspawn-cgroup.c index 4206fe94e5..f1d45bbb9b 100644 --- a/src/nspawn/nspawn-cgroup.c +++ b/src/nspawn/nspawn-cgroup.c @@ -108,7 +108,10 @@ int create_subcgroup( if (r < 0) return log_error_errno(r, "Failed to add process " PID_FMT " to cgroup %s: %m", pid->pid, payload); - r = nsresource_add_cgroup(userns_fd, cgroup_fd); + r = nsresource_add_cgroup( + /* vl= */ NULL, + userns_fd, + cgroup_fd); if (r < 0) return log_error_errno(r, "Failed to add cgroup %s to userns: %m", payload); } else { diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 47300ff7e3..08afa171ae 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -19,6 +19,7 @@ #include "sd-id128.h" #include "sd-netlink.h" #include "sd-path.h" +#include "sd-varlink.h" #include "alloc-util.h" #include "barrier.h" @@ -5442,7 +5443,13 @@ static int run_container( } else { _cleanup_free_ char *host_ifname = NULL; - r = nsresource_add_netif_veth(userns_fd, child_netns_fd, /* namespace_ifname= */ NULL, &host_ifname, /* ret_namespace_ifname= */ NULL); + r = nsresource_add_netif_veth( + /* vl= */ NULL, + 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"); @@ -6030,6 +6037,7 @@ static int run(int argc, char *argv[]) { _cleanup_(mstack_freep) MStack *mstack = NULL; _cleanup_(sd_netlink_unrefp) sd_netlink *nfnl = NULL; _cleanup_(pidref_done) PidRef pid = PIDREF_NULL; + _cleanup_(sd_varlink_unrefp) sd_varlink *nsresource_link = NULL, *mountfsd_link = NULL; log_setup(); @@ -6132,13 +6140,28 @@ static int run(int argc, char *argv[]) { if (arg_userns_mode == USER_NAMESPACE_MANAGED) { /* Let's allocate a 64K userns first, if managed mode is chosen */ + r = nsresource_connect(&nsresource_link); + if (r < 0) { + log_error_errno(r, "Failed to connect to nsresourced: %m"); + goto finish; + } + + r = mountfsd_connect(&mountfsd_link); + if (r < 0) { + log_error_errno(r, "Failed to connect to mountsd: %m"); + goto finish; + } + _cleanup_free_ char *userns_name = NULL; if (asprintf(&userns_name, "nspawn-" PID_FMT "-%s", getpid_cached(), arg_machine) < 0) { r = log_oom(); goto finish; } - userns_fd = nsresource_allocate_userns(userns_name, NSRESOURCE_UIDS_64K); /* allocate 64K UIDs */ + userns_fd = nsresource_allocate_userns( + nsresource_link, + userns_name, + NSRESOURCE_UIDS_64K); /* allocate 64K UIDs */ if (userns_fd < 0) { r = log_error_errno(userns_fd, "Failed to allocate user namespace with 64K users: %m"); goto finish; @@ -6293,6 +6316,7 @@ static int run(int argc, char *argv[]) { if (userns_fd >= 0) { r = mountfsd_mount_directory( + mountfsd_link, arg_directory, userns_fd, determine_dissect_image_flags(), @@ -6443,6 +6467,7 @@ static int run(int argc, char *argv[]) { goto finish; } else { r = mountfsd_mount_image( + mountfsd_link, arg_image, userns_fd, /* options= */ NULL, @@ -6485,6 +6510,7 @@ static int run(int argc, char *argv[]) { r = mstack_open_images( mstack, + mountfsd_link, userns_fd, arg_image_policy, /* image_filter= */ NULL, @@ -6507,6 +6533,9 @@ static int run(int argc, char *argv[]) { if (r < 0) goto finish; + mountfsd_link = sd_varlink_unref(mountfsd_link); + nsresource_link = sd_varlink_unref(nsresource_link); + if (!arg_quiet) { const char *t = arg_mstack ?: arg_image ?: arg_directory; _cleanup_free_ char *u = NULL; @@ -6547,7 +6576,8 @@ static int run(int argc, char *argv[]) { mstack, userns_fd, fds, - veth_name, &veth_created, + veth_name, + &veth_created, &expose_args, &master, &pid, &ret); if (r <= 0) diff --git a/src/portable/portable.c b/src/portable/portable.c index 191125b9c5..2fe015d94c 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -6,6 +6,7 @@ #include "sd-bus.h" #include "sd-messages.h" +#include "sd-varlink.h" #include "bus-common-errors.h" #include "bus-error.h" @@ -483,12 +484,20 @@ static int portable_extract_by_path( return log_error_errno(r, "Failed to extract image name from path '%s': %m", path); if (scope == RUNTIME_SCOPE_USER && uid_is_foreign(st.st_uid)) { - _cleanup_close_ int userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K); + _cleanup_close_ int userns_fd = nsresource_allocate_userns( + /* vl= */ NULL, + /* name= */ NULL, + NSRESOURCE_UIDS_64K); if (userns_fd < 0) return log_debug_errno(userns_fd, "Failed to allocate user namespace: %m"); _cleanup_close_ int mfd = -EBADF; - r = mountfsd_mount_directory_fd(rfd, userns_fd, DISSECT_IMAGE_FOREIGN_UID, &mfd); + r = mountfsd_mount_directory_fd( + /* vl= */ NULL, + rfd, + userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + &mfd); if (r < 0) return r; @@ -604,11 +613,15 @@ static int portable_extract_by_path( return log_debug_errno(r, "Failed to create temporary directory: %m"); if (scope == RUNTIME_SCOPE_USER) { - userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K); + userns_fd = nsresource_allocate_userns( + /* vl= */ NULL, + /* name= */ NULL, + NSRESOURCE_UIDS_64K); if (userns_fd < 0) return log_debug_errno(userns_fd, "Failed to allocate user namespace: %m"); r = mountfsd_mount_image_fd( + /* vl= */ NULL, rfd, userns_fd, /* options= */ NULL, @@ -1808,7 +1821,10 @@ static int install_image( if (flags & PORTABLE_MIXED_COPY_LINK) { if (scope == RUNTIME_SCOPE_USER) { - _cleanup_close_ int userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K); + _cleanup_close_ int userns_fd = nsresource_allocate_userns( + /* vl= */ NULL, + /* name= */ NULL, + NSRESOURCE_UIDS_64K); if (userns_fd < 0) return log_debug_errno(userns_fd, "Failed to allocate user namespace: %m"); @@ -1820,21 +1836,31 @@ static int install_image( if (fstat(fd, &st) < 0) return log_error_errno(errno, "Failed to stat '%s': %m", image_path); + _cleanup_(sd_varlink_unrefp) sd_varlink *mountfsd_link = NULL; + r = mountfsd_connect(&mountfsd_link); + if (r < 0) + return r; + _cleanup_close_ int tree_fd = -EBADF; if (uid_is_foreign(st.st_uid)) { - r = mountfsd_mount_directory_fd(fd, userns_fd, DISSECT_IMAGE_FOREIGN_UID, &tree_fd); + r = mountfsd_mount_directory_fd( + mountfsd_link, + fd, + userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + &tree_fd); if (r < 0) return r; } else tree_fd = TAKE_FD(fd); _cleanup_close_ int directory_fd = -EBADF; - r = mountfsd_make_directory(target, MODE_INVALID, /* flags= */ 0, &directory_fd); + r = mountfsd_make_directory(mountfsd_link, target, MODE_INVALID, /* flags= */ 0, &directory_fd); if (r < 0) return r; _cleanup_close_ int copy_fd = -EBADF; - r = mountfsd_mount_directory_fd(directory_fd, userns_fd, DISSECT_IMAGE_FOREIGN_UID, ©_fd); + r = mountfsd_mount_directory_fd(mountfsd_link, directory_fd, userns_fd, DISSECT_IMAGE_FOREIGN_UID, ©_fd); if (r < 0) return r; diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index 064a3e8002..0575310a2c 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -11,6 +11,7 @@ #include "sd-json.h" #include "sd-path.h" +#include "sd-varlink.h" #include "alloc-util.h" #include "blockdev-util.h" @@ -1279,12 +1280,16 @@ static int unprivileged_remove(Image *i) { assert(i); - _cleanup_close_ int userns_fd = nsresource_allocate_userns(/* name= */ NULL, /* size= */ NSRESOURCE_UIDS_64K); + _cleanup_close_ int userns_fd = nsresource_allocate_userns( + /* vl= */ NULL, + /* name= */ NULL, + /* size= */ NSRESOURCE_UIDS_64K); if (userns_fd < 0) return log_debug_errno(userns_fd, "Failed to allocate transient user namespace: %m"); _cleanup_close_ int tree_fd = -EBADF; r = mountfsd_mount_directory( + /* vl= */ NULL, i->path, userns_fd, DISSECT_IMAGE_FOREIGN_UID, @@ -1623,13 +1628,22 @@ static int unprivileged_clone(Image *i, const char *new_path) { assert(i); assert(new_path); - _cleanup_close_ int userns_fd = nsresource_allocate_userns(/* name= */ NULL, /* size= */ NSRESOURCE_UIDS_64K); + _cleanup_close_ int userns_fd = nsresource_allocate_userns( + /* vl= */ NULL, + /* name= */ NULL, + /* size= */ NSRESOURCE_UIDS_64K); if (userns_fd < 0) return log_debug_errno(userns_fd, "Failed to allocate transient user namespace: %m"); + _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; + r = mountfsd_connect(&link); + if (r < 0) + return r; + /* Map original image */ _cleanup_close_ int tree_fd = -EBADF; r = mountfsd_mount_directory( + link, i->path, userns_fd, DISSECT_IMAGE_FOREIGN_UID, @@ -1640,6 +1654,7 @@ static int unprivileged_clone(Image *i, const char *new_path) { /* Make new image */ _cleanup_close_ int new_fd = -EBADF; r = mountfsd_make_directory( + link, new_path, MODE_INVALID, /* flags= */ 0, @@ -1650,6 +1665,7 @@ static int unprivileged_clone(Image *i, const char *new_path) { /* Mount new image */ _cleanup_close_ int target_fd = -EBADF; r = mountfsd_mount_directory_fd( + link, new_fd, userns_fd, DISSECT_IMAGE_FOREIGN_UID, @@ -1657,6 +1673,8 @@ static int unprivileged_clone(Image *i, const char *new_path) { if (r < 0) return r; + link = sd_varlink_unref(link); + /* Fork off child that moves into userns and does the copying */ return copy_tree_at_foreign(tree_fd, target_fd, userns_fd); } diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 0c0274fbc5..eb4b6bd609 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -4958,6 +4958,7 @@ int verity_dissect_and_mount( return log_debug_errno(userns_fd, "Failed to open our own user namespace: %m"); r = mountfsd_mount_image( + /* vl= */ NULL, src_fd >= 0 ? FORMAT_PROC_FD_PATH(src_fd) : src, userns_fd, options, @@ -5125,7 +5126,30 @@ static void mount_image_reply_parameters_done(MountImageReplyParameters *p) { #endif +int mountfsd_connect(sd_varlink **ret) { + int r; + + assert(ret); + + _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; + r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.MountFileSystem"); + if (r < 0) + return log_debug_errno(r, "Failed to connect to mountfsd: %m"); + + r = sd_varlink_set_allow_fd_passing_input(vl, true); + if (r < 0) + return log_debug_errno(r, "Failed to enable varlink fd passing for read: %m"); + + r = sd_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"); + + *ret = TAKE_PTR(vl); + return 0; +} + int mountfsd_mount_image_fd( + sd_varlink *vl, int image_fd, int userns_fd, const MountOptions *options, @@ -5149,7 +5173,6 @@ int mountfsd_mount_image_fd( _cleanup_(dissected_image_unrefp) DissectedImage *di = NULL; _cleanup_close_ int verity_data_fd = -EBADF; - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; _cleanup_free_ char *ps = NULL; const char *error_id; int r; @@ -5157,17 +5180,14 @@ int mountfsd_mount_image_fd( assert(image_fd >= 0); assert(ret); - r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.MountFileSystem"); - if (r < 0) - return log_error_errno(r, "Failed to connect to mountfsd: %m"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = mountfsd_connect(&_vl); + if (r < 0) + return r; - r = sd_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 = sd_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"); + vl = _vl; + } _cleanup_close_ int reopened_fd = -EBADF; @@ -5337,6 +5357,7 @@ int mountfsd_mount_image_fd( } int mountfsd_mount_image( + sd_varlink *vl, const char *path, int userns_fd, const MountOptions *options, @@ -5355,7 +5376,7 @@ int mountfsd_mount_image( return log_debug_errno(errno, "Failed to open '%s': %m", path); _cleanup_(dissected_image_unrefp) DissectedImage *di = NULL; - r = mountfsd_mount_image_fd(image_fd, userns_fd, options, image_policy, verity, flags, &di); + r = mountfsd_mount_image_fd(vl, image_fd, userns_fd, options, image_policy, verity, flags, &di); if (r < 0) return r; @@ -5370,6 +5391,7 @@ int mountfsd_mount_image( } int mountfsd_mount_directory_fd( + sd_varlink *vl, int directory_fd, int userns_fd, DissectImageFlags flags, @@ -5383,18 +5405,14 @@ int mountfsd_mount_directory_fd( /* Pick one identity, not both, that makes no sense. */ assert(!FLAGS_SET(flags, DISSECT_IMAGE_FOREIGN_UID|DISSECT_IMAGE_IDENTITY_UID)); - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; - r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.MountFileSystem"); - if (r < 0) - return log_error_errno(r, "Failed to connect to mountfsd: %m"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = mountfsd_connect(&_vl); + if (r < 0) + return r; - r = sd_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 = sd_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"); + vl = _vl; + } r = sd_varlink_push_dup_fd(vl, directory_fd); if (r < 0) @@ -5441,6 +5459,7 @@ int mountfsd_mount_directory_fd( } int mountfsd_mount_directory( + sd_varlink *vl, const char *path, int userns_fd, DissectImageFlags flags, @@ -5453,10 +5472,11 @@ int mountfsd_mount_directory( if (directory_fd < 0) return log_debug_errno(errno, "Failed to open '%s': %m", path); - return mountfsd_mount_directory_fd(directory_fd, userns_fd, flags, ret_mount_fd); + return mountfsd_mount_directory_fd(vl, directory_fd, userns_fd, flags, ret_mount_fd); } int mountfsd_make_directory_fd( + sd_varlink *vl, int parent_fd, const char *name, mode_t mode, @@ -5468,18 +5488,14 @@ int mountfsd_make_directory_fd( assert(parent_fd >= 0); assert(name); - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; - r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.MountFileSystem"); - if (r < 0) - return log_error_errno(r, "Failed to connect to mountfsd: %m"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = mountfsd_connect(&_vl); + if (r < 0) + return r; - r = sd_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 = sd_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"); + vl = _vl; + } r = sd_varlink_push_dup_fd(vl, parent_fd); if (r < 0) @@ -5519,6 +5535,7 @@ int mountfsd_make_directory_fd( } int mountfsd_make_directory( + sd_varlink *vl, const char *path, mode_t mode, DissectImageFlags flags, @@ -5540,7 +5557,7 @@ int mountfsd_make_directory( if (fd < 0) return log_debug_errno(r, "Failed to open '%s': %m", parent); - return mountfsd_make_directory_fd(fd, dirname, mode, flags, ret_directory_fd); + return mountfsd_make_directory_fd(vl, fd, dirname, mode, flags, ret_directory_fd); } int copy_tree_at_foreign(int source_fd, int target_fd, int userns_fd) { @@ -5600,6 +5617,7 @@ int remove_tree_foreign(const char *path, int userns_fd) { _cleanup_close_ int tree_fd = -EBADF; r = mountfsd_mount_directory( + /* vl= */ NULL, path, userns_fd, DISSECT_IMAGE_FOREIGN_UID, @@ -5611,7 +5629,7 @@ int remove_tree_foreign(const char *path, int userns_fd) { "rm-tree", /* stdio_fds= */ NULL, (int[]) { userns_fd, tree_fd }, 2, - FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_REOPEN_LOG|FORK_WAIT, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_WAIT|FORK_REOPEN_LOG, /* ret= */ NULL); if (r < 0) return r; @@ -5625,19 +5643,19 @@ int remove_tree_foreign(const char *path, int userns_fd) { userns_fd, /* root_fd= */ -EBADF); if (r < 0) { - log_error_errno(r, "Failed to join user namespace: %m"); + log_debug_errno(r, "Failed to join user namespace: %m"); _exit(EXIT_FAILURE); } _cleanup_close_ int dfd = fd_reopen(tree_fd, O_DIRECTORY|O_CLOEXEC); if (dfd < 0) { - log_error_errno(r, "Failed to reopen tree fd: %m"); + log_debug_errno(r, "Failed to reopen tree fd: %m"); _exit(EXIT_FAILURE); } r = rm_rf_children(dfd, REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_CHMOD, /* root_dev= */ NULL); if (r < 0) - log_warning_errno(r, "Failed to empty '%s' directory in foreign UID mode, ignoring: %m", path); + log_debug_errno(r, "Failed to empty '%s' directory in foreign UID mode, ignoring: %m", path); _exit(EXIT_SUCCESS); } diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index aa8c6a2737..09d7db5952 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -269,13 +269,23 @@ static inline const char* dissected_partition_fstype(const DissectedPartition *m int get_common_dissect_directory(char **ret); -int mountfsd_mount_image_fd(int image_fd, int userns_fd, const MountOptions *options, const ImagePolicy *image_policy, const VeritySettings *verity, DissectImageFlags flags, DissectedImage **ret); -int mountfsd_mount_image(const char *path, int userns_fd, const MountOptions *options, const ImagePolicy *image_policy, const VeritySettings *verity, DissectImageFlags flags, DissectedImage **ret); -int mountfsd_mount_directory_fd(int directory_fd, int userns_fd, DissectImageFlags flags, int *ret_mount_fd); -int mountfsd_mount_directory(const char *path, int userns_fd, DissectImageFlags flags, int *ret_mount_fd); +int mountfsd_connect(sd_varlink **ret); -int mountfsd_make_directory_fd(int parent_fd, const char *name, mode_t mode, DissectImageFlags flags, int *ret_directory_fd); -int mountfsd_make_directory(const char *path, mode_t mode, DissectImageFlags flags, int *ret_directory_fd); +/* All the calls below take a 'link' parameter, that may be an already established Varlink connection object + * towards systemd-mountfsd, previously created via mountfsd_connect(). This serves two purposes: first of + * all allows more efficient resource usage, as this allows recycling already allocated resources for + * multiple calls. Secondly, the user credentials are pinned at time of mountfsd_connect(), and the caller + * hence can drop privileges afterwards while keeping open the connection and still execute relevant + * operations under the original identity, until the connection is closed. The 'link' parameter may be passed + * as NULL in which case a short-lived connection is created, just to execute the requested operation. */ + +int mountfsd_mount_image_fd(sd_varlink *vl, int image_fd, int userns_fd, const MountOptions *options, const ImagePolicy *image_policy, const VeritySettings *verity, DissectImageFlags flags, DissectedImage **ret); +int mountfsd_mount_image(sd_varlink *vl, const char *path, int userns_fd, const MountOptions *options, const ImagePolicy *image_policy, const VeritySettings *verity, DissectImageFlags flags, DissectedImage **ret); +int mountfsd_mount_directory_fd(sd_varlink *vl, int directory_fd, int userns_fd, DissectImageFlags flags, int *ret_mount_fd); +int mountfsd_mount_directory(sd_varlink *vl, const char *path, int userns_fd, DissectImageFlags flags, int *ret_mount_fd); + +int mountfsd_make_directory_fd(sd_varlink *vl, int parent_fd, const char *name, mode_t mode, DissectImageFlags flags, int *ret_directory_fd); +int mountfsd_make_directory(sd_varlink *vl, const char *path, mode_t mode, DissectImageFlags flags, int *ret_directory_fd); int copy_tree_at_foreign(int source_fd, int target_fd, int userns_fd); int remove_tree_foreign(const char *path, int userns_fd); diff --git a/src/shared/mstack.c b/src/shared/mstack.c index 8b0beb21ad..9cbc316319 100644 --- a/src/shared/mstack.c +++ b/src/shared/mstack.c @@ -4,6 +4,8 @@ #include #include +#include "sd-varlink.h" + #include "alloc-util.h" #include "chase.h" #include "dissect-image.h" @@ -515,6 +517,7 @@ static const char *mount_name(MStackMount *m) { int mstack_open_images( MStack *mstack, + sd_varlink *mountfsd_link, int userns_fd, const ImagePolicy *image_policy, const ImageFilter *image_filter, @@ -524,6 +527,16 @@ int mstack_open_images( assert(mstack); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (userns_fd >= 0 && !mountfsd_link) { + /* User a single connection for all mounts */ + r = mountfsd_connect(&_vl); + if (r < 0) + return r; + + mountfsd_link = _vl; + } + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { DissectImageFlags dissect_image_flags = @@ -549,6 +562,7 @@ int mstack_open_images( if (userns_fd >= 0) { r = mountfsd_mount_image_fd( + mountfsd_link, m->what_fd, userns_fd, /* options= */ NULL, @@ -642,6 +656,7 @@ int mstack_open_images( if (userns_fd >= 0) { r = mountfsd_mount_directory_fd( + mountfsd_link, m->what_fd, userns_fd, dissect_image_flags, @@ -1070,6 +1085,7 @@ int mstack_apply( int dir_fd, const char *where, const char *temp_mount_dir, + sd_varlink *link, int userns_fd, const ImagePolicy *image_policy, const ImageFilter *image_filter, @@ -1084,7 +1100,7 @@ int mstack_apply( if (r < 0) return r; - r = mstack_open_images(&mstack, userns_fd, image_policy, image_filter, flags); + r = mstack_open_images(&mstack, link, userns_fd, image_policy, image_filter, flags); if (r < 0) return r; diff --git a/src/shared/mstack.h b/src/shared/mstack.h index e526f17d98..b71ff86940 100644 --- a/src/shared/mstack.h +++ b/src/shared/mstack.h @@ -51,12 +51,12 @@ MStack *mstack_free(MStack *mstack); DEFINE_TRIVIAL_CLEANUP_FUNC(MStack*, mstack_free); int mstack_load(const char *dir, int dir_fd, MStack **ret); -int mstack_open_images(MStack *mstack, int userns_fd, const ImagePolicy *image_policy, const ImageFilter *image_filter, MStackFlags flags); +int mstack_open_images(MStack *mstack, sd_varlink *mountfsd_link, int userns_fd, const ImagePolicy *image_policy, const ImageFilter *image_filter, MStackFlags flags); int mstack_make_mounts(MStack *mstack, const char *temp_mount_dir, MStackFlags flags); int mstack_bind_mounts(MStack *mstack, const char *where, int where_fd, MStackFlags flags, int *ret_root_fd); /* The four calls above in one */ -int mstack_apply(const char *dir, int dir_fd, const char *where, const char *temp_mount_dir, int userns_fd, const ImagePolicy *image_policy, const ImageFilter *image_filter, MStackFlags flags, int *ret_root_fd); +int mstack_apply(const char *dir, int dir_fd, const char *where, const char *temp_mount_dir, sd_varlink *mountfsd_link, int userns_fd, const ImagePolicy *image_policy, const ImageFilter *image_filter, MStackFlags flags, int *ret_root_fd); int mstack_is_read_only(MStack *mstack); int mstack_is_foreign_uid_owned(MStack *mstack); diff --git a/src/shared/nsresource.c b/src/shared/nsresource.c index 651d9bdf4d..615f99eff1 100644 --- a/src/shared/nsresource.c +++ b/src/shared/nsresource.c @@ -57,8 +57,25 @@ static int make_pid_name(char **ret) { return 0; } -int nsresource_allocate_userns(const char *name, uint64_t size) { +int nsresource_connect(sd_varlink **ret) { + int r; + + assert(ret); + _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; + r = sd_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 = sd_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"); + + *ret = TAKE_PTR(vl); + return 0; +} + +int nsresource_allocate_userns(sd_varlink *vl, const char *name, uint64_t size) { _cleanup_close_ int userns_fd = -EBADF; _cleanup_free_ char *_name = NULL; const char *error_id; @@ -77,13 +94,14 @@ int nsresource_allocate_userns(const char *name, uint64_t size) { if (size <= 0 || size > UINT64_C(0x100000000)) /* Note: the server actually only allows allocating 1 or 64K right now */ return -EINVAL; - r = sd_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"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = nsresource_connect(&_vl); + if (r < 0) + return r; - r = sd_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"); + vl = _vl; + } userns_fd = userns_acquire_empty(); if (userns_fd < 0) @@ -113,8 +131,7 @@ int nsresource_allocate_userns(const char *name, uint64_t size) { return TAKE_FD(userns_fd); } -int nsresource_register_userns(const char *name, int userns_fd) { - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; +int nsresource_register_userns(sd_varlink *vl, const char *name, int userns_fd) { _cleanup_close_ int _userns_fd = -EBADF; _cleanup_free_ char *_name = NULL; const char *error_id; @@ -138,13 +155,14 @@ int nsresource_register_userns(const char *name, int userns_fd) { userns_fd = _userns_fd; } - r = sd_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"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = nsresource_connect(&_vl); + if (r < 0) + return r; - r = sd_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"); + vl = _vl; + } userns_fd_idx = sd_varlink_push_dup_fd(vl, userns_fd); if (userns_fd_idx < 0) @@ -169,8 +187,7 @@ int nsresource_register_userns(const char *name, int userns_fd) { return 0; } -int nsresource_add_mount(int userns_fd, int mount_fd) { - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; +int nsresource_add_mount(sd_varlink *vl, int userns_fd, int mount_fd) { _cleanup_close_ int _userns_fd = -EBADF; int r, userns_fd_idx, mount_fd_idx; const char *error_id; @@ -185,13 +202,14 @@ int nsresource_add_mount(int userns_fd, int mount_fd) { userns_fd = _userns_fd; } - r = sd_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"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = nsresource_connect(&_vl); + if (r < 0) + return r; - r = sd_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"); + vl = _vl; + } userns_fd_idx = sd_varlink_push_dup_fd(vl, userns_fd); if (userns_fd_idx < 0) @@ -221,8 +239,7 @@ int nsresource_add_mount(int userns_fd, int mount_fd) { return 1; } -int nsresource_add_cgroup(int userns_fd, int cgroup_fd) { - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; +int nsresource_add_cgroup(sd_varlink *vl, int userns_fd, int cgroup_fd) { _cleanup_close_ int _userns_fd = -EBADF; int r, userns_fd_idx, cgroup_fd_idx; const char *error_id; @@ -237,13 +254,14 @@ int nsresource_add_cgroup(int userns_fd, int cgroup_fd) { userns_fd = _userns_fd; } - r = sd_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"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = nsresource_connect(&_vl); + if (r < 0) + return r; - r = sd_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"); + vl = _vl; + } userns_fd_idx = sd_varlink_push_dup_fd(vl, userns_fd); if (userns_fd_idx < 0) @@ -287,6 +305,7 @@ static void interface_params_done(InterfaceParams *p) { } int nsresource_add_netif_veth( + sd_varlink *vl, int userns_fd, int netns_fd, const char *namespace_ifname, @@ -294,7 +313,6 @@ int nsresource_add_netif_veth( char **ret_namespace_ifname) { _cleanup_close_ int _userns_fd = -EBADF, _netns_fd = -EBADF; - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; int r, userns_fd_idx, netns_fd_idx; const char *error_id; @@ -314,13 +332,14 @@ int nsresource_add_netif_veth( netns_fd = _netns_fd; } - r = sd_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"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = nsresource_connect(&_vl); + if (r < 0) + return r; - r = sd_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"); + vl = _vl; + } userns_fd_idx = sd_varlink_push_dup_fd(vl, userns_fd); if (userns_fd_idx < 0) @@ -368,11 +387,11 @@ int nsresource_add_netif_veth( } int nsresource_add_netif_tap( + sd_varlink *vl, int userns_fd, char **ret_host_ifname) { _cleanup_close_ int _userns_fd = -EBADF; - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; int r, userns_fd_idx; const char *error_id; @@ -384,13 +403,14 @@ int nsresource_add_netif_tap( userns_fd = _userns_fd; } - r = sd_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"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = nsresource_connect(&_vl); + if (r < 0) + return r; - r = sd_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"); + vl = _vl; + } r = sd_varlink_set_allow_fd_passing_input(vl, true); if (r < 0) diff --git a/src/shared/nsresource.h b/src/shared/nsresource.h index 136b1f85ac..93957a10c8 100644 --- a/src/shared/nsresource.h +++ b/src/shared/nsresource.h @@ -7,9 +7,19 @@ #define NSRESOURCE_UIDS_64K 0x10000U #define NSRESOURCE_UIDS_1 1U -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_veth(int userns_fd, int netns_fd, const char *namespace_ifname, char **ret_host_ifname, char **ret_namespace_ifname); -int nsresource_add_netif_tap(int userns_fd, char **ret_host_ifname); +int nsresource_connect(sd_varlink **ret); + +/* All the calls below take a 'link' parameter, that may be an already established Varlink connection object + * towards systemd-nsresourced, previously created via nsresource_connect(). This serves two purposes: first + * of all allows more efficient resource usage, as this allows recycling already allocated resources for + * multiple calls. Secondly, the user credentials are pinned at time of nsresource_connect(), and the caller + * hence can drop privileges afterwards while keeping open the connection and still execute relevant + * operations under the original identity, until the connection is closed. The 'link' parameter may be passed + * as NULL in which case a short-lived connection is created, just to execute the requested operation. */ + +int nsresource_allocate_userns(sd_varlink *vl, const char *name, uint64_t size); +int nsresource_register_userns(sd_varlink *vl, const char *name, int userns_fd); +int nsresource_add_mount(sd_varlink *vl, int userns_fd, int mount_fd); +int nsresource_add_cgroup(sd_varlink *vl, int userns_fd, int cgroup_fd); +int nsresource_add_netif_veth(sd_varlink *vl, int userns_fd, int netns_fd, const char *namespace_ifname, char **ret_host_ifname, char **ret_namespace_ifname); +int nsresource_add_netif_tap(sd_varlink *vl, int userns_fd, char **ret_host_ifname); diff --git a/src/test/test-mstack.c b/src/test/test-mstack.c index 15400af511..7c370fcbe9 100644 --- a/src/test/test-mstack.c +++ b/src/test/test-mstack.c @@ -90,6 +90,7 @@ TEST(mstack) { ASSERT_OK(mstack_open_images( mstack, + /* mountfsd_link= */ NULL, /* userns_fd= */ -EBADF, /* image_policy= */ NULL, /* image_filter= */ NULL, diff --git a/src/test/test-nsresource.c b/src/test/test-nsresource.c index aacd776d7f..b8845b8ffe 100644 --- a/src/test/test-nsresource.c +++ b/src/test/test-nsresource.c @@ -2,6 +2,8 @@ #include +#include "sd-varlink.h" + #include "errno-util.h" #include "fd-util.h" #include "namespace-util.h" @@ -16,13 +18,18 @@ TEST(delegatetap) { return (void) log_tests_skipped_errno(userns_fd, "User namespaces not available"); ASSERT_OK(userns_fd); - r = nsresource_register_userns("foobar", userns_fd); + _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; + r = nsresource_connect(&link); if (ERRNO_IS_NEG_DISCONNECT(r) || r == -ENOENT || ERRNO_IS_NEG_NOT_SUPPORTED(r)) return (void) log_tests_skipped_errno(r, "systemd-nsresourced cannot be reached"); + + r = nsresource_register_userns(link, "foobar", userns_fd); + if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) + return (void) log_tests_skipped_errno(r, "systemd-nsresourced does not work"); ASSERT_OK(r); _cleanup_free_ char *ifname = NULL; - _cleanup_close_ int tap_fd = nsresource_add_netif_tap(userns_fd, &ifname); + _cleanup_close_ int tap_fd = nsresource_add_netif_tap(link, userns_fd, &ifname); if (ERRNO_IS_NEG_NOT_SUPPORTED(tap_fd)) return (void) log_tests_skipped_errno(tap_fd, "tap device support not available"); ASSERT_OK(tap_fd); diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 0c957b48aa..7cbe7fd5f9 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -12,6 +12,7 @@ #include "sd-daemon.h" #include "sd-event.h" #include "sd-id128.h" +#include "sd-varlink.h" #include "alloc-util.h" #include "architecture.h" @@ -2041,11 +2042,16 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { if (asprintf(&userns_name, "vmspawn-" PID_FMT "-%s", getpid_cached(), arg_machine) < 0) return log_oom(); - r = nsresource_register_userns(userns_name, delegate_userns_fd); + _cleanup_(sd_varlink_unrefp) sd_varlink *nsresource_link = NULL; + r = nsresource_connect(&nsresource_link); + if (r < 0) + return log_error_errno(r, "Failed to connect to nsresourced: %m"); + + r = nsresource_register_userns(nsresource_link, userns_name, delegate_userns_fd); if (r < 0) return log_error_errno(r, "Failed to register user namespace with systemd-nsresourced: %m"); - tap_fd = nsresource_add_netif_tap(delegate_userns_fd, /* ret_host_ifname= */ NULL); + tap_fd = nsresource_add_netif_tap(nsresource_link, delegate_userns_fd, /* ret_host_ifname= */ NULL); if (tap_fd < 0) return log_error_errno(tap_fd, "Failed to allocate network tap device: %m"); From e93fd7109408c1b652627fa58eb2e8aeb5c9e7f9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 27 Nov 2025 08:07:31 +0100 Subject: [PATCH 29/36] core: introduce PinnedResource This introduces PinnedResources as a structure combining pinned references to a root directory, root image, or root mstack. This is not only easier to work with, but essential to make certain unpriv things work, as we need some mechanism to pin resources before we drop into a userns which might possibly not provide access anymore to those resources. Hence this does two things: introduce the new structure, and immediately hook it up so that we pin things properly before dropping into userns, and then makes use of this after dropping the right way, and enables unpriv userns operation. The concept is generic enough to eventually implement extension images + mount images with the same structure, but in order to keep the changes managable this is left for another time. (This also makes one further clean-up: client-side verity-reuse checks are moved server side if we are unpriv. Previously we'd do them client side, but they were doomed to fail because of lack of privs. Hence let's drop the client side if we are unpriv and purely do them server-side in that case.) --- src/core/exec-invoke.c | 232 ++++++++++++++++++---------- src/core/namespace.c | 296 ++++++++++++++++++++++-------------- src/core/namespace.h | 27 +++- src/core/service.c | 1 - src/shared/shared-forward.h | 1 + src/test/test-namespace.c | 1 - src/test/test-ns.c | 14 +- 7 files changed, 372 insertions(+), 200 deletions(-) diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 888a2555fb..315ef41031 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -56,6 +56,7 @@ #include "mkdir-label.h" #include "mount-util.h" #include "mountpoint-util.h" +#include "mstack.h" #include "namespace-util.h" #include "nsflags.h" #include "nsresource.h" @@ -3537,8 +3538,7 @@ static int compile_symlinks( static bool insist_on_sandboxing( const ExecContext *context, - const char *root_dir, - const char *root_image, + const PinnedResource *rootfs, const BindMount *bind_mounts, size_t n_bind_mounts) { @@ -3552,7 +3552,7 @@ static bool insist_on_sandboxing( if (context->n_temporary_filesystems > 0) return true; - if (root_dir || root_image || context->root_directory_as_fd) + if (pinned_resource_is_set(rootfs)) return true; if (context->n_mount_images > 0) @@ -3579,8 +3579,7 @@ static bool insist_on_sandboxing( static int setup_ephemeral( const ExecContext *context, ExecRuntime *runtime, - char **root_image, /* both input and output! modified if ephemeral logic enabled */ - char **root_directory, /* ditto */ + PinnedResource *rootfs, /* both input and output! modified if ephemeral logic enabled */ char **reterr_path) { _cleanup_close_ int fd = -EBADF; @@ -3588,12 +3587,10 @@ static int setup_ephemeral( int r; assert(context); - assert(!context->root_directory_as_fd); assert(runtime); - assert(root_image); - assert(root_directory); + assert(rootfs); - if (!*root_image && !*root_directory) + if (!rootfs->image && !rootfs->directory) return 0; if (!runtime->ephemeral_copy) @@ -3619,32 +3616,32 @@ static int setup_ephemeral( if (fd != -EAGAIN) return log_debug_errno(fd, "Failed to receive file descriptor queued on ephemeral storage socket: %m"); - if (*root_image) { - log_debug("Making ephemeral copy of %s to %s", *root_image, new_root); + if (rootfs->image) { + log_debug("Making ephemeral copy of %s to %s", rootfs->image, new_root); - fd = copy_file(*root_image, new_root, O_EXCL, 0600, + fd = copy_file(rootfs->image, new_root, O_EXCL, 0600, COPY_LOCK_BSD|COPY_REFLINK|COPY_CRTIME|COPY_NOCOW_AFTER); if (fd < 0) { - *reterr_path = strdup(*root_image); + *reterr_path = strdup(rootfs->image); return log_debug_errno(fd, "Failed to copy image %s to %s: %m", - *root_image, new_root); + rootfs->image, new_root); } } else { - assert(*root_directory); + assert(rootfs->directory); - log_debug("Making ephemeral snapshot of %s to %s", *root_directory, new_root); + log_debug("Making ephemeral snapshot of %s to %s", rootfs->directory, new_root); fd = btrfs_subvol_snapshot_at( - AT_FDCWD, *root_directory, + AT_FDCWD, rootfs->directory, AT_FDCWD, new_root, BTRFS_SNAPSHOT_FALLBACK_COPY | BTRFS_SNAPSHOT_FALLBACK_DIRECTORY | BTRFS_SNAPSHOT_RECURSIVE | BTRFS_SNAPSHOT_LOCK_BSD); if (fd < 0) { - *reterr_path = strdup(*root_directory); + *reterr_path = strdup(rootfs->directory); return log_debug_errno(fd, "Failed to snapshot directory %s to %s: %m", - *root_directory, new_root); + rootfs->directory, new_root); } } @@ -3652,11 +3649,14 @@ static int setup_ephemeral( if (r < 0) return log_debug_errno(r, "Failed to queue file descriptor on ephemeral storage socket: %m"); - if (*root_image) - free_and_replace(*root_image, new_root); - else { - assert(*root_directory); - free_and_replace(*root_directory, new_root); + if (rootfs->image) { + free_and_replace(rootfs->image, new_root); + close_and_replace(rootfs->image_fd, fd); + } else { + assert(rootfs->directory); + + free_and_replace(rootfs->directory, new_root); + close_and_replace(rootfs->directory_fd, fd); } return 1; @@ -3710,20 +3710,35 @@ static int verity_settings_prepare( return 0; } -static int pick_versions( +static int pin_rootfs( const ExecContext *context, const ExecParameters *params, - char **ret_root_image, - char **ret_root_directory, + PinnedResource *ret, char **reterr_path) { int r; assert(context); - assert(!context->root_directory_as_fd); assert(params); - assert(ret_root_image); - assert(ret_root_directory); + assert(ret); + + if (!FLAGS_SET(params->flags, EXEC_APPLY_CHROOT)) { + *ret = PINNED_RESOURCE_NULL; + return 0; + } + + if (context->root_directory_as_fd) { + _cleanup_close_ int fd = fcntl(params->root_directory_fd, F_DUPFD_CLOEXEC, 3); + if (fd < 0) + return log_debug_errno(errno, "Failed to duplicate root directory fd: %m"); + + *ret = (PinnedResource) { + .directory_fd = TAKE_FD(fd), + .image_fd = -EBADF, + }; + + return 1; + } if (context->root_image) { _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; @@ -3745,8 +3760,24 @@ static int pick_versions( return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "No matching entry in .v/ directory %s found.", context->root_image); } - *ret_root_image = TAKE_PTR(result.path); - *ret_root_directory = NULL; + /* path_pick() returns us an O_PATH fd, let's turn this into a fully opened file, because + * mountfsd will want this later, and it wants a fully opened fd, so that security checks + * have been passed */ + _cleanup_close_ int reopened_fd = -EBADF; + reopened_fd = fd_reopen(result.fd, O_CLOEXEC|O_NONBLOCK|O_NOCTTY|O_RDWR); + if (ERRNO_IS_NEG_FS_WRITE_REFUSED(reopened_fd)) + reopened_fd = fd_reopen(result.fd, O_CLOEXEC|O_NONBLOCK|O_NOCTTY|O_RDONLY); + if (reopened_fd < 0) { + *reterr_path = strdup(context->root_image); + return log_debug_errno(reopened_fd, "Failed to open image '%s': %m", context->root_image); + } + + *ret = (PinnedResource) { + .image = TAKE_PTR(result.path), + .image_fd = TAKE_FD(reopened_fd), + .directory_fd = -EBADF, + }; + return r; } @@ -3770,12 +3801,53 @@ static int pick_versions( return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "No matching entry in .v/ directory %s found.", context->root_directory); } - *ret_root_image = NULL; - *ret_root_directory = TAKE_PTR(result.path); + *ret = (PinnedResource) { + .directory = TAKE_PTR(result.path), + .directory_fd = TAKE_FD(result.fd), + .image_fd = -EBADF, + }; + return r; } - *ret_root_image = *ret_root_directory = NULL; + if (context->root_mstack) { + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + + r = path_pick(/* toplevel_path= */ NULL, + /* toplevel_fd= */ AT_FDCWD, + context->root_mstack, + pick_filter_image_mstack, + /* n_filters= */ 1, + PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE, + &result); + if (r < 0) { + *reterr_path = strdup(context->root_mstack); + return r; + } + + if (!result.path) { + *reterr_path = strdup(context->root_mstack); + return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "No matching entry in .v/ directory %s found.", context->root_mstack); + } + + _cleanup_(mstack_freep) MStack *mstack = NULL; + r = mstack_load(result.path, result.fd, &mstack); + if (r < 0) { + *reterr_path = TAKE_PTR(result.path); + return r; + } + + *ret = (PinnedResource) { + .mstack = TAKE_PTR(result.path), + .mstack_loaded = TAKE_PTR(mstack), + .image_fd = -EBADF, + .directory_fd = -EBADF, + }; + + return r; + } + + *ret = PINNED_RESOURCE_NULL; return 0; } @@ -3783,7 +3855,8 @@ static int apply_mount_namespace( ExecCommandFlags command_flags, const ExecContext *context, const ExecParameters *params, - ExecRuntime *runtime, + const ExecRuntime *runtime, + const PinnedResource *rootfs, const char *memory_pressure_path, bool needs_sandboxing, uid_t exec_directory_uid, @@ -3798,7 +3871,7 @@ static int apply_mount_namespace( _cleanup_strv_free_ char **empty_directories = NULL, **symlinks = NULL, **read_write_paths_cleanup = NULL; _cleanup_free_ char *creds_path = NULL, *incoming_dir = NULL, *propagate_dir = NULL, - *private_namespace_dir = NULL, *host_os_release_stage = NULL, *root_image = NULL, *root_dir = NULL; + *private_namespace_dir = NULL, *host_os_release_stage = NULL; const char *tmp_dir = NULL, *var_tmp_dir = NULL; char **read_write_paths; bool setup_os_release_symlink; @@ -3812,26 +3885,6 @@ static int apply_mount_namespace( CLEANUP_ARRAY(bind_mounts, n_bind_mounts, bind_mount_free_many); - if (params->flags & EXEC_APPLY_CHROOT && !context->root_directory_as_fd) { - r = pick_versions( - context, - params, - &root_image, - &root_dir, - reterr_path); - if (r < 0) - return r; - - r = setup_ephemeral( - context, - runtime, - &root_image, - &root_dir, - reterr_path); - if (r < 0) - return r; - } - r = compile_bind_mounts(context, params, exec_directory_uid, exec_directory_gid, &bind_mounts, &n_bind_mounts, &empty_directories); if (r < 0) return r; @@ -3870,7 +3923,7 @@ static int apply_mount_namespace( } /* Symlinks (exec dirs, os-release) are set up after other mounts, before they are made read-only. */ - setup_os_release_symlink = needs_sandboxing && exec_context_get_effective_mount_apivfs(context) && (root_dir || root_image); + setup_os_release_symlink = needs_sandboxing && exec_context_get_effective_mount_apivfs(context) && pinned_resource_is_set(rootfs); r = compile_symlinks(context, params, setup_os_release_symlink, &symlinks); if (r < 0) return r; @@ -3917,10 +3970,10 @@ static int apply_mount_namespace( return -ENOMEM; } - if (root_image) { + if (rootfs->image) { r = verity_settings_prepare( &verity, - root_image, + rootfs->image, &context->root_hash, context->root_hash_path, &context->root_hash_sig, context->root_hash_sig_path, context->root_verity); @@ -3931,9 +3984,7 @@ static int apply_mount_namespace( NamespaceParameters parameters = { .runtime_scope = params->runtime_scope, - .root_directory = root_dir, - .root_image = root_image, - .root_directory_fd = params->flags & EXEC_APPLY_CHROOT ? params->root_directory_fd : -EBADF, + .rootfs = rootfs, .root_image_options = context->root_image_options, .root_image_policy = context->root_image_policy ?: &image_policy_service, @@ -3981,7 +4032,7 @@ static int apply_mount_namespace( /* If DynamicUser=no and RootDirectory= is set then lets pass a relaxed sandbox info, * otherwise enforce it, don't ignore protected paths and fail if we are enable to apply the * sandbox inside the mount namespace. */ - .ignore_protect_paths = !needs_sandboxing && !context->dynamic_user && root_dir, + .ignore_protect_paths = !needs_sandboxing && !context->dynamic_user && pinned_resource_is_set(rootfs), .protect_control_groups = needs_sandboxing ? exec_get_protect_control_groups(context) : PROTECT_CONTROL_GROUPS_NO, .protect_kernel_tunables = needs_sandboxing && context->protect_kernel_tunables, @@ -4007,6 +4058,7 @@ static int apply_mount_namespace( .protect_proc = needs_sandboxing ? context->protect_proc : PROTECT_PROC_DEFAULT, .proc_subset = needs_sandboxing ? context->proc_subset : PROC_SUBSET_ALL, .private_bpf = needs_sandboxing ? context->private_bpf : PRIVATE_BPF_NO, + .private_users = needs_sandboxing ? context->private_users : PRIVATE_USERS_NO, .bpffs_pidref = bpffs_pidref, .bpffs_socket_fd = bpffs_socket_fd, @@ -4023,17 +4075,18 @@ static int apply_mount_namespace( if (r == -ENOANO) { if (insist_on_sandboxing( context, - root_dir, root_image, + rootfs, bind_mounts, n_bind_mounts)) return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Failed to set up namespace, and refusing to continue since " "the selected namespacing options alter mount environment non-trivially.\n" - "Bind mounts: %zu, temporary filesystems: %zu, root directory: %s, root image: %s, dynamic user: %s", + "Bind mounts: %zu, temporary filesystems: %zu, root directory: %s, root image: %s, root mstack: %s, dynamic user: %s", n_bind_mounts, context->n_temporary_filesystems, - yes_no(root_dir), - yes_no(root_image), + yes_no(rootfs->directory_fd >= 0), + yes_no(rootfs->image_fd >= 0), + yes_no(!!rootfs->mstack_loaded), yes_no(context->dynamic_user)); log_debug("Failed to set up namespace, assuming containerized execution and ignoring."); @@ -4656,7 +4709,8 @@ static bool exec_namespace_is_delegated( static int setup_delegated_namespaces( const ExecContext *context, ExecParameters *params, - ExecRuntime *runtime, + const ExecRuntime *runtime, + const PinnedResource *rootfs, bool delegate, const char *memory_pressure_path, uid_t uid, @@ -4682,6 +4736,7 @@ static int setup_delegated_namespaces( assert(context); assert(params); assert(runtime); + assert(rootfs); assert(reterr_exit_status); if (exec_needs_network_namespace(context) && @@ -4787,7 +4842,8 @@ static int setup_delegated_namespaces( context, params, runtime, - memory_pressure_path, + rootfs, + memory_pressure_path, needs_sandboxing, uid, gid, @@ -5943,6 +5999,20 @@ int exec_invoke( } } + _cleanup_(pinned_resource_done) PinnedResource rootfs = PINNED_RESOURCE_NULL; + _cleanup_free_ char *error_path = NULL; + r = pin_rootfs(context, params, &rootfs, &error_path); + if (r < 0) { + *exit_status = EXIT_NAMESPACE; + return log_error_errno(r, "Failed to open service's root fs%s%s: %m", error_path ? ": " : "", strempty(error_path)); + } + + r = setup_ephemeral(context, runtime, &rootfs, &error_path); + if (r < 0) { + *exit_status = EXIT_NAMESPACE; + return log_error_errno(r, "Failed to make ephemeral copy of service's root fs%s%s: %m", error_path ? ": " : "", strempty(error_path)); + } + /* Load a bunch of libraries we'll possibly need later, before we turn off dlopen() */ (void) dlopen_bpf(); (void) dlopen_cryptsetup(); @@ -5979,13 +6049,14 @@ int exec_invoke( /* allow_setgroups= */ false); /* If it was requested explicitly and we can't set it up, fail early. Otherwise, continue and let * the actual requested operations fail (or silently continue). */ - if (r < 0 && context->private_users != PRIVATE_USERS_NO) { - *exit_status = EXIT_USER; - return log_error_errno(r, "Failed to set up user namespacing for unprivileged user: %m"); - } - if (r < 0) - log_info_errno(r, "Failed to set up user namespacing for unprivileged user, ignoring: %m"); - else { + if (r < 0) { + if (context->private_users != PRIVATE_USERS_NO) { + *exit_status = EXIT_USER; + return log_error_errno(r, "Failed to set up user namespacing for unprivileged user: %m"); + } + + log_notice_errno(r, "Failed to set up user namespacing for unprivileged user, ignoring: %m"); + } else { assert(r > 0); userns_set_up = true; log_debug("Set up unprivileged user namespace"); @@ -5997,6 +6068,7 @@ int exec_invoke( context, params, runtime, + &rootfs, /* delegate= */ false, memory_pressure_path, uid, @@ -6093,6 +6165,7 @@ int exec_invoke( context, params, runtime, + &rootfs, /* delegate= */ true, memory_pressure_path, uid, @@ -6112,6 +6185,10 @@ int exec_invoke( nsresource_link = sd_varlink_unref(nsresource_link); mountfsd_link = sd_varlink_unref(mountfsd_link); + /* We don't need the pinned rootfs anymore at this point. Close the fds now, so that they are + * definitely gone before we do our fd rearrangements below. */ + pinned_resource_done(&rootfs); + /* Kill unnecessary process, for the case that e.g. when the bpffs mount point is hidden. */ pidref_done_sigkill_wait(&bpffs_pidref); @@ -6545,7 +6622,6 @@ int exec_invoke( } } #endif - } if (!strv_isempty(context->unset_environment)) { diff --git a/src/core/namespace.c b/src/core/namespace.c index 504f80cb63..d363ac0973 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -1297,13 +1297,6 @@ static int create_temporary_mount_point(RuntimeScope scope, char **ret) { return 0; } -static bool namespace_with_rootfs(const NamespaceParameters *p) { - /* Returns true, if we have a root dir, root image or too mstack, and hence the root mount is - * changed */ - - return p->root_image || p->root_directory || p->root_directory_fd >= 0 || p->root_mstack; -} - static int mount_private_dev(const MountEntry *m, const NamespaceParameters *p) { static const char devnodes[] = "/dev/null\0" @@ -1368,7 +1361,7 @@ static int mount_private_dev(const MountEntry *m, const NamespaceParameters *p) /* We assume /run/systemd/journal/ is available if not changing root, which isn't entirely accurate * but shouldn't matter, as either way the user would get ENOENT when accessing /dev/log */ - if (!namespace_with_rootfs(p) || p->bind_log_sockets) { + if (!pinned_resource_is_set(p->rootfs) || p->bind_log_sockets) { const char *devlog = strjoina(temporary_mount, "/dev/log"); if (symlink("/run/systemd/journal/dev-log", devlog) < 0) log_debug_errno(errno, @@ -2562,7 +2555,6 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; _cleanup_(dissected_image_unrefp) DissectedImage *dissected_image = NULL; - _cleanup_(mstack_freep) MStack *mstack = NULL; _cleanup_strv_free_ char **hierarchies = NULL; _cleanup_(mount_list_done) MountList ml = {}; _cleanup_close_ int userns_fd = -EBADF; @@ -2592,83 +2584,136 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { bool setup_propagate = !isempty(p->propagate_dir) && !isempty(p->incoming_dir); unsigned long mount_propagation_flag = p->mount_propagation_flag != 0 ? p->mount_propagation_flag : MS_SHARED; - if (p->root_image) { - /* Make the whole image read-only if we can determine that we only access it in a read-only fashion. */ - if (namespace_read_only(p)) - dissect_image_flags |= DISSECT_IMAGE_READ_ONLY; + /* Make the whole image read-only if we can determine that we only access it in a read-only fashion. */ + bool ro = namespace_read_only(p); + if (ro) { + dissect_image_flags |= DISSECT_IMAGE_READ_ONLY; + mstack_flags |= MSTACK_RDONLY; + } - SET_FLAG(dissect_image_flags, DISSECT_IMAGE_NO_PARTITION_TABLE, p->verity && p->verity->data_path); + _cleanup_close_ int _root_mount_fd = -EBADF; + int root_mount_fd = -EBADF; + if (pinned_resource_is_set(p->rootfs)) { + if (p->rootfs->directory_fd >= 0) { + + /* In "managed" mode we need to map from foreign UID/GID space, hence go via mountfsd */ + if (p->private_users == PRIVATE_USERS_MANAGED) { + 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_directory_fd( + p->mountfsd_link, + p->rootfs->directory_fd, + userns_fd, + dissect_image_flags, + &_root_mount_fd); + if (r < 0) + return r; + + root_mount_fd = _root_mount_fd; + } + + /* Try to to clone the directory mount if we have privs to, so that we can apply the + * MS_SLAVE propagation settings right-away. */ + if (root_mount_fd < 0) { + _root_mount_fd = open_tree_attr_with_fallback( + p->rootfs->directory_fd, + "", + OPEN_TREE_CLONE|OPEN_TREE_CLOEXEC|AT_SYMLINK_NOFOLLOW|AT_EMPTY_PATH|AT_RECURSIVE, + &(struct mount_attr) { + /* We just remounted / as slave, but that didn't affect the detached + * mount that we just mounted, so remount that one as slave recursive + * as well now. */ + .propagation = MS_SLAVE, + }); + if (_root_mount_fd < 0 && !ERRNO_IS_NEG_PRIVILEGE(_root_mount_fd) && _root_mount_fd != -EINVAL) + return log_debug_errno(_root_mount_fd, "Failed to clone specified directory: %m"); + + root_mount_fd = _root_mount_fd; + } + /* If we have only a root fd (and we couldn't make it ours), and we have no path, + * then try to go on with the literal fd */ + if (root_mount_fd < 0 && !p->rootfs->directory) + root_mount_fd = p->rootfs->directory_fd; + } + + if (p->rootfs->image_fd >= 0) { + SET_FLAG(dissect_image_flags, DISSECT_IMAGE_NO_PARTITION_TABLE, p->verity && p->verity->data_path); - /* First check if we have a verity device already open and with a fstype pinned by policy. If it - * cannot be found, then fallback to the slow path (full dissect). */ - r = dissected_image_new_from_existing_verity( - p->root_image, - p->verity, - p->root_image_options, - p->root_image_policy, - /* image_filter= */ NULL, - p->runtime_scope, - dissect_image_flags, - &dissected_image); - if (r < 0 && !ERRNO_IS_NEG_DEVICE_ABSENT(r) && r != -ENOPKG) - return r; - if (r >= 0) - log_debug("Reusing pre-existing verity-protected root image %s", p->root_image); - else { if (p->runtime_scope == RUNTIME_SCOPE_SYSTEM) { /* In system mode we mount directly */ - 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 = dissect_loop_device( - loop_device, + /* First check if we have a verity device already open and with a fstype pinned by policy. If it + * cannot be found, then fallback to the slow path (full dissect). */ + r = dissected_image_new_from_existing_verity( + p->rootfs->image, p->verity, p->root_image_options, p->root_image_policy, /* image_filter= */ NULL, + p->runtime_scope, dissect_image_flags, &dissected_image); - if (r < 0) - return log_debug_errno(r, "Failed to dissect image: %m"); - - r = dissected_image_load_verity_sig_partition( - dissected_image, - loop_device->fd, - p->verity); - if (r < 0) + if (r < 0 && !ERRNO_IS_NEG_DEVICE_ABSENT(r) && r != -ENOPKG) return r; + if (r >= 0) + log_debug("Reusing pre-existing verity-protected root image %s", p->rootfs->image); + else { + r = loop_device_make( + p->rootfs->image_fd, + FLAGS_SET(dissect_image_flags, DISSECT_IMAGE_DEVICE_READ_ONLY) ? O_RDONLY : -1 /* < 0 means take access mode from fd */, + /* offset= */ 0, + /* size= */ UINT64_MAX, + /* 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_guess_verity_roothash( - dissected_image, - p->verity); - if (r < 0) - return r; + r = dissect_loop_device( + loop_device, + p->verity, + p->root_image_options, + p->root_image_policy, + /* image_filter= */ NULL, + dissect_image_flags, + &dissected_image); + if (r < 0) + return log_debug_errno(r, "Failed to dissect image: %m"); - r = dissected_image_decrypt( - dissected_image, - /* root= */ NULL, - /* passphrase= */ NULL, - p->verity, - p->root_image_policy, - 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_guess_verity_roothash( + dissected_image, + p->verity); + if (r < 0) + return r; + + r = dissected_image_decrypt( + dissected_image, + /* root= */ NULL, + /* passphrase= */ NULL, + p->verity, + p->root_image_policy, + 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( + r = mountfsd_mount_image_fd( p->mountfsd_link, - p->root_image, + p->rootfs->image_fd, userns_fd, p->root_image_options, p->root_image_policy, @@ -2679,33 +2724,28 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { return r; } } - } else if (p->root_mstack) { - if (namespace_read_only(p)) - mstack_flags |= MSTACK_RDONLY; - r = mstack_load(p->root_mstack, /* dir_fd= */ -EBADF, &mstack); - if (r < 0) - return r; + if (p->rootfs->mstack_loaded) { + if (p->runtime_scope != RUNTIME_SCOPE_SYSTEM) { + 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"); + } - if (p->runtime_scope != RUNTIME_SCOPE_SYSTEM) { - 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 = mstack_open_images( + p->rootfs->mstack_loaded, + p->mountfsd_link, + userns_fd, + p->root_image_policy, + /* image_filter= */ NULL, + mstack_flags); + if (r < 0) + return r; } - - r = mstack_open_images( - mstack, - p->mountfsd_link, - userns_fd, - p->root_image_policy, - /* image_filter= */ NULL, - mstack_flags); - if (r < 0) - return r; } - if (p->root_directory) - root = p->root_directory; + if (p->rootfs && p->rootfs->directory) + root = p->rootfs->directory; else { /* /run/systemd should have been created by PID 1 early on already, but in some cases, like * when running tests (test-execute), it might not have been created yet so let's make sure @@ -3046,21 +3086,36 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { /* Remount / as SLAVE so that nothing now mounted in the namespace * shows up in the parent */ - if (mount(NULL, "/", NULL, MS_SLAVE|MS_REC, NULL) < 0) - return log_debug_errno(errno, "Failed to remount '/' as SLAVE: %m"); + r = mount_nofollow_verbose(LOG_DEBUG, /* what= */ NULL, "/", /* fstype= */ NULL, MS_SLAVE|MS_REC, /* options= */ NULL); + if (r < 0) + return r; - if (p->root_directory_fd >= 0) { + if (root_mount_fd >= 0) { + /* If we have root_mount_fd we have a ready-to-use detached mount. Attach it. */ - if (move_mount(p->root_directory_fd, "", AT_FDCWD, root, MOVE_MOUNT_F_EMPTY_PATH) < 0) + if (move_mount(root_mount_fd, "", AT_FDCWD, root, MOVE_MOUNT_F_EMPTY_PATH) < 0) return log_debug_errno(errno, "Failed to move detached mount to '%s': %m", root); - /* We just remounted / as slave, but that didn't affect the detached mount that we just - * mounted, so remount that one as slave recursive as well now. */ + r = mount_nofollow_verbose(LOG_DEBUG, /* what= */ NULL, root, /* fstype= */ NULL, MS_SLAVE|MS_REC, /* options= */ NULL); + if (r < 0) + return r; - if (mount(NULL, root, NULL, MS_SLAVE|MS_REC, NULL) < 0) - return log_debug_errno(errno, "Failed to remount '%s' as SLAVE: %m", root); + } else if (p->rootfs && p->rootfs->directory) { + + /* If we do not have root_mount_fd, but a directory was specified, then we can use it directly. */ + + /* A root directory is specified. Turn its directory into bind mount, if it isn't one yet. */ + r = path_is_mount_point_full(root, /* root = */ NULL, AT_SYMLINK_FOLLOW); + if (r < 0) + return log_debug_errno(r, "Failed to detect that %s is a mount point or not: %m", root); + if (r == 0) { + r = mount_nofollow_verbose(LOG_DEBUG, root, root, /* fstype= */ NULL, MS_BIND|MS_REC, /* options= */ NULL); + if (r < 0) + return r; + } + + } else if (dissected_image) { - } else if (p->root_image) { /* A root image is specified, mount it to the right place */ r = dissected_image_mount( dissected_image, @@ -3084,24 +3139,13 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { if (r < 0) return log_debug_errno(r, "Failed to relinquish dissected image: %m"); - } else if (p->root_directory) { + } else if (p->rootfs && p->rootfs->mstack_loaded) { - /* A root directory is specified. Turn its directory into bind mount, if it isn't one yet. */ - r = path_is_mount_point_full(root, /* root= */ NULL, AT_SYMLINK_FOLLOW); - if (r < 0) - return log_debug_errno(r, "Failed to detect that %s is a mount point or not: %m", root); - if (r == 0) { - r = mount_nofollow_verbose(LOG_DEBUG, root, root, NULL, MS_BIND|MS_REC, NULL); - if (r < 0) - return r; - } - - } else if (p->root_mstack) { - r = mstack_make_mounts(mstack, root, mstack_flags); + r = mstack_make_mounts(p->rootfs->mstack_loaded, root, mstack_flags); if (r < 0) return r; - r = mstack_bind_mounts(mstack, root, /* where_fd= */ -EBADF, mstack_flags, /* ret_root_fd= */ NULL); + r = mstack_bind_mounts(p->rootfs->mstack_loaded, root, /* where_fd= */ -EBADF, mstack_flags, /* ret_root_fd= */ NULL); if (r < 0) return r; @@ -3113,7 +3157,7 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { } /* Try to set up the new root directory before mounting anything else there. */ - if (namespace_with_rootfs(p)) + if (pinned_resource_is_set(p->rootfs)) (void) base_filesystem_create(root, UID_INVALID, GID_INVALID); /* Now make the magic happen */ @@ -3122,8 +3166,8 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { return r; /* MS_MOVE does not work on MS_SHARED so the remount MS_SHARED will be done later */ - r = mount_switch_root(root, /* mount_propagation_flag= */ 0); - if (r == -EINVAL && p->root_directory) { + r = mount_switch_root(root, /* mount_propagation_flag = */ 0); + if (r == -EINVAL && p->rootfs && p->rootfs->directory) { /* If we are using root_directory and we don't have privileges (ie: user manager in a user * namespace) and the root_directory is already a mount point in the parent namespace, * MS_MOVE will fail as we don't have permission to change it (with EINVAL rather than @@ -3132,6 +3176,7 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { r = mount_nofollow_verbose(LOG_DEBUG, root, root, NULL, MS_BIND|MS_REC, NULL); if (r < 0) return r; + r = mount_switch_root(root, /* mount_propagation_flag= */ 0); } if (r < 0) @@ -4196,3 +4241,26 @@ static const char* const private_pids_table[_PRIVATE_PIDS_MAX] = { }; DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(private_pids, PrivatePIDs, PRIVATE_PIDS_YES); + +void pinned_resource_done(PinnedResource *p) { + assert(p); + + p->directory_fd = safe_close(p->directory_fd); + p->directory = mfree(p->directory); + p->image_fd = safe_close(p->image_fd); + p->image = mfree(p->image); + p->mstack_loaded = mstack_free(p->mstack_loaded); + p->mstack = mfree(p->mstack); +} + +bool pinned_resource_is_set(const PinnedResource *p) { + if (!p) + return false; + + return p->directory_fd >= 0 || + p->directory || + p->image_fd >= 0 || + p->image || + p->mstack_loaded || + p->mstack; +} diff --git a/src/core/namespace.h b/src/core/namespace.h index 26b0bf8ff2..318836651c 100644 --- a/src/core/namespace.h +++ b/src/core/namespace.h @@ -91,6 +91,24 @@ typedef enum PrivatePIDs { _PRIVATE_PIDS_INVALID = -EINVAL, } PrivatePIDs; +typedef struct PinnedResource { + /* Pins a disk image, directory or mstack by file descriptors. The paths are stored too, but they are + * intended to be decoration only, to enhance log messages and should not be load-bearing + * otherwise. */ + int directory_fd; + char *directory; + int image_fd; + char *image; + MStack *mstack_loaded; + char *mstack; +} PinnedResource; + +#define PINNED_RESOURCE_NULL \ + (PinnedResource) { \ + .directory_fd = -EBADF, \ + .image_fd = -EBADF, \ + } + typedef struct BindMount { char *source; char *destination; @@ -128,10 +146,7 @@ typedef struct MountImage { typedef struct NamespaceParameters { RuntimeScope runtime_scope; - int root_directory_fd; - const char *root_directory; - const char *root_image; - const char *root_mstack; + const PinnedResource *rootfs; const MountOptions *root_image_options; const ImagePolicy *root_image_policy; @@ -201,6 +216,7 @@ typedef struct NamespaceParameters { PrivateTmp private_tmp; PrivateTmp private_var_tmp; PrivatePIDs private_pids; + PrivateUsers private_users; PidRef *bpffs_pidref; int bpffs_socket_fd; @@ -304,3 +320,6 @@ int refresh_extensions_in_namespace( const PidRef *target, const char *hierarchy_env, const NamespaceParameters *p); + +void pinned_resource_done(PinnedResource *p); +bool pinned_resource_is_set(const PinnedResource *p); diff --git a/src/core/service.c b/src/core/service.c index aa3690e92a..b9efd9bdb9 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -3019,7 +3019,6 @@ static void service_enter_refresh_extensions(Service *s) { .n_extension_images = s->exec_context.n_extension_images, .extension_directories = s->exec_context.extension_directories, .extension_image_policy = s->exec_context.extension_image_policy, - .root_directory_fd = -EBADF, }; /* Only reload confext, and not sysext as they also typically contain the executable(s) used diff --git a/src/shared/shared-forward.h b/src/shared/shared-forward.h index 82bdf86330..09ce806ad2 100644 --- a/src/shared/shared-forward.h +++ b/src/shared/shared-forward.h @@ -74,6 +74,7 @@ typedef struct LoopDevice LoopDevice; typedef struct MachineBindUserContext MachineBindUserContext; typedef struct MachineCredentialContext MachineCredentialContext; typedef struct MountOptions MountOptions; +typedef struct MStack MStack; typedef struct OpenFile OpenFile; typedef struct Pkcs11EncryptedKey Pkcs11EncryptedKey; typedef struct Table Table; diff --git a/src/test/test-namespace.c b/src/test/test-namespace.c index c487fc54ec..5b67cd9f96 100644 --- a/src/test/test-namespace.c +++ b/src/test/test-namespace.c @@ -198,7 +198,6 @@ TEST(protect_kernel_logs) { static const NamespaceParameters p = { .runtime_scope = RUNTIME_SCOPE_SYSTEM, .protect_kernel_logs = true, - .root_directory_fd = -EBADF, }; int r; diff --git a/src/test/test-ns.c b/src/test/test-ns.c index c6d6f2e423..9a17ed4f36 100644 --- a/src/test/test-ns.c +++ b/src/test/test-ns.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include #include #include @@ -77,11 +78,20 @@ int main(int argc, char *argv[]) { else log_info("Not chrooted"); + _cleanup_(pinned_resource_done) PinnedResource pr = PINNED_RESOURCE_NULL; + + if (root_directory) { + pr.directory_fd = open(root_directory, O_PATH|O_CLOEXEC|O_DIRECTORY); + assert_se(pr.directory_fd >= 0); + + pr.directory = strdup(root_directory); + assert_se(pr.directory); + } + NamespaceParameters p = { .runtime_scope = RUNTIME_SCOPE_SYSTEM, - .root_directory = root_directory, - .root_directory_fd = -EBADF, + .rootfs = &pr, .read_write_paths = (char**) writable, .read_only_paths = (char**) readonly, From 968f3ebd20d049ecefdcd41e567150bbe449ed82 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 28 Nov 2025 18:26:56 +0100 Subject: [PATCH 30/36] discover-image: make sure we can remove mstacks --- src/shared/discover-image.c | 138 +++++++++++++++++++++++++----------- 1 file changed, 95 insertions(+), 43 deletions(-) diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index 0575310a2c..4c0752c979 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -45,6 +45,7 @@ #include "path-lookup.h" #include "path-util.h" #include "process-util.h" +#include "recurse-dir.h" #include "rm-rf.h" #include "runtime-scope.h" #include "stat-util.h" @@ -1275,11 +1276,95 @@ int image_discover( return 0; } +static int unpriv_remove_cb( + RecurseDirEvent event, + const char *path, + int dir_fd, + int inode_fd, + const struct dirent *de, + const struct statx *sx, + void *userdata) { + + int r, userns_fd = PTR_TO_FD(userdata); + + assert(sx); + + if (event == RECURSE_DIR_ENTER && + S_ISDIR(sx->stx_mode) && + uid_is_foreign(sx->stx_uid)) { + + /* This is owned by the foreign UID range, and a dir, let's remove it via mountfsd userns + * shenanigans. */ + + _cleanup_close_ int tree_fd = -EBADF; + r = mountfsd_mount_directory_fd( + /* vl= */ NULL, + inode_fd, + userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + &tree_fd); + if (r < 0) + return r; + + /* Fork off child that moves into userns and does the copying */ + r = pidref_safe_fork_full( + "rm-tree", + /* stdio_fds= */ NULL, + (int[]) { userns_fd, tree_fd, }, 2, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_WAIT|FORK_REOPEN_LOG, + /* ret= */ NULL); + if (r < 0) + log_debug_errno(r, "Process that was supposed to remove subtree '%s' failed, ignoring: %m", empty_to_root(path)); + else if (r == 0) { + /* child */ + + r = namespace_enter( + /* pidns_fd= */ -EBADF, + /* mntns_fd= */ -EBADF, + /* netns_fd= */ -EBADF, + userns_fd, + /* root_fd= */ -EBADF); + if (r < 0) { + log_debug_errno(r, "Failed to join user namespace: %m"); + _exit(EXIT_FAILURE); + } + + _cleanup_close_ int dfd = fd_reopen(tree_fd, O_DIRECTORY|O_CLOEXEC); + if (dfd < 0) { + log_error_errno(r, "Failed to reopen tree fd: %m"); + _exit(EXIT_FAILURE); + } + + r = rm_rf_children(dfd, REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_CHMOD, /* root_dev= */ NULL); + if (r < 0) { + log_error_errno(r, "Failed to empty '%s' directory in foreign UID mode: %m", empty_to_root(path)); + _exit(EXIT_FAILURE); + } + + _exit(EXIT_SUCCESS); + } + + /* Don't descent further into this one, and delete it immediately */ + return RECURSE_DIR_UNLINK_GRACEFUL; + } + + /* Everything else try to remove */ + if (event == RECURSE_DIR_LEAVE) + return RECURSE_DIR_UNLINK_GRACEFUL; + + return RECURSE_DIR_CONTINUE; +} + static int unprivileged_remove(Image *i) { int r; assert(i); + /* We want this to work in complex .mstack/ hierarchies, where the main directory (and maybe a .v/ + * directory below or two) might be owned by the user themselves, but some subdirs might be owned by + * the foreign UID range. We deal with this by recursively descending down the tree, and removing + * foreign-owned ranges via userns shenanigans, and the rest just like that. */ + _cleanup_close_ int userns_fd = nsresource_allocate_userns( /* vl= */ NULL, /* name= */ NULL, @@ -1287,52 +1372,16 @@ static int unprivileged_remove(Image *i) { if (userns_fd < 0) return log_debug_errno(userns_fd, "Failed to allocate transient user namespace: %m"); - _cleanup_close_ int tree_fd = -EBADF; - r = mountfsd_mount_directory( - /* vl= */ NULL, + r = recurse_dir_at( + AT_FDCWD, i->path, - userns_fd, - DISSECT_IMAGE_FOREIGN_UID, - &tree_fd); + /* statx_mask= */ STATX_TYPE|STATX_UID, + /* n_depth_max= */ UINT_MAX, + RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE|RECURSE_DIR_SAME_MOUNT|RECURSE_DIR_INODE_FD|RECURSE_DIR_TOPLEVEL, + unpriv_remove_cb, + FD_TO_PTR(userns_fd)); if (r < 0) return r; - /* Fork off child that moves into userns and does the copying */ - r = pidref_safe_fork_full( - "rm-tree", - /* stdio_fds= */ NULL, - (int[]) { userns_fd, tree_fd, }, 2, - FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_WAIT|FORK_REOPEN_LOG, - /* ret= */ NULL); - if (r < 0) - return log_debug_errno(r, "Process that was supposed to remove tree failed: %m"); - if (r == 0) { - /* child */ - - r = namespace_enter( - /* pidns_fd= */ -EBADF, - /* mntns_fd= */ -EBADF, - /* netns_fd= */ -EBADF, - userns_fd, - /* root_fd= */ -EBADF); - if (r < 0) { - log_debug_errno(r, "Failed to join user namespace: %m"); - _exit(EXIT_FAILURE); - } - - _cleanup_close_ int dfd = fd_reopen(tree_fd, O_DIRECTORY|O_CLOEXEC); - if (dfd < 0) { - log_error_errno(r, "Failed to reopen tree fd: %m"); - _exit(EXIT_FAILURE); - } - - r = rm_rf_children(dfd, REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_CHMOD, /* root_dev= */ NULL); - if (r < 0) { - log_error_errno(r, "Failed to empty '%s' directory in foreign UID mode: %m", i->path); - _exit(EXIT_FAILURE); - } - - _exit(EXIT_SUCCESS); - } return 0; } @@ -1374,6 +1423,9 @@ int image_remove(Image *i, RuntimeScope scope) { /* Allow deletion of read-only directories */ (void) chattr_path(i->path, 0, FS_IMMUTABLE_FL); + _fallthrough_; + + case IMAGE_MSTACK: /* If this is foreign owned, try an unprivileged remove first, but accept if that doesn't work, and do it directly either way, maybe it works */ if (i->foreign_uid_owned) (void) unprivileged_remove(i); From 6ebaea6990d3b59a1b3a7eaf457302f93e8b9153 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 16 Feb 2026 09:39:40 +0100 Subject: [PATCH 31/36] portable: fix log levels portable_extract_by_path() and install_image() can't agree whether to be of the "logging" or "non-logging" kind --- src/portable/portable.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/portable/portable.c b/src/portable/portable.c index 2fe015d94c..c86a6c2722 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -471,7 +471,7 @@ static int portable_extract_by_path( _cleanup_close_ int rfd = open(path, O_PATH|O_CLOEXEC); if (rfd < 0) - return log_error_errno(errno, "Failed to open '%s': %m", path); + return log_debug_errno(errno, "Failed to open '%s': %m", path); struct stat st; if (fstat(rfd, &st) < 0) @@ -481,7 +481,7 @@ static int portable_extract_by_path( _cleanup_free_ char *image_name = NULL; r = path_extract_filename(path, &image_name); if (r < 0) - return log_error_errno(r, "Failed to extract image name from path '%s': %m", path); + return log_debug_errno(r, "Failed to extract image name from path '%s': %m", path); if (scope == RUNTIME_SCOPE_USER && uid_is_foreign(st.st_uid)) { _cleanup_close_ int userns_fd = nsresource_allocate_userns( @@ -596,7 +596,7 @@ static int portable_extract_by_path( /* root_hash_path= */ NULL, /* root_hash_sig_path= */ NULL); if (r < 0) - return log_error_errno(r, "Failed to read verity artifacts for %s: %m", path); + return log_debug_errno(r, "Failed to read verity artifacts for %s: %m", path); if (verity.data_path) flags |= DISSECT_IMAGE_NO_PARTITION_TABLE; @@ -1830,11 +1830,11 @@ static int install_image( _cleanup_close_ int fd = open(image_path, O_DIRECTORY|O_CLOEXEC); if (fd < 0) - return log_error_errno(errno, "Failed to open '%s': %m", image_path); + return log_debug_errno(errno, "Failed to open '%s': %m", image_path); struct stat st; if (fstat(fd, &st) < 0) - return log_error_errno(errno, "Failed to stat '%s': %m", image_path); + return log_debug_errno(errno, "Failed to stat '%s': %m", image_path); _cleanup_(sd_varlink_unrefp) sd_varlink *mountfsd_link = NULL; r = mountfsd_connect(&mountfsd_link); From cab2caa1701e24fee77cd34d21eeb4d94b2d5141 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 08:29:13 +0100 Subject: [PATCH 32/36] mountpoint-util: fix typo in comment --- src/basic/mountpoint-util.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/basic/mountpoint-util.c b/src/basic/mountpoint-util.c index 10c4f75efd..624a3fcbed 100644 --- a/src/basic/mountpoint-util.c +++ b/src/basic/mountpoint-util.c @@ -621,7 +621,7 @@ bool mount_new_api_supported(void) { if (cache >= 0) return cache; - /* This is the newer API among the ones we use, so use it as boundary */ + /* This is the newest API among the ones we use, so use it as boundary */ r = RET_NERRNO(mount_setattr(-EBADF, NULL, 0, NULL, 0)); if (r == 0 || ERRNO_IS_NOT_SUPPORTED(r)) /* This should return an error if it is working properly */ return (cache = false); From eb581ff6d9556d29f1b9b57d6a40c4adefde16a6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 27 Nov 2025 09:36:34 +0100 Subject: [PATCH 33/36] man: document everything we just added --- man/importctl.xml | 24 ++++- man/rules/meson.build | 2 + man/systemd-mstack.xml | 218 +++++++++++++++++++++++++++++++++++++++++ man/systemd-nspawn.xml | 13 +++ man/systemd.exec.xml | 20 ++++ man/systemd.mstack.xml | 126 ++++++++++++++++++++++++ 6 files changed, 400 insertions(+), 3 deletions(-) create mode 100644 man/systemd-mstack.xml create mode 100644 man/systemd.mstack.xml diff --git a/man/importctl.xml b/man/importctl.xml index 9e97b2a241..3c14b83f08 100644 --- a/man/importctl.xml +++ b/man/importctl.xml @@ -39,9 +39,9 @@ importctl may be used to download, import, and export disk images via systemd-importd.service8. - importctl operates both on block-level disk images (such as DDIs) as well as - file-system-level images (tarballs). It supports disk images in one of the four following - classes: + importctl operates both on block-level disk images (such as DDIs), + file-system-level images (tarballs), as well as OCI images. It supports disk images in one of the four + following classes: VM images or full OS container images, that may be run via @@ -171,6 +171,24 @@ + + pull-oci REF [NAME] + + Downloads the specified OCI container image, and makes it available under the + specified local name in the image directory for the selected . The first + argument must be an OCI container reference, such as library/nginx If the local + name is omitted, it is automatically derived from the last component of the URL, with its suffix + removed. + + When downloading images of this type no image verification is done beyond the usual + authentication of the HTTPS certificates. + + Note that pressing Control-c during execution of this command will not abort the download. Use + cancel-transfer, described below. + + + + import-tar FILE [NAME] import-raw FILE [NAME] diff --git a/man/rules/meson.build b/man/rules/meson.build index 9eae3e8249..94ac1c919b 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1074,6 +1074,7 @@ manpages = [ ['systemd-modules-load.service', '8', ['systemd-modules-load'], 'HAVE_KMOD'], ['systemd-mount', '1', ['systemd-umount'], ''], ['systemd-mountfsd.service', '8', ['systemd-mountfsd'], 'ENABLE_MOUNTFSD'], + ['systemd-mstack', '1', ['mount.mstack'], 'HAVE_BLKID'], ['systemd-mute-console', '1', ['systemd-mute-console.socket', 'systemd-mute-console@.service'], @@ -1249,6 +1250,7 @@ manpages = [ ['systemd.kill', '5', [], ''], ['systemd.link', '5', [], ''], ['systemd.mount', '5', [], ''], + ['systemd.mstack', '7', [], ''], ['systemd.net-naming-scheme', '7', [], ''], ['systemd.netdev', '5', [], 'ENABLE_NETWORKD'], ['systemd.network', '5', [], 'ENABLE_NETWORKD'], diff --git a/man/systemd-mstack.xml b/man/systemd-mstack.xml new file mode 100644 index 0000000000..41ae1fd831 --- /dev/null +++ b/man/systemd-mstack.xml @@ -0,0 +1,218 @@ + + + + + + + + systemd-mstack + systemd + + + + systemd-mstack + 1 + + + + systemd-mstack + mount.mstack + Mstack Discoverable Disk Images (DDIs) + + + + + systemd-mstack OPTIONS IMAGE + + + systemd-mstack OPTIONS --mount IMAGE PATH + + + systemd-mstack OPTIONS --umount PATH + + + + + Description + + systemd-mstack is a tool for introspecting and interacting with + .mstack/ mount stack directories, as described in + systemd.mstack7. It + supports three different operations: + + + Show general mount stack information, including all described + overlayfs layers and bind mounts. + + Mount a mount stack to a local directory. + + Unmount a mount stack from a local directory. + + + The systemd-mstack command may be invoked as mount.mstack in + which case it implements the mount8 "external + helper" interface. This ensures mount stack directories compatible with systemd-mstack + can be mounted directly by mount and fstab5. For + details see below. + + + + + + Commands + + If neither of the command switches listed below are passed the specified mount stack is opened and + general information about it is shown, including a list of all defined layers. + + + + + + + Mount the specified mount stack to the specified directory. + + To unmount a mount stack directory mounted like this use the operation. + + Note that this functionality is also available in mount8 via a + command such as mount -t mstack mystack.mstack targetdir/, as well as in fstab5. For + details, see below. + + + + + + + + This is a shortcut for . + + + + + + + + + Unmount a mount stack from the specified directory. This command expects one argument: + a directory where mount stack was mounted. + + All mounted mounts will be recursively unmounted + + + + + + + + This is a shortcut for . + + + + + + + + + + + + Options + + The following options are understood: + + + + + + + Operate in read-only mode. By default, will establish + writable mount points. If this option is specified they are established in read-only mode + instead. + + + + + + + + If combined with the directory to mount the mount stack to + is created if it is missing. Note that the directory is not automatically removed when the mount + stack is unmounted again. + + + + + + + + If combined with the specified directory where the mount stack + is mounted is removed after unmounting it. + + + + + + + + + + + + + + Exit status + + On success, 0 is returned, a non-zero failure code otherwise. + + + + Invocation as <command>/sbin/mount.mstack</command> + + + + The systemd-mstack executable may be symlinked to + /sbin/mount.mstack. If invoked through that it implements mount8's + "external helper" interface for the (pseudo) file system type mstack. This means + conformant mount stack directories may be mounted directly via + + # mount -t mstack mystack.mstack targetdir/ + + in a fashion mostly equivalent to: + + # systemd-mstack --mount mystack.mstack targetdir/ + + Note that since a single mount stack may contain multiple mount points it should later be unmounted with + umount -R targetdir/, for recursive operation. + + This functionality is particularly useful to mount mount stacks automatically at boot via simple + /etc/fstab entries. For example: + + /path/to/mystack.nspawn /images/mystack/ mstack defaults 0 0 + + When invoked this way the mount options ro, rw map to the + corresponding options listed above (i.e. ). + + + + See Also + + systemd1 + systemd.mstack7 + systemd-nspawn1 + systemd.exec5 + mount8 + umount8 + + + + diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml index fc2c0e145b..da598d090c 100644 --- a/man/systemd-nspawn.xml +++ b/man/systemd-nspawn.xml @@ -370,6 +370,18 @@ + + + + Mount stack to mount the root directory for the container from. Takes a path to a + directory implementing + systemd.mstack7. This + may be used to run a container of an overlayfs assembled from a number of layers, + possibly writable and augmented with additional bind mounts. + + + + @@ -2038,6 +2050,7 @@ After=sys-subsystem-net-devices-ens1.device importctl1 systemd-mountfsd.service8 systemd-nsresourced.service8 + systemd.mstack7 btrfs8 diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index 59b6919a75..825621693e 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -352,6 +352,26 @@ + + RootMStack= + + Takes a path to a + systemd.mstack7 + directory encapsulating a mount stack consisting of layers and bind mounts. Similar to + RootDirectory= and RootImage= this runs the service off a + distinct root file system, in this case set up via overlayfs. + + Since .mstack/ directories may reference disk images (DDIs) similar device + policy extensions and dependencies are in effect when RootMStack= is used as are + if RootImage= is used. + + + + + + + + MountAPIVFS= diff --git a/man/systemd.mstack.xml b/man/systemd.mstack.xml new file mode 100644 index 0000000000..778d2cc9dc --- /dev/null +++ b/man/systemd.mstack.xml @@ -0,0 +1,126 @@ + + + + + + + + systemd.mstack + systemd + + + + systemd.mstack + 7 + + + + systemd.mstack + Mount stacks in self descriptive directories + + + + Description + + Directories with the .mstack/ suffix may encode 'mount stacks' for assembling OS + mount hierarchies based on bind and overlay mounts, for use in + systemd-nspawn1's + switch or the service manager's setting for + services. .mstack/ directroies may contain various files and subdirectories, where + each will effect one layer of an overlayfs mount, or a bind mount. The name of the + file or subdirectory indicates how it shall used in the mount hierarchy. Specifically, the following + names are defined: + + + A layer@id/ directory will be turned into + a layer of an overlayfs mount. The id identifier is used to define the order of the + layers: a version sort is executed, with the first entry being the bottom layer in the + overlayfs stack, and the last entry becoming the highest layer (precisely: + highest "lowerdir") in the overlayfs stack. + + Similar, a layer@id.raw regular file + will be mounted as a DDI, and the resulting mount will be turned into an overlayfs layer, following the + same sorting rules. + + An rw directory will be turned into a writable layer at the very top + of the overlayfs stack. A subdirectory data of it will become + the "upperdir", a subdirectory work will become the "workdir". Note that these two + subdirectories do not need to be created explicitly, they are created automatically on first use should + they be missing. + + A bind@location/ directory will be bind + mounted to the mount point indicated by the location identifier, in read-write + fashion. The loction is encoded via the same escaping logic used for naming .mount + units, i.e. slashes become dashes. + + Similar, a + bind@location.raw file will be mounted as a DDI, and the + resulting mount bind mounted to the specified location. + + A robind@location/ is treated very + similar to bind@location/, but the resulting bind mount + is read-only. + + Similar, robind@location.raw creates a + read-only bind mount from a DDI. + + If a root/ subdirectory it is used as root of the resulting mount + hierarchy, and only the usr/ subtree of the overlayfs mount will be bound to + usr/ in the hierarchy. + + + Note that each of the entry types above may be a symbolic link pointing to a directory or image + file, instead a directory or image file itself. + + On each listed file or subdirectory type the + systemd.v7 + functionality may be used, for automatic selection of versioned resources. + + Use the + systemd-mstack1 tool + to process or mount .mstack/ directories from the command line. + + + + Examples + + The following .mstack/ consists of two read-only overlayfs layers as DDI, plus one + writable directory one on top. The read-only layers are symlinked: + + + foobar.mstack/layer@0.raw → ../base.raw + foobar.mstack/layer@1.raw → ../app.raw + foobar.mstack/rw/ + + + The following .mstack/ consists of a read-only DDI mounted to /usr/ + and writable root: + + + waldo.mstack/layer@0.raw → ../vendor.raw + waldo.mstack/root/ + + + The following .mstack/ consists of a read-only DDI mounted as root, but a + writable /var/ mounted on top: + + + quux.mstack/layer@0.raw → ../myapp1.raw + quux.mstack/bind:var → ../myapp1-var/ + + + + + See Also + + systemd1 + systemd-mstack1 + systemd-nspawn1 + systemd.exec5 + systemd.v7 + systemd-vpick1 + + + + From e31ee582fcd4c5227b78296b78dfdb632e932270 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 26 Nov 2025 09:07:28 +0100 Subject: [PATCH 34/36] ci: add test for OCI downloading --- test/units/TEST-13-NSPAWN.pull-oci.sh | 145 ++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100755 test/units/TEST-13-NSPAWN.pull-oci.sh diff --git a/test/units/TEST-13-NSPAWN.pull-oci.sh b/test/units/TEST-13-NSPAWN.pull-oci.sh new file mode 100755 index 0000000000..e673b711f0 --- /dev/null +++ b/test/units/TEST-13-NSPAWN.pull-oci.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +if ! can_do_rootless_nspawn; then + echo "Skipping unpriv nspawn test" + exit 0 +fi + +# We need FSCONFIG_SET_FD support in overlayfs for .mstack to work. Let's skip +# this test on old kernels, that didn't have that yet. Ideally we'd check for +# the feature itself here, but I couldn't figure out a nice way to detect +# support for this from shell, hence let's do a version check instead. +if systemd-analyze condition 'ConditionVersion= < 6.13' ; then + echo "Kernel too old for FSCONFIG_SET_FD support on overlayfs, skipping pull-oci test". + exit 0 +fi + +export SYSTEMD_LOG_LEVEL=debug +export SYSTEMD_LOG_TARGET=journal + +at_exit() { + rm -rf /var/tmp/pull-oci-test + rm -rf /home/testuser/.local/state/machines/ocibasic + rm -rf /home/testuser/.local/state/machines/ocilayer +} + +trap at_exit EXIT + +# Install a PK rule that allows 'testuser' user to register a machine even +# though they are not on an fg console, just for testing +mkdir -p /etc/polkit-1/rules.d +cat >/etc/polkit-1/rules.d/registermachinetest.rules <<'EOF' +polkit.addRule(function(action, subject) { + if (action.id == "org.freedesktop.machine1.register-machine" && + subject.user == "testuser") { + return polkit.Result.YES; + } +}); +EOF + +run0 -u testuser mkdir -p .local/state/machines + +create_dummy_container /home/testuser/.local/state/machines/ocibasic +cat >/home/testuser/.local/state/machines/ocibasic/sbin/init <home/testuser/.local/state/machines/ocilayer/etc/waldo </var/tmp/pull-oci-test/ocibasic.tar.gz +run0 --pipe -u testuser importctl -m --user export-tar --format=gzip ocilayer - >/var/tmp/pull-oci-test/ocilayer.tar.gz + +OCIBASIC_SHA256="$(sha256sum /var/tmp/pull-oci-test/ocibasic.tar.gz | cut -d' ' -f1)" +OCIBASIC_SIZE="$(stat -c %s /var/tmp/pull-oci-test/ocibasic.tar.gz)" +OCILAYER_SHA256="$(sha256sum /var/tmp/pull-oci-test/ocilayer.tar.gz | cut -d' ' -f1)" +OCILAYER_SIZE="$(stat -c %s /var/tmp/pull-oci-test/ocilayer.tar.gz)" + +# Let's now put together a simple, fake, static OCI registry that sits on +# file:// rather than https://, so that we don't have to spawn an HTTP +# server. After all we don't want to test the server side code, but only the +# client side code, and libcurl nicely abstracts https:// or ftp:// from us. + +mkdir -p /var/tmp/pull-oci-test/v2/ocicombo/manifests +cat >/var/tmp/pull-oci-test/v2/ocicombo/manifests/latest < /usr/lib/systemd/oci-registry/registry.localfile.oci-registry < Date: Tue, 17 Feb 2026 15:03:07 +0100 Subject: [PATCH 35/36] ci: drop 'Ex' suffix from transient props The "Ex" is mostly internal, and our parsers will append it automatically when needed --- test/units/TEST-07-PID1.delegate-namespaces.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/units/TEST-07-PID1.delegate-namespaces.sh b/test/units/TEST-07-PID1.delegate-namespaces.sh index 61d0986685..0221c5a9c3 100755 --- a/test/units/TEST-07-PID1.delegate-namespaces.sh +++ b/test/units/TEST-07-PID1.delegate-namespaces.sh @@ -88,14 +88,14 @@ testcase_multiple_features() { -p PrivateTmp=yes \ -p PrivateDevices=yes \ -p PrivateNetwork=yes \ - -p PrivateUsersEx=self \ + -p PrivateUsers=self \ -p PrivateIPC=yes \ -p ProtectHostname=yes \ -p ProtectClock=yes \ -p ProtectKernelTunables=yes \ -p ProtectKernelModules=yes \ -p ProtectKernelLogs=yes \ - -p ProtectControlGroupsEx=private \ + -p ProtectControlGroups=private \ -p LockPersonality=yes \ -p Environment=ABC=QED \ -p DelegateNamespaces=yes \ From 3bbada87e290f3f0c2ca17f4f10396ec037b03c9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 19 Feb 2026 00:01:03 +0100 Subject: [PATCH 36/36] update TODO --- TODO | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/TODO b/TODO index cf346e0509..d2065bc75b 100644 --- a/TODO +++ b/TODO @@ -124,6 +124,15 @@ Features: * networkd: maintain a file in /run/ that can be symlinked into /run/issue.d/ that always shows the current primary IP address +* oci: add support for blake hashes for layers + +* oci: add support for "importctl import-oci" which implements the "OCI layout" + spec (i.e. acquiring via local fs access), as opposed to the current + "importctl pull-oci" which focusses on the "OCI image spec", i.e. downloads + from the web (i.e. acquiring via URLs). + +* oci: support "data" in any OCI decriptor, not just manifest config. + * report: - should the list of metrics use JSON-SEQ? or maybe be wrapped in a json array (the latter might be necessary, once we sign the combination) @@ -204,6 +213,10 @@ Features: * homed/pam_systemd: allow authentication by ssh-agent, so that run0/polkit can be allowed if caller comes with the right ssh-agent keys. +* machined: gc for OCI layers that are not referenced anymore by any .mstack/ links. + +* pull-oci: progress notification + * networkd/machined: implement reverse name lookups in the resolved hook * networkd's resolved hook: optionally map all lease IP addresses handed out to @@ -220,19 +233,8 @@ Features: the file systems to disk later (using btrfs device replacement), if needed as part of an installer logic. -* add a concept of overlay directory stacks to image discovery, i.e. have a dir - with a name suffix of ".ovl" or so that contains DDIs and plain dirs (and - possible .v dirs) that are glued together on use via overlayfs. one special - subdir should be used as writable top layer. - * journald: log pidfid as another field, i.e. _PIDFDID= -* systemd-nspawn: something like --volatile= but that isn't volatile, but - stores the data in some separate dir on disk. Usecase: keep always up-to-date - DDIs of some OS in your home dir, but combine its /usr/ with a locally - maintained root fs in a regular dir to maintain local state. (idea: call it - --mutable= and take dir or DDI and merge in) - * measure all log-in attempts into a new nvpcr * measure all DDI activations into a new nvpcr @@ -1128,12 +1130,6 @@ Features: automatically support reverting back to older OS version images if newer ones fail to boot. -* implement new "systemd-fsrebind" tool that works like gpt-auto-generator but - looks at a root dir and then applies vpick on various dirs/images to pick a - root tree, a /usr/ tree, a /home/, a /srv/, a /var/ tree and so on. Dirs - could also be btrfs subvols (combine with btrfs auto-snapshort approach for - creating versions like these automatically). - * remove tomoyo support, it's obsolete and unmaintained apparently * In .socket units, add ConnectStream=, ConnectDatagram=,