diff --git a/meson.build b/meson.build index 08a3e41..c0d056c 100644 --- a/meson.build +++ b/meson.build @@ -104,6 +104,14 @@ xargs_src = files( xargs_inc = include_directories('src/xargs') +pci_src = files( + 'src/pci/pci.c', + 'src/pci/lspci.c', + 'src/pci/setpci.c', +) + +pci_inc = include_directories('src/pci') + gzip_defines = [ '-DNO_BZIP2_SUPPORT', '-DNO_ZSTD_SUPPORT', @@ -128,8 +136,9 @@ vx = executable('vx', find_src, getdate_gen, xargs_src, + pci_src, compat_src, - include_directories : [compat_inc, gzip_inc, grep_inc, find_inc, xargs_inc], + include_directories : [compat_inc, gzip_inc, grep_inc, find_inc, xargs_inc, pci_inc], c_args : compat_args + gzip_defines, dependencies : [libm, libz, liblzma, libacl], install : true, @@ -137,7 +146,7 @@ vx = executable('vx', # Create symlinks for all applets pointing to the vx binary bindir = get_option('bindir') -foreach name : ['patch', 'diff', 'cmp', 'diff3', 'sdiff', 'which', 'gzip', 'gunzip', 'zcat', 'uncompress', 'grep', 'egrep', 'fgrep', 'rgrep', 'find', 'xargs'] +foreach name : ['patch', 'diff', 'cmp', 'diff3', 'sdiff', 'which', 'gzip', 'gunzip', 'zcat', 'uncompress', 'grep', 'egrep', 'fgrep', 'rgrep', 'find', 'xargs', 'lspci', 'setpci'] meson.add_install_script('sh', '-c', 'ln -sf vx "$MESON_INSTALL_DESTDIR_PREFIX/' + bindir + '/' + name + '"', ) @@ -161,6 +170,8 @@ install_man( 'src/grep/grep.1', 'src/find/find.1', 'src/xargs/xargs.1', + 'src/pci/lspci.8', + 'src/pci/setpci.8', ) # --- Gzip shell script utilities --- @@ -182,7 +193,7 @@ endforeach test_env = environment() test_env.set('VX', vx.full_path()) -foreach t : ['test_patch', 'test_diff', 'test_cmp', 'test_diff3', 'test_sdiff', 'test_which', 'test_gzip', 'test_grep', 'test_find', 'test_xargs'] +foreach t : ['test_patch', 'test_diff', 'test_cmp', 'test_diff3', 'test_sdiff', 'test_which', 'test_gzip', 'test_grep', 'test_find', 'test_xargs', 'test_lspci', 'test_setpci'] test(t, find_program('tests/' + t + '.sh'), env : test_env, depends : vx, diff --git a/src/main.c b/src/main.c index 3cd8188..161045a 100644 --- a/src/main.c +++ b/src/main.c @@ -13,6 +13,8 @@ int gzip_main(int argc, char *argv[]); int grep_main(int argc, char *argv[]); int find_main(int argc, char *argv[]); int xargs_main(int argc, char *argv[]); +int lspci_main(int argc, char *argv[]); +int setpci_main(int argc, char *argv[]); struct applet { const char *name; @@ -37,6 +39,8 @@ static const struct applet applets[] = { { "rgrep", grep_main }, { "find", find_main }, { "xargs", xargs_main }, + { "lspci", lspci_main }, + { "setpci", setpci_main }, }; static const size_t n_applets = sizeof(applets) / sizeof(applets[0]); diff --git a/src/pci/lspci.8 b/src/pci/lspci.8 new file mode 100644 index 0000000..cc72e31 --- /dev/null +++ b/src/pci/lspci.8 @@ -0,0 +1,43 @@ +.Dd June 7, 2026 +.Dt LSPCI 8 +.Os +.Sh NAME +.Nm lspci +.Nd list PCI devices using Linux sysfs +.Sh SYNOPSIS +.Nm +.Op Fl Dknvx +.Op Fl d Ar vendor:device[:class] +.Op Fl i Ar file +.Op Fl s Ar slot +.Sh DESCRIPTION +.Nm +lists PCI devices discovered under +.Pa /sys/bus/pci/devices . +It resolves names from +.Pa pci.ids +when an ID database is available. +.Sh OPTIONS +.Bl -tag -width indent +.It Fl D +Always print the PCI domain. +.It Fl d Ar vendor:device[:class] +Filter devices by numeric IDs. +.It Fl i Ar file +Use an alternate +.Pa pci.ids +database. +.It Fl k +Show the kernel driver in use. +.It Fl n +Print numeric identifiers. +Repeat to print both names and numbers. +.It Fl s Ar slot +Filter by slot in +.Ar [domain:]bus:slot[.func] +form. +.It Fl v +Show additional device details. +.It Fl x +Dump the first 64 bytes of configuration space. +.El diff --git a/src/pci/lspci.c b/src/pci/lspci.c new file mode 100644 index 0000000..4e217d8 --- /dev/null +++ b/src/pci/lspci.c @@ -0,0 +1,242 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pci.h" + +static void __dead2 +usage(void) +{ + fprintf(stderr, + "usage: lspci [-Dknvx] [-d vendor:device[:class]] [-i file] [-s slot]\n"); + exit(1); +} + +static void +format_addr(const struct vx_pci_device *dev, bool show_domain, char *buf, + size_t buflen) +{ + if (show_domain || dev->domain != 0) { + snprintf(buf, buflen, "%04x:%02x:%02x.%u", dev->domain, dev->bus, + dev->slot, dev->func); + } else { + snprintf(buf, buflen, "%02x:%02x.%u", dev->bus, dev->slot, + dev->func); + } +} + +static void +format_class_text(const char *ids_path, const struct vx_pci_device *dev, + int numeric_mode, char *buf, size_t buflen) +{ + char class_name[128]; + + if (numeric_mode == 1) { + snprintf(buf, buflen, "%04x", (unsigned int)(dev->class_code >> 8)); + return; + } + if (vx_pci_lookup_class_name(ids_path, dev->class_code, class_name, + sizeof(class_name)) != 0) + strlcpy(class_name, "Unclassified device", sizeof(class_name)); + if (numeric_mode == 2) + snprintf(buf, buflen, "%s [%04x]", class_name, + (unsigned int)(dev->class_code >> 8)); + else + strlcpy(buf, class_name, buflen); +} + +static void +format_device_text(const char *ids_path, const struct vx_pci_device *dev, + int numeric_mode, char *buf, size_t buflen) +{ + char vendor_name[128]; + char device_name[192]; + int have_vendor, have_device; + + if (numeric_mode == 1) { + snprintf(buf, buflen, "%04x:%04x", dev->vendor_id, dev->device_id); + return; + } + + have_vendor = vx_pci_lookup_vendor_name(ids_path, dev->vendor_id, + vendor_name, sizeof(vendor_name)) == 0; + have_device = vx_pci_lookup_device_name(ids_path, dev->vendor_id, + dev->device_id, device_name, sizeof(device_name)) == 0; + + if (!have_vendor) + snprintf(vendor_name, sizeof(vendor_name), "Vendor %04x", + dev->vendor_id); + if (!have_device) + snprintf(device_name, sizeof(device_name), "Device %04x", + dev->device_id); + + if (numeric_mode == 2) { + snprintf(buf, buflen, "%s %s [%04x:%04x]", vendor_name, + device_name, dev->vendor_id, dev->device_id); + } else { + snprintf(buf, buflen, "%s %s", vendor_name, device_name); + } +} + +static void +format_subsystem_text(const char *ids_path, const struct vx_pci_device *dev, + char *buf, size_t buflen) +{ + char subsystem_name[192]; + + if (dev->subsystem_vendor_id == 0 && dev->subsystem_device_id == 0) { + buf[0] = '\0'; + return; + } + if (vx_pci_lookup_subsystem_name(ids_path, dev->vendor_id, dev->device_id, + dev->subsystem_vendor_id, dev->subsystem_device_id, subsystem_name, + sizeof(subsystem_name)) == 0) { + strlcpy(buf, subsystem_name, buflen); + return; + } + snprintf(buf, buflen, "%04x:%04x", dev->subsystem_vendor_id, + dev->subsystem_device_id); +} + +static void +print_hex_dump(const struct vx_pci_device *dev) +{ + size_t offset, limit, i; + + limit = dev->config_len; + if (limit > 64) + limit = 64; + for (offset = 0; offset < limit; offset += 16) { + printf("\t%02zx:", offset); + for (i = 0; i < 16 && offset + i < limit; i++) + printf(" %02x", dev->config[offset + i]); + putchar('\n'); + } +} + +static void +print_verbose(const char *ids_path, const struct vx_pci_device *dev, + bool show_driver) +{ + char subsystem[192]; + char speed[64]; + char width[64]; + + format_subsystem_text(ids_path, dev, subsystem, sizeof(subsystem)); + if (subsystem[0] != '\0') + printf("\tSubsystem: %s\n", subsystem); + if (dev->irq > 0) + printf("\tIRQ: %d\n", dev->irq); + if (dev->numa_node >= 0) + printf("\tNUMA node: %d\n", dev->numa_node); + if ((show_driver || dev->driver[0] != '\0') && dev->driver[0] != '\0') + printf("\tKernel driver in use: %s\n", dev->driver); + + if (vx_pci_read_attribute(dev, "current_link_speed", + speed, sizeof(speed)) == 0) { + if (vx_pci_read_attribute(dev, "current_link_width", + width, sizeof(width)) == 0) + printf("\tLnkSta: Speed %s, Width %s\n", speed, width); + } +} + +int +lspci_main(int argc, char *argv[]) +{ + struct vx_pci_device_list list; + struct vx_pci_slot_filter slot_filter; + struct vx_pci_id_filter id_filter; + const char *ids_path; + int ch; + bool show_domain, show_driver, show_hex, verbose; + int numeric_mode; + int exitcode; + + memset(&slot_filter, 0, sizeof(slot_filter)); + memset(&id_filter, 0, sizeof(id_filter)); + show_domain = false; + show_driver = false; + show_hex = false; + verbose = false; + numeric_mode = 0; + ids_path = vx_pci_default_ids_path(); + + optind = 1; + while ((ch = getopt(argc, argv, "Dd:i:kns:vx")) != -1) { + switch (ch) { + case 'D': + show_domain = true; + break; + case 'd': + if (vx_pci_parse_id_filter(optarg, &id_filter) != 0) + errx(1, "invalid device selector '%s'", optarg); + break; + case 'i': + ids_path = optarg; + break; + case 'k': + show_driver = true; + break; + case 'n': + numeric_mode++; + if (numeric_mode > 2) + numeric_mode = 2; + break; + case 's': + if (vx_pci_parse_slot_filter(optarg, &slot_filter) != 0) + errx(1, "invalid slot selector '%s'", optarg); + break; + case 'v': + verbose = true; + break; + case 'x': + show_hex = true; + break; + default: + usage(); + } + } + if (optind != argc) + usage(); + + if (vx_pci_enumerate_devices(&list) != 0) + err(1, "failed to enumerate PCI devices"); + + exitcode = 0; + for (size_t i = 0; i < list.count; i++) { + struct vx_pci_device *dev; + char addr[16]; + char class_text[160]; + char device_text[320]; + + dev = &list.devices[i]; + if (!vx_pci_slot_matches(dev, &slot_filter) || + !vx_pci_id_matches(dev, &id_filter)) + continue; + + format_addr(dev, show_domain, addr, sizeof(addr)); + format_class_text(ids_path, dev, numeric_mode, class_text, + sizeof(class_text)); + format_device_text(ids_path, dev, numeric_mode, device_text, + sizeof(device_text)); + + printf("%s %s: %s", addr, class_text, device_text); + if (dev->revision != 0 || dev->class_code != 0) + printf(" (rev %02x)", dev->revision); + putchar('\n'); + if (verbose || show_driver) + print_verbose(ids_path, dev, show_driver); + if (show_hex) + print_hex_dump(dev); + } + + vx_pci_free_devices(&list); + return exitcode; +} diff --git a/src/pci/pci.c b/src/pci/pci.c new file mode 100644 index 0000000..0f8aa28 --- /dev/null +++ b/src/pci/pci.c @@ -0,0 +1,723 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pci.h" + +#define VX_PCI_SYSFS_ROOT "/sys/bus/pci/devices" + +static int +vx_parse_hex_component(const char *text, unsigned long max, int *value) +{ + char *end; + unsigned long parsed; + + errno = 0; + parsed = strtoul(text, &end, 16); + if (errno != 0 || *text == '\0' || *end != '\0' || parsed > max) + return -1; + *value = (int)parsed; + return 0; +} + +static int +vx_read_trimmed_file(const char *path, char *buf, size_t buflen) +{ + FILE *fp; + size_t len; + + fp = fopen(path, "r"); + if (fp == NULL) + return -1; + if (fgets(buf, (int)buflen, fp) == NULL) { + fclose(fp); + return -1; + } + fclose(fp); + + len = strlen(buf); + while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) { + buf[len - 1] = '\0'; + len--; + } + return 0; +} + +int +vx_pci_read_attribute(const struct vx_pci_device *dev, const char *name, + char *buf, size_t buflen) +{ + int dirfd, fd; + FILE *fp; + size_t len; + + dirfd = open(dev->sysfs_path, O_RDONLY | O_DIRECTORY); + if (dirfd < 0) + return -1; + fd = openat(dirfd, name, O_RDONLY); + close(dirfd); + if (fd < 0) + return -1; + fp = fdopen(fd, "r"); + if (fp == NULL) { + close(fd); + return -1; + } + if (fgets(buf, (int)buflen, fp) == NULL) { + fclose(fp); + return -1; + } + fclose(fp); + + len = strlen(buf); + while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) { + buf[len - 1] = '\0'; + len--; + } + return 0; +} + +static int +vx_read_hex_file(const char *dir, const char *name, unsigned long max, + unsigned long *value) +{ + char path[PATH_MAX]; + char buf[64]; + char *end; + + snprintf(path, sizeof(path), "%s/%s", dir, name); + if (vx_read_trimmed_file(path, buf, sizeof(buf)) != 0) + return -1; + + errno = 0; + *value = strtoul(buf, &end, 0); + if (errno != 0 || *buf == '\0' || *end != '\0' || *value > max) + return -1; + return 0; +} + +static int +vx_read_int_file(const char *dir, const char *name, int *value) +{ + char path[PATH_MAX]; + char buf[64]; + char *end; + long parsed; + + snprintf(path, sizeof(path), "%s/%s", dir, name); + if (vx_read_trimmed_file(path, buf, sizeof(buf)) != 0) + return -1; + + errno = 0; + parsed = strtol(buf, &end, 0); + if (errno != 0 || *buf == '\0' || *end != '\0') + return -1; + *value = (int)parsed; + return 0; +} + +static void +vx_read_driver_name(const char *dir, char *buf, size_t buflen) +{ + char path[PATH_MAX]; + char linkbuf[PATH_MAX]; + ssize_t len; + char *base; + + buf[0] = '\0'; + snprintf(path, sizeof(path), "%s/driver", dir); + len = readlink(path, linkbuf, sizeof(linkbuf) - 1); + if (len < 0) + return; + linkbuf[len] = '\0'; + base = strrchr(linkbuf, '/'); + if (base == NULL) + base = linkbuf; + else + base++; + strlcpy(buf, base, buflen); +} + +static int +vx_load_config_space(const char *dir, struct vx_pci_device *dev) +{ + char path[PATH_MAX]; + int fd; + ssize_t nread; + + snprintf(path, sizeof(path), "%s/config", dir); + fd = open(path, O_RDONLY); + if (fd < 0) + return -1; + nread = pread(fd, dev->config, sizeof(dev->config), 0); + close(fd); + if (nread < 0) + return -1; + dev->config_len = (size_t)nread; + return 0; +} + +static int +vx_pci_addr_cmp(const void *lhs, const void *rhs) +{ + const struct vx_pci_device *left; + const struct vx_pci_device *right; + + left = lhs; + right = rhs; + return strcmp(left->addr, right->addr); +} + +int +vx_pci_enumerate_devices(struct vx_pci_device_list *list) +{ + DIR *dirp; + struct dirent *dp; + struct vx_pci_device *devices; + size_t count, capacity; + + memset(list, 0, sizeof(*list)); + dirp = opendir(VX_PCI_SYSFS_ROOT); + if (dirp == NULL) + return -1; + + devices = NULL; + count = 0; + capacity = 0; + while ((dp = readdir(dirp)) != NULL) { + struct vx_pci_device dev; + unsigned long value; + struct stat st; + int domain, bus, slot, func; + + if (dp->d_name[0] == '.') + continue; + if (sscanf(dp->d_name, "%x:%x:%x.%x", + &domain, &bus, &slot, &func) != 4) + continue; + + memset(&dev, 0, sizeof(dev)); + strlcpy(dev.addr, dp->d_name, sizeof(dev.addr)); + snprintf(dev.sysfs_path, sizeof(dev.sysfs_path), "%s/%s", + VX_PCI_SYSFS_ROOT, dp->d_name); + if (stat(dev.sysfs_path, &st) != 0 || !S_ISDIR(st.st_mode)) + continue; + + dev.domain = (uint16_t)domain; + dev.bus = (uint8_t)bus; + dev.slot = (uint8_t)slot; + dev.func = (uint8_t)func; + dev.irq = -1; + dev.numa_node = -1; + + if (vx_read_hex_file(dev.sysfs_path, "vendor", 0xffff, &value) == 0) + dev.vendor_id = (uint16_t)value; + if (vx_read_hex_file(dev.sysfs_path, "device", 0xffff, &value) == 0) + dev.device_id = (uint16_t)value; + if (vx_read_hex_file(dev.sysfs_path, "subsystem_vendor", 0xffff, + &value) == 0) + dev.subsystem_vendor_id = (uint16_t)value; + if (vx_read_hex_file(dev.sysfs_path, "subsystem_device", 0xffff, + &value) == 0) + dev.subsystem_device_id = (uint16_t)value; + if (vx_read_int_file(dev.sysfs_path, "irq", &dev.irq) != 0) + dev.irq = -1; + if (vx_read_int_file(dev.sysfs_path, "numa_node", + &dev.numa_node) != 0) + dev.numa_node = -1; + vx_read_driver_name(dev.sysfs_path, dev.driver, sizeof(dev.driver)); + (void)vx_load_config_space(dev.sysfs_path, &dev); + if (dev.config_len >= 0x10) { + dev.revision = dev.config[0x08]; + dev.class_code = ((uint32_t)dev.config[0x0b] << 16) | + ((uint32_t)dev.config[0x0a] << 8) | dev.config[0x09]; + dev.header_type = dev.config[0x0e]; + } + + if (count == capacity) { + size_t newcap; + struct vx_pci_device *newdevs; + + newcap = (capacity == 0) ? 16 : capacity * 2; + newdevs = reallocarray(devices, newcap, sizeof(*newdevs)); + if (newdevs == NULL) { + free(devices); + closedir(dirp); + return -1; + } + devices = newdevs; + capacity = newcap; + } + devices[count++] = dev; + } + closedir(dirp); + + qsort(devices, count, sizeof(*devices), vx_pci_addr_cmp); + list->devices = devices; + list->count = count; + return 0; +} + +void +vx_pci_free_devices(struct vx_pci_device_list *list) +{ + free(list->devices); + list->devices = NULL; + list->count = 0; +} + +int +vx_pci_parse_slot_filter(const char *spec, struct vx_pci_slot_filter *filter) +{ + char *copy, *last, *middle; + + memset(filter, 0, sizeof(*filter)); + filter->domain = -1; + filter->bus = -1; + filter->slot = -1; + filter->func = -1; + if (spec == NULL || *spec == '\0') + return -1; + + copy = strdup(spec); + if (copy == NULL) + return -1; + + last = strchr(copy, '.'); + if (last != NULL) { + *last++ = '\0'; + if (vx_parse_hex_component(last, 7, &filter->func) != 0) { + free(copy); + return -1; + } + } + + last = strrchr(copy, ':'); + if (last != NULL) { + *last++ = '\0'; + if (vx_parse_hex_component(last, 0x1f, &filter->slot) != 0) { + free(copy); + return -1; + } + middle = strrchr(copy, ':'); + if (middle != NULL) { + *middle++ = '\0'; + if (vx_parse_hex_component(middle, 0xff, + &filter->bus) != 0 || + vx_parse_hex_component(copy, 0xffff, + &filter->domain) != 0) { + free(copy); + return -1; + } + } else { + if (vx_parse_hex_component(copy, 0xff, + &filter->bus) != 0) { + free(copy); + return -1; + } + } + } else { + if (vx_parse_hex_component(copy, 0x1f, &filter->slot) != 0) { + free(copy); + return -1; + } + } + + filter->has_filter = true; + free(copy); + return 0; +} + +int +vx_pci_parse_id_filter(const char *spec, struct vx_pci_id_filter *filter) +{ + char *copy, *vendor, *device, *klass, *next; + + memset(filter, 0, sizeof(*filter)); + filter->vendor = -1; + filter->device = -1; + filter->class_value = -1; + filter->class_digits = 0; + if (spec == NULL || *spec == '\0') + return -1; + + copy = strdup(spec); + if (copy == NULL) + return -1; + + vendor = copy; + next = strchr(vendor, ':'); + if (next == NULL) { + free(copy); + return -1; + } + *next++ = '\0'; + device = next; + klass = strchr(device, ':'); + if (klass != NULL) + *klass++ = '\0'; + + if (*vendor != '\0' && + vx_parse_hex_component(vendor, 0xffff, &filter->vendor) != 0) { + free(copy); + return -1; + } + if (*device != '\0' && + vx_parse_hex_component(device, 0xffff, &filter->device) != 0) { + free(copy); + return -1; + } + if (klass != NULL && *klass != '\0') { + if (vx_parse_hex_component(klass, 0xffffff, + &filter->class_value) != 0) { + free(copy); + return -1; + } + filter->class_digits = (int)strlen(klass); + if (filter->class_digits != 4 && filter->class_digits != 6) { + free(copy); + return -1; + } + } + + filter->has_filter = true; + free(copy); + return 0; +} + +bool +vx_pci_slot_matches(const struct vx_pci_device *dev, + const struct vx_pci_slot_filter *filter) +{ + if (!filter->has_filter) + return true; + if (filter->domain >= 0 && dev->domain != (uint16_t)filter->domain) + return false; + if (filter->bus >= 0 && dev->bus != (uint8_t)filter->bus) + return false; + if (filter->slot >= 0 && dev->slot != (uint8_t)filter->slot) + return false; + if (filter->func >= 0 && dev->func != (uint8_t)filter->func) + return false; + return true; +} + +bool +vx_pci_id_matches(const struct vx_pci_device *dev, + const struct vx_pci_id_filter *filter) +{ + if (!filter->has_filter) + return true; + if (filter->vendor >= 0 && dev->vendor_id != (uint16_t)filter->vendor) + return false; + if (filter->device >= 0 && dev->device_id != (uint16_t)filter->device) + return false; + if (filter->class_value >= 0) { + if (filter->class_digits == 4) { + if ((int)(dev->class_code >> 8) != filter->class_value) + return false; + } else if ((int)dev->class_code != filter->class_value) { + return false; + } + } + return true; +} + +const char * +vx_pci_default_ids_path(void) +{ + static const char *const candidates[] = { + "/usr/share/hwdata/pci.ids", + "/usr/share/misc/pci.ids", + "/usr/share/pci.ids", + }; + size_t i; + + for (i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) { + if (access(candidates[i], R_OK) == 0) + return candidates[i]; + } + return NULL; +} + +static void +vx_copy_name(char *dst, size_t dstlen, const char *src) +{ + size_t len; + + while (*src == ' ' || *src == '\t') + src++; + len = strcspn(src, "\r\n"); + if (len >= dstlen) + len = dstlen - 1; + memcpy(dst, src, len); + dst[len] = '\0'; +} + +int +vx_pci_lookup_vendor_name(const char *ids_path, uint16_t vendor_id, + char *buf, size_t buflen) +{ + FILE *fp; + char line[512]; + unsigned int parsed; + + if (ids_path == NULL) + return -1; + fp = fopen(ids_path, "r"); + if (fp == NULL) + return -1; + while (fgets(line, sizeof(line), fp) != NULL) { + if (line[0] == '#' || line[0] == '\n' || line[0] == 'C' || + line[0] == '\t') + continue; + if (sscanf(line, "%x", &parsed) == 1 && parsed == vendor_id) { + char *name = strchr(line, ' '); + + if (name == NULL) + continue; + vx_copy_name(buf, buflen, name); + fclose(fp); + return 0; + } + } + fclose(fp); + return -1; +} + +int +vx_pci_lookup_device_name(const char *ids_path, uint16_t vendor_id, + uint16_t device_id, char *buf, size_t buflen) +{ + FILE *fp; + char line[512]; + unsigned int parsed_vendor, parsed_device; + bool in_vendor; + + if (ids_path == NULL) + return -1; + fp = fopen(ids_path, "r"); + if (fp == NULL) + return -1; + + in_vendor = false; + while (fgets(line, sizeof(line), fp) != NULL) { + if (line[0] == '#' || line[0] == '\n') + continue; + if (line[0] == 'C') { + in_vendor = false; + continue; + } + if (line[0] != '\t') { + in_vendor = (sscanf(line, "%x", &parsed_vendor) == 1 && + parsed_vendor == vendor_id); + continue; + } + if (!in_vendor || line[1] == '\t') + continue; + if (sscanf(line + 1, "%x", &parsed_device) == 1 && + parsed_device == device_id) { + char *name = strchr(line + 1, ' '); + + if (name == NULL) + continue; + vx_copy_name(buf, buflen, name); + fclose(fp); + return 0; + } + } + fclose(fp); + return -1; +} + +int +vx_pci_lookup_subsystem_name(const char *ids_path, uint16_t vendor_id, + uint16_t device_id, uint16_t subsystem_vendor_id, + uint16_t subsystem_device_id, char *buf, size_t buflen) +{ + FILE *fp; + char line[512]; + unsigned int parsed_vendor, parsed_device, parsed_subvendor, parsed_subdevice; + bool in_vendor, in_device; + + if (ids_path == NULL) + return -1; + fp = fopen(ids_path, "r"); + if (fp == NULL) + return -1; + + in_vendor = false; + in_device = false; + while (fgets(line, sizeof(line), fp) != NULL) { + if (line[0] == '#' || line[0] == '\n') + continue; + if (line[0] == 'C') { + in_vendor = false; + in_device = false; + continue; + } + if (line[0] != '\t') { + in_vendor = (sscanf(line, "%x", &parsed_vendor) == 1 && + parsed_vendor == vendor_id); + in_device = false; + continue; + } + if (line[1] != '\t') { + in_device = false; + if (in_vendor && + sscanf(line + 1, "%x", &parsed_device) == 1 && + parsed_device == device_id) + in_device = true; + continue; + } + if (!in_vendor || !in_device) + continue; + if (sscanf(line + 2, "%x %x", &parsed_subvendor, + &parsed_subdevice) == 2 && + parsed_subvendor == subsystem_vendor_id && + parsed_subdevice == subsystem_device_id) { + char *name = strchr(line + 2, ' '); + + if (name == NULL) + continue; + name = strchr(name + 1, ' '); + if (name == NULL) + continue; + vx_copy_name(buf, buflen, name); + fclose(fp); + return 0; + } + } + fclose(fp); + return -1; +} + +int +vx_pci_lookup_class_name(const char *ids_path, uint32_t class_code, + char *buf, size_t buflen) +{ + FILE *fp; + char line[512]; + unsigned int base_class, subclass, parsed; + bool in_class; + char base_name[128]; + + if (ids_path == NULL) + return -1; + fp = fopen(ids_path, "r"); + if (fp == NULL) + return -1; + + base_class = (class_code >> 16) & 0xff; + subclass = (class_code >> 8) & 0xff; + in_class = false; + base_name[0] = '\0'; + while (fgets(line, sizeof(line), fp) != NULL) { + if (line[0] == '#' || line[0] == '\n') + continue; + if (line[0] == 'C' && line[1] == ' ') { + if (sscanf(line + 2, "%x", &parsed) == 1 && + parsed == base_class) { + char *name = strchr(line + 2, ' '); + + in_class = true; + if (name != NULL) + vx_copy_name(base_name, + sizeof(base_name), name); + } else { + in_class = false; + } + continue; + } + if (!in_class || line[0] != '\t' || line[1] == '\t') + continue; + if (sscanf(line + 1, "%x", &parsed) == 1 && parsed == subclass) { + char *name = strchr(line + 1, ' '); + + if (name == NULL) + continue; + vx_copy_name(buf, buflen, name); + fclose(fp); + return 0; + } + } + fclose(fp); + if (base_name[0] == '\0') + return -1; + strlcpy(buf, base_name, buflen); + return 0; +} + +int +vx_pci_read_config(const struct vx_pci_device *dev, uint16_t offset, + unsigned int width, uint32_t *value) +{ + uint32_t result; + + if (width != 1 && width != 2 && width != 4) + return -1; + if ((size_t)offset + width > dev->config_len) + return -1; + + result = 0; + switch (width) { + case 1: + result = dev->config[offset]; + break; + case 2: + result = (uint32_t)dev->config[offset] | + ((uint32_t)dev->config[offset + 1] << 8); + break; + case 4: + result = (uint32_t)dev->config[offset] | + ((uint32_t)dev->config[offset + 1] << 8) | + ((uint32_t)dev->config[offset + 2] << 16) | + ((uint32_t)dev->config[offset + 3] << 24); + break; + } + *value = result; + return 0; +} + +int +vx_pci_write_config(struct vx_pci_device *dev, uint16_t offset, + unsigned int width, uint32_t value, uint32_t mask) +{ + uint8_t bytes[4]; + uint32_t current; + uint32_t merged; + int dirfd, fd; + ssize_t nwritten; + unsigned int i; + + if (vx_pci_read_config(dev, offset, width, ¤t) != 0) + return -1; + + merged = (current & ~mask) | (value & mask); + for (i = 0; i < width; i++) + bytes[i] = (merged >> (i * 8)) & 0xff; + + dirfd = open(dev->sysfs_path, O_RDONLY | O_DIRECTORY); + if (dirfd < 0) + return -1; + fd = openat(dirfd, "config", O_RDWR); + close(dirfd); + if (fd < 0) + return -1; + nwritten = pwrite(fd, bytes, width, offset); + close(fd); + if (nwritten != (ssize_t)width) + return -1; + + for (i = 0; i < width; i++) + dev->config[offset + i] = bytes[i]; + return 0; +} diff --git a/src/pci/pci.h b/src/pci/pci.h new file mode 100644 index 0000000..19c78b8 --- /dev/null +++ b/src/pci/pci.h @@ -0,0 +1,81 @@ +#ifndef VX_PCI_H +#define VX_PCI_H + +#include +#include +#include +#include + +#define VX_PCI_CONFIG_SPACE_SIZE 4096 + +struct vx_pci_slot_filter { + bool has_filter; + int domain; + int bus; + int slot; + int func; +}; + +struct vx_pci_id_filter { + bool has_filter; + int vendor; + int device; + int class_value; + int class_digits; +}; + +struct vx_pci_device { + char addr[16]; + char sysfs_path[PATH_MAX]; + uint16_t domain; + uint8_t bus; + uint8_t slot; + uint8_t func; + uint16_t vendor_id; + uint16_t device_id; + uint16_t subsystem_vendor_id; + uint16_t subsystem_device_id; + uint32_t class_code; + uint8_t revision; + uint8_t header_type; + int irq; + int numa_node; + size_t config_len; + uint8_t config[VX_PCI_CONFIG_SPACE_SIZE]; + char driver[128]; +}; + +struct vx_pci_device_list { + struct vx_pci_device *devices; + size_t count; +}; + +int vx_pci_enumerate_devices(struct vx_pci_device_list *list); +void vx_pci_free_devices(struct vx_pci_device_list *list); + +int vx_pci_parse_slot_filter(const char *spec, struct vx_pci_slot_filter *filter); +int vx_pci_parse_id_filter(const char *spec, struct vx_pci_id_filter *filter); +bool vx_pci_slot_matches(const struct vx_pci_device *dev, + const struct vx_pci_slot_filter *filter); +bool vx_pci_id_matches(const struct vx_pci_device *dev, + const struct vx_pci_id_filter *filter); + +const char *vx_pci_default_ids_path(void); +int vx_pci_lookup_vendor_name(const char *ids_path, uint16_t vendor_id, + char *buf, size_t buflen); +int vx_pci_lookup_device_name(const char *ids_path, uint16_t vendor_id, + uint16_t device_id, char *buf, size_t buflen); +int vx_pci_lookup_subsystem_name(const char *ids_path, uint16_t vendor_id, + uint16_t device_id, uint16_t subsystem_vendor_id, + uint16_t subsystem_device_id, char *buf, size_t buflen); +int vx_pci_lookup_class_name(const char *ids_path, uint32_t class_code, + char *buf, size_t buflen); + +int vx_pci_read_attribute(const struct vx_pci_device *dev, const char *name, + char *buf, size_t buflen); +int vx_pci_read_config(const struct vx_pci_device *dev, uint16_t offset, + unsigned int width, uint32_t *value); +int vx_pci_write_config(struct vx_pci_device *dev, uint16_t offset, + unsigned int width, uint32_t value, uint32_t mask); + +#endif diff --git a/src/pci/setpci.8 b/src/pci/setpci.8 new file mode 100644 index 0000000..ab6d326 --- /dev/null +++ b/src/pci/setpci.8 @@ -0,0 +1,36 @@ +.Dd June 7, 2026 +.Dt SETPCI 8 +.Os +.Sh NAME +.Nm setpci +.Nd inspect and modify PCI configuration space via Linux sysfs +.Sh SYNOPSIS +.Nm +.Op Fl Dfvr +.Op Fl d Ar vendor:device[:class] +.Op Fl s Ar slot +.Ar reg Ns Op = Ns Ar value Ns Op :mask +.Sh DESCRIPTION +.Nm +reads or writes PCI configuration registers by operating on +.Pa /sys/bus/pci/devices/*/config . +.Sh OPTIONS +.Bl -tag -width indent +.It Fl D +Dry run. +Report matches without committing writes. +.It Fl d Ar vendor:device[:class] +Filter devices by numeric IDs. +.It Fl f +Do not fail when no device matches. +.It Fl r +Accepted for compatibility. +.It Fl s Ar slot +Filter by slot in +.Ar [domain:]bus:slot[.func] +form. +.It Fl v +Print dry-run write details to standard error. +.It Fl -dumpregs +List supported register names. +.El diff --git a/src/pci/setpci.c b/src/pci/setpci.c new file mode 100644 index 0000000..a248f85 --- /dev/null +++ b/src/pci/setpci.c @@ -0,0 +1,338 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pci.h" + +struct reg_def { + const char *name; + uint16_t offset; + unsigned int width; +}; + +static const struct reg_def reg_defs[] = { + { "VENDOR_ID", 0x00, 2 }, + { "DEVICE_ID", 0x02, 2 }, + { "COMMAND", 0x04, 2 }, + { "STATUS", 0x06, 2 }, + { "REVISION", 0x08, 1 }, + { "CLASS_PROG", 0x09, 1 }, + { "CLASS_DEVICE", 0x0a, 2 }, + { "CACHE_LINE_SIZE", 0x0c, 1 }, + { "LATENCY_TIMER", 0x0d, 1 }, + { "HEADER_TYPE", 0x0e, 1 }, + { "BIST", 0x0f, 1 }, + { "BASE_ADDRESS_0", 0x10, 4 }, + { "BASE_ADDRESS_1", 0x14, 4 }, + { "BASE_ADDRESS_2", 0x18, 4 }, + { "BASE_ADDRESS_3", 0x1c, 4 }, + { "BASE_ADDRESS_4", 0x20, 4 }, + { "BASE_ADDRESS_5", 0x24, 4 }, + { "SUBSYSTEM_VENDOR_ID", 0x2c, 2 }, + { "SUBSYSTEM_ID", 0x2e, 2 }, + { "CAPABILITIES", 0x34, 1 }, + { "INTERRUPT_LINE", 0x3c, 1 }, + { "INTERRUPT_PIN", 0x3d, 1 }, + { "IRQ_LINE", 0x3c, 1 }, +}; + +static const struct option long_options[] = { + { "dumpregs", no_argument, NULL, 1 }, + { NULL, 0, NULL, 0 }, +}; + +struct reg_op { + uint16_t offset; + unsigned int width; + bool is_write; + uint32_t value; + uint32_t mask; +}; + +static void __dead2 +usage(void) +{ + fprintf(stderr, + "usage: setpci [-Dfvr] [-d vendor:device[:class]] [-s slot] reg[=value[:mask]] ...\n"); + exit(1); +} + +static void +dumpregs(void) +{ + size_t i; + + printf("cap pos w name\n"); + for (i = 0; i < sizeof(reg_defs) / sizeof(reg_defs[0]); i++) { + printf(" %02x %c %s\n", reg_defs[i].offset, + reg_defs[i].width == 1 ? 'B' : + reg_defs[i].width == 2 ? 'W' : 'L', + reg_defs[i].name); + } +} + +static const struct reg_def * +find_reg_def(const char *name) +{ + size_t i; + + for (i = 0; i < sizeof(reg_defs) / sizeof(reg_defs[0]); i++) { + if (strcasecmp(reg_defs[i].name, name) == 0) + return ®_defs[i]; + } + return NULL; +} + +static int +parse_width_char(char width_char, unsigned int *width) +{ + switch (width_char) { + case 'B': + case 'b': + *width = 1; + return 0; + case 'W': + case 'w': + *width = 2; + return 0; + case 'L': + case 'l': + *width = 4; + return 0; + default: + return -1; + } +} + +static int +parse_reg_spec(const char *text, struct reg_op *op) +{ + char *copy, *base, *eq, *plus, *dot, *mask; + const struct reg_def *def; + char *end; + unsigned long parsed; + + memset(op, 0, sizeof(*op)); + copy = strdup(text); + if (copy == NULL) + return -1; + + eq = strchr(copy, '='); + if (eq != NULL) + *eq++ = '\0'; + base = copy; + + dot = strrchr(base, '.'); + if (dot != NULL) { + *dot++ = '\0'; + if (parse_width_char(*dot, &op->width) != 0) { + free(copy); + return -1; + } + } + + plus = strrchr(base, '+'); + if (plus != NULL) + *plus++ = '\0'; + + def = find_reg_def(base); + if (def != NULL) { + op->offset = def->offset; + if (op->width == 0) + op->width = def->width; + } else { + errno = 0; + parsed = strtoul(base, &end, 16); + if (errno != 0 || *base == '\0' || *end != '\0' || parsed > 0xfff) { + free(copy); + return -1; + } + op->offset = (uint16_t)parsed; + if (op->width == 0) + op->width = 2; + } + + if (plus != NULL) { + errno = 0; + parsed = strtoul(plus, &end, 16); + if (errno != 0 || *plus == '\0' || *end != '\0' || + parsed > 0xfff) { + free(copy); + return -1; + } + op->offset += (uint16_t)parsed; + } + + if (eq != NULL) { + op->is_write = true; + mask = strchr(eq, ':'); + if (mask != NULL) + *mask++ = '\0'; + + errno = 0; + parsed = strtoul(eq, &end, 16); + if (errno != 0 || *eq == '\0' || *end != '\0') { + free(copy); + return -1; + } + op->value = (uint32_t)parsed; + if (mask != NULL) { + errno = 0; + parsed = strtoul(mask, &end, 16); + if (errno != 0 || *mask == '\0' || *end != '\0') { + free(copy); + return -1; + } + op->mask = (uint32_t)parsed; + } else if (op->width == 1) { + op->mask = 0xff; + } else if (op->width == 2) { + op->mask = 0xffff; + } else { + op->mask = 0xffffffffU; + } + } + + free(copy); + return 0; +} + +static void +print_value(uint32_t value, unsigned int width) +{ + if (width == 1) + printf("%02x\n", value & 0xff); + else if (width == 2) + printf("%04x\n", value & 0xffff); + else + printf("%08x\n", value); +} + +int +setpci_main(int argc, char *argv[]) +{ + struct vx_pci_device_list list; + struct vx_pci_slot_filter global_slot_filter; + struct vx_pci_slot_filter current_slot_filter; + struct vx_pci_id_filter id_filter; + bool dry_run, verbose, ignore_missing; + int ch; + int exitcode; + size_t i; + + memset(&global_slot_filter, 0, sizeof(global_slot_filter)); + memset(¤t_slot_filter, 0, sizeof(current_slot_filter)); + memset(&id_filter, 0, sizeof(id_filter)); + dry_run = false; + verbose = false; + ignore_missing = false; + + optind = 1; + while ((ch = getopt_long(argc, argv, "Dd:frs:v", + long_options, NULL)) != -1) { + switch (ch) { + case 'D': + dry_run = true; + break; + case 'd': + if (vx_pci_parse_id_filter(optarg, &id_filter) != 0) + errx(1, "invalid device selector '%s'", optarg); + break; + case 'f': + ignore_missing = true; + break; + case 'r': + break; + case 's': + if (vx_pci_parse_slot_filter(optarg, + &global_slot_filter) != 0) + errx(1, "invalid slot selector '%s'", optarg); + current_slot_filter = global_slot_filter; + break; + case 'v': + verbose = true; + break; + case 1: + dumpregs(); + return 0; + default: + usage(); + } + } + if (optind >= argc) + usage(); + + if (vx_pci_enumerate_devices(&list) != 0) + err(1, "failed to enumerate PCI devices"); + + exitcode = 0; + current_slot_filter = global_slot_filter; + for (i = (size_t)optind; i < (size_t)argc; i++) { + struct reg_op op; + char *token; + size_t matched; + + token = argv[i]; + if (token[0] != '\0' && token[strlen(token) - 1] == ':') { + char slotbuf[64]; + + if (strlen(token) >= sizeof(slotbuf)) + errx(1, "device selector too long"); + strlcpy(slotbuf, token, sizeof(slotbuf)); + slotbuf[strlen(slotbuf) - 1] = '\0'; + if (vx_pci_parse_slot_filter(slotbuf, + ¤t_slot_filter) != 0) + errx(1, "invalid device selector '%s'", token); + continue; + } + if (parse_reg_spec(token, &op) != 0) + errx(1, "invalid register spec '%s'", token); + + matched = 0; + for (size_t di = 0; di < list.count; di++) { + struct vx_pci_device *dev; + uint32_t current; + + dev = &list.devices[di]; + if (!vx_pci_slot_matches(dev, ¤t_slot_filter) || + !vx_pci_id_matches(dev, &id_filter)) + continue; + if (vx_pci_read_config(dev, op.offset, op.width, + ¤t) != 0) + continue; + matched++; + if (!op.is_write) { + print_value(current, op.width); + continue; + } + if (dry_run) { + if (verbose) { + uint32_t next; + + next = (current & ~op.mask) | + (op.value & op.mask); + fprintf(stderr, "%s %02x.%c %08x -> %08x\n", + dev->addr, op.offset, + op.width == 1 ? 'B' : + op.width == 2 ? 'W' : 'L', + current, next); + } + continue; + } + if (vx_pci_write_config(dev, op.offset, op.width, op.value, + op.mask) != 0) + err(1, "failed to write %s", dev->addr); + } + if (matched == 0 && !ignore_missing) + errx(1, "no devices matched '%s'", token); + } + + vx_pci_free_devices(&list); + return exitcode; +} diff --git a/tests/test_lspci.sh b/tests/test_lspci.sh new file mode 100644 index 0000000..f59bfe3 --- /dev/null +++ b/tests/test_lspci.sh @@ -0,0 +1,48 @@ +#!/bin/sh +_test_name="lspci" +. "$(dirname "$0")/harness.sh" + +setup_tmpdir +LSPCI="$VX lspci" + +echo "Running lspci tests..." + +dev_path="/sys/bus/pci/devices/$(ls /sys/bus/pci/devices | head -n 1)" +slot="$(basename "$dev_path")" +short_slot="$(printf '%s' "$slot" | sed 's/^0000://')" +vendor="$(sed 's/^0x//' "$dev_path/vendor")" +device="$(sed 's/^0x//' "$dev_path/device")" +class="$(sed 's/^0x//' "$dev_path/class" | cut -c1-4)" + +out="$($LSPCI | sed -n '1p')" +if [ -n "$out" ]; then + pass "basic: lists at least one device" +else + fail "basic: lists at least one device" "got empty output" +fi + +out="$($LSPCI -D -s "$slot")" +case "$out" in + "$slot "*) pass "filter: -D preserves domain-qualified slot" ;; + *) fail "filter: -D preserves domain-qualified slot" "got: $out" ;; +esac + +out="$($LSPCI -n -s "$short_slot")" +expected="$short_slot $class: $vendor:$device" +case "$out" in + "$expected"*) pass "numeric: -n shows class and ids" ;; + *) fail "numeric: -n shows class and ids" "expected prefix: $expected, got: $out" ;; +esac + +out="$($LSPCI -d "$vendor:$device" | sed -n '1p')" +case "$out" in + *"$vendor"*|*"$device"*) pass "filter: -d finds matching device" ;; + "") fail "filter: -d finds matching device" "got empty output" ;; + *) pass "filter: -d finds matching device" ;; +esac + +hex_out="$($LSPCI -x -s "$short_slot")" +printf '%s\n' "$hex_out" > hex.out +check_grep " 00:" hex.out "hex: emits config dump" + +summary diff --git a/tests/test_setpci.sh b/tests/test_setpci.sh new file mode 100644 index 0000000..79fabc6 --- /dev/null +++ b/tests/test_setpci.sh @@ -0,0 +1,33 @@ +#!/bin/sh +_test_name="setpci" +. "$(dirname "$0")/harness.sh" + +setup_tmpdir +SETPCI="$VX setpci" + +echo "Running setpci tests..." + +dev_path="/sys/bus/pci/devices/$(ls /sys/bus/pci/devices | head -n 1)" +slot="$(basename "$dev_path")" +short_slot="$(printf '%s' "$slot" | sed 's/^0000://')" +vendor="$(sed 's/^0x//' "$dev_path/vendor")" +device="$(sed 's/^0x//' "$dev_path/device")" +command_reg="$(od -An -tx2 -N2 -j4 "$dev_path/config" | tr -d ' \n')" + +$SETPCI --dumpregs > dumpregs.out +check_grep "VENDOR_ID" dumpregs.out "dumpregs: lists standard names" + +out="$($SETPCI -s "$short_slot" VENDOR_ID)" +check_eq "$out" "$vendor" "read: VENDOR_ID matches sysfs" + +out="$($SETPCI -s "$short_slot" DEVICE_ID)" +check_eq "$out" "$device" "read: DEVICE_ID matches sysfs" + +out="$($SETPCI -s "$short_slot" 04.w)" +check_eq "$out" "$command_reg" "read: numeric register syntax works" + +check_exit "dry-run: masked write succeeds" 0 $SETPCI -D -s "$short_slot" COMMAND="$command_reg:ffff" +after="$($SETPCI -s "$short_slot" COMMAND)" +check_eq "$after" "$command_reg" "dry-run: command register unchanged" + +summary