diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 6396789e4a..65d28e6bfa 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -552,3 +552,6 @@ SYSTEMD_HOME_DEBUG_SUFFIX=foo \ * `$SYSTEMD_REPART_MKFS_OPTIONS_` – configure additional arguments to use for `mkfs` when formatting partition file systems. There's one variable for each of the supported file systems. + +* `$SYSTEMD_REPART_OVERRIDE_FSTYPE` – if set the value will override the file + system type specified in Format= lines in partition definition files. diff --git a/man/repart.d.xml b/man/repart.d.xml index cfdd39d5eb..79908a0805 100644 --- a/man/repart.d.xml +++ b/man/repart.d.xml @@ -468,10 +468,11 @@ This option cannot be combined with CopyBlocks=. When - systemd-repart8 - is invoked with the or command line switches the - source paths specified are taken relative to the specified root directory or disk image root. - + systemd-repart8 is + invoked with the command line switch the file paths are taken + relative to the specified directory. If is not used, but the + or switches are used, the source paths are taken + relative to the specified root directory or disk image root. diff --git a/man/systemd-repart.xml b/man/systemd-repart.xml index 89d7f2c285..d05b74f57d 100644 --- a/man/systemd-repart.xml +++ b/man/systemd-repart.xml @@ -276,6 +276,9 @@ so that the tool operates on the configuration and machine ID stored in the root file system later transitioned into itself. + See for a more restricted option that only affects + CopyFiles=. + @@ -500,6 +503,60 @@ + + PATH + PATH + + Specifies a source directory all CopyFiles= source paths shall be + considered relative to. This is similar to , but exclusively applies to the + CopyFiles= setting. If and + are used in combination the former applies as usual, except for + CopyFiles= where the latter takes precedence. + + + + + + TYPE + + Takes one of sysext, confext or + portable. Generates a Discoverable Disk Image (DDI) for a system extension + (sysext, see + systemd-sysext8 + for details), configuration extension (confext) or portable service. The generated image will consist + of a signed Verity erofs file system as root partition. In this mode of operation + the partition definitions in /usr/lib/repart.d/*.conf and related directories + are not read, and is not supported, as appropriate definitions for + the selected DDI class will be chosen automatically. + + Must be used in conjunction with to specify the file hierarchy + to populate the DDI with. The specified directory should contain an etc/ + subdirectory if confext is selected. If sysext is selected it + should contain either a usr/ or opt/ directory, or both. If + portable is used a full OS file hierarchy can be provided. + + This option implies , and + (the latter two can be overriden). + + The private key and certificate for signing the DDI must be specified via the + and switches. + + + + + + + + + + Shortcuts for , + , , + respectively. + + + + @@ -514,13 +571,41 @@ On success, 0 is returned, a non-zero failure code otherwise. + + Example + + + Generate a configuration extension image + + The following creates a configuration extension DDI (confext) for an + /etc/motd update. + + mkdir tree tree/etc tree/etc/extension-release.d +echo "Hello World" > tree/etc/motd +cat > tree/etc/extension-release.d/extension-release.my-motd <<EOF +ID=fedora +VERSION_ID=38 +IMAGE_ID=my-motd +IMAGE_VERSION=7 +EOF +systemd-repart -C --private-key=privkey.pem --certificate=cert.crt -s tree/ /var/lib/confexts/my-motd.confext.raw +systemd-confext refresh + + The DDI generated that way may be applied to the system with + systemd-confext1. + + + + See Also systemd1, repart.d5, machine-id5, - systemd-cryptenroll1 + systemd-cryptenroll1, + portablectl1, + systemd-sysext8 diff --git a/meson.build b/meson.build index 1e59d353ee..88bc1367e0 100644 --- a/meson.build +++ b/meson.build @@ -171,6 +171,7 @@ systemdstatedir = localstatedir / 'lib/systemd' catalogstatedir = systemdstatedir / 'catalog' randomseeddir = localstatedir / 'lib/systemd' profiledir = libexecdir / 'portable' / 'profile' +repartdefinitionsdir = libexecdir / 'repart/definitions' ntpservicelistdir = prefixdir / 'lib/systemd/ntp-units.d' credstoredir = prefixdir / 'lib/credstore' diff --git a/src/basic/fileio.c b/src/basic/fileio.c index c41db65eb1..527f53daee 100644 --- a/src/basic/fileio.c +++ b/src/basic/fileio.c @@ -1076,117 +1076,171 @@ int fdopen_independent(int fd, const char *mode, FILE **ret) { return 0; } -static int search_and_fopen_internal( +static int search_and_open_internal( const char *path, - const char *mode, + int mode, /* if ret_fd is NULL this is an [FRWX]_OK mode for access(), otherwise an open mode for open() */ const char *root, char **search, - FILE **ret, + int *ret_fd, char **ret_path) { + int r; + + assert(!ret_fd || !FLAGS_SET(mode, O_CREAT)); /* We don't support O_CREAT for this */ assert(path); - assert(mode); - assert(ret); + + if (path_is_absolute(path)) { + _cleanup_close_ int fd = -EBADF; + + if (ret_fd) + /* We only specify 0777 here to appease static analyzers, it's never used since we + * don't support O_CREAT here */ + r = fd = RET_NERRNO(open(path, mode, 0777)); + else + r = RET_NERRNO(access(path, mode)); + if (r < 0) + return r; + + if (ret_path) { + r = path_simplify_alloc(path, ret_path); + if (r < 0) + return r; + } + + if (ret_fd) + *ret_fd = TAKE_FD(fd); + + return 0; + } if (!path_strv_resolve_uniq(search, root)) return -ENOMEM; STRV_FOREACH(i, search) { + _cleanup_close_ int fd = -EBADF; _cleanup_free_ char *p = NULL; - FILE *f; p = path_join(root, *i, path); if (!p) return -ENOMEM; - f = fopen(p, mode); - if (f) { + if (ret_fd) + /* as above, 0777 is static analyzer appeasement */ + r = fd = RET_NERRNO(open(p, mode, 0777)); + else + r = RET_NERRNO(access(p, F_OK)); + if (r >= 0) { if (ret_path) *ret_path = path_simplify(TAKE_PTR(p)); - *ret = f; + if (ret_fd) + *ret_fd = TAKE_FD(fd); + return 0; } - - if (errno != ENOENT) - return -errno; + if (r != -ENOENT) + return r; } return -ENOENT; } -int search_and_fopen( - const char *filename, - const char *mode, +int search_and_open( + const char *path, + int mode, const char *root, - const char **search, - FILE **ret, + char **search, + int *ret_fd, char **ret_path) { _cleanup_strv_free_ char **copy = NULL; - int r; - assert(filename); - assert(mode); - assert(ret); - - if (path_is_absolute(filename)) { - _cleanup_fclose_ FILE *f = NULL; - - f = fopen(filename, mode); - if (!f) - return -errno; - - if (ret_path) { - r = path_simplify_alloc(filename, ret_path); - if (r < 0) - return r; - } - - *ret = TAKE_PTR(f); - return 0; - } + assert(path); copy = strv_copy((char**) search); if (!copy) return -ENOMEM; - return search_and_fopen_internal(filename, mode, root, copy, ret, ret_path); + return search_and_open_internal(path, mode, root, copy, ret_fd, ret_path); } -int search_and_fopen_nulstr( - const char *filename, +static int search_and_fopen_internal( + const char *path, const char *mode, const char *root, - const char *search, - FILE **ret, + char **search, + FILE **ret_file, char **ret_path) { - _cleanup_strv_free_ char **s = NULL; + _cleanup_free_ char *found_path = NULL; + _cleanup_close_ int fd = -EBADF; int r; - if (path_is_absolute(filename)) { - _cleanup_fclose_ FILE *f = NULL; + assert(path); + assert(mode || !ret_file); - f = fopen(filename, mode); + r = search_and_open( + path, + mode ? fopen_mode_to_flags(mode) : 0, + root, + search, + ret_file ? &fd : NULL, + ret_path ? &found_path : NULL); + if (r < 0) + return r; + + if (ret_file) { + FILE *f = take_fdopen(&fd, mode); if (!f) return -errno; - if (ret_path) { - r = path_simplify_alloc(filename, ret_path); - if (r < 0) - return r; - } - - *ret = TAKE_PTR(f); - return 0; + *ret_file = f; } - s = strv_split_nulstr(search); - if (!s) + if (ret_path) + *ret_path = TAKE_PTR(found_path); + + return 0; +} + +int search_and_fopen( + const char *path, + const char *mode, + const char *root, + const char **search, + FILE **ret_file, + char **ret_path) { + + _cleanup_strv_free_ char **copy = NULL; + + assert(path); + assert(mode || !ret_file); + + copy = strv_copy((char**) search); + if (!copy) return -ENOMEM; - return search_and_fopen_internal(filename, mode, root, s, ret, ret_path); + return search_and_fopen_internal(path, mode, root, copy, ret_file, ret_path); +} + +int search_and_fopen_nulstr( + const char *path, + const char *mode, + const char *root, + const char *search, + FILE **ret_file, + char **ret_path) { + + _cleanup_strv_free_ char **l = NULL; + + assert(path); + assert(mode || !ret_file); + + l = strv_split_nulstr(search); + if (!l) + return -ENOMEM; + + return search_and_fopen_internal(path, mode, root, l, ret_file, ret_path); } int fflush_and_check(FILE *f) { diff --git a/src/basic/fileio.h b/src/basic/fileio.h index 769bf394fd..cd18bcc117 100644 --- a/src/basic/fileio.h +++ b/src/basic/fileio.h @@ -129,8 +129,12 @@ static inline int fopen_unlocked(const char *path, const char *mode, FILE **ret) int fdopen_independent(int fd, const char *mode, FILE **ret); -int search_and_fopen(const char *path, const char *mode, const char *root, const char **search, FILE **ret, char **ret_path); -int search_and_fopen_nulstr(const char *path, const char *mode, const char *root, const char *search, FILE **ret, char **ret_path); +int search_and_open(const char *path, int mode, const char *root, char **search, int *ret_fd, char **ret_path); +static inline int search_and_access(const char *path, int mode, const char *root, char**search, char **ret_path) { + return search_and_open(path, mode, root, search, NULL, ret_path); +} +int search_and_fopen(const char *path, const char *mode, const char *root, const char **search, FILE **ret_file, char **ret_path); +int search_and_fopen_nulstr(const char *path, const char *mode, const char *root, const char *search, FILE **ret_file, char **ret_path); int fflush_and_check(FILE *f); int fflush_sync_and_check(FILE *f); diff --git a/src/partition/definitions/confext.repart.d/10-root.conf b/src/partition/definitions/confext.repart.d/10-root.conf new file mode 100644 index 0000000000..e41dc0578b --- /dev/null +++ b/src/partition/definitions/confext.repart.d/10-root.conf @@ -0,0 +1,15 @@ +# 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. + +[Partition] +Type=root +Format=erofs +CopyFiles=/etc/ +Verity=data +VerityMatchKey=root diff --git a/src/partition/definitions/confext.repart.d/20-root-verity.conf b/src/partition/definitions/confext.repart.d/20-root-verity.conf new file mode 100644 index 0000000000..437d88e068 --- /dev/null +++ b/src/partition/definitions/confext.repart.d/20-root-verity.conf @@ -0,0 +1,13 @@ +# 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. + +[Partition] +Type=root-verity +Verity=hash +VerityMatchKey=root diff --git a/src/partition/definitions/confext.repart.d/30-root-verity-sig.conf b/src/partition/definitions/confext.repart.d/30-root-verity-sig.conf new file mode 100644 index 0000000000..df160154a7 --- /dev/null +++ b/src/partition/definitions/confext.repart.d/30-root-verity-sig.conf @@ -0,0 +1,13 @@ +# 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. + +[Partition] +Type=root-verity-sig +Verity=signature +VerityMatchKey=root diff --git a/src/partition/definitions/portable.repart.d/10-root.conf b/src/partition/definitions/portable.repart.d/10-root.conf new file mode 100644 index 0000000000..78758002f5 --- /dev/null +++ b/src/partition/definitions/portable.repart.d/10-root.conf @@ -0,0 +1,15 @@ +# 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. + +[Partition] +Type=root +Format=erofs +CopyFiles=/ +Verity=data +VerityMatchKey=root diff --git a/src/partition/definitions/portable.repart.d/20-root-verity.conf b/src/partition/definitions/portable.repart.d/20-root-verity.conf new file mode 100644 index 0000000000..437d88e068 --- /dev/null +++ b/src/partition/definitions/portable.repart.d/20-root-verity.conf @@ -0,0 +1,13 @@ +# 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. + +[Partition] +Type=root-verity +Verity=hash +VerityMatchKey=root diff --git a/src/partition/definitions/portable.repart.d/30-root-verity-sig.conf b/src/partition/definitions/portable.repart.d/30-root-verity-sig.conf new file mode 100644 index 0000000000..df160154a7 --- /dev/null +++ b/src/partition/definitions/portable.repart.d/30-root-verity-sig.conf @@ -0,0 +1,13 @@ +# 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. + +[Partition] +Type=root-verity-sig +Verity=signature +VerityMatchKey=root diff --git a/src/partition/definitions/sysext.repart.d/10-root.conf b/src/partition/definitions/sysext.repart.d/10-root.conf new file mode 100644 index 0000000000..41a7ca56d7 --- /dev/null +++ b/src/partition/definitions/sysext.repart.d/10-root.conf @@ -0,0 +1,15 @@ +# 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. + +[Partition] +Type=root +Format=erofs +CopyFiles=/usr/ /opt/ +Verity=data +VerityMatchKey=root diff --git a/src/partition/definitions/sysext.repart.d/20-root-verity.conf b/src/partition/definitions/sysext.repart.d/20-root-verity.conf new file mode 100644 index 0000000000..437d88e068 --- /dev/null +++ b/src/partition/definitions/sysext.repart.d/20-root-verity.conf @@ -0,0 +1,13 @@ +# 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. + +[Partition] +Type=root-verity +Verity=hash +VerityMatchKey=root diff --git a/src/partition/definitions/sysext.repart.d/30-root-verity-sig.conf b/src/partition/definitions/sysext.repart.d/30-root-verity-sig.conf new file mode 100644 index 0000000000..df160154a7 --- /dev/null +++ b/src/partition/definitions/sysext.repart.d/30-root-verity-sig.conf @@ -0,0 +1,13 @@ +# 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. + +[Partition] +Type=root-verity-sig +Verity=signature +VerityMatchKey=root diff --git a/src/partition/meson.build b/src/partition/meson.build index 19f49f606e..78cde2ff52 100644 --- a/src/partition/meson.build +++ b/src/partition/meson.build @@ -48,3 +48,15 @@ executables += [ 'install' : have_standalone_binaries, }, ] + +if conf.get('ENABLE_REPART') == 1 + install_data('definitions/confext.repart.d/10-root.conf', install_dir : repartdefinitionsdir / 'confext.repart.d') + install_data('definitions/confext.repart.d/20-root-verity.conf', install_dir : repartdefinitionsdir / 'confext.repart.d') + install_data('definitions/confext.repart.d/30-root-verity-sig.conf', install_dir : repartdefinitionsdir / 'confext.repart.d') + install_data('definitions/portable.repart.d/10-root.conf', install_dir : repartdefinitionsdir / 'portable.repart.d') + install_data('definitions/portable.repart.d/20-root-verity.conf', install_dir : repartdefinitionsdir / 'portable.repart.d') + install_data('definitions/portable.repart.d/30-root-verity-sig.conf', install_dir : repartdefinitionsdir / 'portable.repart.d') + install_data('definitions/sysext.repart.d/10-root.conf', install_dir : repartdefinitionsdir / 'sysext.repart.d') + install_data('definitions/sysext.repart.d/20-root-verity.conf', install_dir : repartdefinitionsdir / 'sysext.repart.d') + install_data('definitions/sysext.repart.d/30-root-verity-sig.conf', install_dir : repartdefinitionsdir / 'sysext.repart.d') +endif diff --git a/src/partition/repart.c b/src/partition/repart.c index 1dd05b3bde..c2eec29ca2 100644 --- a/src/partition/repart.c +++ b/src/partition/repart.c @@ -107,13 +107,16 @@ * sector size devices were generally assumed to have an even number of sectors, hence at the worst we'll * waste 3K per partition, which is probably fine. */ -static enum { +typedef enum EmptyMode { + EMPTY_UNSET, /* no choice has been made yet */ EMPTY_REFUSE, /* refuse empty disks, never create a partition table */ EMPTY_ALLOW, /* allow empty disks, create partition table if necessary */ EMPTY_REQUIRE, /* require an empty disk, create a partition table */ EMPTY_FORCE, /* make disk empty, erase everything, create a partition table always */ EMPTY_CREATE, /* create disk as loopback file, create a partition table always */ -} arg_empty = EMPTY_REFUSE; + _EMPTY_MODE_MAX, + _EMPTY_MODE_INVALID = -EINVAL, +} EmptyMode; typedef enum FilterPartitionType { FILTER_PARTITIONS_NONE, @@ -123,6 +126,7 @@ typedef enum FilterPartitionType { _FILTER_PARTITIONS_INVALID = -EINVAL, } FilterPartitionsType; +static EmptyMode arg_empty = EMPTY_UNSET; static bool arg_dry_run = true; static const char *arg_node = NULL; static char *arg_root = NULL; @@ -161,6 +165,8 @@ static ImagePolicy *arg_image_policy = NULL; static Architecture arg_architecture = _ARCHITECTURE_INVALID; static int arg_offline = -1; static char **arg_copy_from = NULL; +static char *arg_copy_source = NULL; +static char *arg_make_ddi = NULL; STATIC_DESTRUCTOR_REGISTER(arg_root, freep); STATIC_DESTRUCTOR_REGISTER(arg_image, freep); @@ -174,6 +180,8 @@ STATIC_DESTRUCTOR_REGISTER(arg_tpm2_public_key, freep); STATIC_DESTRUCTOR_REGISTER(arg_filter_partitions, freep); STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); STATIC_DESTRUCTOR_REGISTER(arg_copy_from, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_copy_source, freep); +STATIC_DESTRUCTOR_REGISTER(arg_make_ddi, freep); typedef struct FreeArea FreeArea; @@ -301,6 +309,15 @@ typedef struct Context { bool from_scratch; } Context; +static const char *empty_mode_table[_EMPTY_MODE_MAX] = { + [EMPTY_UNSET] = "unset", + [EMPTY_REFUSE] = "refuse", + [EMPTY_ALLOW] = "allow", + [EMPTY_REQUIRE] = "require", + [EMPTY_FORCE] = "force", + [EMPTY_CREATE] = "create", +}; + static const char *encrypt_mode_table[_ENCRYPT_MODE_MAX] = { [ENCRYPT_OFF] = "off", [ENCRYPT_KEY_FILE] = "key-file", @@ -321,6 +338,7 @@ static const char *minimize_mode_table[_MINIMIZE_MODE_MAX] = { [MINIMIZE_GUESS] = "guess", }; +DEFINE_PRIVATE_STRING_TABLE_LOOKUP(empty_mode, EmptyMode); DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING_WITH_BOOLEAN(encrypt_mode, EncryptMode, ENCRYPT_KEY_FILE); DEFINE_PRIVATE_STRING_TABLE_LOOKUP(verity_mode, VerityMode); DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING_WITH_BOOLEAN(minimize_mode, MinimizeMode, MINIMIZE_BEST); @@ -1373,9 +1391,18 @@ static int config_parse_fstype( void *userdata) { char **fstype = ASSERT_PTR(data); + const char *e; assert(rvalue); + /* Let's provide an easy way to override the chosen fstype for file system partitions */ + e = secure_getenv("SYSTEMD_REPART_OVERRIDE_FSTYPE"); + if (e && !streq(rvalue, e)) { + log_syntax(unit, LOG_NOTICE, filename, line, 0, + "Overriding defined file system type '%s' with '%s'.", rvalue, e); + rvalue = e; + } + if (!filename_is_valid(rvalue)) return log_syntax(unit, LOG_ERR, filename, line, 0, "File system type is not valid, refusing: %s", rvalue); @@ -2416,6 +2443,9 @@ static int context_load_partition_table(Context *context) { /* Always reinitiaize the disk, don't consider what there was on the disk before */ from_scratch = true; break; + + default: + assert_not_reached(); } if (from_scratch) { @@ -3356,6 +3386,9 @@ static int context_wipe_and_discard(Context *context) { assert(context); + if (arg_empty == EMPTY_CREATE) /* If we just created the image, no need to wipe */ + return 0; + /* Wipe and discard the contents of all partitions we are about to create. We skip the discarding if * we were supposed to start from scratch anyway, as in that case we just discard the whole block * device in one go early on. */ @@ -4267,11 +4300,11 @@ static int add_exclude_path(const char *path, Hashmap **denylist, DenyType type) if (!st) return log_oom(); - r = chase_and_stat(path, arg_root, CHASE_PREFIX_ROOT, NULL, st); + r = chase_and_stat(path, arg_copy_source, CHASE_PREFIX_ROOT, NULL, st); if (r == -ENOENT) return 0; if (r < 0) - return log_error_errno(r, "Failed to stat source file '%s/%s': %m", strempty(arg_root), path); + return log_error_errno(r, "Failed to stat source file '%s/%s': %m", strempty(arg_copy_source), path); r = hashmap_ensure_put(denylist, &inode_hash_ops, st, INT_TO_PTR(type)); if (r == -EEXIST) @@ -4395,11 +4428,11 @@ static int add_subvolume_path(const char *path, Set **subvolumes) { if (!st) return log_oom(); - r = chase_and_stat(path, arg_root, CHASE_PREFIX_ROOT, NULL, st); + r = chase_and_stat(path, arg_copy_source, CHASE_PREFIX_ROOT, NULL, st); if (r == -ENOENT) return 0; if (r < 0) - return log_error_errno(r, "Failed to stat source file '%s/%s': %m", strempty(arg_root), path); + return log_error_errno(r, "Failed to stat source file '%s/%s': %m", strempty(arg_copy_source), path); r = set_ensure_consume(subvolumes, &inode_hash_ops, TAKE_PTR(st)); if (r < 0) @@ -4462,9 +4495,9 @@ static int do_copy_files(Context *context, Partition *p, const char *root) { if (rfd < 0) return rfd; - sfd = chase_and_open(*source, arg_root, CHASE_PREFIX_ROOT, O_PATH|O_DIRECTORY|O_CLOEXEC|O_NOCTTY, NULL); + sfd = chase_and_open(*source, arg_copy_source, CHASE_PREFIX_ROOT, O_PATH|O_DIRECTORY|O_CLOEXEC|O_NOCTTY, NULL); if (sfd < 0) - return log_error_errno(sfd, "Failed to open source file '%s%s': %m", strempty(arg_root), *source); + return log_error_errno(sfd, "Failed to open source file '%s%s': %m", strempty(arg_copy_source), *source); (void) copy_xattr(sfd, NULL, rfd, NULL, COPY_ALL_XATTRS); (void) copy_access(sfd, rfd); @@ -4486,9 +4519,13 @@ static int do_copy_files(Context *context, Partition *p, const char *root) { if (r < 0) return r; - sfd = chase_and_open(*source, arg_root, CHASE_PREFIX_ROOT, O_CLOEXEC|O_NOCTTY, NULL); + sfd = chase_and_open(*source, arg_copy_source, CHASE_PREFIX_ROOT, O_CLOEXEC|O_NOCTTY, NULL); + if (sfd == -ENOENT) { + log_notice_errno(sfd, "Failed to open source file '%s%s', skipping: %m", strempty(arg_copy_source), *source); + continue; + } if (sfd < 0) - return log_error_errno(sfd, "Failed to open source file '%s%s': %m", strempty(arg_root), *source); + return log_error_errno(sfd, "Failed to open source file '%s%s': %m", strempty(arg_copy_source), *source); r = fd_verify_regular(sfd); if (r < 0) { @@ -4534,7 +4571,7 @@ static int do_copy_files(Context *context, Partition *p, const char *root) { denylist, subvolumes_by_source_inode); if (r < 0) return log_error_errno(r, "Failed to copy '%s%s' to '%s%s': %m", - strempty(arg_root), *source, strempty(root), *target); + strempty(arg_copy_source), *source, strempty(root), *target); } else { _cleanup_free_ char *dn = NULL, *fn = NULL; @@ -4565,7 +4602,7 @@ static int do_copy_files(Context *context, Partition *p, const char *root) { r = copy_bytes(sfd, tfd, UINT64_MAX, COPY_REFLINK|COPY_HOLES|COPY_SIGINT|COPY_TRUNCATE); if (r < 0) - return log_error_errno(r, "Failed to copy '%s' to '%s%s': %m", *source, strempty(arg_root), *target); + return log_error_errno(r, "Failed to copy '%s' to '%s%s': %m", *source, strempty(arg_copy_source), *target); (void) copy_xattr(sfd, NULL, tfd, NULL, COPY_ALL_XATTRS); (void) copy_access(sfd, tfd); @@ -5420,7 +5457,8 @@ static int context_write_partition_table(Context *context) { log_info("Applying changes."); - if (context->from_scratch) { + if (context->from_scratch && arg_empty != EMPTY_CREATE) { + /* Erase everything if we operate from scratch, except if the image was just created anyway, and thus is definitely empty. */ r = context_wipe_range(context, 0, context->total); if (r < 0) return r; @@ -6364,7 +6402,11 @@ static int help(void) { " --sector-size=SIZE Set the logical sector size for the image\n" " --architecture=ARCH Set the generic architecture for the image\n" " --offline=BOOL Whether to build the image offline\n" + " -s --copy-source=PATH Specify the primary source tree to copy files from\n" " --copy-from=IMAGE Copy partitions from the given image(s)\n" + " -S --make-ddi=sysext Make a system extension DDI\n" + " -C --make-ddi=confext Make a configuration extension DDI\n" + " -P --make-ddi=portable Make a portable service DDI\n" "\nSee the %s for details.\n", program_invocation_short_name, ansi_highlight(), @@ -6409,6 +6451,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_ARCHITECTURE, ARG_OFFLINE, ARG_COPY_FROM, + ARG_MAKE_DDI, }; static const struct option options[] = { @@ -6444,15 +6487,17 @@ static int parse_argv(int argc, char *argv[]) { { "architecture", required_argument, NULL, ARG_ARCHITECTURE }, { "offline", required_argument, NULL, ARG_OFFLINE }, { "copy-from", required_argument, NULL, ARG_COPY_FROM }, + { "copy-source", required_argument, NULL, 's' }, + { "make-ddi", required_argument, NULL, ARG_MAKE_DDI }, {} }; - int c, r, dry_run = -1; + int c, r; assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + while ((c = getopt_long(argc, argv, "hs:SCP", options, NULL)) >= 0) switch (c) { @@ -6477,24 +6522,15 @@ static int parse_argv(int argc, char *argv[]) { break; case ARG_EMPTY: - if (isempty(optarg) || streq(optarg, "refuse")) - arg_empty = EMPTY_REFUSE; - else if (streq(optarg, "allow")) - arg_empty = EMPTY_ALLOW; - else if (streq(optarg, "require")) - arg_empty = EMPTY_REQUIRE; - else if (streq(optarg, "force")) - arg_empty = EMPTY_FORCE; - else if (streq(optarg, "create")) { - arg_empty = EMPTY_CREATE; + if (isempty(optarg)) { + arg_empty = EMPTY_UNSET; + break; + } + + arg_empty = empty_mode_from_string(optarg); + if (arg_empty < 0) + return log_error_errno(arg_empty, "Failed to parse --empty= parameter: %s", optarg); - if (dry_run < 0) - dry_run = false; /* Imply --dry-run=no if we create the loopback file - * anew. After all we cannot really break anyone's - * partition tables that way. */ - } else - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Failed to parse --empty= parameter: %s", optarg); break; case ARG_DISCARD: @@ -6780,6 +6816,39 @@ static int parse_argv(int argc, char *argv[]) { break; } + case 's': + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_copy_source); + if (r < 0) + return r; + break; + + case ARG_MAKE_DDI: + if (!filename_is_valid(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid DDI type: %s", optarg); + + r = free_and_strdup_warn(&arg_make_ddi, optarg); + if (r < 0) + return r; + break; + + case 'S': + r = free_and_strdup_warn(&arg_make_ddi, "sysext"); + if (r < 0) + return r; + break; + + case 'C': + r = free_and_strdup_warn(&arg_make_ddi, "confext"); + if (r < 0) + return r; + break; + + case 'P': + r = free_and_strdup_warn(&arg_make_ddi, "portable"); + if (r < 0) + return r; + break; + case '?': return -EINVAL; @@ -6789,7 +6858,39 @@ static int parse_argv(int argc, char *argv[]) { if (argc - optind > 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Expected at most one argument, the path to the block device."); + "Expected at most one argument, the path to the block device or image file."); + + if (arg_make_ddi) { + if (arg_definitions) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Combination of --make-ddi= and --definitions= is not supported."); + if (!IN_SET(arg_empty, EMPTY_UNSET, EMPTY_CREATE)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Combination of --make-ddi= and --empty=%s is not supported.", empty_mode_to_string(arg_empty)); + + /* Imply automatic sizing in DDI mode */ + if (arg_size == UINT64_MAX) + arg_size_auto = true; + + if (!arg_copy_source) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No --copy-source= specified, refusing."); + + r = dir_is_empty(arg_copy_source, /* ignore_hidden_or_backup= */ false); + if (r < 0) + return log_error_errno(r, "Failed to determine if '%s' is empty: %m", arg_copy_source); + if (r > 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Source directory '%s' is empty, refusing to create empty image.", arg_copy_source); + + if (sd_id128_is_null(arg_seed) && !arg_randomize) { + /* We don't want that /etc/machine-id leaks into any image built this way, hence + * let's randomize the seed if not specified explicitly */ + log_notice("No seed value specified, randomizing generated UUIDs, resulting image will not be reproducible."); + arg_randomize = true; + } + + arg_empty = EMPTY_CREATE; + } + + if (arg_empty == EMPTY_UNSET) /* default to refuse mode, if not otherwise specified */ + arg_empty = EMPTY_REFUSE; if (arg_factory_reset > 0 && IN_SET(arg_empty, EMPTY_FORCE, EMPTY_REQUIRE, EMPTY_CREATE)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), @@ -6799,10 +6900,15 @@ static int parse_argv(int argc, char *argv[]) { arg_dry_run = true; /* When --can-factory-reset is specified we don't make changes, hence * non-dry-run mode makes no sense. Thus, imply dry run mode so that we * open things strictly read-only. */ - else if (dry_run >= 0) - arg_dry_run = dry_run; + else if (arg_empty == EMPTY_CREATE) + arg_dry_run = false; /* Imply --dry-run=no if we create the loopback file anew. After all we + * cannot really break anyone's partition tables that way. */ - if (arg_empty == EMPTY_CREATE && (arg_size == UINT64_MAX && !arg_size_auto)) + /* Disable pager once we are not just reviewing, but doing things. */ + if (!arg_dry_run) + arg_pager_flags |= PAGER_DISABLE; + + if (arg_empty == EMPTY_CREATE && arg_size == UINT64_MAX && !arg_size_auto) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "If --empty=create is specified, --size= must be specified, too."); @@ -6829,11 +6935,11 @@ static int parse_argv(int argc, char *argv[]) { if (IN_SET(arg_empty, EMPTY_FORCE, EMPTY_REQUIRE, EMPTY_CREATE) && !arg_node && !arg_image) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "A path to a device node or loopback file must be specified when --empty=force, --empty=require or --empty=create are used."); + "A path to a device node or image file must be specified when --make-ddi=, --empty=force, --empty=require or --empty=create are used."); if (arg_split && !arg_node) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "A path to a loopback file must be specified when --split is used."); + "A path to an image file must be specified when --split is used."); if (arg_tpm2_public_key_pcr_mask_use_default && arg_tpm2_public_key) arg_tpm2_public_key_pcr_mask = INDEX_TO_MASK(uint32_t, TPM2_PCR_KERNEL_BOOT); @@ -7332,6 +7438,13 @@ static int run(int argc, char *argv[]) { } } + if (!arg_copy_source && arg_root) { + /* If no explicit copy source is specified, then use --root=/--image= */ + arg_copy_source = strdup(arg_root); + if (!arg_copy_source) + return log_oom(); + } + context = context_new(arg_seed); if (!context) return log_oom(); @@ -7340,7 +7453,22 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; - strv_uniq(arg_definitions); + if (arg_make_ddi) { + _cleanup_free_ char *d = NULL, *dp = NULL; + assert(!arg_definitions); + + d = strjoin(arg_make_ddi, ".repart.d/"); + if (!d) + return log_oom(); + + r = search_and_access(d, F_OK, arg_root, CONF_PATHS_USR_STRV("systemd/repart/definitions"), &dp); + if (r < 0) + return log_error_errno(errno, "DDI type '%s' is not defined: %m", arg_make_ddi); + + if (strv_consume(&arg_definitions, TAKE_PTR(dp)) < 0) + return log_oom(); + } else + strv_uniq(arg_definitions); r = context_read_definitions(context); if (r < 0) diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index f94fac1b41..50662037de 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -85,6 +85,13 @@ static const char* const image_search_path_initrd[_IMAGE_CLASS_MAX] = { "/.extra/sysext\0" /* put sysext picked up by systemd-stub last, since not trusted */ }; +static const char* image_class_suffix_table[_IMAGE_CLASS_MAX] = { + [IMAGE_SYSEXT] = ".sysext", + [IMAGE_CONFEXT] = ".confext", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(image_class_suffix, ImageClass); + static Image *image_free(Image *i) { assert(i); @@ -201,29 +208,36 @@ static int image_new( return 0; } -static int extract_pretty(const char *path, const char *suffix, char **ret) { +static int extract_pretty( + const char *path, + const char *class_suffix, + const char *format_suffix, + char **ret) { + _cleanup_free_ char *name = NULL; - const char *p; + int r; assert(path); assert(ret); - p = last_path_component(path); + r = path_extract_filename(path, &name); + if (r < 0) + return r; - name = strdupcspn(p, "/"); - if (!name) - return -ENOMEM; - - if (suffix) { - char *e; - - e = endswith(name, suffix); - if (!e) + if (format_suffix) { + char *e = endswith(name, format_suffix); + if (!e) /* Format suffix is required */ return -EINVAL; *e = 0; } + if (class_suffix) { + char *e = endswith(name, class_suffix); + if (e) /* Class suffix is optional */ + *e = 0; + } + if (!image_name_is_valid(name)) return -EINVAL; @@ -282,7 +296,7 @@ static int image_make( return 0; if (!pretty) { - r = extract_pretty(filename, NULL, &pretty_buffer); + r = extract_pretty(filename, image_class_suffix_to_string(c), NULL, &pretty_buffer); if (r < 0) return r; @@ -369,7 +383,7 @@ static int image_make( (void) fd_getcrtime_at(dfd, filename, AT_SYMLINK_FOLLOW, &crtime); if (!pretty) { - r = extract_pretty(filename, ".raw", &pretty_buffer); + r = extract_pretty(filename, image_class_suffix_to_string(c), ".raw", &pretty_buffer); if (r < 0) return r; @@ -403,7 +417,7 @@ static int image_make( return 0; if (!pretty) { - r = extract_pretty(filename, NULL, &pretty_buffer); + r = extract_pretty(filename, NULL, NULL, &pretty_buffer); if (r < 0) return r; @@ -592,8 +606,7 @@ int image_discover( FOREACH_DIRENT_ALL(de, d, return -errno) { _cleanup_(image_unrefp) Image *image = NULL; - _cleanup_free_ char *truncated = NULL; - const char *pretty; + _cleanup_free_ char *pretty = NULL; struct stat st; int flags; @@ -610,22 +623,16 @@ int image_discover( return -errno; } - if (S_ISREG(st.st_mode)) { - const char *e; - - e = endswith(de->d_name, ".raw"); - if (!e) - continue; - - truncated = strndup(de->d_name, e - de->d_name); - if (!truncated) - return -ENOMEM; - - pretty = truncated; - } else if (S_ISDIR(st.st_mode) || S_ISBLK(st.st_mode)) - pretty = de->d_name; + if (S_ISREG(st.st_mode)) + r = extract_pretty(de->d_name, image_class_suffix_to_string(class), ".raw", &pretty); + else if (S_ISDIR(st.st_mode)) + r = extract_pretty(de->d_name, image_class_suffix_to_string(class), NULL, &pretty); + else if (S_ISBLK(st.st_mode)) + r = extract_pretty(de->d_name, NULL, NULL, &pretty); else continue; + if (r < 0) + return r; if (!image_name_is_valid(pretty)) continue; diff --git a/src/shared/import-util.c b/src/shared/import-util.c index ec89064506..9057b78d28 100644 --- a/src/shared/import-util.c +++ b/src/shared/import-util.c @@ -167,6 +167,8 @@ int raw_strip_suffixes(const char *p, char **ret) { ".xz\0" ".gz\0" ".bz2\0" + ".sysext.raw\0" + ".confext.raw\0" ".raw\0" ".qcow2\0" ".img\0" diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c index 33bfb3dc80..784accc15d 100644 --- a/src/sysext/sysext.c +++ b/src/sysext/sysext.c @@ -591,13 +591,13 @@ static int merge_subprocess(Hashmap *images, const char *workspace) { /* Let's create the workspace if it's missing */ r = mkdir_p(workspace, 0700); if (r < 0) - return log_error_errno(r, "Failed to create /run/systemd/sysext: %m"); + return log_error_errno(r, "Failed to create '%s': %m", workspace); /* Let's mount a tmpfs to our workspace. This way we don't need to clean up the inodes we mount over, * but let the kernel do that entirely automatically, once our namespace dies. Note that this file * system won't be visible to anyone but us, since we opened our own namespace and then made the * /run/ hierarchy (which our workspace is contained in) MS_SLAVE, see above. */ - r = mount_nofollow_verbose(LOG_ERR, "sysext", workspace, "tmpfs", 0, "mode=0700"); + r = mount_nofollow_verbose(LOG_ERR, image_class_info[arg_image_class].short_identifier, workspace, "tmpfs", 0, "mode=0700"); if (r < 0) return r; diff --git a/src/test/test-fileio.c b/src/test/test-fileio.c index 0eb7b07331..6efa8f2939 100644 --- a/src/test/test-fileio.c +++ b/src/test/test-fileio.c @@ -582,12 +582,23 @@ TEST(search_and_fopen) { f = safe_fclose(f); p = mfree(p); + r = search_and_fopen(basename(name), NULL, NULL, (const char**) dirs, NULL, &p); + assert_se(r >= 0); + assert_se(e = path_startswith(p, "/tmp/")); + assert_se(streq(basename(name), e)); + p = mfree(p); + r = search_and_fopen(name, "re", NULL, (const char**) dirs, &f, &p); assert_se(r >= 0); assert_se(path_equal(name, p)); f = safe_fclose(f); p = mfree(p); + r = search_and_fopen(name, NULL, NULL, (const char**) dirs, NULL, &p); + assert_se(r >= 0); + assert_se(path_equal(name, p)); + p = mfree(p); + r = search_and_fopen(basename(name), "re", "/", (const char**) dirs, &f, &p); assert_se(r >= 0); assert_se(e = path_startswith(p, "/tmp/")); @@ -595,16 +606,28 @@ TEST(search_and_fopen) { f = safe_fclose(f); p = mfree(p); + r = search_and_fopen(basename(name), NULL, "/", (const char**) dirs, NULL, &p); + assert_se(r >= 0); + assert_se(e = path_startswith(p, "/tmp/")); + assert_se(streq(basename(name), e)); + p = mfree(p); + r = search_and_fopen("/a/file/which/does/not/exist/i/guess", "re", NULL, (const char**) dirs, &f, &p); assert_se(r == -ENOENT); + r = search_and_fopen("/a/file/which/does/not/exist/i/guess", NULL, NULL, (const char**) dirs, NULL, &p); + assert_se(r == -ENOENT); r = search_and_fopen("afilewhichdoesnotexistiguess", "re", NULL, (const char**) dirs, &f, &p); assert_se(r == -ENOENT); + r = search_and_fopen("afilewhichdoesnotexistiguess", NULL, NULL, (const char**) dirs, NULL, &p); + assert_se(r == -ENOENT); r = unlink(name); assert_se(r == 0); r = search_and_fopen(basename(name), "re", NULL, (const char**) dirs, &f, &p); assert_se(r == -ENOENT); + r = search_and_fopen(basename(name), NULL, NULL, (const char**) dirs, NULL, &p); + assert_se(r == -ENOENT); } TEST(search_and_fopen_nulstr) { diff --git a/test/units/testsuite-50.sh b/test/units/testsuite-50.sh index 7bede7da1a..58be950d5f 100755 --- a/test/units/testsuite-50.sh +++ b/test/units/testsuite-50.sh @@ -2,6 +2,7 @@ # SPDX-License-Identifier: LGPL-2.1-or-later # -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*- # ex: ts=8 sw=4 sts=4 et filetype=sh +# shellcheck disable=SC2233,SC2235 set -eux set -o pipefail @@ -624,4 +625,36 @@ systemd-sysext merge systemd-sysext unmerge systemctl status foo.service 2>&1 | grep -v -q -F "Warning" +# Test systemd-repart --make-ddi=: +if command -v mksquashfs >/dev/null 2>&1; then + + openssl req -config /dev/null -subj="/CN=waldo" -x509 -sha256 -nodes -days 365 -newkey rsa:4096 -keyout /tmp/test-50-privkey.key -out /tmp/test-50-cert.crt + + mkdir -p /tmp/test-50-confext/etc/extension-release.d/ + + echo "foobar50" > /tmp/test-50-confext/etc/waldo + + ( grep -e '^\(ID\|VERSION_ID\)=' /etc/os-release ; echo IMAGE_ID=waldo ; echo IMAGE_VERSION=7 ) > /tmp/test-50-confext/etc/extension-release.d/extension-release.waldo + + mkdir -p /run/confexts + + SYSTEMD_REPART_OVERRIDE_FSTYPE=squashfs systemd-repart -C -s /tmp/test-50-confext --certificate=/tmp/test-50-cert.crt --private-key=/tmp/test-50-privkey.key /run/confexts/waldo.confext.raw + rm -rf /tmp/test-50-confext + + mkdir -p /run/verity.d + cp /tmp/test-50-cert.crt /run/verity.d/ + systemd-dissect --mtree /run/confexts/waldo.confext.raw + + systemd-confext refresh + + read -r X < /etc/waldo + test "$X" = foobar50 + + rm /run/verity.d/test-50-cert.crt /run/confexts/waldo.confext.raw /tmp/test-50-cert.crt /tmp/test-50-privkey.key + + systemd-confext refresh + + (! test -f /tmp/test-50-confext/etc/waldo ) +fi + touch /testok