diff --git a/man/repart.d.xml b/man/repart.d.xml index 43540234a7..d404645588 100644 --- a/man/repart.d.xml +++ b/man/repart.d.xml @@ -422,12 +422,25 @@ CopyBlocks= - Takes a path to a regular file, block device node or directory. If specified and the - partition is newly created the data from the specified path is written to the newly created - partition, on the block level. If a directory is specified the backing block device of the file - system the directory is on is determined and the data read directly from that. This option is useful - to efficiently replicate existing file systems on the block level on a new partition, for example to - build a simple OS installer or OS image builder. + Takes a path to a regular file, block device node or directory, or the special value + auto. If specified and the partition is newly created, the data from the specified + path is written to the newly created partition, on the block level. If a directory is specified, the + backing block device of the file system the directory is on is determined, and the data read directly + from that. This option is useful to efficiently replicate existing file systems onto new partitions + on the block level — for example to build a simple OS installer or an OS image builder. + + If the special value auto is specified, the source to copy from is + automatically picked up from the running system (or the image specified with + — if used). A partition that matches both the configured partition type (as + declared with Type= above), and the currently mounted directory appropriate for + that partition type is determined. For example, if the partition type is set to + root the partition backing the root directory (/) is used as + source to copy from — if its partition type is set to root as well. If the + declared type is usr the partition backing /usr/ is used as + source to copy blocks from — if its partition type is set to usr too. The logic is + capable of automatically tracking down the the backing partitions for encrypted and Verity-enabled + volumes. CopyBlocks=auto is useful for implementing "self-replicating" systems, + i.e. systems that are their own installer. The file specified here must have a size that is a multiple of the basic block size 512 and not be empty. If this option is used, the size allocation algorithm is slightly altered: the partition is diff --git a/src/partition/repart.c b/src/partition/repart.c index b011eb8895..12ad0dd49e 100644 --- a/src/partition/repart.c +++ b/src/partition/repart.c @@ -26,6 +26,7 @@ #include "conf-parser.h" #include "cryptsetup-util.h" #include "def.h" +#include "dirent-util.h" #include "efivars.h" #include "errno-util.h" #include "fd-util.h" @@ -44,6 +45,7 @@ #include "mkdir.h" #include "mkfs-util.h" #include "mount-util.h" +#include "mountpoint-util.h" #include "parse-argument.h" #include "parse-util.h" #include "path-util.h" @@ -157,6 +159,7 @@ struct Partition { FreeArea *allocated_to_area; char *copy_blocks_path; + bool copy_blocks_auto; int copy_blocks_fd; uint64_t copy_blocks_size; @@ -1162,6 +1165,53 @@ static int config_parse_copy_files( return 0; } +static int config_parse_copy_blocks( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_free_ char *d = NULL; + Partition *partition = data; + int r; + + assert(rvalue); + assert(partition); + + if (isempty(rvalue)) { + partition->copy_blocks_path = mfree(partition->copy_blocks_path); + partition->copy_blocks_auto = false; + return 0; + } + + if (streq(rvalue, "auto")) { + partition->copy_blocks_path = mfree(partition->copy_blocks_path); + partition->copy_blocks_auto = true; + return 0; + } + + r = specifier_printf(rvalue, specifier_table, NULL, &d); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to expand specifiers in CopyBlocks= source path, ignoring: %s", rvalue); + return 0; + } + + r = path_simplify_and_warn(d, PATH_CHECK_ABSOLUTE, unit, filename, line, lvalue); + if (r < 0) + return 0; + + free_and_replace(partition->copy_blocks_path, d); + partition->copy_blocks_auto = false; + return 0; +} + static int config_parse_make_dirs( const char *unit, const char *filename, @@ -1216,22 +1266,22 @@ static DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT(config_parse_encrypt, encrypt_mode, static int partition_read_definition(Partition *p, const char *path) { ConfigTableItem table[] = { - { "Partition", "Type", config_parse_type, 0, &p->type_uuid }, - { "Partition", "Label", config_parse_label, 0, &p->new_label }, - { "Partition", "UUID", config_parse_id128, 0, &p->new_uuid }, - { "Partition", "Priority", config_parse_int32, 0, &p->priority }, - { "Partition", "Weight", config_parse_weight, 0, &p->weight }, - { "Partition", "PaddingWeight", config_parse_weight, 0, &p->padding_weight }, - { "Partition", "SizeMinBytes", config_parse_size4096, 1, &p->size_min }, - { "Partition", "SizeMaxBytes", config_parse_size4096, -1, &p->size_max }, - { "Partition", "PaddingMinBytes", config_parse_size4096, 1, &p->padding_min }, - { "Partition", "PaddingMaxBytes", config_parse_size4096, -1, &p->padding_max }, - { "Partition", "FactoryReset", config_parse_bool, 0, &p->factory_reset }, - { "Partition", "CopyBlocks", config_parse_path, 0, &p->copy_blocks_path }, - { "Partition", "Format", config_parse_fstype, 0, &p->format }, - { "Partition", "CopyFiles", config_parse_copy_files, 0, p }, - { "Partition", "MakeDirectories", config_parse_make_dirs, 0, p }, - { "Partition", "Encrypt", config_parse_encrypt, 0, &p->encrypt }, + { "Partition", "Type", config_parse_type, 0, &p->type_uuid }, + { "Partition", "Label", config_parse_label, 0, &p->new_label }, + { "Partition", "UUID", config_parse_id128, 0, &p->new_uuid }, + { "Partition", "Priority", config_parse_int32, 0, &p->priority }, + { "Partition", "Weight", config_parse_weight, 0, &p->weight }, + { "Partition", "PaddingWeight", config_parse_weight, 0, &p->padding_weight }, + { "Partition", "SizeMinBytes", config_parse_size4096, 1, &p->size_min }, + { "Partition", "SizeMaxBytes", config_parse_size4096, -1, &p->size_max }, + { "Partition", "PaddingMinBytes", config_parse_size4096, 1, &p->padding_min }, + { "Partition", "PaddingMaxBytes", config_parse_size4096, -1, &p->padding_max }, + { "Partition", "FactoryReset", config_parse_bool, 0, &p->factory_reset }, + { "Partition", "CopyBlocks", config_parse_copy_blocks, 0, p }, + { "Partition", "Format", config_parse_fstype, 0, &p->format }, + { "Partition", "CopyFiles", config_parse_copy_files, 0, p }, + { "Partition", "MakeDirectories", config_parse_make_dirs, 0, p }, + { "Partition", "Encrypt", config_parse_encrypt, 0, &p->encrypt }, {} }; int r; @@ -1257,15 +1307,16 @@ static int partition_read_definition(Partition *p, const char *path) { return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), "Type= not defined, refusing."); - if (p->copy_blocks_path && (p->format || !strv_isempty(p->copy_files) || !strv_isempty(p->make_directories))) + if ((p->copy_blocks_path || p->copy_blocks_auto) && + (p->format || !strv_isempty(p->copy_files) || !strv_isempty(p->make_directories))) return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), - "Format= and CopyBlocks= cannot be combined, refusing."); + "Format=/CopyFiles=/MakeDirectories= and CopyBlocks= cannot be combined, refusing."); if ((!strv_isempty(p->copy_files) || !strv_isempty(p->make_directories)) && streq_ptr(p->format, "swap")) return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), "Format=swap and CopyFiles= cannot be combined, refusing."); - if (!p->format && (!strv_isempty(p->copy_files) || !strv_isempty(p->make_directories) || (p->encrypt != ENCRYPT_OFF && !p->copy_blocks_path))) { + if (!p->format && (!strv_isempty(p->copy_files) || !strv_isempty(p->make_directories) || (p->encrypt != ENCRYPT_OFF && !(p->copy_blocks_path || p->copy_blocks_auto)))) { /* Pick "ext4" as file system if we are configured to copy files or encrypt the device */ p->format = strdup("ext4"); if (!p->format) @@ -3459,7 +3510,311 @@ static int context_can_factory_reset(Context *context) { return false; } -static int context_open_copy_block_paths(Context *context) { +static int resolve_copy_blocks_auto_candidate( + dev_t partition_devno, + sd_id128_t partition_type_uuid, + dev_t restrict_devno, + sd_id128_t *ret_uuid) { + + _cleanup_(blkid_free_probep) blkid_probe b = NULL; + _cleanup_free_ char *p = NULL; + _cleanup_close_ int fd = -1; + const char *pttype, *t; + sd_id128_t pt_parsed, u; + blkid_partition pp; + dev_t whole_devno; + blkid_partlist pl; + struct stat st; + int r; + + /* Checks if the specified partition has the specified GPT type UUID, and is located on the specified + * 'restrict_devno' device. The type check is particularly relevant if we have Verity volume which is + * backed by two separate partitions: the data and the hash partitions, and we need to find the right + * one of the two. */ + + r = block_get_whole_disk(partition_devno, &whole_devno); + if (r < 0) + return log_error_errno( + r, + "Unable to determine containing block device of partition %u:%u: %m", + major(partition_devno), minor(partition_devno)); + + if (restrict_devno != (dev_t) -1 && + restrict_devno != whole_devno) + return log_error_errno( + SYNTHETIC_ERRNO(EPERM), + "Partition %u:%u is located outside of block device %u:%u, refusing.", + major(partition_devno), minor(partition_devno), + major(restrict_devno), minor(restrict_devno)); + + r = device_path_make_major_minor(S_IFBLK, whole_devno, &p); + if (r < 0) + return log_error_errno(r, "Failed to convert block device to device node path: %m"); + + fd = open(p, O_RDONLY|O_CLOEXEC|O_NONBLOCK); + if (fd < 0) + return log_error_errno(r, "Failed to open '%s': %m", p); + + if (fstat(fd, &st) < 0) + return log_error_errno(r, "Failed to stat '%s': %m", p); + + if (!S_ISBLK(st.st_mode) || st.st_rdev != whole_devno) + return log_error_errno( + SYNTHETIC_ERRNO(EPERM), + "Opened and determined block device don't match, refusing."); + + b = blkid_new_probe(); + if (!b) + return log_oom(); + + errno = 0; + r = blkid_probe_set_device(b, fd, 0, 0); + if (r != 0) + return log_error_errno(errno_or_else(ENOMEM), "Failed to open block device '%s': %m", p); + + (void) blkid_probe_enable_partitions(b, 1); + (void) blkid_probe_set_partitions_flags(b, BLKID_PARTS_ENTRY_DETAILS); + + errno = 0; + r = blkid_do_safeprobe(b); + if (IN_SET(r, -2, 1)) { /* nothing found or ambiguous result */ + log_debug("Didn't find partition table on block device '%s'.", p); + return false; + } + if (r != 0) + return log_error_errno(errno_or_else(EIO), "Unable to probe for partition table of '%s': %m", p); + + (void) blkid_probe_lookup_value(b, "PTTYPE", &pttype, NULL); + if (!streq_ptr(pttype, "gpt")) { + log_debug("Didn't find a GPT partition table on '%s'.", p); + return false; + } + + errno = 0; + pl = blkid_probe_get_partitions(b); + if (!pl) + return log_error_errno(errno_or_else(EIO), "Unable read partition table of '%s': %m", p); + errno = 0; + + pp = blkid_partlist_devno_to_partition(pl, partition_devno); + if (!pp) { + log_debug("Partition %u:%u has no matching partition table entry on '%s'.", + major(partition_devno), minor(partition_devno), p); + return false; + } + + t = blkid_partition_get_type_string(pp); + if (isempty(t)) { + log_debug("Partition %u:%u has no type on '%s'.", + major(partition_devno), minor(partition_devno), p); + return false; + } + + r = sd_id128_from_string(t, &pt_parsed); + if (r < 0) { + log_debug_errno(r, "Failed to parse partition type \"%s\": %m", t); + return false; + } + + if (!sd_id128_equal(pt_parsed, partition_type_uuid)) { + log_debug("Partition %u:%u has non-matching partition type " SD_ID128_FORMAT_STR " (needed: " SD_ID128_FORMAT_STR "), ignoring.", + major(partition_devno), minor(partition_devno), + SD_ID128_FORMAT_VAL(pt_parsed), SD_ID128_FORMAT_VAL(partition_type_uuid)); + return false; + } + + t = blkid_partition_get_uuid(pp); + if (isempty(t)) { + log_debug("Partition %u:%u has no UUID.", + major(partition_devno), minor(partition_devno)); + return false; + } + + r = sd_id128_from_string(t, &u); + if (r < 0) { + log_debug_errno(r, "Failed to parse partition UUID \"%s\": %m", t); + return false; + } + + log_debug("Automatically found partition %u:%u of right type " SD_ID128_FORMAT_STR ".", + major(partition_devno), minor(partition_devno), + SD_ID128_FORMAT_VAL(pt_parsed)); + + if (ret_uuid) + *ret_uuid = u; + + return true; +} + +static int find_backing_devno( + const char *path, + const char *root, + dev_t *ret) { + + _cleanup_free_ char *resolved = NULL; + int r; + + assert(path); + + r = chase_symlinks(path, root, CHASE_PREFIX_ROOT, &resolved, NULL); + if (r < 0) + return r; + + r = path_is_mount_point(resolved, NULL, 0); + if (r < 0) + return r; + if (r == 0) /* Not a mount point, then it's not a partition of its own, let's not automatically use it. */ + return -ENOENT; + + r = get_block_device(resolved, ret); + if (r < 0) + return r; + if (r == 0) /* Not backed by physical file system, we can't use this */ + return -ENOENT; + + return 0; +} + +static int resolve_copy_blocks_auto( + sd_id128_t type_uuid, + const char *root, + dev_t restrict_devno, + char **ret_path, + sd_id128_t *ret_uuid) { + + const char *try1 = NULL, *try2 = NULL; + char p[SYS_BLOCK_PATH_MAX("/slaves")]; + _cleanup_(closedirp) DIR *d = NULL; + sd_id128_t found_uuid = SD_ID128_NULL; + dev_t devno, found = 0; + int r; + + assert(ret_path); + + /* Enforce some security restrictions: CopyBlocks=auto should not be an avenue to get outside of the + * --root=/--image= confinement. Specifically, refuse CopyBlocks= in combination with --root= at all, + * and restrict block device references in the --image= case to loopback block device we set up. + * + * restrict_devno contain the dev_t of the loop back device we operate on in case of --image=, and + * thus declares which device (and its partition subdevices) we shall limit access to. If + * restrict_devno is zero no device probing access shall be allowed at all (used for --root=) and if + * it is (dev_t) -1 then free access shall be allowed (if neither switch is used). */ + + if (restrict_devno == 0) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), + "Automatic discovery of backing block devices not permitted in --root= mode, refusing."); + + /* Handles CopyBlocks=auto, and finds the right source partition to copy from. We look for matching + * partitions in the host, using the appropriate directory as key and ensuring that the partition + * type matches. */ + + if (gpt_partition_type_is_root(type_uuid)) + try1 = "/"; + else if (gpt_partition_type_is_usr(type_uuid)) + try1 = "/usr/"; + else if (gpt_partition_type_is_root_verity(type_uuid)) + try1 = "/"; + else if (gpt_partition_type_is_usr_verity(type_uuid)) + try1 = "/usr/"; + else if (sd_id128_equal(type_uuid, GPT_ESP)) { + try1 = "/efi/"; + try2 = "/boot/"; + } else if (sd_id128_equal(type_uuid, GPT_XBOOTLDR)) + try1 = "/boot/"; + else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Partition type " SD_ID128_FORMAT_STR " not supported from automatic source block device discovery.", + SD_ID128_FORMAT_VAL(type_uuid)); + + r = find_backing_devno(try1, root, &devno); + if (r == -ENOENT && try2) + r = find_backing_devno(try2, root, &devno); + if (r < 0) + return log_error_errno(r, "Failed to resolve automatic CopyBlocks= path for partition type " SD_ID128_FORMAT_STR ", sorry: %m", + SD_ID128_FORMAT_VAL(type_uuid)); + + xsprintf_sys_block_path(p, "/slaves", devno); + d = opendir(p); + if (d) { + struct dirent *de; + + for (;;) { + _cleanup_free_ char *q = NULL, *t = NULL; + sd_id128_t u; + dev_t sl; + + errno = 0; + de = readdir_no_dot(d); + if (!de) { + if (errno != 0) + return log_error_errno(errno, "Failed to read directory '%s': %m", p); + + break; + } + + if (!IN_SET(de->d_type, DT_LNK, DT_UNKNOWN)) + continue; + + q = path_join(p, de->d_name, "/dev"); + if (!q) + return log_oom(); + + r = read_one_line_file(q, &t); + if (r < 0) + return log_error_errno(r, "Failed to read %s: %m", q); + + r = parse_dev(t, &sl); + if (r < 0) { + log_debug_errno(r, "Failed to parse %s, ignoring: %m", q); + continue; + } + if (major(sl) == 0) { + log_debug_errno(r, "Device backing %s is special, ignoring: %m", q); + continue; + } + + r = resolve_copy_blocks_auto_candidate(sl, type_uuid, restrict_devno, &u); + if (r < 0) + return r; + if (r > 0) { + /* We found a matching one! */ + if (found != 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), + "Multiple matching partitions found, refusing."); + + found = sl; + found_uuid = u; + } + } + } else if (errno != ENOENT) + return log_error_errno(errno, "Failed open %s: %m", p); + else { + r = resolve_copy_blocks_auto_candidate(devno, type_uuid, restrict_devno, &found_uuid); + if (r < 0) + return r; + if (r > 0) + found = devno; + } + + if (found == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENXIO), + "Unable to automatically discover suitable partition to copy blocks from."); + + r = device_path_make_major_minor(S_IFBLK, found, ret_path); + if (r < 0) + return log_error_errno(r, "Failed to convert dev_t to device node path: %m"); + + if (ret_uuid) + *ret_uuid = found_uuid; + + return 0; +} + +static int context_open_copy_block_paths( + Context *context, + const char *root, + dev_t restrict_devno) { + Partition *p; int r; @@ -3467,6 +3822,8 @@ static int context_open_copy_block_paths(Context *context) { LIST_FOREACH(partitions, p, context->partitions) { _cleanup_close_ int source_fd = -1; + _cleanup_free_ char *opened = NULL; + sd_id128_t uuid = SD_ID128_NULL; uint64_t size; struct stat st; @@ -3476,16 +3833,39 @@ static int context_open_copy_block_paths(Context *context) { if (PARTITION_EXISTS(p)) /* Never copy over partitions that already exist! */ continue; - if (!p->copy_blocks_path) + if (p->copy_blocks_path) { + + source_fd = chase_symlinks_and_open(p->copy_blocks_path, root, CHASE_PREFIX_ROOT, O_RDONLY|O_CLOEXEC|O_NONBLOCK, &opened); + if (source_fd < 0) + return log_error_errno(source_fd, "Failed to open '%s': %m", p->copy_blocks_path); + + if (fstat(source_fd, &st) < 0) + return log_error_errno(errno, "Failed to stat block copy file '%s': %m", opened); + + if (!S_ISREG(st.st_mode) && restrict_devno != (dev_t) -1) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), + "Copying from block device node is not permitted in --image=/--root= mode, refusing."); + + } else if (p->copy_blocks_auto) { + + r = resolve_copy_blocks_auto(p->type_uuid, root, restrict_devno, &opened, &uuid); + if (r < 0) + return r; + + source_fd = open(opened, O_RDONLY|O_CLOEXEC|O_NOCTTY); + if (source_fd < 0) + return log_error_errno(errno, "Failed to open automatically determined source block copy device '%s': %m", opened); + + if (fstat(source_fd, &st) < 0) + return log_error_errno(errno, "Failed to stat block copy file '%s': %m", opened); + + /* If we found it automatically, it must be a block device, let's enforce that */ + if (!S_ISBLK(st.st_mode)) + return log_error_errno(SYNTHETIC_ERRNO(EBADF), + "Automatically detected source block copy device '%s' is not a block device, refusing: %m", opened); + } else continue; - source_fd = open(p->copy_blocks_path, O_RDONLY|O_CLOEXEC|O_NOCTTY); - if (source_fd < 0) - return log_error_errno(errno, "Failed to open block copy file '%s': %m", p->copy_blocks_path); - - if (fstat(source_fd, &st) < 0) - return log_error_errno(errno, "Failed to stat block copy file '%s': %m", p->copy_blocks_path); - if (S_ISDIR(st.st_mode)) { _cleanup_free_ char *bdev = NULL; @@ -3500,14 +3880,14 @@ static int context_open_copy_block_paths(Context *context) { r = btrfs_get_block_device_fd(source_fd, &devt); if (r == -EUCLEAN) - return btrfs_log_dev_root(LOG_ERR, r, p->copy_blocks_path); + return btrfs_log_dev_root(LOG_ERR, r, opened); if (r < 0) - return log_error_errno(r, "Unable to determine backing block device of '%s': %m", p->copy_blocks_path); + return log_error_errno(r, "Unable to determine backing block device of '%s': %m", opened); r = device_path_make_major_minor(S_IFBLK, devt, &bdev); } if (r < 0) - return log_error_errno(r, "Failed to determine block device path for block device backing '%s': %m", p->copy_blocks_path); + return log_error_errno(r, "Failed to determine block device path for block device backing '%s': %m", opened); safe_close(source_fd); @@ -3528,15 +3908,21 @@ static int context_open_copy_block_paths(Context *context) { if (ioctl(source_fd, BLKGETSIZE64, &size) != 0) return log_error_errno(errno, "Failed to determine size of block device to copy from: %m"); } else - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified path to copy blocks from '%s' is not a regular file, block device or directory, refusing: %m", p->copy_blocks_path); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified path to copy blocks from '%s' is not a regular file, block device or directory, refusing: %m", opened); if (size <= 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "File to copy bytes from '%s' has zero size, refusing.", p->copy_blocks_path); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "File to copy bytes from '%s' has zero size, refusing.", opened); if (size % 512 != 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "File to copy bytes from '%s' has size that is not multiple of 512, refusing.", p->copy_blocks_path); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "File to copy bytes from '%s' has size that is not multiple of 512, refusing.", opened); p->copy_blocks_fd = TAKE_FD(source_fd); p->copy_blocks_size = size; + + free_and_replace(p->copy_blocks_path, opened); + + /* When copying from an existing partition copy that partitions UUID if none is configured explicitly */ + if (sd_id128_is_null(p->new_uuid) && !sd_id128_is_null(uuid)) + p->new_uuid = uuid; } return 0; @@ -3970,7 +4356,7 @@ static int acquire_root_devno( if (S_ISBLK(st.st_mode)) { /* Refuse referencing explicit block devices if a root dir is specified, after all we should - * be able to leave the image the root path constraints us to. */ + * not be able to leave the image the root path constrains us to. */ if (root) return -EPERM; @@ -4399,7 +4785,12 @@ static int run(int argc, char *argv[]) { return r; /* Open all files to copy blocks from now, since we want to take their size into consideration */ - r = context_open_copy_block_paths(context); + r = context_open_copy_block_paths( + context, + arg_root, + loop_device ? loop_device->devno : /* if --image= is specified, only allow partitions on the loopback device*/ + arg_root && !arg_image ? 0 : /* if --root= is specified, don't accept any block device */ + (dev_t) -1); /* if neither is specified, make no restrictions */ if (r < 0) return r;