dm: add support to directly boot to a mapped device
authorHelen Koike <helen.koike@collabora.com>
Thu, 21 Feb 2019 20:33:34 +0000 (17:33 -0300)
committerMike Snitzer <snitzer@redhat.com>
Tue, 5 Mar 2019 19:53:50 +0000 (14:53 -0500)
Add a "create" module parameter, which allows device-mapper targets to
be configured at boot time. This enables early use of DM targets in the
boot process (as the root device or otherwise) without the need of an
initramfs.

The syntax used in the boot param is based on the concise format from
the dmsetup tool to follow the rule of least surprise:

dmsetup table --concise /dev/mapper/lroot

Which is:
dm-mod.create=<name>,<uuid>,<minor>,<flags>,<table>[,<table>+][;<name>,<uuid>,<minor>,<flags>,<table>[,<table>+]+]

Where,
<name> ::= The device name.
<uuid> ::= xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | ""
<minor> ::= The device minor number | ""
<flags> ::= "ro" | "rw"
<table> ::= <start_sector> <num_sectors> <target_type> <target_args>
<target_type> ::= "verity" | "linear" | ...

For example, the following could be added in the boot parameters:
dm-mod.create="lroot,,,rw, 0 4096 linear 98:16 0, 4096 4096 linear 98:32 0" root=/dev/dm-0

Only the targets that were tested are allowed and the ones that don't
change any block device when the device is create as read-only. For
example, mirror and cache targets are not allowed. The rationale behind
this is that if the user makes a mistake, choosing the wrong device to
be the mirror or the cache can corrupt data.

The only targets initially allowed are:
* crypt
* delay
* linear
* snapshot-origin
* striped
* verity

Co-developed-by: Will Drewry <wad@chromium.org>
Co-developed-by: Kees Cook <keescook@chromium.org>
Co-developed-by: Enric Balletbo i Serra <enric.balletbo@collabora.com>
Signed-off-by: Helen Koike <helen.koike@collabora.com>
Reviewed-by: Kees Cook <keescook@chromium.org>
Signed-off-by: Mike Snitzer <snitzer@redhat.com>
Documentation/device-mapper/dm-init.txt [new file with mode: 0644]
drivers/md/Kconfig
drivers/md/Makefile
drivers/md/dm-init.c [new file with mode: 0644]
drivers/md/dm-ioctl.c
include/linux/device-mapper.h

