diff options
Diffstat (limited to 'src/sysupdate')
-rw-r--r-- | src/sysupdate/meson.build | 31 | ||||
-rw-r--r-- | src/sysupdate/sysupdate-cache.c | 88 | ||||
-rw-r--r-- | src/sysupdate/sysupdate-cache.h | 18 | ||||
-rw-r--r-- | src/sysupdate/sysupdate-instance.c | 63 | ||||
-rw-r--r-- | src/sysupdate/sysupdate-instance.h | 67 | ||||
-rw-r--r-- | src/sysupdate/sysupdate-partition.c | 284 | ||||
-rw-r--r-- | src/sysupdate/sysupdate-partition.h | 49 | ||||
-rw-r--r-- | src/sysupdate/sysupdate-pattern.c | 643 | ||||
-rw-r--r-- | src/sysupdate/sysupdate-pattern.h | 18 | ||||
-rw-r--r-- | src/sysupdate/sysupdate-resource.c | 707 | ||||
-rw-r--r-- | src/sysupdate/sysupdate-resource.h | 111 | ||||
-rw-r--r-- | src/sysupdate/sysupdate-transfer.c | 1252 | ||||
-rw-r--r-- | src/sysupdate/sysupdate-transfer.h | 62 | ||||
-rw-r--r-- | src/sysupdate/sysupdate-update-set.c | 63 | ||||
-rw-r--r-- | src/sysupdate/sysupdate-update-set.h | 32 | ||||
-rw-r--r-- | src/sysupdate/sysupdate-util.c | 4 | ||||
-rw-r--r-- | src/sysupdate/sysupdate.c | 1416 | ||||
-rw-r--r-- | src/sysupdate/sysupdate.h | 21 |
18 files changed, 4929 insertions, 0 deletions
diff --git a/src/sysupdate/meson.build b/src/sysupdate/meson.build new file mode 100644 index 0000000..417b47a --- /dev/null +++ b/src/sysupdate/meson.build @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +systemd_sysupdate_sources = files( + 'sysupdate-cache.c', + 'sysupdate-instance.c', + 'sysupdate-partition.c', + 'sysupdate-pattern.c', + 'sysupdate-resource.c', + 'sysupdate-transfer.c', + 'sysupdate-update-set.c', + 'sysupdate.c', +) + +executables += [ + libexec_template + { + 'name' : 'systemd-sysupdate', + 'public' : true, + 'conditions' : ['ENABLE_SYSUPDATE'], + 'sources' : systemd_sysupdate_sources, + 'link_with' : [ + libshared, + libshared_fdisk, + ], + 'dependencies' : [ + libblkid, + libfdisk, + libopenssl, + threads, + ], + }, +] diff --git a/src/sysupdate/sysupdate-cache.c b/src/sysupdate/sysupdate-cache.c new file mode 100644 index 0000000..8dad3ee --- /dev/null +++ b/src/sysupdate/sysupdate-cache.c @@ -0,0 +1,88 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "memory-util.h" +#include "sysupdate-cache.h" + +#define WEB_CACHE_ENTRIES_MAX 64U +#define WEB_CACHE_ITEM_SIZE_MAX (64U*1024U*1024U) + +static WebCacheItem* web_cache_item_free(WebCacheItem *i) { + if (!i) + return NULL; + + free(i->url); + return mfree(i); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(WebCacheItem*, web_cache_item_free); + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(web_cache_hash_ops, char, string_hash_func, string_compare_func, WebCacheItem, web_cache_item_free); + +int web_cache_add_item( + Hashmap **web_cache, + const char *url, + bool verified, + const void *data, + size_t size) { + + _cleanup_(web_cache_item_freep) WebCacheItem *item = NULL; + _cleanup_free_ char *u = NULL; + int r; + + assert(web_cache); + assert(url); + assert(data || size == 0); + + if (size > WEB_CACHE_ITEM_SIZE_MAX) + return -E2BIG; + + item = web_cache_get_item(*web_cache, url, verified); + if (item && memcmp_nn(item->data, item->size, data, size) == 0) + return 0; + + if (hashmap_size(*web_cache) >= (size_t) (WEB_CACHE_ENTRIES_MAX + !!hashmap_get(*web_cache, url))) + return -ENOSPC; + + r = hashmap_ensure_allocated(web_cache, &web_cache_hash_ops); + if (r < 0) + return r; + + u = strdup(url); + if (!u) + return -ENOMEM; + + item = malloc(offsetof(WebCacheItem, data) + size + 1); + if (!item) + return -ENOMEM; + + *item = (WebCacheItem) { + .url = TAKE_PTR(u), + .size = size, + .verified = verified, + }; + + /* Just to be extra paranoid, let's NUL terminate the downloaded buffer */ + *(uint8_t*) mempcpy(item->data, data, size) = 0; + + web_cache_item_free(hashmap_remove(*web_cache, url)); + + r = hashmap_put(*web_cache, item->url, item); + if (r < 0) + return r; + + TAKE_PTR(item); + return 1; +} + +WebCacheItem* web_cache_get_item(Hashmap *web_cache, const char *url, bool verified) { + WebCacheItem *i; + + i = hashmap_get(web_cache, url); + if (!i) + return NULL; + + if (i->verified != verified) + return NULL; + + return i; +} diff --git a/src/sysupdate/sysupdate-cache.h b/src/sysupdate/sysupdate-cache.h new file mode 100644 index 0000000..d6a7897 --- /dev/null +++ b/src/sysupdate/sysupdate-cache.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "hashmap.h" + +typedef struct WebCacheItem { + char *url; + bool verified; + size_t size; + uint8_t data[]; +} WebCacheItem; + +/* A simple in-memory cache for downloaded manifests. Very likely multiple transfers will use the same + * manifest URLs, hence let's make sure we only download them once within each sysupdate invocation. */ + +int web_cache_add_item(Hashmap **cache, const char *url, bool verified, const void *data, size_t size); + +WebCacheItem* web_cache_get_item(Hashmap *cache, const char *url, bool verified); diff --git a/src/sysupdate/sysupdate-instance.c b/src/sysupdate/sysupdate-instance.c new file mode 100644 index 0000000..16bfab9 --- /dev/null +++ b/src/sysupdate/sysupdate-instance.c @@ -0,0 +1,63 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <fcntl.h> +#include <sys/stat.h> + +#include "sysupdate-instance.h" + +void instance_metadata_destroy(InstanceMetadata *m) { + assert(m); + free(m->version); +} + +int instance_new( + Resource *rr, + const char *path, + const InstanceMetadata *f, + Instance **ret) { + + _cleanup_(instance_freep) Instance *i = NULL; + _cleanup_free_ char *p = NULL, *v = NULL; + + assert(rr); + assert(path); + assert(f); + assert(f->version); + assert(ret); + + p = strdup(path); + if (!p) + return log_oom(); + + v = strdup(f->version); + if (!v) + return log_oom(); + + i = new(Instance, 1); + if (!i) + return log_oom(); + + *i = (Instance) { + .resource = rr, + .metadata = *f, + .path = TAKE_PTR(p), + .partition_info = PARTITION_INFO_NULL, + }; + + i->metadata.version = TAKE_PTR(v); + + *ret = TAKE_PTR(i); + return 0; +} + +Instance *instance_free(Instance *i) { + if (!i) + return NULL; + + instance_metadata_destroy(&i->metadata); + + free(i->path); + partition_info_destroy(&i->partition_info); + + return mfree(i); +} diff --git a/src/sysupdate/sysupdate-instance.h b/src/sysupdate/sysupdate-instance.h new file mode 100644 index 0000000..2860d29 --- /dev/null +++ b/src/sysupdate/sysupdate-instance.h @@ -0,0 +1,67 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <stdbool.h> +#include <sys/types.h> + +#include "sd-id128.h" + +#include "fs-util.h" +#include "time-util.h" + +typedef struct InstanceMetadata InstanceMetadata; +typedef struct Instance Instance; + +#include "sysupdate-resource.h" +#include "sysupdate-partition.h" + +struct InstanceMetadata { + /* Various bits of metadata for each instance, that is either derived from the filename/GPT label or + * from metadata of the file/partition itself */ + char *version; + sd_id128_t partition_uuid; + bool partition_uuid_set; + uint64_t partition_flags; /* GPT partition flags */ + bool partition_flags_set; + usec_t mtime; + mode_t mode; + uint64_t size; /* uncompressed size of the file */ + uint64_t tries_done, tries_left; /* for boot assessment counters */ + int no_auto; + int read_only; + int growfs; + uint8_t sha256sum[32]; /* SHA256 sum of the download (i.e. compressed) file */ + bool sha256sum_set; +}; + +#define INSTANCE_METADATA_NULL \ + { \ + .mtime = USEC_INFINITY, \ + .mode = MODE_INVALID, \ + .size = UINT64_MAX, \ + .tries_done = UINT64_MAX, \ + .tries_left = UINT64_MAX, \ + .no_auto = -1, \ + .read_only = -1, \ + .growfs = -1, \ + } + +struct Instance { + /* A pointer back to the resource this belongs to */ + Resource *resource; + + /* Metadata of this version */ + InstanceMetadata metadata; + + /* Where we found the instance */ + char *path; + PartitionInfo partition_info; +}; + +void instance_metadata_destroy(InstanceMetadata *m); + +int instance_new(Resource *rr, const char *path, const InstanceMetadata *f, Instance **ret); +Instance *instance_free(Instance *i); + +DEFINE_TRIVIAL_CLEANUP_FUNC(Instance*, instance_free); diff --git a/src/sysupdate/sysupdate-partition.c b/src/sysupdate/sysupdate-partition.c new file mode 100644 index 0000000..6f8e072 --- /dev/null +++ b/src/sysupdate/sysupdate-partition.c @@ -0,0 +1,284 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <sys/file.h> + +#include "alloc-util.h" +#include "extract-word.h" +#include "gpt.h" +#include "id128-util.h" +#include "parse-util.h" +#include "stdio-util.h" +#include "string-util.h" +#include "sysupdate-partition.h" + +void partition_info_destroy(PartitionInfo *p) { + assert(p); + + p->label = mfree(p->label); + p->device = mfree(p->device); +} + +int read_partition_info( + struct fdisk_context *c, + struct fdisk_table *t, + size_t i, + PartitionInfo *ret) { + + _cleanup_free_ char *label_copy = NULL, *device = NULL; + const char *label; + struct fdisk_partition *p; + uint64_t start, size, flags; + unsigned long ssz; + sd_id128_t ptid, id; + GptPartitionType type; + size_t partno; + int r; + + assert(c); + assert(t); + assert(ret); + + p = fdisk_table_get_partition(t, i); + if (!p) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to read partition metadata: %m"); + + if (fdisk_partition_is_used(p) <= 0) { + *ret = (PartitionInfo) PARTITION_INFO_NULL; + return 0; /* not found! */ + } + + if (fdisk_partition_has_partno(p) <= 0 || + fdisk_partition_has_start(p) <= 0 || + fdisk_partition_has_size(p) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Found a partition without a number, position or size."); + + partno = fdisk_partition_get_partno(p); + + start = fdisk_partition_get_start(p); + ssz = fdisk_get_sector_size(c); + assert(start <= UINT64_MAX / ssz); + start *= ssz; + + size = fdisk_partition_get_size(p); + assert(size <= UINT64_MAX / ssz); + size *= ssz; + + label = fdisk_partition_get_name(p); + if (!label) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Found a partition without a label."); + + r = fdisk_partition_get_type_as_id128(p, &ptid); + if (r < 0) + return log_error_errno(r, "Failed to read partition type UUID: %m"); + + r = fdisk_partition_get_uuid_as_id128(p, &id); + if (r < 0) + return log_error_errno(r, "Failed to read partition UUID: %m"); + + r = fdisk_partition_get_attrs_as_uint64(p, &flags); + if (r < 0) + return log_error_errno(r, "Failed to get partition flags: %m"); + + r = fdisk_partition_to_string(p, c, FDISK_FIELD_DEVICE, &device); + if (r != 0) + return log_error_errno(r, "Failed to get partition device name: %m"); + + label_copy = strdup(label); + if (!label_copy) + return log_oom(); + + type = gpt_partition_type_from_uuid(ptid); + + *ret = (PartitionInfo) { + .partno = partno, + .start = start, + .size = size, + .flags = flags, + .type = ptid, + .uuid = id, + .label = TAKE_PTR(label_copy), + .device = TAKE_PTR(device), + .no_auto = FLAGS_SET(flags, SD_GPT_FLAG_NO_AUTO) && gpt_partition_type_knows_no_auto(type), + .read_only = FLAGS_SET(flags, SD_GPT_FLAG_READ_ONLY) && gpt_partition_type_knows_read_only(type), + .growfs = FLAGS_SET(flags, SD_GPT_FLAG_GROWFS) && gpt_partition_type_knows_growfs(type), + }; + + return 1; /* found! */ +} + +int find_suitable_partition( + const char *device, + uint64_t space, + sd_id128_t *partition_type, + PartitionInfo *ret) { + + _cleanup_(partition_info_destroy) PartitionInfo smallest = PARTITION_INFO_NULL; + _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL; + _cleanup_(fdisk_unref_tablep) struct fdisk_table *t = NULL; + size_t n_partitions; + int r; + + assert(device); + assert(ret); + + r = fdisk_new_context_at(AT_FDCWD, device, /* read_only= */ true, /* sector_size= */ UINT32_MAX, &c); + if (r < 0) + return log_error_errno(r, "Failed to create fdisk context from '%s': %m", device); + + if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT)) + return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "Disk %s has no GPT disk label, not suitable.", device); + + r = fdisk_get_partitions(c, &t); + if (r < 0) + return log_error_errno(r, "Failed to acquire partition table: %m"); + + n_partitions = fdisk_table_get_nents(t); + for (size_t i = 0; i < n_partitions; i++) { + _cleanup_(partition_info_destroy) PartitionInfo pinfo = PARTITION_INFO_NULL; + + r = read_partition_info(c, t, i, &pinfo); + if (r < 0) + return r; + if (r == 0) /* not assigned */ + continue; + + /* Filter out non-matching partition types */ + if (partition_type && !sd_id128_equal(pinfo.type, *partition_type)) + continue; + + if (!streq_ptr(pinfo.label, "_empty")) /* used */ + continue; + + if (space != UINT64_MAX && pinfo.size < space) /* too small */ + continue; + + if (smallest.partno != SIZE_MAX && smallest.size <= pinfo.size) /* already found smaller */ + continue; + + smallest = pinfo; + pinfo = (PartitionInfo) PARTITION_INFO_NULL; + } + + if (smallest.partno == SIZE_MAX) + return log_error_errno(SYNTHETIC_ERRNO(ENOSPC), "No available partition of a suitable size found."); + + *ret = smallest; + smallest = (PartitionInfo) PARTITION_INFO_NULL; + + return 0; +} + +int patch_partition( + const char *device, + const PartitionInfo *info, + PartitionChange change) { + + _cleanup_(fdisk_unref_partitionp) struct fdisk_partition *pa = NULL; + _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL; + bool tweak_no_auto, tweak_read_only, tweak_growfs; + GptPartitionType type; + int r, fd; + + assert(device); + assert(info); + assert(change <= _PARTITION_CHANGE_MAX); + + if (change == 0) /* Nothing to do */ + return 0; + + r = fdisk_new_context_at(AT_FDCWD, device, /* read_only= */ false, /* sector_size= */ UINT32_MAX, &c); + if (r < 0) + return log_error_errno(r, "Failed to create fdisk context from '%s': %m", device); + + assert_se((fd = fdisk_get_devfd(c)) >= 0); + + /* Make sure udev doesn't read the device while we make changes (this lock is released automatically + * by the kernel when the fd is closed, i.e. when the fdisk context is freed, hence no explicit + * unlock by us here anywhere.) */ + if (flock(fd, LOCK_EX) < 0) + return log_error_errno(errno, "Failed to lock block device '%s': %m", device); + + if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT)) + return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "Disk %s has no GPT disk label, not suitable.", device); + + r = fdisk_get_partition(c, info->partno, &pa); + if (r < 0) + return log_error_errno(r, "Failed to read partition %zu of GPT label of '%s': %m", info->partno, device); + + if (change & PARTITION_LABEL) { + r = fdisk_partition_set_name(pa, info->label); + if (r < 0) + return log_error_errno(r, "Failed to update partition label: %m"); + } + + if (change & PARTITION_UUID) { + r = fdisk_partition_set_uuid(pa, SD_ID128_TO_UUID_STRING(info->uuid)); + if (r < 0) + return log_error_errno(r, "Failed to update partition UUID: %m"); + } + + type = gpt_partition_type_from_uuid(info->type); + + /* Tweak the read-only flag, but only if supported by the partition type */ + tweak_no_auto = + FLAGS_SET(change, PARTITION_NO_AUTO) && + gpt_partition_type_knows_no_auto(type); + tweak_read_only = + FLAGS_SET(change, PARTITION_READ_ONLY) && + gpt_partition_type_knows_read_only(type); + tweak_growfs = + FLAGS_SET(change, PARTITION_GROWFS) && + gpt_partition_type_knows_growfs(type); + + if (change & PARTITION_FLAGS) { + uint64_t flags; + + /* Update the full flags parameter, and import the read-only flag into it */ + + flags = info->flags; + if (tweak_no_auto) + SET_FLAG(flags, SD_GPT_FLAG_NO_AUTO, info->no_auto); + if (tweak_read_only) + SET_FLAG(flags, SD_GPT_FLAG_READ_ONLY, info->read_only); + if (tweak_growfs) + SET_FLAG(flags, SD_GPT_FLAG_GROWFS, info->growfs); + + r = fdisk_partition_set_attrs_as_uint64(pa, flags); + if (r < 0) + return log_error_errno(r, "Failed to update partition flags: %m"); + + } else if (tweak_no_auto || tweak_read_only || tweak_growfs) { + uint64_t old_flags, new_flags; + + /* So we aren't supposed to update the full flags parameter, but we are supposed to update + * the RO flag of it. */ + + r = fdisk_partition_get_attrs_as_uint64(pa, &old_flags); + if (r < 0) + return log_error_errno(r, "Failed to get old partition flags: %m"); + + new_flags = old_flags; + if (tweak_no_auto) + SET_FLAG(new_flags, SD_GPT_FLAG_NO_AUTO, info->no_auto); + if (tweak_read_only) + SET_FLAG(new_flags, SD_GPT_FLAG_READ_ONLY, info->read_only); + if (tweak_growfs) + SET_FLAG(new_flags, SD_GPT_FLAG_GROWFS, info->growfs); + + if (new_flags != old_flags) { + r = fdisk_partition_set_attrs_as_uint64(pa, new_flags); + if (r < 0) + return log_error_errno(r, "Failed to update partition flags: %m"); + } + } + + r = fdisk_set_partition(c, info->partno, pa); + if (r < 0) + return log_error_errno(r, "Failed to update partition: %m"); + + r = fdisk_write_disklabel(c); + if (r < 0) + return log_error_errno(r, "Failed to write updated partition table: %m"); + + return 0; +} diff --git a/src/sysupdate/sysupdate-partition.h b/src/sysupdate/sysupdate-partition.h new file mode 100644 index 0000000..672eb93 --- /dev/null +++ b/src/sysupdate/sysupdate-partition.h @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <sys/types.h> + +#include "sd-id128.h" + +#include "fdisk-util.h" +#include "macro.h" + +typedef struct PartitionInfo PartitionInfo; + +typedef enum PartitionChange { + PARTITION_FLAGS = 1 << 0, + PARTITION_NO_AUTO = 1 << 1, + PARTITION_READ_ONLY = 1 << 2, + PARTITION_GROWFS = 1 << 3, + PARTITION_UUID = 1 << 4, + PARTITION_LABEL = 1 << 5, + _PARTITION_CHANGE_MAX = (1 << 6) - 1, /* all of the above */ + _PARTITION_CHANGE_INVALID = -EINVAL, +} PartitionChange; + +struct PartitionInfo { + size_t partno; + uint64_t start, size; + uint64_t flags; + sd_id128_t type, uuid; + char *label; + char *device; /* Note that this might point to some non-existing path in case we operate on a loopback file */ + bool no_auto:1; + bool read_only:1; + bool growfs:1; +}; + +#define PARTITION_INFO_NULL \ + { \ + .partno = SIZE_MAX, \ + .start = UINT64_MAX, \ + .size = UINT64_MAX, \ + } + +void partition_info_destroy(PartitionInfo *p); + +int read_partition_info(struct fdisk_context *c, struct fdisk_table *t, size_t i, PartitionInfo *ret); + +int find_suitable_partition(const char *device, uint64_t space, sd_id128_t *partition_type, PartitionInfo *ret); +int patch_partition(const char *device, const PartitionInfo *info, PartitionChange change); diff --git a/src/sysupdate/sysupdate-pattern.c b/src/sysupdate/sysupdate-pattern.c new file mode 100644 index 0000000..ff018d8 --- /dev/null +++ b/src/sysupdate/sysupdate-pattern.c @@ -0,0 +1,643 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "hexdecoct.h" +#include "list.h" +#include "parse-util.h" +#include "path-util.h" +#include "stdio-util.h" +#include "string-util.h" +#include "sysupdate-pattern.h" + +typedef enum PatternElementType { + PATTERN_LITERAL, + PATTERN_VERSION, + PATTERN_PARTITION_UUID, + PATTERN_PARTITION_FLAGS, + PATTERN_MTIME, + PATTERN_MODE, + PATTERN_SIZE, + PATTERN_TRIES_DONE, + PATTERN_TRIES_LEFT, + PATTERN_NO_AUTO, + PATTERN_READ_ONLY, + PATTERN_GROWFS, + PATTERN_SHA256SUM, + PATTERN_SLASH, + _PATTERN_ELEMENT_TYPE_MAX, + _PATTERN_ELEMENT_TYPE_INVALID = -EINVAL, +} PatternElementType; + +typedef struct PatternElement PatternElement; + +struct PatternElement { + PatternElementType type; + LIST_FIELDS(PatternElement, elements); + char literal[]; +}; + +static PatternElement *pattern_element_free_all(PatternElement *e) { + LIST_CLEAR(elements, e, free); + + return NULL; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(PatternElement*, pattern_element_free_all); + +static PatternElementType pattern_element_type_from_char(char c) { + switch (c) { + case 'v': + return PATTERN_VERSION; + case 'u': + return PATTERN_PARTITION_UUID; + case 'f': + return PATTERN_PARTITION_FLAGS; + case 't': + return PATTERN_MTIME; + case 'm': + return PATTERN_MODE; + case 's': + return PATTERN_SIZE; + case 'd': + return PATTERN_TRIES_DONE; + case 'l': + return PATTERN_TRIES_LEFT; + case 'a': + return PATTERN_NO_AUTO; + case 'r': + return PATTERN_READ_ONLY; + case 'g': + return PATTERN_GROWFS; + case 'h': + return PATTERN_SHA256SUM; + default: + return _PATTERN_ELEMENT_TYPE_INVALID; + } +} + +static bool valid_char(char x) { + + /* Let's refuse control characters here, and let's reserve some characters typically used in pattern + * languages so that we can use them later, possibly. */ + + if ((unsigned) x < ' ' || x >= 127) + return false; + + return !IN_SET(x, '$', '*', '?', '[', ']', '!', '\\', '|'); +} + +static int pattern_split( + const char *pattern, + PatternElement **ret) { + + _cleanup_(pattern_element_free_allp) PatternElement *first = NULL; + bool at = false, last_literal = true, last_slash = false; + PatternElement *last = NULL; + uint64_t mask_found = 0; + size_t l, k = 0; + + assert(pattern); + + l = strlen(pattern); + + for (const char *e = pattern; *e != 0; e++) { + if (*e == '@') { + if (!at) { + at = true; + continue; + } + + /* Two at signs in a sequence, write out one */ + at = false; + + } else if (at) { + PatternElementType t; + uint64_t bit; + + t = pattern_element_type_from_char(*e); + if (t < 0) + return log_debug_errno(t, "Unknown pattern field marker '@%c'.", *e); + + bit = UINT64_C(1) << t; + if (mask_found & bit) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Pattern field marker '@%c' appears twice in pattern.", *e); + + /* We insist that two pattern field markers are separated by some literal string that + * we can use to separate the fields when parsing. */ + if (!last_literal && !last_slash) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Found two pattern field markers without separating literal."); + + if (ret) { + PatternElement *z; + + z = malloc(offsetof(PatternElement, literal)); + if (!z) + return -ENOMEM; + + z->type = t; + LIST_INSERT_AFTER(elements, first, last, z); + last = z; + } + + mask_found |= bit; + last_slash = last_literal = at = false; + continue; + } + + if (*e == '/') { + if (ret) { + PatternElement *z; + + z = malloc(offsetof(PatternElement, literal)); + if (!z) + return -ENOMEM; + + z->type = PATTERN_SLASH; + LIST_INSERT_AFTER(elements, first, last, z); + last = z; + } + + last_literal = false; + last_slash = true; + continue ; + } + + if (!valid_char(*e)) + return log_debug_errno( + SYNTHETIC_ERRNO(EBADRQC), + "Invalid character 0x%0x in pattern, refusing.", + (unsigned) *e); + + last_literal = true; + last_slash = false; + + if (!ret) + continue; + + if (!last || last->type != PATTERN_LITERAL) { + PatternElement *z; + + z = malloc0(offsetof(PatternElement, literal) + l + 1); /* l is an upper bound to all literal elements */ + if (!z) + return -ENOMEM; + + z->type = PATTERN_LITERAL; + k = 0; + + LIST_INSERT_AFTER(elements, first, last, z); + last = z; + } + + assert(last); + assert(last->type == PATTERN_LITERAL); + + last->literal[k++] = *e; + } + + if (at) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Trailing @ character found, refusing."); + if (!(mask_found & (UINT64_C(1) << PATTERN_VERSION))) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Version field marker '@v' not specified in pattern, refusing."); + + if (ret) + *ret = TAKE_PTR(first); + + return 0; +} + +int pattern_match(const char *pattern, const char *s, InstanceMetadata *ret) { + _cleanup_(instance_metadata_destroy) InstanceMetadata found = INSTANCE_METADATA_NULL; + _cleanup_(pattern_element_free_allp) PatternElement *elements = NULL; + const char *p; + int r; + + assert(pattern); + assert(s); + + r = pattern_split(pattern, &elements); + if (r < 0) + return r; + + p = s; + LIST_FOREACH(elements, e, elements) { + _cleanup_free_ char *t = NULL; + const char *n; + + if (e->type == PATTERN_SLASH) { + if (*p == '/') { + ++p; + continue; + } else if (*p == '\0') + goto retry; + else + goto nope; + } + + if (e->type == PATTERN_LITERAL) { + const char *k; + + /* Skip literal fields */ + k = startswith(p, e->literal); + if (!k) + goto nope; + + p = k; + continue; + } + + if (e->elements_next) { + /* The next element must be literal, as we use it to determine where to split */ + assert(e->elements_next->type == PATTERN_LITERAL); + + n = strstr(p, e->elements_next->literal); + if (!n) + goto nope; + + } else + /* End of the string */ + assert_se(n = strchr(p, 0)); + t = strndup(p, n - p); + if (!t) + return -ENOMEM; + + switch (e->type) { + + case PATTERN_VERSION: + if (!version_is_valid(t)) { + log_debug("Version string is not valid, refusing: %s", t); + goto nope; + } + + assert(!found.version); + found.version = TAKE_PTR(t); + break; + + case PATTERN_PARTITION_UUID: { + sd_id128_t id; + + if (sd_id128_from_string(t, &id) < 0) + goto nope; + + assert(!found.partition_uuid_set); + found.partition_uuid = id; + found.partition_uuid_set = true; + break; + } + + case PATTERN_PARTITION_FLAGS: { + uint64_t f; + + if (safe_atoux64(t, &f) < 0) + goto nope; + + if (found.partition_flags_set && found.partition_flags != f) + goto nope; + + assert(!found.partition_flags_set); + found.partition_flags = f; + found.partition_flags_set = true; + break; + } + + case PATTERN_MTIME: { + uint64_t v; + + if (safe_atou64(t, &v) < 0) + goto nope; + if (v == USEC_INFINITY) /* Don't permit our internal special infinity value */ + goto nope; + if (v / 1000000U > TIME_T_MAX) /* Make sure this fits in a timespec structure */ + goto nope; + + assert(found.mtime == USEC_INFINITY); + found.mtime = v; + break; + } + + case PATTERN_MODE: { + mode_t m; + + r = parse_mode(t, &m); + if (r < 0) + goto nope; + if (m & ~0775) /* Don't allow world-writable files or suid files to be generated this way */ + goto nope; + + assert(found.mode == MODE_INVALID); + found.mode = m; + break; + } + + case PATTERN_SIZE: { + uint64_t u; + + r = safe_atou64(t, &u); + if (r < 0) + goto nope; + if (u == UINT64_MAX) + goto nope; + + assert(found.size == UINT64_MAX); + found.size = u; + break; + } + + case PATTERN_TRIES_DONE: { + uint64_t u; + + r = safe_atou64(t, &u); + if (r < 0) + goto nope; + if (u == UINT64_MAX) + goto nope; + + assert(found.tries_done == UINT64_MAX); + found.tries_done = u; + break; + } + + case PATTERN_TRIES_LEFT: { + uint64_t u; + + r = safe_atou64(t, &u); + if (r < 0) + goto nope; + if (u == UINT64_MAX) + goto nope; + + assert(found.tries_left == UINT64_MAX); + found.tries_left = u; + break; + } + + case PATTERN_NO_AUTO: + r = parse_boolean(t); + if (r < 0) + goto nope; + + assert(found.no_auto < 0); + found.no_auto = r; + break; + + case PATTERN_READ_ONLY: + r = parse_boolean(t); + if (r < 0) + goto nope; + + assert(found.read_only < 0); + found.read_only = r; + break; + + case PATTERN_GROWFS: + r = parse_boolean(t); + if (r < 0) + goto nope; + + assert(found.growfs < 0); + found.growfs = r; + break; + + case PATTERN_SHA256SUM: { + _cleanup_free_ void *d = NULL; + size_t l; + + if (strlen(t) != sizeof(found.sha256sum) * 2) + goto nope; + + r = unhexmem(t, sizeof(found.sha256sum) * 2, &d, &l); + if (r == -ENOMEM) + return r; + if (r < 0) + goto nope; + + assert(!found.sha256sum_set); + assert(l == sizeof(found.sha256sum)); + memcpy(found.sha256sum, d, l); + found.sha256sum_set = true; + break; + } + + default: + assert_se("unexpected pattern element"); + } + + p = n; + } + + if (ret) { + *ret = found; + found = (InstanceMetadata) INSTANCE_METADATA_NULL; + } + + return PATTERN_MATCH_YES; + +nope: + if (ret) + *ret = (InstanceMetadata) INSTANCE_METADATA_NULL; + + return PATTERN_MATCH_NO; + +retry: + if (ret) + *ret = (InstanceMetadata) INSTANCE_METADATA_NULL; + + return PATTERN_MATCH_RETRY; +} + +int pattern_match_many(char **patterns, const char *s, InstanceMetadata *ret) { + _cleanup_(instance_metadata_destroy) InstanceMetadata found = INSTANCE_METADATA_NULL; + int r; + + STRV_FOREACH(p, patterns) { + r = pattern_match(*p, s, &found); + if (r < 0) + return r; + if (r > 0) { + if (ret) { + *ret = found; + found = (InstanceMetadata) INSTANCE_METADATA_NULL; + } + + return r; + } + } + + if (ret) + *ret = (InstanceMetadata) INSTANCE_METADATA_NULL; + + return PATTERN_MATCH_NO; +} + +int pattern_valid(const char *pattern) { + int r; + + r = pattern_split(pattern, NULL); + if (r == -EINVAL) + return false; + if (r < 0) + return r; + + return true; +} + +int pattern_format( + const char *pattern, + const InstanceMetadata *fields, + char **ret) { + + _cleanup_(pattern_element_free_allp) PatternElement *elements = NULL; + _cleanup_free_ char *j = NULL; + int r; + + assert(pattern); + assert(fields); + assert(ret); + + r = pattern_split(pattern, &elements); + if (r < 0) + return r; + + LIST_FOREACH(elements, e, elements) { + + switch (e->type) { + + case PATTERN_SLASH: + if (!strextend(&j, "/")) + return -ENOMEM; + + break; + + case PATTERN_LITERAL: + if (!strextend(&j, e->literal)) + return -ENOMEM; + + break; + + case PATTERN_VERSION: + if (!fields->version) + return -ENXIO; + + if (!strextend(&j, fields->version)) + return -ENOMEM; + break; + + case PATTERN_PARTITION_UUID: { + char formatted[SD_ID128_STRING_MAX]; + + if (!fields->partition_uuid_set) + return -ENXIO; + + if (!strextend(&j, sd_id128_to_string(fields->partition_uuid, formatted))) + return -ENOMEM; + + break; + } + + case PATTERN_PARTITION_FLAGS: + if (!fields->partition_flags_set) + return -ENXIO; + + r = strextendf(&j, "%" PRIx64, fields->partition_flags); + if (r < 0) + return r; + + break; + + case PATTERN_MTIME: + if (fields->mtime == USEC_INFINITY) + return -ENXIO; + + r = strextendf(&j, "%" PRIu64, fields->mtime); + if (r < 0) + return r; + + break; + + case PATTERN_MODE: + if (fields->mode == MODE_INVALID) + return -ENXIO; + + r = strextendf(&j, "%03o", fields->mode); + if (r < 0) + return r; + + break; + + case PATTERN_SIZE: + if (fields->size == UINT64_MAX) + return -ENXIO; + + r = strextendf(&j, "%" PRIu64, fields->size); + if (r < 0) + return r; + break; + + case PATTERN_TRIES_DONE: + if (fields->tries_done == UINT64_MAX) + return -ENXIO; + + r = strextendf(&j, "%" PRIu64, fields->tries_done); + if (r < 0) + return r; + break; + + case PATTERN_TRIES_LEFT: + if (fields->tries_left == UINT64_MAX) + return -ENXIO; + + r = strextendf(&j, "%" PRIu64, fields->tries_left); + if (r < 0) + return r; + break; + + case PATTERN_NO_AUTO: + if (fields->no_auto < 0) + return -ENXIO; + + if (!strextend(&j, one_zero(fields->no_auto))) + return -ENOMEM; + + break; + + case PATTERN_READ_ONLY: + if (fields->read_only < 0) + return -ENXIO; + + if (!strextend(&j, one_zero(fields->read_only))) + return -ENOMEM; + + break; + + case PATTERN_GROWFS: + if (fields->growfs < 0) + return -ENXIO; + + if (!strextend(&j, one_zero(fields->growfs))) + return -ENOMEM; + + break; + + case PATTERN_SHA256SUM: { + _cleanup_free_ char *h = NULL; + + if (!fields->sha256sum_set) + return -ENXIO; + + h = hexmem(fields->sha256sum, sizeof(fields->sha256sum)); + if (!h) + return -ENOMEM; + + if (!strextend(&j, h)) + return -ENOMEM; + + break; + } + + default: + assert_not_reached(); + } + } + + *ret = TAKE_PTR(j); + return 0; +} diff --git a/src/sysupdate/sysupdate-pattern.h b/src/sysupdate/sysupdate-pattern.h new file mode 100644 index 0000000..e8ea104 --- /dev/null +++ b/src/sysupdate/sysupdate-pattern.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <stdbool.h> + +#include "sysupdate-instance.h" +#include "time-util.h" + +enum { + PATTERN_MATCH_NO, + PATTERN_MATCH_YES, + PATTERN_MATCH_RETRY, +}; + +int pattern_match(const char *pattern, const char *s, InstanceMetadata *ret); +int pattern_match_many(char **patterns, const char *s, InstanceMetadata *ret); +int pattern_valid(const char *pattern); +int pattern_format(const char *pattern, const InstanceMetadata *fields, char **ret); diff --git a/src/sysupdate/sysupdate-resource.c b/src/sysupdate/sysupdate-resource.c new file mode 100644 index 0000000..e4bdd88 --- /dev/null +++ b/src/sysupdate/sysupdate-resource.c @@ -0,0 +1,707 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <fcntl.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "blockdev-util.h" +#include "chase.h" +#include "device-util.h" +#include "devnum-util.h" +#include "dirent-util.h" +#include "env-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "find-esp.h" +#include "glyph-util.h" +#include "gpt.h" +#include "hexdecoct.h" +#include "import-util.h" +#include "macro.h" +#include "process-util.h" +#include "sort-util.h" +#include "string-table.h" +#include "sysupdate-cache.h" +#include "sysupdate-instance.h" +#include "sysupdate-pattern.h" +#include "sysupdate-resource.h" +#include "sysupdate.h" +#include "utf8.h" + +void resource_destroy(Resource *rr) { + assert(rr); + + free(rr->path); + strv_free(rr->patterns); + + for (size_t i = 0; i < rr->n_instances; i++) + instance_free(rr->instances[i]); + free(rr->instances); +} + +static int resource_add_instance( + Resource *rr, + const char *path, + const InstanceMetadata *f, + Instance **ret) { + + Instance *i; + int r; + + assert(rr); + assert(path); + assert(f); + assert(f->version); + + if (!GREEDY_REALLOC(rr->instances, rr->n_instances + 1)) + return log_oom(); + + r = instance_new(rr, path, f, &i); + if (r < 0) + return r; + + rr->instances[rr->n_instances++] = i; + + if (ret) + *ret = i; + + return 0; +} + +static int resource_load_from_directory_recursive( + Resource *rr, + DIR* d, + const char* relpath, + mode_t m) { + int r; + + for (;;) { + _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL; + _cleanup_free_ char *joined = NULL, *rel_joined = NULL; + Instance *instance; + struct dirent *de; + struct stat st; + + errno = 0; + de = readdir_no_dot(d); + if (!de) { + if (errno != 0) + return log_error_errno(errno, "Failed to read directory '%s': %m", rr->path); + break; + } + + switch (de->d_type) { + + case DT_UNKNOWN: + break; + + case DT_DIR: + if (!IN_SET(m, S_IFDIR, S_IFREG)) + continue; + + break; + + case DT_REG: + if (m != S_IFREG) + continue; + break; + + default: + continue; + } + + if (fstatat(dirfd(d), de->d_name, &st, AT_NO_AUTOMOUNT) < 0) { + if (errno == ENOENT) /* Gone by now? */ + continue; + + return log_error_errno(errno, "Failed to stat %s/%s: %m", rr->path, de->d_name); + } + + if (!(S_ISDIR(st.st_mode) && S_ISREG(m)) && ((st.st_mode & S_IFMT) != m)) + continue; + + rel_joined = path_join(relpath, de->d_name); + if (!rel_joined) + return log_oom(); + + r = pattern_match_many(rr->patterns, rel_joined, &extracted_fields); + if (r == PATTERN_MATCH_RETRY) { + _cleanup_closedir_ DIR *subdir = NULL; + + subdir = xopendirat(dirfd(d), rel_joined, 0); + if (!subdir) + continue; + + r = resource_load_from_directory_recursive(rr, subdir, rel_joined, m); + if (r < 0) + return r; + if (r == 0) + continue; + } + else if (r < 0) + return log_error_errno(r, "Failed to match pattern: %m"); + else if (r == PATTERN_MATCH_NO) + continue; + + if (de->d_type == DT_DIR && m != S_IFDIR) + continue; + + joined = path_join(rr->path, rel_joined); + if (!joined) + return log_oom(); + + r = resource_add_instance(rr, joined, &extracted_fields, &instance); + if (r < 0) + return r; + + /* Inherit these from the source, if not explicitly overwritten */ + if (instance->metadata.mtime == USEC_INFINITY) + instance->metadata.mtime = timespec_load(&st.st_mtim) ?: USEC_INFINITY; + + if (instance->metadata.mode == MODE_INVALID) + instance->metadata.mode = st.st_mode & 0775; /* mask out world-writability and suid and stuff, for safety */ + } + + return 0; +} + +static int resource_load_from_directory( + Resource *rr, + mode_t m) { + _cleanup_closedir_ DIR *d = NULL; + + assert(rr); + assert(IN_SET(rr->type, RESOURCE_TAR, RESOURCE_REGULAR_FILE, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME)); + assert(IN_SET(m, S_IFREG, S_IFDIR)); + + d = opendir(rr->path); + if (!d) { + if (errno == ENOENT) { + log_debug_errno(errno, "Directory %s does not exist, not loading any resources: %m", rr->path); + return 0; + } + + return log_error_errno(errno, "Failed to open directory '%s': %m", rr->path); + } + + return resource_load_from_directory_recursive(rr, d, NULL, m); +} + +static int resource_load_from_blockdev(Resource *rr) { + _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL; + _cleanup_(fdisk_unref_tablep) struct fdisk_table *t = NULL; + size_t n_partitions; + int r; + + assert(rr); + + r = fdisk_new_context_at(AT_FDCWD, rr->path, /* read_only= */ true, /* sector_size= */ UINT32_MAX, &c); + if (r < 0) + return log_error_errno(r, "Failed to create fdisk context from '%s': %m", rr->path); + + if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT)) + return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "Disk %s has no GPT disk label, not suitable.", rr->path); + + r = fdisk_get_partitions(c, &t); + if (r < 0) + return log_error_errno(r, "Failed to acquire partition table: %m"); + + n_partitions = fdisk_table_get_nents(t); + for (size_t i = 0; i < n_partitions; i++) { + _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL; + _cleanup_(partition_info_destroy) PartitionInfo pinfo = PARTITION_INFO_NULL; + Instance *instance; + + r = read_partition_info(c, t, i, &pinfo); + if (r < 0) + return r; + if (r == 0) /* not assigned */ + continue; + + /* Check if partition type matches */ + if (rr->partition_type_set && !sd_id128_equal(pinfo.type, rr->partition_type.uuid)) + continue; + + /* A label of "_empty" means "not used so far" for us */ + if (streq_ptr(pinfo.label, "_empty")) { + rr->n_empty++; + continue; + } + + r = pattern_match_many(rr->patterns, pinfo.label, &extracted_fields); + if (r < 0) + return log_error_errno(r, "Failed to match pattern: %m"); + if (IN_SET(r, PATTERN_MATCH_NO, PATTERN_MATCH_RETRY)) + continue; + + r = resource_add_instance(rr, pinfo.device, &extracted_fields, &instance); + if (r < 0) + return r; + + instance->partition_info = pinfo; + pinfo = (PartitionInfo) PARTITION_INFO_NULL; + + /* Inherit data from source if not configured explicitly */ + if (!instance->metadata.partition_uuid_set) { + instance->metadata.partition_uuid = instance->partition_info.uuid; + instance->metadata.partition_uuid_set = true; + } + + if (!instance->metadata.partition_flags_set) { + instance->metadata.partition_flags = instance->partition_info.flags; + instance->metadata.partition_flags_set = true; + } + + if (instance->metadata.read_only < 0) + instance->metadata.read_only = instance->partition_info.read_only; + } + + return 0; +} + +static int download_manifest( + const char *url, + bool verify_signature, + char **ret_buffer, + size_t *ret_size) { + + _cleanup_free_ char *buffer = NULL, *suffixed_url = NULL; + _cleanup_close_pair_ int pfd[2] = EBADF_PAIR; + _cleanup_fclose_ FILE *manifest = NULL; + size_t size = 0; + pid_t pid; + int r; + + assert(url); + assert(ret_buffer); + assert(ret_size); + + /* Download a SHA256SUMS file as manifest */ + + r = import_url_append_component(url, "SHA256SUMS", &suffixed_url); + if (r < 0) + return log_error_errno(r, "Failed to append SHA256SUMS to URL: %m"); + + if (pipe2(pfd, O_CLOEXEC) < 0) + return log_error_errno(errno, "Failed to allocate pipe: %m"); + + log_info("%s Acquiring manifest file %s%s", special_glyph(SPECIAL_GLYPH_DOWNLOAD), + suffixed_url, special_glyph(SPECIAL_GLYPH_ELLIPSIS)); + + r = safe_fork_full("(sd-pull)", + (int[]) { -EBADF, pfd[1], STDERR_FILENO }, + NULL, 0, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_REARRANGE_STDIO|FORK_LOG, + &pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + + const char *cmdline[] = { + "systemd-pull", + "raw", + "--direct", /* just download the specified URL, don't download anything else */ + "--verify", verify_signature ? "signature" : "no", /* verify the manifest file */ + suffixed_url, + "-", /* write to stdout */ + NULL + }; + + execv(pull_binary_path(), (char *const*) cmdline); + log_error_errno(errno, "Failed to execute %s tool: %m", pull_binary_path()); + _exit(EXIT_FAILURE); + }; + + pfd[1] = safe_close(pfd[1]); + + /* We'll first load the entire manifest into memory before parsing it. That's because the + * systemd-pull tool can validate the download only after its completion, but still pass the data to + * us as it runs. We thus need to check the return value of the process *before* parsing, to be + * reasonably safe. */ + + manifest = fdopen(pfd[0], "r"); + if (!manifest) + return log_error_errno(errno, "Failed allocate FILE object for manifest file: %m"); + + TAKE_FD(pfd[0]); + + r = read_full_stream(manifest, &buffer, &size); + if (r < 0) + return log_error_errno(r, "Failed to read manifest file from child: %m"); + + manifest = safe_fclose(manifest); + + r = wait_for_terminate_and_check("(sd-pull)", pid, WAIT_LOG); + if (r < 0) + return r; + if (r != 0) + return -EPROTO; + + *ret_buffer = TAKE_PTR(buffer); + *ret_size = size; + + return 0; +} + +static int resource_load_from_web( + Resource *rr, + bool verify, + Hashmap **web_cache) { + + size_t manifest_size = 0, left = 0; + _cleanup_free_ char *buf = NULL; + const char *manifest, *p; + size_t line_nr = 1; + WebCacheItem *ci; + int r; + + assert(rr); + + ci = web_cache ? web_cache_get_item(*web_cache, rr->path, verify) : NULL; + if (ci) { + log_debug("Manifest web cache hit for %s.", rr->path); + + manifest = (char*) ci->data; + manifest_size = ci->size; + } else { + log_debug("Manifest web cache miss for %s.", rr->path); + + r = download_manifest(rr->path, verify, &buf, &manifest_size); + if (r < 0) + return r; + + manifest = buf; + } + + if (memchr(manifest, 0, manifest_size)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Manifest file has embedded NUL byte, refusing."); + if (!utf8_is_valid_n(manifest, manifest_size)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Manifest file is not valid UTF-8, refusing."); + + p = manifest; + left = manifest_size; + + while (left > 0) { + _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL; + _cleanup_free_ char *fn = NULL; + _cleanup_free_ void *h = NULL; + Instance *instance; + const char *e; + size_t hlen; + + /* 64 character hash + separator + filename + newline */ + if (left < 67) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Corrupt manifest at line %zu, refusing.", line_nr); + + if (p[0] == '\\') + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "File names with escapes not supported in manifest at line %zu, refusing.", line_nr); + + r = unhexmem(p, 64, &h, &hlen); + if (r < 0) + return log_error_errno(r, "Failed to parse digest at manifest line %zu, refusing.", line_nr); + + p += 64, left -= 64; + + if (*p != ' ') + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing space separator at manifest line %zu, refusing.", line_nr); + p++, left--; + + if (!IN_SET(*p, '*', ' ')) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing binary/text input marker at manifest line %zu, refusing.", line_nr); + p++, left--; + + e = memchr(p, '\n', left); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Truncated manifest file at line %zu, refusing.", line_nr); + if (e == p) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Empty filename specified at manifest line %zu, refusing.", line_nr); + + fn = strndup(p, e - p); + if (!fn) + return log_oom(); + + if (!filename_is_valid(fn)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid filename specified at manifest line %zu, refusing.", line_nr); + if (string_has_cc(fn, NULL)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Filename contains control characters at manifest line %zu, refusing.", line_nr); + + r = pattern_match_many(rr->patterns, fn, &extracted_fields); + if (r < 0) + return log_error_errno(r, "Failed to match pattern: %m"); + if (r == PATTERN_MATCH_YES) { + _cleanup_free_ char *path = NULL; + + r = import_url_append_component(rr->path, fn, &path); + if (r < 0) + return log_error_errno(r, "Failed to build instance URL: %m"); + + r = resource_add_instance(rr, path, &extracted_fields, &instance); + if (r < 0) + return r; + + assert(hlen == sizeof(instance->metadata.sha256sum)); + + if (instance->metadata.sha256sum_set) { + if (memcmp(instance->metadata.sha256sum, h, hlen) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "SHA256 sum parsed from filename and manifest don't match at line %zu, refusing.", line_nr); + } else { + memcpy(instance->metadata.sha256sum, h, hlen); + instance->metadata.sha256sum_set = true; + } + } + + left -= (e - p) + 1; + p = e + 1; + + line_nr++; + } + + if (!ci && web_cache) { + r = web_cache_add_item(web_cache, rr->path, verify, manifest, manifest_size); + if (r < 0) + log_debug_errno(r, "Failed to add manifest '%s' to cache, ignoring: %m", rr->path); + else + log_debug("Added manifest '%s' to cache.", rr->path); + } + + return 0; +} + +static int instance_cmp(Instance *const*a, Instance *const*b) { + int r; + + assert(a); + assert(b); + assert(*a); + assert(*b); + assert((*a)->metadata.version); + assert((*b)->metadata.version); + + /* Newest version at the beginning */ + r = strverscmp_improved((*a)->metadata.version, (*b)->metadata.version); + if (r != 0) + return -r; + + /* Instances don't have to be uniquely named (uniqueness on partition tables is not enforced at all, + * and since we allow multiple matching patterns not even in directories they are unique). Hence + * let's order by path as secondary ordering key. */ + return path_compare((*a)->path, (*b)->path); +} + +int resource_load_instances(Resource *rr, bool verify, Hashmap **web_cache) { + int r; + + assert(rr); + + switch (rr->type) { + + case RESOURCE_TAR: + case RESOURCE_REGULAR_FILE: + r = resource_load_from_directory(rr, S_IFREG); + break; + + case RESOURCE_DIRECTORY: + case RESOURCE_SUBVOLUME: + r = resource_load_from_directory(rr, S_IFDIR); + break; + + case RESOURCE_PARTITION: + r = resource_load_from_blockdev(rr); + break; + + case RESOURCE_URL_FILE: + case RESOURCE_URL_TAR: + r = resource_load_from_web(rr, verify, web_cache); + break; + + default: + assert_not_reached(); + } + if (r < 0) + return r; + + typesafe_qsort(rr->instances, rr->n_instances, instance_cmp); + return 0; +} + +Instance* resource_find_instance(Resource *rr, const char *version) { + Instance key = { + .metadata.version = (char*) version, + }, *k = &key; + + Instance **found; + found = typesafe_bsearch(&k, rr->instances, rr->n_instances, instance_cmp); + if (!found) + return NULL; + + return *found; +} + +int resource_resolve_path( + Resource *rr, + const char *root, + const char *node) { + + _cleanup_free_ char *p = NULL; + dev_t d; + int r; + + assert(rr); + + if (IN_SET(rr->path_relative_to, PATH_RELATIVE_TO_ESP, PATH_RELATIVE_TO_XBOOTLDR, PATH_RELATIVE_TO_BOOT) && + !IN_SET(rr->type, RESOURCE_REGULAR_FILE, RESOURCE_DIRECTORY)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Paths relative to %s are only allowed for regular-file or directory resources.", + path_relative_to_to_string(rr->path_relative_to)); + + if (rr->path_auto) { + struct stat orig_root_stats; + + /* NB: If the root mount has been replaced by some form of volatile file system (overlayfs), + * the original root block device node is symlinked in /run/systemd/volatile-root. Let's + * follow that link here. If that doesn't exist, we check the backing device of "/usr". We + * don't actually check the backing device of the root fs "/", in order to support + * environments where the root fs is a tmpfs, and the OS itself placed exclusively in + * /usr/. */ + + if (rr->type != RESOURCE_PARTITION) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Automatic root path discovery only supported for partition resources."); + + if (node) { /* If --image= is specified, directly use the loopback device */ + r = free_and_strdup_warn(&rr->path, node); + if (r < 0) + return r; + + return 0; + } + + if (root) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), + "Block device is not allowed when using --root= mode."); + + r = stat("/run/systemd/volatile-root", &orig_root_stats); + if (r < 0) { + if (errno == ENOENT) /* volatile-root not found */ + r = get_block_device_harder("/usr/", &d); + else + return log_error_errno(r, "Failed to stat /run/systemd/volatile-root: %m"); + } else if (!S_ISBLK(orig_root_stats.st_mode)) /* symlink was present but not block device */ + return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "/run/systemd/volatile-root is not linked to a block device."); + else /* symlink was present and a block device */ + d = orig_root_stats.st_rdev; + + } else if (rr->type == RESOURCE_PARTITION) { + _cleanup_close_ int fd = -EBADF, real_fd = -EBADF; + _cleanup_free_ char *resolved = NULL; + struct stat st; + + r = chase(rr->path, root, CHASE_PREFIX_ROOT, &resolved, &fd); + if (r < 0) + return log_error_errno(r, "Failed to resolve '%s': %m", rr->path); + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat '%s': %m", resolved); + + if (S_ISBLK(st.st_mode) && root) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "When using --root= or --image= access to device nodes is prohibited."); + + if (S_ISREG(st.st_mode) || S_ISBLK(st.st_mode)) { + /* Not a directory, hence no need to find backing block device for the path */ + free_and_replace(rr->path, resolved); + return 0; + } + + if (!S_ISDIR(st.st_mode)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "Target path '%s' does not refer to regular file, directory or block device, refusing.", rr->path); + + if (node) { /* If --image= is specified all file systems are backed by the same loopback device, hence shortcut things. */ + r = free_and_strdup_warn(&rr->path, node); + if (r < 0) + return r; + + return 0; + } + + real_fd = fd_reopen(fd, O_RDONLY|O_CLOEXEC|O_DIRECTORY); + if (real_fd < 0) + return log_error_errno(real_fd, "Failed to convert O_PATH file descriptor for %s to regular file descriptor: %m", rr->path); + + r = get_block_device_harder_fd(fd, &d); + + } else if (RESOURCE_IS_FILESYSTEM(rr->type)) { + _cleanup_free_ char *resolved = NULL, *relative_to = NULL; + ChaseFlags chase_flags = CHASE_PREFIX_ROOT; + + if (rr->path_relative_to == PATH_RELATIVE_TO_ROOT) { + relative_to = strdup(empty_to_root(root)); + if (!relative_to) + return log_oom(); + } else { /* boot, esp, or xbootldr */ + r = 0; + if (IN_SET(rr->path_relative_to, PATH_RELATIVE_TO_BOOT, PATH_RELATIVE_TO_XBOOTLDR)) + r = find_xbootldr_and_warn(root, NULL, /* unprivileged_mode= */ -1, &relative_to, NULL, NULL); + if (r == -ENOKEY || rr->path_relative_to == PATH_RELATIVE_TO_ESP) + r = find_esp_and_warn(root, NULL, -1, &relative_to, NULL, NULL, NULL, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to resolve $BOOT: %m"); + log_debug("Resolved $BOOT to '%s'", relative_to); + + /* Since this partition is read from EFI, there should be no symlinks */ + chase_flags |= CHASE_PROHIBIT_SYMLINKS; + } + + r = chase(rr->path, relative_to, chase_flags, &resolved, NULL); + if (r < 0) + return log_error_errno(r, "Failed to resolve '%s' (relative to '%s'): %m", rr->path, relative_to); + + free_and_replace(rr->path, resolved); + return 0; + } else + return 0; /* Otherwise assume there's nothing to resolve */ + + if (r < 0) + return log_error_errno(r, "Failed to determine block device of file system: %m"); + + r = block_get_whole_disk(d, &d); + if (r < 0) + return log_error_errno(r, "Failed to find whole disk device for partition backing file system: %m"); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "File system is not placed on a partition block device, cannot determine whole block device backing root file system."); + + r = devname_from_devnum(S_IFBLK, d, &p); + if (r < 0) + return r; + + if (rr->path) + log_info("Automatically discovered block device '%s' from '%s'.", p, rr->path); + else + log_info("Automatically discovered root block device '%s'.", p); + + free_and_replace(rr->path, p); + return 1; +} + +static const char *resource_type_table[_RESOURCE_TYPE_MAX] = { + [RESOURCE_URL_FILE] = "url-file", + [RESOURCE_URL_TAR] = "url-tar", + [RESOURCE_TAR] = "tar", + [RESOURCE_PARTITION] = "partition", + [RESOURCE_REGULAR_FILE] = "regular-file", + [RESOURCE_DIRECTORY] = "directory", + [RESOURCE_SUBVOLUME] = "subvolume", +}; + +DEFINE_STRING_TABLE_LOOKUP(resource_type, ResourceType); + +static const char *path_relative_to_table[_PATH_RELATIVE_TO_MAX] = { + [PATH_RELATIVE_TO_ROOT] = "root", + [PATH_RELATIVE_TO_ESP] = "esp", + [PATH_RELATIVE_TO_XBOOTLDR] = "xbootldr", + [PATH_RELATIVE_TO_BOOT] = "boot", +}; + +DEFINE_STRING_TABLE_LOOKUP(path_relative_to, PathRelativeTo); diff --git a/src/sysupdate/sysupdate-resource.h b/src/sysupdate/sysupdate-resource.h new file mode 100644 index 0000000..51f74cf --- /dev/null +++ b/src/sysupdate/sysupdate-resource.h @@ -0,0 +1,111 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <stdbool.h> +#include <sys/types.h> + +#include "gpt.h" +#include "hashmap.h" +#include "macro.h" + +/* Forward declare this type so that the headers below can use it */ +typedef struct Resource Resource; + +#include "sysupdate-instance.h" + +typedef enum ResourceType { + RESOURCE_URL_FILE, + RESOURCE_URL_TAR, + RESOURCE_TAR, + RESOURCE_PARTITION, + RESOURCE_REGULAR_FILE, + RESOURCE_DIRECTORY, + RESOURCE_SUBVOLUME, + _RESOURCE_TYPE_MAX, + _RESOURCE_TYPE_INVALID = -EINVAL, +} ResourceType; + +static inline bool RESOURCE_IS_SOURCE(ResourceType t) { + return IN_SET(t, + RESOURCE_URL_FILE, + RESOURCE_URL_TAR, + RESOURCE_TAR, + RESOURCE_REGULAR_FILE, + RESOURCE_DIRECTORY, + RESOURCE_SUBVOLUME); +} + +static inline bool RESOURCE_IS_TARGET(ResourceType t) { + return IN_SET(t, + RESOURCE_PARTITION, + RESOURCE_REGULAR_FILE, + RESOURCE_DIRECTORY, + RESOURCE_SUBVOLUME); +} + +/* Returns true for all resources that deal with file system objects, i.e. where we operate on top of the + * file system layer, instead of below. */ +static inline bool RESOURCE_IS_FILESYSTEM(ResourceType t) { + return IN_SET(t, + RESOURCE_TAR, + RESOURCE_REGULAR_FILE, + RESOURCE_DIRECTORY, + RESOURCE_SUBVOLUME); +} + +static inline bool RESOURCE_IS_TAR(ResourceType t) { + return IN_SET(t, + RESOURCE_TAR, + RESOURCE_URL_TAR); +} + +static inline bool RESOURCE_IS_URL(ResourceType t) { + return IN_SET(t, + RESOURCE_URL_TAR, + RESOURCE_URL_FILE); +} + +typedef enum PathRelativeTo { + /* Please make sure to follow the naming of the corresponding PartitionDesignator enum values, + * where this makes sense, like for the following three. */ + PATH_RELATIVE_TO_ROOT, + PATH_RELATIVE_TO_ESP, + PATH_RELATIVE_TO_XBOOTLDR, + PATH_RELATIVE_TO_BOOT, /* Refers to $BOOT from the BLS. No direct counterpart in PartitionDesignator */ + _PATH_RELATIVE_TO_MAX, + _PATH_RELATIVE_TO_INVALID = -EINVAL, +} PathRelativeTo; + +struct Resource { + ResourceType type; + + /* Where to look for instances, and what to match precisely */ + char *path; + bool path_auto; /* automatically find root path (only available if target resource, not source resource) */ + PathRelativeTo path_relative_to; + char **patterns; + GptPartitionType partition_type; + bool partition_type_set; + + /* All instances of this resource we found */ + Instance **instances; + size_t n_instances; + + /* If this is a partition resource (RESOURCE_PARTITION), then how many partition slots are currently unassigned, that we can use */ + size_t n_empty; +}; + +void resource_destroy(Resource *rr); + +int resource_load_instances(Resource *rr, bool verify, Hashmap **web_cache); + +Instance* resource_find_instance(Resource *rr, const char *version); + +int resource_resolve_path(Resource *rr, const char *root, const char *node); + +ResourceType resource_type_from_string(const char *s) _pure_; +const char *resource_type_to_string(ResourceType t) _const_; + +PathRelativeTo path_relative_to_from_string(const char *s) _pure_; +const char *path_relative_to_to_string(PathRelativeTo r) _const_; diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c new file mode 100644 index 0000000..f8f4a15 --- /dev/null +++ b/src/sysupdate/sysupdate-transfer.c @@ -0,0 +1,1252 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-id128.h" + +#include "alloc-util.h" +#include "blockdev-util.h" +#include "chase.h" +#include "conf-parser.h" +#include "dirent-util.h" +#include "fd-util.h" +#include "glyph-util.h" +#include "gpt.h" +#include "hexdecoct.h" +#include "install-file.h" +#include "mkdir.h" +#include "parse-helpers.h" +#include "parse-util.h" +#include "process-util.h" +#include "rm-rf.h" +#include "specifier.h" +#include "stat-util.h" +#include "stdio-util.h" +#include "strv.h" +#include "sync-util.h" +#include "sysupdate-pattern.h" +#include "sysupdate-resource.h" +#include "sysupdate-transfer.h" +#include "sysupdate.h" +#include "tmpfile-util.h" +#include "web-util.h" + +/* Default value for InstancesMax= for fs object targets */ +#define DEFAULT_FILE_INSTANCES_MAX 3 + +Transfer *transfer_free(Transfer *t) { + if (!t) + return NULL; + + t->temporary_path = rm_rf_subvolume_and_free(t->temporary_path); + + free(t->definition_path); + free(t->min_version); + strv_free(t->protected_versions); + free(t->current_symlink); + free(t->final_path); + + partition_info_destroy(&t->partition_info); + + resource_destroy(&t->source); + resource_destroy(&t->target); + + return mfree(t); +} + +Transfer *transfer_new(void) { + Transfer *t; + + t = new(Transfer, 1); + if (!t) + return NULL; + + *t = (Transfer) { + .source.type = _RESOURCE_TYPE_INVALID, + .target.type = _RESOURCE_TYPE_INVALID, + .remove_temporary = true, + .mode = MODE_INVALID, + .tries_left = UINT64_MAX, + .tries_done = UINT64_MAX, + .verify = true, + + /* the three flags, as configured by the user */ + .no_auto = -1, + .read_only = -1, + .growfs = -1, + + /* the read only flag, as ultimately determined */ + .install_read_only = -1, + + .partition_info = PARTITION_INFO_NULL, + }; + + return t; +} + +static const Specifier specifier_table[] = { + COMMON_SYSTEM_SPECIFIERS, + COMMON_TMP_SPECIFIERS, + {} +}; + +static int config_parse_protect_version( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_free_ char *resolved = NULL; + char ***protected_versions = ASSERT_PTR(data); + int r; + + assert(rvalue); + + r = specifier_printf(rvalue, NAME_MAX, specifier_table, arg_root, NULL, &resolved); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to expand specifiers in ProtectVersion=, ignoring: %s", rvalue); + return 0; + } + + if (!version_is_valid(resolved)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "ProtectVersion= string is not valid, ignoring: %s", resolved); + return 0; + } + + r = strv_extend(protected_versions, resolved); + if (r < 0) + return log_oom(); + + return 0; +} + +static int config_parse_min_version( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_free_ char *resolved = NULL; + char **version = ASSERT_PTR(data); + int r; + + assert(rvalue); + + r = specifier_printf(rvalue, NAME_MAX, specifier_table, arg_root, NULL, &resolved); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to expand specifiers in MinVersion=, ignoring: %s", rvalue); + return 0; + } + + if (!version_is_valid(rvalue)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "MinVersion= string is not valid, ignoring: %s", resolved); + return 0; + } + + return free_and_replace(*version, resolved); +} + +static int config_parse_current_symlink( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_free_ char *resolved = NULL; + char **current_symlink = ASSERT_PTR(data); + int r; + + assert(rvalue); + + r = specifier_printf(rvalue, NAME_MAX, specifier_table, arg_root, NULL, &resolved); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to expand specifiers in CurrentSymlink=, ignoring: %s", rvalue); + return 0; + } + + r = path_simplify_and_warn(resolved, 0, unit, filename, line, lvalue); + if (r < 0) + return 0; + + return free_and_replace(*current_symlink, resolved); +} + +static int config_parse_instances_max( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + uint64_t *instances_max = data, i; + int r; + + assert(rvalue); + assert(data); + + if (isempty(rvalue)) { + *instances_max = 0; /* Revert to default logic, see transfer_read_definition() */ + return 0; + } + + r = safe_atou64(rvalue, &i); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse InstancesMax= value, ignoring: %s", rvalue); + return 0; + } + + if (i < 2) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "InstancesMax= value must be at least 2, bumping: %s", rvalue); + *instances_max = 2; + } else + *instances_max = i; + + return 0; +} + +static int config_parse_resource_pattern( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + char ***patterns = ASSERT_PTR(data); + int r; + + assert(rvalue); + + if (isempty(rvalue)) { + *patterns = strv_free(*patterns); + return 0; + } + + for (;;) { + _cleanup_free_ char *word = NULL, *resolved = NULL; + + r = extract_first_word(&rvalue, &word, NULL, EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_RELAX); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to extract first pattern from MatchPattern=, ignoring: %s", rvalue); + return 0; + } + if (r == 0) + break; + + r = specifier_printf(word, NAME_MAX, specifier_table, arg_root, NULL, &resolved); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to expand specifiers in MatchPattern=, ignoring: %s", rvalue); + return 0; + } + + if (!pattern_valid(resolved)) + return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL), + "MatchPattern= string is not valid, refusing: %s", resolved); + + r = strv_consume(patterns, TAKE_PTR(resolved)); + if (r < 0) + return log_oom(); + } + + strv_uniq(*patterns); + return 0; +} + +static int config_parse_resource_path( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + _cleanup_free_ char *resolved = NULL; + Resource *rr = ASSERT_PTR(data); + int r; + + assert(rvalue); + + if (streq(rvalue, "auto")) { + rr->path_auto = true; + rr->path = mfree(rr->path); + return 0; + } + + r = specifier_printf(rvalue, PATH_MAX-1, specifier_table, arg_root, NULL, &resolved); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to expand specifiers in Path=, ignoring: %s", rvalue); + return 0; + } + + /* Note that we don't validate the path as being absolute or normalized. We'll do that in + * transfer_read_definition() as we might not know yet whether Path refers to a URL or a file system + * path. */ + + rr->path_auto = false; + return free_and_replace(rr->path, resolved); +} + +static DEFINE_CONFIG_PARSE_ENUM(config_parse_resource_type, resource_type, ResourceType, "Invalid resource type"); + +static DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT(config_parse_resource_path_relto, path_relative_to, PathRelativeTo, + PATH_RELATIVE_TO_ROOT, "Invalid PathRelativeTo= value"); + +static int config_parse_resource_ptype( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Resource *rr = ASSERT_PTR(data); + int r; + + assert(rvalue); + + r = gpt_partition_type_from_string(rvalue, &rr->partition_type); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed parse partition type, ignoring: %s", rvalue); + return 0; + } + + rr->partition_type_set = true; + return 0; +} + +static int config_parse_partition_uuid( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Transfer *t = ASSERT_PTR(data); + int r; + + assert(rvalue); + + r = sd_id128_from_string(rvalue, &t->partition_uuid); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed parse partition UUID, ignoring: %s", rvalue); + return 0; + } + + t->partition_uuid_set = true; + return 0; +} + +static int config_parse_partition_flags( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Transfer *t = ASSERT_PTR(data); + int r; + + assert(rvalue); + + r = safe_atou64(rvalue, &t->partition_flags); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed parse partition flags, ignoring: %s", rvalue); + return 0; + } + + t->partition_flags_set = true; + return 0; +} + +int transfer_read_definition(Transfer *t, const char *path) { + int r; + + assert(t); + assert(path); + + ConfigTableItem table[] = { + { "Transfer", "MinVersion", config_parse_min_version, 0, &t->min_version }, + { "Transfer", "ProtectVersion", config_parse_protect_version, 0, &t->protected_versions }, + { "Transfer", "Verify", config_parse_bool, 0, &t->verify }, + { "Source", "Type", config_parse_resource_type, 0, &t->source.type }, + { "Source", "Path", config_parse_resource_path, 0, &t->source }, + { "Source", "PathRelativeTo", config_parse_resource_path_relto, 0, &t->source.path_relative_to }, + { "Source", "MatchPattern", config_parse_resource_pattern, 0, &t->source.patterns }, + { "Target", "Type", config_parse_resource_type, 0, &t->target.type }, + { "Target", "Path", config_parse_resource_path, 0, &t->target }, + { "Target", "PathRelativeTo", config_parse_resource_path_relto, 0, &t->target.path_relative_to }, + { "Target", "MatchPattern", config_parse_resource_pattern, 0, &t->target.patterns }, + { "Target", "MatchPartitionType", config_parse_resource_ptype, 0, &t->target }, + { "Target", "PartitionUUID", config_parse_partition_uuid, 0, t }, + { "Target", "PartitionFlags", config_parse_partition_flags, 0, t }, + { "Target", "PartitionNoAuto", config_parse_tristate, 0, &t->no_auto }, + { "Target", "PartitionGrowFileSystem", config_parse_tristate, 0, &t->growfs }, + { "Target", "ReadOnly", config_parse_tristate, 0, &t->read_only }, + { "Target", "Mode", config_parse_mode, 0, &t->mode }, + { "Target", "TriesLeft", config_parse_uint64, 0, &t->tries_left }, + { "Target", "TriesDone", config_parse_uint64, 0, &t->tries_done }, + { "Target", "InstancesMax", config_parse_instances_max, 0, &t->instances_max }, + { "Target", "RemoveTemporary", config_parse_bool, 0, &t->remove_temporary }, + { "Target", "CurrentSymlink", config_parse_current_symlink, 0, &t->current_symlink }, + {} + }; + + r = config_parse(NULL, path, NULL, + "Transfer\0" + "Source\0" + "Target\0", + config_item_table_lookup, table, + CONFIG_PARSE_WARN, + t, + NULL); + if (r < 0) + return r; + + if (!RESOURCE_IS_SOURCE(t->source.type)) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Source Type= must be one of url-file, url-tar, tar, regular-file, directory, subvolume."); + + if (t->target.type < 0) { + switch (t->source.type) { + + case RESOURCE_URL_FILE: + case RESOURCE_REGULAR_FILE: + t->target.type = + t->target.path && path_startswith(t->target.path, "/dev/") ? + RESOURCE_PARTITION : RESOURCE_REGULAR_FILE; + break; + + case RESOURCE_URL_TAR: + case RESOURCE_TAR: + case RESOURCE_DIRECTORY: + t->target.type = RESOURCE_DIRECTORY; + break; + + case RESOURCE_SUBVOLUME: + t->target.type = RESOURCE_SUBVOLUME; + break; + + default: + assert_not_reached(); + } + } + + if (!RESOURCE_IS_TARGET(t->target.type)) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Target Type= must be one of partition, regular-file, directory, subvolume."); + + if ((IN_SET(t->source.type, RESOURCE_URL_FILE, RESOURCE_PARTITION, RESOURCE_REGULAR_FILE) && + !IN_SET(t->target.type, RESOURCE_PARTITION, RESOURCE_REGULAR_FILE)) || + (IN_SET(t->source.type, RESOURCE_URL_TAR, RESOURCE_TAR, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME) && + !IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME))) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Target type '%s' is incompatible with source type '%s', refusing.", + resource_type_to_string(t->source.type), resource_type_to_string(t->target.type)); + + if (!t->source.path && !t->source.path_auto) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Source specification lacks Path=."); + + if (t->source.path) { + if (RESOURCE_IS_FILESYSTEM(t->source.type) || t->source.type == RESOURCE_PARTITION) + if (!path_is_absolute(t->source.path) || !path_is_normalized(t->source.path)) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Source path is not a normalized, absolute path: %s", t->source.path); + + /* We unofficially support file:// in addition to http:// and https:// for url + * sources. That's mostly for testing, since it relieves us from having to set up a HTTP + * server, and CURL abstracts this away from us thankfully. */ + if (RESOURCE_IS_URL(t->source.type)) + if (!http_url_is_valid(t->source.path) && !file_url_is_valid(t->source.path)) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Source path is not a valid HTTP or HTTPS URL: %s", t->source.path); + } + + if (strv_isempty(t->source.patterns)) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Source specification lacks MatchPattern=."); + + if (!t->target.path && !t->target.path_auto) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Target specification lacks Path= field."); + + if (t->target.path && + (!path_is_absolute(t->target.path) || !path_is_normalized(t->target.path))) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Target path is not a normalized, absolute path: %s", t->target.path); + + if (strv_isempty(t->target.patterns)) { + strv_free(t->target.patterns); + t->target.patterns = strv_copy(t->source.patterns); + if (!t->target.patterns) + return log_oom(); + } + + if (t->current_symlink && !RESOURCE_IS_FILESYSTEM(t->target.type) && !path_is_absolute(t->current_symlink)) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Current symlink must be absolute path if target is partition: %s", t->current_symlink); + + /* When no instance limit is set, use all available partition slots in case of partitions, or 3 in case of fs objects */ + if (t->instances_max == 0) + t->instances_max = t->target.type == RESOURCE_PARTITION ? UINT64_MAX : DEFAULT_FILE_INSTANCES_MAX; + + return 0; +} + +int transfer_resolve_paths( + Transfer *t, + const char *root, + const char *node) { + + int r; + + /* If Path=auto is used in [Source] or [Target] sections, let's automatically detect the path of the + * block device to use. Moreover, if this path points to a directory but we need a block device, + * automatically determine the backing block device, so that users can reference block devices by + * mount point. */ + + assert(t); + + r = resource_resolve_path(&t->source, root, node); + if (r < 0) + return r; + + r = resource_resolve_path(&t->target, root, node); + if (r < 0) + return r; + + return 0; +} + +static void transfer_remove_temporary(Transfer *t) { + _cleanup_closedir_ DIR *d = NULL; + int r; + + assert(t); + + if (!t->remove_temporary) + return; + + if (!IN_SET(t->target.type, RESOURCE_REGULAR_FILE, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME)) + return; + + /* Removes all temporary files/dirs from previous runs in the target directory, i.e. all those starting with '.#' */ + + d = opendir(t->target.path); + if (!d) { + if (errno == ENOENT) + return; + + log_debug_errno(errno, "Failed to open target directory '%s', ignoring: %m", t->target.path); + return; + } + + for (;;) { + struct dirent *de; + + errno = 0; + de = readdir_no_dot(d); + if (!de) { + if (errno != 0) + log_debug_errno(errno, "Failed to read target directory '%s', ignoring: %m", t->target.path); + break; + } + + if (!startswith(de->d_name, ".#")) + continue; + + r = rm_rf_child(dirfd(d), de->d_name, REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_CHMOD); + if (r == -ENOENT) + continue; + if (r < 0) { + log_warning_errno(r, "Failed to remove temporary resource instance '%s/%s', ignoring: %m", t->target.path, de->d_name); + continue; + } + + log_debug("Removed temporary resource instance '%s/%s'.", t->target.path, de->d_name); + } +} + +int transfer_vacuum( + Transfer *t, + uint64_t space, + const char *extra_protected_version) { + + uint64_t instances_max, limit; + int r, count = 0; + + assert(t); + + transfer_remove_temporary(t); + + /* First, calculate how many instances to keep, based on the instance limit — but keep at least one */ + + instances_max = arg_instances_max != UINT64_MAX ? arg_instances_max : t->instances_max; + assert(instances_max >= 1); + if (instances_max == UINT64_MAX) /* Keep infinite instances? */ + limit = UINT64_MAX; + else if (space > instances_max) + return log_error_errno(SYNTHETIC_ERRNO(ENOSPC), + "Asked to delete more instances than total maximum allowed number of instances, refusing."); + else if (space == instances_max) + return log_error_errno(SYNTHETIC_ERRNO(ENOSPC), + "Asked to delete all possible instances, can't allow that. One instance must always remain."); + else + limit = instances_max - space; + + if (t->target.type == RESOURCE_PARTITION) { + uint64_t rm, remain; + + /* If we are looking at a partition table, we also have to take into account how many + * partition slots of the right type are available */ + + if (t->target.n_empty + t->target.n_instances < 2) + return log_error_errno(SYNTHETIC_ERRNO(ENOSPC), + "Partition table has less than two partition slots of the right type " SD_ID128_UUID_FORMAT_STR " (%s), refusing.", + SD_ID128_FORMAT_VAL(t->target.partition_type.uuid), + gpt_partition_type_uuid_to_string(t->target.partition_type.uuid)); + if (space > t->target.n_empty + t->target.n_instances) + return log_error_errno(SYNTHETIC_ERRNO(ENOSPC), + "Partition table does not have enough partition slots of right type " SD_ID128_UUID_FORMAT_STR " (%s) for operation.", + SD_ID128_FORMAT_VAL(t->target.partition_type.uuid), + gpt_partition_type_uuid_to_string(t->target.partition_type.uuid)); + if (space == t->target.n_empty + t->target.n_instances) + return log_error_errno(SYNTHETIC_ERRNO(ENOSPC), + "Asked to empty all partition table slots of the right type " SD_ID128_UUID_FORMAT_STR " (%s), can't allow that. One instance must always remain.", + SD_ID128_FORMAT_VAL(t->target.partition_type.uuid), + gpt_partition_type_uuid_to_string(t->target.partition_type.uuid)); + + rm = LESS_BY(space, t->target.n_empty); + remain = LESS_BY(t->target.n_instances, rm); + limit = MIN(limit, remain); + } + + while (t->target.n_instances > limit) { + Instance *oldest; + size_t p = t->target.n_instances - 1; + + for (;;) { + oldest = t->target.instances[p]; + assert(oldest); + + /* If this is listed among the protected versions, then let's not remove it */ + if (!strv_contains(t->protected_versions, oldest->metadata.version) && + (!extra_protected_version || !streq(extra_protected_version, oldest->metadata.version))) + break; + + log_debug("Version '%s' is protected, not removing.", oldest->metadata.version); + if (p == 0) { + oldest = NULL; + break; + } + + p--; + } + + if (!oldest) /* Nothing more to remove */ + break; + + assert(oldest->resource); + + log_info("%s Removing old '%s' (%s).", special_glyph(SPECIAL_GLYPH_RECYCLING), oldest->path, resource_type_to_string(oldest->resource->type)); + + switch (t->target.type) { + + case RESOURCE_REGULAR_FILE: + case RESOURCE_DIRECTORY: + case RESOURCE_SUBVOLUME: + r = rm_rf(oldest->path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_MISSING_OK|REMOVE_CHMOD); + if (r < 0 && r != -ENOENT) + return log_error_errno(r, "Failed to make room, deleting '%s' failed: %m", oldest->path); + + (void) rmdir_parents(oldest->path, t->target.path); + + break; + + case RESOURCE_PARTITION: { + PartitionInfo pinfo = oldest->partition_info; + + /* label "_empty" means "no contents" for our purposes */ + pinfo.label = (char*) "_empty"; + + r = patch_partition(t->target.path, &pinfo, PARTITION_LABEL); + if (r < 0) + return r; + + t->target.n_empty++; + break; + } + + default: + assert_not_reached(); + break; + } + + instance_free(oldest); + memmove(t->target.instances + p, t->target.instances + p + 1, (t->target.n_instances - p - 1) * sizeof(Instance*)); + t->target.n_instances--; + + count++; + } + + return count; +} + +static void compile_pattern_fields( + const Transfer *t, + const Instance *i, + InstanceMetadata *ret) { + + assert(t); + assert(i); + assert(ret); + + *ret = (InstanceMetadata) { + .version = i->metadata.version, + + /* We generally prefer explicitly configured values for the transfer over those automatically + * derived from the source instance. Also, if the source is a tar archive, then let's not + * patch mtime/mode and use the one embedded in the tar file */ + .partition_uuid = t->partition_uuid_set ? t->partition_uuid : i->metadata.partition_uuid, + .partition_uuid_set = t->partition_uuid_set || i->metadata.partition_uuid_set, + .partition_flags = t->partition_flags_set ? t->partition_flags : i->metadata.partition_flags, + .partition_flags_set = t->partition_flags_set || i->metadata.partition_flags_set, + .mtime = RESOURCE_IS_TAR(i->resource->type) ? USEC_INFINITY : i->metadata.mtime, + .mode = t->mode != MODE_INVALID ? t->mode : (RESOURCE_IS_TAR(i->resource->type) ? MODE_INVALID : i->metadata.mode), + .size = i->metadata.size, + .tries_done = t->tries_done != UINT64_MAX ? t->tries_done : + i->metadata.tries_done != UINT64_MAX ? i->metadata.tries_done : 0, + .tries_left = t->tries_left != UINT64_MAX ? t->tries_left : + i->metadata.tries_left != UINT64_MAX ? i->metadata.tries_left : 3, + .no_auto = t->no_auto >= 0 ? t->no_auto : i->metadata.no_auto, + .read_only = t->read_only >= 0 ? t->read_only : i->metadata.read_only, + .growfs = t->growfs >= 0 ? t->growfs : i->metadata.growfs, + .sha256sum_set = i->metadata.sha256sum_set, + }; + + memcpy(ret->sha256sum, i->metadata.sha256sum, sizeof(ret->sha256sum)); +} + +static int run_helper( + const char *name, + const char *path, + const char * const cmdline[]) { + + int r; + + assert(name); + assert(path); + assert(cmdline); + + r = safe_fork(name, FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT, NULL); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + + execv(path, (char *const*) cmdline); + log_error_errno(errno, "Failed to execute %s tool: %m", path); + _exit(EXIT_FAILURE); + } + + return 0; +} + +int transfer_acquire_instance(Transfer *t, Instance *i) { + _cleanup_free_ char *formatted_pattern = NULL, *digest = NULL; + char offset[DECIMAL_STR_MAX(uint64_t)+1], max_size[DECIMAL_STR_MAX(uint64_t)+1]; + const char *where = NULL; + InstanceMetadata f; + Instance *existing; + int r; + + assert(t); + assert(i); + assert(i->resource); + assert(t == container_of(i->resource, Transfer, source)); + + /* Does this instance already exist in the target? Then we don't need to acquire anything */ + existing = resource_find_instance(&t->target, i->metadata.version); + if (existing) { + log_info("No need to acquire '%s', already installed.", i->path); + return 0; + } + + assert(!t->final_path); + assert(!t->temporary_path); + assert(!strv_isempty(t->target.patterns)); + + /* Format the target name using the first pattern specified */ + compile_pattern_fields(t, i, &f); + r = pattern_format(t->target.patterns[0], &f, &formatted_pattern); + if (r < 0) + return log_error_errno(r, "Failed to format target pattern: %m"); + + if (RESOURCE_IS_FILESYSTEM(t->target.type)) { + + if (!path_is_valid_full(formatted_pattern, /* accept_dot_dot = */ false)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as file name, refusing: %s", formatted_pattern); + + t->final_path = path_join(t->target.path, formatted_pattern); + if (!t->final_path) + return log_oom(); + + r = mkdir_parents(t->final_path, 0755); + if (r < 0) + return log_error_errno(r, "Cannot create target directory: %m"); + + r = tempfn_random(t->final_path, "sysupdate", &t->temporary_path); + if (r < 0) + return log_error_errno(r, "Failed to generate temporary target path: %m"); + + where = t->final_path; + } + + if (t->target.type == RESOURCE_PARTITION) { + r = gpt_partition_label_valid(formatted_pattern); + if (r < 0) + return log_error_errno(r, "Failed to determine if formatted pattern is suitable as GPT partition label: %s", formatted_pattern); + if (!r) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_pattern); + + r = find_suitable_partition( + t->target.path, + i->metadata.size, + t->target.partition_type_set ? &t->target.partition_type.uuid : NULL, + &t->partition_info); + if (r < 0) + return r; + + xsprintf(offset, "%" PRIu64, t->partition_info.start); + xsprintf(max_size, "%" PRIu64, t->partition_info.size); + + where = t->partition_info.device; + } + + assert(where); + + log_info("%s Acquiring %s %s %s...", special_glyph(SPECIAL_GLYPH_DOWNLOAD), i->path, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), where); + + if (RESOURCE_IS_URL(i->resource->type)) { + /* For URL sources we require the SHA256 sum to be known so that we can validate the + * download. */ + + if (!i->metadata.sha256sum_set) + return log_error_errno(r, "SHA256 checksum not known for download '%s', refusing.", i->path); + + digest = hexmem(i->metadata.sha256sum, sizeof(i->metadata.sha256sum)); + if (!digest) + return log_oom(); + } + + switch (i->resource->type) { /* Source */ + + case RESOURCE_REGULAR_FILE: + + switch (t->target.type) { /* Target */ + + case RESOURCE_REGULAR_FILE: + + /* regular file → regular file (why fork off systemd-import for such a simple file + * copy case? implicit decompression mostly, and thus also sandboxing. Also, the + * importer has some tricks up its sleeve, such as sparse file generation, which we + * want to take benefit of, too.) */ + + r = run_helper("(sd-import-raw)", + import_binary_path(), + (const char* const[]) { + "systemd-import", + "raw", + "--direct", /* just copy/unpack the specified file, don't do anything else */ + arg_sync ? "--sync=yes" : "--sync=no", + i->path, + t->temporary_path, + NULL + }); + break; + + case RESOURCE_PARTITION: + + /* regular file → partition */ + + r = run_helper("(sd-import-raw)", + import_binary_path(), + (const char* const[]) { + "systemd-import", + "raw", + "--direct", /* just copy/unpack the specified file, don't do anything else */ + "--offset", offset, + "--size-max", max_size, + arg_sync ? "--sync=yes" : "--sync=no", + i->path, + t->target.path, + NULL + }); + break; + + default: + assert_not_reached(); + } + + break; + + case RESOURCE_DIRECTORY: + case RESOURCE_SUBVOLUME: + assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME)); + + /* directory/subvolume → directory/subvolume */ + + r = run_helper("(sd-import-fs)", + import_fs_binary_path(), + (const char* const[]) { + "systemd-import-fs", + "run", + "--direct", /* just untar the specified file, don't do anything else */ + arg_sync ? "--sync=yes" : "--sync=no", + t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no", + i->path, + t->temporary_path, + NULL + }); + break; + + case RESOURCE_TAR: + assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME)); + + /* tar → directory/subvolume */ + + r = run_helper("(sd-import-tar)", + import_binary_path(), + (const char* const[]) { + "systemd-import", + "tar", + "--direct", /* just untar the specified file, don't do anything else */ + arg_sync ? "--sync=yes" : "--sync=no", + t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no", + i->path, + t->temporary_path, + NULL + }); + break; + + case RESOURCE_URL_FILE: + + switch (t->target.type) { + + case RESOURCE_REGULAR_FILE: + + /* url file → regular file */ + + r = run_helper("(sd-pull-raw)", + pull_binary_path(), + (const char* const[]) { + "systemd-pull", + "raw", + "--direct", /* just download the specified URL, don't download anything else */ + "--verify", digest, /* validate by explicit SHA256 sum */ + arg_sync ? "--sync=yes" : "--sync=no", + i->path, + t->temporary_path, + NULL + }); + break; + + case RESOURCE_PARTITION: + + /* url file → partition */ + + r = run_helper("(sd-pull-raw)", + pull_binary_path(), + (const char* const[]) { + "systemd-pull", + "raw", + "--direct", /* just download the specified URL, don't download anything else */ + "--verify", digest, /* validate by explicit SHA256 sum */ + "--offset", offset, + "--size-max", max_size, + arg_sync ? "--sync=yes" : "--sync=no", + i->path, + t->target.path, + NULL + }); + break; + + default: + assert_not_reached(); + } + + break; + + case RESOURCE_URL_TAR: + assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME)); + + r = run_helper("(sd-pull-tar)", + pull_binary_path(), + (const char*const[]) { + "systemd-pull", + "tar", + "--direct", /* just download the specified URL, don't download anything else */ + "--verify", digest, /* validate by explicit SHA256 sum */ + t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no", + arg_sync ? "--sync=yes" : "--sync=no", + i->path, + t->temporary_path, + NULL + }); + break; + + default: + assert_not_reached(); + } + if (r < 0) + return r; + + if (RESOURCE_IS_FILESYSTEM(t->target.type)) { + bool need_sync = false; + assert(t->temporary_path); + + /* Apply file attributes if set */ + if (f.mtime != USEC_INFINITY) { + struct timespec ts; + + timespec_store(&ts, f.mtime); + + if (utimensat(AT_FDCWD, t->temporary_path, (struct timespec[2]) { ts, ts }, AT_SYMLINK_NOFOLLOW) < 0) + return log_error_errno(errno, "Failed to adjust mtime of '%s': %m", t->temporary_path); + + need_sync = true; + } + + if (f.mode != MODE_INVALID) { + /* Try with AT_SYMLINK_NOFOLLOW first, because it's the safe thing to do. Older + * kernels don't support that however, in that case we fall back to chmod(). Not as + * safe, but shouldn't be a problem, given that we don't create symlinks here. */ + if (fchmodat(AT_FDCWD, t->temporary_path, f.mode, AT_SYMLINK_NOFOLLOW) < 0 && + (!ERRNO_IS_NOT_SUPPORTED(errno) || chmod(t->temporary_path, f.mode) < 0)) + return log_error_errno(errno, "Failed to adjust mode of '%s': %m", t->temporary_path); + + need_sync = true; + } + + /* Synchronize */ + if (arg_sync && need_sync) { + if (t->target.type == RESOURCE_REGULAR_FILE) + r = fsync_path_and_parent_at(AT_FDCWD, t->temporary_path); + else { + assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME)); + r = syncfs_path(AT_FDCWD, t->temporary_path); + } + if (r < 0) + return log_error_errno(r, "Failed to synchronize file system backing '%s': %m", t->temporary_path); + } + + t->install_read_only = f.read_only; + } + + if (t->target.type == RESOURCE_PARTITION) { + free_and_replace(t->partition_info.label, formatted_pattern); + t->partition_change = PARTITION_LABEL; + + if (f.partition_uuid_set) { + t->partition_info.uuid = f.partition_uuid; + t->partition_change |= PARTITION_UUID; + } + + if (f.partition_flags_set) { + t->partition_info.flags = f.partition_flags; + t->partition_change |= PARTITION_FLAGS; + } + + if (f.no_auto >= 0) { + t->partition_info.no_auto = f.no_auto; + t->partition_change |= PARTITION_NO_AUTO; + } + + if (f.read_only >= 0) { + t->partition_info.read_only = f.read_only; + t->partition_change |= PARTITION_READ_ONLY; + } + + if (f.growfs >= 0) { + t->partition_info.growfs = f.growfs; + t->partition_change |= PARTITION_GROWFS; + } + } + + /* For regular file cases the only step left is to install the file in place, which install_file() + * will do via rename(). For partition cases the only step left is to update the partition table, + * which is done at the same place. */ + + log_info("Successfully acquired '%s'.", i->path); + return 0; +} + +int transfer_install_instance( + Transfer *t, + Instance *i, + const char *root) { + + int r; + + assert(t); + assert(i); + assert(i->resource); + assert(t == container_of(i->resource, Transfer, source)); + + if (t->temporary_path) { + assert(RESOURCE_IS_FILESYSTEM(t->target.type)); + assert(t->final_path); + + r = install_file(AT_FDCWD, t->temporary_path, + AT_FDCWD, t->final_path, + INSTALL_REPLACE| + (t->install_read_only > 0 ? INSTALL_READ_ONLY : 0)| + (t->target.type == RESOURCE_REGULAR_FILE ? INSTALL_FSYNC_FULL : INSTALL_SYNCFS)); + if (r < 0) + return log_error_errno(r, "Failed to move '%s' into place: %m", t->final_path); + + log_info("Successfully installed '%s' (%s) as '%s' (%s).", + i->path, + resource_type_to_string(i->resource->type), + t->final_path, + resource_type_to_string(t->target.type)); + + t->temporary_path = mfree(t->temporary_path); + } + + if (t->partition_change != 0) { + assert(t->target.type == RESOURCE_PARTITION); + + r = patch_partition( + t->target.path, + &t->partition_info, + t->partition_change); + if (r < 0) + return r; + + log_info("Successfully installed '%s' (%s) as '%s' (%s).", + i->path, + resource_type_to_string(i->resource->type), + t->partition_info.device, + resource_type_to_string(t->target.type)); + } + + if (t->current_symlink) { + _cleanup_free_ char *buf = NULL, *parent = NULL, *relative = NULL, *resolved = NULL; + const char *link_path, *link_target; + bool resolve_link_path = false; + + if (RESOURCE_IS_FILESYSTEM(t->target.type)) { + + assert(t->target.path); + + if (path_is_absolute(t->current_symlink)) { + link_path = t->current_symlink; + resolve_link_path = true; + } else { + buf = path_make_absolute(t->current_symlink, t->target.path); + if (!buf) + return log_oom(); + + link_path = buf; + } + + link_target = t->final_path; + + } else if (t->target.type == RESOURCE_PARTITION) { + + assert(path_is_absolute(t->current_symlink)); + + link_path = t->current_symlink; + link_target = t->partition_info.device; + + resolve_link_path = true; + } else + assert_not_reached(); + + if (resolve_link_path && root) { + r = chase(link_path, root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, NULL); + if (r < 0) + return log_error_errno(r, "Failed to resolve current symlink path '%s': %m", link_path); + + link_path = resolved; + } + + if (link_target) { + r = path_extract_directory(link_path, &parent); + if (r < 0) + return log_error_errno(r, "Failed to extract directory of target path '%s': %m", link_path); + + r = path_make_relative(parent, link_target, &relative); + if (r < 0) + return log_error_errno(r, "Failed to make symlink path '%s' relative to '%s': %m", link_target, parent); + + r = symlink_atomic(relative, link_path); + if (r < 0) + return log_error_errno(r, "Failed to update current symlink '%s' %s '%s': %m", + link_path, + special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), + relative); + + log_info("Updated symlink '%s' %s '%s'.", + link_path, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), relative); + } + } + + return 0; +} diff --git a/src/sysupdate/sysupdate-transfer.h b/src/sysupdate/sysupdate-transfer.h new file mode 100644 index 0000000..b0c2a6e --- /dev/null +++ b/src/sysupdate/sysupdate-transfer.h @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <stdbool.h> +#include <sys/types.h> + +#include "sd-id128.h" + +/* Forward declare this type so that the headers below can use it */ +typedef struct Transfer Transfer; + +#include "sysupdate-partition.h" +#include "sysupdate-resource.h" + +struct Transfer { + char *definition_path; + char *min_version; + char **protected_versions; + char *current_symlink; + bool verify; + + Resource source, target; + + uint64_t instances_max; + bool remove_temporary; + + /* When creating a new partition/file, optionally override these attributes explicitly */ + sd_id128_t partition_uuid; + bool partition_uuid_set; + uint64_t partition_flags; + bool partition_flags_set; + mode_t mode; + uint64_t tries_left, tries_done; + int no_auto; + int read_only; + int growfs; + + /* If we create a new file/dir/subvol in the fs, the temporary and final path we create it under, as well as the read-only flag for it */ + char *temporary_path; + char *final_path; + int install_read_only; + + /* If we write to a partition in a partition table, the metrics of it */ + PartitionInfo partition_info; + PartitionChange partition_change; +}; + +Transfer *transfer_new(void); + +Transfer *transfer_free(Transfer *t); +DEFINE_TRIVIAL_CLEANUP_FUNC(Transfer*, transfer_free); + +int transfer_read_definition(Transfer *t, const char *path); + +int transfer_resolve_paths(Transfer *t, const char *root, const char *node); + +int transfer_vacuum(Transfer *t, uint64_t space, const char *extra_protected_version); + +int transfer_acquire_instance(Transfer *t, Instance *i); + +int transfer_install_instance(Transfer *t, Instance *i, const char *root); diff --git a/src/sysupdate/sysupdate-update-set.c b/src/sysupdate/sysupdate-update-set.c new file mode 100644 index 0000000..6d6051d --- /dev/null +++ b/src/sysupdate/sysupdate-update-set.c @@ -0,0 +1,63 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "glyph-util.h" +#include "string-util.h" +#include "sysupdate-update-set.h" +#include "terminal-util.h" + +UpdateSet *update_set_free(UpdateSet *us) { + if (!us) + return NULL; + + free(us->version); + free(us->instances); /* The objects referenced by this array are freed via resource_free(), not us */ + + return mfree(us); +} + +int update_set_cmp(UpdateSet *const*a, UpdateSet *const*b) { + assert(a); + assert(b); + assert(*a); + assert(*b); + assert((*a)->version); + assert((*b)->version); + + /* Newest version at the beginning */ + return -strverscmp_improved((*a)->version, (*b)->version); +} + +const char *update_set_flags_to_color(UpdateSetFlags flags) { + + if (flags == 0 || (flags & UPDATE_OBSOLETE)) + return (flags & UPDATE_NEWEST) ? ansi_highlight_grey() : ansi_grey(); + + if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_NEWEST)) + return ansi_highlight(); + + if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_PROTECTED)) + return ansi_highlight_magenta(); + + if ((flags & (UPDATE_AVAILABLE|UPDATE_INSTALLED|UPDATE_NEWEST|UPDATE_OBSOLETE)) == (UPDATE_AVAILABLE|UPDATE_NEWEST)) + return ansi_highlight_green(); + + return NULL; +} + +const char *update_set_flags_to_glyph(UpdateSetFlags flags) { + + if (flags == 0 || (flags & UPDATE_OBSOLETE)) + return special_glyph(SPECIAL_GLYPH_MULTIPLICATION_SIGN); + + if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_NEWEST)) + return special_glyph(SPECIAL_GLYPH_BLACK_CIRCLE); + + if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_PROTECTED)) + return special_glyph(SPECIAL_GLYPH_WHITE_CIRCLE); + + if ((flags & (UPDATE_AVAILABLE|UPDATE_INSTALLED|UPDATE_NEWEST|UPDATE_OBSOLETE)) == (UPDATE_AVAILABLE|UPDATE_NEWEST)) + return special_glyph(SPECIAL_GLYPH_CIRCLE_ARROW); + + return " "; +} diff --git a/src/sysupdate/sysupdate-update-set.h b/src/sysupdate/sysupdate-update-set.h new file mode 100644 index 0000000..5dd94bc --- /dev/null +++ b/src/sysupdate/sysupdate-update-set.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <stdbool.h> +#include <sys/types.h> + +typedef struct UpdateSet UpdateSet; + +#include "sysupdate-instance.h" + +typedef enum UpdateSetFlags { + UPDATE_NEWEST = 1 << 0, + UPDATE_AVAILABLE = 1 << 1, + UPDATE_INSTALLED = 1 << 2, + UPDATE_OBSOLETE = 1 << 3, + UPDATE_PROTECTED = 1 << 4, +} UpdateSetFlags; + +struct UpdateSet { + UpdateSetFlags flags; + char *version; + Instance **instances; + size_t n_instances; +}; + +UpdateSet *update_set_free(UpdateSet *us); + +int update_set_cmp(UpdateSet *const*a, UpdateSet *const*b); + +const char *update_set_flags_to_color(UpdateSetFlags flags); +const char *update_set_flags_to_glyph(UpdateSetFlags flags); diff --git a/src/sysupdate/sysupdate-util.c b/src/sysupdate/sysupdate-util.c new file mode 100644 index 0000000..eacc592 --- /dev/null +++ b/src/sysupdate/sysupdate-util.c @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "path-util.h" +#include "sysupdate-util.h" diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c new file mode 100644 index 0000000..023eaac --- /dev/null +++ b/src/sysupdate/sysupdate.c @@ -0,0 +1,1416 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <getopt.h> +#include <unistd.h> + +#include "build.h" +#include "bus-error.h" +#include "bus-locator.h" +#include "chase.h" +#include "conf-files.h" +#include "constants.h" +#include "dirent-util.h" +#include "dissect-image.h" +#include "fd-util.h" +#include "format-table.h" +#include "glyph-util.h" +#include "hexdecoct.h" +#include "login-util.h" +#include "main-func.h" +#include "mount-util.h" +#include "os-util.h" +#include "pager.h" +#include "parse-argument.h" +#include "parse-util.h" +#include "path-util.h" +#include "pretty-print.h" +#include "set.h" +#include "sort-util.h" +#include "string-util.h" +#include "strv.h" +#include "sysupdate-transfer.h" +#include "sysupdate-update-set.h" +#include "sysupdate.h" +#include "terminal-util.h" +#include "utf8.h" +#include "verbs.h" + +static char *arg_definitions = NULL; +bool arg_sync = true; +uint64_t arg_instances_max = UINT64_MAX; +static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; +static PagerFlags arg_pager_flags = 0; +static bool arg_legend = true; +char *arg_root = NULL; +static char *arg_image = NULL; +static bool arg_reboot = false; +static char *arg_component = NULL; +static int arg_verify = -1; +static ImagePolicy *arg_image_policy = NULL; + +STATIC_DESTRUCTOR_REGISTER(arg_definitions, freep); +STATIC_DESTRUCTOR_REGISTER(arg_root, freep); +STATIC_DESTRUCTOR_REGISTER(arg_image, freep); +STATIC_DESTRUCTOR_REGISTER(arg_component, freep); +STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); + +typedef struct Context { + Transfer **transfers; + size_t n_transfers; + + UpdateSet **update_sets; + size_t n_update_sets; + + UpdateSet *newest_installed, *candidate; + + Hashmap *web_cache; /* Cache for downloaded resources, keyed by URL */ +} Context; + +static Context *context_free(Context *c) { + if (!c) + return NULL; + + for (size_t i = 0; i < c->n_transfers; i++) + transfer_free(c->transfers[i]); + free(c->transfers); + + for (size_t i = 0; i < c->n_update_sets; i++) + update_set_free(c->update_sets[i]); + free(c->update_sets); + + hashmap_free(c->web_cache); + + return mfree(c); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(Context*, context_free); + +static Context *context_new(void) { + /* For now, no fields to initialize non-zero */ + return new0(Context, 1); +} + +static int context_read_definitions( + Context *c, + const char *directory, + const char *component, + const char *root, + const char *node) { + + _cleanup_strv_free_ char **files = NULL; + int r; + + assert(c); + + if (directory) + r = conf_files_list_strv(&files, ".conf", NULL, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) STRV_MAKE(directory)); + else if (component) { + _cleanup_strv_free_ char **n = NULL; + char **l = CONF_PATHS_STRV(""); + size_t k = 0; + + n = new0(char*, strv_length(l) + 1); + if (!n) + return log_oom(); + + STRV_FOREACH(i, l) { + char *j; + + j = strjoin(*i, "sysupdate.", component, ".d"); + if (!j) + return log_oom(); + + n[k++] = j; + } + + r = conf_files_list_strv(&files, ".conf", root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) n); + } else + r = conf_files_list_strv(&files, ".conf", root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) CONF_PATHS_STRV("sysupdate.d")); + if (r < 0) + return log_error_errno(r, "Failed to enumerate *.conf files: %m"); + + STRV_FOREACH(f, files) { + _cleanup_(transfer_freep) Transfer *t = NULL; + + if (!GREEDY_REALLOC(c->transfers, c->n_transfers + 1)) + return log_oom(); + + t = transfer_new(); + if (!t) + return log_oom(); + + t->definition_path = strdup(*f); + if (!t->definition_path) + return log_oom(); + + r = transfer_read_definition(t, *f); + if (r < 0) + return r; + + c->transfers[c->n_transfers++] = TAKE_PTR(t); + } + + if (c->n_transfers == 0) { + if (arg_component) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), + "No transfer definitions for component '%s' found.", arg_component); + + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), + "No transfer definitions found."); + } + + for (size_t i = 0; i < c->n_transfers; i++) { + r = transfer_resolve_paths(c->transfers[i], root, node); + if (r < 0) + return r; + } + + return 0; +} + +static int context_load_installed_instances(Context *c) { + int r; + + assert(c); + + log_info("Discovering installed instances%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS)); + + for (size_t i = 0; i < c->n_transfers; i++) { + r = resource_load_instances( + &c->transfers[i]->target, + arg_verify >= 0 ? arg_verify : c->transfers[i]->verify, + &c->web_cache); + if (r < 0) + return r; + } + + return 0; +} + +static int context_load_available_instances(Context *c) { + int r; + + assert(c); + + log_info("Discovering available instances%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS)); + + for (size_t i = 0; i < c->n_transfers; i++) { + assert(c->transfers[i]); + + r = resource_load_instances( + &c->transfers[i]->source, + arg_verify >= 0 ? arg_verify : c->transfers[i]->verify, + &c->web_cache); + if (r < 0) + return r; + } + + return 0; +} + +static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags) { + _cleanup_free_ Instance **cursor_instances = NULL; + _cleanup_free_ char *boundary = NULL; + bool newest_found = false; + int r; + + assert(c); + assert(IN_SET(flags, UPDATE_AVAILABLE, UPDATE_INSTALLED)); + + for (;;) { + bool incomplete = false, exists = false; + UpdateSetFlags extra_flags = 0; + _cleanup_free_ char *cursor = NULL; + UpdateSet *us = NULL; + + for (size_t k = 0; k < c->n_transfers; k++) { + Transfer *t = c->transfers[k]; + bool cursor_found = false; + Resource *rr; + + assert(t); + + if (flags == UPDATE_AVAILABLE) + rr = &t->source; + else { + assert(flags == UPDATE_INSTALLED); + rr = &t->target; + } + + for (size_t j = 0; j < rr->n_instances; j++) { + Instance *i = rr->instances[j]; + + assert(i); + + /* Is the instance we are looking at equal or newer than the boundary? If so, we + * already checked this version, and it wasn't complete, let's ignore it. */ + if (boundary && strverscmp_improved(i->metadata.version, boundary) >= 0) + continue; + + if (cursor) { + if (strverscmp_improved(i->metadata.version, cursor) != 0) + continue; + } else { + cursor = strdup(i->metadata.version); + if (!cursor) + return log_oom(); + } + + cursor_found = true; + + if (!cursor_instances) { + cursor_instances = new(Instance*, c->n_transfers); + if (!cursor_instances) + return -ENOMEM; + } + cursor_instances[k] = i; + break; + } + + if (!cursor) /* No suitable instance beyond the boundary found? Then we are done! */ + break; + + if (!cursor_found) { + /* Hmm, we didn't find the version indicated by 'cursor' among the instances + * of this transfer, let's skip it. */ + incomplete = true; + break; + } + + if (t->min_version && strverscmp_improved(t->min_version, cursor) > 0) + extra_flags |= UPDATE_OBSOLETE; + + if (strv_contains(t->protected_versions, cursor)) + extra_flags |= UPDATE_PROTECTED; + } + + if (!cursor) /* EOL */ + break; + + r = free_and_strdup_warn(&boundary, cursor); + if (r < 0) + return r; + + if (incomplete) /* One transfer was missing this version, ignore the whole thing */ + continue; + + /* See if we already have this update set in our table */ + for (size_t i = 0; i < c->n_update_sets; i++) { + if (strverscmp_improved(c->update_sets[i]->version, cursor) != 0) + continue; + + /* We only store the instances we found first, but we remember we also found it again */ + c->update_sets[i]->flags |= flags | extra_flags; + exists = true; + newest_found = true; + break; + } + + if (exists) + continue; + + /* Doesn't exist yet, let's add it */ + if (!GREEDY_REALLOC(c->update_sets, c->n_update_sets + 1)) + return log_oom(); + + us = new(UpdateSet, 1); + if (!us) + return log_oom(); + + *us = (UpdateSet) { + .flags = flags | (newest_found ? 0 : UPDATE_NEWEST) | extra_flags, + .version = TAKE_PTR(cursor), + .instances = TAKE_PTR(cursor_instances), + .n_instances = c->n_transfers, + }; + + c->update_sets[c->n_update_sets++] = us; + + newest_found = true; + + /* Remember which one is the newest installed */ + if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED)) == (UPDATE_NEWEST|UPDATE_INSTALLED)) + c->newest_installed = us; + + /* Remember which is the newest non-obsolete, available (and not installed) version, which we declare the "candidate" */ + if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE)) == (UPDATE_NEWEST|UPDATE_AVAILABLE)) + c->candidate = us; + } + + /* Newest installed is newer than or equal to candidate? Then suppress the candidate */ + if (c->newest_installed && c->candidate && strverscmp_improved(c->newest_installed->version, c->candidate->version) >= 0) + c->candidate = NULL; + + return 0; +} + +static int context_discover_update_sets(Context *c) { + int r; + + assert(c); + + log_info("Determining installed update sets%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS)); + + r = context_discover_update_sets_by_flag(c, UPDATE_INSTALLED); + if (r < 0) + return r; + + log_info("Determining available update sets%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS)); + + r = context_discover_update_sets_by_flag(c, UPDATE_AVAILABLE); + if (r < 0) + return r; + + typesafe_qsort(c->update_sets, c->n_update_sets, update_set_cmp); + return 0; +} + +static const char *update_set_flags_to_string(UpdateSetFlags flags) { + + switch ((unsigned) flags) { + + case 0: + return "n/a"; + + case UPDATE_INSTALLED|UPDATE_NEWEST: + case UPDATE_INSTALLED|UPDATE_NEWEST|UPDATE_PROTECTED: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED: + return "current"; + + case UPDATE_AVAILABLE|UPDATE_NEWEST: + case UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED: + return "candidate"; + + case UPDATE_INSTALLED: + case UPDATE_INSTALLED|UPDATE_AVAILABLE: + return "installed"; + + case UPDATE_INSTALLED|UPDATE_PROTECTED: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PROTECTED: + return "protected"; + + case UPDATE_AVAILABLE: + case UPDATE_AVAILABLE|UPDATE_PROTECTED: + return "available"; + + case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_NEWEST: + case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED: + return "current+obsolete"; + + case UPDATE_INSTALLED|UPDATE_OBSOLETE: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE: + return "installed+obsolete"; + + case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_PROTECTED: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_PROTECTED: + return "protected+obsolete"; + + case UPDATE_AVAILABLE|UPDATE_OBSOLETE: + case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_PROTECTED: + case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST: + case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED: + return "available+obsolete"; + + default: + assert_not_reached(); + } +} + + +static int context_show_table(Context *c) { + _cleanup_(table_unrefp) Table *t = NULL; + int r; + + assert(c); + + t = table_new("", "version", "installed", "available", "assessment"); + if (!t) + return log_oom(); + + (void) table_set_align_percent(t, table_get_cell(t, 0, 0), 100); + (void) table_set_align_percent(t, table_get_cell(t, 0, 2), 50); + (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 50); + + for (size_t i = 0; i < c->n_update_sets; i++) { + UpdateSet *us = c->update_sets[i]; + const char *color; + + color = update_set_flags_to_color(us->flags); + + r = table_add_many(t, + TABLE_STRING, update_set_flags_to_glyph(us->flags), + TABLE_SET_COLOR, color, + TABLE_STRING, us->version, + TABLE_SET_COLOR, color, + TABLE_STRING, special_glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_INSTALLED)), + TABLE_SET_COLOR, color, + TABLE_STRING, special_glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_AVAILABLE)), + TABLE_SET_COLOR, color, + TABLE_STRING, update_set_flags_to_string(us->flags), + TABLE_SET_COLOR, color); + 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 UpdateSet *context_update_set_by_version(Context *c, const char *version) { + assert(c); + assert(version); + + for (size_t i = 0; i < c->n_update_sets; i++) + if (streq(c->update_sets[i]->version, version)) + return c->update_sets[i]; + + return NULL; +} + +static int context_show_version(Context *c, const char *version) { + bool show_fs_columns = false, show_partition_columns = false, + have_fs_attributes = false, have_partition_attributes = false, + have_size = false, have_tries = false, have_no_auto = false, + have_read_only = false, have_growfs = false, have_sha256 = false; + _cleanup_(table_unrefp) Table *t = NULL; + UpdateSet *us; + int r; + + assert(c); + assert(version); + + us = context_update_set_by_version(c, version); + if (!us) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version); + + if (arg_json_format_flags & (JSON_FORMAT_OFF|JSON_FORMAT_PRETTY|JSON_FORMAT_PRETTY_AUTO)) + (void) pager_open(arg_pager_flags); + + if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) + printf("%s%s%s Version: %s\n" + " State: %s%s%s\n" + "Installed: %s%s\n" + "Available: %s%s\n" + "Protected: %s%s%s\n" + " Obsolete: %s%s%s\n\n", + strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_glyph(us->flags), ansi_normal(), us->version, + strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_string(us->flags), ansi_normal(), + yes_no(us->flags & UPDATE_INSTALLED), FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_NEWEST) ? " (newest)" : "", + yes_no(us->flags & UPDATE_AVAILABLE), (us->flags & (UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST)) == (UPDATE_AVAILABLE|UPDATE_NEWEST) ? " (newest)" : "", + FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED) ? ansi_highlight() : "", yes_no(FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED)), ansi_normal(), + us->flags & UPDATE_OBSOLETE ? ansi_highlight_red() : "", yes_no(us->flags & UPDATE_OBSOLETE), ansi_normal()); + + + t = table_new("type", "path", "ptuuid", "ptflags", "mtime", "mode", "size", "tries-done", "tries-left", "noauto", "ro", "growfs", "sha256"); + if (!t) + return log_oom(); + + (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 100); + (void) table_set_align_percent(t, table_get_cell(t, 0, 4), 100); + (void) table_set_align_percent(t, table_get_cell(t, 0, 5), 100); + (void) table_set_align_percent(t, table_get_cell(t, 0, 6), 100); + (void) table_set_align_percent(t, table_get_cell(t, 0, 7), 100); + (void) table_set_align_percent(t, table_get_cell(t, 0, 8), 100); + table_set_ersatz_string(t, TABLE_ERSATZ_DASH); + + /* Determine if the target will make use of partition/fs attributes for any of the transfers */ + for (size_t n = 0; n < c->n_transfers; n++) { + Transfer *tr = c->transfers[n]; + + if (tr->target.type == RESOURCE_PARTITION) + show_partition_columns = true; + if (RESOURCE_IS_FILESYSTEM(tr->target.type)) + show_fs_columns = true; + } + + for (size_t n = 0; n < us->n_instances; n++) { + Instance *i = us->instances[n]; + + r = table_add_many(t, + TABLE_STRING, resource_type_to_string(i->resource->type), + TABLE_PATH, i->path); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.partition_uuid_set) { + have_partition_attributes = true; + r = table_add_cell(t, NULL, TABLE_UUID, &i->metadata.partition_uuid); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.partition_flags_set) { + have_partition_attributes = true; + r = table_add_cell(t, NULL, TABLE_UINT64_HEX, &i->metadata.partition_flags); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.mtime != USEC_INFINITY) { + have_fs_attributes = true; + r = table_add_cell(t, NULL, TABLE_TIMESTAMP, &i->metadata.mtime); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.mode != MODE_INVALID) { + have_fs_attributes = true; + r = table_add_cell(t, NULL, TABLE_MODE, &i->metadata.mode); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.size != UINT64_MAX) { + have_size = true; + r = table_add_cell(t, NULL, TABLE_SIZE, &i->metadata.size); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.tries_done != UINT64_MAX) { + have_tries = true; + r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_done); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.tries_left != UINT64_MAX) { + have_tries = true; + r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_left); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.no_auto >= 0) { + bool b; + + have_no_auto = true; + b = i->metadata.no_auto; + r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + if (i->metadata.read_only >= 0) { + bool b; + + have_read_only = true; + b = i->metadata.read_only; + r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.growfs >= 0) { + bool b; + + have_growfs = true; + b = i->metadata.growfs; + r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.sha256sum_set) { + _cleanup_free_ char *formatted = NULL; + + have_sha256 = true; + + formatted = hexmem(i->metadata.sha256sum, sizeof(i->metadata.sha256sum)); + if (!formatted) + return log_oom(); + + r = table_add_cell(t, NULL, TABLE_STRING, formatted); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + } + + /* Hide the fs/partition columns if we don't have any data to show there */ + if (!have_fs_attributes) + show_fs_columns = false; + if (!have_partition_attributes) + show_partition_columns = false; + + if (!show_partition_columns) + (void) table_hide_column_from_display(t, 2, 3); + if (!show_fs_columns) + (void) table_hide_column_from_display(t, 4, 5); + if (!have_size) + (void) table_hide_column_from_display(t, 6); + if (!have_tries) + (void) table_hide_column_from_display(t, 7, 8); + if (!have_no_auto) + (void) table_hide_column_from_display(t, 9); + if (!have_read_only) + (void) table_hide_column_from_display(t, 10); + if (!have_growfs) + (void) table_hide_column_from_display(t, 11); + if (!have_sha256) + (void) table_hide_column_from_display(t, 12); + + return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend); +} + +static int context_vacuum( + Context *c, + uint64_t space, + const char *extra_protected_version) { + + int r, count = 0; + + assert(c); + + if (space == 0) + log_info("Making room%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS)); + else + log_info("Making room for %" PRIu64 " updates%s", space,special_glyph(SPECIAL_GLYPH_ELLIPSIS)); + + for (size_t i = 0; i < c->n_transfers; i++) { + r = transfer_vacuum(c->transfers[i], space, extra_protected_version); + if (r < 0) + return r; + + count = MAX(count, r); + } + + if (count > 0) + log_info("Removed %i instances.", count); + else + log_info("Removed no instances."); + + return 0; +} + +static int context_make_offline(Context **ret, const char *node) { + _cleanup_(context_freep) Context* context = NULL; + int r; + + assert(ret); + + /* Allocates a context object and initializes everything we can initialize offline, i.e. without + * checking on the update source (i.e. the Internet) what versions are available */ + + context = context_new(); + if (!context) + return log_oom(); + + r = context_read_definitions(context, arg_definitions, arg_component, arg_root, node); + if (r < 0) + return r; + + r = context_load_installed_instances(context); + if (r < 0) + return r; + + *ret = TAKE_PTR(context); + return 0; +} + +static int context_make_online(Context **ret, const char *node) { + _cleanup_(context_freep) Context* context = NULL; + int r; + + assert(ret); + + /* Like context_make_offline(), but also communicates with the update source looking for new + * versions. */ + + r = context_make_offline(&context, node); + if (r < 0) + return r; + + r = context_load_available_instances(context); + if (r < 0) + return r; + + r = context_discover_update_sets(context); + if (r < 0) + return r; + + *ret = TAKE_PTR(context); + return 0; +} + +static int context_apply( + Context *c, + const char *version, + UpdateSet **ret_applied) { + + UpdateSet *us = NULL; + int r; + + assert(c); + + if (version) { + us = context_update_set_by_version(c, version); + if (!us) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version); + } else { + if (!c->candidate) { + log_info("No update needed."); + + if (ret_applied) + *ret_applied = NULL; + + return 0; + } + + us = c->candidate; + } + + if (FLAGS_SET(us->flags, UPDATE_INSTALLED)) { + log_info("Selected update '%s' is already installed. Skipping update.", us->version); + + if (ret_applied) + *ret_applied = NULL; + + return 0; + } + if (!FLAGS_SET(us->flags, UPDATE_AVAILABLE)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not available, refusing.", us->version); + if (FLAGS_SET(us->flags, UPDATE_OBSOLETE)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is obsolete, refusing.", us->version); + + assert((us->flags & (UPDATE_AVAILABLE|UPDATE_INSTALLED|UPDATE_OBSOLETE)) == UPDATE_AVAILABLE); + + if (!FLAGS_SET(us->flags, UPDATE_NEWEST)) + log_notice("Selected update '%s' is not the newest, proceeding anyway.", us->version); + if (c->newest_installed && strverscmp_improved(c->newest_installed->version, us->version) > 0) + log_notice("Selected update '%s' is older than newest installed version, proceeding anyway.", us->version); + + log_info("Selected update '%s' for install.", us->version); + + (void) sd_notifyf(false, + "STATUS=Making room for '%s'.", us->version); + + /* Let's make some room. We make sure for each transfer we have one free space to fill. While + * removing stuff we'll protect the version we are trying to acquire. Why that? Maybe an earlier + * download succeeded already, in which case we shouldn't remove it just to acquire it again */ + r = context_vacuum( + c, + /* space = */ 1, + /* extra_protected_version = */ us->version); + if (r < 0) + return r; + + if (arg_sync) + sync(); + + (void) sd_notifyf(false, + "STATUS=Updating to '%s'.\n", us->version); + + /* There should now be one instance picked for each transfer, and the order is the same */ + assert(us->n_instances == c->n_transfers); + + for (size_t i = 0; i < c->n_transfers; i++) { + r = transfer_acquire_instance(c->transfers[i], us->instances[i]); + if (r < 0) + return r; + } + + if (arg_sync) + sync(); + + for (size_t i = 0; i < c->n_transfers; i++) { + r = transfer_install_instance(c->transfers[i], us->instances[i], arg_root); + if (r < 0) + return r; + } + + log_info("%s Successfully installed update '%s'.", special_glyph(SPECIAL_GLYPH_SPARKLES), us->version); + + if (ret_applied) + *ret_applied = us; + + return 1; +} + +static int reboot_now(void) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_close_unrefp) sd_bus *bus = NULL; + int r; + + r = sd_bus_open_system(&bus); + if (r < 0) + return log_error_errno(r, "Failed to open bus connection: %m"); + + r = bus_call_method(bus, bus_login_mgr, "RebootWithFlags", &error, NULL, "t", + (uint64_t) SD_LOGIND_ROOT_CHECK_INHIBITORS); + if (r < 0) + return log_error_errno(r, "Failed to issue reboot request: %s", bus_error_message(&error, r)); + + return 0; +} + +static int process_image( + bool ro, + char **ret_mounted_dir, + LoopDevice **ret_loop_device) { + + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(umount_and_freep) char *mounted_dir = NULL; + int r; + + assert(ret_mounted_dir); + assert(ret_loop_device); + + if (!arg_image) + return 0; + + assert(!arg_root); + + r = mount_image_privately_interactively( + arg_image, + arg_image_policy, + (ro ? DISSECT_IMAGE_READ_ONLY : 0) | + DISSECT_IMAGE_FSCK | + DISSECT_IMAGE_MKDIR | + DISSECT_IMAGE_GROWFS | + DISSECT_IMAGE_RELAX_VAR_CHECK | + DISSECT_IMAGE_USR_NO_ROOT | + DISSECT_IMAGE_GENERIC_ROOT | + DISSECT_IMAGE_REQUIRE_ROOT, + &mounted_dir, + /* ret_dir_fd= */ NULL, + &loop_device); + if (r < 0) + return r; + + arg_root = strdup(mounted_dir); + if (!arg_root) + return log_oom(); + + *ret_mounted_dir = TAKE_PTR(mounted_dir); + *ret_loop_device = TAKE_PTR(loop_device); + + return 0; +} + +static int verb_list(int argc, char **argv, void *userdata) { + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; + _cleanup_(context_freep) Context* context = NULL; + const char *version; + int r; + + assert(argc <= 2); + version = argc >= 2 ? argv[1] : NULL; + + r = process_image(/* ro= */ true, &mounted_dir, &loop_device); + if (r < 0) + return r; + + r = context_make_online(&context, loop_device ? loop_device->node : NULL); + if (r < 0) + return r; + + if (version) + return context_show_version(context, version); + else + return context_show_table(context); +} + +static int verb_check_new(int argc, char **argv, void *userdata) { + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; + _cleanup_(context_freep) Context* context = NULL; + int r; + + assert(argc <= 1); + + r = process_image(/* ro= */ true, &mounted_dir, &loop_device); + if (r < 0) + return r; + + r = context_make_online(&context, loop_device ? loop_device->node : NULL); + if (r < 0) + return r; + + if (!context->candidate) { + log_debug("No candidate found."); + return EXIT_FAILURE; + } + + puts(context->candidate->version); + return EXIT_SUCCESS; +} + +static int verb_vacuum(int argc, char **argv, void *userdata) { + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; + _cleanup_(context_freep) Context* context = NULL; + int r; + + assert(argc <= 1); + + r = process_image(/* ro= */ false, &mounted_dir, &loop_device); + if (r < 0) + return r; + + r = context_make_offline(&context, loop_device ? loop_device->node : NULL); + if (r < 0) + return r; + + return context_vacuum(context, 0, NULL); +} + +static int verb_update(int argc, char **argv, void *userdata) { + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; + _cleanup_(context_freep) Context* context = NULL; + _cleanup_free_ char *booted_version = NULL; + UpdateSet *applied = NULL; + const char *version; + int r; + + assert(argc <= 2); + version = argc >= 2 ? argv[1] : NULL; + + if (arg_reboot) { + /* If automatic reboot on completion is requested, let's first determine the currently booted image */ + + r = parse_os_release(arg_root, "IMAGE_VERSION", &booted_version); + if (r < 0) + return log_error_errno(r, "Failed to parse /etc/os-release: %m"); + if (!booted_version) + return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION field."); + } + + r = process_image(/* ro= */ false, &mounted_dir, &loop_device); + if (r < 0) + return r; + + r = context_make_online(&context, loop_device ? loop_device->node : NULL); + if (r < 0) + return r; + + r = context_apply(context, version, &applied); + if (r < 0) + return r; + + if (r > 0 && arg_reboot) { + assert(applied); + assert(booted_version); + + if (strverscmp_improved(applied->version, booted_version) > 0) { + log_notice("Newly installed version is newer than booted version, rebooting."); + return reboot_now(); + } + + log_info("Booted version is newer or identical to newly installed version, not rebooting."); + } + + return 0; +} + +static int verb_pending_or_reboot(int argc, char **argv, void *userdata) { + _cleanup_(context_freep) Context* context = NULL; + _cleanup_free_ char *booted_version = NULL; + int r; + + assert(argc == 1); + + if (arg_image || arg_root) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "The --root=/--image= switches may not be combined with the '%s' operation.", argv[0]); + + r = context_make_offline(&context, NULL); + if (r < 0) + return r; + + log_info("Determining installed update sets%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS)); + + r = context_discover_update_sets_by_flag(context, UPDATE_INSTALLED); + if (r < 0) + return r; + if (!context->newest_installed) + return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "Couldn't find any suitable installed versions."); + + r = parse_os_release(arg_root, "IMAGE_VERSION", &booted_version); + if (r < 0) /* yes, arg_root is NULL here, but we have to pass something, and it's a lot more readable + * if we see what the first argument is about */ + return log_error_errno(r, "Failed to parse /etc/os-release: %m"); + if (!booted_version) + return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION= field."); + + r = strverscmp_improved(context->newest_installed->version, booted_version); + if (r > 0) { + log_notice("Newest installed version '%s' is newer than booted version '%s'.%s", + context->newest_installed->version, booted_version, + streq(argv[0], "pending") ? " Reboot recommended." : ""); + + if (streq(argv[0], "reboot")) + return reboot_now(); + + return EXIT_SUCCESS; + } else if (r == 0) + log_info("Newest installed version '%s' matches booted version '%s'.", + context->newest_installed->version, booted_version); + else + log_warning("Newest installed version '%s' is older than booted version '%s'.", + context->newest_installed->version, booted_version); + + if (streq(argv[0], "pending")) /* When called as 'pending' tell the caller via failure exit code that there's nothing newer installed */ + return EXIT_FAILURE; + + return EXIT_SUCCESS; +} + +static int component_name_valid(const char *c) { + _cleanup_free_ char *j = NULL; + + /* See if the specified string enclosed in the directory prefix+suffix would be a valid file name */ + + if (isempty(c)) + return false; + + if (string_has_cc(c, NULL)) + return false; + + if (!utf8_is_valid(c)) + return false; + + j = strjoin("sysupdate.", c, ".d"); + if (!j) + return -ENOMEM; + + return filename_is_valid(j); +} + +static int verb_components(int argc, char **argv, void *userdata) { + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; + _cleanup_set_free_ Set *names = NULL; + _cleanup_free_ char **z = NULL; /* We use simple free() rather than strv_free() here, since set_free() will free the strings for us */ + char **l = CONF_PATHS_STRV(""); + bool has_default_component = false; + int r; + + assert(argc <= 1); + + r = process_image(/* ro= */ false, &mounted_dir, &loop_device); + if (r < 0) + return r; + + STRV_FOREACH(i, l) { + _cleanup_closedir_ DIR *d = NULL; + _cleanup_free_ char *p = NULL; + + r = chase_and_opendir(*i, arg_root, CHASE_PREFIX_ROOT, &p, &d); + if (r == -ENOENT) + continue; + if (r < 0) + return log_error_errno(r, "Failed to open directory '%s': %m", *i); + + for (;;) { + _cleanup_free_ char *n = NULL; + struct dirent *de; + const char *e, *a; + + de = readdir_ensure_type(d); + if (!de) { + if (errno != 0) + return log_error_errno(errno, "Failed to enumerate directory '%s': %m", p); + + break; + } + + if (de->d_type != DT_DIR) + continue; + + if (dot_or_dot_dot(de->d_name)) + continue; + + if (streq(de->d_name, "sysupdate.d")) { + has_default_component = true; + continue; + } + + e = startswith(de->d_name, "sysupdate."); + if (!e) + continue; + + a = endswith(e, ".d"); + if (!a) + continue; + + n = strndup(e, a - e); + if (!n) + return log_oom(); + + r = component_name_valid(n); + if (r < 0) + return log_error_errno(r, "Unable to validate component name: %m"); + if (r == 0) + continue; + + r = set_ensure_consume(&names, &string_hash_ops_free, TAKE_PTR(n)); + if (r < 0 && r != -EEXIST) + return log_error_errno(r, "Failed to add component to set: %m"); + } + } + + if (!has_default_component && set_isempty(names)) { + log_info("No components defined."); + return 0; + } + + z = set_get_strv(names); + if (!z) + return log_oom(); + + strv_sort(z); + + if (has_default_component) + printf("%s<default>%s\n", + ansi_highlight(), ansi_normal()); + + STRV_FOREACH(i, z) + puts(*i); + + return 0; +} + +static int verb_help(int argc, char **argv, void *userdata) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-sysupdate", "8", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] [VERSION]\n" + "\n%5$sUpdate OS images.%6$s\n" + "\n%3$sCommands:%4$s\n" + " list [VERSION] Show installed and available versions\n" + " check-new Check if there's a new version available\n" + " update [VERSION] Install new version now\n" + " vacuum Make room, by deleting old versions\n" + " pending Report whether a newer version is installed than\n" + " currently booted\n" + " reboot Reboot if a newer version is installed than booted\n" + " components Show list of components\n" + " -h --help Show this help\n" + " --version Show package version\n" + "\n%3$sOptions:%4$s\n" + " -C --component=NAME Select component to update\n" + " --definitions=DIR Find transfer definitions in specified directory\n" + " --root=PATH Operate on an alternate filesystem root\n" + " --image=PATH Operate on disk image as filesystem root\n" + " --image-policy=POLICY\n" + " Specify disk image dissection policy\n" + " -m --instances-max=INT How many instances to maintain\n" + " --sync=BOOL Controls whether to sync data to disk\n" + " --verify=BOOL Force signature verification on or off\n" + " --reboot Reboot after updating to newer version\n" + " --no-pager Do not pipe output into a pager\n" + " --no-legend Do not show the headers and footers\n" + " --json=pretty|short|off\n" + " Generate JSON output\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_SYNC, + ARG_DEFINITIONS, + ARG_JSON, + ARG_ROOT, + ARG_IMAGE, + ARG_IMAGE_POLICY, + ARG_REBOOT, + ARG_VERIFY, + }; + + 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 }, + { "definitions", required_argument, NULL, ARG_DEFINITIONS }, + { "instances-max", required_argument, NULL, 'm' }, + { "sync", required_argument, NULL, ARG_SYNC }, + { "json", required_argument, NULL, ARG_JSON }, + { "root", required_argument, NULL, ARG_ROOT }, + { "image", required_argument, NULL, ARG_IMAGE }, + { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, + { "reboot", no_argument, NULL, ARG_REBOOT }, + { "component", required_argument, NULL, 'C' }, + { "verify", required_argument, NULL, ARG_VERIFY }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hm:C:", options, NULL)) >= 0) { + + switch (c) { + + case 'h': + return verb_help(0, NULL, NULL); + + case ARG_VERSION: + return version(); + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_NO_LEGEND: + arg_legend = false; + break; + + case 'm': + r = safe_atou64(optarg, &arg_instances_max); + if (r < 0) + return log_error_errno(r, "Failed to parse --instances-max= parameter: %s", optarg); + + break; + + case ARG_SYNC: + r = parse_boolean_argument("--sync=", optarg, &arg_sync); + if (r < 0) + return r; + break; + + case ARG_DEFINITIONS: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_definitions); + if (r < 0) + return r; + break; + + case ARG_JSON: + r = parse_json_argument(optarg, &arg_json_format_flags); + if (r <= 0) + return r; + + break; + + case ARG_ROOT: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_root); + if (r < 0) + return r; + break; + + case ARG_IMAGE: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image); + if (r < 0) + return r; + break; + + case ARG_IMAGE_POLICY: + r = parse_image_policy_argument(optarg, &arg_image_policy); + if (r < 0) + return r; + break; + + case ARG_REBOOT: + arg_reboot = true; + break; + + case 'C': + if (isempty(optarg)) { + arg_component = mfree(arg_component); + break; + } + + r = component_name_valid(optarg); + if (r < 0) + return log_error_errno(r, "Failed to determine if component name is valid: %m"); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", optarg); + + r = free_and_strdup_warn(&arg_component, optarg); + if (r < 0) + return r; + + break; + + case ARG_VERIFY: { + bool b; + + r = parse_boolean_argument("--verify=", optarg, &b); + if (r < 0) + return r; + + arg_verify = b; + break; + } + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + } + + if (arg_image && arg_root) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported."); + + if ((arg_image || arg_root) && arg_reboot) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --root= or --image=."); + + if (arg_definitions && arg_component) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --component= switches may not be combined."); + + return 1; +} + +static int sysupdate_main(int argc, char *argv[]) { + + static const Verb verbs[] = { + { "list", VERB_ANY, 2, VERB_DEFAULT, verb_list }, + { "components", VERB_ANY, 1, 0, verb_components }, + { "check-new", VERB_ANY, 1, 0, verb_check_new }, + { "update", VERB_ANY, 2, 0, verb_update }, + { "vacuum", VERB_ANY, 1, 0, verb_vacuum }, + { "reboot", 1, 1, 0, verb_pending_or_reboot }, + { "pending", 1, 1, 0, verb_pending_or_reboot }, + { "help", VERB_ANY, 1, 0, verb_help }, + {} + }; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + return sysupdate_main(argc, argv); +} + +DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); diff --git a/src/sysupdate/sysupdate.h b/src/sysupdate/sysupdate.h new file mode 100644 index 0000000..6d387b7 --- /dev/null +++ b/src/sysupdate/sysupdate.h @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <stdbool.h> + +extern bool arg_sync; +extern uint64_t arg_instances_max; +extern char *arg_root; + +static inline const char* import_binary_path(void) { + return secure_getenv("SYSTEMD_IMPORT_PATH") ?: SYSTEMD_IMPORT_PATH; +} + +static inline const char* import_fs_binary_path(void) { + return secure_getenv("SYSTEMD_IMPORT_FS_PATH") ?: SYSTEMD_IMPORT_FS_PATH; +} + +static inline const char *pull_binary_path(void) { + return secure_getenv("SYSTEMD_PULL_PATH") ?: SYSTEMD_PULL_PATH; +} |