From b60e0f577740af89516f7c74967d7182637f27af Mon Sep 17 00:00:00 2001 From: maanyagoenka Date: Fri, 24 Feb 2023 00:37:52 +0000 Subject: [PATCH 1/8] os-util: add a new confext image type and the ability to parse their release files Adds a new image type called IMAGE_CONFEXT which is similar to IMAGE_SYSEXT but works for the /etc/ directory instead of /usr/ and /opt/. This commit also adds the ability to parse the release file that is present with the confext image in /etc/confext-release.d/ directory. --- TODO | 24 ++++++-------- src/basic/os-util.c | 63 ++++++++++++++++++++++++++----------- src/basic/os-util.h | 29 +++++++++++------ src/core/namespace.c | 2 +- src/portable/portable.c | 10 +++--- src/shared/discover-image.c | 18 +++++++---- src/shared/discover-image.h | 12 +------ src/shared/dissect-image.c | 6 ++-- src/sysext/sysext.c | 4 +-- 9 files changed, 98 insertions(+), 70 deletions(-) diff --git a/TODO b/TODO index b0e4665ad8..73edc66d78 100644 --- a/TODO +++ b/TODO @@ -521,13 +521,13 @@ Features: * add support for asymmetric LUKS2 TPM based encryption. i.e. allow preparing an encrypted image on some host given a public key belonging to a specific other host, so that only hosts possessing the private key in the TPM2 chip - can decrypt the volume key and activate the volume. Usecase: systemd-syscfg - for a central orchestrator to generate syscfg images securely that can only + can decrypt the volume key and activate the volume. Usecase: systemd-confext + for a central orchestrator to generate confext images securely that can only be activated on one specific host (which can be used for installing a bunch of creds in /etc/credstore/ for example). Extending on this: allow binding LUKS2 TPM based encryption also to the TPM2 internal clock. Net result: - prepare a syscfg image that can only be activated on a specific host that - runs a specific software in a specific time window. syscfg would be + prepare a confext image that can only be activated on a specific host that + runs a specific software in a specific time window. confext would be automatically invalidated outside of it. * maybe add a "systemd-report" tool, that generates a TPM2-backed "report" of @@ -538,17 +538,17 @@ Features: this: have the report tool upload these reports every 3min somewhere. Then have the orchestrator collect these reports centrally over a 3min time window, and use them to determine what which node should now start/stop what, - and generate a small syscfg for each node, that uses Uphold= to pin services - on each node. The syscfg would be encrypted using the asymmetric encryption + and generate a small confext for each node, that uses Uphold= to pin services + on each node. The confext would be encrypted using the asymmetric encryption proposed above, so that it can only be activated on the specific host, if the software is in a good state, and within a specific time frame. Then run a loop on each node that sends report to orchestrator and then sysupdate to - update syscfg. Orchestrator would be stateless, i.e. operate on desired + update confext. Orchestrator would be stateless, i.e. operate on desired config and collected reports in the last 3min time window only, and thus can be trivially scaled up since all instances of the orchestrator should come to the same conclusions given the same inputs of reports/desired workload info. Could also be used to deliver Wireguard secrets and thus to clients, thus - permitting zero-trust networking: secrets are rolled over via syscfg updates, + permitting zero-trust networking: secrets are rolled over via confext updates, and via the time window TPM logic invalidated if node doesn't keep itself updated, or becomes corrupted in some way. @@ -597,7 +597,7 @@ Features: keyring, so that the kernel does this validation for us for verity and kernel modules -* for systemd-syscfg: add a tool that can generate suitable DDIs with verity + +* for systemd-confext: add a tool that can generate suitable DDIs with verity + sig using squashfs-tools-ng's library. Maybe just systemd-repart called under a new name with a built-in config? @@ -914,12 +914,6 @@ Features: * sysext: measure all activated sysext into a TPM PCR -* maybe add a "syscfg" concept, that is almost entirely identical to "sysext", - but operates on /etc/ instead of /usr/ and /opt/. Use case would be: trusted, - authenticated, atomic, additive configuration management primitive: drop in a - configuration bundle, and activate it, so that it is instantly visible, - comprehensively. - * systemd-dissect: show available versions inside of a disk image, i.e. if multiple versions are around of the same resource, show which ones. (in other words: show partition labels). diff --git a/src/basic/os-util.c b/src/basic/os-util.c index c8b23b10e4..8cb8d9302b 100644 --- a/src/basic/os-util.c +++ b/src/basic/os-util.c @@ -19,6 +19,21 @@ #include "utf8.h" #include "xattr-util.h" +/* Helper struct for naming simplicity and reusability */ +static const struct { + const char *release_file_directory; + const char *release_file_path_prefix; +} image_class_release_info[_IMAGE_CLASS_MAX] = { + [IMAGE_SYSEXT] = { + .release_file_directory = "/usr/lib/extension-release.d/", + .release_file_path_prefix = "/usr/lib/extension-release.d/extension-release.", + }, + [IMAGE_CONFEXT] = { + .release_file_directory = "/etc/extension-release.d/", + .release_file_path_prefix = "/etc/extension-release.d/extension-release.", + } +}; + bool image_name_is_valid(const char *s) { if (!filename_is_valid(s)) return false; @@ -36,10 +51,12 @@ bool image_name_is_valid(const char *s) { return true; } -int path_is_extension_tree(const char *path, const char *extension, bool relax_extension_release_check) { +int path_is_extension_tree(ImageClass image_class, const char *path, const char *extension, bool relax_extension_release_check) { int r; assert(path); + assert(image_class >= 0); + assert(image_class < _IMAGE_CLASS_MAX); /* Does the path exist at all? If not, generate an error immediately. This is useful so that a missing root dir * always results in -ENOENT, and we can properly distinguish the case where the whole root doesn't exist from @@ -48,8 +65,9 @@ int path_is_extension_tree(const char *path, const char *extension, bool relax_e return -errno; /* We use /usr/lib/extension-release.d/extension-release[.NAME] as flag for something being a system extension, + * /etc/extension-release.d/extension-release[.NAME] as flag for something being a system configuration, and finally, * and {/etc|/usr/lib}/os-release as a flag for something being an OS (when not an extension). */ - r = open_extension_release(path, extension, relax_extension_release_check, NULL, NULL); + r = open_extension_release(path, image_class, extension, relax_extension_release_check, NULL, NULL); if (r == -ENOENT) /* We got nothing */ return 0; if (r < 0) @@ -96,18 +114,21 @@ static int extension_release_strict_xattr_value(int extension_release_fd, const return false; } -int open_extension_release(const char *root, const char *extension, bool relax_extension_release_check, char **ret_path, int *ret_fd) { +int open_extension_release(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char **ret_path, int *ret_fd) { _cleanup_free_ char *q = NULL; int r, fd; if (extension) { + assert(image_class >= 0); + assert(image_class < _IMAGE_CLASS_MAX); + const char *extension_full_path; if (!image_name_is_valid(extension)) return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "The extension name %s is invalid.", extension); - extension_full_path = strjoina("/usr/lib/extension-release.d/extension-release.", extension); + extension_full_path = strjoina(image_class_release_info[image_class].release_file_path_prefix, extension); r = chase(extension_full_path, root, CHASE_PREFIX_ROOT, ret_path ? &q : NULL, ret_fd ? &fd : NULL); log_full_errno_zerook(LOG_DEBUG, MIN(r, 0), "Checking for %s: %m", extension_full_path); @@ -120,10 +141,10 @@ int open_extension_release(const char *root, const char *extension, bool relax_e _cleanup_free_ char *extension_release_dir_path = NULL; _cleanup_closedir_ DIR *extension_release_dir = NULL; - r = chase_and_opendir("/usr/lib/extension-release.d/", root, CHASE_PREFIX_ROOT, + r = chase_and_opendir(image_class_release_info[image_class].release_file_directory, root, CHASE_PREFIX_ROOT, &extension_release_dir_path, &extension_release_dir); if (r < 0) - return log_debug_errno(r, "Cannot open %s/usr/lib/extension-release.d/, ignoring: %m", root); + return log_debug_errno(r, "Cannot open %s%s, ignoring: %m", root, image_class_release_info[image_class].release_file_directory); r = -ENOENT; FOREACH_DIRENT(de, extension_release_dir, return -errno) { @@ -137,7 +158,7 @@ int open_extension_release(const char *root, const char *extension, bool relax_e continue; if (!image_name_is_valid(image_name)) { - log_debug("%s/%s is not a valid extension-release file name, ignoring.", + log_debug("%s/%s is not a valid release file name, ignoring.", extension_release_dir_path, de->d_name); continue; } @@ -149,7 +170,7 @@ int open_extension_release(const char *root, const char *extension, bool relax_e O_PATH|O_CLOEXEC|O_NOFOLLOW); if (extension_release_fd < 0) return log_debug_errno(errno, - "Failed to open extension-release file %s/%s: %m", + "Failed to open release file %s/%s: %m", extension_release_dir_path, de->d_name); @@ -219,16 +240,16 @@ int open_extension_release(const char *root, const char *extension, bool relax_e return 0; } -int fopen_extension_release(const char *root, const char *extension, bool relax_extension_release_check, char **ret_path, FILE **ret_file) { +int fopen_extension_release(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char **ret_path, FILE **ret_file) { _cleanup_free_ char *p = NULL; _cleanup_close_ int fd = -EBADF; FILE *f; int r; if (!ret_file) - return open_extension_release(root, extension, relax_extension_release_check, ret_path, NULL); + return open_extension_release(root, image_class, extension, relax_extension_release_check, ret_path, NULL); - r = open_extension_release(root, extension, relax_extension_release_check, ret_path ? &p : NULL, &fd); + r = open_extension_release(root, image_class, extension, relax_extension_release_check, ret_path ? &p : NULL, &fd); if (r < 0) return r; @@ -243,24 +264,27 @@ int fopen_extension_release(const char *root, const char *extension, bool relax_ return 0; } -static int parse_release_internal(const char *root, bool relax_extension_release_check, const char *extension, va_list ap) { +static int parse_release_internal(const char *root, ImageClass image_class, bool relax_extension_release_check, const char *extension, va_list ap) { _cleanup_fclose_ FILE *f = NULL; _cleanup_free_ char *p = NULL; int r; - r = fopen_extension_release(root, extension, relax_extension_release_check, &p, &f); + r = fopen_extension_release(root, image_class, extension, relax_extension_release_check, &p, &f); if (r < 0) return r; return parse_env_filev(f, p, ap); } -int _parse_extension_release(const char *root, bool relax_extension_release_check, const char *extension, ...) { +int _parse_extension_release(const char *root, ImageClass image_class, bool relax_extension_release_check, const char *extension, ...) { va_list ap; int r; + assert(image_class >= 0); + assert(image_class < _IMAGE_CLASS_MAX); + va_start(ap, extension); - r = parse_release_internal(root, relax_extension_release_check, extension, ap); + r = parse_release_internal(root, image_class, relax_extension_release_check, extension, ap); va_end(ap); return r; @@ -271,7 +295,7 @@ int _parse_os_release(const char *root, ...) { int r; va_start(ap, root); - r = parse_release_internal(root, /* relax_extension_release_check= */ false, NULL, ap); + r = parse_release_internal(root, -1, /* relax_extension_release_check= */ false, NULL, ap); va_end(ap); return r; @@ -318,12 +342,15 @@ int load_os_release_pairs_with_prefix(const char *root, const char *prefix, char return 0; } -int load_extension_release_pairs(const char *root, const char *extension, bool relax_extension_release_check, char ***ret) { +int load_extension_release_pairs(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char ***ret) { _cleanup_fclose_ FILE *f = NULL; _cleanup_free_ char *p = NULL; int r; - r = fopen_extension_release(root, extension, relax_extension_release_check, &p, &f); + assert(image_class >= 0); + assert(image_class < _IMAGE_CLASS_MAX); + + r = fopen_extension_release(root, image_class, extension, relax_extension_release_check, &p, &f); if (r < 0) return r; diff --git a/src/basic/os-util.h b/src/basic/os-util.h index 3bafeaeb92..2f641ba9cd 100644 --- a/src/basic/os-util.h +++ b/src/basic/os-util.h @@ -5,33 +5,44 @@ #include #include "time-util.h" +typedef enum ImageClass { + IMAGE_MACHINE, + IMAGE_PORTABLE, + IMAGE_SYSEXT, + IMAGE_CONFEXT, + _IMAGE_CLASS_MAX, + _IMAGE_CLASS_INVALID = -EINVAL, +} ImageClass; + +const char* image_class_to_string(ImageClass cl) _const_; +ImageClass image_class_from_string(const char *s) _pure_; /* The *_extension_release flavours will look for /usr/lib/extension-release/extension-release.NAME * in accordance with the OS extension specification, rather than for /usr/lib/ or /etc/os-release. */ bool image_name_is_valid(const char *s) _pure_; -int path_is_extension_tree(const char *path, const char *extension, bool relax_extension_release_check); +int path_is_extension_tree(ImageClass image_class, const char *path, const char *extension, bool relax_extension_release_check); static inline int path_is_os_tree(const char *path) { - return path_is_extension_tree(path, NULL, false); + return path_is_extension_tree(IMAGE_SYSEXT, path, NULL, false); } -int open_extension_release(const char *root, const char *extension, bool relax_extension_release_check, char **ret_path, int *ret_fd); +int open_extension_release(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char **ret_path, int *ret_fd); static inline int open_os_release(const char *root, char **ret_path, int *ret_fd) { - return open_extension_release(root, NULL, false, ret_path, ret_fd); + return open_extension_release(root, IMAGE_SYSEXT, NULL, false, ret_path, ret_fd); } -int fopen_extension_release(const char *root, const char *extension, bool relax_extension_release_check, char **ret_path, FILE **ret_file); +int fopen_extension_release(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char **ret_path, FILE **ret_file); static inline int fopen_os_release(const char *root, char **ret_path, FILE **ret_file) { - return fopen_extension_release(root, NULL, false, ret_path, ret_file); + return fopen_extension_release(root, IMAGE_SYSEXT, NULL, false, ret_path, ret_file); } -int _parse_extension_release(const char *root, bool relax_extension_release_check, const char *extension, ...) _sentinel_; +int _parse_extension_release(const char *root, ImageClass image_class, bool relax_extension_release_check, const char *extension, ...) _sentinel_; int _parse_os_release(const char *root, ...) _sentinel_; -#define parse_extension_release(root, relax_extension_release_check, extension, ...) _parse_extension_release(root, relax_extension_release_check, extension, __VA_ARGS__, NULL) +#define parse_extension_release(root, image_class, relax_extension_release_check, extension, ...) _parse_extension_release(root, image_class, relax_extension_release_check, extension, __VA_ARGS__, NULL) #define parse_os_release(root, ...) _parse_os_release(root, __VA_ARGS__, NULL) -int load_extension_release_pairs(const char *root, const char *extension, bool relax_extension_release_check, char ***ret); +int load_extension_release_pairs(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char ***ret); int load_os_release_pairs(const char *root, char ***ret); int load_os_release_pairs_with_prefix(const char *root, const char *prefix, char ***ret); diff --git a/src/core/namespace.c b/src/core/namespace.c index 8b141a2484..531970ee15 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -1423,7 +1423,7 @@ static int apply_one_mount( if (isempty(host_os_release_id)) return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "'ID' field not found or empty in 'os-release' data of OS tree '%s': %m", empty_to_root(root_directory)); - r = load_extension_release_pairs(mount_entry_source(m), extension_name, /* relax_extension_release_check= */ false, &extension_release); + r = load_extension_release_pairs(mount_entry_source(m), IMAGE_SYSEXT, extension_name, /* relax_extension_release_check= */ false, &extension_release); if (r == -ENOENT && m->ignore) return 0; if (r < 0) diff --git a/src/portable/portable.c b/src/portable/portable.c index adfd846bab..1878157e65 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -198,7 +198,7 @@ static int extract_now( /* First, find os-release/extension-release and send it upstream (or just save it). */ if (path_is_extension) { os_release_id = strjoina("/usr/lib/extension-release.d/extension-release.", image_name); - r = open_extension_release(where, image_name, relax_extension_release_check, &os_release_path, &os_release_fd); + r = open_extension_release(where, IMAGE_SYSEXT, image_name, relax_extension_release_check, &os_release_path, &os_release_fd); } else { os_release_id = "/etc/os-release"; r = open_os_release(where, &os_release_path, &os_release_fd); @@ -948,17 +948,17 @@ static int append_release_log_fields( static const char *const field_versions[_IMAGE_CLASS_MAX][4]= { [IMAGE_PORTABLE] = { "IMAGE_VERSION", "VERSION_ID", "BUILD_ID", NULL }, - [IMAGE_EXTENSION] = { "SYSEXT_IMAGE_VERSION", "SYSEXT_VERSION_ID", "SYSEXT_BUILD_ID", NULL }, + [IMAGE_SYSEXT] = { "SYSEXT_IMAGE_VERSION", "SYSEXT_VERSION_ID", "SYSEXT_BUILD_ID", NULL }, }; static const char *const field_ids[_IMAGE_CLASS_MAX][3]= { [IMAGE_PORTABLE] = { "IMAGE_ID", "ID", NULL }, - [IMAGE_EXTENSION] = { "SYSEXT_IMAGE_ID", "SYSEXT_ID", NULL }, + [IMAGE_SYSEXT] = { "SYSEXT_IMAGE_ID", "SYSEXT_ID", NULL }, }; _cleanup_strv_free_ char **fields = NULL; const char *id = NULL, *version = NULL; int r; - assert(IN_SET(type, IMAGE_PORTABLE, IMAGE_EXTENSION)); + assert(IN_SET(type, IMAGE_PORTABLE, IMAGE_SYSEXT)); assert(!strv_isempty((char *const *)field_ids[type])); assert(!strv_isempty((char *const *)field_versions[type])); assert(field_name); @@ -1106,7 +1106,7 @@ static int install_chroot_dropin( * still be able to identify what applies to what. */ r = append_release_log_fields(&text, ordered_hashmap_get(extension_releases, ext->name), - IMAGE_EXTENSION, + IMAGE_SYSEXT, "PORTABLE_EXTENSION_NAME_AND_VERSION"); if (r < 0) return r; diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index eed0a5629e..0343d2e20b 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -63,9 +63,14 @@ static const char* const image_search_path[_IMAGE_CLASS_MAX] = { * because extension images are supposed to extend /usr/, so you get into recursive races, especially * with directory-based extensions, as the kernel's OverlayFS explicitly checks for this and errors * out with -ELOOP if it finds that a lowerdir= is a child of another lowerdir=. */ - [IMAGE_EXTENSION] = "/etc/extensions\0" /* only place symlinks here */ - "/run/extensions\0" /* and here too */ - "/var/lib/extensions\0", /* the main place for images */ + [IMAGE_SYSEXT] = "/etc/extensions\0" /* only place symlinks here */ + "/run/extensions\0" /* and here too */ + "/var/lib/extensions\0", /* the main place for images */ + + [IMAGE_CONFEXT] = "/run/confexts\0" /* only place symlinks here */ + "/var/lib/confexts\0" /* the main place for images */ + "/usr/local/lib/confexts\0" + "/usr/lib/confexts\0", }; static Image *image_free(Image *i) { @@ -1152,7 +1157,7 @@ int image_read_metadata(Image *i) { _cleanup_free_ char *hostname = NULL; _cleanup_free_ char *path = NULL; - if (i->class == IMAGE_EXTENSION) { + if (i->class == IMAGE_SYSEXT) { r = extension_has_forbidden_content(i->path); if (r < 0) return r; @@ -1190,7 +1195,7 @@ int image_read_metadata(Image *i) { if (r < 0) log_debug_errno(r, "Failed to read os-release in image, ignoring: %m"); - r = load_extension_release_pairs(i->path, i->name, /* relax_extension_release_check= */ false, &extension_release); + r = load_extension_release_pairs(i->path, i->class, i->name, /* relax_extension_release_check= */ false, &extension_release); if (r < 0) log_debug_errno(r, "Failed to read extension-release in image, ignoring: %m"); @@ -1325,7 +1330,8 @@ DEFINE_STRING_TABLE_LOOKUP(image_type, ImageType); static const char* const image_class_table[_IMAGE_CLASS_MAX] = { [IMAGE_MACHINE] = "machine", [IMAGE_PORTABLE] = "portable", - [IMAGE_EXTENSION] = "extension", + [IMAGE_SYSEXT] = "extension", + [IMAGE_CONFEXT] = "confext" }; DEFINE_STRING_TABLE_LOOKUP(image_class, ImageClass); diff --git a/src/shared/discover-image.h b/src/shared/discover-image.h index 3c6928619c..342b161577 100644 --- a/src/shared/discover-image.h +++ b/src/shared/discover-image.h @@ -9,18 +9,11 @@ #include "hashmap.h" #include "lock-util.h" #include "macro.h" +#include "os-util.h" #include "path-util.h" #include "string-util.h" #include "time-util.h" -typedef enum ImageClass { - IMAGE_MACHINE, - IMAGE_PORTABLE, - IMAGE_EXTENSION, - _IMAGE_CLASS_MAX, - _IMAGE_CLASS_INVALID = -EINVAL, -} ImageClass; - typedef enum ImageType { IMAGE_DIRECTORY, IMAGE_SUBVOLUME, @@ -77,9 +70,6 @@ int image_read_only(Image *i, bool b); const char* image_type_to_string(ImageType t) _const_; ImageType image_type_from_string(const char *s) _pure_; -const char* image_class_to_string(ImageClass cl) _const_; -ImageClass image_class_from_string(const char *s) _pure_; - int image_path_lock(const char *path, int operation, LockFile *global, LockFile *local); int image_name_lock(const char *name, int operation, LockFile *ret); diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 6000af0ce0..03dcd45e35 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -1791,7 +1791,7 @@ int dissected_image_mount( if (r < 0) return r; if (r == 0) { - r = path_is_extension_tree(where, m->image_name, FLAGS_SET(flags, DISSECT_IMAGE_RELAX_SYSEXT_CHECK)); + r = path_is_extension_tree(IMAGE_SYSEXT, where, m->image_name, FLAGS_SET(flags, DISSECT_IMAGE_RELAX_SYSEXT_CHECK)); if (r < 0) return r; if (r > 0) @@ -3054,7 +3054,7 @@ int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_ * we allow a fallback that matches on the first extension-release * file found in the directory, if one named after the image cannot * be found first. */ - r = open_extension_release(t, m->image_name, /* relax_extension_release_check= */ false, NULL, &fd); + r = open_extension_release(t, IMAGE_SYSEXT, m->image_name, /* relax_extension_release_check= */ false, NULL, &fd); if (r < 0) fd = r; /* Propagate the error. */ break; @@ -3606,7 +3606,7 @@ int verity_dissect_and_mount( assert(!isempty(required_host_os_release_id)); - r = load_extension_release_pairs(dest, dissected_image->image_name, relax_extension_release_check, &extension_release); + r = load_extension_release_pairs(dest, IMAGE_SYSEXT, dissected_image->image_name, relax_extension_release_check, &extension_release); if (r < 0) return log_debug_errno(r, "Failed to parse image %s extension-release metadata: %m", dissected_image->image_name); diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c index 5632b72f3d..e7d8e801fc 100644 --- a/src/sysext/sysext.c +++ b/src/sysext/sysext.c @@ -729,7 +729,7 @@ static int image_discover_and_read_metadata(Hashmap **ret_images) { if (!images) return log_oom(); - r = image_discover(IMAGE_EXTENSION, arg_root, images); + r = image_discover(IMAGE_SYSEXT, arg_root, images); if (r < 0) return log_error_errno(r, "Failed to discover extension images: %m"); @@ -832,7 +832,7 @@ static int verb_list(int argc, char **argv, void *userdata) { if (!images) return log_oom(); - r = image_discover(IMAGE_EXTENSION, arg_root, images); + r = image_discover(IMAGE_SYSEXT, arg_root, images); if (r < 0) return log_error_errno(r, "Failed to discover extension images: %m"); From 30dfe035eb8a6539f5997a798402d2d5225f8567 Mon Sep 17 00:00:00 2001 From: maanyagoenka Date: Wed, 29 Mar 2023 20:34:21 +0000 Subject: [PATCH 2/8] extension-release: establish compatibility between host file and extension-release file The release file that accompanies the confext images needs to be host compatible to be able to be merged into the host /etc/ directory. This commit checks for version compatibility between the image file and the host file. --- src/core/namespace.c | 5 ++- src/portable/portable.c | 2 +- src/shared/dissect-image.c | 3 +- src/shared/extension-util.c | 82 ++++++++++++++++++++----------------- src/shared/extension-util.h | 13 +++--- src/sysext/sysext.c | 5 ++- 6 files changed, 62 insertions(+), 48 deletions(-) diff --git a/src/core/namespace.c b/src/core/namespace.c index 531970ee15..a71beeb18b 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -1435,7 +1435,8 @@ static int apply_one_mount( host_os_release_version_id, host_os_release_sysext_level, /* host_sysext_scope */ NULL, /* Leave empty, we need to accept both system and portable */ - extension_release); + extension_release, + IMAGE_SYSEXT); if (r == 0) return log_debug_errno(SYNTHETIC_ERRNO(ESTALE), "Directory %s extension-release metadata does not match the root's", extension_name); if (r < 0) @@ -2155,7 +2156,7 @@ int setup_namespace( } if (n_extension_images > 0 || !strv_isempty(extension_directories)) { - r = parse_env_extension_hierarchies(&hierarchies); + r = parse_env_extension_hierarchies(&hierarchies, "SYSTEMD_SYSEXT_HIERARCHIES"); if (r < 0) return r; } diff --git a/src/portable/portable.c b/src/portable/portable.c index 1878157e65..f3fc06f6fd 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -607,7 +607,7 @@ static int extract_image_and_extensions( return r; if (validate_sysext) { - r = extension_release_validate(ext->path, id, version_id, sysext_level, "portable", extension_release); + r = extension_release_validate(ext->path, id, version_id, sysext_level, "portable", extension_release, IMAGE_SYSEXT); 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 03dcd45e35..a68610b80e 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -3616,7 +3616,8 @@ int verity_dissect_and_mount( required_host_os_release_version_id, required_host_os_release_sysext_level, required_sysext_scope, - extension_release); + extension_release, + IMAGE_SYSEXT); 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/extension-util.c b/src/shared/extension-util.c index fa83f6b6fd..43a19bf262 100644 --- a/src/shared/extension-util.c +++ b/src/shared/extension-util.c @@ -13,39 +13,42 @@ int extension_release_validate( const char *name, 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 *host_os_extension_release_level, + const char *host_extension_scope, + char **extension_release, + ImageClass image_class) { - const char *extension_release_id = NULL, *extension_release_sysext_level = NULL, *extension_architecture = NULL; + const char *extension_release_id = NULL, *extension_release_level = NULL, *extension_architecture = NULL; + const char *extension_level = image_class == IMAGE_CONFEXT ? "CONFEXT_LEVEL" : "SYSEXT_LEVEL"; + const char *extension_scope = image_class == IMAGE_CONFEXT ? "CONFEXT_SCOPE" : "SYSEXT_SCOPE"; assert(name); assert(!isempty(host_os_release_id)); - /* Now that we can look into the extension image, let's see if the OS version is compatible */ + /* Now that we can look into the extension/confext image, let's see if the OS version is compatible */ if (strv_isempty(extension_release)) { - log_debug("Extension '%s' carries no extension-release data, ignoring extension.", name); + log_debug("Extension '%s' carries no release data, ignoring.", name); return 0; } - if (host_sysext_scope) { - _cleanup_strv_free_ char **extension_sysext_scope_list = NULL; - const char *extension_sysext_scope; + if (host_extension_scope) { + _cleanup_strv_free_ char **scope_list = NULL; + const char *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) + scope = strv_env_pairs_get(extension_release, extension_scope); + if (scope) { + scope_list = strv_split(scope, WHITESPACE); + if (!scope_list) return -ENOMEM; } - /* by default extension are good for attachment in portable service and on the system */ + /* 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); + scope_list ?: STRV_MAKE("system", "portable"), + host_extension_scope); if (!valid) { - log_debug("Extension '%s' is not suitable for scope %s, ignoring extension.", name, host_sysext_scope); + log_debug("Extension '%s' is not suitable for scope %s, ignoring.", name, host_extension_scope); return 0; } } @@ -54,21 +57,21 @@ int extension_release_validate( * the future we could check if the kernel also supports 32 bit or binfmt has a translator set up for the architecture */ extension_architecture = strv_env_pairs_get(extension_release, "ARCHITECTURE"); if (!isempty(extension_architecture) && !streq(extension_architecture, "_any") && - !streq(architecture_to_string(uname_architecture()), extension_architecture)) { + !streq(architecture_to_string(uname_architecture()), extension_architecture)) { log_debug("Extension '%s' is for architecture '%s', but deployed on top of '%s'.", - name, extension_architecture, architecture_to_string(uname_architecture())); + name, extension_architecture, architecture_to_string(uname_architecture())); 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' or be '_any'", - name, host_os_release_id); + log_debug("Extension '%s' does not contain ID in release file but requested to match '%s' or be '_any'", + name, host_os_release_id); return 0; } - /* A sysext with no host OS dependency (static binaries or scripts) can match - * '_any' host OS, and VERSION_ID or SYSEXT_LEVEL are not required anywhere */ + /* A sysext(or confext) with no host OS dependency (static binaries or scripts) can match + * '_any' host OS, and VERSION_ID or SYSEXT_LEVEL(or CONFEXT_LEVEL) are not required anywhere */ if (streq(extension_release_id, "_any")) { log_debug("Extension '%s' matches '_any' OS.", name); return 1; @@ -81,18 +84,18 @@ int extension_release_validate( } /* Rolling releases do not typically set VERSION_ID (eg: ArchLinux) */ - if (isempty(host_os_release_version_id) && isempty(host_os_release_sysext_level)) { + if (isempty(host_os_release_version_id) && isempty(host_os_extension_release_level)) { log_debug("No version info on the host (rolling release?), but ID in %s matched.", name); return 1; } /* If the extension has a sysext API level declared, then it must match the host API * level. Otherwise, compare OS version as a whole */ - extension_release_sysext_level = strv_env_pairs_get(extension_release, "SYSEXT_LEVEL"); - if (!isempty(host_os_release_sysext_level) && !isempty(extension_release_sysext_level)) { - if (!streq_ptr(host_os_release_sysext_level, extension_release_sysext_level)) { - log_debug("Extension '%s' is for sysext API level '%s', but running on sysext API level '%s'", - name, strna(extension_release_sysext_level), strna(host_os_release_sysext_level)); + extension_release_level = strv_env_pairs_get(extension_release, extension_level); + if (!isempty(host_os_extension_release_level) && !isempty(extension_release_level)) { + if (!streq_ptr(host_os_extension_release_level, extension_release_level)) { + log_debug("Extension '%s' is for API level '%s', but running on API level '%s'", + name, strna(extension_release_level), strna(host_os_extension_release_level)); return 0; } } else if (!isempty(host_os_release_version_id)) { @@ -100,7 +103,7 @@ int extension_release_validate( extension_release_version_id = strv_env_pairs_get(extension_release, "VERSION_ID"); if (isempty(extension_release_version_id)) { - log_debug("Extension '%s' does not contain VERSION_ID in extension-release but requested to match '%s'", + log_debug("Extension '%s' does not contain VERSION_ID in release file but requested to match '%s'", name, strna(host_os_release_version_id)); return 0; } @@ -110,7 +113,7 @@ int extension_release_validate( name, strna(extension_release_version_id), strna(host_os_release_version_id)); return 0; } - } else if (isempty(host_os_release_version_id) && isempty(host_os_release_sysext_level)) { + } else if (isempty(host_os_release_version_id) && isempty(host_os_extension_release_level)) { /* Rolling releases do not typically set VERSION_ID (eg: ArchLinux) */ log_debug("No version info on the host (rolling release?), but ID in %s matched.", name); return 1; @@ -120,16 +123,21 @@ int extension_release_validate( return 1; } -int parse_env_extension_hierarchies(char ***ret_hierarchies) { +int parse_env_extension_hierarchies(char ***ret_hierarchies, const char *hierarchy_env) { _cleanup_free_ char **l = NULL; int r; - r = getenv_path_list("SYSTEMD_SYSEXT_HIERARCHIES", &l); + assert(hierarchy_env); + r = getenv_path_list(hierarchy_env, &l); if (r == -ENXIO) { - /* Default when unset */ - l = strv_new("/usr", "/opt"); - if (!l) - return -ENOMEM; + if (streq(hierarchy_env, "SYSTEMD_CONFEXT_HIERARCHIES")) + /* Default for confext when unset */ + l = strv_new("/etc"); + else if (streq(hierarchy_env, "SYSTEMD_SYSEXT_HIERARCHIES")) + /* Default for sysext when unset */ + l = strv_new("/usr", "/opt"); + else + return -ENXIO; } else if (r < 0) return r; diff --git a/src/shared/extension-util.h b/src/shared/extension-util.h index fba8acaf19..3cad219d51 100644 --- a/src/shared/extension-util.h +++ b/src/shared/extension-util.h @@ -1,6 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "os-util.h" + /* Given an image name (for logging purposes), a set of os-release values from the host and a key-value pair * vector of extension-release variables, check that the distro and (system extension level or distro * version) match and return 1, and 0 otherwise. */ @@ -8,12 +10,13 @@ int extension_release_validate( const char *name, 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 *host_os_extension_release_level, + const char *host_extension_scope, + char **extension_release, + ImageClass image_class); -/* Parse SYSTEMD_SYSEXT_HIERARCHIES and if not set, return "/usr /opt" */ -int parse_env_extension_hierarchies(char ***ret_hierarchies); +/* Parse hierarchy variables and if not set, return "/usr /opt" for sysext and "/etc" for confext */ +int parse_env_extension_hierarchies(char ***ret_hierarchies, const char *hierarchy_env); /* Insist that extension images do not overwrite the underlying OS release file (it's fine if they place one * in /etc/os-release, i.e. where things don't matter, as they aren't merged.) */ diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c index e7d8e801fc..97cf8ba52e 100644 --- a/src/sysext/sysext.c +++ b/src/sysext/sysext.c @@ -574,7 +574,8 @@ static int merge_subprocess(Hashmap *images, const char *workspace) { host_os_release_version_id, host_os_release_sysext_level, in_initrd() ? "initrd" : "system", - img->extension_release); + img->extension_release, + IMAGE_SYSEXT); if (r < 0) return r; if (r == 0) { @@ -996,7 +997,7 @@ static int run(int argc, char *argv[]) { /* For debugging purposes it might make sense to do this for other hierarchies than /usr/ and * /opt/, but let's make that a hacker/debugging feature, i.e. env var instead of cmdline * switch. */ - r = parse_env_extension_hierarchies(&arg_hierarchies); + r = parse_env_extension_hierarchies(&arg_hierarchies, "SYSTEMD_SYSEXT_HIERARCHIES"); if (r < 0) return log_error_errno(r, "Failed to parse $SYSTEMD_SYSEXT_HIERARCHIES environment variable: %m"); From 4da1df42ac419180f8bbfd6bc53a50e02b14af85 Mon Sep 17 00:00:00 2001 From: maanyagoenka Date: Wed, 29 Mar 2023 20:35:18 +0000 Subject: [PATCH 3/8] confext: add multi call functionality to sysext The confext concept is an extension of the existing sysext concept and allows to extend the host's filesystem or a unit's filesystem with signed images that add new files to the /etc/ directory using OverlayFS. --- src/sysext/meson.build | 4 ++ src/sysext/sysext.c | 92 +++++++++++++++++++++++++++++------------- 2 files changed, 68 insertions(+), 28 deletions(-) diff --git a/src/sysext/meson.build b/src/sysext/meson.build index f159adb8cc..ede953712c 100644 --- a/src/sysext/meson.build +++ b/src/sysext/meson.build @@ -1,3 +1,7 @@ # SPDX-License-Identifier: LGPL-2.1-or-later systemd_sysext_sources = files('sysext.c') + +meson.add_install_script(meson_make_symlink, + rootbindir / 'systemd-sysext', + rootbindir / 'systemd-confext') diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c index 97cf8ba52e..d235073743 100644 --- a/src/sysext/sysext.c +++ b/src/sysext/sysext.c @@ -39,16 +39,49 @@ #include "user-util.h" #include "verbs.h" -static char **arg_hierarchies = NULL; /* "/usr" + "/opt" by default */ +static char **arg_hierarchies = NULL; /* "/usr" + "/opt" by default for sysext and /etc by default for confext */ static char *arg_root = NULL; static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; static PagerFlags arg_pager_flags = 0; static bool arg_legend = true; static bool arg_force = false; +/* Is set to IMAGE_CONFEXT when systemd is called with the confext functionality instead of the default */ +static ImageClass arg_image_class = IMAGE_SYSEXT; + STATIC_DESTRUCTOR_REGISTER(arg_hierarchies, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_root, freep); +/* Helper struct for naming simplicity and reusability */ +static const struct { + const char *dot_directory_name; + const char *directory_name; + const char *short_identifier; + const char *short_identifier_plural; + const char *level_env; + const char *scope_env; + const char *name_env; +} image_class_info[_IMAGE_CLASS_MAX] = { + [IMAGE_SYSEXT] = { + .dot_directory_name = ".systemd-sysext", + .directory_name = "systemd-sysext", + .short_identifier = "sysext", + .short_identifier_plural = "extensions", + .level_env = "SYSEXT_LEVEL", + .scope_env = "SYSEXT_SCOPE", + .name_env = "SYSTEMD_SYSEXT_HIERARCHIES", + }, + [IMAGE_CONFEXT] = { + .dot_directory_name = ".systemd-confext", + .directory_name = "systemd-confext", + .short_identifier = "confext", + .short_identifier_plural = "confexts", + .level_env = "CONFEXT_LEVEL", + .scope_env = "CONFEXT_SCOPE", + .name_env = "SYSTEMD_CONFEXT_HIERARCHIES", + } +}; + static int is_our_mount_point(const char *p) { _cleanup_free_ char *buf = NULL, *f = NULL; struct stat st; @@ -70,26 +103,26 @@ static int is_our_mount_point(const char *p) { /* So we know now that it's a mount point. Now let's check if it's one of ours, so that we don't * accidentally unmount the user's own /usr/ but just the mounts we established ourselves. We do this * check by looking into the metadata directory we place in merged mounts: if the file - * .systemd-sysext/dev contains the major/minor device pair of the mount we have a good reason to + * ../dev contains the major/minor device pair of the mount we have a good reason to * believe this is one of our mounts. This thorough check has the benefit that we aren't easily * confused if people tar up one of our merged trees and untar them elsewhere where we might mistake * them for a live sysext tree. */ - f = path_join(p, ".systemd-sysext/dev"); + f = path_join(p, image_class_info[arg_image_class].dot_directory_name, "dev"); if (!f) return log_oom(); r = read_one_line_file(f, &buf); if (r == -ENOENT) { - log_debug("Hierarchy '%s' does not carry a .systemd-sysext/dev file, not a sysext merged tree.", p); + log_debug("Hierarchy '%s' does not carry a %s/dev file, not a merged tree.", p, image_class_info[arg_image_class].dot_directory_name); return false; } if (r < 0) - return log_error_errno(r, "Failed to determine whether hierarchy '%s' contains '.systemd-sysext/dev': %m", p); + return log_error_errno(r, "Failed to determine whether hierarchy '%s' contains '%s/dev': %m", p, image_class_info[arg_image_class].dot_directory_name); r = parse_devnum(buf, &dev); if (r < 0) - return log_error_errno(r, "Failed to parse device major/minor stored in '.systemd-sysext/dev' file on '%s': %m", p); + return log_error_errno(r, "Failed to parse device major/minor stored in '%s/dev' file on '%s': %m", image_class_info[arg_image_class].dot_directory_name, p); if (lstat(p, &st) < 0) return log_error_errno(r, "Failed to stat %s: %m", p); @@ -205,7 +238,7 @@ static int verb_status(int argc, char **argv, void *userdata) { continue; } - f = path_join(*p, ".systemd-sysext/extensions"); + f = path_join(*p, image_class_info[arg_image_class].dot_directory_name, image_class_info[arg_image_class].short_identifier_plural); if (!f) return log_oom(); @@ -272,7 +305,7 @@ static int mount_overlayfs( } /* Now mount the actual overlayfs */ - r = mount_nofollow_verbose(LOG_ERR, "sysext", where, "overlay", MS_RDONLY, options); + r = mount_nofollow_verbose(LOG_ERR, image_class_info[arg_image_class].short_identifier, where, "overlay", MS_RDONLY, options); if (r < 0) return r; @@ -315,7 +348,7 @@ static int merge_hierarchy( /* Let's generate a metadata file that lists all extensions we took into account for this * hierarchy. We include this in the final fs, to make things nicely discoverable and * recognizable. */ - f = path_join(meta_path, ".systemd-sysext/extensions"); + f = path_join(meta_path, image_class_info[arg_image_class].dot_directory_name, image_class_info[arg_image_class].short_identifier_plural); if (!f) return log_oom(); @@ -383,12 +416,12 @@ static int merge_hierarchy( /* Now we have mounted the new file system. Let's now figure out its .st_dev field, and make that * available in the metadata directory. This is useful to detect whether the metadata dir actually * belongs to the fs it is found on: if .st_dev of the top-level mount matches it, it's pretty likely - * we are looking at a live sysext tree, and not an unpacked tar or so of one. */ + * we are looking at a live tree, and not an unpacked tar or so of one. */ if (stat(overlay_path, &st) < 0) return log_error_errno(r, "Failed to stat mount '%s': %m", overlay_path); free(f); - f = path_join(meta_path, ".systemd-sysext/dev"); + f = path_join(meta_path, image_class_info[arg_image_class].dot_directory_name, "dev"); if (!f) return log_oom(); @@ -410,7 +443,7 @@ static int strverscmp_improvedp(char *const* a, char *const* b) { static int merge_subprocess(Hashmap *images, const char *workspace) { _cleanup_free_ char *host_os_release_id = NULL, *host_os_release_version_id = NULL, *host_os_release_sysext_level = NULL, - *buf = NULL; + *host_os_release_confext_level = NULL, *buf = NULL; _cleanup_strv_free_ char **extensions = NULL, **paths = NULL; size_t n_extensions = 0; unsigned n_ignored = 0; @@ -437,11 +470,13 @@ static int merge_subprocess(Hashmap *images, const char *workspace) { return r; /* Acquire host OS release info, so that we can compare it with the extension's data */ + char **host_os_release_level = (arg_image_class == IMAGE_CONFEXT) ? &host_os_release_confext_level : &host_os_release_sysext_level; r = parse_os_release( arg_root, "ID", &host_os_release_id, "VERSION_ID", &host_os_release_version_id, - "SYSEXT_LEVEL", &host_os_release_sysext_level); + image_class_info[arg_image_class].level_env, + host_os_release_level); if (r < 0) return log_error_errno(r, "Failed to acquire 'os-release' data of OS tree '%s': %m", empty_to_root(arg_root)); if (isempty(host_os_release_id)) @@ -453,7 +488,7 @@ static int merge_subprocess(Hashmap *images, const char *workspace) { HASHMAP_FOREACH(img, images) { _cleanup_free_ char *p = NULL; - p = path_join(workspace, "extensions", img->name); + p = path_join(workspace, image_class_info[arg_image_class].short_identifier_plural, img->name); if (!p) return log_oom(); @@ -575,7 +610,7 @@ static int merge_subprocess(Hashmap *images, const char *workspace) { host_os_release_sysext_level, in_initrd() ? "initrd" : "system", img->extension_release, - IMAGE_SYSEXT); + arg_image_class); if (r < 0) return r; if (r == 0) { @@ -620,7 +655,7 @@ static int merge_subprocess(Hashmap *images, const char *workspace) { assert_se(img = hashmap_get(images, extensions[n_extensions - 1 - k])); - p = path_join(workspace, "extensions", img->name); + p = path_join(workspace, image_class_info[arg_image_class].short_identifier_plural, img->name); if (!p) return log_oom(); @@ -696,7 +731,7 @@ static int merge(Hashmap *images) { pid_t pid; int r; - r = safe_fork("(sd-sysext)", FORK_DEATHSIG|FORK_LOG|FORK_NEW_MOUNTNS, &pid); + r = safe_fork("(sd-merge)", FORK_DEATHSIG|FORK_LOG|FORK_NEW_MOUNTNS, &pid); if (r < 0) return log_error_errno(r, "Failed to fork off child: %m"); if (r == 0) { @@ -712,7 +747,7 @@ static int merge(Hashmap *images) { _exit(r > 0 ? EXIT_SUCCESS : 123); /* 123 means: didn't find any extensions */ } - r = wait_for_terminate_and_check("(sd-sysext)", pid, WAIT_LOG_ABNORMAL); + r = wait_for_terminate_and_check("(sd-merge)", pid, WAIT_LOG_ABNORMAL); if (r < 0) return r; @@ -730,9 +765,9 @@ static int image_discover_and_read_metadata(Hashmap **ret_images) { if (!images) return log_oom(); - r = image_discover(IMAGE_SYSEXT, arg_root, images); + r = image_discover(arg_image_class, arg_root, images); if (r < 0) - return log_error_errno(r, "Failed to discover extension images: %m"); + return log_error_errno(r, "Failed to discover images: %m"); HASHMAP_FOREACH(img, images) { r = image_read_metadata(img); @@ -833,9 +868,9 @@ static int verb_list(int argc, char **argv, void *userdata) { if (!images) return log_oom(); - r = image_discover(IMAGE_SYSEXT, arg_root, images); + r = image_discover(arg_image_class, arg_root, images); if (r < 0) - return log_error_errno(r, "Failed to discover extension images: %m"); + return log_error_errno(r, "Failed to discover images: %m"); if ((arg_json_format_flags & JSON_FORMAT_OFF) && hashmap_isempty(images)) { log_info("No OS extensions found."); @@ -871,11 +906,11 @@ static int verb_help(int argc, char **argv, void *userdata) { return log_oom(); printf("%1$s [OPTIONS...] COMMAND\n" - "\n%5$sMerge extension images into /usr/ and /opt/ hierarchies.%6$s\n" - "\n%3$sCommands:%4$s\n" + "\n%5$sMerge extension images into /usr/ and /opt/ hierarchies for\n" + " sysext and into the /etc/ hierarchy for confext.%6$s\n" " status Show current merge status (default)\n" - " merge Merge extensions into /usr/ and /opt/\n" - " unmerge Unmerge extensions from /usr/ and /opt/\n" + " merge Merge extensions into relevant hierarchies\n" + " unmerge Unmerge extensions from relevant hierarchies\n" " refresh Unmerge/merge extensions again\n" " list List installed extensions\n" " -h --help Show this help\n" @@ -987,6 +1022,7 @@ static int sysext_main(int argc, char *argv[]) { static int run(int argc, char *argv[]) { int r; + arg_image_class = invoked_as(argv, "systemd-confext") ? IMAGE_CONFEXT : IMAGE_SYSEXT; log_setup(); @@ -997,9 +1033,9 @@ static int run(int argc, char *argv[]) { /* For debugging purposes it might make sense to do this for other hierarchies than /usr/ and * /opt/, but let's make that a hacker/debugging feature, i.e. env var instead of cmdline * switch. */ - r = parse_env_extension_hierarchies(&arg_hierarchies, "SYSTEMD_SYSEXT_HIERARCHIES"); + r = parse_env_extension_hierarchies(&arg_hierarchies, image_class_info[arg_image_class].name_env); if (r < 0) - return log_error_errno(r, "Failed to parse $SYSTEMD_SYSEXT_HIERARCHIES environment variable: %m"); + return log_error_errno(r, "Failed to parse environment variable: %m"); return sysext_main(argc, argv); } From 1f839f48e08f7e399718872d4137bd18671546ad Mon Sep 17 00:00:00 2001 From: maanyagoenka Date: Fri, 24 Feb 2023 08:33:15 +0000 Subject: [PATCH 4/8] confext: add the systemd-confext.service file --- units/meson.build | 1 + units/systemd-confext.service | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 units/systemd-confext.service diff --git a/units/meson.build b/units/meson.build index 06d68c1d6b..e6eb300661 100644 --- a/units/meson.build +++ b/units/meson.build @@ -138,6 +138,7 @@ units = [ ['systemd-reboot.service', ''], ['systemd-rfkill.socket', 'ENABLE_RFKILL'], ['systemd-sysext.service', 'ENABLE_SYSEXT'], + ['systemd-confext.service', 'ENABLE_SYSEXT'], ['systemd-sysupdate.timer', 'ENABLE_SYSUPDATE'], ['systemd-sysupdate-reboot.timer', 'ENABLE_SYSUPDATE'], ['systemd-sysusers.service', 'ENABLE_SYSUSERS', diff --git a/units/systemd-confext.service b/units/systemd-confext.service new file mode 100644 index 0000000000..b9d0b21508 --- /dev/null +++ b/units/systemd-confext.service @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Merge System Configuration Images into /etc/ +Documentation=man:systemd-confext.service(8) + +ConditionCapability=CAP_SYS_ADMIN +ConditionDirectoryNotEmpty=|/run/confexts +ConditionDirectoryNotEmpty=|/var/lib/confexts +ConditionDirectoryNotEmpty=|/usr/local/lib/confexts +ConditionDirectoryNotEmpty=|/usr/lib/confexts + +DefaultDependencies=no +After=local-fs.target +Before=sysinit.target systemd-tmpfiles-setup.service +Conflicts=shutdown.target initrd-switch-root.target +Before=shutdown.target initrd-switch-root.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=systemd-confext merge +ExecStop=systemd-confext unmerge + +[Install] +WantedBy=sysinit.target From c3c6a4f0a936fac6c24a8d4e1aaa09a8050e845c Mon Sep 17 00:00:00 2001 From: maanyagoenka Date: Fri, 24 Feb 2023 08:35:38 +0000 Subject: [PATCH 5/8] confext: add tests for systemd-confext --- test/units/testsuite-50.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/units/testsuite-50.sh b/test/units/testsuite-50.sh index 546a915a2e..3af90a69c6 100755 --- a/test/units/testsuite-50.sh +++ b/test/units/testsuite-50.sh @@ -483,6 +483,17 @@ test ! -e "/dev/loop/by-ref/$name" systemd-dissect --detach "${image}.raw" (! systemd-dissect --detach "${image}.raw") +# check for confext functionality +mkdir -p /run/confexts/test/etc/extension-release.d +echo "ID=_any" >/run/confexts/test/etc/extension-release.d/extension-release.test +echo "ARCHITECTURE=_any" >>/run/confexts/test/etc/extension-release.d/extension-release.test +echo "MARKER_CONFEXT_123" >/run/confexts/test/etc/testfile +systemd-confext merge +grep -q -F "MARKER_CONFEXT_123" /etc/testfile +systemd-confext status +systemd-confext unmerge +rm -rf /run/confexts/ + echo OK >/testok exit 0 From f7700ea599e9265910cab84a696d9f0fdc17f916 Mon Sep 17 00:00:00 2001 From: maanyagoenka Date: Fri, 31 Mar 2023 17:33:00 +0000 Subject: [PATCH 6/8] test-os-util: add tests for sysext and confext release files --- src/test/test-os-util.c | 48 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/test/test-os-util.c b/src/test/test-os-util.c index bc9e3ec91b..94680389fd 100644 --- a/src/test/test-os-util.c +++ b/src/test/test-os-util.c @@ -1,13 +1,17 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include - +#include "fileio.h" #include "fs-util.h" #include "log.h" +#include "mkdir.h" #include "os-util.h" +#include "path-util.h" +#include "rm-rf.h" #include "string-util.h" #include "strv.h" #include "tests.h" +#include "tmpfile-util.h" TEST(path_is_os_tree) { assert_se(path_is_os_tree("/") > 0); @@ -55,6 +59,48 @@ TEST(parse_os_release) { assert_se(unsetenv("SYSTEMD_OS_RELEASE") == 0); } +TEST(parse_extension_release) { + /* Let's assume that we have a valid extension image */ + _cleanup_free_ char *id = NULL, *version_id = NULL, *foobar = NULL, *a = NULL, *b = NULL; + _cleanup_(rm_rf_physical_and_freep) char *tempdir = NULL; + + int r = mkdtemp_malloc("/tmp/test-os-util.XXXXXX", &tempdir); + if (r < 0) + log_error_errno(r, "Failed to setup working directory: %m"); + + assert_se(a = path_join(tempdir, "/usr/lib/extension-release.d/extension-release.test")); + assert_se(mkdir_parents(a, 0777) >= 0); + + r = write_string_file(a, "ID=the-id \n VERSION_ID=the-version-id", WRITE_STRING_FILE_CREATE); + if (r < 0) + log_error_errno(r, "Failed to write file: %m"); + + assert_se(parse_extension_release(tempdir, IMAGE_SYSEXT, false, "test", "ID", &id, "VERSION_ID", &version_id) == 0); + log_info("ID: %s VERSION_ID: %s", id, version_id); + assert_se(streq(id, "the-id")); + assert_se(streq(version_id, "the-version-id")); + + assert_se(b = path_join(tempdir, "/etc/extension-release.d/extension-release.tester")); + assert_se(mkdir_parents(b, 0777) >= 0); + + r = write_string_file(b, "ID=\"ignored\" \n ID=\"the-id\" \n VERSION_ID='the-version-id'", WRITE_STRING_FILE_CREATE); + if (r < 0) + log_error_errno(r, "Failed to write file: %m"); + + assert_se(parse_extension_release(tempdir, IMAGE_CONFEXT, false, "tester", "ID", &id, "VERSION_ID", &version_id) == 0); + log_info("ID: %s VERSION_ID: %s", id, version_id); + assert_se(streq(id, "the-id")); + assert_se(streq(version_id, "the-version-id")); + + assert_se(parse_extension_release(tempdir, IMAGE_CONFEXT, false, "tester", "FOOBAR", &foobar) == 0); + log_info("FOOBAR: %s", strnull(foobar)); + assert_se(foobar == NULL); + + assert_se(parse_extension_release(tempdir, IMAGE_SYSEXT, false, "test", "FOOBAR", &foobar) == 0); + log_info("FOOBAR: %s", strnull(foobar)); + assert_se(foobar == NULL); +} + TEST(load_os_release_pairs) { _cleanup_(unlink_tempfilep) char tmpfile[] = "/tmp/test-os-util.XXXXXX"; assert_se(write_tmpfile(tmpfile, From 1f4f1666906e4dbd8126c98068eb2d4f69b879af Mon Sep 17 00:00:00 2001 From: maanyagoenka Date: Fri, 24 Feb 2023 08:42:45 +0000 Subject: [PATCH 7/8] confext: documentation and man page updates for confext --- docs/ENVIRONMENT.md | 4 ++- man/os-release.xml | 17 ++++++++++ man/rules/meson.build | 5 ++- man/systemd-sysext.xml | 72 ++++++++++++++++++++++++++++++++---------- 4 files changed, 80 insertions(+), 18 deletions(-) diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 3ec5573ff9..f29ca9226b 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -328,7 +328,9 @@ the journal instead of only when logging in debug mode. paths. Only "real" file systems and directories that only contain "real" file systems as submounts should be used. Do not specify API file systems such as `/proc/` or `/sys/` here, or hierarchies that have them as submounts. In - particular, do not specify the root directory `/` here. + particular, do not specify the root directory `/` here. Similarly, + `$SYSTEMD_CONFEXT_HIERARCHIES` works for confext images and supports the + systemd-confext multi-call functionality of sysext. `systemd-tmpfiles`: diff --git a/man/os-release.xml b/man/os-release.xml index e74f27b990..6cc786acf9 100644 --- a/man/os-release.xml +++ b/man/os-release.xml @@ -442,6 +442,17 @@ + + CONFEXT_LEVEL= + + Semantically the same as SYSEXT_LEVEL= but for confext images. + See /etc/extension-release.d/extension-release.IMAGE + for more information. + + Examples: CONFEXT_LEVEL=2, CONFEXT_LEVEL=15.14. + + + SYSEXT_SCOPE= Takes a space-separated list of one or more of the strings @@ -453,6 +464,12 @@ but not to initrd environments. + + CONFEXT_SCOPE= + + Semantically the same as SYSEXT_SCOPE= but for confext images. + + PORTABLE_PREFIXES= Takes a space-separated list of one or more valid prefix match strings for the diff --git a/man/rules/meson.build b/man/rules/meson.build index 63a68c3211..b6c88db390 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1043,7 +1043,10 @@ manpages = [ 'systemd-suspend-then-hibernate.service'], ''], ['systemd-sysctl.service', '8', ['systemd-sysctl'], ''], - ['systemd-sysext', '8', ['systemd-sysext.service'], ''], + ['systemd-sysext', + '8', + ['systemd-confext', 'systemd-confext.service', 'systemd-sysext.service'], + ''], ['systemd-system-update-generator', '8', [], ''], ['systemd-system.conf', '5', diff --git a/man/systemd-sysext.xml b/man/systemd-sysext.xml index 96e40ddf95..f3a12e0a1c 100644 --- a/man/systemd-sysext.xml +++ b/man/systemd-sysext.xml @@ -19,6 +19,8 @@ systemd-sysext systemd-sysext.service + systemd-confext + systemd-confext.service Activates System Extension Images @@ -31,6 +33,14 @@ systemd-sysext.service + + systemd-confext + OPTIONS + COMMAND + + + systemd-confext.service + @@ -129,6 +139,29 @@ The extension-release file follows the same format and semantics, and carries the same content, as the os-release file of the OS, but it describes the resources carried in the extension image. + + The systemd-confext concept follows the same principle as the + systemd-sysext1 + functionality but instead of working on /usr and /opt, + confext will extend only /etc. Files and directories contained + in the confext images outside of the /etc/ hierarchy are not + merged, and hence have no effect when included in the image. Formats for these images are of the + same as sysext images. + + Confexts are looked for in the directories /run/confexts/, + /var/lib/confexts/, /usr/lib/confexts/ and + /usr/local/lib/confexts/. The first two listed directories are not suitable for + carrying large binary images, however are still useful for carrying symlinks to them. The primary place + for installing system extensions is /var/lib/confexts/. Any directories found in + these search directories are considered directory based confext images, any files with the + .raw suffix are considered disk image based confext images. + + Again, just like sysext images, the confext images will contain a + /etc/extension-release.d/extension-release.$name + file, which must match the image name (with the usual escape hatch of xattr), and again with content + being one or more of ID=, VERSION_ID=, and + CONFEXT_LEVEL. Confext images will then be checked and matched against the + base OS layer. @@ -153,20 +186,25 @@ /usr/ as if it was installed in the OS image itself.) This case works regardless if the underlying host /usr/ is managed as immutable disk image or is a traditional package manager controlled (i.e. writable) tree. - + + For the confext case, the OSConfig project aims to perform runtime reconfiguration of OS services. + Sometimes, there is a need to swap certain configuration parameter values or restart only a specific + service without deployment of new code or a complete OS deployment. In other words, we want to be able + to tie the most frequently configured options to runtime updateable flags that can be changed without a + system reboot. This will help reduce servicing times when there is a need for changing the OS configuration. Commands - The following commands are understood: + The following commands are understood by both the sysext and confext concepts: When invoked without any command verb, or when is specified - the current merge status is shown, separately for both /usr/ and - /opt/. + the current merge status is shown, separately (for both /usr/ and + /opt/ of sysext and for /etc/ of confext). @@ -174,14 +212,15 @@ Merges all currently installed system extension images into /usr/ and /opt/, by overmounting these hierarchies with an overlayfs file system combining the underlying hierarchies with those included in - the extension images. This command will fail if the hierarchies are already merged. + the extension images. This command will fail if the hierarchies are already merged. For confext, the merge + happens into the /etc/ directory instead. Unmerges all currently installed system extension images from - /usr/ and /opt/, by unmounting the - overlayfs file systems created by + /usr/ and /opt/ for sysext and /etc/, + for confext, by unmounting the overlayfs file systems created by prior. @@ -191,11 +230,11 @@ mounted the existing overlayfs instance is unmounted temporarily, and then replaced by a new version. This command is useful after installing/removing system extension images, in order to update the overlayfs file system accordingly. If no system extensions - are installed when this command is executed, the equivalent of is - executed, without establishing any new overlayfs instance. Note that currently - there's a brief moment where neither the old nor the new overlayfs file system is - mounted. This implies that all resources supplied by a system extension will briefly disappear — even - if it exists continuously during the refresh operation. + are installed when this command is executed, the equivalent of is executed, + without establishing any new overlayfs instance. + Note that currently there's a brief moment where neither the old nor the new overlayfs + file system is mounted. This implies that all resources supplied by a system extension will briefly + disappear — even if it exists continuously during the refresh operation. @@ -218,16 +257,17 @@ Operate relative to the specified root directory, i.e. establish the overlayfs mount not on the top-level host /usr/ and - /opt/ hierarchies, but below some specified root directory. + /opt/ hierarchies for sysext or /etc/ for confext, + but below some specified root directory. When merging system extensions into /usr/ and - /opt/, ignore version incompatibilities, i.e. force merging regardless of - whether the version information included in the extension images matches the host or - not. + /opt/ for sysext and /etc/ for confext, + ignore version incompatibilities, i.e. force merging regardless of + whether the version information included in the images matches the host or not. From bbcc658e3583d20ab922fd3518670375271048fa Mon Sep 17 00:00:00 2001 From: maanyagoenka Date: Fri, 24 Feb 2023 08:44:36 +0000 Subject: [PATCH 8/8] confext: shell completion for systemd-confext --- shell-completion/bash/systemd-confext | 85 +++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 shell-completion/bash/systemd-confext diff --git a/shell-completion/bash/systemd-confext b/shell-completion/bash/systemd-confext new file mode 100644 index 0000000000..c8eca3b2cb --- /dev/null +++ b/shell-completion/bash/systemd-confext @@ -0,0 +1,85 @@ +# systemd-confext(8) completion -*- shell-script -*- +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# systemd is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with systemd; If not, see . + +__contains_word() { + local w word=$1; shift + for w in "$@"; do + [[ $w = "$word" ]] && return + done +} + +_systemd-confext() { + local i verb comps + local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} words cword + local -A OPTS=( + [STANDALONE]='-h --help --version + --no-pager + --no-legend + --force' + [ARG]='--root + --json' + ) + + local -A VERBS=( + [STANDALONE]='status + merge + unmerge + refresh + list' + ) + + _init_completion || return + + if __contains_word "$prev" ${OPTS[ARG]}; then + case $prev in + --root) + comps=$(compgen -A directory -- "$cur" ) + compopt -o dirnames + ;; + --json) + comps='pretty short off' + ;; + esac + COMPREPLY=( $(compgen -W '$comps' -- "$cur") ) + return 0 + fi + + if [[ "$cur" = -* ]]; then + COMPREPLY=( $(compgen -W '${OPTS[*]}' -- "$cur") ) + return 0 + fi + + for ((i=0; i < COMP_CWORD; i++)); do + if __contains_word "${COMP_WORDS[i]}" ${VERBS[*]} && + ! __contains_word "${COMP_WORDS[i-1]}" ${OPTS[ARG]}; then + verb=${COMP_WORDS[i]} + break + fi + done + + if [[ -z ${verb-} ]]; then + comps=${VERBS[*]} + elif __contains_word "$verb" ${VERBS[STANDALONE]}; then + comps='' + fi + + COMPREPLY=( $(compgen -W '$comps' -- "$cur") ) + return 0 +} + +complete -F _systemd-confext systemd-confext