/* SPDX-License-Identifier: LGPL-2.1-or-later */ #include "sd-device.h" #include "alloc-util.h" #include "ansi-color.h" #include "blockdev-list.h" #include "blockdev-util.h" #include "device-private.h" #include "device-util.h" #include "devnum-util.h" #include "errno-util.h" #include "path-util.h" #include "string-util.h" #include "strv.h" #include "terminal-util.h" void block_device_done(BlockDevice *d) { assert(d); d->node = mfree(d->node); d->symlinks = strv_free(d->symlinks); d->model = mfree(d->model); d->vendor = mfree(d->vendor); d->subsystem = mfree(d->subsystem); } void block_device_array_free(BlockDevice *d, size_t n_devices) { FOREACH_ARRAY(i, d, n_devices) block_device_done(i); free(d); } static int blockdev_get_prop(sd_device *d, const char *prop1, const char *prop2, char **ret_value) { int r, ret = 0; assert(d); assert(prop1); assert(ret_value); FOREACH_STRING(prop, prop1, prop2) { const char *m = NULL; r = sd_device_get_property_value(d, prop, &m); if (r < 0 && r != -ENOENT) RET_GATHER(ret, log_device_debug_errno(d, r, "Failed to acquire '%s' from device, ignoring: %m", prop)); else if (!isempty(m)) return strdup_to(ret_value, m); } return ret < 0 ? ret : -ENOENT; } static int blockdev_get_subsystem(sd_device *d, char **ret_subsystem) { int r; assert(d); assert(ret_subsystem); /* We prefer the explicitly set block device subsystem property, because if it is set it's generally * the most useful. If it's not set we'll look for the subsystem of the first parent device that * isn't of subsystem 'block'. The former covers 'virtual' block devices such as loopback, device * mapper, zram, while the latter covers physical block devices such as USB or NVME. */ r = blockdev_get_prop(d, "ID_BLOCK_SUBSYSTEM", /* prop2= */ NULL, ret_subsystem); if (r >= 0) return r; int ret = r != -ENOENT ? r : 0; sd_device *q = d; for (;;) { r = sd_device_get_parent(q, &q); if (r < 0) { if (r != -ENOENT) RET_GATHER(ret, log_device_debug_errno(q, r, "Failed to get parent device, ignoring: %m")); break; } const char *s = NULL; r = sd_device_get_subsystem(q, &s); if (r < 0) RET_GATHER(ret, log_device_debug_errno(q, r, "Failed to get subsystem of device, ignoring: %m")); else if (!isempty(s) && !streq(s, "block")) return strdup_to(ret_subsystem, s); } return ret < 0 ? ret : -ENOENT; } int blockdev_list_get_root_devnos(BlockDevListFlags flags, dev_t *ret_root, dev_t *ret_whole_root) { int r; /* Looks up the devno of the block device the OS is booted from, plus the whole-disk devno (e.g. * /dev/sda for /dev/sda3). Returns both as 0 if root can't be determined – which is fine, callers * pass these to blockdev_list_one() and devnum_set_and_equal() handles zero gracefully. * * Split out of blockdev_list() so subscribers can look them up once up-front instead of per uevent. */ dev_t root_devno = 0, whole_root_devno = 0; if (FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_ROOT)) { r = blockdev_get_root(LOG_DEBUG, &root_devno); if (r < 0) log_debug_errno(r, "Failed to get block device of root device, ignoring: %m"); else if (r > 0) { r = block_get_whole_disk(root_devno, &whole_root_devno); if (r < 0) log_debug_errno(r, "Failed to get whole block device of root device, ignoring: %m"); } } if (ret_root) *ret_root = root_devno; if (ret_whole_root) *ret_whole_root = whole_root_devno; return 0; } int blockdev_list_one( sd_device *dev, BlockDevListFlags flags, dev_t root_devno, dev_t whole_root_devno, BlockDevice *ret) { int r; assert(dev); /* Per-device counterpart to blockdev_list(). Applies the same filters and metadata collection, * and reports via a multi-valued return: * * MATCH_NO – cleanly fails a static filter (never interesting) * MATCH_YES – fully matches * MATCH_FILTERED – passes static filters but fails a dynamic one (IGNORE_EMPTY / IGNORE_READ_ONLY) * MATCH_SKIPPED – an unexpected error tripped during filter evaluation; the caller should * treat this like _FILTERED, but the distinct code makes the soft failure * visible rather than silently swallowed here. * * On each of the four success codes *ret is initialized: BLOCK_DEVICE_NULL for MATCH_NO and * MATCH_SKIPPED, fully populated for MATCH_YES / MATCH_FILTERED (with d.size / d.read_only * reflecting the current state so callers can tell which dynamic filter tripped). On negative * return *ret is untouched. * * blockdev_list() itself collapses everything except _YES back into "skip". */ const char *node; r = sd_device_get_devname(dev, &node); if (r < 0) { log_device_warning_errno(dev, r, "Failed to get device node of discovered block device, ignoring: %m"); goto skipped; } if (FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_ROOT) && root_devno != 0) { dev_t devno; r = sd_device_get_devnum(dev, &devno); if (r < 0) { log_device_warning_errno(dev, r, "Failed to get major/minor of discovered block device, ignoring: %m"); goto skipped; } if (devnum_set_and_equal(devno, root_devno) || devnum_set_and_equal(devno, whole_root_devno)) goto no_match; } bool is_loop = false; if (flags & (BLOCKDEV_LIST_IGNORE_LOOP|BLOCKDEV_LIST_IGNORE_VIRTUAL)) { r = device_sysname_startswith(dev, "loop"); if (r < 0) { log_device_warning_errno(dev, r, "Failed to check device name of discovered block device '%s', ignoring: %m", node); goto skipped; } is_loop = r > 0; if (is_loop && FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_LOOP)) goto no_match; } /* Loopback devices are virtual devices too, but they are covered by BLOCKDEV_LIST_IGNORE_LOOP above, * hence exclude them here. */ if (FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_VIRTUAL) && !is_loop) { const char *devpath; r = sd_device_get_devpath(dev, &devpath); if (r < 0) { log_device_warning_errno(dev, r, "Failed to acquire device path of discovered block device '%s', ignoring: %m", node); goto skipped; } if (path_startswith(devpath, "/devices/virtual/")) goto no_match; } if (FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_PARTITIONS)) { r = device_is_devtype(dev, "partition"); if (r < 0) { log_device_warning_errno(dev, r, "Failed to check if device is a partition '%s', ignoring: %m", node); goto skipped; } if (r > 0) goto no_match; } if (FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_ZRAM)) { r = device_sysname_startswith(dev, "zram"); if (r < 0) { log_device_warning_errno(dev, r, "Failed to check device name of discovered block device '%s', ignoring: %m", node); goto skipped; } if (r > 0) goto no_match; } if (FLAGS_SET(flags, BLOCKDEV_LIST_REQUIRE_PARTITION_SCANNING)) { r = blockdev_partscan_enabled(dev); if (r < 0) { log_device_warning_errno(dev, r, "Unable to determine whether '%s' supports partition scanning, skipping device: %m", node); goto skipped; } if (r == 0) { log_device_debug(dev, "Device '%s' does not support partition scanning, skipping.", node); goto no_match; } } if (FLAGS_SET(flags, BLOCKDEV_LIST_REQUIRE_LUKS)) { const char *fstype; r = sd_device_get_property_value(dev, "ID_FS_TYPE", &fstype); if (r == -ENOENT) goto no_match; /* No detected filesystem → not a LUKS superblock. */ if (r < 0) { log_device_warning_errno(dev, r, "Failed to acquire ID_FS_TYPE of device '%s', ignoring: %m", node); goto skipped; } if (!streq(fstype, "crypto_LUKS")) goto no_match; } bool dynamic_filtered = false; uint64_t size = UINT64_MAX; if (FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_EMPTY) || ret) { r = device_get_sysattr_u64(dev, "size", &size); if (r < 0) log_device_debug_errno(dev, r, "Failed to acquire size of device '%s', ignoring: %m", node); else /* the 'size' sysattr is always in multiples of 512, even on 4K sector block devices! */ assert_se(MUL_ASSIGN_SAFE(&size, 512)); /* Overflow check for coverity */ if (size == 0 && FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_EMPTY)) { log_device_debug(dev, "Device '%s' has a zero size, assuming drive without a medium.", node); dynamic_filtered = true; } } int ro = -1; if (FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_READ_ONLY) || FLAGS_SET(flags, BLOCKDEV_LIST_METADATA)) { r = device_get_sysattr_bool(dev, "ro"); if (r < 0) log_device_debug_errno(dev, r, "Failed to acquire read-only flag of device '%s', ignoring: %m", node); else ro = r; if (ro > 0 && FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_READ_ONLY)) { log_device_debug(dev, "Device '%s' is read-only.", node); dynamic_filtered = true; } } if (!ret) return dynamic_filtered ? BLOCKDEV_LIST_MATCH_FILTERED : BLOCKDEV_LIST_MATCH_YES; /* Wrap the metadata population in a nested scope so the cleanup-attributed locals don't * intersect the goto-target label below (jumping into a protected scope is a clang error). */ { _cleanup_strv_free_ char **list = NULL; if (FLAGS_SET(flags, BLOCKDEV_LIST_SHOW_SYMLINKS)) { FOREACH_DEVICE_DEVLINK(dev, sl) if (strv_extend(&list, sl) < 0) return log_oom(); strv_sort(list); } _cleanup_free_ char *model = NULL, *vendor = NULL, *subsystem = NULL; if (FLAGS_SET(flags, BLOCKDEV_LIST_METADATA)) { (void) blockdev_get_prop(dev, "ID_MODEL_FROM_DATABASE", "ID_MODEL", &model); (void) blockdev_get_prop(dev, "ID_VENDOR_FROM_DATABASE", "ID_VENDOR", &vendor); (void) blockdev_get_subsystem(dev, &subsystem); } uint64_t diskseq = UINT64_MAX; if (FLAGS_SET(flags, BLOCKDEV_LIST_DISKSEQ)) { r = sd_device_get_diskseq(dev, &diskseq); if (r < 0) log_device_debug_errno(dev, r, "Failed to acquire diskseq of device '%s', ignoring: %m", node); } uint64_t read_bytes, write_bytes; bool bytes_good = false; if (FLAGS_SET(flags, BLOCKDEV_LIST_RW_STATS)) { const char *v; r = sd_device_get_sysattr_value(dev, "stat", &v); if (r < 0) log_device_full_errno(dev, ERRNO_IS_NEG_DEVICE_ABSENT(r) ? LOG_DEBUG : LOG_WARNING, r, "%s: failed to read 'stat' attribute, ignoring: %m", node); else { /* Fields 3 and 7: the number of sectors read and written. The "stat" attribute * always counts in 512 byte sectors, regardless of the device's logical block size. * The skipped fields are consumed with %*s, since their values may exceed any fixed * integer width. */ if (sscanf(v, "%*s %*s %"SCNu64" %*s %*s %*s %"SCNu64, &read_bytes, &write_bytes) < 2) log_device_warning(dev, "%s: failed to parse 'stat' attribute.", node); else if (!MUL_SAFE(&read_bytes, read_bytes, UINT64_C(512)) || !MUL_SAFE(&write_bytes, write_bytes, UINT64_C(512))) log_device_debug(dev, "%s: sector counters out of range, ignoring.", node); else bytes_good = true; } } _cleanup_free_ char *m = strdup(node); if (!m) return log_oom(); *ret = (BlockDevice) { .node = TAKE_PTR(m), .symlinks = TAKE_PTR(list), .model = TAKE_PTR(model), .vendor = TAKE_PTR(vendor), .subsystem = TAKE_PTR(subsystem), .diskseq = diskseq, .size = size, .read_bytes = bytes_good ? read_bytes : UINT64_MAX, .write_bytes = bytes_good ? write_bytes : UINT64_MAX, .read_only = ro, }; return dynamic_filtered ? BLOCKDEV_LIST_MATCH_FILTERED : BLOCKDEV_LIST_MATCH_YES; } no_match: if (ret) *ret = BLOCK_DEVICE_NULL; return BLOCKDEV_LIST_MATCH_NO; skipped: if (ret) *ret = BLOCK_DEVICE_NULL; return BLOCKDEV_LIST_MATCH_SKIPPED; } int blockdev_list(BlockDevListFlags flags, BlockDevice **ret_devices, size_t *ret_n_devices) { _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; int r; assert(!!ret_devices == !!ret_n_devices); /* If ret_devices/ret_n_devices are passed, returns a list of matching block devices, otherwise * prints the list to stdout */ BlockDevice *l = NULL; size_t n = 0; CLEANUP_ARRAY(l, n, block_device_array_free); dev_t root_devno = 0, whole_root_devno = 0; (void) blockdev_list_get_root_devnos(flags, &root_devno, &whole_root_devno); if (sd_device_enumerator_new(&e) < 0) return log_oom(); r = sd_device_enumerator_add_match_subsystem(e, "block", /* match= */ true); if (r < 0) return log_error_errno(r, "Failed to add subsystem match: %m"); if (!(flags & (BLOCKDEV_LIST_REQUIRE_LUKS | BLOCKDEV_LIST_SHOW_SYMLINKS | BLOCKDEV_LIST_METADATA))) { /* If everything we need comes from the device name or sysfs directly, no udev db entry is needed. */ r = sd_device_enumerator_allow_uninitialized(e); if (r < 0) log_debug_errno(r, "Failed to allow uninitialized devices, ignoring: %m"); } if (FLAGS_SET(flags, BLOCKDEV_LIST_REQUIRE_LUKS)) { /* blockdev_list_one() enforces this filter authoritatively; the enumerator match is just * an optimization so we don't iterate non-LUKS devices here. */ r = sd_device_enumerator_add_match_property(e, "ID_FS_TYPE", "crypto_LUKS"); if (r < 0) return log_error_errno(r, "Failed to add match for LUKS block devices: %m"); } FOREACH_DEVICE(e, dev) { _cleanup_(block_device_done) BlockDevice d = BLOCK_DEVICE_NULL; r = blockdev_list_one(dev, flags, root_devno, whole_root_devno, ret_devices ? &d : NULL); if (r < 0) return r; if (r != BLOCKDEV_LIST_MATCH_YES) continue; /* MATCH_NO or MATCH_FILTERED – bulk enumeration treats both as "skip" */ if (ret_devices) { if (!GREEDY_REALLOC(l, n+1)) return log_oom(); l[n++] = TAKE_STRUCT(d); } else { const char *node; if (sd_device_get_devname(dev, &node) < 0) continue; printf("%s\n", node); if (FLAGS_SET(flags, BLOCKDEV_LIST_SHOW_SYMLINKS)) FOREACH_DEVICE_DEVLINK(dev, sl) printf("%s%s%s%s\n", on_tty() ? " " : "", ansi_grey(), sl, ansi_normal()); } } if (ret_devices) *ret_devices = TAKE_PTR(l); if (ret_n_devices) *ret_n_devices = n; return 0; }