diff --git a/man/systemd-dissect.xml b/man/systemd-dissect.xml index 955909ff91..f857ecacc7 100644 --- a/man/systemd-dissect.xml +++ b/man/systemd-dissect.xml @@ -34,6 +34,9 @@ systemd-dissect OPTIONS IMAGE + + systemd-dissect OPTIONS IMAGE COMMAND + systemd-dissect OPTIONS IMAGE PATH TARGET @@ -160,6 +163,19 @@ standard output. + + + + Runs the specified command with the specified OS image mounted. This will mount the + image to a temporary directory, switch the current working directory to it, and invoke the specified + command line as child process. Once the process ends it will unmount the image again, and remove the + temporary directory. If no command is specified a shell is invoked. The image is mounted writable, + use to switch to read-only operation. The invoked process will have the + $SYSTEMD_DISSECT_ROOT environment variable set, containing the absolute path name + of the temporary mount point, i.e. the same directory that is set as the current working + directory. + + @@ -294,14 +310,24 @@ - Exit status - On success, 0 is returned, a non-zero failure code - otherwise. + On success, 0 is returned, a non-zero failure code otherwise. If the + command is used the exit status of the invoked command is propagated. + + + + + Examples + + + Generate a tarball from an OS disk image + + $ systemd-dissect --with foo.raw tar cz . > foo.tar.gz + diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index f2851e25f8..a1e9cb6add 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -36,6 +36,7 @@ #include "parse-util.h" #include "path-util.h" #include "pretty-print.h" +#include "process-util.h" #include "recurse-dir.h" #include "stat-util.h" #include "string-util.h" @@ -49,6 +50,7 @@ static enum { ACTION_MOUNT, ACTION_UMOUNT, ACTION_LIST, + ACTION_WITH, ACTION_COPY_FROM, ACTION_COPY_TO, } arg_action = ACTION_DISSECT; @@ -68,8 +70,10 @@ static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; static PagerFlags arg_pager_flags = 0; static bool arg_legend = true; static bool arg_rmdir = false; +static char **arg_argv = NULL; STATIC_DESTRUCTOR_REGISTER(arg_verity_settings, verity_settings_done); +STATIC_DESTRUCTOR_REGISTER(arg_argv, strv_freep); static int help(void) { _cleanup_free_ char *link = NULL; @@ -83,6 +87,7 @@ static int help(void) { "%1$s [OPTIONS...] --mount IMAGE PATH\n" "%1$s [OPTIONS...] --umount PATH\n" "%1$s [OPTIONS...] --list IMAGE\n" + "%1$s [OPTIONS...] --with IMAGE [COMMAND…]\n" "%1$s [OPTIONS...] --copy-from IMAGE PATH [TARGET]\n" "%1$s [OPTIONS...] --copy-to IMAGE [SOURCE] PATH\n\n" "%5$sDissect a Discoverable Disk Image (DDI).%6$s\n\n" @@ -113,6 +118,7 @@ static int help(void) { " -U Shortcut for --umount --rmdir\n" " -l --list List all the files and directories of the specified\n" " OS image\n" + " --with Mount, run command, unmount\n" " -x --copy-from Copy files from image to host\n" " -a --copy-to Copy files from host to image\n" "\nSee the %2$s for details.\n", @@ -126,12 +132,56 @@ static int help(void) { return 0; } +static int patch_argv(int *argc, char ***argv, char ***buf) { + _cleanup_free_ char **l = NULL; + char **e; + + assert(argc); + assert(*argc >= 0); + assert(argv); + assert(*argv); + assert(buf); + + /* Ugly hack: if --with is included in command line, also insert "--" immediately after it, to make + * getopt_long() stop processing switches */ + + for (e = *argv + 1; e < *argv + *argc; e++) { + assert(*e); + + if (streq(*e, "--with")) + break; + } + + if (e >= *argv + *argc || streq_ptr(e[1], "--")) { + /* No --with used? Or already followed by "--"? Then don't do anything */ + *buf = NULL; + return 0; + } + + /* Insert the extra "--" right after the --with */ + l = new(char*, *argc + 2); + if (!l) + return log_oom(); + + size_t idx = e - *argv + 1; + memcpy(l, *argv, sizeof(char*) * idx); /* copy everything up to and including the --with */ + l[idx] = (char*) "--"; /* insert "--" */ + memcpy(l + idx + 1, e + 1, sizeof(char*) * (*argc - idx + 1)); /* copy the rest, including trailing NULL entry */ + + (*argc)++; + (*argv) = l; + + *buf = TAKE_PTR(l); + return 1; +} + static int parse_argv(int argc, char *argv[]) { enum { ARG_VERSION = 0x100, ARG_NO_PAGER, ARG_NO_LEGEND, + ARG_WITH, ARG_DISCARD, ARG_FSCK, ARG_GROWFS, @@ -150,6 +200,7 @@ static int parse_argv(int argc, char *argv[]) { { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, { "mount", no_argument, NULL, 'm' }, { "umount", no_argument, NULL, 'u' }, + { "with", no_argument, NULL, ARG_WITH }, { "read-only", no_argument, NULL, 'r' }, { "discard", required_argument, NULL, ARG_DISCARD }, { "fsck", required_argument, NULL, ARG_FSCK }, @@ -166,11 +217,16 @@ static int parse_argv(int argc, char *argv[]) { {} }; + _cleanup_free_ char **buf = NULL; /* we use free(), not strv_free() here, as we don't copy the strings here */ int c, r; assert(argc >= 0); assert(argv); + r = patch_argv(&argc, &argv, &buf); + if (r < 0) + return r; + while ((c = getopt_long(argc, argv, "hmurMUlxa", options, NULL)) >= 0) { switch (c) { @@ -222,6 +278,10 @@ static int parse_argv(int argc, char *argv[]) { arg_flags |= DISSECT_IMAGE_READ_ONLY; break; + case ARG_WITH: + arg_action = ACTION_WITH; + break; + case 'x': arg_action = ACTION_COPY_FROM; arg_flags |= DISSECT_IMAGE_READ_ONLY; @@ -332,7 +392,6 @@ static int parse_argv(int argc, char *argv[]) { default: assert_not_reached(); } - } switch (arg_action) { @@ -403,6 +462,20 @@ static int parse_argv(int argc, char *argv[]) { arg_flags |= DISSECT_IMAGE_REQUIRE_ROOT; break; + case ACTION_WITH: + if (optind >= argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Expected an image file path and an optional command line."); + + arg_image = argv[optind]; + if (argc > optind + 1) { + arg_argv = strv_copy(argv + optind + 1); + if (!arg_argv) + return log_oom(); + } + + break; + default: assert_not_reached(); } @@ -518,6 +591,8 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { "Mach. Info:"); strv_pair_print(m->os_release, "OS Release:"); + strv_pair_print(m->initrd_release, + "initrd R.:"); strv_pair_print(m->extension_release, " Ext. Rel.:"); @@ -525,6 +600,7 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { !sd_id128_is_null(m->machine_id) || !strv_isempty(m->machine_info) || !strv_isempty(m->os_release) || + !strv_isempty(m->initrd_release) || !strv_isempty(m->extension_release)) putc('\n', stdout); @@ -535,6 +611,8 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { printf(" %s portable service\n", COLOR_MARK_BOOL(strv_env_pairs_get(m->os_release, "PORTABLE_PREFIXES"))); + printf(" %s initrd\n", + COLOR_MARK_BOOL(!strv_isempty(m->initrd_release))); r = get_sysext_scopes(m, &sysext_scopes); if (r < 0) @@ -549,7 +627,7 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { putc('\n', stdout); } else { - _cleanup_(json_variant_unrefp) JsonVariant *mi = NULL, *osr = NULL, *exr = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *mi = NULL, *osr = NULL, *irdr = NULL, *exr = NULL; _cleanup_strv_free_ char **sysext_scopes = NULL; if (!strv_isempty(m->machine_info)) { @@ -564,6 +642,12 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { return log_oom(); } + if (!strv_isempty(m->initrd_release)) { + r = strv_pair_to_json(m->initrd_release, &irdr); + if (r < 0) + return log_oom(); + } + if (!strv_isempty(m->extension_release)) { r = strv_pair_to_json(m->extension_release, &exr); if (r < 0) @@ -581,9 +665,11 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(m->machine_id), "machineId", JSON_BUILD_ID128(m->machine_id)), JSON_BUILD_PAIR_CONDITION(mi, "machineInfo", JSON_BUILD_VARIANT(mi)), JSON_BUILD_PAIR_CONDITION(osr, "osRelease", JSON_BUILD_VARIANT(osr)), + JSON_BUILD_PAIR_CONDITION(osr, "initrdRelease", JSON_BUILD_VARIANT(irdr)), JSON_BUILD_PAIR_CONDITION(exr, "extensionRelease", JSON_BUILD_VARIANT(exr)), JSON_BUILD_PAIR("useBootableUefi", JSON_BUILD_BOOLEAN(m->partitions[PARTITION_ESP].found)), JSON_BUILD_PAIR_CONDITION(m->has_init_system >= 0, "useBootableContainer", JSON_BUILD_BOOLEAN(m->has_init_system)), + JSON_BUILD_PAIR("useInitrd", JSON_BUILD_BOOLEAN(!strv_isempty(m->initrd_release))), JSON_BUILD_PAIR("usePortableService", JSON_BUILD_BOOLEAN(strv_env_pairs_get(m->os_release, "PORTABLE_MATCHES"))), JSON_BUILD_PAIR("useSystemExtension", JSON_BUILD_BOOLEAN(strv_contains(sysext_scopes, "system"))), JSON_BUILD_PAIR("useInitRDExtension", JSON_BUILD_BOOLEAN(strv_contains(sysext_scopes, "initrd"))), @@ -958,6 +1044,97 @@ static int action_umount(const char *path) { return 0; } +static int action_with(DissectedImage *m, LoopDevice *d) { + _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; + _cleanup_(rmdir_and_freep) char *created_dir = NULL; + _cleanup_free_ char *temp = NULL; + int r, rcode; + + r = dissected_image_decrypt_interactively( + m, NULL, + &arg_verity_settings, + arg_flags); + if (r < 0) + return r; + + r = tempfn_random_child(NULL, program_invocation_short_name, &temp); + if (r < 0) + return log_error_errno(r, "Failed to generate temporary mount directory: %m"); + + r = mkdir_p(temp, 0700); + if (r < 0) + return log_error_errno(r, "Failed to create mount point: %m"); + + created_dir = TAKE_PTR(temp); + + r = dissected_image_mount_and_warn(m, created_dir, UID_INVALID, UID_INVALID, arg_flags); + if (r < 0) + return r; + + mounted_dir = TAKE_PTR(created_dir); + + r = dissected_image_relinquish(m); + if (r < 0) + return log_error_errno(r, "Failed to relinquish DM and loopback block devices: %m"); + + r = loop_device_flock(d, LOCK_UN); + if (r < 0) + return log_error_errno(r, "Failed to unlock loopback block device: %m"); + + rcode = safe_fork("(with)", FORK_CLOSE_ALL_FDS|FORK_LOG|FORK_WAIT, NULL); + if (rcode == 0) { + /* Child */ + + if (chdir(mounted_dir) < 0) { + log_error_errno(errno, "Failed to change to '%s' directory: %m", mounted_dir); + _exit(EXIT_FAILURE); + } + + if (setenv("SYSTEMD_DISSECT_ROOT", mounted_dir, /* overwrite= */ true) < 0) { + log_error_errno(errno, "Failed to set $SYSTEMD_DISSECT_ROOT: %m"); + _exit(EXIT_FAILURE); + } + + if (strv_isempty(arg_argv)) { + const char *sh; + + sh = secure_getenv("SHELL"); + if (sh) { + execvp(sh, STRV_MAKE(sh)); + log_warning_errno(errno, "Failed to execute $SHELL, falling back to /bin/sh: %m"); + } + + execl("/bin/sh", "sh", NULL); + log_error_errno(errno, "Failed to invoke /bin/sh: %m"); + } else { + execvp(arg_argv[0], arg_argv); + log_error_errno(errno, "Failed to execute '%s': %m", arg_argv[0]); + } + + _exit(EXIT_FAILURE); + } + + /* Let's manually detach everything, to make things synchronous */ + r = loop_device_flock(d, LOCK_SH); + if (r < 0) + log_warning_errno(r, "Failed to lock loopback block device, ignoring: %m"); + + r = umount_recursive(mounted_dir, 0); + if (r < 0) + log_warning_errno(r, "Failed to unmount '%s', ignoring: %m", mounted_dir); + else + loop_device_unrelinquish(d); /* Let's try to destroy the loopback device */ + + created_dir = TAKE_PTR(mounted_dir); + + if (rmdir(created_dir) < 0) + log_warning_errno(r, "Failed to remove directory '%s', ignoring: %m", created_dir); + + temp = TAKE_PTR(created_dir); + + return rcode; +} + static int run(int argc, char *argv[]) { _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL; _cleanup_(loop_device_unrefp) LoopDevice *d = NULL; @@ -1026,6 +1203,10 @@ static int run(int argc, char *argv[]) { r = action_list_or_copy(m, d); break; + case ACTION_WITH: + r = action_with(m, d); + break; + default: assert_not_reached(); } diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 101db51a13..2f19031216 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -1296,6 +1296,7 @@ DissectedImage* dissected_image_unref(DissectedImage *m) { free(m->hostname); strv_free(m->machine_info); strv_free(m->os_release); + strv_free(m->initrd_release); strv_free(m->extension_release); return mfree(m); @@ -2771,6 +2772,7 @@ int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_ META_MACHINE_ID, META_MACHINE_INFO, META_OS_RELEASE, + META_INITRD_RELEASE, META_EXTENSION_RELEASE, META_HAS_INIT_SYSTEM, _META_MAX, @@ -2782,11 +2784,13 @@ int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_ [META_MACHINE_INFO] = "/etc/machine-info\0", [META_OS_RELEASE] = ("/etc/os-release\0" "/usr/lib/os-release\0"), + [META_INITRD_RELEASE] = ("/etc/initrd-release\0" + "/usr/lib/initrd-release\0"), [META_EXTENSION_RELEASE] = "extension-release\0", /* Used only for logging. */ [META_HAS_INIT_SYSTEM] = "has-init-system\0", /* ditto */ }; - _cleanup_strv_free_ char **machine_info = NULL, **os_release = NULL, **extension_release = NULL; + _cleanup_strv_free_ char **machine_info = NULL, **os_release = NULL, **initrd_release = NULL, **extension_release = NULL; _cleanup_close_pair_ int error_pipe[2] = { -1, -1 }; _cleanup_(rmdir_and_freep) char *t = NULL; _cleanup_(sigkill_waitp) pid_t child = 0; @@ -2982,6 +2986,13 @@ int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_ break; + case META_INITRD_RELEASE: + r = load_env_file_pairs(f, "initrd-release", &initrd_release); + if (r < 0) + log_debug_errno(r, "Failed to read initrd release file of image: %m"); + + break; + case META_EXTENSION_RELEASE: r = load_env_file_pairs(f, "extension-release", &extension_release); if (r < 0) @@ -3024,6 +3035,7 @@ int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_ m->machine_id = machine_id; strv_free_and_replace(m->machine_info, machine_info); strv_free_and_replace(m->os_release, os_release); + strv_free_and_replace(m->initrd_release, initrd_release); strv_free_and_replace(m->extension_release, extension_release); m->has_init_system = has_init_system; diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index 8007b544e7..46675d22ab 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -233,6 +233,7 @@ struct DissectedImage { sd_id128_t machine_id; char **machine_info; char **os_release; + char **initrd_release; char **extension_release; int has_init_system; }; diff --git a/test/TEST-50-DISSECT/test.sh b/test/TEST-50-DISSECT/test.sh index 0940382d52..276dd7c716 100755 --- a/test/TEST-50-DISSECT/test.sh +++ b/test/TEST-50-DISSECT/test.sh @@ -12,7 +12,6 @@ TEST_INSTALL_VERITY_MINIMAL=1 # shellcheck source=test/test-functions . "${TEST_BASE_DIR:?}/test-functions" - command -v mksquashfs >/dev/null 2>&1 || exit 0 command -v veritysetup >/dev/null 2>&1 || exit 0 command -v sfdisk >/dev/null 2>&1 || exit 0 @@ -27,6 +26,7 @@ test_append_files() { install_dmevent generate_module_dependencies inst_binary wc + inst_binary sha256sum if command -v openssl >/dev/null 2>&1; then inst_binary openssl fi diff --git a/test/units/testsuite-50.sh b/test/units/testsuite-50.sh index 3ab020d687..10634a6ba9 100755 --- a/test/units/testsuite-50.sh +++ b/test/units/testsuite-50.sh @@ -41,6 +41,14 @@ systemd-dissect --json=short "${image}.raw" | grep -q -F '{"rw":"ro","designator systemd-dissect "${image}.raw" | grep -q -F "MARKER=1" systemd-dissect "${image}.raw" | grep -q -F -f <(sed 's/"//g' "$os_release") +systemd-dissect --list "${image}.raw" | grep -q '^etc/os-release$' + +read -r SHA256SUM1 _ < <(systemd-dissect --copy-from "${image}.raw" etc/os-release | sha256sum) +test "$SHA256SUM1" != "" +read -r SHA256SUM2 _ < <(systemd-dissect --read-only --with "${image}.raw" sha256sum etc/os-release) +test "$SHA256SUM2" != "" +test "$SHA256SUM1" = "$SHA256SUM2" + mv "${image}.verity" "${image}.fooverity" mv "${image}.roothash" "${image}.foohash" systemd-dissect --json=short "${image}.raw" --root-hash="${roothash}" --verity-data="${image}.fooverity" | grep -q -F '{"rw":"ro","designator":"root","partition_uuid":null,"partition_label":null,"fstype":"squashfs","architecture":null,"verity":"external"'