]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
repart: Add support for generating verity sig partitions 24635/head
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Sun, 11 Sep 2022 08:49:24 +0000 (10:49 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Fri, 23 Sep 2022 14:15:37 +0000 (16:15 +0200)
man/repart.d.xml
meson.build
src/partition/repart.c
test/TEST-58-REPART/test.sh
test/units/testsuite-58.sh

index 280ce6b8eaf1c5eb2903cd69f7ffef3f735cca15..eec48fb5690eb59b343bfa016ced50cc732d2afd 100644 (file)
       <varlistentry>
         <term><varname>Verity=</varname></term>
 
-        <listitem><para>Takes one of <literal>off</literal>, <literal>data</literal> or
-        <literal>hash</literal>. Defaults to <literal>off</literal>. If set to <literal>off</literal> or
-        <literal>data</literal>, the partition is populated with content as specified by
-        <varname>CopyBlocks=</varname> or <varname>CopyFiles=</varname>. If set to <literal>hash</literal>,
-        the partition will be populated with verity hashes from a matching verity data partition. A matching
-        data partition is a partition with <varname>Verity=</varname> set to <literal>data</literal> and the
-        same verity match key (as configured with <varname>VerityMatchKey=</varname>). If not explicitly
-        configured, the data partition's UUID will be set to the first 128 bits of the verity root hash.
-        Similarly, if not configured, the hash partition's UUID will be set to the final 128 bits of the
-        verity root hash. The verity root hash itself will be included in the output of
-        <command>systemd-repart</command>.</para>
+        <listitem><para>Takes one of <literal>off</literal>, <literal>data</literal>,
+        <literal>hash</literal> or <literal>signature</literal>. Defaults to <literal>off</literal>. If set
+        to <literal>off</literal> or <literal>data</literal>, the partition is populated with content as
+        specified by <varname>CopyBlocks=</varname> or <varname>CopyFiles=</varname>. If set to
+        <literal>hash</literal>, the partition will be populated with verity hashes from the matching verity
+        data partition. If set to <literal>signature</literal>, The partition will be populated with a JSON
+        object containing a signature of the verity root hash of the matching verity hash partition.</para>
+
+        <para>A matching verity partition is a partition with the same verity match key (as configured with
+        <varname>VerityMatchKey=</varname>).</para>
+
+        <para>If not explicitly configured, the data partition's UUID will be set to the first 128
+        bits of the verity root hash. Similarly, if not configured, the hash partition's UUID will be set to
+        the final 128 bits of the verity root hash. The verity root hash itself will be included in the
+        output of <command>systemd-repart</command>.</para>
 
         <para>This option has no effect if the partition already exists.</para>
 
index 6022617832012a39a4d9ed2cffd1f5aa5f9577b5..a20e94c55a53417c4fa26ab2f32c77baa782cbc4 100644 (file)
@@ -3682,7 +3682,8 @@ if conf.get('ENABLE_REPART') == 1
                 link_with : [libshared],
                 dependencies : [threads,
                                 libblkid,
-                                libfdisk],
+                                libfdisk,
+                                libopenssl],
                 install_rpath : rootpkglibdir,
                 install : true,
                 install_dir : rootbindir)
index 1dbe045bd23b8cb5a95e50132ef76bc2f2a5eb10..b03928d823b2b4e3564ab651611492dea347f0a1 100644 (file)
@@ -40,6 +40,7 @@
 #include "hexdecoct.h"
 #include "hmac.h"
 #include "id128-util.h"
+#include "io-util.h"
 #include "json.h"
 #include "list.h"
 #include "loop-util.h"
@@ -48,6 +49,7 @@
 #include "mkfs-util.h"
 #include "mount-util.h"
 #include "mountpoint-util.h"
+#include "openssl-util.h"
 #include "parse-argument.h"
 #include "parse-helpers.h"
 #include "pretty-print.h"
