From 3131c24bd61ee25a63494594e1cfb02ae5382e58 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 23 Nov 2021 10:37:03 +0100 Subject: [PATCH 1/9] pretty-print: add helper for quickly outputting red/green cross/check marks Compound constructors FTW! --- src/shared/pretty-print.h | 23 +++++++++++++++++++++++ src/test/test-pretty-print.c | 14 ++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/shared/pretty-print.h b/src/shared/pretty-print.h index 4619f4e4d7..45644da67d 100644 --- a/src/shared/pretty-print.h +++ b/src/shared/pretty-print.h @@ -1,6 +1,9 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "glyph-util.h" +#include "terminal-util.h" + void print_separator(void); int file_url_from_path(const char *path, char **ret); @@ -17,3 +20,23 @@ typedef enum CatFlags { int cat_files(const char *file, char **dropins, CatFlags flags); int conf_files_cat(const char *root, const char *name); + +#define RED_CROSS_MARK_MAX (STRLEN(ANSI_HIGHLIGHT_RED) + STRLEN("✗") + STRLEN(ANSI_NORMAL) + 1) +#define GREEN_CHECK_MARK_MAX (STRLEN(ANSI_HIGHLIGHT_GREEN) + STRLEN("✓") + STRLEN(ANSI_NORMAL) + 1) + +static inline const char *red_cross_mark_internal(char buffer[static RED_CROSS_MARK_MAX]) { + assert(buffer); + assert_se(stpcpy(stpcpy(stpcpy(buffer, ansi_highlight_red()), special_glyph(SPECIAL_GLYPH_CROSS_MARK)), ansi_normal()) < buffer + RED_CROSS_MARK_MAX); + return buffer; +} + +static inline const char *green_check_mark_internal(char buffer[static GREEN_CHECK_MARK_MAX]) { + assert(buffer); + assert_se(stpcpy(stpcpy(stpcpy(buffer, ansi_highlight_green()), special_glyph(SPECIAL_GLYPH_CHECK_MARK)), ansi_normal()) < buffer + GREEN_CHECK_MARK_MAX); + return buffer; +} + +#define RED_CROSS_MARK() red_cross_mark_internal((char[RED_CROSS_MARK_MAX]) {}) +#define GREEN_CHECK_MARK() green_check_mark_internal((char[GREEN_CHECK_MARK_MAX]) {}) + +#define COLOR_MARK_BOOL(b) ((b) ? GREEN_CHECK_MARK() : RED_CROSS_MARK()) diff --git a/src/test/test-pretty-print.c b/src/test/test-pretty-print.c index dbae34e73e..4620d39bb6 100644 --- a/src/test/test-pretty-print.c +++ b/src/test/test-pretty-print.c @@ -31,11 +31,25 @@ static void test_cat_files(void) { assert_se(cat_files("/etc/fstab", STRV_MAKE("/etc/fstab", "/etc/fstab"), 0) == 0); } +static void test_red_green_cross_check_mark(void) { + bool b = false; + + printf("yeah: <%s>\n", GREEN_CHECK_MARK()); + printf("nay: <%s>\n", RED_CROSS_MARK()); + + printf("%s → %s → %s → %s\n", + COLOR_MARK_BOOL(b), + COLOR_MARK_BOOL(!b), + COLOR_MARK_BOOL(!!b), + COLOR_MARK_BOOL(!!!b)); +} + int main(int argc, char *argv[]) { test_setup_logging(LOG_INFO); test_terminal_urlify(); test_cat_files(); + test_red_green_cross_check_mark(); print_separator(); From ae54abe7fe239f1606a4becc2a41f2d7530371a2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 23 Nov 2021 14:08:01 +0100 Subject: [PATCH 2/9] bootctl: use new red/green check/cross mark helpers at two places --- src/boot/bootctl.c | 6 ++---- src/test/test-gpt.c | 5 +++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/boot/bootctl.c b/src/boot/bootctl.c index c19a51de62..07ad949a0c 100644 --- a/src/boot/bootctl.c +++ b/src/boot/bootctl.c @@ -1240,11 +1240,9 @@ static void read_efi_var(const char *variable, char **ret) { } static void print_yes_no_line(bool first, bool good, const char *name) { - printf("%s%s%s%s %s\n", + printf("%s%s %s\n", first ? " Features: " : " ", - ansi_highlight_green_red(good), - special_glyph_check_mark(good), - ansi_normal(), + COLOR_MARK_BOOL(good), name); } diff --git a/src/test/test-gpt.c b/src/test/test-gpt.c index 9b0eb57373..7ee044ba50 100644 --- a/src/test/test-gpt.c +++ b/src/test/test-gpt.c @@ -4,6 +4,7 @@ #include "glyph-util.h" #include "gpt.h" #include "log.h" +#include "pretty-print.h" #include "strv.h" #include "terminal-util.h" #include "tests.h" @@ -32,11 +33,11 @@ static void test_gpt_types_against_architectures(void) { r = gpt_partition_type_uuid_from_string(joined, &id); if (r < 0) { - printf("%s%s%s %s\n", ansi_highlight_red(), special_glyph(SPECIAL_GLYPH_CROSS_MARK), ansi_normal(), joined); + printf("%s %s\n", RED_CROSS_MARK(), joined); continue; } - printf("%s%s%s %s\n", ansi_highlight_green(), special_glyph(SPECIAL_GLYPH_CHECK_MARK), ansi_normal(), joined); + printf("%s %s\n", GREEN_CHECK_MARK(), joined); if (streq(prefix, "root-") && streq(suffix, "")) assert_se(gpt_partition_type_is_root(id)); From 8570b98b120c8979cb07f7ac06d4df4e24cb90bd Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 19 Nov 2021 15:47:20 +0100 Subject: [PATCH 3/9] dissect: add helper call for unifying three loops --- src/dissect/dissect.c | 49 +++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 7b4769d1b0..599bc8f6d5 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -369,6 +369,19 @@ static int strv_pair_to_json(char **l, JsonVariant **ret) { return json_variant_new_array_strv(ret, jl); } +static void strv_pair_print(char **l, const char *prefix) { + char **p, **q; + + assert(prefix); + + STRV_FOREACH_PAIR(p, q, l) { + if (p == l) + printf("%s %s=%s\n", prefix, *p, *q); + else + printf("%*s %s=%s\n", (int) strlen(prefix), "", *p, *q); + } +} + static int action_dissect(DissectedImage *m, LoopDevice *d) { _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; _cleanup_(table_unrefp) Table *t = NULL; @@ -412,38 +425,18 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { if (!sd_id128_is_null(m->machine_id)) printf("Machine ID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(m->machine_id)); - if (!strv_isempty(m->machine_info)) { - char **p, **q; - - STRV_FOREACH_PAIR(p, q, m->machine_info) - printf("%s %s=%s\n", - p == m->machine_info ? "Mach. Info:" : " ", - *p, *q); - } - - if (!strv_isempty(m->os_release)) { - char **p, **q; - - STRV_FOREACH_PAIR(p, q, m->os_release) - printf("%s %s=%s\n", - p == m->os_release ? "OS Release:" : " ", - *p, *q); - } - - if (!strv_isempty(m->extension_release)) { - char **p, **q; - - STRV_FOREACH_PAIR(p, q, m->extension_release) - printf("%s %s=%s\n", - p == m->extension_release ? "Extension Release:" : " ", - *p, *q); - } + strv_pair_print(m->machine_info, + "Mach. Info:"); + strv_pair_print(m->os_release, + "OS Release:"); + strv_pair_print(m->extension_release, + " Ext. Rel.:"); if (m->hostname || !sd_id128_is_null(m->machine_id) || !strv_isempty(m->machine_info) || - !strv_isempty(m->extension_release) || - !strv_isempty(m->os_release)) + !strv_isempty(m->os_release) || + !strv_isempty(m->extension_release)) putc('\n', stdout); } else { _cleanup_(json_variant_unrefp) JsonVariant *mi = NULL, *osr = NULL, *exr = NULL; From a4e0d6171318b5f381a1cbdb6de895920d7f15d6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 19 Nov 2021 15:48:41 +0100 Subject: [PATCH 4/9] dissect-image: when extracting metadata from image also check if it contains init system It's good to know whether we can boot the image. --- src/shared/dissect-image.c | 75 +++++++++++++++++++++++++++++++++----- src/shared/dissect-image.h | 1 + 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 18c7991570..b92df413a9 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -46,6 +46,7 @@ #include "hostname-setup.h" #include "id128-util.h" #include "import-util.h" +#include "io-util.h" #include "mkdir-label.h" #include "mount-util.h" #include "mountpoint-util.h" @@ -748,10 +749,14 @@ int dissect_image( if (r != 0) return errno_or_else(EIO); - m = new0(DissectedImage, 1); + m = new(DissectedImage, 1); if (!m) return -ENOMEM; + *m = (DissectedImage) { + .has_init_system = -1, + }; + r = sd_device_get_sysname(d, &sysname); if (r < 0) return log_debug_errno(r, "Failed to get device sysname: %m"); @@ -3012,6 +3017,7 @@ int dissected_image_acquire_metadata(DissectedImage *m) { META_MACHINE_INFO, META_OS_RELEASE, META_EXTENSION_RELEASE, + META_HAS_INIT_SYSTEM, _META_MAX, }; @@ -3021,7 +3027,8 @@ int dissected_image_acquire_metadata(DissectedImage *m) { [META_MACHINE_INFO] = "/etc/machine-info\0", [META_OS_RELEASE] = ("/etc/os-release\0" "/usr/lib/os-release\0"), - [META_EXTENSION_RELEASE] = "extension-release\0", /* Used only for logging. */ + [META_EXTENSION_RELEASE] = "extension-release\0", /* Used only for logging. */ + [META_HAS_INIT_SYSTEM] = "has-init-system\0", /* ditto */ }; _cleanup_strv_free_ char **machine_info = NULL, **os_release = NULL, **extension_release = NULL; @@ -3032,6 +3039,7 @@ int dissected_image_acquire_metadata(DissectedImage *m) { _cleanup_free_ char *hostname = NULL; unsigned n_meta_initialized = 0; int fds[2 * _META_MAX], r, v; + int has_init_system = -1; ssize_t n; BLOCK_SIGNALS(SIGCHLD); @@ -3063,6 +3071,7 @@ int dissected_image_acquire_metadata(DissectedImage *m) { if (r < 0) goto finish; if (r == 0) { + /* Child in a new mount namespace */ error_pipe[0] = safe_close(error_pipe[0]); r = dissected_image_mount( @@ -3092,7 +3101,9 @@ int dissected_image_acquire_metadata(DissectedImage *m) { fds[2*k] = safe_close(fds[2*k]); - if (k == META_EXTENSION_RELEASE) { + switch (k) { + + case META_EXTENSION_RELEASE: /* As per the os-release spec, if the image is an extension it will have a file * named after the image name in extension-release.d/ - we use the image name * and try to resolve it with the extension-release helpers, as sometimes @@ -3105,12 +3116,42 @@ int dissected_image_acquire_metadata(DissectedImage *m) { r = open_extension_release(t, m->image_name, NULL, &fd); if (r < 0) fd = r; /* Propagate the error. */ - } else + break; + + case META_HAS_INIT_SYSTEM: { + bool found = false; + const char *init; + + FOREACH_STRING(init, + "/usr/lib/systemd/systemd", /* systemd on /usr merged system */ + "/lib/systemd/systemd", /* systemd on /usr non-merged systems */ + "/sbin/init") { /* traditional path the Linux kernel invokes */ + + r = chase_symlinks(init, t, CHASE_PREFIX_ROOT, NULL, NULL); + if (r < 0) { + if (r != -ENOENT) + log_debug_errno(r, "Failed to resolve %s, ignoring: %m", init); + } else { + found = true; + break; + } + } + + r = loop_write(fds[2*k+1], &found, sizeof(found), false); + if (r < 0) + goto inner_fail; + + continue; + } + + default: NULSTR_FOREACH(p, paths[k]) { fd = chase_symlinks_and_open(p, t, CHASE_PREFIX_ROOT, O_RDONLY|O_CLOEXEC|O_NOCTTY, NULL); if (fd >= 0) break; } + } + if (fd < 0) { log_debug_errno(fd, "Failed to read %s file of image, ignoring: %m", paths[k]); fds[2*k+1] = safe_close(fds[2*k+1]); @@ -3118,15 +3159,17 @@ int dissected_image_acquire_metadata(DissectedImage *m) { } r = copy_bytes(fd, fds[2*k+1], UINT64_MAX, 0); - if (r < 0) { - (void) write(error_pipe[1], &r, sizeof(r)); - _exit(EXIT_FAILURE); - } + if (r < 0) + goto inner_fail; fds[2*k+1] = safe_close(fds[2*k+1]); } _exit(EXIT_SUCCESS); + + inner_fail: + (void) write(error_pipe[1], &r, sizeof(r)); + _exit(EXIT_FAILURE); } error_pipe[1] = safe_close(error_pipe[1]); @@ -3194,7 +3237,20 @@ int dissected_image_acquire_metadata(DissectedImage *m) { log_debug_errno(r, "Failed to read extension release file: %m"); break; - } + + case META_HAS_INIT_SYSTEM: { + bool b = false; + size_t nr; + + errno = 0; + nr = fread(&b, 1, sizeof(b), f); + if (nr != sizeof(b)) + log_debug_errno(errno_or_else(EIO), "Failed to read has-init-system boolean: %m"); + else + has_init_system = b; + + break; + }} } r = wait_for_terminate_and_check("(sd-dissect)", child, 0); @@ -3218,6 +3274,7 @@ int dissected_image_acquire_metadata(DissectedImage *m) { strv_free_and_replace(m->machine_info, machine_info); strv_free_and_replace(m->os_release, os_release); strv_free_and_replace(m->extension_release, extension_release); + m->has_init_system = has_init_system; finish: for (unsigned k = 0; k < n_meta_initialized; k++) diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index 635ca5b76c..b75b86ab99 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -163,6 +163,7 @@ struct DissectedImage { char **machine_info; char **os_release; char **extension_release; + int has_init_system; }; struct MountOptions { From 60c5f7002bfee928fb8799eb35e5cc95da982a48 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 18 Nov 2021 22:00:31 +0100 Subject: [PATCH 5/9] extension-release.d/: add a new field SYSEXT_SCOPE= for clarifying what a system extension is for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should make things a bit more robust since it ensures system extension can only applied to the right environments. Right now three different "scopes" are defined: 1. "system" (for regular OS systems, after the initrd transition) 2. "initrd" (for sysext images that apply to the initrd environment) 3. "portable" (for sysext images that apply to portable images) If not specified we imply a default of "system portable", i.e. any image where the field is not specified is implicitly OK for application to OS images and for portable services – but not for initrds. --- man/os-release.xml | 12 ++++++++++++ src/core/namespace.c | 2 +- src/portable/portable.c | 2 +- src/shared/dissect-image.c | 14 ++++++++------ src/shared/dissect-image.h | 2 +- src/shared/extension-release.c | 23 +++++++++++++++++++++++ src/shared/extension-release.h | 1 + src/shared/mount-util.c | 2 +- src/sysext/sysext.c | 7 ++++++- 9 files changed, 54 insertions(+), 11 deletions(-) diff --git a/man/os-release.xml b/man/os-release.xml index ef5ef8b2e1..a985151b4d 100644 --- a/man/os-release.xml +++ b/man/os-release.xml @@ -407,6 +407,18 @@ Examples: SYSEXT_LEVEL=2, SYSEXT_LEVEL=15.14. + + + SYSEXT_SCOPE= + Takes a space-separated list of one or more of the strings + system, initrd and portable. This field is + only supported in extension-release.d/ files and indicates what environments + the system extension is applicable to: i.e. to regular systems, to initial RAM filesystems + ("initrd") or to portable service images. If unspecified, SYSEXT_SCOPE=system + portable is implied, i.e. any system extension without this field is applicable to + regular systems and to portable service environments, but not to initrd + environments. + diff --git a/src/core/namespace.c b/src/core/namespace.c index c8e7e65e27..9393a202c4 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -1149,7 +1149,7 @@ static int mount_image(const MountEntry *m, const char *root_directory) { r = verity_dissect_and_mount( mount_entry_source(m), mount_entry_path(m), m->image_options, - host_os_release_id, host_os_release_version_id, host_os_release_sysext_level); + host_os_release_id, host_os_release_version_id, host_os_release_sysext_level, NULL); if (r == -ENOENT && m->ignore) return 0; if (r == -ESTALE && host_os_release_id) diff --git a/src/portable/portable.c b/src/portable/portable.c index 8ccb8f5228..612893b688 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -595,7 +595,7 @@ static int extract_image_and_extensions( if (r < 0) return r; - r = extension_release_validate(ext->path, id, version_id, sysext_level, extension_release); + r = extension_release_validate(ext->path, id, version_id, sysext_level, "portable", extension_release); if (r == 0) return sd_bus_error_set_errnof(error, SYNTHETIC_ERRNO(ESTALE), "Image %s extension-release metadata does not match the root's", ext->path); if (r < 0) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index b92df413a9..06d0319f75 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -3525,7 +3525,8 @@ int verity_dissect_and_mount( const MountOptions *options, const char *required_host_os_release_id, const char *required_host_os_release_version_id, - const char *required_host_os_release_sysext_level) { + const char *required_host_os_release_sysext_level, + const char *required_sysext_scope) { _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL; @@ -3611,11 +3612,12 @@ int verity_dissect_and_mount( return log_debug_errno(r, "Failed to parse image %s extension-release metadata: %m", dissected_image->image_name); r = extension_release_validate( - dissected_image->image_name, - required_host_os_release_id, - required_host_os_release_version_id, - required_host_os_release_sysext_level, - extension_release); + dissected_image->image_name, + required_host_os_release_id, + required_host_os_release_version_id, + required_host_os_release_sysext_level, + required_sysext_scope, + extension_release); if (r == 0) return log_debug_errno(SYNTHETIC_ERRNO(ESTALE), "Image %s extension-release metadata does not match the root's", dissected_image->image_name); if (r < 0) diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index b75b86ab99..8ad26bc45b 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -228,4 +228,4 @@ bool dissected_image_verity_sig_ready(const DissectedImage *image, PartitionDesi int mount_image_privately_interactively(const char *path, DissectImageFlags flags, char **ret_directory, LoopDevice **ret_loop_device, DecryptedImage **ret_decrypted_image); -int verity_dissect_and_mount(const char *src, const char *dest, const MountOptions *options, const char *required_host_os_release_id, const char *required_host_os_release_version_id, const char *required_host_os_release_sysext_level); +int verity_dissect_and_mount(const char *src, const char *dest, const MountOptions *options, const char *required_host_os_release_id, const char *required_host_os_release_version_id, const char *required_host_os_release_sysext_level, const char *required_sysext_scope); diff --git a/src/shared/extension-release.c b/src/shared/extension-release.c index 29cbecbf57..dccc999907 100644 --- a/src/shared/extension-release.c +++ b/src/shared/extension-release.c @@ -12,6 +12,7 @@ int extension_release_validate( const char *host_os_release_id, const char *host_os_release_version_id, const char *host_os_release_sysext_level, + const char *host_sysext_scope, char **extension_release) { const char *extension_release_id = NULL, *extension_release_sysext_level = NULL; @@ -25,6 +26,28 @@ int extension_release_validate( return 0; } + if (host_sysext_scope) { + _cleanup_strv_free_ char **extension_sysext_scope_list = NULL; + const char *extension_sysext_scope; + bool valid; + + extension_sysext_scope = strv_env_pairs_get(extension_release, "SYSEXT_SCOPE"); + if (extension_sysext_scope) { + extension_sysext_scope_list = strv_split(extension_sysext_scope, WHITESPACE); + if (!extension_sysext_scope_list) + return -ENOMEM; + } + + /* by default extension are good for attachment in portable service and on the system */ + valid = strv_contains( + extension_sysext_scope_list ?: STRV_MAKE("system", "portable"), + host_sysext_scope); + if (!valid) { + log_debug("Extension '%s' is not suitable for scope %s, ignoring extension.", name, host_sysext_scope); + return 0; + } + } + extension_release_id = strv_env_pairs_get(extension_release, "ID"); if (isempty(extension_release_id)) { log_debug("Extension '%s' does not contain ID in extension-release but requested to match '%s'", diff --git a/src/shared/extension-release.h b/src/shared/extension-release.h index d026a9b225..5c3fee24be 100644 --- a/src/shared/extension-release.h +++ b/src/shared/extension-release.h @@ -9,6 +9,7 @@ int extension_release_validate( const char *host_os_release_id, const char *host_os_release_version_id, const char *host_os_release_sysext_level, + const char *host_sysext_scope, char **extension_release); /* Parse SYSTEMD_SYSEXT_HIERARCHIES and if not set, return "/usr /opt" */ diff --git a/src/shared/mount-util.c b/src/shared/mount-util.c index 8d4a6cd25a..c75c02f5be 100644 --- a/src/shared/mount-util.c +++ b/src/shared/mount-util.c @@ -874,7 +874,7 @@ static int mount_in_namespace( mount_tmp_created = true; if (is_image) - r = verity_dissect_and_mount(FORMAT_PROC_FD_PATH(chased_src_fd), mount_tmp, options, NULL, NULL, NULL); + r = verity_dissect_and_mount(FORMAT_PROC_FD_PATH(chased_src_fd), mount_tmp, options, NULL, NULL, NULL, NULL); else r = mount_follow_verbose(LOG_DEBUG, FORMAT_PROC_FD_PATH(chased_src_fd), mount_tmp, NULL, MS_BIND, NULL); if (r < 0) diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c index b9387e904a..5abf1bb418 100644 --- a/src/sysext/sysext.c +++ b/src/sysext/sysext.c @@ -432,12 +432,17 @@ static int validate_version( return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Extension image contains /usr/lib/os-release file, which is not allowed (it may carry /etc/os-release), refusing."); - return extension_release_validate( + r = extension_release_validate( img->name, host_os_release_id, host_os_release_version_id, host_os_release_sysext_level, + in_initrd() ? "initrd" : "system", img->extension_release); + if (r < 0) + return log_error_errno(r, "Failed to validate extension release information: %m"); + + return r; } static int merge_subprocess(Hashmap *images, const char *workspace) { From 8a129c808a1ae78cb4593e99adb4c567c31bc72e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 19 Nov 2021 16:19:19 +0100 Subject: [PATCH 6/9] os-release: add new PORTABLE_PREFIXES= field for declaring valid portable service match prefixes --- docs/PORTABLE_SERVICES.md | 14 +++ man/os-release.xml | 11 ++ src/portable/portable.c | 170 +++++++++++++++++++++-------- src/portable/portable.h | 2 +- src/portable/portabled-image-bus.c | 1 + 5 files changed, 154 insertions(+), 44 deletions(-) diff --git a/docs/PORTABLE_SERVICES.md b/docs/PORTABLE_SERVICES.md index df6eb9958e..dd9164126f 100644 --- a/docs/PORTABLE_SERVICES.md +++ b/docs/PORTABLE_SERVICES.md @@ -247,6 +247,20 @@ image. To facilitate 3 and 4 you also need to include a boot loader in the image. As mentioned, `mkosi -b` takes care of all of that for you, but any other image generator should work too. +The +[os-release(5)](https://www.freedesktop.org/software/systemd/man/os-release.html) +file may optionally be extended with a `PORTABLE_PREFIXES=` field listing all +supported portable service prefixes for the image (see above). This is useful +for informational purposes (as it allows recognizing portable service images +from their contents as such), but is also useful to protect the image from +being used under a wrong name and prefix. This is particularly relevant if the +images are cryptographically authenticated (via Verity or a similar mechanism) +as this way the (not necessarily authenticated) image file name can be +validated against the (authenticated) image contents. If the field is not +specified the image will work fine, but is not necessarily recognizable as +portable service image, and any set of units included in the image may be +attached, there are no restrictions enforced. + ## Extension Images Portable services can be delivered as one or multiple images that extend the base diff --git a/man/os-release.xml b/man/os-release.xml index a985151b4d..1826a60d1a 100644 --- a/man/os-release.xml +++ b/man/os-release.xml @@ -419,6 +419,17 @@ regular systems and to portable service environments, but not to initrd environments. + + + PORTABLE_PREFIXES= + Takes a space-separated list of one or more valid prefix match strings for the + Portable Services logic. This field + serves two purposes: it's informational, identifying portable service images as such (and thus + allowing them to be distinguished from other OS images, such as bootable system images); whenever a + portable service image is attached the specified or implied portable service prefix is checked + against this list, to enforce restrictions how images may be attached to a + system. + diff --git a/src/portable/portable.c b/src/portable/portable.c index 612893b688..2d1006eabd 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -13,6 +13,7 @@ #include "discover-image.h" #include "dissect-image.h" #include "env-file.h" +#include "env-util.h" #include "errno-list.h" #include "escape.h" #include "extension-release.h" @@ -509,20 +510,20 @@ static int extract_image_and_extensions( OrderedHashmap **ret_extension_images, PortableMetadata **ret_os_release, Hashmap **ret_unit_files, + char ***ret_valid_prefixes, sd_bus_error *error) { _cleanup_free_ char *id = NULL, *version_id = NULL, *sysext_level = NULL; _cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL; _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL; _cleanup_hashmap_free_ Hashmap *unit_files = NULL; + _cleanup_strv_free_ char **valid_prefixes = NULL; _cleanup_(image_unrefp) Image *image = NULL; Image *ext; int r; assert(name_or_path); assert(matches); - assert(ret_image); - assert(ret_extension_images); r = image_find_harder(IMAGE_PORTABLE, name_or_path, NULL, &image); if (r < 0) @@ -553,10 +554,12 @@ static int extract_image_and_extensions( if (r < 0) return r; - /* If we are layering extension images on top of a runtime image, check that the os-release and extension-release metadata - * match, otherwise reject it immediately as invalid, or it will fail when the units are started. */ - if (validate_sysext) { + /* If we are layering extension images on top of a runtime image, check that the os-release and + * extension-release metadata match, otherwise reject it immediately as invalid, or it will fail when + * the units are started. Also, collect valid portable prefixes if caller requested that. */ + if (validate_sysext || ret_valid_prefixes) { _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *prefixes = NULL; r = take_fdopen_unlocked(&os_release->fd, "r", &f); if (r < 0) @@ -565,9 +568,16 @@ static int extract_image_and_extensions( r = parse_env_file(f, os_release->name, "ID", &id, "VERSION_ID", &version_id, - "SYSEXT_LEVEL", &sysext_level); + "SYSEXT_LEVEL", &sysext_level, + "PORTABLE_PREFIXES", &prefixes); if (r < 0) return r; + + if (prefixes) { + valid_prefixes = strv_split(prefixes, WHITESPACE); + if (!valid_prefixes) + return -ENOMEM; + } } ORDERED_HASHMAP_FOREACH(ext, extension_images) { @@ -575,6 +585,7 @@ static int extract_image_and_extensions( _cleanup_hashmap_free_ Hashmap *extra_unit_files = NULL; _cleanup_strv_free_ char **extension_release = NULL; _cleanup_fclose_ FILE *f = NULL; + const char *e; r = portable_extract_by_path(ext->path, /* path_is_extension= */ true, matches, &extension_release_meta, &extra_unit_files, error); if (r < 0) @@ -584,7 +595,7 @@ static int extract_image_and_extensions( if (r < 0) return r; - if (!validate_sysext) + if (!validate_sysext && !ret_valid_prefixes) continue; r = take_fdopen_unlocked(&extension_release_meta->fd, "r", &f); @@ -595,19 +606,40 @@ static int extract_image_and_extensions( if (r < 0) return r; - r = extension_release_validate(ext->path, id, version_id, sysext_level, "portable", extension_release); - if (r == 0) - return sd_bus_error_set_errnof(error, SYNTHETIC_ERRNO(ESTALE), "Image %s extension-release metadata does not match the root's", ext->path); - if (r < 0) - return sd_bus_error_set_errnof(error, r, "Failed to compare image %s extension-release metadata with the root's os-release: %m", ext->path); + if (validate_sysext) { + r = extension_release_validate(ext->path, id, version_id, sysext_level, "portable", extension_release); + if (r == 0) + return sd_bus_error_set_errnof(error, SYNTHETIC_ERRNO(ESTALE), "Image %s extension-release metadata does not match the root's", ext->path); + if (r < 0) + return sd_bus_error_set_errnof(error, r, "Failed to compare image %s extension-release metadata with the root's os-release: %m", ext->path); + } + + e = strv_env_pairs_get(extension_release, "PORTABLE_PREFIXES"); + if (e) { + _cleanup_strv_free_ char **l = NULL; + + l = strv_split(e, WHITESPACE); + if (!l) + return -ENOMEM; + + r = strv_extend_strv(&valid_prefixes, l, true); + if (r < 0) + return r; + } } - *ret_image = TAKE_PTR(image); - *ret_extension_images = TAKE_PTR(extension_images); + strv_sort(valid_prefixes); + + if (ret_image) + *ret_image = TAKE_PTR(image); + if (ret_extension_images) + *ret_extension_images = TAKE_PTR(extension_images); if (ret_os_release) *ret_os_release = TAKE_PTR(os_release); if (ret_unit_files) *ret_unit_files = TAKE_PTR(unit_files); + if (ret_valid_prefixes) + *ret_valid_prefixes = TAKE_PTR(valid_prefixes); return 0; } @@ -618,23 +650,29 @@ int portable_extract( char **extension_image_paths, PortableMetadata **ret_os_release, Hashmap **ret_unit_files, + char ***ret_valid_prefixes, sd_bus_error *error) { _cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL; _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL; _cleanup_hashmap_free_ Hashmap *unit_files = NULL; + _cleanup_(strv_freep) char **valid_prefixes = NULL; _cleanup_(image_unrefp) Image *image = NULL; int r; - r = extract_image_and_extensions(name_or_path, - matches, - extension_image_paths, - /* validate_sysext= */ false, - &image, - &extension_images, - &os_release, - &unit_files, - error); + assert(name_or_path); + + r = extract_image_and_extensions( + name_or_path, + matches, + extension_image_paths, + /* validate_sysext= */ false, + &image, + &extension_images, + &os_release, + &unit_files, + ret_valid_prefixes ? &valid_prefixes : NULL, + error); if (r < 0) return r; @@ -651,8 +689,12 @@ int portable_extract( isempty(extensions) ? "" : extensions); } - *ret_os_release = TAKE_PTR(os_release); - *ret_unit_files = TAKE_PTR(unit_files); + if (ret_os_release) + *ret_os_release = TAKE_PTR(os_release); + if (ret_unit_files) + *ret_unit_files = TAKE_PTR(unit_files); + if (ret_valid_prefixes) + *ret_valid_prefixes = TAKE_PTR(valid_prefixes); return 0; } @@ -1211,6 +1253,18 @@ static int install_image_and_extensions_symlinks( return 0; } +static bool prefix_matches_compatible(char **matches, char **valid_prefixes) { + char **m; + + /* Checks if all 'matches' are included in the list of 'valid_prefixes' */ + + STRV_FOREACH(m, matches) + if (!strv_contains(valid_prefixes, *m)) + return false; + + return true; +} + int portable_attach( sd_bus *bus, const char *name_or_path, @@ -1225,33 +1279,63 @@ int portable_attach( _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL; _cleanup_hashmap_free_ Hashmap *unit_files = NULL; _cleanup_(lookup_paths_free) LookupPaths paths = {}; + _cleanup_strv_free_ char **valid_prefixes = NULL; _cleanup_(image_unrefp) Image *image = NULL; PortableMetadata *item; int r; - r = extract_image_and_extensions(name_or_path, - matches, - extension_image_paths, - /* validate_sysext= */ true, - &image, - &extension_images, - /* os_release= */ NULL, - &unit_files, - error); + r = extract_image_and_extensions( + name_or_path, + matches, + extension_image_paths, + /* validate_sysext= */ true, + &image, + &extension_images, + /* os_release= */ NULL, + &unit_files, + &valid_prefixes, + error); if (r < 0) return r; - if (hashmap_isempty(unit_files)) { - _cleanup_free_ char *extensions = strv_join(extension_image_paths, ", "); - if (!extensions) + if (valid_prefixes && !prefix_matches_compatible(matches, valid_prefixes)) { + _cleanup_free_ char *matches_joined = NULL, *extensions_joined = NULL, *valid_prefixes_joined = NULL; + + matches_joined = strv_join(matches, "', '"); + if (!matches_joined) return -ENOMEM; - return sd_bus_error_setf(error, - SD_BUS_ERROR_INVALID_ARGS, - "Couldn't find any matching unit files in image '%s%s%s', refusing.", - image->path, - isempty(extensions) ? "" : "' or any of its extensions '", - isempty(extensions) ? "" : extensions); + extensions_joined = strv_join(extension_image_paths, ", "); + if (!extensions_joined) + return -ENOMEM; + + valid_prefixes_joined = strv_join(valid_prefixes, ", "); + if (!valid_prefixes_joined) + return -ENOMEM; + + return sd_bus_error_setf( + error, + SD_BUS_ERROR_INVALID_ARGS, + "Selected matches '%s' are not compatible with portable service image '%s%s%s', refusing. (Acceptable prefix matches are: %s)", + matches_joined, + image->path, + isempty(extensions_joined) ? "" : "' or any of its extensions '", + strempty(extensions_joined), + valid_prefixes_joined); + } + + if (hashmap_isempty(unit_files)) { + _cleanup_free_ char *extensions_joined = strv_join(extension_image_paths, ", "); + if (!extensions_joined) + return -ENOMEM; + + return sd_bus_error_setf( + error, + SD_BUS_ERROR_INVALID_ARGS, + "Couldn't find any matching unit files in image '%s%s%s', refusing.", + image->path, + isempty(extensions_joined) ? "" : "' or any of its extensions '", + strempty(extensions_joined)); } r = lookup_paths_init(&paths, UNIT_FILE_SYSTEM, LOOKUP_PATHS_SPLIT_USR, NULL); diff --git a/src/portable/portable.h b/src/portable/portable.h index 077ab3333f..2837e8b286 100644 --- a/src/portable/portable.h +++ b/src/portable/portable.h @@ -65,7 +65,7 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(PortableMetadata*, portable_metadata_unref); int portable_metadata_hashmap_to_sorted_array(Hashmap *unit_files, PortableMetadata ***ret); -int portable_extract(const char *image, char **matches, char **extension_image_paths, PortableMetadata **ret_os_release, Hashmap **ret_unit_files, sd_bus_error *error); +int portable_extract(const char *image, char **matches, char **extension_image_paths, PortableMetadata **ret_os_release, Hashmap **ret_unit_files, char ***ret_valid_prefixes, sd_bus_error *error); int portable_attach(sd_bus *bus, const char *name_or_path, char **matches, const char *profile, char **extension_images, PortableFlags flags, PortableChange **changes, size_t *n_changes, sd_bus_error *error); int portable_detach(sd_bus *bus, const char *name_or_path, char **extension_image_paths, PortableFlags flags, PortableChange **changes, size_t *n_changes, sd_bus_error *error); diff --git a/src/portable/portabled-image-bus.c b/src/portable/portabled-image-bus.c index 23c6e2633a..ede062dbfb 100644 --- a/src/portable/portabled-image-bus.c +++ b/src/portable/portabled-image-bus.c @@ -162,6 +162,7 @@ int bus_image_common_get_metadata( extension_images, &os_release, &unit_files, + NULL, error); if (r < 0) return r; From 994c9c70a166c5e5c383cf651b6cb4077e8f183e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 19 Nov 2021 16:20:00 +0100 Subject: [PATCH 7/9] dissect: show intended purpose of images in dissection output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With this the tool will show whether an image is intended to be a bootable OS image, a system extension, or a portable service image. Example output with this patch: Name: image_53.raw Size: 3.2G Machine ID: bb9f2921198040feb7c82270bf66e4b8 OS Release: NAME=Fedora VERSION=34 (Thirty Four) … PRIVACY_POLICY_URL=https://fedoraproject.org/wiki/Legal:PrivacyPolicy IMAGE_VERSION=53 Use As: ✓ bootable system for UEFI ✓ bootable system for container ✗ portable service ✗ extension for system ✗ extension for initrd ✗ extension for portable service RW DESIGNATOR PARTITION UUID PARTITION LABEL FSTYPE ARCHITECTURE VERITY GROWFS NODE PARTNO rw root d56af2b4-35eb-2e40-8f08-2d3b7012b0fc Root Partition btrfs x86-64 no no /dev/loop1p2 2 rw esp e3d11339-05c1-a74a-ae77-aae76812c134 ESP System Partition vfat - - no /dev/loop1p1 1 … --- src/dissect/dissect.c | 64 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 599bc8f6d5..753d4aefd7 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -11,6 +11,7 @@ #include "chase-symlinks.h" #include "copy.h" #include "dissect-image.h" +#include "env-util.h" #include "fd-util.h" #include "fileio.h" #include "format-table.h" @@ -382,6 +383,33 @@ static void strv_pair_print(char **l, const char *prefix) { } } +static int get_sysext_scopes(DissectedImage *m, char ***ret_scopes) { + _cleanup_strv_free_ char **l = NULL; + const char *e; + + assert(m); + assert(ret_scopes); + + /* If there's no extension-release file its not a system extension. Otherwise the SYSEXT_SCOPE field + * indicates which scope it is for — and it defaults to "system" + "portable" if unset. */ + + if (!m->extension_release) { + *ret_scopes = NULL; + return 0; + } + + e = strv_env_pairs_get(m->extension_release, "SYSEXT_SCOPE"); + if (e) + l = strv_split(e, WHITESPACE); + else + l = strv_new("system", "portable"); + if (!l) + return -ENOMEM; + + *ret_scopes = TAKE_PTR(l); + return 1; +} + static int action_dissect(DissectedImage *m, LoopDevice *d) { _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; _cleanup_(table_unrefp) Table *t = NULL; @@ -419,6 +447,8 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { else if (r < 0) return log_error_errno(r, "Failed to acquire image metadata: %m"); else if (arg_json_format_flags & JSON_FORMAT_OFF) { + _cleanup_strv_free_ char **sysext_scopes = NULL; + if (m->hostname) printf(" Hostname: %s\n", m->hostname); @@ -438,8 +468,30 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { !strv_isempty(m->os_release) || !strv_isempty(m->extension_release)) putc('\n', stdout); + + printf(" Use As: %s bootable system for UEFI\n", COLOR_MARK_BOOL(m->partitions[PARTITION_ESP].found)); + + if (m->has_init_system >= 0) + printf(" %s bootable system for container\n", COLOR_MARK_BOOL(m->has_init_system)); + + printf(" %s portable service\n", + COLOR_MARK_BOOL(strv_env_pairs_get(m->os_release, "PORTABLE_PREFIXES"))); + + r = get_sysext_scopes(m, &sysext_scopes); + if (r < 0) + return log_error_errno(r, "Failed to parse SYSEXT_SCOPE: %m"); + + printf(" %s extension for system\n", + COLOR_MARK_BOOL(strv_contains(sysext_scopes, "system"))); + printf(" %s extension for initrd\n", + COLOR_MARK_BOOL(strv_contains(sysext_scopes, "initrd"))); + printf(" %s extension for portable service\n", + COLOR_MARK_BOOL(strv_contains(sysext_scopes, "portable"))); + + putc('\n', stdout); } else { _cleanup_(json_variant_unrefp) JsonVariant *mi = NULL, *osr = NULL, *exr = NULL; + _cleanup_strv_free_ char **sysext_scopes = NULL; if (!strv_isempty(m->machine_info)) { r = strv_pair_to_json(m->machine_info, &mi); @@ -459,6 +511,10 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { return log_oom(); } + r = get_sysext_scopes(m, &sysext_scopes); + if (r < 0) + return log_error_errno(r, "Failed to parse SYSEXT_SCOPE: %m"); + r = json_build(&v, JSON_BUILD_OBJECT( JSON_BUILD_PAIR("name", JSON_BUILD_STRING(basename(arg_image))), JSON_BUILD_PAIR("size", JSON_BUILD_INTEGER(size)), @@ -466,7 +522,13 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(m->machine_id), "machineId", JSON_BUILD_ID128(m->machine_id)), JSON_BUILD_PAIR_CONDITION(mi, "machineInfo", JSON_BUILD_VARIANT(mi)), JSON_BUILD_PAIR_CONDITION(osr, "osRelease", JSON_BUILD_VARIANT(osr)), - JSON_BUILD_PAIR_CONDITION(exr, "extensionRelease", JSON_BUILD_VARIANT(exr)))); + JSON_BUILD_PAIR_CONDITION(exr, "extensionRelease", JSON_BUILD_VARIANT(exr)), + JSON_BUILD_PAIR("useBootableUefi", JSON_BUILD_BOOLEAN(m->partitions[PARTITION_ESP].found)), + JSON_BUILD_PAIR_CONDITION(m->has_init_system >= 0, "useBootableContainer", JSON_BUILD_BOOLEAN(m->has_init_system)), + JSON_BUILD_PAIR("usePortableService", JSON_BUILD_BOOLEAN(strv_env_pairs_get(m->os_release, "PORTABLE_MATCHES"))), + JSON_BUILD_PAIR("useSystemExtension", JSON_BUILD_BOOLEAN(strv_contains(sysext_scopes, "system"))), + JSON_BUILD_PAIR("useInitRDExtension", JSON_BUILD_BOOLEAN(strv_contains(sysext_scopes, "initrd"))), + JSON_BUILD_PAIR("usePortableExtension", JSON_BUILD_BOOLEAN(strv_contains(sysext_scopes, "portable"))))); if (r < 0) return log_oom(); } From 9ead4184132c373612fda67fa623a1e3e1c4dfb6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 23 Nov 2021 16:34:40 +0100 Subject: [PATCH 8/9] test: test new SYSEXT_SCOPE=/PORTABLE_PREFIXES= fields in TEST-29 --- test/test-functions | 5 ++++- test/units/testsuite-29.sh | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/test/test-functions b/test/test-functions index a4b7efb5ec..0a3745de52 100644 --- a/test/test-functions +++ b/test/test-functions @@ -594,6 +594,7 @@ install_verity_minimal() { touch "$initdir/etc/machine-id" "$initdir/etc/resolv.conf" touch "$initdir/opt/some_file" echo MARKER=1 >>"$initdir/usr/lib/os-release" + echo "PORTABLE_PREFIXES=app0 minimal" >>"$initdir/usr/lib/os-release" echo -e "[Service]\nExecStartPre=cat /usr/lib/os-release\nExecStart=sleep 120" >"$initdir/usr/lib/systemd/system/app0.service" cp "$initdir/usr/lib/systemd/system/app0.service" "$initdir/usr/lib/systemd/system/app0-foo.service" @@ -638,7 +639,9 @@ EOF export initdir="$TESTDIR/app1" mkdir -p "$initdir/usr/lib/extension-release.d" "$initdir/usr/lib/systemd/system" "$initdir/opt" grep "^ID=" "$os_release" >"$initdir/usr/lib/extension-release.d/extension-release.app2" - echo "${version_id}" >>"$initdir/usr/lib/extension-release.d/extension-release.app2" + ( echo "${version_id}" + echo "SYSEXT_SCOPE=portable" + echo "PORTABLE_PREFIXES=app1" ) >>"$initdir/usr/lib/extension-release.d/extension-release.app2" setfattr -n user.extension-release.strict -v false "$initdir/usr/lib/extension-release.d/extension-release.app2" cat >"$initdir/usr/lib/systemd/system/app1.service" </run/systemd/system/systemd-portabled.service.d/override.conf @@ -98,10 +103,19 @@ portablectl detach --now --runtime --extension /usr/share/app1.raw /usr/share/mi # portablectl also works with directory paths rather than images -mkdir /tmp/rootdir /tmp/app1 /tmp/overlay +mkdir /tmp/rootdir /tmp/app1 /tmp/overlay /tmp/os-release-fix /tmp/os-release-fix/etc mount /usr/share/app1.raw /tmp/app1 mount /usr/share/minimal_0.raw /tmp/rootdir -mount -t overlay overlay -o lowerdir=/tmp/app1:/tmp/rootdir /tmp/overlay + +# Fix up os-release to drop the valid PORTABLE_SERVICES field (because we are +# bypassing the sysext logic in portabled here it will otherwise not see the +# extensions additional valid prefix) +grep -v "^PORTABLE_PREFIXES=" /tmp/rootdir/etc/os-release > /tmp/os-release-fix/etc/os-release + +mount -t overlay overlay -o lowerdir=/tmp/os-release-fix:/tmp/app1:/tmp/rootdir /tmp/overlay + +grep . /tmp/overlay/usr/lib/extension-release.d/* +grep . /tmp/overlay/etc/os-release portablectl "${ARGS[@]}" attach --copy=symlink --now --runtime /tmp/overlay app1 From f5ba8115e00799dd3ff1d7ee096284510cdfbcdf Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 18 Nov 2021 22:00:20 +0100 Subject: [PATCH 9/9] update TODO --- TODO | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/TODO b/TODO index 7786166c35..3c6d76266c 100644 --- a/TODO +++ b/TODO @@ -94,6 +94,17 @@ Features: magic into a string. Then use that to replace fstype_magic_to_name() in homed sources, and similar code. +* man: rework os-release(5), and clearly separate our extension-release.d/ and + initrd-release parts, i.e. list explicitly which fields are about what. + +* sysext: before applying a sysext, do a superficial validation run so that + things are not rearranged to wildy. I.e. protect against accidental fuckups, + such as masking out /usr/lib/ or so. We should probably refuse if existing + inodes are replaced by other types of inodes or so. + +* sysext: ensure one can build a sysext that can safely apply to *any* system + (because it contains only static go binaries in /opt/ or so) + * userdb: when synthesizing NSS records, pick "best" password from defined passwords, not just the first. i.e. if there are multiple defined, prefer unlocked over locked and prefer non-empty over empty.