/* SPDX-License-Identifier: GPL-2.0-or-later */ /* * compose persistent device path * * Logic based on Hannes Reinecke's shell script. */ #include #include #include #include #include #include #include #include "alloc-util.h" #include "dirent-util.h" #include "fd-util.h" #include "parse-util.h" #include "string-util.h" #include "strv.h" #include "sysexits.h" #include "udev-builtin.h" #include "udev-util.h" _printf_(2,3) static void path_prepend(char **path, const char *fmt, ...) { va_list va; _cleanup_free_ char *pre = NULL; int r; va_start(va, fmt); r = vasprintf(&pre, fmt, va); va_end(va); if (r < 0) { log_oom(); exit(EX_OSERR); } if (*path) { char *new; new = strjoin(pre, "-", *path); if (!new) { log_oom(); exit(EX_OSERR); } free_and_replace(*path, new); } else *path = TAKE_PTR(pre); } /* ** Linux only supports 32 bit luns. ** See drivers/scsi/scsi_scan.c::scsilun_to_int() for more details. */ static int format_lun_number(sd_device *dev, char **path) { const char *sysnum; unsigned long lun; int r; r = sd_device_get_sysnum(dev, &sysnum); if (r < 0) return r; if (!sysnum) return -ENOENT; r = safe_atolu_full(sysnum, 10, &lun); if (r < 0) return r; if (lun < 256) /* address method 0, peripheral device addressing with bus id of zero */ path_prepend(path, "lun-%lu", lun); else /* handle all other lun addressing methods by using a variant of the original lun format */ path_prepend(path, "lun-0x%04lx%04lx00000000", lun & 0xffff, (lun >> 16) & 0xffff); return 0; } static sd_device *skip_subsystem(sd_device *dev, const char *subsys) { sd_device *parent; assert(dev); assert(subsys); /* Unlike the function name, this drops multiple parent devices EXCEPT FOR THE LAST ONE. * The last one will be dropped at the end of the loop in builtin_path_id(). * E.g. * Input: /sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.0 * Output: /sys/devices/pci0000:00/0000:00:14.0/usb1 */ for (parent = dev; ; ) { const char *subsystem; if (sd_device_get_subsystem(parent, &subsystem) < 0) break; if (!streq(subsystem, subsys)) break; dev = parent; if (sd_device_get_parent(dev, &parent) < 0) break; } return dev; } static sd_device *handle_scsi_fibre_channel(sd_device *parent, char **path) { sd_device *targetdev; _cleanup_(sd_device_unrefp) sd_device *fcdev = NULL; const char *port, *sysname; _cleanup_free_ char *lun = NULL; assert(parent); assert(path); if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_target", &targetdev) < 0) return NULL; if (sd_device_get_sysname(targetdev, &sysname) < 0) return NULL; if (sd_device_new_from_subsystem_sysname(&fcdev, "fc_transport", sysname) < 0) return NULL; if (sd_device_get_sysattr_value(fcdev, "port_name", &port) < 0) return NULL; format_lun_number(parent, &lun); path_prepend(path, "fc-%s-%s", port, lun); return parent; } static sd_device *handle_scsi_sas_wide_port(sd_device *parent, char **path) { sd_device *targetdev, *target_parent; _cleanup_(sd_device_unrefp) sd_device *sasdev = NULL; const char *sas_address, *sysname; _cleanup_free_ char *lun = NULL; assert(parent); assert(path); if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_target", &targetdev) < 0) return NULL; if (sd_device_get_parent(targetdev, &target_parent) < 0) return NULL; if (sd_device_get_sysname(target_parent, &sysname) < 0) return NULL; if (sd_device_new_from_subsystem_sysname(&sasdev, "sas_device", sysname) < 0) return NULL; if (sd_device_get_sysattr_value(sasdev, "sas_address", &sas_address) < 0) return NULL; format_lun_number(parent, &lun); path_prepend(path, "sas-%s-%s", sas_address, lun); return parent; } static sd_device *handle_scsi_sas(sd_device *parent, char **path) { sd_device *targetdev, *target_parent, *port, *expander; _cleanup_(sd_device_unrefp) sd_device *target_sasdev = NULL, *expander_sasdev = NULL, *port_sasdev = NULL; const char *sas_address = NULL; const char *phy_id; const char *phy_count, *sysname; _cleanup_free_ char *lun = NULL; assert(parent); assert(path); if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_target", &targetdev) < 0) return NULL; if (sd_device_get_parent(targetdev, &target_parent) < 0) return NULL; if (sd_device_get_sysname(target_parent, &sysname) < 0) return NULL; /* Get sas device */ if (sd_device_new_from_subsystem_sysname(&target_sasdev, "sas_device", sysname) < 0) return NULL; /* The next parent is sas port */ if (sd_device_get_parent(target_parent, &port) < 0) return NULL; if (sd_device_get_sysname(port, &sysname) < 0) return NULL; /* Get port device */ if (sd_device_new_from_subsystem_sysname(&port_sasdev, "sas_port", sysname) < 0) return NULL; if (sd_device_get_sysattr_value(port_sasdev, "num_phys", &phy_count) < 0) return NULL; /* Check if we are simple disk */ if (strncmp(phy_count, "1", 2) != 0) return handle_scsi_sas_wide_port(parent, path); /* Get connected phy */ if (sd_device_get_sysattr_value(target_sasdev, "phy_identifier", &phy_id) < 0) return NULL; /* The port's parent is either hba or expander */ if (sd_device_get_parent(port, &expander) < 0) return NULL; if (sd_device_get_sysname(expander, &sysname) < 0) return NULL; /* Get expander device */ if (sd_device_new_from_subsystem_sysname(&expander_sasdev, "sas_device", sysname) >= 0) { /* Get expander's address */ if (sd_device_get_sysattr_value(expander_sasdev, "sas_address", &sas_address) < 0) return NULL; } format_lun_number(parent, &lun); if (sas_address) path_prepend(path, "sas-exp%s-phy%s-%s", sas_address, phy_id, lun); else path_prepend(path, "sas-phy%s-%s", phy_id, lun); return parent; } static sd_device *handle_scsi_iscsi(sd_device *parent, char **path) { sd_device *transportdev; _cleanup_(sd_device_unrefp) sd_device *sessiondev = NULL, *conndev = NULL; const char *target, *connname, *addr, *port; _cleanup_free_ char *lun = NULL; const char *sysname, *sysnum; assert(parent); assert(path); /* find iscsi session */ for (transportdev = parent; ; ) { if (sd_device_get_parent(transportdev, &transportdev) < 0) return NULL; if (sd_device_get_sysname(transportdev, &sysname) < 0) return NULL; if (startswith(sysname, "session")) break; } /* find iscsi session device */ if (sd_device_new_from_subsystem_sysname(&sessiondev, "iscsi_session", sysname) < 0) return NULL; if (sd_device_get_sysattr_value(sessiondev, "targetname", &target) < 0) return NULL; if (sd_device_get_sysnum(transportdev, &sysnum) < 0 || !sysnum) return NULL; connname = strjoina("connection", sysnum, ":0"); if (sd_device_new_from_subsystem_sysname(&conndev, "iscsi_connection", connname) < 0) return NULL; if (sd_device_get_sysattr_value(conndev, "persistent_address", &addr) < 0) return NULL; if (sd_device_get_sysattr_value(conndev, "persistent_port", &port) < 0) return NULL; format_lun_number(parent, &lun); path_prepend(path, "ip-%s:%s-iscsi-%s-%s", addr, port, target, lun); return parent; } static sd_device *handle_scsi_ata(sd_device *parent, char **path, char **compat_path) { sd_device *targetdev, *target_parent; _cleanup_(sd_device_unrefp) sd_device *atadev = NULL; const char *port_no, *sysname, *name; unsigned host, bus, target, lun; assert(parent); assert(path); if (sd_device_get_sysname(parent, &name) < 0) return NULL; if (sscanf(name, "%u:%u:%u:%u", &host, &bus, &target, &lun) != 4) return NULL; if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_host", &targetdev) < 0) return NULL; if (sd_device_get_parent(targetdev, &target_parent) < 0) return NULL; if (sd_device_get_sysname(target_parent, &sysname) < 0) return NULL; if (sd_device_new_from_subsystem_sysname(&atadev, "ata_port", sysname) < 0) return NULL; if (sd_device_get_sysattr_value(atadev, "port_no", &port_no) < 0) return NULL; if (bus != 0) /* Devices behind port multiplier have a bus != 0 */ path_prepend(path, "ata-%s.%u.0", port_no, bus); else /* Master/slave are distinguished by target id */ path_prepend(path, "ata-%s.%u", port_no, target); /* old compatible persistent link for ATA devices */ if (compat_path) path_prepend(compat_path, "ata-%s", port_no); return parent; } static sd_device *handle_scsi_default(sd_device *parent, char **path) { sd_device *hostdev; int host, bus, target, lun; const char *name, *base, *pos; _cleanup_closedir_ DIR *dir = NULL; int basenum = -1; assert(parent); assert(path); if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_host", &hostdev) < 0) return NULL; if (sd_device_get_sysname(parent, &name) < 0) return NULL; if (sscanf(name, "%d:%d:%d:%d", &host, &bus, &target, &lun) != 4) return NULL; /* * Rebase host offset to get the local relative number * * Note: This is by definition racy, unreliable and too simple. * Please do not copy this model anywhere. It's just a left-over * from the time we had no idea how things should look like in * the end. * * Making assumptions about a global in-kernel counter and use * that to calculate a local offset is a very broken concept. It * can only work as long as things are in strict order. * * The kernel needs to export the instance/port number of a * controller directly, without the need for rebase magic like * this. Manual driver unbind/bind, parallel hotplug/unplug will * get into the way of this "I hope it works" logic. */ if (sd_device_get_syspath(hostdev, &base) < 0) return NULL; pos = strrchr(base, '/'); if (!pos) return NULL; base = strndupa_safe(base, pos - base); dir = opendir(base); if (!dir) return NULL; FOREACH_DIRENT_ALL(de, dir, break) { unsigned i; if (de->d_name[0] == '.') continue; if (!IN_SET(de->d_type, DT_DIR, DT_LNK)) continue; if (!startswith(de->d_name, "host")) continue; if (safe_atou_full(&de->d_name[4], 10, &i) < 0) continue; /* * find the smallest number; the host really needs to export its * own instance number per parent device; relying on the global host * enumeration and plainly rebasing the numbers sounds unreliable */ if (basenum == -1 || (int) i < basenum) basenum = i; } if (basenum == -1) return hostdev; host -= basenum; path_prepend(path, "scsi-%i:%i:%i:%i", host, bus, target, lun); return hostdev; } static sd_device *handle_scsi_hyperv(sd_device *parent, char **path, size_t guid_str_len) { sd_device *hostdev; sd_device *vmbusdev; const char *guid_str; _cleanup_free_ char *lun = NULL; char guid[39]; assert(parent); assert(path); assert(guid_str_len < sizeof(guid)); if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_host", &hostdev) < 0) return NULL; if (sd_device_get_parent(hostdev, &vmbusdev) < 0) return NULL; if (sd_device_get_sysattr_value(vmbusdev, "device_id", &guid_str) < 0) return NULL; if (strlen(guid_str) < guid_str_len || guid_str[0] != '{' || guid_str[guid_str_len-1] != '}') return NULL; size_t k = 0; for (size_t i = 1; i < guid_str_len-1; i++) { if (guid_str[i] == '-') continue; guid[k++] = guid_str[i]; } guid[k] = '\0'; format_lun_number(parent, &lun); path_prepend(path, "vmbus-%s-%s", guid, lun); return parent; } static sd_device *handle_scsi(sd_device *parent, char **path, char **compat_path, bool *supported_parent) { const char *devtype, *id, *name; if (sd_device_get_devtype(parent, &devtype) < 0 || !streq(devtype, "scsi_device")) return parent; /* firewire */ if (sd_device_get_sysattr_value(parent, "ieee1394_id", &id) >= 0) { path_prepend(path, "ieee1394-0x%s", id); *supported_parent = true; return skip_subsystem(parent, "scsi"); } /* scsi sysfs does not have a "subsystem" for the transport */ if (sd_device_get_syspath(parent, &name) < 0) return NULL; if (strstr(name, "/rport-")) { *supported_parent = true; return handle_scsi_fibre_channel(parent, path); } if (strstr(name, "/end_device-")) { *supported_parent = true; return handle_scsi_sas(parent, path); } if (strstr(name, "/session")) { *supported_parent = true; return handle_scsi_iscsi(parent, path); } if (strstr(name, "/ata")) return handle_scsi_ata(parent, path, compat_path); if (strstr(name, "/vmbus_")) return handle_scsi_hyperv(parent, path, 37); else if (strstr(name, "/VMBUS")) return handle_scsi_hyperv(parent, path, 38); return handle_scsi_default(parent, path); } static sd_device *handle_cciss(sd_device *parent, char **path) { const char *str; unsigned controller, disk; if (sd_device_get_sysname(parent, &str) < 0) return NULL; if (sscanf(str, "c%ud%u%*s", &controller, &disk) != 2) return NULL; path_prepend(path, "cciss-disk%u", disk); return skip_subsystem(parent, "cciss"); } static void handle_scsi_tape(sd_device *dev, char **path) { const char *name; /* must be the last device in the syspath */ if (*path) return; if (sd_device_get_sysname(dev, &name) < 0) return; if (startswith(name, "nst") && strchr("lma", name[3])) path_prepend(path, "nst%c", name[3]); else if (startswith(name, "st") && strchr("lma", name[2])) path_prepend(path, "st%c", name[2]); } static sd_device *handle_usb(sd_device *parent, char **path) { const char *devtype, *str, *port; if (sd_device_get_devtype(parent, &devtype) < 0) return parent; if (!STR_IN_SET(devtype, "usb_interface", "usb_device")) return parent; if (sd_device_get_sysname(parent, &str) < 0) return parent; port = strchr(str, '-'); if (!port) return parent; port++; /* USB host number may change across reboots (and probably even without reboot). The part after * USB host number is determined by device topology and so does not change. Hence, drop the * host number and always use '0' instead. */ path_prepend(path, "usb-0:%s", port); return skip_subsystem(parent, "usb"); } static sd_device *handle_bcma(sd_device *parent, char **path) { const char *sysname; unsigned core; if (sd_device_get_sysname(parent, &sysname) < 0) return NULL; if (sscanf(sysname, "bcma%*u:%u", &core) != 1) return NULL; path_prepend(path, "bcma-%u", core); return parent; } /* Handle devices of AP bus in System z platform. */ static sd_device *handle_ap(sd_device *parent, char **path) { const char *type, *func; assert(parent); assert(path); if (sd_device_get_sysattr_value(parent, "type", &type) >= 0 && sd_device_get_sysattr_value(parent, "ap_functions", &func) >= 0) path_prepend(path, "ap-%s-%s", type, func); else { const char *sysname; if (sd_device_get_sysname(parent, &sysname) >= 0) path_prepend(path, "ap-%s", sysname); } return skip_subsystem(parent, "ap"); } static int find_real_nvme_parent(sd_device *dev, sd_device **ret) { _cleanup_(sd_device_unrefp) sd_device *nvme = NULL; const char *sysname, *end; int r; /* If the device belongs to "nvme-subsystem" (not to be confused with "nvme"), which happens when * NVMe multipathing is enabled in the kernel (/sys/module/nvme_core/parameters/multipath is Y), * then the syspath is something like the following: * /sys/devices/virtual/nvme-subsystem/nvme-subsys0/nvme0n1 * Hence, we need to find the 'real parent' in "nvme" subsystem, e.g, * /sys/devices/pci0000:00/0000:00:1c.4/0000:3c:00.0/nvme/nvme0 */ assert(dev); assert(ret); r = sd_device_get_sysname(dev, &sysname); if (r < 0) return r; /* The sysname format of nvme block device is nvme%d[c%d]n%d[p%d], e.g. nvme0n1p2 or nvme0c1n2. * (Note, nvme device with 'c' can be ignored, as they are hidden. ) * The sysname format of nvme subsystem device is nvme%d. * See nvme_alloc_ns() and nvme_init_ctrl() in drivers/nvme/host/core.c for more details. */ end = startswith(sysname, "nvme"); if (!end) return -ENXIO; end += strspn(end, DIGITS); sysname = strndupa(sysname, end - sysname); r = sd_device_new_from_subsystem_sysname(&nvme, "nvme", sysname); if (r < 0) return r; *ret = TAKE_PTR(nvme); return 0; } static int builtin_path_id(sd_device *dev, sd_netlink **rtnl, int argc, char *argv[], bool test) { _cleanup_(sd_device_unrefp) sd_device *dev_other_branch = NULL; _cleanup_free_ char *path = NULL, *compat_path = NULL; bool supported_transport = false, supported_parent = false; const char *subsystem; int r; assert(dev); /* walk up the chain of devices and compose path */ for (sd_device *parent = dev; parent; ) { const char *subsys, *sysname; if (sd_device_get_subsystem(parent, &subsys) < 0 || sd_device_get_sysname(parent, &sysname) < 0) { ; } else if (streq(subsys, "scsi_tape")) { handle_scsi_tape(parent, &path); } else if (streq(subsys, "scsi")) { parent = handle_scsi(parent, &path, &compat_path, &supported_parent); supported_transport = true; } else if (streq(subsys, "cciss")) { parent = handle_cciss(parent, &path); supported_transport = true; } else if (streq(subsys, "usb")) { parent = handle_usb(parent, &path); supported_transport = true; } else if (streq(subsys, "bcma")) { parent = handle_bcma(parent, &path); supported_transport = true; } else if (streq(subsys, "serio")) { const char *sysnum; if (sd_device_get_sysnum(parent, &sysnum) >= 0 && sysnum) { path_prepend(&path, "serio-%s", sysnum); parent = skip_subsystem(parent, "serio"); } } else if (streq(subsys, "pci")) { path_prepend(&path, "pci-%s", sysname); if (compat_path) path_prepend(&compat_path, "pci-%s", sysname); parent = skip_subsystem(parent, "pci"); supported_parent = true; } else if (streq(subsys, "platform")) { path_prepend(&path, "platform-%s", sysname); if (compat_path) path_prepend(&compat_path, "platform-%s", sysname); parent = skip_subsystem(parent, "platform"); supported_transport = true; supported_parent = true; } else if (streq(subsys, "amba")) { path_prepend(&path, "amba-%s", sysname); if (compat_path) path_prepend(&compat_path, "amba-%s", sysname); parent = skip_subsystem(parent, "amba"); supported_transport = true; supported_parent = true; } else if (streq(subsys, "acpi")) { path_prepend(&path, "acpi-%s", sysname); if (compat_path) path_prepend(&compat_path, "acpi-%s", sysname); parent = skip_subsystem(parent, "acpi"); supported_parent = true; } else if (streq(subsys, "xen")) { path_prepend(&path, "xen-%s", sysname); if (compat_path) path_prepend(&compat_path, "xen-%s", sysname); parent = skip_subsystem(parent, "xen"); supported_parent = true; } else if (streq(subsys, "virtio")) { parent = skip_subsystem(parent, "virtio"); supported_transport = true; } else if (streq(subsys, "scm")) { path_prepend(&path, "scm-%s", sysname); if (compat_path) path_prepend(&compat_path, "scm-%s", sysname); parent = skip_subsystem(parent, "scm"); supported_transport = true; supported_parent = true; } else if (streq(subsys, "ccw")) { path_prepend(&path, "ccw-%s", sysname); if (compat_path) path_prepend(&compat_path, "ccw-%s", sysname); parent = skip_subsystem(parent, "ccw"); supported_transport = true; supported_parent = true; } else if (streq(subsys, "ccwgroup")) { path_prepend(&path, "ccwgroup-%s", sysname); if (compat_path) path_prepend(&compat_path, "ccwgroup-%s", sysname); parent = skip_subsystem(parent, "ccwgroup"); supported_transport = true; supported_parent = true; } else if (streq(subsys, "ap")) { parent = handle_ap(parent, &path); supported_transport = true; supported_parent = true; } else if (streq(subsys, "iucv")) { path_prepend(&path, "iucv-%s", sysname); if (compat_path) path_prepend(&compat_path, "iucv-%s", sysname); parent = skip_subsystem(parent, "iucv"); supported_transport = true; supported_parent = true; } else if (STR_IN_SET(subsys, "nvme", "nvme-subsystem")) { const char *nsid; if (sd_device_get_sysattr_value(dev, "nsid", &nsid) >= 0) { path_prepend(&path, "nvme-%s", nsid); if (compat_path) path_prepend(&compat_path, "nvme-%s", nsid); if (streq(subsys, "nvme-subsystem")) { r = find_real_nvme_parent(dev, &dev_other_branch); if (r < 0) return r; parent = dev_other_branch; } parent = skip_subsystem(parent, "nvme"); supported_parent = true; supported_transport = true; } } else if (streq(subsys, "spi")) { const char *sysnum; if (sd_device_get_sysnum(parent, &sysnum) >= 0 && sysnum) { path_prepend(&path, "cs-%s", sysnum); parent = skip_subsystem(parent, "spi"); } } if (!parent) break; if (sd_device_get_parent(parent, &parent) < 0) break; } if (!path) return -ENOENT; /* * Do not return devices with an unknown parent device type. They * might produce conflicting IDs if the parent does not provide a * unique and predictable name. */ if (!supported_parent) return -ENOENT; /* * Do not return block devices without a well-known transport. Some * devices do not expose their buses and do not provide a unique * and predictable name that way. */ if (sd_device_get_subsystem(dev, &subsystem) >= 0 && streq(subsystem, "block") && !supported_transport) return -ENOENT; { char tag[UDEV_NAME_SIZE]; size_t i = 0; /* compose valid udev tag name */ for (const char *p = path; *p; p++) { if (ascii_isdigit(*p) || ascii_isalpha(*p) || *p == '-') { tag[i++] = *p; continue; } /* skip all leading '_' */ if (i == 0) continue; /* avoid second '_' */ if (tag[i-1] == '_') continue; tag[i++] = '_'; } /* strip trailing '_' */ while (i > 0 && tag[i-1] == '_') i--; tag[i] = '\0'; udev_builtin_add_property(dev, test, "ID_PATH", path); udev_builtin_add_property(dev, test, "ID_PATH_TAG", tag); } /* * Compatible link generation for ATA devices * we assign compat_link to the env variable * ID_PATH_ATA_COMPAT */ if (compat_path) udev_builtin_add_property(dev, test, "ID_PATH_ATA_COMPAT", compat_path); return 0; } const UdevBuiltin udev_builtin_path_id = { .name = "path_id", .cmd = builtin_path_id, .help = "Compose persistent device path", .run_once = true, };