diff options
author | Lennart Poettering <lennart@poettering.net> | 2020-08-13 11:26:49 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-08-13 11:26:49 +0200 |
commit | 830171936c4e9c072e85685d8c061175dda2ad71 (patch) | |
tree | ef1b54d5395d0964be9479ec61a348a63dc21eec | |
parent | c53da7ed02a5d732c9449f79c19675b90a6032e3 (diff) | |
parent | 1af83e7c37bc408678b43add88555e92102d9031 (diff) | |
download | systemd-830171936c4e9c072e85685d8c061175dda2ad71.tar.gz |
Merge pull request #16612 from poettering/dissect-copy
teach systemd-dissect file copying, and make it officially supported, move to /usr/bin + man page
-rw-r--r-- | TODO | 11 | ||||
-rw-r--r-- | man/rules/meson.build | 1 | ||||
-rw-r--r-- | man/systemd-dissect.xml | 253 | ||||
-rw-r--r-- | meson.build | 3 | ||||
-rw-r--r-- | src/basic/copy.c | 15 | ||||
-rw-r--r-- | src/basic/copy.h | 1 | ||||
-rw-r--r-- | src/basic/mkdir.c | 2 | ||||
-rw-r--r-- | src/dissect/dissect.c | 625 | ||||
-rw-r--r-- | src/firstboot/firstboot.c | 1 | ||||
-rw-r--r-- | src/nspawn/nspawn.c | 13 | ||||
-rw-r--r-- | src/partition/repart.c | 2 | ||||
-rw-r--r-- | src/shared/dissect-image.c | 96 | ||||
-rw-r--r-- | src/shared/dissect-image.h | 2 | ||||
-rw-r--r-- | src/shared/json.c | 45 | ||||
-rw-r--r-- | src/shared/json.h | 6 | ||||
-rwxr-xr-x | test/units/testsuite-50.sh | 30 |
16 files changed, 973 insertions, 133 deletions
@@ -42,9 +42,6 @@ Features: mounting a subdir of the root fs as actual root. This can be used as fstype-agnostic version of btrfs' rootflags=subvol=foobar. -* add --copy-from and --copy-to command to systemd-dissect which copies stuff - in and out of a disk image - * Support ProtectProc= or so, using: https://patchwork.kernel.org/cover/11310197/ * if /usr/bin/swapoff fails due to OOM, log a friendly explanatory message about it @@ -112,10 +109,6 @@ Features: * systemd-path: add ESP and XBOOTLDR path. Add "private" runtime/state/cache dir enum, mapping to $RUNTIME_DIRECTORY, $STATE_DIRECTORY and such -* make "systemd-dissect" an official supported tool, i.e. move to /usr/bin/ and - provide man page. Given that we now have a tool that can generate images like - this, it's useful to have one that can dump contents of them, too. - * All tools that support --root= should also learn --image= so that they can operate on disk images directly. Specifically: bootctl, systemctl, coredumpctl. (Already done: systemd-nspawn, systemd-firstboot, @@ -1153,10 +1146,6 @@ Features: - optionally automatically add FORWARD rules to iptables whenever nspawn is running, remove them when shut down. -* dissect - - refuse mounting over a mount point - - automatically discover .roothash files in dissect, similarly to nspawn - * machined: - add an API so that libvirt-lxc can inform us about network interfaces being removed or added to an existing machine diff --git a/man/rules/meson.build b/man/rules/meson.build index 3fb454faa9..d545f032a2 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -818,6 +818,7 @@ manpages = [ ['systemd-debug-generator', '8', [], ''], ['systemd-delta', '1', [], ''], ['systemd-detect-virt', '1', [], ''], + ['systemd-dissect', '1', [], ''], ['systemd-environment-d-generator', '8', ['30-systemd-environment-d-generator'], diff --git a/man/systemd-dissect.xml b/man/systemd-dissect.xml new file mode 100644 index 0000000000..fd70e1bfc1 --- /dev/null +++ b/man/systemd-dissect.xml @@ -0,0 +1,253 @@ +<?xml version='1.0'?> <!--*-nxml-*--> +<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" + "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"> +<!-- SPDX-License-Identifier: LGPL-2.1+ --> + +<refentry id="systemd-dissect" + xmlns:xi="http://www.w3.org/2001/XInclude"> + + <refentryinfo> + <title>systemd-dissect</title> + <productname>systemd</productname> + </refentryinfo> + + <refmeta> + <refentrytitle>systemd-dissect</refentrytitle> + <manvolnum>1</manvolnum> + </refmeta> + + <refnamediv> + <refname>systemd-dissect</refname> + <refpurpose>Dissect file system OS images</refpurpose> + </refnamediv> + + <refsynopsisdiv> + <cmdsynopsis> + <command>systemd-dissect <arg choice="opt" rep="repeat">OPTIONS</arg> <arg choice="plain"><replaceable>IMAGE</replaceable></arg></command> + </cmdsynopsis> + <cmdsynopsis> + <command>systemd-dissect <arg choice="opt" rep="repeat">OPTIONS</arg> <option>--mount</option> <arg choice="plain"><replaceable>IMAGE</replaceable></arg> <arg choice="plain"><replaceable>PATH</replaceable></arg></command> + </cmdsynopsis> + <cmdsynopsis> + <command>systemd-dissect <arg choice="opt" rep="repeat">OPTIONS</arg> <option>--copy-from</option> <arg choice="plain"><replaceable>IMAGE</replaceable></arg> <arg choice="plain"><replaceable>PATH</replaceable></arg> <arg choice="opt"><replaceable>TARGET</replaceable></arg></command> + </cmdsynopsis> + <cmdsynopsis> + <command>systemd-dissect <arg choice="opt" rep="repeat">OPTIONS</arg> <option>--copy-to</option> <arg choice="plain"><replaceable>IMAGE</replaceable></arg> <arg choice="opt"><replaceable>SOURCE</replaceable></arg> <arg choice="plain"><replaceable>PATH</replaceable></arg></command> + </cmdsynopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para><command>systemd-dissect</command> is a tool for introspecting and interacting with file system OS + disk images. It supports four different operations:</para> + + <orderedlist> + <listitem><para>Show general OS image information, including the image's + <citerefentry><refentrytitle>os-release</refentrytitle><manvolnum>5</manvolnum></citerefentry> data, + machine ID, partition information and more.</para></listitem> + + <listitem><para>Mount an OS image to a local directory. In this mode it will dissect the OS image and + mount the included partitions according to their designation onto a directory and possibly + sub-directories.</para></listitem> + + <listitem><para>Copy files and directories in and out of an OS image.</para></listitem> + </orderedlist> + + <para>The tool may operate on three types of OS images:</para> + + <orderedlist> + <listitem><para>OS disk images containing a GPT partition table envelope, with partitions marked + according to the <ulink url="https://systemd.io/DISCOVERABLE_PARTITIONS">Discoverable Partitions + Specification</ulink>.</para></listitem> + + <listitem><para>OS disk images containing just a plain file-system without an enveloping partition + table. (This file system is assumed to be the root file system of the OS.)</para></listitem> + + <listitem><para>OS disk images containing a GPT or MBR partition table, with a single + partition only. (This partition is assumed to contain the root file system of the OS.)</para></listitem> + </orderedlist> + + <para>OS images may use any kind of Linux-supported file systems. In addition they may make use of LUKS + disk encryption, and contain Verity integrity information. Note that qualifying OS images may be booted + with <citerefentry><refentrytitle>system-nspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry>'s + <option>--image=</option> switch, and be used as root file system for system service using the + <varname>RootImage=</varname> unit file setting, see + <citerefentry><refentrytitle>system.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para> + </refsect1> + + <refsect1> + <title>Commands</title> + + <para>If neither of the command switches listed below are passed the specified disk image is opened and + general information about the image and the contained partitions and their use is shown.</para> + + <variablelist> + <varlistentry> + <term><option>--mount</option></term> + <term><option>-m</option></term> + + <listitem><para>Mount the specified OS image to the specified directory. This will dissect the image, + determine the OS root file system — as well as possibly other partitions — and mount them to the + specified directory. If the OS image contains multiple partitions marked with the <ulink + url="https://systemd.io/DISCOVERABLE_PARTITIONS">Discoverable Partitions Specification</ulink> + multiple nested mounts are established. This command expects two arguments: a path to an image file + and a path to a directory where to mount the image.</para> + + <para>To unmount an OS image mounted like this use <citerefentry + project='man-pages'><refentrytitle>umount</refentrytitle><manvolnum>8</manvolnum></citerefentry>'s + <option>-R</option> switch (for recursive operation), so that the OS image and all nested partition + mounts are unmounted.</para> + + <para>When the OS image contains LUKS encrypted or Verity integrity protected file systems + appropriate volumes are automatically set up and marked for automatic disassembly when the image is + unmounted.</para> + + <para>The OS image may either be specified as path to an OS image stored in a regular file or may + refer to block device node (in the latter case the block device must be the "whole" device, i.e. not + a partition device). (The other supported commands described here support this, too.)</para> + + <para>All mounted file systems are checked with the appropriate <citerefentry + project='man-pages'><refentrytitle>fsck</refentrytitle><manvolnum>8</manvolnum></citerefentry> + implementation in automatic fixing mode, unless explicitly turned off (<option>--fsck=no</option>) or + read-only operation is requested (<option>--read-only</option>).</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>-M</option></term> + + <listitem><para>This is a shortcut for <option>--mount --mkdir</option>.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>--copy-from</option></term> + <term><option>-x</option></term> + + <listitem><para>Copies a file or directory from the specified OS image into the specified location on + the host file system. Expects three arguments: a path to an image file, a source path (relative to + the image's root directory) and a destination path (relative to the current working directory, or an + absolute path, both outside of the image). If the destination path is omitted or specified as dash + (<literal>-</literal>), the specified file is written to standard output. If the source path in the + image file system refers to a regular file it is copied to the destination path. In this case access + mode, extended attributes and timestamps are copied as well, but file ownership is not. If the source + path in the image refers to a directory, it is copied to the destination path, recursively with all + containing files and directories. In this case the file ownership is copied too.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>--copy-to</option></term> + <term><option>-a</option></term> + + <listitem><para>Copies a file or directory from the specified location in the host file system into + the specified OS image. Expects three arguments: a path to an image file, a source path (relative to + the current working directory, or an absolute path, both outside of the image) and a destination path + (relative to the image's root directory). If the source path is omitted or specified as dash + (<literal>-</literal>), the data to write is read from standard input. If the source path in the host + file system refers to a regular file, it is copied to the destination path. In this case access mode, + extended attributes and timestamps are copied as well, but file ownership is not. If the source path + in the host file system refers to a directory it is copied to the destination path, recursively with + all containing files and directories. In this case the file ownership is copied + too.</para> + + <para>As with <option>--mount</option> file system checks are implicitly run before the copy + operation begins.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>--json=</option><replaceable>MODE</replaceable></term> + + <listitem><para>Shows output formatted as JSON. Expects one of <literal>short</literal> (for the + shortest possible output without any redundant whitespace or line breaks), <literal>pretty</literal> + (for a pretty version of the same, with indentation and line breaks) or <literal>off</literal> (to turn + off json output).</para></listitem> + </varlistentry> + + <xi:include href="standard-options.xml" xpointer="help" /> + <xi:include href="standard-options.xml" xpointer="version" /> + </variablelist> + + </refsect1> + + <refsect1> + <title>Options</title> + + <para>The following options are understood:</para> + + <variablelist> + <varlistentry> + <term><option>--read-only</option></term> + <term><option>-r</option></term> + + <listitem><para>Operate in read-only mode. By default <option>--mount</option> will establish + writable mount points. If this option is specified they are established in read-only mode + instead.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>--fsck=no</option></term> + + <listitem><para>Turn off automatic file system checking. By default when an image is accessed for + writing (by <option>--mount</option> or <option>--add</option>) the file systems contained in the OS + image are automatically checked using the appropriate <citerefentry + project='man-pages'><refentrytitle>fsck</refentrytitle><manvolnum>8</manvolnum></citerefentry> + command, in automatic fixing mode. This behavior may be switched off using + <option>--fsck=no</option>.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>--mkdir</option></term> + + <listitem><para>If combined with <option>--mount</option> the directory to mount the OS image to is + created if it is missing. Note that the directory is not automatically removed when the disk image is + unmounted again.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>--discard=</option></term> + + <listitem><para>Takes one of <literal>disabled</literal>, <literal>loop</literal>, + <literal>all</literal>, <literal>crypto</literal>. If <literal>disabled</literal> the image is + accessed with empty block discarding turned off. if <literal>loop</literal> discarding is enabled if + operating on a regular file. If <literal>crypt</literal> discarding is enabled even on encrypted file + systems. If <literal>all</literal> discarding is unconditionally enabled.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>--root-hash=</option></term> + <term><option>--root-hash-sig=</option></term> + <term><option>--verity-data=</option></term> + + <listitem><para>Configure various aspects of Verity data integrity for the OS + image. <option>--root-hash=</option> expects a hex-encoding top-level Verity hash to use for setting + up the Verity integrity protection. <option>--root-hash-sig=</option> expects the path to a file + containing a PKCS#7 signature file for the hash. This signature is passed to the kernel during + activation, which will match it against signature keys available in the kernel + keyring. <option>--verity-data=</option> expects the path to a file with the Verity data to use for + the OS image, in case it is stored in a detached file. It is recommended to embed the Verity data + directly in the image, using the Verity mechanisms in the <ulink + url="https://systemd.io/DISCOVERABLE_PARTITIONS">Discoverable Partitions Specification</ulink>.</para></listitem> + </varlistentry> + + </variablelist> + + </refsect1> + + <refsect1> + <title>Exit status</title> + + <para>On success, 0 is returned, a non-zero failure code + otherwise.</para> + </refsect1> + + <refsect1> + <title>See Also</title> + <para> + <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>, + <citerefentry><refentrytitle>system-nspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry>, + <citerefentry><refentrytitle>system.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry>, + <ulink url="https://systemd.io/DISCOVERABLE_PARTITIONS">Discoverable Partitions Specification</ulink>, + <citerefentry project='man-pages'><refentrytitle>umount</refentrytitle><manvolnum>8</manvolnum></citerefentry> + </para> + </refsect1> + +</refentry> diff --git a/meson.build b/meson.build index 134e95d475..227fe097a8 100644 --- a/meson.build +++ b/meson.build @@ -1898,8 +1898,7 @@ if conf.get('HAVE_BLKID') == 1 include_directories : includes, link_with : [libshared], install_rpath : rootlibexecdir, - install : true, - install_dir : rootlibexecdir) + install : true) endif if conf.get('ENABLE_RESOLVE') == 1 diff --git a/src/basic/copy.c b/src/basic/copy.c index b384010ae3..54f7235b16 100644 --- a/src/basic/copy.c +++ b/src/basic/copy.c @@ -969,6 +969,21 @@ int copy_times(int fdf, int fdt, CopyFlags flags) { return 0; } +int copy_access(int fdf, int fdt) { + struct stat st; + + assert(fdf >= 0); + assert(fdt >= 0); + + if (fstat(fdf, &st) < 0) + return -errno; + + if (fchmod(fdt, st.st_mode & 07777) < 0) + return -errno; + + return 0; +} + int copy_xattr(int fdf, int fdt) { _cleanup_free_ char *names = NULL; int ret = 0, r; diff --git a/src/basic/copy.h b/src/basic/copy.h index af8e88af04..ab9031038e 100644 --- a/src/basic/copy.h +++ b/src/basic/copy.h @@ -61,4 +61,5 @@ static inline int copy_bytes(int fdf, int fdt, uint64_t max_bytes, CopyFlags cop } int copy_times(int fdf, int fdt, CopyFlags flags); +int copy_access(int fdf, int fdt); int copy_xattr(int fdf, int fdt); diff --git a/src/basic/mkdir.c b/src/basic/mkdir.c index 6ebc2b95fd..d591e65e41 100644 --- a/src/basic/mkdir.c +++ b/src/basic/mkdir.c @@ -106,7 +106,7 @@ int mkdir_parents_internal(const char *prefix, const char *path, mode_t mode, ui /* return immediately if directory exists */ e = strrchr(path, '/'); if (!e) - return -EINVAL; + return 0; if (e == path) return 0; diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 318cd37c6f..375c5739b2 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -4,26 +4,44 @@ #include <getopt.h> #include <linux/loop.h> #include <stdio.h> +#include <sys/ioctl.h> +#include <sys/mount.h> #include "architecture.h" +#include "copy.h" #include "dissect-image.h" +#include "fd-util.h" +#include "format-table.h" +#include "format-util.h" +#include "fs-util.h" #include "hexdecoct.h" #include "log.h" #include "loop-util.h" #include "main-func.h" +#include "mkdir.h" +#include "mount-util.h" +#include "namespace-util.h" #include "parse-util.h" #include "path-util.h" +#include "pretty-print.h" +#include "stat-util.h" #include "string-util.h" #include "strv.h" +#include "terminal-util.h" +#include "tmpfile-util.h" #include "user-util.h" #include "util.h" static enum { ACTION_DISSECT, ACTION_MOUNT, + ACTION_COPY_FROM, + ACTION_COPY_TO, } arg_action = ACTION_DISSECT; static const char *arg_image = NULL; static const char *arg_path = NULL; +static const char *arg_source = NULL; +static const char *arg_target = NULL; static DissectImageFlags arg_flags = DISSECT_IMAGE_REQUIRE_ROOT|DISSECT_IMAGE_DISCARD_ON_LOOP|DISSECT_IMAGE_RELAX_VAR_CHECK|DISSECT_IMAGE_FSCK; static void *arg_root_hash = NULL; static char *arg_verity_data = NULL; @@ -31,21 +49,31 @@ static size_t arg_root_hash_size = 0; static char *arg_root_hash_sig_path = NULL; static void *arg_root_hash_sig = NULL; static size_t arg_root_hash_sig_size = 0; +static bool arg_json = false; +static JsonFormatFlags arg_json_format_flags = 0; STATIC_DESTRUCTOR_REGISTER(arg_root_hash, freep); STATIC_DESTRUCTOR_REGISTER(arg_verity_data, freep); STATIC_DESTRUCTOR_REGISTER(arg_root_hash_sig_path, freep); STATIC_DESTRUCTOR_REGISTER(arg_root_hash_sig, freep); -static void help(void) { - printf("%s [OPTIONS...] IMAGE\n" - "%s [OPTIONS...] --mount IMAGE PATH\n" - "Dissect a file system OS image.\n\n" - " -h --help Show this help\n" - " --version Show package version\n" - " -m --mount Mount the image to the specified directory\n" +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-dissect", "1", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] IMAGE\n" + "%1$s [OPTIONS...] --mount IMAGE PATH\n" + "%1$s [OPTIONS...] --copy-from IMAGE PATH [TARGET]\n" + "%1$s [OPTIONS...] --copy-to IMAGE [SOURCE] PATH\n\n" + "%5$sDissect a file system OS image.%6$s\n\n" + "%3$sOptions:%4$s\n" " -r --read-only Mount read-only\n" " --fsck=BOOL Run fsck before mounting\n" + " --mkdir Make mount directory before mounting, if missing\n" " --discard=MODE Choose 'discard' mode (disabled, loop, all, crypto)\n" " --root-hash=HASH Specify root hash for verity\n" " --root-hash-sig=SIG Specify pkcs7 signature of root hash for verity\n" @@ -53,9 +81,23 @@ static void help(void) { " or as an ASCII base64 encoded string prefixed by\n" " 'base64:'\n" " --verity-data=PATH Specify data file with hash tree for verity if it is\n" - " not embedded in IMAGE\n", - program_invocation_short_name, - program_invocation_short_name); + " not embedded in IMAGE\n" + " --json=pretty|short|off\n" + " Generate JSON output\n" + "\n%3$sCommands:%4$s\n" + " -h --help Show this help\n" + " --version Show package version\n" + " -m --mount Mount the image to the specified directory\n" + " -M Shortcut for --mount --mkdir\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" + , program_invocation_short_name + , link + , ansi_underline(), ansi_normal() + , ansi_highlight(), ansi_normal()); + + return 0; } static int parse_argv(int argc, char *argv[]) { @@ -67,6 +109,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_FSCK, ARG_VERITY_DATA, ARG_ROOT_HASH_SIG, + ARG_MKDIR, + ARG_JSON, }; static const struct option options[] = { @@ -79,6 +123,10 @@ static int parse_argv(int argc, char *argv[]) { { "fsck", required_argument, NULL, ARG_FSCK }, { "verity-data", required_argument, NULL, ARG_VERITY_DATA }, { "root-hash-sig", required_argument, NULL, ARG_ROOT_HASH_SIG }, + { "mkdir", no_argument, NULL, ARG_MKDIR }, + { "copy-from", no_argument, NULL, 'x' }, + { "copy-to", no_argument, NULL, 'a' }, + { "json", required_argument, NULL, ARG_JSON }, {} }; @@ -87,13 +135,12 @@ static int parse_argv(int argc, char *argv[]) { assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "hmr", options, NULL)) >= 0) { + while ((c = getopt_long(argc, argv, "hmrMxa", options, NULL)) >= 0) { switch (c) { case 'h': - help(); - return 0; + return help(); case ARG_VERSION: return version(); @@ -102,6 +149,25 @@ static int parse_argv(int argc, char *argv[]) { arg_action = ACTION_MOUNT; break; + case ARG_MKDIR: + arg_flags |= DISSECT_IMAGE_MKDIR; + break; + + case 'M': + /* Shortcut combination of the above two */ + arg_action = ACTION_MOUNT; + arg_flags |= DISSECT_IMAGE_MKDIR; + break; + + case 'x': + arg_action = ACTION_COPY_FROM; + arg_flags |= DISSECT_IMAGE_READ_ONLY; + break; + + case 'a': + arg_action = ACTION_COPY_TO; + break; + case 'r': arg_flags |= DISSECT_IMAGE_READ_ONLY; break; @@ -117,7 +183,13 @@ static int parse_argv(int argc, char *argv[]) { flags = DISSECT_IMAGE_DISCARD_ON_LOOP | DISSECT_IMAGE_DISCARD; else if (streq(optarg, "crypt")) flags = DISSECT_IMAGE_DISCARD_ANY; - else + else if (streq(optarg, "list")) { + puts("disabled\n" + "all\n" + "crypt\n" + "loop"); + return 0; + } else return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown --discard= parameter: %s", optarg); @@ -184,6 +256,26 @@ static int parse_argv(int argc, char *argv[]) { SET_FLAG(arg_flags, DISSECT_IMAGE_FSCK, r); break; + case ARG_JSON: + if (streq(optarg, "pretty")) { + arg_json = true; + arg_json_format_flags = JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO; + } else if (streq(optarg, "short")) { + arg_json = true; + arg_json_format_flags = JSON_FORMAT_NEWLINE; + } else if (streq(optarg, "off")) { + arg_json = false; + arg_json_format_flags = 0; + } else if (streq(optarg, "help")) { + puts("pretty\n" + "short\n" + "off"); + return 0; + } else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown argument to --json=: %s", optarg); + + break; + case '?': return -EINVAL; @@ -198,7 +290,7 @@ static int parse_argv(int argc, char *argv[]) { case ACTION_DISSECT: if (optind + 1 != argc) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Expected a file path as only argument."); + "Expected an image file path as only argument."); arg_image = argv[optind]; arg_flags |= DISSECT_IMAGE_READ_ONLY; @@ -207,87 +299,102 @@ static int parse_argv(int argc, char *argv[]) { case ACTION_MOUNT: if (optind + 2 != argc) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Expected a file path and mount point path as only arguments."); + "Expected an image file path and mount point path as only arguments."); arg_image = argv[optind]; arg_path = argv[optind + 1]; break; - default: - assert_not_reached("Unknown action."); - } - - return 1; -} - -static int run(int argc, char *argv[]) { - _cleanup_(loop_device_unrefp) LoopDevice *d = NULL; - _cleanup_(decrypted_image_unrefp) DecryptedImage *di = NULL; - _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL; - int r; - - log_parse_environment(); - log_open(); + case ACTION_COPY_FROM: + if (argc < optind + 2 || argc > optind + 3) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Expected an image file path, a source path and an optional destination path as only arguments."); - r = parse_argv(argc, argv); - if (r <= 0) - return r; + arg_image = argv[optind]; + arg_source = argv[optind + 1]; + arg_target = argc > optind + 2 ? argv[optind + 2] : "-" /* this means stdout */ ; - r = loop_device_make_by_path(arg_image, (arg_flags & DISSECT_IMAGE_READ_ONLY) ? O_RDONLY : O_RDWR, LO_FLAGS_PARTSCAN, &d); - if (r < 0) - return log_error_errno(r, "Failed to set up loopback device: %m"); + arg_flags |= DISSECT_IMAGE_READ_ONLY; + break; - r = verity_metadata_load(arg_image, NULL, arg_root_hash ? NULL : &arg_root_hash, &arg_root_hash_size, - arg_verity_data ? NULL : &arg_verity_data, - arg_root_hash_sig_path || arg_root_hash_sig ? NULL : &arg_root_hash_sig_path); - if (r < 0) - return log_error_errno(r, "Failed to read verity artefacts for %s: %m", arg_image); - arg_flags |= arg_verity_data ? DISSECT_IMAGE_NO_PARTITION_TABLE : 0; + case ACTION_COPY_TO: + if (argc < optind + 2 || argc > optind + 3) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Expected an image file path, an optional source path and a destination path as only arguments."); - r = dissect_image_and_warn(d->fd, arg_image, arg_root_hash, arg_root_hash_size, arg_verity_data, NULL, arg_flags, &m); - if (r < 0) - return r; + arg_image = argv[optind]; - switch (arg_action) { + if (argc > optind + 2) { + arg_source = argv[optind + 1]; + arg_target = argv[optind + 2]; + } else { + arg_source = "-"; /* this means stdin */ + arg_target = argv[optind + 1]; + } - case ACTION_DISSECT: { - unsigned i; + break; - for (i = 0; i < _PARTITION_DESIGNATOR_MAX; i++) { - DissectedPartition *p = m->partitions + i; + default: + assert_not_reached("Unknown action."); + } - if (!p->found) - continue; + return 1; +} - printf("Found %s '%s' partition", - p->rw ? "writable" : "read-only", - partition_designator_to_string(i)); +static int strv_pair_to_json(char **l, JsonVariant **ret) { + _cleanup_strv_free_ char **jl = NULL; + char **a, **b; - if (!sd_id128_is_null(p->uuid)) - printf(" (UUID " SD_ID128_FORMAT_STR ")", SD_ID128_FORMAT_VAL(p->uuid)); + STRV_FOREACH_PAIR(a, b, l) { + char *j; - if (p->fstype) - printf(" of type %s", p->fstype); + j = strjoin(*a, "=", *b); + if (!j) + return log_oom(); - if (p->architecture != _ARCHITECTURE_INVALID) - printf(" for %s", architecture_to_string(p->architecture)); + if (strv_consume(&jl, j) < 0) + return log_oom(); + } - if (dissected_image_can_do_verity(m, i)) - printf(" %s verity", dissected_image_has_verity(m, i) ? "with" : "without"); + return json_variant_new_array_strv(ret, jl); +} - if (p->partno >= 0) - printf(" on partition #%i", p->partno); +static int action_dissect(DissectedImage *m, LoopDevice *d) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(table_unrefp) Table *t = NULL; + uint64_t size = UINT64_MAX; + int r; - if (p->node) - printf(" (%s)", p->node); + assert(m); + assert(d); - putchar('\n'); - } + if (!arg_json) + printf(" Name: %s\n", basename(arg_image)); - r = dissected_image_acquire_metadata(m); - if (r < 0) - return log_error_errno(r, "Failed to acquire image metadata: %m"); + if (ioctl(d->fd, BLKGETSIZE64, &size) < 0) + log_debug_errno(errno, "Failed to query size of loopback device: %m"); + else if (!arg_json) { + char s[FORMAT_BYTES_MAX]; + printf(" Size: %s\n", format_bytes(s, sizeof(s), size)); + } + if (!arg_json) + putc('\n', stdout); + + r = dissected_image_acquire_metadata(m); + if (r == -ENXIO) + return log_error_errno(r, "No root partition discovered."); + if (r == -EMEDIUMTYPE) + return log_error_errno(r, "Not a valid OS image, no os-release file included."); + if (r == -EUCLEAN) + return log_error_errno(r, "File system check of image failed."); + if (r == -EUNATCH) + log_warning_errno(r, "OS image is encrypted, proceeding without showing OS image metadata."); + else if (r == -EBUSY) + log_warning_errno(r, "OS image is currently in use, proceeding without showing OS image metadata."); + else if (r < 0) + return log_error_errno(r, "Failed to acquire image metadata: %m"); + else if (!arg_json) { if (m->hostname) printf(" Hostname: %s\n", m->hostname); @@ -311,35 +418,381 @@ static int run(int argc, char *argv[]) { p == m->os_release ? "OS Release:" : " ", *p, *q); } + } - break; + if (arg_json) { + _cleanup_(json_variant_unrefp) JsonVariant *mi = NULL, *osr = NULL; + + if (!strv_isempty(m->machine_info)) { + r = strv_pair_to_json(m->machine_info, &mi); + if (r < 0) + return log_oom(); + } + + if (!strv_isempty(m->os_release)) { + r = strv_pair_to_json(m->os_release, &osr); + if (r < 0) + return log_oom(); + } + + r = json_build(&v, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("name", JSON_BUILD_STRING(basename(arg_image))), + JSON_BUILD_PAIR("size", JSON_BUILD_INTEGER(size)), + JSON_BUILD_PAIR_CONDITION(m->hostname, "hostname", JSON_BUILD_STRING(m->hostname)), + 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)))); + if (r < 0) + return log_oom(); } - case ACTION_MOUNT: - r = dissected_image_decrypt_interactively(m, NULL, arg_root_hash, arg_root_hash_size, arg_verity_data, arg_root_hash_sig_path, arg_root_hash_sig, arg_root_hash_sig_size, arg_flags, &di); + if (!arg_json) + putc('\n', stdout); + + t = table_new("rw", "designator", "partition uuid", "fstype", "architecture", "verity", "node", "partno"); + if (!t) + return log_oom(); + + (void) table_set_empty_string(t, "-"); + (void) table_set_align_percent(t, table_get_cell(t, 0, 7), 100); + + for (unsigned i = 0; i < _PARTITION_DESIGNATOR_MAX; i++) { + DissectedPartition *p = m->partitions + i; + + if (!p->found) + continue; + + r = table_add_many( + t, + TABLE_STRING, p->rw ? "rw" : "ro", + TABLE_STRING, partition_designator_to_string(i)); if (r < 0) - return r; + return table_log_add_error(r); - r = dissected_image_mount(m, arg_path, UID_INVALID, arg_flags); - if (r == -EUCLEAN) - return log_error_errno(r, "File system check on image failed: %m"); + if (sd_id128_is_null(p->uuid)) + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + else + r = table_add_cell(t, NULL, TABLE_UUID, &p->uuid); + if (r < 0) + return table_log_add_error(r); + + r = table_add_many( + t, + TABLE_STRING, p->fstype, + TABLE_STRING, architecture_to_string(p->architecture)); + if (r < 0) + return table_log_add_error(r); + + if (arg_verity_data) + r = table_add_cell(t, NULL, TABLE_STRING, "external"); + else if (dissected_image_can_do_verity(m, i)) + r = table_add_cell(t, NULL, TABLE_STRING, yes_no(dissected_image_has_verity(m, i))); + else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); if (r < 0) - return log_error_errno(r, "Failed to mount image: %m"); + return table_log_add_error(r); - if (di) { - r = decrypted_image_relinquish(di); + if (p->partno < 0) /* no partition table, naked file system */ { + r = table_add_cell(t, NULL, TABLE_STRING, arg_image); if (r < 0) - return log_error_errno(r, "Failed to relinquish DM devices: %m"); + return table_log_add_error(r); + + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + } else { + r = table_add_cell(t, NULL, TABLE_STRING, p->node); + if (r < 0) + return table_log_add_error(r); + + r = table_add_cell(t, NULL, TABLE_INT, &p->partno); } + if (r < 0) + return table_log_add_error(r); + } + + if (arg_json) { + _cleanup_(json_variant_unrefp) JsonVariant *jt = NULL; + + r = table_to_json(t, &jt); + if (r < 0) + return log_error_errno(r, "Failed to convert table to JSON: %m"); + + r = json_variant_set_field(&v, "mounts", jt); + if (r < 0) + return log_oom(); + + json_variant_dump(v, arg_json_format_flags, stdout, NULL); + } else { + r = table_print(t, stdout); + if (r < 0) + return log_error_errno(r, "Failed to dump table: %m"); + } + + return 0; +} + +static int action_mount(DissectedImage *m, LoopDevice *d) { + _cleanup_(decrypted_image_unrefp) DecryptedImage *di = NULL; + int r; + + assert(m); + assert(d); - loop_device_relinquish(d); + r = dissected_image_decrypt_interactively( + m, NULL, + arg_root_hash, arg_root_hash_size, + arg_verity_data, + arg_root_hash_sig_path, arg_root_hash_sig, arg_root_hash_sig_size, + arg_flags, + &di); + if (r < 0) + return r; + + r = dissected_image_mount_and_warn(m, arg_path, UID_INVALID, arg_flags); + if (r < 0) + return r; + + if (di) { + r = decrypted_image_relinquish(di); + if (r < 0) + return log_error_errno(r, "Failed to relinquish DM devices: %m"); + } + + loop_device_relinquish(d); + return 0; +} + +static int action_copy(DissectedImage *m, LoopDevice *d) { + _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; + _cleanup_(decrypted_image_unrefp) DecryptedImage *di = NULL; + _cleanup_(rmdir_and_freep) char *created_dir = NULL; + _cleanup_free_ char *temp = NULL; + int r; + + assert(m); + assert(d); + + r = dissected_image_decrypt_interactively( + m, NULL, + arg_root_hash, arg_root_hash_size, + arg_verity_data, + arg_root_hash_sig_path, arg_root_hash_sig, arg_root_hash_sig_size, + arg_flags, + &di); + if (r < 0) + return r; + + r = detach_mount_namespace(); + if (r < 0) + return log_error_errno(r, "Failed to detach mount namespace: %m"); + + 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, arg_flags); + if (r < 0) + return r; + + mounted_dir = TAKE_PTR(created_dir); + + if (di) { + r = decrypted_image_relinquish(di); + if (r < 0) + return log_error_errno(r, "Failed to relinquish DM devices: %m"); + } + + loop_device_relinquish(d); + + if (arg_action == ACTION_COPY_FROM) { + _cleanup_close_ int source_fd = -1, target_fd = -1; + + source_fd = chase_symlinks_and_open(arg_source, mounted_dir, CHASE_PREFIX_ROOT|CHASE_WARN, O_RDONLY|O_CLOEXEC|O_NOCTTY, NULL); + if (source_fd < 0) + return log_error_errno(source_fd, "Failed to open source path '%s' in image '%s': %m", arg_source, arg_image); + + /* Copying to stdout? */ + if (streq(arg_target, "-")) { + r = copy_bytes(source_fd, STDOUT_FILENO, (uint64_t) -1, COPY_REFLINK); + if (r < 0) + return log_error_errno(r, "Failed to copy bytes from %s in mage '%s' to stdout: %m", arg_source, arg_image); + + /* When we copy to stdou we don't copy any attributes (i.e. no access mode, no ownership, no xattr, no times) */ + return 0; + } + + /* Try to copy as directory? */ + r = copy_directory_fd(source_fd, arg_target, COPY_REFLINK|COPY_MERGE_EMPTY|COPY_SIGINT); + if (r >= 0) + return 0; + if (r != -ENOTDIR) + return log_error_errno(r, "Failed to copy %s in image '%s' to '%s': %m", arg_source, arg_image, arg_target); + + r = fd_verify_regular(source_fd); + if (r == -EISDIR) + return log_error_errno(r, "Target '%s' exists already and is not a directory.", arg_target); + if (r < 0) + return log_error_errno(r, "Source path %s in image '%s' is neither regular file nor directory, refusing: %m", arg_source, arg_image); + + /* Nah, it's a plain file! */ + target_fd = open(arg_target, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW, 0600); + if (target_fd < 0) + return log_error_errno(errno, "Failed to create regular file at target path '%s': %m", arg_target); + + r = copy_bytes(source_fd, target_fd, (uint64_t) -1, COPY_REFLINK); + if (r < 0) + return log_error_errno(r, "Failed to copy bytes from %s in mage '%s' to '%s': %m", arg_source, arg_image, arg_target); + + (void) copy_xattr(source_fd, target_fd); + (void) copy_access(source_fd, target_fd); + (void) copy_times(source_fd, target_fd, 0); + + /* When this is a regular file we don't copy ownership! */ + + } else { + _cleanup_close_ int source_fd = -1, target_fd = -1; + _cleanup_close_ int dfd = -1; + _cleanup_free_ char *dn = NULL; + + assert(arg_action == ACTION_COPY_TO); + + dn = dirname_malloc(arg_target); + if (!dn) + return log_oom(); + + r = chase_symlinks(dn, mounted_dir, CHASE_PREFIX_ROOT|CHASE_WARN, NULL, &dfd); + if (r < 0) + return log_error_errno(r, "Failed to open '%s': %m", dn); + + /* Are we reading from stdin? */ + if (streq(arg_source, "-")) { + target_fd = openat(dfd, basename(arg_target), O_WRONLY|O_CREAT|O_CLOEXEC|O_NOCTTY|O_EXCL, 0644); + if (target_fd < 0) + return log_error_errno(errno, "Failed to open target file '%s': %m", arg_target); + + r = copy_bytes(STDIN_FILENO, target_fd, (uint64_t) -1, COPY_REFLINK); + if (r < 0) + return log_error_errno(r, "Failed to copy bytes from stdin to '%s' in image '%s': %m", arg_target, arg_image); + + /* When we copy from stdin we don't copy any attributes (i.e. no access mode, no ownership, no xattr, no times) */ + return 0; + } + + source_fd = open(arg_source, O_RDONLY|O_CLOEXEC|O_NOCTTY); + if (source_fd < 0) + return log_error_errno(source_fd, "Failed to open source path '%s': %m", arg_source); + + r = fd_verify_regular(source_fd); + if (r < 0) { + if (r != -EISDIR) + return log_error_errno(r, "Source '%s' is neither regular file nor directory: %m", arg_source); + + /* We are looking at a directory. */ + + target_fd = openat(dfd, basename(arg_target), O_RDONLY|O_DIRECTORY|O_CLOEXEC); + if (target_fd < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to open destination '%s': %m", arg_target); + + r = copy_tree_at(source_fd, ".", dfd, basename(arg_target), UID_INVALID, GID_INVALID, COPY_REFLINK|COPY_REPLACE|COPY_SIGINT); + } else + r = copy_tree_at(source_fd, ".", target_fd, ".", UID_INVALID, GID_INVALID, COPY_REFLINK|COPY_REPLACE|COPY_SIGINT); + if (r < 0) + return log_error_errno(r, "Failed to copy '%s' to '%s' in image '%s': %m", arg_source, arg_target, arg_image); + + return 0; + } + + /* We area looking at a regular file */ + target_fd = openat(dfd, basename(arg_target), O_WRONLY|O_CREAT|O_CLOEXEC|O_NOCTTY|O_EXCL, 0600); + if (target_fd < 0) + return log_error_errno(errno, "Failed to open target file '%s': %m", arg_target); + + r = copy_bytes(source_fd, target_fd, (uint64_t) -1, COPY_REFLINK); + if (r < 0) + return log_error_errno(r, "Failed to copy bytes from '%s' to '%s' in image '%s': %m", arg_source, arg_target, arg_image); + + (void) copy_xattr(source_fd, target_fd); + (void) copy_access(source_fd, target_fd); + (void) copy_times(source_fd, target_fd, 0); + + /* When this is a regular file we don't copy ownership! */ + } + + return 0; +} + +static int run(int argc, char *argv[]) { + _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL; + _cleanup_(loop_device_unrefp) LoopDevice *d = NULL; + int r; + + log_parse_environment(); + log_open(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + r = verity_metadata_load( + arg_image, NULL, + arg_root_hash ? NULL : &arg_root_hash, + &arg_root_hash_size, + arg_verity_data ? NULL : &arg_verity_data, + arg_root_hash_sig_path || arg_root_hash_sig ? NULL : &arg_root_hash_sig_path); + if (r < 0) + return log_error_errno(r, "Failed to read verity artifacts for %s: %m", arg_image); + + r = loop_device_make_by_path( + arg_image, + (arg_flags & DISSECT_IMAGE_READ_ONLY) ? O_RDONLY : O_RDWR, + arg_verity_data ? 0 : LO_FLAGS_PARTSCAN, + &d); + if (r < 0) + return log_error_errno(r, "Failed to set up loopback device: %m"); + + if (arg_verity_data) + arg_flags |= DISSECT_IMAGE_NO_PARTITION_TABLE; /* We only support Verity per file system, + * hence if there's external Verity data + * available we turn off partition table + * support */ + r = dissect_image_and_warn( + d->fd, + arg_image, + arg_root_hash, + arg_root_hash_size, + arg_verity_data, + NULL, + arg_flags, + &m); + if (r < 0) + return r; + + switch (arg_action) { + + case ACTION_DISSECT: + r = action_dissect(m, d); + break; + + case ACTION_MOUNT: + r = action_mount(m, d); + break; + + case ACTION_COPY_FROM: + case ACTION_COPY_TO: + r = action_copy(m, d); break; default: assert_not_reached("Unknown action."); } - return 0; + return r; } DEFINE_MAIN_FUNCTION(run); diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index d56de0bb25..e4c7a2d374 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -23,7 +23,6 @@ #include "memory-util.h" #include "mkdir.h" #include "mount-util.h" -#include "namespace-util.h" #include "os-util.h" #include "parse-util.h" #include "path-util.h" diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 6d6fe87ed1..1b83f5ad58 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -3369,14 +3369,13 @@ static int outer_child( * uid shift known. That way we can mount VFAT file systems shifted to the right place right away. This * makes sure ESP partitions and userns are compatible. */ - r = dissected_image_mount(dissected_image, directory, arg_uid_shift, - DISSECT_IMAGE_MOUNT_ROOT_ONLY|DISSECT_IMAGE_DISCARD_ON_LOOP| - (arg_read_only ? DISSECT_IMAGE_READ_ONLY : DISSECT_IMAGE_FSCK)| - (arg_start_mode == START_BOOT ? DISSECT_IMAGE_VALIDATE_OS : 0)); - if (r == -EUCLEAN) - return log_error_errno(r, "File system check for image failed: %m"); + r = dissected_image_mount_and_warn( + dissected_image, directory, arg_uid_shift, + DISSECT_IMAGE_MOUNT_ROOT_ONLY|DISSECT_IMAGE_DISCARD_ON_LOOP| + (arg_read_only ? DISSECT_IMAGE_READ_ONLY : DISSECT_IMAGE_FSCK)| + (arg_start_mode == START_BOOT ? DISSECT_IMAGE_VALIDATE_OS : 0)); if (r < 0) - return log_error_errno(r, "Failed to mount image root file system: %m"); + return r; } r = determine_uid_shift(directory); diff --git a/src/partition/repart.c b/src/partition/repart.c index 9a9fd6dff3..572db3bdc4 100644 --- a/src/partition/repart.c +++ b/src/partition/repart.c @@ -2826,7 +2826,7 @@ static int help(void) { " --seed=UUID 128bit seed UUID to derive all UUIDs from\n" " --size=BYTES Grow loopback file to specified size\n" " --json=pretty|short|off\n" - " Generate json output\n" + " Generate JSON output\n" "\nSee the %s for details.\n" , program_invocation_short_name , ansi_highlight(), ansi_normal() diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index d3f183b50c..8cbf4828b8 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -9,6 +9,7 @@ #include <sys/mount.h> #include <sys/prctl.h> #include <sys/wait.h> +#include <sysexits.h> #include "sd-device.h" #include "sd-id128.h" @@ -1003,9 +1004,9 @@ static int mount_partition( if (!m->found || !node || !fstype) return 0; - /* Stacked encryption? Yuck */ + /* We are looking at an encrypted partition? This either means stacked encryption, or the caller didn't call dissected_image_decrypt() beforehand. Let's return a recognizable error for this case. */ if (streq_ptr(fstype, "crypto_LUKS")) - return -ELOOP; + return -EUNATCH; rw = m->rw && !(flags & DISSECT_IMAGE_READ_ONLY); @@ -1047,6 +1048,12 @@ static int mount_partition( if (!strextend_with_separator(&options, ",", m->mount_options, NULL)) return -ENOMEM; + if (FLAGS_SET(flags, DISSECT_IMAGE_MKDIR)) { + r = mkdir_p(p, 0755); + if (r < 0) + return r; + } + r = mount_verbose(LOG_DEBUG, node, p, fstype, MS_NODEV|(rw ? 0 : MS_RDONLY), options); if (r < 0) return r; @@ -1060,6 +1067,15 @@ int dissected_image_mount(DissectedImage *m, const char *where, uid_t uid_shift, assert(m); assert(where); + /* Returns: + * + * -ENXIO → No root partition found + * -EMEDIUMTYPE → DISSECT_IMAGE_VALIDATE_OS set but no os-release file found + * -EUNATCH → Encrypted partition found for which no dm-crypt was set up yet + * -EUCLEAN → fsck for file system failed + * -EBUSY → File system already mounted/used elsewhere (kernel) + */ + if (!m->partitions[PARTITION_ROOT].found) return -ENXIO; @@ -1080,6 +1096,10 @@ int dissected_image_mount(DissectedImage *m, const char *where, uid_t uid_shift, if (flags & DISSECT_IMAGE_MOUNT_ROOT_ONLY) return 0; + /* Mask DISSECT_IMAGE_MKDIR for all subdirs: the idea is that only the top-level mount point is + * created if needed, but the image itself not modified. */ + flags &= ~DISSECT_IMAGE_MKDIR; + r = mount_partition(m->partitions + PARTITION_HOME, where, "/home", uid_shift, flags); if (r < 0) return r; @@ -1125,6 +1145,29 @@ int dissected_image_mount(DissectedImage *m, const char *where, uid_t uid_shift, return 0; } +int dissected_image_mount_and_warn(DissectedImage *m, const char *where, uid_t uid_shift, DissectImageFlags flags) { + int r; + + assert(m); + assert(where); + + r = dissected_image_mount(m, where, uid_shift, flags); + if (r == -ENXIO) + return log_error_errno(r, "Not root file system found in image."); + if (r == -EMEDIUMTYPE) + return log_error_errno(r, "No suitable os-release file in image found."); + if (r == -EUNATCH) + return log_error_errno(r, "Encrypted file system discovered, but decryption not requested."); + if (r == -EUCLEAN) + return log_error_errno(r, "File system check on image failed."); + if (r == -EBUSY) + return log_error_errno(r, "File system already mounted elsewhere."); + if (r < 0) + return log_error_errno(r, "Failed to mount image: %m"); + + return r; +} + #if HAVE_LIBCRYPTSETUP typedef struct DecryptedPartition { struct crypt_device *device; @@ -1586,7 +1629,14 @@ int decrypted_image_relinquish(DecryptedImage *d) { return 0; } -int verity_metadata_load(const char *image, const char *root_hash_path, void **ret_roothash, size_t *ret_roothash_size, char **ret_verity_data, char **ret_roothashsig) { +int verity_metadata_load( + const char *image, + const char *root_hash_path, + void **ret_roothash, + size_t *ret_roothash_size, + char **ret_verity_data, + char **ret_roothashsig) { + _cleanup_free_ char *verity_filename = NULL, *roothashsig_filename = NULL; _cleanup_free_ void *roothash_decoded = NULL; size_t roothash_decoded_size = 0; @@ -1722,12 +1772,14 @@ int dissected_image_acquire_metadata(DissectedImage *m) { }; _cleanup_strv_free_ char **machine_info = NULL, **os_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; sd_id128_t machine_id = SD_ID128_NULL; _cleanup_free_ char *hostname = NULL; unsigned n_meta_initialized = 0, k; - int fds[2 * _META_MAX], r; + int fds[2 * _META_MAX], r, v; + ssize_t n; BLOCK_SIGNALS(SIGCHLD); @@ -1743,18 +1795,28 @@ int dissected_image_acquire_metadata(DissectedImage *m) { if (r < 0) goto finish; + if (pipe2(error_pipe, O_CLOEXEC) < 0) { + r = -errno; + goto finish; + } + r = safe_fork("(sd-dissect)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE, &child); if (r < 0) goto finish; if (r == 0) { + error_pipe[0] = safe_close(error_pipe[0]); + r = dissected_image_mount(m, t, UID_INVALID, DISSECT_IMAGE_READ_ONLY|DISSECT_IMAGE_MOUNT_ROOT_ONLY|DISSECT_IMAGE_VALIDATE_OS); if (r < 0) { + /* Let parent know the error */ + (void) write(error_pipe[1], &r, sizeof(r)); + log_debug_errno(r, "Failed to mount dissected image: %m"); _exit(EXIT_FAILURE); } for (k = 0; k < _META_MAX; k++) { - _cleanup_close_ int fd = -1; + _cleanup_close_ int fd = -ENOENT; const char *p; fds[2*k] = safe_close(fds[2*k]); @@ -1766,12 +1828,15 @@ int dissected_image_acquire_metadata(DissectedImage *m) { } if (fd < 0) { log_debug_errno(fd, "Failed to read %s file of image, ignoring: %m", paths[k]); + fds[2*k+1] = safe_close(fds[2*k+1]); continue; } r = copy_bytes(fd, fds[2*k+1], (uint64_t) -1, 0); - if (r < 0) + if (r < 0) { + (void) write(error_pipe[1], &r, sizeof(r)); _exit(EXIT_FAILURE); + } fds[2*k+1] = safe_close(fds[2*k+1]); } @@ -1779,6 +1844,8 @@ int dissected_image_acquire_metadata(DissectedImage *m) { _exit(EXIT_SUCCESS); } + error_pipe[1] = safe_close(error_pipe[1]); + for (k = 0; k < _META_MAX; k++) { _cleanup_fclose_ FILE *f = NULL; @@ -1836,7 +1903,16 @@ int dissected_image_acquire_metadata(DissectedImage *m) { r = wait_for_terminate_and_check("(sd-dissect)", child, 0); child = 0; if (r < 0) - goto finish; + return r; + + n = read(error_pipe[0], &v, sizeof(v)); + if (n < 0) + return -errno; + if (n == sizeof(v)) + return v; /* propagate error sent to us from child */ + if (n != 0) + return -EIO; + if (r != EXIT_SUCCESS) return -EPROTO; @@ -1995,11 +2071,9 @@ int mount_image_privately_interactively( created_dir = TAKE_PTR(temp); - r = dissected_image_mount(dissected_image, created_dir, UID_INVALID, flags); - if (r == -EUCLEAN) - return log_error_errno(r, "File system check on image failed: %m"); + r = dissected_image_mount_and_warn(dissected_image, created_dir, UID_INVALID, flags); if (r < 0) - return log_error_errno(r, "Failed to mount image: %m"); + return r; if (decrypted_image) { r = decrypted_image_relinquish(decrypted_image); diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index 7f67c8745e..4d21789e18 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -69,6 +69,7 @@ typedef enum DissectImageFlags { DISSECT_IMAGE_FSCK = 1 << 11, /* File system check the partition before mounting (no effect when combined with DISSECT_IMAGE_READ_ONLY) */ DISSECT_IMAGE_NO_PARTITION_TABLE = 1 << 12, /* Only recognize single file system images */ DISSECT_IMAGE_VERITY_SHARE = 1 << 13, /* When activating a verity device, reuse existing one if already open */ + DISSECT_IMAGE_MKDIR = 1 << 14, /* Make directory to mount right before mounting, if missing */ } DissectImageFlags; struct DissectedImage { @@ -105,6 +106,7 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(DissectedImage*, dissected_image_unref); int dissected_image_decrypt(DissectedImage *m, const char *passphrase, const void *root_hash, size_t root_hash_size, const char *verity_data, const char *root_hash_sig_path, const void *root_hash_sig, size_t root_hash_sig_size, DissectImageFlags flags, DecryptedImage **ret); int dissected_image_decrypt_interactively(DissectedImage *m, const char *passphrase, const void *root_hash, size_t root_hash_size, const char *verity_data, const char *root_hash_sig_path, const void *root_hash_sig, size_t root_hash_sig_size, DissectImageFlags flags, DecryptedImage **ret); int dissected_image_mount(DissectedImage *m, const char *dest, uid_t uid_shift, DissectImageFlags flags); +int dissected_image_mount_and_warn(DissectedImage *m, const char *where, uid_t uid_shift, DissectImageFlags flags); int dissected_image_acquire_metadata(DissectedImage *m); diff --git a/src/shared/json.c b/src/shared/json.c index 27a3a518fe..2e151578ff 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -430,6 +430,12 @@ int json_variant_new_base64(JsonVariant **ret, const void *p, size_t n) { return json_variant_new_stringn(ret, s, k); } +int json_variant_new_id128(JsonVariant **ret, sd_id128_t id) { + char s[SD_ID128_STRING_MAX]; + + return json_variant_new_string(ret, sd_id128_to_string(id, s)); +} + static void json_variant_set(JsonVariant *a, JsonVariant *b) { assert(a); @@ -1964,6 +1970,17 @@ int json_variant_set_field_boolean(JsonVariant **v, const char *field, bool b) { return json_variant_set_field(v, field, m); } +int json_variant_set_field_strv(JsonVariant **v, const char *field, char **l) { + _cleanup_(json_variant_unrefp) JsonVariant *m = NULL; + int r; + + r = json_variant_new_array_strv(&m, l); + if (r < 0) + return r; + + return json_variant_set_field(v, field, m); +} + int json_variant_merge(JsonVariant **v, JsonVariant *m) { _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; _cleanup_free_ JsonVariant **array = NULL; @@ -3579,6 +3596,34 @@ int json_buildv(JsonVariant **ret, va_list ap) { break; } + case _JSON_BUILD_ID128: { + sd_id128_t id; + + if (!IN_SET(current->expect, EXPECT_TOPLEVEL, EXPECT_OBJECT_VALUE, EXPECT_ARRAY_ELEMENT)) { + r = -EINVAL; + goto finish; + } + + id = va_arg(ap, sd_id128_t); + + if (current->n_suppress == 0) { + r = json_variant_new_id128(&add, id); + if (r < 0) + goto finish; + } + + n_subtract = 1; + + if (current->expect == EXPECT_TOPLEVEL) + current->expect = EXPECT_END; + else if (current->expect == EXPECT_OBJECT_VALUE) + current->expect = EXPECT_OBJECT_KEY; + else + assert(current->expect == EXPECT_ARRAY_ELEMENT); + + break; + } + case _JSON_BUILD_OBJECT_BEGIN: if (!IN_SET(current->expect, EXPECT_TOPLEVEL, EXPECT_OBJECT_VALUE, EXPECT_ARRAY_ELEMENT)) { diff --git a/src/shared/json.h b/src/shared/json.h index ceb01a2028..ae71593d7b 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -7,6 +7,8 @@ #include <stdint.h> #include <stdio.h> +#include "sd-id128.h" + #include "macro.h" #include "string-util.h" #include "log.h" @@ -65,6 +67,7 @@ int json_variant_new_array_bytes(JsonVariant **ret, const void *p, size_t n); int json_variant_new_array_strv(JsonVariant **ret, char **l); int json_variant_new_object(JsonVariant **ret, JsonVariant **array, size_t n); int json_variant_new_null(JsonVariant **ret); +int json_variant_new_id128(JsonVariant **ret, sd_id128_t id); static inline int json_variant_new_string(JsonVariant **ret, const char *s) { return json_variant_new_stringn(ret, s, (size_t) -1); @@ -183,6 +186,7 @@ int json_variant_set_field_string(JsonVariant **v, const char *field, const char int json_variant_set_field_integer(JsonVariant **v, const char *field, intmax_t value); int json_variant_set_field_unsigned(JsonVariant **v, const char *field, uintmax_t value); int json_variant_set_field_boolean(JsonVariant **v, const char *field, bool b); +int json_variant_set_field_strv(JsonVariant **v, const char *field, char **l); int json_variant_append_array(JsonVariant **v, JsonVariant *element); @@ -223,6 +227,7 @@ enum { _JSON_BUILD_LITERAL, _JSON_BUILD_STRV, _JSON_BUILD_BASE64, + _JSON_BUILD_ID128, _JSON_BUILD_MAX, }; @@ -243,6 +248,7 @@ enum { #define JSON_BUILD_LITERAL(l) _JSON_BUILD_LITERAL, ({ const char *_x = l; _x; }) #define JSON_BUILD_STRV(l) _JSON_BUILD_STRV, ({ char **_x = l; _x; }) #define JSON_BUILD_BASE64(p, n) _JSON_BUILD_BASE64, ({ const void *_x = p; _x; }), ({ size_t _y = n; _y; }) +#define JSON_BUILD_ID128(id) _JSON_BUILD_ID128, ({ sd_id128_t _x = id; _x; }) int json_build(JsonVariant **ret, ...); int json_buildv(JsonVariant **ret, va_list ap); diff --git a/test/units/testsuite-50.sh b/test/units/testsuite-50.sh index 587184e854..bb3b20da3e 100755 --- a/test/units/testsuite-50.sh +++ b/test/units/testsuite-50.sh @@ -28,25 +28,25 @@ cp /usr/share/minimal.* "${image_dir}/" image="${image_dir}/minimal" roothash="$(cat ${image}.roothash)" -/usr/lib/systemd/systemd-dissect ${image}.raw | grep -q -F "Found read-only 'root' partition of type squashfs with verity" -/usr/lib/systemd/systemd-dissect ${image}.raw | grep -q -F "MARKER=1" -/usr/lib/systemd/systemd-dissect ${image}.raw | grep -q -F -f /usr/lib/os-release +systemd-dissect --json=short ${image}.raw | grep -q -F '{"rw":"ro","designator":"root","partition_uuid":null,"fstype":"squashfs","architecture":null,"verity":"external"' +systemd-dissect ${image}.raw | grep -q -F "MARKER=1" +systemd-dissect ${image}.raw | grep -q -F -f /usr/lib/os-release mv ${image}.verity ${image}.fooverity mv ${image}.roothash ${image}.foohash -/usr/lib/systemd/systemd-dissect ${image}.raw --root-hash=${roothash} --verity-data=${image}.fooverity | grep -q -F "Found read-only 'root' partition of type squashfs with verity" -/usr/lib/systemd/systemd-dissect ${image}.raw --root-hash=${roothash} --verity-data=${image}.fooverity | grep -q -F "MARKER=1" -/usr/lib/systemd/systemd-dissect ${image}.raw --root-hash=${roothash} --verity-data=${image}.fooverity | grep -q -F -f /usr/lib/os-release +systemd-dissect --json=short ${image}.raw --root-hash=${roothash} --verity-data=${image}.fooverity | grep -q -F '{"rw":"ro","designator":"root","partition_uuid":null,"fstype":"squashfs","architecture":null,"verity":"external"' +systemd-dissect ${image}.raw --root-hash=${roothash} --verity-data=${image}.fooverity | grep -q -F "MARKER=1" +systemd-dissect ${image}.raw --root-hash=${roothash} --verity-data=${image}.fooverity | grep -q -F -f /usr/lib/os-release mv ${image}.fooverity ${image}.verity mv ${image}.foohash ${image}.roothash mkdir -p ${image_dir}/mount ${image_dir}/mount2 -/usr/lib/systemd/systemd-dissect --mount ${image}.raw ${image_dir}/mount +systemd-dissect --mount ${image}.raw ${image_dir}/mount cat ${image_dir}/mount/usr/lib/os-release | grep -q -F -f /usr/lib/os-release cat ${image_dir}/mount/etc/os-release | grep -q -F -f /usr/lib/os-release cat ${image_dir}/mount/usr/lib/os-release | grep -q -F "MARKER=1" # Verity volume should be shared (opened only once) -/usr/lib/systemd/systemd-dissect --mount ${image}.raw ${image_dir}/mount2 +systemd-dissect --mount ${image}.raw ${image_dir}/mount2 verity_count=$(ls -1 /dev/mapper/ | grep -c verity) # In theory we should check that count is exactly one. In practice, libdevmapper # randomly and unpredictably fails with an unhelpful EINVAL when a device is open @@ -111,12 +111,16 @@ dd if=${image}.raw of=${loop}p1 dd if=${image}.verity of=${loop}p2 losetup -d ${loop} -/usr/lib/systemd/systemd-dissect --root-hash ${roothash} ${image}.gpt | grep -q "Found read-only 'root' partition (UUID $(head -c 32 ${image}.roothash)) of type squashfs for .* with verity on partition #1" -/usr/lib/systemd/systemd-dissect --root-hash ${roothash} ${image}.gpt | grep -q "Found read-only 'root-verity' partition (UUID $(tail -c 32 ${image}.roothash)) of type DM_verity_hash for .* on partition #2" -/usr/lib/systemd/systemd-dissect --root-hash ${roothash} ${image}.gpt | grep -q -F "MARKER=1" -/usr/lib/systemd/systemd-dissect --root-hash ${roothash} ${image}.gpt | grep -q -F -f /usr/lib/os-release +# Derive partition UUIDs from root hash, in UUID syntax +ROOT_UUID=$(systemd-id128 -u show $(head -c 32 ${image}.roothash) -u | tail -n 1 | cut -b 6-) +VERITY_UUID=$(systemd-id128 -u show $(tail -c 32 ${image}.roothash) -u | tail -n 1 | cut -b 6-) -/usr/lib/systemd/systemd-dissect --root-hash ${roothash} --mount ${image}.gpt ${image_dir}/mount +systemd-dissect --json=short --root-hash ${roothash} ${image}.gpt | grep -q '{"rw":"ro","designator":"root","partition_uuid":"'$ROOT_UUID'","fstype":"squashfs","architecture":"x86-64","verity":"yes","node":' +systemd-dissect --json=short --root-hash ${roothash} ${image}.gpt | grep -q '{"rw":"ro","designator":"root-verity","partition_uuid":"'$VERITY_UUID'","fstype":"DM_verity_hash","architecture":"x86-64","verity":null,"node":' +systemd-dissect --root-hash ${roothash} ${image}.gpt | grep -q -F "MARKER=1" +systemd-dissect --root-hash ${roothash} ${image}.gpt | grep -q -F -f /usr/lib/os-release + +systemd-dissect --root-hash ${roothash} --mount ${image}.gpt ${image_dir}/mount cat ${image_dir}/mount/usr/lib/os-release | grep -q -F -f /usr/lib/os-release cat ${image_dir}/mount/etc/os-release | grep -q -F -f /usr/lib/os-release cat ${image_dir}/mount/usr/lib/os-release | grep -q -F "MARKER=1" |