| /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
| |
| #include <linux/blkpg.h> |
| #include <sys/file.h> |
| #include <sys/ioctl.h> |
| #include <sys/mount.h> |
| #include <unistd.h> |
| |
| #include "sd-device.h" |
| |
| #include "alloc-util.h" |
| #include "blockdev-util.h" |
| #include "btrfs-util.h" |
| #include "device-util.h" |
| #include "devnum-util.h" |
| #include "dirent-util.h" |
| #include "errno-util.h" |
| #include "fd-util.h" |
| #include "fileio.h" |
| #include "missing_magic.h" |
| #include "parse-util.h" |
| |
| static int fd_get_devnum(int fd, BlockDeviceLookupFlag flags, dev_t *ret) { |
| struct stat st; |
| dev_t devnum; |
| int r; |
| |
| assert(fd >= 0); |
| assert(ret); |
| |
| if (fstat(fd, &st) < 0) |
| return -errno; |
| |
| if (S_ISBLK(st.st_mode)) |
| devnum = st.st_rdev; |
| else if (!FLAGS_SET(flags, BLOCK_DEVICE_LOOKUP_BACKING)) |
| return -ENOTBLK; |
| else if (!S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode)) |
| return -ENOTBLK; |
| else if (major(st.st_dev) != 0) |
| devnum = st.st_dev; |
| else { |
| /* If major(st.st_dev) is zero, this might mean we are backed by btrfs, which needs special |
| * handing, to get the backing device node. */ |
| |
| r = btrfs_get_block_device_fd(fd, &devnum); |
| if (r == -ENOTTY) /* not btrfs */ |
| return -ENOTBLK; |
| if (r < 0) |
| return r; |
| } |
| |
| *ret = devnum; |
| return 0; |
| } |
| |
| int block_device_is_whole_disk(sd_device *dev) { |
| const char *s; |
| int r; |
| |
| assert(dev); |
| |
| r = sd_device_get_subsystem(dev, &s); |
| if (r < 0) |
| return r; |
| |
| if (!streq(s, "block")) |
| return -ENOTBLK; |
| |
| r = sd_device_get_devtype(dev, &s); |
| if (r < 0) |
| return r; |
| |
| return streq(s, "disk"); |
| } |
| |
| int block_device_get_whole_disk(sd_device *dev, sd_device **ret) { |
| int r; |
| |
| assert(dev); |
| assert(ret); |
| |
| /* Do not unref returned sd_device object. */ |
| |
| r = block_device_is_whole_disk(dev); |
| if (r < 0) |
| return r; |
| if (r == 0) { |
| r = sd_device_get_parent(dev, &dev); |
| if (r == -ENOENT) /* Already removed? Let's return a recognizable error. */ |
| return -ENODEV; |
| if (r < 0) |
| return r; |
| |
| r = block_device_is_whole_disk(dev); |
| if (r < 0) |
| return r; |
| if (r == 0) |
| return -ENXIO; |
| } |
| |
| *ret = dev; |
| return 0; |
| } |
| |
| static int block_device_get_originating(sd_device *dev, sd_device **ret) { |
| _cleanup_(sd_device_unrefp) sd_device *first_found = NULL; |
| const char *suffix; |
| sd_device *child; |
| dev_t devnum = 0; /* avoid false maybe-uninitialized warning */ |
| |
| /* For the specified block device tries to chase it through the layers, in case LUKS-style DM |
| * stacking is used, trying to find the next underlying layer. */ |
| |
| assert(dev); |
| assert(ret); |
| |
| FOREACH_DEVICE_CHILD_WITH_SUFFIX(dev, child, suffix) { |
| sd_device *child_whole_disk; |
| dev_t n; |
| |
| if (!path_startswith(suffix, "slaves")) |
| continue; |
| |
| if (block_device_get_whole_disk(child, &child_whole_disk) < 0) |
| continue; |
| |
| if (sd_device_get_devnum(child_whole_disk, &n) < 0) |
| continue; |
| |
| if (!first_found) { |
| first_found = sd_device_ref(child); |
| devnum = n; |
| continue; |
| } |
| |
| /* We found a device backed by multiple other devices. We don't really support automatic |
| * discovery on such setups, with the exception of dm-verity partitions. In this case there |
| * are two backing devices: the data partition and the hash partition. We are fine with such |
| * setups, however, only if both partitions are on the same physical device. Hence, let's |
| * verify this by iterating over every node in the 'slaves/' directory and comparing them with |
| * the first that gets returned by readdir(), to ensure they all point to the same device. */ |
| if (n != devnum) |
| return -ENOTUNIQ; |
| } |
| |
| if (!first_found) |
| return -ENOENT; |
| |
| *ret = TAKE_PTR(first_found); |
| return 1; /* found */ |
| } |
| |
| int block_device_new_from_fd(int fd, BlockDeviceLookupFlag flags, sd_device **ret) { |
| _cleanup_(sd_device_unrefp) sd_device *dev = NULL; |
| dev_t devnum; |
| int r; |
| |
| assert(fd >= 0); |
| assert(ret); |
| |
| r = fd_get_devnum(fd, flags, &devnum); |
| if (r < 0) |
| return r; |
| |
| r = sd_device_new_from_devnum(&dev, 'b', devnum); |
| if (r < 0) |
| return r; |
| |
| if (FLAGS_SET(flags, BLOCK_DEVICE_LOOKUP_ORIGINATING)) { |
| _cleanup_(sd_device_unrefp) sd_device *dev_origin = NULL; |
| sd_device *dev_whole_disk; |
| |
| r = block_device_get_whole_disk(dev, &dev_whole_disk); |
| if (r < 0) |
| return r; |
| |
| r = block_device_get_originating(dev_whole_disk, &dev_origin); |
| if (r < 0 && r != -ENOENT) |
| return r; |
| if (r > 0) |
| device_unref_and_replace(dev, dev_origin); |
| } |
| |
| if (FLAGS_SET(flags, BLOCK_DEVICE_LOOKUP_WHOLE_DISK)) { |
| sd_device *dev_whole_disk; |
| |
| r = block_device_get_whole_disk(dev, &dev_whole_disk); |
| if (r < 0) |
| return r; |
| |
| *ret = sd_device_ref(dev_whole_disk); |
| return 0; |
| } |
| |
| *ret = sd_device_ref(dev); |
| return 0; |
| } |
| |
| int block_device_new_from_path(const char *path, BlockDeviceLookupFlag flags, sd_device **ret) { |
| _cleanup_close_ int fd = -EBADF; |
| |
| assert(path); |
| assert(ret); |
| |
| fd = open(path, O_CLOEXEC|O_PATH); |
| if (fd < 0) |
| return -errno; |
| |
| return block_device_new_from_fd(fd, flags, ret); |
| } |
| |
| int block_get_whole_disk(dev_t d, dev_t *ret) { |
| char p[SYS_BLOCK_PATH_MAX("/partition")]; |
| _cleanup_free_ char *s = NULL; |
| dev_t devt; |
| int r; |
| |
| assert(ret); |
| |
| if (major(d) == 0) |
| return -ENODEV; |
| |
| /* If it has a queue this is good enough for us */ |
| xsprintf_sys_block_path(p, "/queue", d); |
| if (access(p, F_OK) >= 0) { |
| *ret = d; |
| return 0; |
| } |
| if (errno != ENOENT) |
| return -errno; |
| |
| /* If it is a partition find the originating device */ |
| xsprintf_sys_block_path(p, "/partition", d); |
| if (access(p, F_OK) < 0) |
| return -errno; |
| |
| /* Get parent dev_t */ |
| xsprintf_sys_block_path(p, "/../dev", d); |
| r = read_one_line_file(p, &s); |
| if (r < 0) |
| return r; |
| |
| r = parse_devnum(s, &devt); |
| if (r < 0) |
| return r; |
| |
| /* Only return this if it is really good enough for us. */ |
| xsprintf_sys_block_path(p, "/queue", devt); |
| if (access(p, F_OK) < 0) |
| return -errno; |
| |
| *ret = devt; |
| return 1; |
| } |
| |
| int get_block_device_fd(int fd, dev_t *ret) { |
| struct stat st; |
| int r; |
| |
| assert(fd >= 0); |
| assert(ret); |
| |
| /* Gets the block device directly backing a file system. If the block device is encrypted, returns |
| * the device mapper block device. */ |
| |
| if (fstat(fd, &st)) |
| return -errno; |
| |
| if (major(st.st_dev) != 0) { |
| *ret = st.st_dev; |
| return 1; |
| } |
| |
| r = btrfs_get_block_device_fd(fd, ret); |
| if (r > 0) |
| return 1; |
| if (r != -ENOTTY) /* not btrfs */ |
| return r; |
| |
| *ret = 0; |
| return 0; |
| } |
| |
| int get_block_device(const char *path, dev_t *ret) { |
| _cleanup_close_ int fd = -EBADF; |
| |
| assert(path); |
| assert(ret); |
| |
| fd = open(path, O_RDONLY|O_NOFOLLOW|O_CLOEXEC); |
| if (fd < 0) |
| return -errno; |
| |
| return get_block_device_fd(fd, ret); |
| } |
| |
| int block_get_originating(dev_t dt, dev_t *ret) { |
| _cleanup_(sd_device_unrefp) sd_device *dev = NULL, *origin = NULL; |
| int r; |
| |
| assert(ret); |
| |
| r = sd_device_new_from_devnum(&dev, 'b', dt); |
| if (r < 0) |
| return r; |
| |
| r = block_device_get_originating(dev, &origin); |
| if (r < 0) |
| return r; |
| |
| return sd_device_get_devnum(origin, ret); |
| } |
| |
| int get_block_device_harder_fd(int fd, dev_t *ret) { |
| int r; |
| |
| assert(fd >= 0); |
| assert(ret); |
| |
| /* Gets the backing block device for a file system, and handles LUKS encrypted file systems, looking for its |
| * immediate parent, if there is one. */ |
| |
| r = get_block_device_fd(fd, ret); |
| if (r <= 0) |
| return r; |
| |
| r = block_get_originating(*ret, ret); |
| if (r < 0) |
| log_debug_errno(r, "Failed to chase block device, ignoring: %m"); |
| |
| return 1; |
| } |
| |
| int get_block_device_harder(const char *path, dev_t *ret) { |
| _cleanup_close_ int fd = -EBADF; |
| |
| assert(path); |
| assert(ret); |
| |
| fd = open(path, O_RDONLY|O_NOFOLLOW|O_CLOEXEC); |
| if (fd < 0) |
| return -errno; |
| |
| return get_block_device_harder_fd(fd, ret); |
| } |
| |
| int lock_whole_block_device(dev_t devt, int operation) { |
| _cleanup_close_ int lock_fd = -EBADF; |
| dev_t whole_devt; |
| int r; |
| |
| /* Let's get a BSD file lock on the whole block device, as per: https://systemd.io/BLOCK_DEVICE_LOCKING */ |
| |
| r = block_get_whole_disk(devt, &whole_devt); |
| if (r < 0) |
| return r; |
| |
| lock_fd = r = device_open_from_devnum(S_IFBLK, whole_devt, O_RDONLY|O_CLOEXEC|O_NONBLOCK, NULL); |
| if (r < 0) |
| return r; |
| |
| if (flock(lock_fd, operation) < 0) |
| return -errno; |
| |
| return TAKE_FD(lock_fd); |
| } |
| |
| int blockdev_partscan_enabled(int fd) { |
| _cleanup_free_ char *p = NULL, *buf = NULL; |
| unsigned long long ull; |
| struct stat st; |
| int r; |
| |
| /* Checks if partition scanning is correctly enabled on the block device */ |
| |
| if (fstat(fd, &st) < 0) |
| return -errno; |
| |
| if (!S_ISBLK(st.st_mode)) |
| return -ENOTBLK; |
| |
| if (asprintf(&p, "/sys/dev/block/%u:%u/capability", major(st.st_rdev), minor(st.st_rdev)) < 0) |
| return -ENOMEM; |
| |
| r = read_one_line_file(p, &buf); |
| if (r == -ENOENT) /* If the capability file doesn't exist then we are most likely looking at a |
| * partition block device, not the whole block device. And that means we have no |
| * partition scanning on for it (we do for its parent, but not for the partition |
| * itself). */ |
| return false; |
| if (r < 0) |
| return r; |
| |
| r = safe_atollu_full(buf, 16, &ull); |
| if (r < 0) |
| return r; |
| |
| #ifndef GENHD_FL_NO_PART_SCAN |
| #define GENHD_FL_NO_PART_SCAN (0x0200) |
| #endif |
| |
| return !FLAGS_SET(ull, GENHD_FL_NO_PART_SCAN); |
| } |
| |
| static int blockdev_is_encrypted(const char *sysfs_path, unsigned depth_left) { |
| _cleanup_free_ char *p = NULL, *uuids = NULL; |
| _cleanup_closedir_ DIR *d = NULL; |
| int r, found_encrypted = false; |
| |
| assert(sysfs_path); |
| |
| if (depth_left == 0) |
| return -EINVAL; |
| |
| p = path_join(sysfs_path, "dm/uuid"); |
| if (!p) |
| return -ENOMEM; |
| |
| r = read_one_line_file(p, &uuids); |
| if (r != -ENOENT) { |
| if (r < 0) |
| return r; |
| |
| /* The DM device's uuid attribute is prefixed with "CRYPT-" if this is a dm-crypt device. */ |
| if (startswith(uuids, "CRYPT-")) |
| return true; |
| } |
| |
| /* Not a dm-crypt device itself. But maybe it is on top of one? Follow the links in the "slaves/" |
| * subdir. */ |
| |
| p = mfree(p); |
| p = path_join(sysfs_path, "slaves"); |
| if (!p) |
| return -ENOMEM; |
| |
| d = opendir(p); |
| if (!d) { |
| if (errno == ENOENT) /* Doesn't have underlying devices */ |
| return false; |
| |
| return -errno; |
| } |
| |
| for (;;) { |
| _cleanup_free_ char *q = NULL; |
| struct dirent *de; |
| |
| errno = 0; |
| de = readdir_no_dot(d); |
| if (!de) { |
| if (errno != 0) |
| return -errno; |
| |
| break; /* No more underlying devices */ |
| } |
| |
| q = path_join(p, de->d_name); |
| if (!q) |
| return -ENOMEM; |
| |
| r = blockdev_is_encrypted(q, depth_left - 1); |
| if (r < 0) |
| return r; |
| if (r == 0) /* we found one that is not encrypted? then propagate that immediately */ |
| return false; |
| |
| found_encrypted = true; |
| } |
| |
| return found_encrypted; |
| } |
| |
| int fd_is_encrypted(int fd) { |
| char p[SYS_BLOCK_PATH_MAX(NULL)]; |
| dev_t devt; |
| int r; |
| |
| r = get_block_device_fd(fd, &devt); |
| if (r < 0) |
| return r; |
| if (r == 0) /* doesn't have a block device */ |
| return false; |
| |
| xsprintf_sys_block_path(p, NULL, devt); |
| |
| return blockdev_is_encrypted(p, 10 /* safety net: maximum recursion depth */); |
| } |
| |
| int path_is_encrypted(const char *path) { |
| char p[SYS_BLOCK_PATH_MAX(NULL)]; |
| dev_t devt; |
| int r; |
| |
| r = get_block_device(path, &devt); |
| if (r < 0) |
| return r; |
| if (r == 0) /* doesn't have a block device */ |
| return false; |
| |
| xsprintf_sys_block_path(p, NULL, devt); |
| |
| return blockdev_is_encrypted(p, 10 /* safety net: maximum recursion depth */); |
| } |
| |
| int fd_get_whole_disk(int fd, bool backing, dev_t *ret) { |
| dev_t devt; |
| int r; |
| |
| assert(fd >= 0); |
| assert(ret); |
| |
| r = fd_get_devnum(fd, backing ? BLOCK_DEVICE_LOOKUP_BACKING : 0, &devt); |
| if (r < 0) |
| return r; |
| |
| return block_get_whole_disk(devt, ret); |
| } |
| |
| int path_get_whole_disk(const char *path, bool backing, dev_t *ret) { |
| _cleanup_close_ int fd = -EBADF; |
| |
| fd = open(path, O_CLOEXEC|O_PATH); |
| if (fd < 0) |
| return -errno; |
| |
| return fd_get_whole_disk(fd, backing, ret); |
| } |
| |
| int block_device_add_partition( |
| int fd, |
| const char *name, |
| int nr, |
| uint64_t start, |
| uint64_t size) { |
| |
| assert(fd >= 0); |
| assert(name); |
| assert(nr > 0); |
| |
| struct blkpg_partition bp = { |
| .pno = nr, |
| .start = start, |
| .length = size, |
| }; |
| |
| struct blkpg_ioctl_arg ba = { |
| .op = BLKPG_ADD_PARTITION, |
| .data = &bp, |
| .datalen = sizeof(bp), |
| }; |
| |
| if (strlen(name) >= sizeof(bp.devname)) |
| return -EINVAL; |
| |
| strcpy(bp.devname, name); |
| |
| return RET_NERRNO(ioctl(fd, BLKPG, &ba)); |
| } |
| |
| int block_device_remove_partition( |
| int fd, |
| const char *name, |
| int nr) { |
| |
| assert(fd >= 0); |
| assert(name); |
| assert(nr > 0); |
| |
| struct blkpg_partition bp = { |
| .pno = nr, |
| }; |
| |
| struct blkpg_ioctl_arg ba = { |
| .op = BLKPG_DEL_PARTITION, |
| .data = &bp, |
| .datalen = sizeof(bp), |
| }; |
| |
| if (strlen(name) >= sizeof(bp.devname)) |
| return -EINVAL; |
| |
| strcpy(bp.devname, name); |
| |
| return RET_NERRNO(ioctl(fd, BLKPG, &ba)); |
| } |
| |
| int block_device_resize_partition( |
| int fd, |
| int nr, |
| uint64_t start, |
| uint64_t size) { |
| |
| assert(fd >= 0); |
| assert(nr > 0); |
| |
| struct blkpg_partition bp = { |
| .pno = nr, |
| .start = start, |
| .length = size, |
| }; |
| |
| struct blkpg_ioctl_arg ba = { |
| .op = BLKPG_RESIZE_PARTITION, |
| .data = &bp, |
| .datalen = sizeof(bp), |
| }; |
| |
| return RET_NERRNO(ioctl(fd, BLKPG, &ba)); |
| } |
| |
| int partition_enumerator_new(sd_device *dev, sd_device_enumerator **ret) { |
| _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; |
| const char *s; |
| int r; |
| |
| assert(dev); |
| assert(ret); |
| |
| /* Refuse invocation on partition block device, insist on "whole" device */ |
| r = block_device_is_whole_disk(dev); |
| if (r < 0) |
| return r; |
| if (r == 0) |
| return -ENXIO; /* return a recognizable error */ |
| |
| r = sd_device_enumerator_new(&e); |
| if (r < 0) |
| return r; |
| |
| r = sd_device_enumerator_allow_uninitialized(e); |
| if (r < 0) |
| return r; |
| |
| r = sd_device_enumerator_add_match_parent(e, dev); |
| if (r < 0) |
| return r; |
| |
| r = sd_device_get_sysname(dev, &s); |
| if (r < 0) |
| return r; |
| |
| /* Also add sysname check for safety. Hopefully, this also improves performance. */ |
| s = strjoina(s, "*"); |
| r = sd_device_enumerator_add_match_sysname(e, s); |
| if (r < 0) |
| return r; |
| |
| r = sd_device_enumerator_add_match_subsystem(e, "block", /* match = */ true); |
| if (r < 0) |
| return r; |
| |
| r = sd_device_enumerator_add_match_property(e, "DEVTYPE", "partition"); |
| if (r < 0) |
| return r; |
| |
| *ret = TAKE_PTR(e); |
| return 0; |
| } |
| |
| int block_device_remove_all_partitions(sd_device *dev, int fd) { |
| _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; |
| _cleanup_(sd_device_unrefp) sd_device *dev_unref = NULL; |
| _cleanup_close_ int fd_close = -EBADF; |
| bool has_partitions = false; |
| sd_device *part; |
| int r, k = 0; |
| |
| assert(dev || fd >= 0); |
| |
| if (!dev) { |
| r = block_device_new_from_fd(fd, 0, &dev_unref); |
| if (r < 0) |
| return r; |
| |
| dev = dev_unref; |
| } |
| |
| r = partition_enumerator_new(dev, &e); |
| if (r < 0) |
| return r; |
| |
| if (fd < 0) { |
| fd_close = sd_device_open(dev, O_CLOEXEC|O_NONBLOCK|O_NOCTTY|O_RDONLY); |
| if (fd_close < 0) |
| return fd_close; |
| |
| fd = fd_close; |
| } |
| |
| FOREACH_DEVICE(e, part) { |
| const char *v, *devname; |
| int nr; |
| |
| has_partitions = true; |
| |
| r = sd_device_get_devname(part, &devname); |
| if (r < 0) |
| return r; |
| |
| r = sd_device_get_property_value(part, "PARTN", &v); |
| if (r < 0) |
| return r; |
| |
| r = safe_atoi(v, &nr); |
| if (r < 0) |
| return r; |
| |
| r = btrfs_forget_device(devname); |
| if (r < 0 && r != -ENOENT) |
| log_debug_errno(r, "Failed to forget btrfs device %s, ignoring: %m", devname); |
| |
| r = block_device_remove_partition(fd, devname, nr); |
| if (r == -ENODEV) { |
| log_debug("Kernel removed partition %s before us, ignoring", devname); |
| continue; |
| } |
| if (r < 0) { |
| log_debug_errno(r, "Failed to remove partition %s: %m", devname); |
| k = k < 0 ? k : r; |
| continue; |
| } |
| |
| log_debug("Removed partition %s", devname); |
| } |
| |
| return k < 0 ? k : has_partitions; |
| } |
| |
| int block_device_has_partitions(sd_device *dev) { |
| _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; |
| int r; |
| |
| assert(dev); |
| |
| /* Checks if the specified device currently has partitions. */ |
| |
| r = partition_enumerator_new(dev, &e); |
| if (r < 0) |
| return r; |
| |
| return !!sd_device_enumerator_get_device_first(e); |
| } |
| |
| int blockdev_reread_partition_table(sd_device *dev) { |
| _cleanup_close_ int fd = -EBADF; |
| |
| assert(dev); |
| |
| /* Try to re-read the partition table. This only succeeds if none of the devices is busy. */ |
| |
| fd = sd_device_open(dev, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); |
| if (fd < 0) |
| return fd; |
| |
| if (flock(fd, LOCK_EX|LOCK_NB) < 0) |
| return -errno; |
| |
| if (ioctl(fd, BLKRRPART, 0) < 0) |
| return -errno; |
| |
| return 0; |
| } |
| |
| int blockdev_get_sector_size(int fd, uint32_t *ret) { |
| int ssz = 0; |
| |
| assert(fd >= 0); |
| assert(ret); |
| |
| if (ioctl(fd, BLKSSZGET, &ssz) < 0) |
| return -errno; |
| if (ssz <= 0) /* make sure the field is initialized */ |
| return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Block device reported invalid sector size %i.", ssz); |
| |
| *ret = ssz; |
| return 0; |
| } |