@@ -76,6 +78,9 @@
 /* Hard lower limit for new partition sizes */
 #define HARD_MIN_SIZE 4096
 
+/* We know up front we're never going to put more than this in a verity sig partition. */
+#define VERITY_SIG_SIZE (HARD_MIN_SIZE * 4)
+
 /* libfdisk takes off slightly more than 1M of the disk size when creating a GPT disk label */
 #define GPT_METADATA_SIZE (1044*1024)
 
@@ -113,6 +118,8 @@ static PagerFlags arg_pager_flags = 0;
 static bool arg_legend = true;
 static void *arg_key = NULL;
 static size_t arg_key_size = 0;
+static EVP_PKEY *arg_private_key = NULL;
+static X509 *arg_certificate = NULL;
 static char *arg_tpm2_device = NULL;
 static uint32_t arg_tpm2_pcr_mask = UINT32_MAX;
 static char *arg_tpm2_public_key = NULL;
@@ -123,6 +130,8 @@ STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_definitions, strv_freep);
 STATIC_DESTRUCTOR_REGISTER(arg_key, erase_and_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_private_key, EVP_PKEY_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_certificate, X509_freep);
 STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_tpm2_public_key, freep);
 
@@ -143,6 +152,7 @@ typedef enum VerityMode {
         VERITY_OFF,
         VERITY_DATA,
         VERITY_HASH,
+        VERITY_SIG,
         _VERITY_MODE_MAX,
         _VERITY_MODE_INVALID = -EINVAL,
 } VerityMode;
@@ -240,6 +250,7 @@ static const char *verity_mode_table[_VERITY_MODE_MAX] = {
         [VERITY_OFF]  = "off",
         [VERITY_DATA] = "data",
         [VERITY_HASH] = "hash",
+        [VERITY_SIG]  = "signature",
 };
 
 #if HAVE_LIBCRYPTSETUP
@@ -515,6 +526,9 @@ static uint64_t partition_min_size(const Context *context, const Partition *p) {
                 return p->current_size;
         }
 
+        if (p->verity == VERITY_SIG)
+                return VERITY_SIG_SIZE;
+
         sz = p->current_size != UINT64_MAX ? p->current_size : HARD_MIN_SIZE;
 
         if (!PARTITION_EXISTS(p)) {
@@ -556,6 +570,9 @@ static uint64_t partition_max_size(const Context *context, const Partition *p) {
                 return p->current_size;
         }
 
+        if (p->verity == VERITY_SIG)
+                return VERITY_SIG_SIZE;
+
         if (p->size_max == UINT64_MAX)
                 return UINT64_MAX;
 
@@ -1548,7 +1565,8 @@ static int partition_read_definition(Partition *p, const char *path, const char
                                   "VerityMatchKey= can only be set if Verity= is not \"%s\"",
                                   verity_mode_to_string(p->verity));
 
-        if (p->verity == VERITY_HASH && (p->copy_files || p->copy_blocks_path || p->copy_blocks_auto || p->format || p->make_directories))
+        if (IN_SET(p->verity, VERITY_HASH, VERITY_SIG) &&
+                (p->copy_files || p->copy_blocks_path || p->copy_blocks_auto || p->format || p->make_directories))
                 return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
                                   "CopyBlocks=/CopyFiles=/Format=/MakeDirectories= cannot be used with Verity=%s",
                                   verity_mode_to_string(p->verity));
@@ -1557,6 +1575,19 @@ static int partition_read_definition(Partition *p, const char *path, const char
                 return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
                                   "Encrypting verity hash/data partitions is not supported");
 
