--- /dev/null
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <string.h>
+
+#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 <registry>/<name>:<tag>. 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.<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.<name>.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);
+}
--- /dev/null
+/* 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);
+}