diff --git a/Documentation/device-mapper/dm-init.txt b/Documentation/device-mapper/dm-init.txt
new file mode 100644 (file)
index 0000000..8464ee7
--- /dev/null
@@ -0,0 +1,114 @@
+Early creation of mapped devices
+====================================
+
+It is possible to configure a device-mapper device to act as the root device for
+your system in two ways.
+
+The first is to build an initial ramdisk which boots to a minimal userspace
+which configures the device, then pivot_root(8) in to it.
+
+The second is to create one or more device-mappers using the module parameter
+"dm-mod.create=" through the kernel boot command line argument.
+
+The format is specified as a string of data separated by commas and optionally
+semi-colons, where:
+ - a comma is used to separate fields like name, uuid, flags and table
+   (specifies one device)
+ - a semi-colon is used to separate devices.
+
+So the format will look like this:
+
+ dm-mod.create=<name>,<uuid>,<minor>,<flags>,<table>[,<table>+][;<name>,<uuid>,<minor>,<flags>,<table>[,<table>+]+]
+
+Where,
+       <name>          ::= The device name.
+       <uuid>          ::= xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | ""
+       <minor>         ::= The device minor number | ""
+       <flags>         ::= "ro" | "rw"
+       <table>         ::= <start_sector> <num_sectors> <target_type> <target_args>
+       <target_type>   ::= "verity" | "linear" | ... (see list below)
+
+The dm line should be equivalent to the one used by the dmsetup tool with the
+--concise argument.
+
+Target types
+============
+
+Not all target types are available as there are serious risks in allowing
+activation of certain DM targets without first using userspace tools to check
+the validity of associated metadata.
+
+       "cache":                constrained, userspace should verify cache device
+       "crypt":                allowed
+       "delay":                allowed
+       "era":                  constrained, userspace should verify metadata device
+       "flakey":               constrained, meant for test
+       "linear":               allowed
+       "log-writes":           constrained, userspace should verify metadata device
+       "mirror":               constrained, userspace should verify main/mirror device
+       "raid":                 constrained, userspace should verify metadata device
+       "snapshot":             constrained, userspace should verify src/dst device
+       "snapshot-origin":      allowed
+       "snapshot-merge":       constrained, userspace should verify src/dst device
+       "striped":              allowed
+       "switch":               constrained, userspace should verify dev path
+       "thin":                 constrained, requires dm target message from userspace
+       "thin-pool":            constrained, requires dm target message from userspace
+       "verity":               allowed
+       "writecache":           constrained, userspace should verify cache device
+       "zero":                 constrained, not meant for rootfs
+
+If the target is not listed above, it is constrained by default (not tested).
+
+Examples
+========
+An example of booting to a linear array made up of user-mode linux block
+devices:
+
+  dm-mod.create="lroot,,,rw, 0 4096 linear 98:16 0, 4096 4096 linear 98:32 0" root=/dev/dm-0
+
+This will boot to a rw dm-linear target of 8192 sectors split across two block
+devices identified by their major:minor numbers.  After boot, udev will rename
+this target to /dev/mapper/lroot (depending on the rules). No uuid was assigned.
+
+An example of multiple device-mappers, with the dm-mod.create="..." contents is shown here
+split on multiple lines for readability:
+
+  vroot,,,ro,
+    0 1740800 verity 254:0 254:0 1740800 sha1
+      76e9be054b15884a9fa85973e9cb274c93afadb6
+      5b3549d54d6c7a3837b9b81ed72e49463a64c03680c47835bef94d768e5646fe;
+  vram,,,rw,
+    0 32768 linear 1:0 0,
+    32768 32768 linear 1:1 0
+
+Other examples (per target):
+
+"crypt":
+  dm-crypt,,8,ro,
+    0 1048576 crypt aes-xts-plain64
+    babebabebabebabebabebabebabebabebabebabebabebabebabebabebabebabe 0
+    /dev/sda 0 1 allow_discards
+
+"delay":
+  dm-delay,,4,ro,0 409600 delay /dev/sda1 0 500
+
+"linear":
+  dm-linear,,,rw,
+    0 32768 linear /dev/sda1 0,
+    32768 1024000 linear /dev/sda2 0,
+    1056768 204800 linear /dev/sda3 0,
+    1261568 512000 linear /dev/sda4 0
+
+"snapshot-origin":
+  dm-snap-orig,,4,ro,0 409600 snapshot-origin 8:2
+
+"striped":
+  dm-striped,,4,ro,0 1638400 striped 4 4096
+  /dev/sda1 0 /dev/sda2 0 /dev/sda3 0 /dev/sda4 0
+
+"verity":
+  dm-verity,,4,ro,
+    0 1638400 verity 1 8:1 8:2 4096 4096 204800 1 sha256
+    fb1a5a0f00deb908d8b53cb270858975e76cf64105d412ce764225d53b8f3cfd
+    51934789604d1b92399c52e7cb149d1b3a1b74bbbcb103b2a0aaacbed5c08584
index 3db222509e44b456acb46fd2defc1a7978299b8e..2557f198e1750002a1a2d7dacd3fefbb926a7fd7 100644 (file)
@@ -436,6 +436,18 @@ config DM_DELAY
 
        If unsure, say N.
 
+config DM_INIT
+       bool "DM \"dm-mod.create=\" parameter support"
+       depends on BLK_DEV_DM=y
+       ---help---
+       Enable "dm-mod.create=" parameter to create mapped devices at init time.
+       This option is useful to allow mounting rootfs without requiring an
+       initramfs.
+       See Documentation/device-mapper/dm-init.txt for dm-mod.create="..."
+       format.
+
+       If unsure, say N.
+
 config DM_UEVENT
        bool "DM uevents"
        depends on BLK_DEV_DM