+        if (p->verity == VERITY_SIG && !arg_private_key)
+                return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+                                  "Verity signature partition requested but no private key provided (--private-key=)");
+
+        if (p->verity == VERITY_SIG && !arg_certificate)
+                return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+                                  "Verity signature partition requested but no PEM certificate provided (--certificate-file=)");
+
+        if (p->verity == VERITY_SIG && (p->size_min != UINT64_MAX || p->size_max != UINT64_MAX))
+                return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+                                  "SizeMinBytes=/SizeMaxBytes= cannot be used with Verity=%s",
+                                  verity_mode_to_string(p->verity));
+
         /* Verity partitions are read only, let's imply the RO flag hence, unless explicitly configured otherwise. */
         if ((gpt_partition_type_is_root_verity(p->type_uuid) ||
              gpt_partition_type_is_usr_verity(p->type_uuid)) &&
@@ -1665,7 +1696,7 @@ static int context_read_definitions(
                         continue;
 
                 for (VerityMode mode = VERITY_OFF + 1; mode < _VERITY_MODE_MAX; mode++) {
-                        Partition *q;
+                        Partition *q = NULL;
 
                         if (p->verity == mode)
                                 continue;
@@ -1674,7 +1705,7 @@ static int context_read_definitions(
                                 continue;
 
                         r = find_verity_sibling(context, p, mode, &q);
-                        if (r == -ENXIO)
+                        if (mode != VERITY_SIG && r == -ENXIO)
                                 return log_syntax(NULL, LOG_ERR, p->definition_path, 1, SYNTHETIC_ERRNO(EINVAL),
                                                   "Missing verity %s partition for verity %s partition with VerityMatchKey=%s",
                                                   verity_mode_to_string(mode), verity_mode_to_string(p->verity), p->verity_match_key);
@@ -1685,12 +1716,14 @@ static int context_read_definitions(
                         if (r < 0)
                                 return r;
 
-                        if (q->priority != p->priority)
-                                return log_syntax(NULL, LOG_ERR, p->definition_path, 1, SYNTHETIC_ERRNO(EINVAL),
-                                                  "Priority mismatch (%i != %i) for verity sibling partitions with VerityMatchKey=%s",
-                                                  p->priority, q->priority, p->verity_match_key);
+                        if (q) {
+                                if (q->priority != p->priority)
+                                        return log_syntax(NULL, LOG_ERR, p->definition_path, 1, SYNTHETIC_ERRNO(EINVAL),
+                                                        "Priority mismatch (%i != %i) for verity sibling partitions with VerityMatchKey=%s",
+                                                        p->priority, q->priority, p->verity_match_key);
 
-                        p->siblings[mode] = q;
+                                p->siblings[mode] = q;
+                        }
                 }
         }
 
@@ -3642,6 +3675,181 @@ static int context_verity_hash(Context *context) {
         return 0;
 }
 
+static int parse_x509_certificate(const char *certificate, size_t certificate_size, X509 **ret) {
+#if HAVE_OPENSSL
+        _cleanup_(X509_freep) X509 *cert = NULL;
+        _cleanup_(BIO_freep) BIO *cb = NULL;
+
+        assert(certificate);
+        assert(certificate_size > 0);
+        assert(ret);
+
+        cb = BIO_new_mem_buf(certificate, certificate_size);
+        if (!cb)
+                return log_oom();
+
+        cert = PEM_read_bio_X509(cb, NULL, NULL, NULL);
+        if (!cert)
+                return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Failed to parse X.509 certificate: %s",
+                                       ERR_error_string(ERR_get_error(), NULL));
+
+        if (ret)
+                *ret = TAKE_PTR(cert);
+
+        return 0;
+#else
+        return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "openssl is not supported, cannot parse X509 certificate.");
+#endif
+}
+
+static int parse_private_key(const char *key, size_t key_size, EVP_PKEY **ret) {
+#if HAVE_OPENSSL
+        _cleanup_(BIO_freep) BIO *kb = NULL;
+        _cleanup_(EVP_PKEY_freep) EVP_PKEY *pk = NULL;
+
+        assert(key);
+        assert(key_size > 0);
+        assert(ret);
+
+        kb = BIO_new_mem_buf(key, key_size);
+        if (!kb)
+                return log_oom();
+
+        pk = PEM_read_bio_PrivateKey(kb, NULL, NULL, NULL);
+        if (!pk)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse PEM private key: %s",
+                                       ERR_error_string(ERR_get_error(), NULL));
+
+        if (ret)
+                *ret = TAKE_PTR(pk);
+
+        return 0;
+#else
+        return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "openssl is not supported, cannot parse private key.");
+#endif
+}
+
+static int sign_verity_roothash(
+                const uint8_t *roothash,
+                size_t roothash_size,
+                uint8_t **ret_signature,
+                size_t *ret_signature_size) {
+
+#if HAVE_OPENSSL
+        _cleanup_(BIO_freep) BIO *rb = NULL;
+        _cleanup_(PKCS7_freep) PKCS7 *p7 = NULL;
+        _cleanup_free_ char *hex = NULL;
+        _cleanup_free_ uint8_t *sig = NULL;
+        int sigsz;
+
+        assert(roothash);
+        assert(roothash_size > 0);
+        assert(ret_signature);
+        assert(ret_signature_size);
+
+        hex = hexmem(roothash, roothash_size);
+        if (!hex)
+                return log_oom();
+
+        rb = BIO_new_mem_buf(hex, -1);
+        if (!rb)
+                return log_oom();
+
+        p7 = PKCS7_sign(arg_certificate, arg_private_key, NULL, rb, PKCS7_DETACHED|PKCS7_NOATTR|PKCS7_BINARY);
+        if (!p7)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to calculate PKCS7 signature: %s",
+                                       ERR_error_string(ERR_get_error(), NULL));
+
+        sigsz = i2d_PKCS7(p7, &sig);
+        if (sigsz < 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to convert PKCS7 signature to DER: %s",
+                                       ERR_error_string(ERR_get_error(), NULL));
+
+        *ret_signature = TAKE_PTR(sig);
+        *ret_signature_size = sigsz;
+
+        return 0;
+#else
+        return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "openssl is not supported, cannot setup verity signature: %m");
+#endif
+}
+
+static int context_verity_sig(Context *context) {
+        int fd = -1, r;
+
+        assert(context);
+
+        LIST_FOREACH(partitions, p, context->partitions) {
+                _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+                _cleanup_free_ uint8_t *sig = NULL;
+                _cleanup_free_ char *text = NULL;
+                Partition *hp;
+                uint8_t fp[X509_FINGERPRINT_SIZE];
+                size_t sigsz, padsz;
+
+                if (p->dropped)
+                        continue;
+
+                if (PARTITION_EXISTS(p))
+                        continue;
+
+                if (p->verity != VERITY_SIG)
+                        continue;
+
+                assert_se(hp = p->siblings[VERITY_HASH]);
+                assert(!hp->dropped);
+
+                assert(arg_certificate);
+
+                if (fd < 0)
+                        assert_se((fd = fdisk_get_devfd(context->fdisk_context)) >= 0);
+
+                r = sign_verity_roothash(hp->roothash, hp->roothash_size, &sig, &sigsz);
+                if (r < 0)
+                        return r;
+
+                r = x509_fingerprint(arg_certificate, fp);
+                if (r < 0)
+                        return log_error_errno(r, "Unable to calculate X509 certificate fingerprint: %m");
+
+                r = json_build(&v,
+                               JSON_BUILD_OBJECT(
+                                        JSON_BUILD_PAIR("rootHash", JSON_BUILD_HEX(hp->roothash, hp->roothash_size)),
+                                        JSON_BUILD_PAIR(
+                                                "certificateFingerprint",
+                                                JSON_BUILD_HEX(fp, sizeof(fp))
+                                        ),
+                                        JSON_BUILD_PAIR("signature", JSON_BUILD_BASE64(sig, sigsz))
+                               )
+                );
+                if (r < 0)
+                        return log_error_errno(r, "Failed to build JSON object: %m");
+
+                r = json_variant_format(v, 0, &text);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to format JSON object: %m");
+
+                padsz = round_up_size(strlen(text), 4096);
+                assert_se(padsz <= p->new_size);
+
+                r = strgrowpad0(&text, padsz);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to pad string to %s", FORMAT_BYTES(padsz));
+
+                if (lseek(fd, p->offset, SEEK_SET) == (off_t) -1)
+                        return log_error_errno(errno, "Failed to seek to partition offset: %m");
+
+                r = loop_write(fd, text, padsz, /*do_poll=*/ false);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to write verity signature to partition: %m");
+
+                if (fsync(fd) < 0)
+                        return log_error_errno(errno, "Failed to synchronize verity signature JSON: %m");
+        }
+
+        return 0;
+}
+
 static int partition_acquire_uuid(Context *context, Partition *p, sd_id128_t *ret) {
         struct {
                 sd_id128_t type_uuid;
@@ -3787,7 +3995,7 @@ static int context_acquire_partition_uuids_and_labels(Context *context) {
 
                 if (!sd_id128_is_null(p->current_uuid))
                         p->new_uuid = p->current_uuid; /* Never change initialized UUIDs */
-                else if (!p->new_uuid_is_set && p->verity == VERITY_OFF) {
+                else if (!p->new_uuid_is_set && !IN_SET(p->verity, VERITY_DATA, VERITY_HASH)) {
                         /* Not explicitly set by user! */
                         r = partition_acquire_uuid(context, p, &p->new_uuid);
                         if (r < 0)
@@ -4205,6 +4413,10 @@ static int context_write_partition_table(
         if (r < 0)
                 return r;
 
+        r = context_verity_sig(context);
+        if (r < 0)
+                return r;
+
         r = context_mangle_partitions(context);
         if (r < 0)
                 return r;
@@ -4747,6 +4959,10 @@ static int help(void) {
                "     --image=PATH         Operate relative to image file\n"
                "     --definitions=DIR    Find partition definitions in specified directory\n"
                "     --key-file=PATH      Key to use when encrypting partitions\n"
+               "     --private-key=PATH   Private key to use when generating verity roothash\n"
+               "                          signatures\n"
+               "     --certificate=PATH   PEM certificate to use when generating verity\n"
+               "                          roothash signatures\n"
                "     --tpm2-device=PATH   Path to TPM2 device node to use\n"
                "     --tpm2-pcrs=PCR1+PCR2+PCR3+…\n"
                "                          TPM2 PCR indexes to use for TPM2 enrollment\n"
@@ -4787,6 +5003,8 @@ static int parse_argv(int argc, char *argv[]) {
                 ARG_SIZE,
                 ARG_JSON,
                 ARG_KEY_FILE,
+                ARG_PRIVATE_KEY,
+                ARG_CERTIFICATE,
                 ARG_TPM2_DEVICE,
                 ARG_TPM2_PCRS,
                 ARG_TPM2_PUBLIC_KEY,
@@ -4812,6 +5030,8 @@ static int parse_argv(int argc, char *argv[]) {
                 { "size",                 required_argument, NULL, ARG_SIZE                 },
                 { "json",                 required_argument, NULL, ARG_JSON                 },
                 { "key-file",             required_argument, NULL, ARG_KEY_FILE             },
+                { "private-key",          required_argument, NULL, ARG_PRIVATE_KEY          },
+                { "certificate",          required_argument, NULL, ARG_CERTIFICATE          },
                 { "tpm2-device",          required_argument, NULL, ARG_TPM2_DEVICE          },
                 { "tpm2-pcrs",            required_argument, NULL, ARG_TPM2_PCRS            },
                 { "tpm2-public-key",      required_argument, NULL, ARG_TPM2_PUBLIC_KEY      },
@@ -4985,6 +5205,46 @@ static int parse_argv(int argc, char *argv[]) {
                         break;
                 }
 
+                case ARG_PRIVATE_KEY: {
+                        _cleanup_(erase_and_freep) char *k = NULL;
+                        size_t n = 0;
+
+                        r = read_full_file_full(
+                                        AT_FDCWD, optarg, UINT64_MAX, SIZE_MAX,
+                                        READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET,
+                                        NULL,
+                                        &k, &n);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to read key file '%s': %m", optarg);
+
+                        EVP_PKEY_free(arg_private_key);
+                        arg_private_key = NULL;
+                        r = parse_private_key(k, n, &arg_private_key);
+                        if (r < 0)
+                                return r;
+                        break;
+                }
+
+                case ARG_CERTIFICATE: {
+                        _cleanup_free_ char *cert = NULL;
+                        size_t n = 0;
+
+                        r = read_full_file_full(
+                                        AT_FDCWD, optarg, UINT64_MAX, SIZE_MAX,
+                                        READ_FULL_FILE_CONNECT_SOCKET,
+                                        NULL,
+                                        &cert, &n);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to read certificate file '%s': %m", optarg);
+
+                        X509_free(arg_certificate);
+                        arg_certificate = NULL;
+                        r = parse_x509_certificate(cert, n, &arg_certificate);
+                        if (r < 0)
+                                return r;
+                        break;
+                }
+
                 case ARG_TPM2_DEVICE: {
                         _cleanup_free_ char *device = NULL;
 
index c2920f9c22af8fcefdc8fd8b384b4f933c984ab0..31c5e67e6a048a45d039e63690b01b46469c47f7 100755 (executable)
@@ -10,6 +10,9 @@ TEST_DESCRIPTION="test systemd-repart"
 test_append_files() {
     if ! get_bool "${TEST_NO_QEMU:=}"; then
         install_dmevent
+        if command -v openssl >/dev/null 2>&1; then
+            inst_binary openssl
+        fi
         instmods dm_verity =md
         generate_module_dependencies
     fi
index 313580f862bfdedb77d96f4487e7de92ff1bbe8c..f41069ee04f5b552900d5a18f02e727ce526dde7 100755 (executable)
@@ -726,17 +726,47 @@ Verity=hash
 VerityMatchKey=root
 EOF
 
+    cat >"$defs/verity-sig.conf" <<EOF
+[Partition]
+Type=root-${architecture}-verity-sig
+Verity=signature
+VerityMatchKey=root
+EOF
+
+    # Unfortunately OpenSSL insists on reading some config file, hence provide one with mostly placeholder contents
+    cat >> "$defs/verity.openssl.cnf" <<EOF
+[ req ]
+prompt = no
+distinguished_name = req_distinguished_name
+
+[ req_distinguished_name ]
+C = DE
+ST = Test State
+L = Test Locality
+O = Org Name
+OU = Org Unit Name
+CN = Common Name
+emailAddress = test@email.com
+EOF
+
+    openssl req -config "$defs/verity.openssl.cnf" -new -x509 -newkey rsa:1024 -keyout "$defs/verity.key" -out "$defs/verity.crt" -days 365 -nodes
+
+    mkdir -p /run/verity.d
+    ln -s "$defs/verity.crt" /run/verity.d/ok.crt
+
     output=$(systemd-repart --definitions="$defs" \
                             --seed="$seed" \
                             --dry-run=no \
                             --empty=create \
                             --size=auto \
                             --json=pretty \
+                            --private-key="$defs/verity.key" \
+                            --certificate="$defs/verity.crt" \
                             "$imgs/verity")
 
     roothash=$(jq -r ".[] | select(.type == \"root-${architecture}-verity\") | .roothash" <<< "$output")
 
-     # Check that we can dissect, mount and unmount a repart verity image.
+    # Check that we can dissect, mount and unmount a repart verity image.
 
     systemd-dissect "$imgs/verity" --root-hash "$roothash"
     systemd-dissect "$imgs/verity" --root-hash "$roothash" -M "$imgs/mnt"