diff --git a/man/systemd-sysupdate.xml b/man/systemd-sysupdate.xml index 3abccb05e4..2fdf3c1f59 100644 --- a/man/systemd-sysupdate.xml +++ b/man/systemd-sysupdate.xml @@ -204,6 +204,31 @@ + + + + Removes orphaned files that were previously installed by a transfer, but are no + longer owned by any currently defined transfer file. Whenever a resource is installed into the file + system, systemd-sysupdate records the target directory and the matching pattern in + an installation database below /var/lib/systemd/sysupdate/. This command + iterates through these records, determines which files they match, and deletes those that are no + longer covered by any of the patterns of the transfer files currently in place. This is useful to + garbage-collect files that used to be owned by a transfer file that has since been modified, disabled + or removed altogether (for example because a component is no longer being updated). + + By default only the selected component is processed (i.e. the one selected via + , or the default one if none were selected). Use + to process all components known to the installation database in a + single invocation. + + This operation only removes files that were installed into the file system (i.e. resources of + type regular-file, directory and subvolume, + see sysupdate.d5); + it does not touch partition-based resources. + + + + @@ -246,6 +271,19 @@ + + + + + Instead of operating on a single component, operate on all known components (as well as + the default, component-less installation). This is currently only supported for the + cleanup command; all other commands will fail if this switch is used. + + This option may not be combined with . + + + + diff --git a/src/basic/recurse-dir.c b/src/basic/recurse-dir.c index 8f691d9229..6e0ce5cd4b 100644 --- a/src/basic/recurse-dir.c +++ b/src/basic/recurse-dir.c @@ -27,6 +27,35 @@ static bool ignore_dirent(const struct dirent *de, RecurseDirFlags flags) { /* Depending on flag either ignore everything starting with ".", or just "." itself and ".." */ + if ((flags & _RECURSE_DIR_MUST_BE_MASK) != 0) { + RecurseDirFlags f; + + switch (de->d_type) { + + case DT_DIR: + f = RECURSE_DIR_MUST_BE_DIRECTORY; + break; + + case DT_REG: + f = RECURSE_DIR_MUST_BE_REGULAR; + break; + + case DT_LNK: + f = RECURSE_DIR_MUST_BE_SYMLINK; + break; + + case DT_SOCK: + f = RECURSE_DIR_MUST_BE_SOCKET; + break; + + default: + return true; + } + + if (!FLAGS_SET(flags, f)) + return true; + } + return FLAGS_SET(flags, RECURSE_DIR_IGNORE_DOT) ? de->d_name[0] == '.' : dot_or_dot_dot(de->d_name); @@ -39,6 +68,9 @@ int readdir_all(int dir_fd, RecurseDirFlags flags, DirectoryEntries **ret) { assert(dir_fd >= 0); + if ((flags & _RECURSE_DIR_MUST_BE_MASK) != 0) /* We need the type to validate it */ + flags |= RECURSE_DIR_ENSURE_TYPE; + /* Returns an array with pointers to "struct dirent" directory entries, optionally sorted. * * Start with space for up to 8 directory entries. We expect at least 2 ("." + ".."), hence hopefully @@ -87,9 +119,6 @@ int readdir_all(int dir_fd, RecurseDirFlags flags, DirectoryEntries **ret) { de->n_entries = 0; struct dirent *entry; FOREACH_DIRENT_IN_BUFFER(entry, de->buffer, de->buffer_size) { - if (ignore_dirent(entry, flags)) - continue; - if (FLAGS_SET(flags, RECURSE_DIR_ENSURE_TYPE)) { r = dirent_ensure_type(dir_fd, entry); if (r == -ENOENT) @@ -99,6 +128,9 @@ int readdir_all(int dir_fd, RecurseDirFlags flags, DirectoryEntries **ret) { return r; } + if (ignore_dirent(entry, flags)) + continue; + de->n_entries++; } @@ -117,14 +149,14 @@ int readdir_all(int dir_fd, RecurseDirFlags flags, DirectoryEntries **ret) { j = 0; FOREACH_DIRENT_IN_BUFFER(entry, de->buffer, de->buffer_size) { - if (ignore_dirent(entry, flags)) - continue; - /* If d_type == DT_UNKNOWN that means we failed to ensure the type in the earlier loop and * didn't include the dentry in de->n_entries and as such should skip it here as well. */ if (FLAGS_SET(flags, RECURSE_DIR_ENSURE_TYPE) && entry->d_type == DT_UNKNOWN) continue; + if (ignore_dirent(entry, flags)) + continue; + de->entries[j++] = entry; } assert(j == de->n_entries); @@ -166,6 +198,9 @@ int recurse_dir( assert(dir_fd >= 0); assert(func); + /* We cannot descend into dirs if we are supposed to ignore them */ + assert((flags & _RECURSE_DIR_MUST_BE_MASK) == 0 || FLAGS_SET(flags, RECURSE_DIR_MUST_BE_DIRECTORY)); + /* This is a lot like ftw()/nftw(), but a lot more modern, i.e. built around openat()/statx()/O_PATH, * and under the assumption that fds are not as 'expensive' as they used to be. */ diff --git a/src/basic/recurse-dir.h b/src/basic/recurse-dir.h index af33c239f7..6b6eac3639 100644 --- a/src/basic/recurse-dir.h +++ b/src/basic/recurse-dir.h @@ -7,14 +7,19 @@ typedef enum RecurseDirFlags { /* Interpreted by readdir_all() */ - RECURSE_DIR_SORT = 1 << 0, /* sort file directory entries before processing them */ - RECURSE_DIR_IGNORE_DOT = 1 << 1, /* ignore all dot files ("." and ".." are always ignored) */ - RECURSE_DIR_ENSURE_TYPE = 1 << 2, /* guarantees that 'd_type' field of 'de' is not DT_UNKNOWN */ + RECURSE_DIR_SORT = 1 << 0, /* sort file directory entries before processing them */ + RECURSE_DIR_IGNORE_DOT = 1 << 1, /* ignore all dot files ("." and ".." are always ignored) */ + RECURSE_DIR_ENSURE_TYPE = 1 << 2, /* guarantees that 'd_type' field of 'de' is not DT_UNKNOWN */ + RECURSE_DIR_MUST_BE_DIRECTORY = 1 << 3, /* ignore all entries that aren't directories */ + RECURSE_DIR_MUST_BE_REGULAR = 1 << 4, /* ignore all entries that aren't regular files */ + RECURSE_DIR_MUST_BE_SYMLINK = 1 << 5, /* ignore all entries that aren't symlinks */ + RECURSE_DIR_MUST_BE_SOCKET = 1 << 6, /* ignore all entries that aren't socket */ + _RECURSE_DIR_MUST_BE_MASK = RECURSE_DIR_MUST_BE_DIRECTORY|RECURSE_DIR_MUST_BE_REGULAR|RECURSE_DIR_MUST_BE_SYMLINK|RECURSE_DIR_MUST_BE_SOCKET, /* Interpreted by recurse_dir() */ - RECURSE_DIR_SAME_MOUNT = 1 << 3, /* skips over subdirectories that are submounts */ - RECURSE_DIR_INODE_FD = 1 << 4, /* passes an opened inode fd (O_DIRECTORY fd in case of dirs, O_PATH otherwise) */ - RECURSE_DIR_TOPLEVEL = 1 << 5, /* call RECURSE_DIR_ENTER/RECURSE_DIR_LEAVE once for top-level dir, too, with dir_fd=-1 and NULL dirent */ + RECURSE_DIR_SAME_MOUNT = 1 << 7, /* skips over subdirectories that are submounts */ + RECURSE_DIR_INODE_FD = 1 << 8, /* passes an opened inode fd (O_DIRECTORY fd in case of dirs, O_PATH otherwise) */ + RECURSE_DIR_TOPLEVEL = 1 << 9, /* call RECURSE_DIR_ENTER/RECURSE_DIR_LEAVE once for top-level dir, too, with dir_fd=-1 and NULL dirent */ } RecurseDirFlags; typedef struct DirectoryEntries { diff --git a/src/basic/sha256.c b/src/basic/sha256.c index 31e535d4d2..ebefdc7540 100644 --- a/src/basic/sha256.c +++ b/src/basic/sha256.c @@ -11,6 +11,8 @@ int sha256_fd(int fd, uint64_t max_size, uint8_t ret[static SHA256_DIGEST_SIZE]) struct sha256_ctx ctx; uint64_t total_size = 0; + assert(fd >= 0); + sha256_init_ctx(&ctx); for (;;) { @@ -53,3 +55,12 @@ int parse_sha256(const char *s, uint8_t ret[static SHA256_DIGEST_SIZE]) { bool sha256_is_valid(const char *s) { return s && in_charset(s, HEXDIGITS) && (strlen(s) == SHA256_DIGEST_SIZE * 2); } + +char* sha256_direct_hex(const void *buffer, size_t sz) { + assert(buffer || sz == 0); + + if (sz == SIZE_MAX) + sz = strlen(buffer); + + return hexmem(SHA256_DIRECT(buffer, sz), SHA256_DIGEST_SIZE); +} diff --git a/src/basic/sha256.h b/src/basic/sha256.h index 74314a0cab..8d3387ea49 100644 --- a/src/basic/sha256.h +++ b/src/basic/sha256.h @@ -11,3 +11,5 @@ int sha256_fd(int fd, uint64_t max_size, uint8_t ret[static SHA256_DIGEST_SIZE]) int parse_sha256(const char *s, uint8_t ret[static SHA256_DIGEST_SIZE]); bool sha256_is_valid(const char *s) _pure_; + +char* sha256_direct_hex(const void *buffer, size_t sz); diff --git a/src/basic/string-util.c b/src/basic/string-util.c index 0484a03473..125d468d47 100644 --- a/src/basic/string-util.c +++ b/src/basic/string-util.c @@ -1145,6 +1145,9 @@ bool string_is_safe(const char *p, StringSafeFlags flags) { if (FLAGS_SET(flags, STRING_FILENAME) && !filename_is_valid(p)) return false; + if (FLAGS_SET(flags, STRING_FILENAME_PART) && !filename_part_is_valid(p)) + return false; + return true; } diff --git a/src/basic/string-util.h b/src/basic/string-util.h index 6242f55931..68ab38c321 100644 --- a/src/basic/string-util.h +++ b/src/basic/string-util.h @@ -229,7 +229,8 @@ typedef enum StringSafeFlags { STRING_ALLOW_QUOTES = 1 << 4, /* Allow quotes (" or ') */ STRING_ALLOW_GLOBS = 1 << 5, /* Allow globs (?, * or [) */ STRING_FILENAME = 1 << 6, /* Verify the string is valid as regular filename */ - STRING_DISALLOW_WHITESPACE = 1 << 7, /* Refuse whitespace (space, tab, newline, …) */ + STRING_FILENAME_PART = 1 << 7, /* Verify the string is valid as part of a regular filename */ + STRING_DISALLOW_WHITESPACE = 1 << 8, /* Refuse whitespace (space, tab, newline, …) */ } StringSafeFlags; bool string_is_safe(const char *p, StringSafeFlags flags) _pure_; diff --git a/src/debug-generator/debug-generator.c b/src/debug-generator/debug-generator.c index 9ef271343c..700ad52893 100644 --- a/src/debug-generator/debug-generator.c +++ b/src/debug-generator/debug-generator.c @@ -279,7 +279,7 @@ static int process_unit_credentials(const char *credentials_dir) { assert(credentials_dir); - r = readdir_all_at(AT_FDCWD, credentials_dir, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des); + r = readdir_all_at(AT_FDCWD, credentials_dir, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_MUST_BE_REGULAR, &des); if (r < 0) return log_error_errno(r, "Failed to enumerate credentials from credentials directory '%s': %m", credentials_dir); @@ -287,9 +287,6 @@ static int process_unit_credentials(const char *credentials_dir) { struct dirent *de = *i; const char *unit, *dropin; - if (de->d_type != DT_REG) - continue; - unit = startswith(de->d_name, "systemd.extra-unit."); dropin = startswith(de->d_name, "systemd.unit-dropin."); diff --git a/src/kernel-install/kernel-install.c b/src/kernel-install/kernel-install.c index c77b23d4bc..132ae803df 100644 --- a/src/kernel-install/kernel-install.c +++ b/src/kernel-install/kernel-install.c @@ -11,7 +11,6 @@ #include "build.h" #include "chase.h" #include "conf-files.h" -#include "dirent-util.h" #include "dissect-image.h" #include "env-file.h" #include "env-util.h" @@ -1247,21 +1246,11 @@ static int verb_add_all(int argc, char *argv[], uintptr_t _data, void *userdata) return log_error_errno(fd, "Failed to open %s/usr/lib/modules/: %m", strempty(arg_root)); _cleanup_free_ DirectoryEntries *de = NULL; - r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de); + r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_MUST_BE_DIRECTORY, &de); if (r < 0) return log_error_errno(r, "Failed to numerate /usr/lib/modules/ contents: %m"); FOREACH_ARRAY(d, de->entries, de->n_entries) { - r = dirent_ensure_type(fd, *d); - if (r < 0) { - if (r != -ENOENT) /* don't log if just gone by now */ - log_debug_errno(r, "Failed to check if '%s/usr/lib/modules/%s' is a directory, ignoring: %m", strempty(arg_root), (*d)->d_name); - continue; - } - - if ((*d)->d_type != DT_DIR) - continue; - _cleanup_free_ char *fn = path_join((*d)->d_name, "vmlinuz"); if (!fn) return log_oom(); @@ -1483,7 +1472,7 @@ static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) { return log_error_errno(fd, "Failed to open %s/usr/lib/modules/: %m", strempty(arg_root)); _cleanup_free_ DirectoryEntries *de = NULL; - r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de); + r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_MUST_BE_DIRECTORY, &de); if (r < 0) return log_error_errno(r, "Failed to numerate /usr/lib/modules/ contents: %m"); @@ -1501,16 +1490,6 @@ static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) { if (!j) return log_oom(); - r = dirent_ensure_type(fd, *d); - if (r < 0) { - if (r != -ENOENT) /* don't log if just gone by now */ - log_debug_errno(r, "Failed to check if '%s/%s' is a directory, ignoring: %m", strempty(arg_root), j); - continue; - } - - if ((*d)->d_type != DT_DIR) - continue; - _cleanup_free_ char *fn = path_join((*d)->d_name, "vmlinuz"); if (!fn) return log_oom(); diff --git a/src/libsystemd/sd-varlink/varlink-util.c b/src/libsystemd/sd-varlink/varlink-util.c index 1eace7724c..d454fc7f48 100644 --- a/src/libsystemd/sd-varlink/varlink-util.c +++ b/src/libsystemd/sd-varlink/varlink-util.c @@ -272,7 +272,7 @@ ssize_t varlink_execute_directory( return log_debug_errno(errno, "Failed to open '%s': %m", path); _cleanup_free_ DirectoryEntries *dentries = NULL; - r = readdir_all(fd, RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &dentries); + r = readdir_all(fd, RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_MUST_BE_SOCKET|RECURSE_DIR_MUST_BE_SYMLINK, &dentries); if (r < 0) return log_debug_errno(r, "Failed to enumerate '%s': %m", path); @@ -282,9 +282,6 @@ ssize_t varlink_execute_directory( FOREACH_ARRAY(dp, dentries->entries, dentries->n_entries) { struct dirent *de = *dp; - if (!IN_SET(de->d_type, DT_SOCK, DT_LNK)) - continue; - t++; _cleanup_free_ char *j = path_join(path, de->d_name); diff --git a/src/nsresourced/userns-registry.c b/src/nsresourced/userns-registry.c index 0bfb1edbde..0da6a6a7c6 100644 --- a/src/nsresourced/userns-registry.c +++ b/src/nsresourced/userns-registry.c @@ -1219,7 +1219,7 @@ int userns_registry_per_uid(int dir_fd, uid_t owner) { _cleanup_free_ DirectoryEntries *de = NULL; - r = readdir_all_at(dir_fd, uid_fn, RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &de); + r = readdir_all_at(dir_fd, uid_fn, RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_MUST_BE_REGULAR, &de); if (r == -ENOENT) return 0; if (r < 0) @@ -1228,9 +1228,6 @@ int userns_registry_per_uid(int dir_fd, uid_t owner) { FOREACH_ARRAY(i, de->entries, de->n_entries) { struct dirent *e = *i; - if (e->d_type != DT_REG) - continue; - if (!startswith(e->d_name, "i") || !endswith(e->d_name, ".userns")) continue; diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index e20b64291a..739973aa46 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -1805,16 +1805,13 @@ int pick_up_credentials(const PickUpCredential *table, size_t n_table_entry) { return log_error_errno(credential_dir_fd, "Failed to open credentials directory: %m"); _cleanup_free_ DirectoryEntries *des = NULL; - r = readdir_all(credential_dir_fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des); + r = readdir_all(credential_dir_fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_MUST_BE_REGULAR, &des); if (r < 0) return log_error_errno(r, "Failed to enumerate credentials: %m"); FOREACH_ARRAY(i, des->entries, des->n_entries) { struct dirent *de = *i; - if (de->d_type != DT_REG) - continue; - FOREACH_ARRAY(t, table, n_table_entry) { r = pick_up_credential_one(credential_dir_fd, de->d_name, t); if (r != 0) { diff --git a/src/shared/pcrextend-util.c b/src/shared/pcrextend-util.c index cf6eb7d1a5..5bd87e03c9 100644 --- a/src/shared/pcrextend-util.c +++ b/src/shared/pcrextend-util.c @@ -358,7 +358,7 @@ int pcrextend_imds_userdata_word(const struct iovec *data, char **ret) { /* We include both a hash of the complete user data, and a truncated version of the data in the word * we measure. The former protects the actual data, the latter is useful for debugging. */ - _cleanup_free_ char *hash = hexmem(SHA256_DIRECT(data->iov_base, data->iov_len), SHA256_DIGEST_SIZE); + _cleanup_free_ char *hash = sha256_direct_hex(data->iov_base, data->iov_len); if (!hash) return log_oom(); diff --git a/src/shared/tar-util.c b/src/shared/tar-util.c index 97c41adba9..e2603680bd 100644 --- a/src/shared/tar-util.c +++ b/src/shared/tar-util.c @@ -14,7 +14,6 @@ #include "chattr-util.h" #include "fd-util.h" #include "fs-util.h" -#include "hexdecoct.h" #include "iovec-util.h" #include "libarchive-util.h" #include "mountpoint-util.h" @@ -1184,7 +1183,7 @@ static int hardlink_lookup( else assert(d->have_unique_mount_id == (r > 0)); - m = hexmem(SHA256_DIRECT(handle->f_handle, handle->handle_bytes), SHA256_DIGEST_SIZE); + m = sha256_direct_hex(handle->f_handle, handle->handle_bytes); if (!m) return log_oom(); diff --git a/src/storage/storagectl.c b/src/storage/storagectl.c index 7e366c11a9..b899513616 100644 --- a/src/storage/storagectl.c +++ b/src/storage/storagectl.c @@ -348,16 +348,13 @@ static int verb_providers(int argc, char *argv[], uintptr_t data, void *userdata return log_error_errno(errno, "Failed to open '%s': %m", socket_path); } else { _cleanup_free_ DirectoryEntries *dentries = NULL; - r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &dentries); + r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_MUST_BE_SOCKET, &dentries); if (r < 0) return log_error_errno(r, "Failed to enumerate '%s': %m", socket_path); FOREACH_ARRAY(dp, dentries->entries, dentries->n_entries) { struct dirent *de = *dp; - if (de->d_type != DT_SOCK) - continue; - if (!storage_provider_name_is_valid(de->d_name)) continue; diff --git a/src/sysupdate/meson.build b/src/sysupdate/meson.build index da554ddcc3..ce1f4c14e7 100644 --- a/src/sysupdate/meson.build +++ b/src/sysupdate/meson.build @@ -2,6 +2,7 @@ systemd_sysupdate_sources = files( 'sysupdate-cache.c', + 'sysupdate-cleanup.c', 'sysupdate-feature.c', 'sysupdate-instance.c', 'sysupdate-partition.c', @@ -45,6 +46,11 @@ executables += [ 'objects' : ['systemd-sysupdate'], 'conditions' : ['ENABLE_SYSUPDATED'], }, + test_template + { + 'sources' : files('test-sysupdate-util.c'), + 'objects' : ['systemd-sysupdate'], + 'conditions' : ['ENABLE_SYSUPDATE'], + }, ] if conf.get('ENABLE_SYSUPDATED') == 1 diff --git a/src/sysupdate/sysupdate-cleanup.c b/src/sysupdate/sysupdate-cleanup.c new file mode 100644 index 0000000000..fac2a0b1b3 --- /dev/null +++ b/src/sysupdate/sysupdate-cleanup.c @@ -0,0 +1,450 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "alloc-util.h" +#include "chase.h" +#include "errno-util.h" +#include "fd-util.h" +#include "fs-util.h" +#include "log.h" +#include "path-util.h" +#include "recurse-dir.h" +#include "rm-rf.h" +#include "sha256.h" +#include "string-util.h" +#include "strv.h" +#include "sysupdate.h" +#include "sysupdate-cleanup.h" +#include "sysupdate-pattern.h" +#include "sysupdate-resource.h" +#include "sysupdate-transfer.h" +#include "sysupdate-util.h" + +/* This implements the "installdb", which is a simple database of directories + patterns that we ever + * installed something into, i.e. for any resource we ever considered "owned" by a transfer file. This is + * useful to automatically clean up "orphaned" files that used to be owned by a transfer file, but might no + * longer be in newer versions of those transfer files, or where the transfer files/components got + * removed/disabled altogether. + * + * This is ultimately just a per-component content-addressable database implemented via a special directory + * in /var/lib/systemd/sysupdate/ that carries symlinks to store the data. Whenever we drop a file into the + * system we create an entry in it. An entry symlink's filename is a SHA256 hash of the symlink's target. The + * target encodes the directory of the transfer file used, suffixed by the pattern used by the transfer + * file. If a transfer file lists multiple patterns, multiple entries are generated, one for each pattern. + * + * The on-disk layout hence looks roughly like this (for a component "foo" with a transfer file that has + * Path=/var/lib/machines/ and two patterns "image_@v.raw" and "image_@v.efi"): + * + * /var/lib/systemd/sysupdate/ + * └── installdb.foo/ + * ├── 8cbee6aa38b98811598118ebbc0eb4c1b7e479e7bfa4312c0b36edc765d1733b → /var/lib/machines/./image_@v.raw + * └── 042266dec8deae09c1e75f3d015734513b75a1daa38b4173c907b5345cf4ed41 → /var/lib/machines/./image_@v.efi + * + * (For the component-less case the directory is just called "installdb", without the ".foo" suffix.) The + * symlink names are the SHA256 hashes (in hex) of their respective targets, and the "/./" separates the + * directory part from the pattern part of the target. + * + * With this in place we have an always updated database of any file and pattern ever owned by any transfer + * file we operated on. When doing a clean-up run, we now iterate through all installdb directories (i.e + * every component ever installed), and all entries in them. We look for all files the entries match. We then + * check if the current set of transfer files also owns these files. If yes, we keep both those files and the + * installdb entry. If however no current transfer files own these files anymore, we first delete the files, + * and then the installdb entry, since it no longer matches any files. */ + +static int context_installdb_acquire_fd(Context *c, bool make) { + assert(c); + + if (c->installdb_fd >= 0) + return 0; + + _cleanup_free_ char *j = NULL; + const char *p; + if (c->component) { + j = strjoin("/var/lib/systemd/sysupdate/installdb.", c->component); + if (!j) + return log_oom(); + + p = j; + } else + p = "/var/lib/systemd/sysupdate/installdb"; + + ChaseFlags flags = CHASE_MUST_BE_DIRECTORY|CHASE_PREFIX_ROOT; + + if (make) + flags |= CHASE_MKDIR_0755; + + c->installdb_fd = chase_and_open( + p, + arg_root, + flags, + O_DIRECTORY|O_CLOEXEC|(make ? O_CREAT : 0), + /* ret_path= */ NULL); + if (c->installdb_fd == -ENOENT && !make) + return 0; + if (c->installdb_fd < 0) + return log_error_errno(c->installdb_fd, "Failed to open install database '%s%s': %m", empty_or_root(arg_root) ? "" : arg_root, p); + + return 1; +} + +static int installdb_make_names(const char *path, const char *pattern, char **ret_key, char **ret_value) { + assert(path); + assert(pattern); + + /* We'll generate a string from the location and the pattern that looks a lot like a path, but + * actually isn't, it's a path concatenated with a pattern. We separate both parts with /./. */ + _cleanup_free_ char *s = strjoin(path, "/./", pattern); + if (!s) + return log_oom(); + + _cleanup_free_ char *h = sha256_direct_hex(s, SIZE_MAX); + if (!h) + return log_oom(); + + if (ret_key) + *ret_key = TAKE_PTR(h); + + if (ret_value) + *ret_value = TAKE_PTR(s); + + return 0; +} + +int context_installdb_record( + Context *c, + const char *path, + char **patterns) { + + int r; + + assert(c); + assert(path); + + /* Creates installdb entries for the specified pairs of directory and pattern. This is called + * whenever we install a new file. */ + + if (strv_isempty(patterns)) + return 0; + + /* The provided path comes with arg_root prefixed. Strip it here again */ + const char *p = arg_root ? ASSERT_PTR(path_startswith(path, arg_root)) : path; + + r = context_installdb_acquire_fd(c, /* make= */ true); + if (r < 0) + return r; + + int ret = 0; + STRV_FOREACH(i, patterns) { + _cleanup_free_ char *key = NULL, *value = NULL; + r = installdb_make_names(p, *i, &key, &value); + if (r < 0) + return r; + + r = symlinkat_idempotent(value, c->installdb_fd, key, /* make_relative= */ false); + if (r < 0) + RET_GATHER(ret, log_warning_errno(r, "Failed to add '%s' in '%s' entry to install database: %m", *i, path)); + } + + return ret; +} + +static int context_is_path_currently_owned( + Context *c, + const char *path, + const char *relpath) { + + int r; + + assert(c); + assert(path); + assert(relpath); + + /* Checks if the there's a transfer file for the directoy 'path', and then if any of its patterns + * match 'relpath' */ + + FOREACH_ARRAY(_t, c->transfers, c->n_transfers) { + Transfer *t = *_t; + + if (!RESOURCE_IS_FILESYSTEM(t->target.type)) + continue; + + if (!path_equal(t->target.path, path)) + continue; + + /* OK, so we found a transfer that covers this directory. Now let's see if any of its patterns match */ + + r = pattern_match_many(t->target.patterns, relpath, /* ret= */ NULL); + if (r < 0) { + _cleanup_free_ char *cl = strv_join(t->target.patterns, "', '"); + if (!cl) + return log_oom(); + + return log_error_errno(r, "Failed to match patterns '%s' against '%s': %m", cl, relpath); + } + + if (IN_SET(r, PATTERN_MATCH_YES, PATTERN_MATCH_RETRY)) /* Yay, this path is pinned by this transfer file */ + return true; + + assert(r == PATTERN_MATCH_NO); + } + + return false; /* We found nothing! The path seems to be unowned. */ +} + +static int context_installdb_process_directory( + Context *c, + const char *path, /* The configured Path= in the original transfer file */ + const char *relpath, /* For recursive path matches the path we encountered so far */ + int dir_fd, + DirectoryEntries *de, + const char *pattern) { + + int r; + + assert(c); + assert(path); + assert(dir_fd >= 0); + assert(de); + assert(pattern); + + int ret = 0; + bool keep_installdb = false; + FOREACH_ARRAY(_d, de->entries, de->n_entries) { + const struct dirent *d = *_d; + + assert(IN_SET(d->d_type, DT_REG, DT_DIR)); /* caller must have filtered via readdir_all() RECURSE_DIR_MUST_BE_xyz flags already */ + + _cleanup_free_ char *j = NULL; + const char *p; + if (relpath) { + j = path_join(relpath, d->d_name); + if (!j) + return log_oom(); + + p = j; + } else + p = d->d_name; + + /* Let's see if this entry matches the pattern we recorded in the installdb? */ + r = pattern_match(pattern, p, /* ret= */ NULL); + if (r < 0) { + log_warning_errno(r, "Failed to match pattern '%s' against '%s', ignoring: %m", pattern, p); + /* Can't match, do not clean up */ + continue; + } + if (r == PATTERN_MATCH_NO) /* No match, do not clean up */ + continue; + if (r == PATTERN_MATCH_RETRY) { + /* Might match in a subdirectory */ + + if (d->d_type != DT_DIR) + continue; + + _cleanup_close_ int subdir_fd = RET_NERRNO(openat(dir_fd, d->d_name, O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW)); + if (subdir_fd == -ENOENT) + continue; + if (subdir_fd < 0) { + RET_GATHER(ret, log_warning_errno(subdir_fd, "Failed to open directory '%s', skipping: %m", p)); + continue; + } + + _cleanup_free_ DirectoryEntries *subde = NULL; + r = readdir_all(subdir_fd, RECURSE_DIR_ENSURE_TYPE|RECURSE_DIR_MUST_BE_DIRECTORY|RECURSE_DIR_MUST_BE_REGULAR, &subde); + if (r < 0) { + RET_GATHER(ret, log_error_errno(r, "Failed to enumerate resource path '%s': %m", p)); + continue; + } + + r = context_installdb_process_directory(c, path, p, subdir_fd, subde, pattern); + if (r < 0) + RET_GATHER(ret, r); + else + keep_installdb = keep_installdb || r; + continue; + } + + assert(r == PATTERN_MATCH_YES); + + /* Ah, we have a match, this is a candidate for cleanup. Let's see if any of the currently defined transfer files want to own it */ + + r = context_is_path_currently_owned(c, path, p); + if (r < 0) + return r; + if (r > 0) { + log_debug("Path '%s' is owned by current transfer files, keeping.", p); + + keep_installdb = true; /* We are keeping the file, let's also keep the installdb entry for it hence */ + continue; + } + + /* OK, we found an orphaned inode that was owned by a previous invocation, but is no longer + * owned by any of the current transfer files. Delete it. */ + + r = rm_rf_child(dir_fd, d->d_name, REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_CHMOD); + if (r < 0) { + if (r != -ENOENT) + RET_GATHER(ret, log_warning_errno(r, "Failed to remove '%s' which is no longer owned by any transfer files: %m", p)); + + continue; + } + + log_info("Successfully removed '%s' which is no longer owned by any transfer files.", p); + } + + /* Report back if there's a reason to keep the installdb entry for this directory */ + return ret < 0 ? ret : keep_installdb; +} + +static int context_installdb_process_entry( + Context *c, + const char *key, + const char *value) { + + int r; + + assert(c); + assert(key); + assert(value); + + _cleanup_free_ char *h = sha256_direct_hex(value, SIZE_MAX); + if (!h) + return log_oom(); + + if (!streq(key, h)) { + log_notice("Invalid hash of install database entry '%s' → '%s', expunging.", key, value); + return 0; + } + + const char *s = strstr(value, "/./"); + if (!s) { + log_notice("Malformed install database entry '%s' → '%s', expunging.", key, value); + return 0; + } + + _cleanup_free_ char *path = strndup(value, s - value); + if (!path) + return log_oom(); + + if (!path_is_absolute(path) || !path_is_normalized(path)) { + log_notice("Install database path '%s' of entry '%s' → '%s' is invalid, expunging database entry.", path, key, value); + return 0; + } + + const char *pattern = s + 3; + + /* NB: We set CHASE_PROHIBIT_SYMLINKS because the path was normalized by the writer of the entry + * already, and if it isn't anymore, then something is fishy. */ + _cleanup_close_ int dir_fd = chase_and_open(path, arg_root, CHASE_MUST_BE_DIRECTORY|CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, O_DIRECTORY|O_CLOEXEC, /* ret_path= */ NULL); + if (dir_fd == -ENOENT) { + log_debug("Install database path '%s' does not exist, expunging database entry.", path); + return 0; + } + if (dir_fd < 0) + return log_error_errno(dir_fd, "Failed to open resource path '%s': %m", path); + + _cleanup_free_ DirectoryEntries *de = NULL; + r = readdir_all(dir_fd, RECURSE_DIR_ENSURE_TYPE|RECURSE_DIR_MUST_BE_DIRECTORY|RECURSE_DIR_MUST_BE_REGULAR, &de); + if (r < 0) + return log_error_errno(r, "Failed to enumerate resource path '%s': %m", path); + + return context_installdb_process_directory(c, path, /* relpath= */ NULL, dir_fd, de, pattern); +} + +int installdb_cleanup_component(const char *node, const char *component) { + int r; + + _cleanup_(context_freep) Context* context = NULL; + r = context_make_offline( + &context, + node, + component, + /* read_definitions_flags= */ 0); + if (r < 0) + return r; + + r = context_installdb_acquire_fd(context, /* make= */ false); + if (r < 0) + return r; + if (r == 0) { + log_debug("Not cleaning up component '%s', install database is empty.", strna(component)); + return 0; + } + + _cleanup_free_ DirectoryEntries *de = NULL; + r = readdir_all(context->installdb_fd, RECURSE_DIR_ENSURE_TYPE|RECURSE_DIR_MUST_BE_SYMLINK, &de); + if (r < 0) + return log_error_errno(r, "Failed to enumerate install database for component '%s': %m", strna(component)); + + int ret = 0; + FOREACH_ARRAY(_d, de->entries, de->n_entries) { + const struct dirent *d = *_d; + + _cleanup_free_ char *v = NULL; + r = readlinkat_malloc(context->installdb_fd, d->d_name, &v); + if (r == -ENOENT) + continue; + if (r < 0) { + log_warning_errno(r, "Failed to read symlink '%s', ignoring: %m", d->d_name); + continue; + } + + r = context_installdb_process_entry(context, d->d_name, v); + if (r < 0) { + RET_GATHER(ret, r); + continue; + } + if (r > 0) /* Still good, keep installdb entry */ + continue; + + r = RET_NERRNO(unlinkat(context->installdb_fd, d->d_name, /* flags= */ 0)); + if (r < 0 && r != -ENOENT) + RET_GATHER(ret, log_warning_errno(r, "Failed to remove install database entry '%s': %m", d->d_name)); + } + + return ret; +} + +int installdb_list_components(char ***ret) { + int r; + + assert(ret); + + _cleanup_close_ int dir_fd = chase_and_open( + "/var/lib/systemd/sysupdate", + arg_root, + CHASE_MUST_BE_DIRECTORY|CHASE_PREFIX_ROOT, + O_DIRECTORY|O_CLOEXEC, + /* ret_path= */ NULL); + if (dir_fd == -ENOENT) { + *ret = NULL; + return 0; + } + if (dir_fd < 0) + return log_error_errno(dir_fd, "Failed to open '/var/lib/systemd/sysupdate/': %m"); + + _cleanup_free_ DirectoryEntries *de = NULL; + r = readdir_all(dir_fd, RECURSE_DIR_ENSURE_TYPE|RECURSE_DIR_MUST_BE_DIRECTORY, &de); + if (r < 0) + return log_error_errno(r, "Failed to enumerate installdb directory '/var/lib/systemd/sysupdate/': %m"); + + _cleanup_strv_free_ char **l = NULL; + FOREACH_ARRAY(_d, de->entries, de->n_entries) { + const struct dirent *d = *_d; + + const char *e = startswith(d->d_name, "installdb."); + if (!e) + continue; + + if (!component_name_valid(e)) + continue; + + if (strv_extend(&l, e) < 0) + return log_oom(); + } + + strv_sort_uniq(l); + *ret = TAKE_PTR(l); + return 0; +} diff --git a/src/sysupdate/sysupdate-cleanup.h b/src/sysupdate/sysupdate-cleanup.h new file mode 100644 index 0000000000..98fbb0a83b --- /dev/null +++ b/src/sysupdate/sysupdate-cleanup.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sysupdate-forward.h" + +int context_installdb_record(Context *c, const char *path, char **patterns); + +int installdb_cleanup_component(const char *node, const char *component); +int installdb_list_components(char ***ret); diff --git a/src/sysupdate/sysupdate-forward.h b/src/sysupdate/sysupdate-forward.h index a8659d756b..f32cb55d32 100644 --- a/src/sysupdate/sysupdate-forward.h +++ b/src/sysupdate/sysupdate-forward.h @@ -6,7 +6,9 @@ #include "shared-forward.h" /* IWYU pragma: export */ typedef struct Context Context; -typedef struct PartitionInfo PartitionInfo; -typedef struct Resource Resource; typedef struct Instance Instance; typedef struct InstanceMetadata InstanceMetadata; +typedef struct PartitionInfo PartitionInfo; +typedef struct Resource Resource; +typedef struct Transfer Transfer; +typedef struct UpdateSet UpdateSet; diff --git a/src/sysupdate/sysupdate-pattern.c b/src/sysupdate/sysupdate-pattern.c index e1cc3dc466..f06265c698 100644 --- a/src/sysupdate/sysupdate-pattern.c +++ b/src/sysupdate/sysupdate-pattern.c @@ -458,7 +458,7 @@ int pattern_match_many(char **patterns, const char *s, InstanceMetadata *ret) { r = pattern_match(*p, s, &found); if (r < 0) return r; - if (r > 0) { + if (r != PATTERN_MATCH_NO) { if (ret) { *ret = found; found = (InstanceMetadata) INSTANCE_METADATA_NULL; diff --git a/src/sysupdate/sysupdate-resource.c b/src/sysupdate/sysupdate-resource.c index abc63960df..275d8d7c15 100644 --- a/src/sysupdate/sysupdate-resource.c +++ b/src/sysupdate/sysupdate-resource.c @@ -158,6 +158,8 @@ static int resource_load_from_directory_recursive( return log_oom(); r = pattern_match_many(rr->patterns, rel_joined_for_matching, &extracted_fields); + if (r < 0) + return log_error_errno(r, "Failed to match pattern: %m"); if (r == PATTERN_MATCH_RETRY) { _cleanup_closedir_ DIR *subdir = NULL; @@ -170,10 +172,7 @@ static int resource_load_from_directory_recursive( 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) + } else if (r == PATTERN_MATCH_NO) continue; if (de->d_type == DT_DIR && m != S_IFDIR) diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c index bb98491592..4d698d620f 100644 --- a/src/sysupdate/sysupdate-transfer.c +++ b/src/sysupdate/sysupdate-transfer.c @@ -35,6 +35,7 @@ #include "strv.h" #include "sync-util.h" #include "sysupdate.h" +#include "sysupdate-cleanup.h" #include "sysupdate-feature.h" #include "sysupdate-instance.h" #include "sysupdate-pattern.h" @@ -1672,6 +1673,8 @@ int transfer_install_instance( resource_type_to_string(t->target.type)); t->temporary_pending_path = mfree(t->temporary_pending_path); + + (void) context_installdb_record(t->context, t->target.path, t->target.patterns); } if (t->final_partition_label) { diff --git a/src/sysupdate/sysupdate-util.c b/src/sysupdate/sysupdate-util.c index 1aba63cf78..b3ed72577e 100644 --- a/src/sysupdate/sysupdate-util.c +++ b/src/sysupdate/sysupdate-util.c @@ -4,8 +4,14 @@ #include "bus-error.h" #include "bus-locator.h" +#include "conf-files.h" +#include "constants.h" #include "log.h" #include "login-util.h" +#include "path-util.h" +#include "set.h" +#include "string-util.h" +#include "strv.h" #include "sysupdate-util.h" int reboot_now(void) { @@ -24,3 +30,72 @@ int reboot_now(void) { return 0; } + +bool component_name_valid(const char *c) { + /* See if the specified string enclosed in the directory prefix+suffix would be a valid file name */ + + if (!string_is_safe(c, STRING_FILENAME_PART)) + return false; + + /* Stack allocation is safe, since STRING_FILENAME_PART includes a length check */ + const char *j = strjoina("sysupdate.", c, ".d"); + + return filename_is_valid(j); +} + +int get_component_list(const char *root, char ***ret) { + int r; + + assert(ret); + + ConfFile **directories = NULL; + size_t n_directories = 0; + CLEANUP_ARRAY(directories, n_directories, conf_file_free_array); + + r = conf_files_list_strv_full( + ".d", + root, + CONF_FILES_DIRECTORY|CONF_FILES_WARN, + (const char * const *) CONF_PATHS_STRV(""), + &directories, + &n_directories); + if (r < 0) + return r; + + _cleanup_set_free_ Set *names = NULL; + + FOREACH_ARRAY(i, directories, n_directories) { + ConfFile *e = *i; + + const char *s = startswith(e->filename, "sysupdate."); + if (!s) + continue; + + const char *a = endswith(s, ".d"); + if (!a) + continue; + + if (a == s) + continue; + + _cleanup_free_ char *n = strndup(s, a - s); + if (!n) + return log_oom(); + + if (!component_name_valid(n)) + continue; + + r = set_ensure_consume(&names, &string_hash_ops_free, TAKE_PTR(n)); + if (r < 0 && r != -EEXIST) + return r; + } + + _cleanup_strv_free_ char **z = set_to_strv(&names); + if (!z) + return -ENOMEM; + + strv_sort(z); + + *ret = TAKE_PTR(z); + return 0; +} diff --git a/src/sysupdate/sysupdate-util.h b/src/sysupdate/sysupdate-util.h index 8504fd24b7..07d9def708 100644 --- a/src/sysupdate/sysupdate-util.h +++ b/src/sysupdate/sysupdate-util.h @@ -1,8 +1,12 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ - #pragma once +#include + int reboot_now(void); -#define SD_SYSUPDATE_OFFLINE (UINT64_C(1) << 0) +#define SD_SYSUPDATE_OFFLINE (UINT64_C(1) << 0) #define SD_SYSUPDATE_FLAGS_ALL (SD_SYSUPDATE_OFFLINE) + +bool component_name_valid(const char *c); +int get_component_list(const char *root, char ***ret); diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index cd5850bd38..a1d292f0eb 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -8,8 +8,12 @@ #include "conf-files.h" #include "constants.h" #include "dissect-image.h" +#include "errno-util.h" +#include "fd-util.h" #include "format-table.h" #include "glyph-util.h" +#include "hashmap.h" +#include "help-util.h" #include "hexdecoct.h" #include "image-policy.h" #include "loop-util.h" @@ -20,20 +24,18 @@ #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 "specifier.h" #include "string-util.h" #include "strv.h" #include "sysupdate.h" +#include "sysupdate-cleanup.h" #include "sysupdate-feature.h" #include "sysupdate-instance.h" #include "sysupdate-transfer.h" #include "sysupdate-update-set.h" #include "sysupdate-util.h" -#include "utf8.h" #include "verbs.h" static char *arg_definitions = NULL; @@ -46,6 +48,7 @@ char *arg_root = NULL; static char *arg_image = NULL; static bool arg_reboot = false; static char *arg_component = NULL; +static bool arg_component_all = false; static int arg_verify = -1; static ImagePolicy *arg_image_policy = NULL; static bool arg_offline = false; @@ -64,27 +67,12 @@ const Specifier specifier_table[] = { {} }; -typedef struct Context { - Transfer **transfers; - size_t n_transfers; - - Transfer **disabled_transfers; - size_t n_disabled_transfers; - - Hashmap *features; /* Defined features, keyed by ID */ - - 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) { +Context* context_free(Context *c) { if (!c) return NULL; + free(c->component); + FOREACH_ARRAY(tr, c->transfers, c->n_transfers) transfer_free(*tr); free(c->transfers); @@ -101,14 +89,21 @@ static Context* context_free(Context *c) { hashmap_free(c->web_cache); + safe_close(c->installdb_fd); + 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); + Context *c = new(Context, 1); + if (!c) + return NULL; + + *c = (Context) { + .installdb_fd = -EBADF, + }; + + return c; } static DEFINE_POINTER_ARRAY_FREE_FUNC(Transfer*, transfer_free); @@ -171,11 +166,6 @@ static int read_definitions( return 0; } -typedef enum ReadDefinitionsFlags { - READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS = 1 << 0, - READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS = 1 << 1, -} ReadDefinitionsFlags; - static int context_read_definitions(Context *c, const char* node, ReadDefinitionsFlags flags) { _cleanup_strv_free_ char **dirs = NULL; int r; @@ -184,7 +174,7 @@ static int context_read_definitions(Context *c, const char* node, ReadDefinition if (arg_definitions) dirs = strv_new(arg_definitions); - else if (arg_component) { + else if (c->component) { char **l = CONF_PATHS_STRV(""); size_t i = 0; @@ -195,7 +185,7 @@ static int context_read_definitions(Context *c, const char* node, ReadDefinition STRV_FOREACH(dir, l) { char *j; - j = strjoin(*dir, "sysupdate.", arg_component, ".d"); + j = strjoin(*dir, "sysupdate.", c->component, ".d"); if (!j) return log_oom(); @@ -251,10 +241,10 @@ static int context_read_definitions(Context *c, const char* node, ReadDefinition if (FLAGS_SET(flags, READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS) && c->n_transfers + (FLAGS_SET(flags, READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS) ? 0 : c->n_disabled_transfers) == 0) { - if (arg_component) + if (c->component) return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No transfer definitions for component '%s' found.", - arg_component); + c->component); return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No transfer definitions found."); @@ -931,7 +921,11 @@ static int context_vacuum( return 0; } -static int context_make_offline(Context **ret, const char *node, ReadDefinitionsFlags read_definitions_flags) { +int context_make_offline( + Context **ret, + const char *node, + const char *component, + ReadDefinitionsFlags read_definitions_flags) { _cleanup_(context_freep) Context* context = NULL; int r; @@ -944,6 +938,10 @@ static int context_make_offline(Context **ret, const char *node, ReadDefinitions if (!context) return log_oom(); + r = free_and_strdup_warn(&context->component, component); + if (r < 0) + return r; + r = context_read_definitions(context, node, read_definitions_flags); if (r < 0) return r; @@ -956,7 +954,11 @@ static int context_make_offline(Context **ret, const char *node, ReadDefinitions return 0; } -static int context_make_online(Context **ret, const char *node) { +static int context_make_online( + Context **ret, + const char *node, + const char *component) { + _cleanup_(context_freep) Context* context = NULL; int r; @@ -965,8 +967,11 @@ static int context_make_online(Context **ret, const char *node) { /* Like context_make_offline(), but also communicates with the update source looking for new * versions (as long as --offline is not specified on the command line). */ - r = context_make_offline(&context, node, - READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS | READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS); + r = context_make_offline( + &context, + node, + component, + READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS); if (r < 0) return r; @@ -1297,11 +1302,17 @@ static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) { assert(argc <= 2); version = argc >= 2 ? argv[1] : NULL; + if (arg_component_all) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + 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); + r = context_make_online( + &context, + loop_device ? loop_device->node : NULL, + arg_component); if (r < 0) return r; @@ -1368,12 +1379,18 @@ static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata assert(argc <= 2); feature_id = argc >= 2 ? argv[1] : NULL; + if (arg_component_all) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + r = process_image(/* ro= */ true, &mounted_dir, &loop_device); if (r < 0) return r; - r = context_make_offline(&context, loop_device ? loop_device->node : NULL, - READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS); + r = context_make_offline( + &context, + loop_device ? loop_device->node : NULL, + arg_component, + READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS); if (r < 0) return r; @@ -1501,11 +1518,17 @@ static int verb_check_new(int argc, char *argv[], uintptr_t _data, void *userdat assert(argc <= 1); + if (arg_component_all) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + 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); + r = context_make_online( + &context, + loop_device ? loop_device->node : NULL, + arg_component); if (r < 0) return r; @@ -1551,6 +1574,9 @@ static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flag assert(argc <= 2); version = argc >= 2 ? argv[1] : NULL; + if (arg_component_all) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + if (arg_instances_max < 2) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --instances-max argument must be >= 2 while updating"); @@ -1569,7 +1595,10 @@ static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flag if (r < 0) return r; - r = context_make_online(&context, loop_device ? loop_device->node : NULL); + r = context_make_online( + &context, + loop_device ? loop_device->node : NULL, + arg_component); if (r < 0) return r; @@ -1633,6 +1662,9 @@ static int verb_vacuum(int argc, char *argv[], uintptr_t _data, void *userdata) assert(argc <= 1); + if (arg_component_all) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + if (arg_instances_max < 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --instances-max argument must be >= 1 while vacuuming"); @@ -1641,8 +1673,11 @@ static int verb_vacuum(int argc, char *argv[], uintptr_t _data, void *userdata) if (r < 0) return r; - r = context_make_offline(&context, loop_device ? loop_device->node : NULL, - READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS); + r = context_make_offline( + &context, + loop_device ? loop_device->node : NULL, + arg_component, + READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS); if (r < 0) return r; @@ -1664,12 +1699,15 @@ static int verb_pending_or_reboot(int argc, char *argv[], uintptr_t _data, void return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --root=/--image= switches may not be combined with the '%s' operation.", argv[0]); - if (arg_component) + if (arg_component || arg_component_all) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "The --component= switch may not be combined with the '%s' operation, which only applies to the booted OS version.", argv[0]); + "The --component= and --component-all switches may not be combined with the '%s' operation, which only applies to the booted OS version.", argv[0]); - r = context_make_offline(&context, /* node= */ NULL, - READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS | READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS); + r = context_make_offline( + &context, + /* node= */ NULL, + arg_component, + READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS); if (r < 0) return r; @@ -1711,105 +1749,47 @@ static int verb_pending_or_reboot(int argc, char *argv[], uintptr_t _data, void 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); -} - VERB_NOARG(verb_components, "components", "Show list of components"); static int verb_components(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(context_freep) Context* context = NULL; _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; - _cleanup_set_free_ Set *names = NULL; bool has_default_component = false; int r; assert(argc <= 1); + if (arg_component_all) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + 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, 0); + r = context_make_offline( + &context, + loop_device ? loop_device->node : NULL, + arg_component, + /* read_definitions_flags= */ 0); if (r < 0) return r; - ConfFile **directories = NULL; - size_t n_directories = 0; - - CLEANUP_ARRAY(directories, n_directories, conf_file_free_array); - - r = conf_files_list_strv_full(".d", arg_root, CONF_FILES_DIRECTORY|CONF_FILES_WARN, - (const char * const *) CONF_PATHS_STRV(""), &directories, &n_directories); + _cleanup_strv_free_ char **z = NULL; + r = get_component_list(arg_root, &z); if (r < 0) - return log_error_errno(r, "Failed to enumerate directories: %m"); - - FOREACH_ARRAY(i, directories, n_directories) { - ConfFile *e = *i; - - if (streq(e->filename, "sysupdate.d")) { - continue; - } - - const char *s = startswith(e->filename, "sysupdate."); - if (!s) - continue; - - const char *a = endswith(s, ".d"); - if (!a) - continue; - - _cleanup_free_ char *n = strndup(s, a - s); - if (!n) - return log_oom(); - - r = component_name_valid(n); - if (r < 0) - return log_error_errno(r, "Unable to validate component name '%s': %m", n); - if (r == 0) - continue; - - r = set_ensure_put(&names, &string_hash_ops_free, n); - if (r < 0 && r != -EEXIST) - return log_error_errno(r, "Failed to add component '%s' to set: %m", n); - TAKE_PTR(n); - } + return log_error_errno(r, "Failed to enumerate components: %m"); /* Does the system have at least one transfer file in /etc/sysupdate.d, which can be considered a * TARGET_HOST? See target_get_argument() in sysupdated.c */ has_default_component = (!arg_definitions && - !arg_component && + !context->component && !arg_root && !arg_image && context->n_transfers > 0); - /* We use simple free() rather than strv_free() here, since set_free() will free the strings for us */ - _cleanup_free_ char **z = set_get_strv(names); - if (!z) - return log_oom(); - - strv_sort(z); - if (!sd_json_format_enabled(arg_json_format_flags)) { - if (!has_default_component && set_isempty(names)) { + if (!has_default_component && strv_isempty(z)) { log_info("No components defined."); return 0; } @@ -1836,14 +1816,39 @@ static int verb_components(int argc, char *argv[], uintptr_t _data, void *userda return 0; } -static int help(void) { - _cleanup_free_ char *link = NULL; - _cleanup_(table_unrefp) Table *common_options = NULL, *options = NULL, *verbs = NULL; +VERB_NOARG(verb_cleanup, "cleanup", "Clean up orphaned files"); +static int verb_cleanup(int argc, char *argv[], uintptr_t _data, void *userdata) { int r; - r = terminal_urlify_man("systemd-sysupdate", "8", &link); + assert(argc <= 1); + + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; + r = process_image(/* ro= */ false, &mounted_dir, &loop_device); if (r < 0) - return log_oom(); + return r; + + const char *node = loop_device ? loop_device->node : NULL; + + int ret = 0; + RET_GATHER(ret, installdb_cleanup_component(node, arg_component)); + + if (arg_component_all) { + _cleanup_strv_free_ char **z = NULL; + r = installdb_list_components(&z); + if (r < 0) + return log_error_errno(r, "Failed to enumerate components: %m"); + + STRV_FOREACH(i, z) + RET_GATHER(ret, installdb_cleanup_component(node, *i)); + } + + return ret; +} + +static int help(void) { + _cleanup_(table_unrefp) Table *common_options = NULL, *options = NULL, *verbs = NULL; + int r; r = verbs_get_help_table(&verbs); if (r < 0) @@ -1859,13 +1864,10 @@ static int help(void) { (void) table_sync_column_widths(0, verbs, common_options, options); - printf("%s [OPTIONS...] [VERSION]\n" - "\n%sUpdate OS images.%s\n" - "\n%sCommands:%s\n", - program_invocation_short_name, - ansi_highlight(), ansi_normal(), - ansi_underline(), ansi_normal()); + help_cmdline("[OPTIONS…] [VERSION]"); + help_abstract("Update OS images."); + help_section("Commands"); r = table_print_or_warn(verbs); if (r < 0) return r; @@ -1874,12 +1876,12 @@ static int help(void) { if (r < 0) return r; - printf("\n%sOptions:%s\n", ansi_underline(), ansi_normal()); + help_section("Options"); r = table_print_or_warn(options); if (r < 0) return r; - printf("\nSee the %s for details.\n", link); + help_man_page_reference("systemd-sysupdate", "8"); return 0; } @@ -1909,19 +1911,24 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { "Select component to update"): if (isempty(opts.arg)) { arg_component = mfree(arg_component); + arg_component_all = false; break; } - r = component_name_valid(opts.arg); - if (r < 0) - return log_error_errno(r, "Failed to determine if component name is valid: %m"); - if (r == 0) + if (!component_name_valid(opts.arg)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", opts.arg); r = free_and_strdup_warn(&arg_component, opts.arg); if (r < 0) return r; + arg_component_all = false; + break; + + OPTION('A', "component-all", NULL, "Process all components"): + + arg_component = mfree(arg_component); + arg_component_all = true; break; OPTION_LONG("definitions", "DIR", diff --git a/src/sysupdate/sysupdate.h b/src/sysupdate/sysupdate.h index 092aeb8771..b925d4f5a2 100644 --- a/src/sysupdate/sysupdate.h +++ b/src/sysupdate/sysupdate.h @@ -4,6 +4,37 @@ #include "specifier.h" #include "sysupdate-forward.h" +typedef struct Context { + char *component; + + Transfer **transfers; + size_t n_transfers; + + Transfer **disabled_transfers; + size_t n_disabled_transfers; + + Hashmap *features; /* Defined features, keyed by ID */ + + UpdateSet **update_sets; + size_t n_update_sets; + + UpdateSet *newest_installed, *candidate; + + Hashmap *web_cache; /* Cache for downloaded resources, keyed by URL */ + + int installdb_fd; +} Context; + +Context* context_free(Context *c); +DEFINE_TRIVIAL_CLEANUP_FUNC(Context*, context_free); + +typedef enum ReadDefinitionsFlags { + READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS = 1 << 0, + READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS = 1 << 1, +} ReadDefinitionsFlags; + +int context_make_offline(Context **ret, const char *node, const char *component, ReadDefinitionsFlags read_definitions_flags); + extern bool arg_sync; extern uint64_t arg_instances_max; extern char *arg_root; diff --git a/src/sysupdate/test-sysupdate-util.c b/src/sysupdate/test-sysupdate-util.c new file mode 100644 index 0000000000..b9a5123e05 --- /dev/null +++ b/src/sysupdate/test-sysupdate-util.c @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sysupdate-util.h" +#include "tests.h" + +TEST(component_name_valid) { + /* Valid component names: anything that turns "sysupdate..d" into a valid filename. */ + ASSERT_TRUE(component_name_valid("foo")); + ASSERT_TRUE(component_name_valid("foo-bar")); + ASSERT_TRUE(component_name_valid("foo.bar")); + ASSERT_TRUE(component_name_valid("foo_bar_baz")); + ASSERT_TRUE(component_name_valid("0815")); + ASSERT_TRUE(component_name_valid("über")); /* valid UTF-8 is fine */ + + /* Invalid: empty, slashes, control characters, invalid UTF-8. */ + ASSERT_FALSE(component_name_valid("")); + ASSERT_FALSE(component_name_valid("foo/bar")); + ASSERT_FALSE(component_name_valid("/foo")); + ASSERT_FALSE(component_name_valid("foo/")); + ASSERT_FALSE(component_name_valid("foo\tbar")); + ASSERT_FALSE(component_name_valid("foo\nbar")); + ASSERT_FALSE(component_name_valid("foo\x7f")); + ASSERT_FALSE(component_name_valid("\xff")); /* not valid UTF-8 */ +} + +DEFINE_TEST_MAIN(LOG_INFO); diff --git a/src/test/test-recurse-dir.c b/src/test/test-recurse-dir.c index b29a7eab9d..453b89d172 100644 --- a/src/test/test-recurse-dir.c +++ b/src/test/test-recurse-dir.c @@ -2,14 +2,17 @@ #include #include +#include #include "fd-util.h" #include "log.h" #include "recurse-dir.h" +#include "rm-rf.h" #include "stat-util.h" #include "strv.h" #include "tests.h" #include "time-util.h" +#include "tmpfile-util.h" static char **list_nftw = NULL; @@ -119,6 +122,71 @@ static int recurse_dir_callback( return RECURSE_DIR_CONTINUE; } +static void assert_entries(DirectoryEntries *de, char **expected) { + ASSERT_NOT_NULL(de); + + /* Verifies that the directory entries enumerated in 'de' are exactly the ones listed in + * 'expected' (order is irrelevant). */ + + ASSERT_EQ(de->n_entries, strv_length(expected)); + + FOREACH_ARRAY(i, de->entries, de->n_entries) + ASSERT_TRUE(strv_contains(expected, (*i)->d_name)); + + STRV_FOREACH(e, expected) { + bool found = false; + + FOREACH_ARRAY(i, de->entries, de->n_entries) + if (streq((*i)->d_name, *e)) { + found = true; + break; + } + + ASSERT_TRUE(found); + } +} + +static void check_readdir_all(int tfd, RecurseDirFlags flags, char **expected) { + _cleanup_free_ DirectoryEntries *de = NULL; + _cleanup_close_ int fd = -EBADF; + + /* readdir_all() consumes the directory fd offset, hence reopen a fresh fd for each enumeration. */ + ASSERT_OK(fd = fd_reopen(tfd, O_DIRECTORY|O_CLOEXEC)); + ASSERT_OK(readdir_all(fd, flags, &de)); + assert_entries(de, expected); +} + +static void test_must_be_flags(void) { + _cleanup_(rm_rf_physical_and_freep) char *t = NULL; + _cleanup_close_ int tfd = -EBADF, reg_fd = -EBADF; + + log_info("/* %s */", __func__); + + /* Populate a temporary directory with one entry of each interesting type and verify that the + * RECURSE_DIR_MUST_BE_* flags select exactly the right subset. */ + + ASSERT_OK(tfd = mkdtemp_open(NULL, O_DIRECTORY|O_CLOEXEC, &t)); + + ASSERT_OK_ERRNO(mkdirat(tfd, "dir", 0777)); + ASSERT_OK_ERRNO(reg_fd = openat(tfd, "reg", O_CREAT|O_EXCL|O_WRONLY|O_CLOEXEC, 0666)); + reg_fd = safe_close(reg_fd); + ASSERT_OK_ERRNO(symlinkat("reg", tfd, "lnk")); + ASSERT_OK_ERRNO(mknodat(tfd, "sock", S_IFSOCK|0666, 0)); + + /* Without any MUST_BE flag we get all four entries. */ + check_readdir_all(tfd, 0, STRV_MAKE("dir", "reg", "lnk", "sock")); + + /* A single MUST_BE flag selects exactly the entries of the matching type. */ + check_readdir_all(tfd, RECURSE_DIR_MUST_BE_DIRECTORY, STRV_MAKE("dir")); + check_readdir_all(tfd, RECURSE_DIR_MUST_BE_REGULAR, STRV_MAKE("reg")); + check_readdir_all(tfd, RECURSE_DIR_MUST_BE_SYMLINK, STRV_MAKE("lnk")); + check_readdir_all(tfd, RECURSE_DIR_MUST_BE_SOCKET, STRV_MAKE("sock")); + + /* The flags may be combined, in which case we get the union of the matching entries. */ + check_readdir_all(tfd, RECURSE_DIR_MUST_BE_REGULAR|RECURSE_DIR_MUST_BE_SYMLINK, STRV_MAKE("reg", "lnk")); + check_readdir_all(tfd, RECURSE_DIR_MUST_BE_DIRECTORY|RECURSE_DIR_MUST_BE_SOCKET, STRV_MAKE("dir", "sock")); +} + int main(int argc, char *argv[]) { _cleanup_strv_free_ char **list_recurse_dir = NULL; const char *p; @@ -128,6 +196,8 @@ int main(int argc, char *argv[]) { log_show_color(true); test_setup_logging(LOG_INFO); + test_must_be_flags(); + if (argc > 1) p = argv[1]; else diff --git a/src/test/test-string-util.c b/src/test/test-string-util.c index 9e8776399b..51f63963a9 100644 --- a/src/test/test-string-util.c +++ b/src/test/test-string-util.c @@ -1639,6 +1639,20 @@ TEST(string_is_safe) { ASSERT_TRUE(string_is_safe("a\nb", STRING_ALLOW_NEWLINES)); ASSERT_FALSE(string_is_safe("a\nb", STRING_ALLOW_NEWLINES | STRING_DISALLOW_WHITESPACE)); + /* STRING_FILENAME_PART: like STRING_FILENAME, but "." and ".." are accepted; '/' still rejected. */ + ASSERT_TRUE(string_is_safe("hello", STRING_FILENAME_PART)); + ASSERT_TRUE(string_is_safe("hello.txt", STRING_FILENAME_PART)); + ASSERT_TRUE(string_is_safe("...", STRING_FILENAME_PART)); + ASSERT_TRUE(string_is_safe(".hidden", STRING_FILENAME_PART)); + ASSERT_TRUE(string_is_safe(".", STRING_FILENAME_PART)); /* accepted, unlike STRING_FILENAME */ + ASSERT_TRUE(string_is_safe("..", STRING_FILENAME_PART)); /* accepted, unlike STRING_FILENAME */ + ASSERT_FALSE(string_is_safe("", STRING_FILENAME_PART)); /* empty still rejected by default */ + ASSERT_TRUE(string_is_safe("", STRING_FILENAME_PART | STRING_ALLOW_EMPTY)); /* ... unless explicitly allowed */ + ASSERT_FALSE(string_is_safe("/", STRING_FILENAME_PART)); + ASSERT_FALSE(string_is_safe("/foo", STRING_FILENAME_PART)); + ASSERT_FALSE(string_is_safe("foo/bar", STRING_FILENAME_PART)); + ASSERT_FALSE(string_is_safe("foo/", STRING_FILENAME_PART)); + /* Pairwise combinations. */ ASSERT_TRUE(string_is_safe("", STRING_ALLOW_EMPTY | STRING_ASCII)); ASSERT_FALSE(string_is_safe("über", STRING_ALLOW_EMPTY | STRING_ASCII)); diff --git a/test/units/TEST-72-SYSUPDATE.sh b/test/units/TEST-72-SYSUPDATE.sh index dc2b3fbb64..10c0f335f6 100755 --- a/test/units/TEST-72-SYSUPDATE.sh +++ b/test/units/TEST-72-SYSUPDATE.sh @@ -643,4 +643,219 @@ set -e [[ $rc -ne 134 ]] grep -F "Manifest hash at line 1 decoded to 31 bytes" "$WORKDIR/malformed-manifest/check-new.log" >/dev/null +# Check the "cleanup" verb and the underlying install database. A successful +# update must record an install database entry for every transfer that installs +# into the file system, and those entries must cover all installed resources +# (regular files as well as directories). Once the transfer file owning some +# resources is removed, "systemd-sysupdate cleanup" must delete the now-orphaned +# resources (and their install database entries), while leaving resources that +# are still owned by a transfer file untouched. +INSTALLDB="/var/lib/systemd/sysupdate/installdb" +CLEANUP="$WORKDIR/cleanup" +rm -rf "$CONFIGDIR" "$INSTALLDB" "$CLEANUP" +mkdir -p "$CONFIGDIR" "$CLEANUP/source" "$CLEANUP/target" + +# The "alpha" transfer installs plain regular files, while the "beta" transfer +# installs whole directories (each populated with a couple of files), to exercise +# the recursive removal of orphaned directory resources during cleanup. +cleanup_new_version() { + local version="${1:?}" + echo "$RANDOM" >"$CLEANUP/source/alpha-$version.bin" + rm -rf "$CLEANUP/source/beta-$version" + mkdir -p "$CLEANUP/source/beta-$version" + echo "$RANDOM" >"$CLEANUP/source/beta-$version/one.txt" + echo "$RANDOM" >"$CLEANUP/source/beta-$version/two.txt" + (cd "$CLEANUP/source" && sha256sum alpha-* >SHA256SUMS) +} + +# Number of install database entries (symlinks) currently recorded. +installdb_count() { + if [[ -d "$INSTALLDB" ]]; then + find "$INSTALLDB" -mindepth 1 -maxdepth 1 -type l | wc -l + else + echo 0 + fi +} + +# Assert that every resource (file or directory) currently installed in the +# target directory is covered by at least one install database entry (i.e. its +# name matches a recorded pattern that points at the target directory). +assert_installdb_covers_target() { + local f base link tgt path pattern glob covered + for f in "$CLEANUP/target"/*; do + [[ -e "$f" ]] || continue + base="$(basename "$f")" + covered=0 + while read -r link; do + tgt="$(readlink "$link")" + # Entries are stored as "/./". + path="${tgt%%/./*}" + pattern="${tgt#*/./}" + # Translate the sysupdate pattern into a shell glob (only @v is used here). + glob="${pattern//@v/*}" + # shellcheck disable=SC2053 + if [[ "$path" == "$CLEANUP/target" && "$base" == $glob ]]; then + covered=1 + break + fi + done < <(find "$INSTALLDB" -mindepth 1 -maxdepth 1 -type l) + [[ "$covered" -eq 1 ]] || { echo "Installed resource '$f' not covered by install database" >&2; exit 1; } + done +} + +# Verify the installed beta- directory matches its source. +verify_beta_synced() { + local version="${1:?}" + test -d "$CLEANUP/target/beta-$version" + cmp "$CLEANUP/source/beta-$version/one.txt" "$CLEANUP/target/beta-$version/one.txt" + cmp "$CLEANUP/source/beta-$version/two.txt" "$CLEANUP/target/beta-$version/two.txt" +} + +cat >"$CONFIGDIR/01-alpha.transfer" <"$CONFIGDIR/02-beta.transfer" <), and "cleanup +# --component-all" must clean up orphaned resources across *all* of them in one +# go. Set up two components, install a resource into each, then drop both transfer +# files and run a single "cleanup --component-all" — it must remove both +# components' resources (and their install database entries). +COMPALL="$WORKDIR/component-all" +rm -rf "$COMPALL" /run/sysupdate.comp-a.d /run/sysupdate.comp-b.d \ + /var/lib/systemd/sysupdate/installdb.comp-a /var/lib/systemd/sysupdate/installdb.comp-b +mkdir -p "$COMPALL/source" "$COMPALL/target-a" "$COMPALL/target-b" \ + /run/sysupdate.comp-a.d /run/sysupdate.comp-b.d + +echo "$RANDOM" >"$COMPALL/source/comp-a-v1.bin" +echo "$RANDOM" >"$COMPALL/source/comp-b-v1.bin" +(cd "$COMPALL/source" && sha256sum comp-* >SHA256SUMS) + +cat >/run/sysupdate.comp-a.d/01-comp-a.transfer </run/sysupdate.comp-b.d/01-comp-b.transfer <