index 822f4e8753bc4b197b93a90df936bd0eeeff464a..a52b703e588e23dfa3a240f8882acc2d1eb29212 100644 (file)
@@ -69,6 +69,10 @@ obj-$(CONFIG_DM_INTEGRITY)   += dm-integrity.o
 obj-$(CONFIG_DM_ZONED)         += dm-zoned.o
 obj-$(CONFIG_DM_WRITECACHE)    += dm-writecache.o
 
+ifeq ($(CONFIG_DM_INIT),y)
+dm-mod-objs                    += dm-init.o
+endif
+
 ifeq ($(CONFIG_DM_UEVENT),y)
 dm-mod-objs                    += dm-uevent.o
 endif
diff --git a/drivers/md/dm-init.c b/drivers/md/dm-init.c
new file mode 100644 (file)
index 0000000..b53f30f
--- /dev/null
@@ -0,0 +1,303 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/*
+ * dm-init.c
+ * Copyright (C) 2017 The Chromium OS Authors <chromium-os-dev@chromium.org>
+ *
+ * This file is released under the GPLv2.
+ */
+
+#include <linux/ctype.h>
+#include <linux/device.h>
+#include <linux/device-mapper.h>
+#include <linux/init.h>
+#include <linux/list.h>
+#include <linux/moduleparam.h>
+
+#define DM_MSG_PREFIX "init"
+#define DM_MAX_DEVICES 256
+#define DM_MAX_TARGETS 256
+#define DM_MAX_STR_SIZE 4096
+
+static char *create;
+
+/*
+ * Format: dm-mod.create=<name>,<uuid>,<minor>,<flags>,<table>[,<table>+][;<name>,<uuid>,<minor>,<flags>,<table>[,<table>+]+]
+ * Table format: <start_sector> <num_sectors> <target_type> <target_args>
+ *
+ * See Documentation/device-mapper/dm-init.txt for dm-mod.create="..." format
+ * details.
+ */
+
+struct dm_device {
+       struct dm_ioctl dmi;
+       struct dm_target_spec *table[DM_MAX_TARGETS];
+       char *target_args_array[DM_MAX_TARGETS];
+       struct list_head list;
+};
+
+const char *dm_allowed_targets[] __initconst = {
+       "crypt",
+       "delay",
+       "linear",
+       "snapshot-origin",
+       "striped",
+       "verity",
+};
+
+static int __init dm_verify_target_type(const char *target)
+{
+       unsigned int i;
+
+       for (i = 0; i < ARRAY_SIZE(dm_allowed_targets); i++) {
+               if (!strcmp(dm_allowed_targets[i], target))
+                       return 0;
+       }
+       return -EINVAL;
+}
+
+static void __init dm_setup_cleanup(struct list_head *devices)
+{
+       struct dm_device *dev, *tmp;
+       unsigned int i;
+
+       list_for_each_entry_safe(dev, tmp, devices, list) {
+               list_del(&dev->list);
+               for (i = 0; i < dev->dmi.target_count; i++) {
+                       kfree(dev->table[i]);
+                       kfree(dev->target_args_array[i]);
+               }
+               kfree(dev);
+       }
+}
+
+/**
+ * str_field_delimit - delimit a string based on a separator char.
+ * @str: the pointer to the string to delimit.
+ * @separator: char that delimits the field
+ *
+ * Find a @separator and replace it by '\0'.
+ * Remove leading and trailing spaces.
+ * Return the remainder string after the @separator.
+ */
+static char __init *str_field_delimit(char **str, char separator)
+{
+       char *s;
+
+       /* TODO: add support for escaped characters */
+       *str = skip_spaces(*str);
+       s = strchr(*str, separator);
+       /* Delimit the field and remove trailing spaces */
+       if (s)
+               *s = '\0';
+       *str = strim(*str);
+       return s ? ++s : NULL;
+}
+
+/**
+ * dm_parse_table_entry - parse a table entry
+ * @dev: device to store the parsed information.
+ * @str: the pointer to a string with the format:
+ *     <start_sector> <num_sectors> <target_type> <target_args>[, ...]
+ *
+ * Return the remainder string after the table entry, i.e, after the comma which
+ * delimits the entry or NULL if reached the end of the string.
+ */
+static char __init *dm_parse_table_entry(struct dm_device *dev, char *str)
+{
+       const unsigned int n = dev->dmi.target_count - 1;
+       struct dm_target_spec *sp;
+       unsigned int i;
+       /* fields:  */
+       char *field[4];
+       char *next;
+
+       field[0] = str;
+       /* Delimit first 3 fields that are separated by space */
+       for (i = 0; i < ARRAY_SIZE(field) - 1; i++) {
+               field[i + 1] = str_field_delimit(&field[i], ' ');
+               if (!field[i + 1])
+                       return ERR_PTR(-EINVAL);
+       }
+       /* Delimit last field that can be terminated by comma */
+       next = str_field_delimit(&field[i], ',');
+
+       sp = kzalloc(sizeof(*sp), GFP_KERNEL);
+       if (!sp)
+               return ERR_PTR(-ENOMEM);
+       dev->table[n] = sp;
+
+       /* start_sector */
+       if (kstrtoull(field[0], 0, &sp->sector_start))
+               return ERR_PTR(-EINVAL);
+       /* num_sector */
+       if (kstrtoull(field[1], 0, &sp->length))
+               return ERR_PTR(-EINVAL);
+       /* target_type */
+       strscpy(sp->target_type, field[2], sizeof(sp->target_type));
+       if (dm_verify_target_type(sp->target_type)) {
+               DMERR("invalid type \"%s\"", sp->target_type);
+               return ERR_PTR(-EINVAL);
+       }
+       /* target_args */
+       dev->target_args_array[n] = kstrndup(field[3], GFP_KERNEL,
+                                            DM_MAX_STR_SIZE);
+       if (!dev->target_args_array[n])
+               return ERR_PTR(-ENOMEM);
+
+       return next;
+}
+
+/**
+ * dm_parse_table - parse "dm-mod.create=" table field
+ * @dev: device to store the parsed information.
+ * @str: the pointer to a string with the format:
+ *     <table>[,<table>+]
+ */
+static int __init dm_parse_table(struct dm_device *dev, char *str)
+{
+       char *table_entry = str;
+
+       while (table_entry) {
+               DMDEBUG("parsing table \"%s\"", str);
+               if (++dev->dmi.target_count >= DM_MAX_TARGETS) {
+                       DMERR("too many targets %u > %d",
+                             dev->dmi.target_count, DM_MAX_TARGETS);
+                       return -EINVAL;
+               }
+               table_entry = dm_parse_table_entry(dev, table_entry);
+               if (IS_ERR(table_entry)) {
+                       DMERR("couldn't parse table");
+                       return PTR_ERR(table_entry);
+               }
+       }
+
+       return 0;
+}
+
+/**
+ * dm_parse_device_entry - parse a device entry
+ * @dev: device to store the parsed information.
+ * @str: the pointer to a string with the format:
+ *     name,uuid,minor,flags,table[; ...]
+ *
+ * Return the remainder string after the table entry, i.e, after the semi-colon
+ * which delimits the entry or NULL if reached the end of the string.
+ */
+static char __init *dm_parse_device_entry(struct dm_device *dev, char *str)
+{
+       /* There are 5 fields: name,uuid,minor,flags,table; */
+       char *field[5];
+       unsigned int i;
+       char *next;
+
+       field[0] = str;
+       /* Delimit first 4 fields that are separated by comma */
+       for (i = 0; i < ARRAY_SIZE(field) - 1; i++) {
+               field[i+1] = str_field_delimit(&field[i], ',');
+               if (!field[i+1])
+                       return ERR_PTR(-EINVAL);
+       }
+       /* Delimit last field that can be delimited by semi-colon */
+       next = str_field_delimit(&field[i], ';');
+
+       /* name */
+       strscpy(dev->dmi.name, field[0], sizeof(dev->dmi.name));
+       /* uuid */
+       strscpy(dev->dmi.uuid, field[1], sizeof(dev->dmi.uuid));
+       /* minor */
+       if (strlen(field[2])) {
+               if (kstrtoull(field[2], 0, &dev->dmi.dev))
+                       return ERR_PTR(-EINVAL);
+               dev->dmi.flags |= DM_PERSISTENT_DEV_FLAG;
+       }
+       /* flags */
+       if (!strcmp(field[3], "ro"))
+               dev->dmi.flags |= DM_READONLY_FLAG;
+       else if (strcmp(field[3], "rw"))
+               return ERR_PTR(-EINVAL);
+       /* table */
+       if (dm_parse_table(dev, field[4]))
+               return ERR_PTR(-EINVAL);
+
+       return next;
+}
+
+/**
+ * dm_parse_devices - parse "dm-mod.create=" argument
+ * @devices: list of struct dm_device to store the parsed information.
+ * @str: the pointer to a string with the format:
+ *     <device>[;<device>+]
+ */
+static int __init dm_parse_devices(struct list_head *devices, char *str)
+{
+       unsigned long ndev = 0;
+       struct dm_device *dev;
+       char *device = str;
+
+       DMDEBUG("parsing \"%s\"", str);
+       while (device) {
+               dev = kzalloc(sizeof(*dev), GFP_KERNEL);
+               if (!dev)
+                       return -ENOMEM;
+               list_add_tail(&dev->list, devices);
+
+               if (++ndev >= DM_MAX_DEVICES) {
+                       DMERR("too many targets %u > %d",
+                             dev->dmi.target_count, DM_MAX_TARGETS);
+                       return -EINVAL;
+               }
+
+               device = dm_parse_device_entry(dev, device);
+               if (IS_ERR(device)) {
+                       DMERR("couldn't parse device");
+                       return PTR_ERR(device);
+               }
+       }
+
+       return 0;
+}
+
+/**
+ * dm_init_init - parse "dm-mod.create=" argument and configure drivers
+ */
+static int __init dm_init_init(void)
+{
+       struct dm_device *dev;
+       LIST_HEAD(devices);
+       char *str;
+       int r;
+
+       if (!create)
+               return 0;
+
+       if (strlen(create) >= DM_MAX_STR_SIZE) {
+               DMERR("Argument is too big. Limit is %d\n", DM_MAX_STR_SIZE);
+               return -EINVAL;
+       }
+       str = kstrndup(create, GFP_KERNEL, DM_MAX_STR_SIZE);
+       if (!str)
+               return -ENOMEM;
+
+       r = dm_parse_devices(&devices, str);
+       if (r)
+               goto out;
+
+       DMINFO("waiting for all devices to be available before creating mapped devices\n");
+       wait_for_device_probe();
+
+       list_for_each_entry(dev, &devices, list) {
+               if (dm_early_create(&dev->dmi, dev->table,
+                                   dev->target_args_array))
+                       break;
+       }
+out:
+       kfree(str);
+       dm_setup_cleanup(&devices);
+       return r;
+}
+
+late_initcall(dm_init_init);
+
+module_param(create, charp, 0);
+MODULE_PARM_DESC(create, "Create a mapped device in early boot");
index f666778ad23728cdc323f7570d837ebb30ede660..c740153b4e52df15ee7b4cab6c9b1e83d897143f 100644 (file)
@@ -2018,3 +2018,106 @@ out:
 
        return r;
 }
+
+
+/**
+ * dm_early_create - create a mapped device in early boot.
+ *
+ * @dmi: Contains main information of the device mapping to be created.
+ * @spec_array: array of pointers to struct dm_target_spec. Describes the
+ * mapping table of the device.
+ * @target_params_array: array of strings with the parameters to a specific
+ * target.
+ *
+ * Instead of having the struct dm_target_spec and the parameters for every
+ * target embedded at the end of struct dm_ioctl (as performed in a normal
+ * ioctl), pass them as arguments, so the caller doesn't need to serialize them.
+ * The size of the spec_array and target_params_array is given by
+ * @dmi->target_count.
+ * This function is supposed to be called in early boot, so locking mechanisms
+ * to protect against concurrent loads are not required.
+ */
+int __init dm_early_create(struct dm_ioctl *dmi,
+                          struct dm_target_spec **spec_array,
+                          char **target_params_array)
+{
+       int r, m = DM_ANY_MINOR;
+       struct dm_table *t, *old_map;
+       struct mapped_device *md;
+       unsigned int i;
+
+       if (!dmi->target_count)
+               return -EINVAL;
+
+       r = check_name(dmi->name);
+       if (r)
+               return r;
+
+       if (dmi->flags & DM_PERSISTENT_DEV_FLAG)
+               m = MINOR(huge_decode_dev(dmi->dev));
+
+       /* alloc dm device */
+       r = dm_create(m, &md);
+       if (r)
+               return r;
+
+       /* hash insert */
+       r = dm_hash_insert(dmi->name, *dmi->uuid ? dmi->uuid : NULL, md);
+       if (r)
+               goto err_destroy_dm;
+
+       /* alloc table */
+       r = dm_table_create(&t, get_mode(dmi), dmi->target_count, md);
+       if (r)
+               goto err_destroy_dm;
+
+       /* add targets */
+       for (i = 0; i < dmi->target_count; i++) {
+               r = dm_table_add_target(t, spec_array[i]->target_type,
+                                       (sector_t) spec_array[i]->sector_start,
+                                       (sector_t) spec_array[i]->length,
+                                       target_params_array[i]);
+               if (r) {
+                       DMWARN("error adding target to table");
+                       goto err_destroy_table;
+               }
+       }
+
+       /* finish table */
+       r = dm_table_complete(t);
+       if (r)
+               goto err_destroy_table;
+
+       md->type = dm_table_get_type(t);
+       /* setup md->queue to reflect md's type (may block) */
+       r = dm_setup_md_queue(md, t);
+       if (r) {
+               DMWARN("unable to set up device queue for new table.");
+               goto err_destroy_table;
+       }
+
+       /* Set new map */
+       dm_suspend(md, 0);
+       old_map = dm_swap_table(md, t);
+       if (IS_ERR(old_map)) {
+               r = PTR_ERR(old_map);
+               goto err_destroy_table;
+       }
+       set_disk_ro(dm_disk(md), !!(dmi->flags & DM_READONLY_FLAG));
+
+       /* resume device */
+       r = dm_resume(md);
+       if (r)
+               goto err_destroy_table;
+
+       DMINFO("%s (%s) is ready", md->disk->disk_name, dmi->name);
+       dm_put(md);
+       return 0;
+
+err_destroy_table:
+       dm_table_destroy(t);
+err_destroy_dm:
+       dm_put(md);
+       dm_destroy(md);
+       return r;
+}
index 52e8709c6df08fe3651d5b9eef14e8d30e930e60..b0672756d0562697238cb9cf53775c3c48d87a4a 100644 (file)
@@ -10,6 +10,7 @@
 
 #include <linux/bio.h>
 #include <linux/blkdev.h>
+#include <linux/dm-ioctl.h>
 #include <linux/math64.h>
 #include <linux/ratelimit.h>
 
@@ -425,6 +426,14 @@ void dm_remap_zone_report(struct dm_target *ti, sector_t start,
                          struct blk_zone *zones, unsigned int *nr_zones);
 union map_info *dm_get_rq_mapinfo(struct request *rq);
 
+/*
+ * Device mapper functions to parse and create devices specified by the
+ * parameter "dm-mod.create="
+ */
+int __init dm_early_create(struct dm_ioctl *dmi,
+                          struct dm_target_spec **spec_array,
+                          char **target_params_array);
+
 struct queue_limits *dm_get_queue_limits(struct mapped_device *md);
 
 /*