diff --git a/lib/system/linux/device.c b/lib/system/linux/device.c index fcd423e0..71090dbb 100644 --- a/lib/system/linux/device.c +++ b/lib/system/linux/device.c @@ -14,9 +14,13 @@ #include #include +#include +#include + #include "irq.h" #define MAX_DRIVERS 64 +#define METAL_UIO_CLASS_PATH "/sys/class/uio" struct linux_bus; struct linux_device; @@ -57,14 +61,37 @@ struct linux_device { struct metal_device device; char dev_name[PATH_MAX]; char dev_path[PATH_MAX]; + /* + * UIO sysfs class directory, such as /sys/class/uio/uio0. UIO map + * attributes are read relative to this path. + */ char cls_path[PATH_MAX]; + char uio_name[PATH_MAX]; + char uio_dev_name[PATH_MAX]; metal_phys_addr_t region_phys[METAL_MAX_DEVICE_REGIONS]; + void *region_map_raw[METAL_MAX_DEVICE_REGIONS]; + size_t region_map_len[METAL_MAX_DEVICE_REGIONS]; struct linux_driver *ldrv; struct sysfs_device *sdev; struct sysfs_attribute *override; int fd; }; +/* + * UIO sysfs reports a full mmap() extent plus a separate offset to the + * usable resource. Keep those inputs together while converting them into the + * libmetal physical address, mmap length, and exported region size. + */ +struct metal_uio_map_info { + const char *dev_name; + metal_phys_addr_t map_addr; + unsigned long map_size; + unsigned long offset; + metal_phys_addr_t *phys; + size_t *map_len; + size_t *region_size; +}; + static struct linux_bus *to_linux_bus(struct metal_bus *bus) { return metal_container_of(bus, struct linux_bus, bus); @@ -75,6 +102,48 @@ static struct linux_device *to_linux_device(struct metal_device *device) return metal_container_of(device, struct linux_device, device); } +/* + * The "uio" bus is synthetic: it has no sysfs bus directory and opens + * devices directly from /sys/class/uio instead of through parent-bus drivers. + */ +static bool metal_linux_is_uio_bus(const struct linux_bus *lbus) +{ + return strcmp(lbus->bus_name, "uio") == 0; +} + +/* + * Read sysfs files whose useful value is the first text line. UIO class-name + * matching needs the newline stripped before comparing against dev_name. + */ +static int metal_linux_read_first_line(const char *path, char *output, + size_t output_len) +{ + FILE *fp; + char *newline; + int result = 0; + + if (!path || !output || output_len < 2) + return -EINVAL; + + fp = fopen(path, "r"); + if (!fp) + return -errno; + + if (!fgets(output, output_len, fp)) { + result = ferror(fp) ? -errno : -ENODATA; + goto close_file; + } + + newline = strchr(output, '\n'); + if (newline) + *newline = '\0'; + +close_file: + fclose(fp); + + return result; +} + static int metal_uio_read_map_attr(struct linux_device *ldev, unsigned int index, const char *name, @@ -100,6 +169,202 @@ static int metal_uio_read_map_attr(struct linux_device *ldev, return 0; } +/* + * Read string-valued UIO sysfs attributes such as /sys/class/uio/uioX/name. + * The value is explicitly terminated so callers can compare it as a C string. + */ +static int metal_uio_read_str_attr(const char *path, char *value, size_t len) +{ + struct sysfs_attribute *attr; + int result = 0; + + if (!value || !len) + return -EINVAL; + + attr = sysfs_open_attribute(path); + if (!attr || sysfs_read_attribute(attr) != 0) { + result = -errno; + goto close_attr; + } + + strncpy(value, attr->value, len - 1); + value[len - 1] = '\0'; + +close_attr: + sysfs_close_attribute(attr); + return result; +} + +/* + * Validate the sysfs map offset before it is applied to the mmap() base. + * The Linux UIO ABI exposes one mmap slot per page-sized index, so the + * per-map offset must stay inside a single host page. + */ +static int metal_linux_uio_validate_offset(const char *dev_name, + unsigned long offset) +{ + const unsigned long page_size = (unsigned long)getpagesize(); + + /* + * The offset is applied inside one page returned by mmap(). Larger + * offsets cannot be represented by adjusting the returned mapping. + */ + if (offset >= page_size) { + metal_log(METAL_LOG_ERROR, + "device %s has invalid UIO offset 0x%lx (page size 0x%lx)\n", + dev_name ? dev_name : "", offset, page_size); + return -EINVAL; + } + + return 0; +} + +/* + * Translate UIO sysfs map attributes into the values libmetal needs: + * the mmap() length for cleanup, the usable physical start address, and + * the usable I/O region size after skipping the map offset. + */ +static int metal_linux_uio_map_info(struct metal_uio_map_info *info) +{ + int result; + + if (!info || !info->phys || !info->map_len || !info->region_size) + return -EINVAL; + + result = metal_linux_uio_validate_offset(info->dev_name, info->offset); + if (result) + return result; + + if (!info->map_size || info->offset >= info->map_size) { + metal_log(METAL_LOG_ERROR, + "device %s has invalid UIO size 0x%lx for offset 0x%lx\n", + info->dev_name ? info->dev_name : "", + info->map_size, info->offset); + return -EINVAL; + } + + if ((unsigned long)(size_t)info->map_size != info->map_size) { + metal_log(METAL_LOG_ERROR, + "device %s UIO size 0x%lx overflows size_t\n", + info->dev_name ? info->dev_name : "", + info->map_size); + return -EOVERFLOW; + } + + if (info->map_addr > (metal_phys_addr_t)-1 - info->offset) { + metal_log(METAL_LOG_ERROR, + "device %s UIO physical address overflow (addr=0x%lx offset=0x%lx)\n", + info->dev_name ? info->dev_name : "", + (unsigned long)info->map_addr, info->offset); + return -EOVERFLOW; + } + + /* + * mmap() uses the full page-aligned map. libmetal clients see only the + * usable resource that starts at offset bytes into that mapping. + */ + *info->phys = info->map_addr + info->offset; + *info->map_len = (size_t)info->map_size; + *info->region_size = (size_t)(info->map_size - info->offset); + + return 0; +} + +/* + * Open by UIO class name by scanning /sys/class/uio/uioX/name for the + * requested libmetal device name. This is the synthetic "uio" bus path: + * there is no parent platform or PCI sysfs device to bind through first, so + * the UIO class name must uniquely identify the device. + */ +static int metal_uio_find_device_by_name(const char *uio_name, + struct linux_device *ldev) +{ + DIR *dir; + struct dirent *entry; + char path[PATH_MAX]; + char value[PATH_MAX]; + bool found = false; + int result = -ENODEV; + + if (!uio_name || !strlen(uio_name) || !ldev) + return -EINVAL; + + dir = opendir(METAL_UIO_CLASS_PATH); + if (!dir) { + result = errno == ENOENT ? -ENODEV : -errno; + return result; + } + + /* + * Walk every UIO class device and compare its reported name against the + * requested libmetal name. Continue after a match so duplicate names can + * be detected instead of silently choosing a nondeterministic device. + */ + while ((entry = readdir(dir)) != NULL) { + if (strncmp(entry->d_name, "uio", 3) != 0) + continue; + + result = snprintf(path, sizeof(path), "%s/%s/name", + METAL_UIO_CLASS_PATH, entry->d_name); + if (result < 0 || result >= (int)sizeof(path)) { + result = -EOVERFLOW; + goto out; + } + + result = metal_linux_read_first_line(path, value, + sizeof(value)); + if (result) + continue; + + if (strcmp(value, uio_name) != 0) + continue; + + if (found) { + /* Duplicate names cannot be opened deterministically. */ + result = -EEXIST; + goto out; + } + found = true; + + result = snprintf(ldev->cls_path, sizeof(ldev->cls_path), + "%s/%s", METAL_UIO_CLASS_PATH, + entry->d_name); + if (result < 0 || result >= (int)sizeof(ldev->cls_path)) { + result = -EOVERFLOW; + goto out; + } + /* + * Fill the same fields as the parent-bus UIO path so both + * open modes can share metal_uio_populate(). + */ + result = snprintf(ldev->dev_path, sizeof(ldev->dev_path), + "/dev/%s", entry->d_name); + if (result < 0 || result >= (int)sizeof(ldev->dev_path)) { + result = -EOVERFLOW; + goto out; + } + result = snprintf(ldev->uio_name, sizeof(ldev->uio_name), + "%s", value); + if (result < 0 || result >= (int)sizeof(ldev->uio_name)) { + result = -EOVERFLOW; + goto out; + } + result = snprintf(ldev->uio_dev_name, + sizeof(ldev->uio_dev_name), "%s", + entry->d_name); + if (result < 0 || result >= (int)sizeof(ldev->uio_dev_name)) { + result = -EOVERFLOW; + goto out; + } + } + + result = found ? 0 : -ENODEV; + +out: + closedir(dir); + return result; +} + static int metal_uio_dev_bind(struct linux_device *ldev, struct linux_driver *ldrv) { @@ -151,17 +416,142 @@ static int metal_uio_dev_bind(struct linux_device *ldev, return 0; } +/* + * Populate the common UIO device state after either open path has resolved + * cls_path and dev_path. Both parent-bus opens and class-name opens share the + * same mmap, IRQ registration, DMA, and close-time cleanup rules. + */ +static int metal_uio_populate(struct linux_bus *lbus, struct linux_device *ldev) +{ + unsigned long offset = 0, size = 0; + metal_phys_addr_t addr = 0, *phys; + struct metal_io_region *io; + struct metal_uio_map_info map_info; + size_t map_len, region_size; + int result, i = 0; + unsigned int j; + void *raw, *virt; + int irq_info; + + do { + if (!access(ldev->dev_path, F_OK)) + break; + usleep(10); + i++; + } while (i < 1000); + if (i >= 1000) { + metal_log(METAL_LOG_ERROR, "failed to open file %s, timeout.\n", + ldev->dev_path); + return -ENODEV; + } + result = metal_open(ldev->dev_path, 0); + if (result < 0) { + metal_log(METAL_LOG_ERROR, "failed to open device %s: %s\n", + ldev->dev_path, strerror(-result)); + return result; + } + ldev->fd = result; + + metal_log(METAL_LOG_DEBUG, "opened %s:%s as %s\n", + lbus->bus_name, ldev->dev_name, ldev->dev_path); + + for (i = 0; i < METAL_MAX_DEVICE_REGIONS; i++) { + phys = &ldev->region_phys[ldev->device.num_regions]; + result = metal_uio_read_map_attr(ldev, i, "offset", &offset); + /* + * A missing offset for the next map marks the end of the UIO + * map list. Other read errors are real open failures. + */ + if (result == -ENOENT) + break; + if (result) + goto fail; + result = (result ? result : + metal_uio_read_map_attr(ldev, i, "addr", &addr)); + result = (result ? result : + metal_uio_read_map_attr(ldev, i, "size", &size)); + if (result) + goto fail; + /* + * UIO sysfs reports addr/size/offset separately. Convert them + * before mmap() so the raw mapping and exposed region stay in + * sync for both normal access and close-time unmap. + */ + map_info.dev_name = ldev->dev_name; + map_info.map_addr = addr; + map_info.map_size = size; + map_info.offset = offset; + map_info.phys = phys; + map_info.map_len = &map_len; + map_info.region_size = ®ion_size; + result = metal_linux_uio_map_info(&map_info); + if (result) + goto fail; + result = metal_map(ldev->fd, i * getpagesize(), map_len, 0, 0, + &raw); + if (result) { + metal_log(METAL_LOG_ERROR, + "failed to mmap device %s map%u (len=0x%zx offset=0x%lx): %s\n", + ldev->dev_name, i, map_len, + (unsigned long)i * (unsigned long)getpagesize(), + strerror(-result)); + goto fail; + } + virt = (void *)((char *)raw + offset); + /* + * Keep the raw mapping for munmap(); expose the adjusted + * address as the usable libmetal I/O region. + */ + io = &ldev->device.regions[ldev->device.num_regions]; + metal_io_init(io, virt, phys, region_size, -1, 0, NULL); + ldev->region_map_raw[ldev->device.num_regions] = raw; + ldev->region_map_len[ldev->device.num_regions] = map_len; + ldev->device.num_regions++; + } + + irq_info = 1; + if (write(ldev->fd, &irq_info, sizeof(irq_info)) <= 0) { + metal_log(METAL_LOG_INFO, + "No IRQ for device %s.\n", ldev->dev_name); + ldev->device.irq_num = 0; + ldev->device.irq_info = (void *)-1; + } else { + ldev->device.irq_num = 1; + ldev->device.irq_info = (void *)(intptr_t)ldev->fd; + metal_linux_irq_register_dev(&ldev->device, ldev->fd); + } + + return 0; + +fail: + for (j = 0; j < ldev->device.num_regions; j++) { + metal_unmap(ldev->region_map_raw[j], + ldev->region_map_len[j]); + ldev->region_map_raw[j] = NULL; + ldev->region_map_len[j] = 0; + } + ldev->device.num_regions = 0; + ldev->device.irq_num = 0; + ldev->device.irq_info = (void *)-1; + if (ldev->fd >= 0) { + close(ldev->fd); + ldev->fd = -1; + } + + return result; +} + +/* + * Open a platform or PCI device that has an associated UIO child. This path + * binds the parent device to a UIO driver before using the common UIO populate + * logic. + */ static int metal_uio_dev_open(struct linux_bus *lbus, struct linux_device *ldev) { char *instance, path[SYSFS_PATH_MAX]; struct linux_driver *ldrv = ldev->ldrv; - unsigned long *phys, offset = 0, size = 0; - struct metal_io_region *io; struct dlist *dlist; - int result, i; - void *virt; - int irq_info; - + int result; ldev->fd = -1; ldev->device.irq_info = (void *)-1; @@ -174,11 +564,23 @@ static int metal_uio_dev_open(struct linux_bus *lbus, struct linux_device *ldev) } metal_log(METAL_LOG_DEBUG, "opened sysfs device %s:%s\n", lbus->bus_name, ldev->dev_name); - + /* + * Errors after this point return through metal_linux_dev_open(), which + * calls dev_close() to release parent sysfs and driver override state. + */ + + /* + * Parent-bus opens still need the requested platform or PCI device + * bound to the selected UIO driver before a /dev/uioX node can exist. + */ result = metal_uio_dev_bind(ldev, ldrv); if (result) return result; + /* + * A bound parent device exposes one UIO child below its sysfs device + * directory. Use that child name to derive both sysfs and /dev paths. + */ result = snprintf(path, sizeof(path), "%s/uio", ldev->sdev->path); if (result >= (int)sizeof(path)) return -EOVERFLOW; @@ -190,78 +592,79 @@ static int metal_uio_dev_open(struct linux_bus *lbus, struct linux_device *ldev) } dlist_for_each_data(dlist, instance, char) { + /* + * The first UIO child is the device node this parent-bus open + * will use for mmap, IRQ, and DMA operations. + */ result = snprintf(ldev->cls_path, sizeof(ldev->cls_path), "%s/%s", path, instance); - if (result >= (int)sizeof(ldev->cls_path)) - return -EOVERFLOW; + if (result >= (int)sizeof(ldev->cls_path)) { + result = -EOVERFLOW; + goto close_list; + } result = snprintf(ldev->dev_path, sizeof(ldev->dev_path), "/dev/%s", instance); - if (result >= (int)sizeof(ldev->dev_path)) - return -EOVERFLOW; + if (result >= (int)sizeof(ldev->dev_path)) { + result = -EOVERFLOW; + goto close_list; + } + result = snprintf(path, sizeof(path), "%s/name", ldev->cls_path); + if (result >= (int)sizeof(path)) { + result = -EOVERFLOW; + goto close_list; + } + ldev->uio_name[0] = '\0'; + metal_uio_read_str_attr(path, ldev->uio_name, + sizeof(ldev->uio_name)); + result = snprintf(ldev->uio_dev_name, + sizeof(ldev->uio_dev_name), "%s", instance); + if (result < 0 || result >= (int)sizeof(ldev->uio_dev_name)) { + result = -EOVERFLOW; + goto close_list; + } break; } + result = 0; + +close_list: sysfs_close_list(dlist); + if (result) + return result; + /* Refuse to continue if the selected UIO class path disappeared. */ if (sysfs_path_is_dir(ldev->cls_path) != 0) { metal_log(METAL_LOG_ERROR, "invalid device class path %s\n", ldev->cls_path); return -ENODEV; } - i = 0; - do { - if (!access(ldev->dev_path, F_OK)) - break; - usleep(10); - i++; - } while (i < 1000); - if (i >= 1000) { - metal_log(METAL_LOG_ERROR, "failed to open file %s, timeout.\n", - ldev->dev_path); - return -ENODEV; - } - result = metal_open(ldev->dev_path, 0); - if (result < 0) { - metal_log(METAL_LOG_ERROR, "failed to open device %s\n", - ldev->dev_path, strerror(-result)); - return result; - } - ldev->fd = result; + /* + * Once cls_path and dev_path are resolved, the rest of the open flow is + * shared with the synthetic UIO class-name path. + */ + return metal_uio_populate(lbus, ldev); +} - metal_log(METAL_LOG_DEBUG, "opened %s:%s as %s\n", - lbus->bus_name, ldev->dev_name, ldev->dev_path); +/* + * Open through the synthetic "uio" bus by treating dev_name as the value found + * in /sys/class/uio/uioX/name. No parent sysfs device is available here. + */ +static int metal_uio_class_dev_open(struct linux_bus *lbus, + struct linux_device *ldev) +{ + int result; - for (i = 0, result = 0; !result && i < METAL_MAX_DEVICE_REGIONS; i++) { - phys = &ldev->region_phys[ldev->device.num_regions]; - result = (result ? result : - metal_uio_read_map_attr(ldev, i, "offset", &offset)); - result = (result ? result : - metal_uio_read_map_attr(ldev, i, "addr", phys)); - result = (result ? result : - metal_uio_read_map_attr(ldev, i, "size", &size)); - result = (result ? result : - metal_map(ldev->fd, i * getpagesize(), size, 0, 0, &virt)); - if (!result) { - io = &ldev->device.regions[ldev->device.num_regions]; - metal_io_init(io, virt, phys, size, -1, 0, NULL); - ldev->device.num_regions++; - } - } + ldev->fd = -1; + ldev->device.irq_info = (void *)-1; - irq_info = 1; - if (write(ldev->fd, &irq_info, sizeof(irq_info)) <= 0) { - metal_log(METAL_LOG_INFO, - "%s: No IRQ for device %s.\n", - __func__, ldev->dev_name); - ldev->device.irq_num = 0; - ldev->device.irq_info = (void *)-1; - } else { - ldev->device.irq_num = 1; - ldev->device.irq_info = (void *)(intptr_t)ldev->fd; - metal_linux_irq_register_dev(&ldev->device, ldev->fd); + result = metal_uio_find_device_by_name(ldev->dev_name, ldev); + if (result) { + metal_log(METAL_LOG_ERROR, "UIO device %s not found\n", + ldev->dev_name); + return result; } - return 0; + return metal_uio_populate(lbus, ldev); } static void metal_uio_dev_close(struct linux_bus *lbus, @@ -271,8 +674,10 @@ static void metal_uio_dev_close(struct linux_bus *lbus, unsigned int i; for (i = 0; i < ldev->device.num_regions; i++) { - metal_unmap(ldev->device.regions[i].virt, - ldev->device.regions[i].size); + metal_unmap(ldev->region_map_raw[i], + ldev->region_map_len[i]); + ldev->region_map_raw[i] = NULL; + ldev->region_map_len[i] = 0; } if (ldev->override) { sysfs_write_attribute(ldev->override, "", 1); @@ -283,7 +688,14 @@ static void metal_uio_dev_close(struct linux_bus *lbus, ldev->sdev = NULL; } if (ldev->fd >= 0) { + /* + * Disable first so unregister only removes device bookkeeping; + * IRQ handler teardown remains in the generic IRQ path. + */ + metal_irq_disable(ldev->fd); + metal_linux_irq_unregister_dev(ldev->fd); close(ldev->fd); + ldev->fd = -1; } } @@ -363,6 +775,22 @@ static void metal_uio_dev_dma_unmap(struct linux_bus *lbus, } static struct linux_bus linux_bus[] = { + { + .bus_name = "uio", + .drivers = { + { + .drv_name = "uio", + .mod_name = "uio", + .cls_name = "uio", + .dev_open = metal_uio_class_dev_open, + .dev_close = metal_uio_dev_close, + .dev_irq_ack = metal_uio_dev_irq_ack, + .dev_dma_map = metal_uio_dev_dma_map, + .dev_dma_unmap = metal_uio_dev_dma_unmap, + }, + { 0 /* sentinel */ } + } + }, { .bus_name = "platform", .drivers = { @@ -428,7 +856,7 @@ static int metal_linux_dev_open(struct metal_bus *bus, struct linux_bus *lbus = to_linux_bus(bus); struct linux_device *ldev = NULL; struct linux_driver *ldrv; - int error; + int error = -ENODEV; ldev = malloc(sizeof(*ldev)); if (!ldev) @@ -437,12 +865,18 @@ static int metal_linux_dev_open(struct metal_bus *bus, for_each_linux_driver(lbus, ldrv) { /* Check if we have a viable driver. */ - if (!ldrv->sdrv || !ldrv->dev_open) + if (!ldrv->dev_open || + (!metal_linux_is_uio_bus(lbus) && !ldrv->sdrv)) continue; /* Reset device data. */ memset(ldev, 0, sizeof(*ldev)); - strncpy(ldev->dev_name, dev_name, sizeof(ldev->dev_name) - 1); + error = snprintf(ldev->dev_name, sizeof(ldev->dev_name), + "%s", dev_name); + if (error < 0 || error >= (int)sizeof(ldev->dev_name)) { + error = -EOVERFLOW; + goto out; + } ldev->fd = -1; ldev->ldrv = ldrv; ldev->device.bus = bus; @@ -450,8 +884,11 @@ static int metal_linux_dev_open(struct metal_bus *bus, /* Try and open the device. */ error = ldrv->dev_open(lbus, ldev); if (error) { - ldrv->dev_close(lbus, ldev); - continue; + /* + * Return the driver's errno while still giving it a + * chance to release any state allocated before failing. + */ + goto close_dev; } *device = &ldev->device; @@ -461,9 +898,15 @@ static int metal_linux_dev_open(struct metal_bus *bus, return 0; } + goto out; + +close_dev: + if (ldrv->dev_close) + ldrv->dev_close(lbus, ldev); +out: free(ldev); - return -ENODEV; + return error; } static void metal_linux_dev_close(struct metal_bus *bus, @@ -488,7 +931,9 @@ static void metal_linux_bus_close(struct metal_bus *bus) ldrv->sdrv = NULL; } - sysfs_close_bus(lbus->sbus); + /* The synthetic UIO bus does not open a sysfs bus handle. */ + if (lbus->sbus) + sysfs_close_bus(lbus->sbus); lbus->sbus = NULL; } @@ -592,6 +1037,16 @@ static int metal_linux_probe_bus(struct linux_bus *lbus) struct linux_driver *ldrv; int ret, error = -ENODEV; + /* + * Register the synthetic bus only when the /sys/class/uio class exists + * and skip normal bus/driver probing. + */ + if (metal_linux_is_uio_bus(lbus)) { + if (sysfs_path_is_dir(METAL_UIO_CLASS_PATH) != 0) + return -ENODEV; + return metal_linux_register_bus(lbus); + } + lbus->sbus = sysfs_open_bus(lbus->bus_name); if (!lbus->sbus) return -ENODEV; @@ -668,4 +1123,3 @@ int metal_linux_get_device_property(struct metal_device *device, status = close(fd); return status < 0 ? -errno : 0; } - diff --git a/lib/system/linux/irq.c b/lib/system/linux/irq.c index 5d84ee01..1042d8b6 100644 --- a/lib/system/linux/irq.c +++ b/lib/system/linux/irq.c @@ -266,10 +266,69 @@ void metal_linux_irq_shutdown(void) void metal_linux_irq_register_dev(struct metal_device *dev, int irq) { - if (irq > MAX_IRQS) { + if (irq < 0 || irq >= MAX_IRQS) { metal_log(METAL_LOG_ERROR, "Failed to register device to irq %d\n", irq); return; } irqs_devs[irq] = dev; } + +/* + * Drop the device pointer associated with a Linux IRQ fd during device close. + * The caller must disable the IRQ first so the dispatch path cannot observe + * an enabled IRQ whose owning device has already been detached. + */ +int metal_linux_irq_unregister_dev(int irq) +{ + int offset; + + if (irq < linux_irq_cntr.irq_base || + irq >= linux_irq_cntr.irq_base + linux_irq_cntr.irq_num) { + metal_log(METAL_LOG_ERROR, + "Failed to unregister device from irq %d\n", irq); + return -EINVAL; + } + + offset = irq - linux_irq_cntr.irq_base; + metal_mutex_acquire(&irq_lock); + /* + * Unregister only detaches the device association. The IRQ handler and + * enabled state remain owned by metal_irq_disable()/unregister(). + */ + if (metal_bitmap_is_bit_set(irqs_enabled, offset)) { + metal_mutex_release(&irq_lock); + return -EINVAL; + } + irqs_devs[irq] = NULL; + metal_mutex_release(&irq_lock); + + return 0; +} + +/* + * Return the device pointer used by the Linux IRQ dispatch path. Tests use + * this to verify close-time bookkeeping without poking at static arrays. + */ +struct metal_device *metal_linux_irq_get_dev(int irq) +{ + if (irq < linux_irq_cntr.irq_base || + irq >= linux_irq_cntr.irq_base + linux_irq_cntr.irq_num) + return NULL; + + return irqs_devs[irq]; +} + +/* + * Report the Linux IRQ enable bit for callers that need to enforce teardown + * ordering before unregistering a device association. + */ +int metal_linux_irq_is_enabled(int irq) +{ + if (irq < linux_irq_cntr.irq_base || + irq >= linux_irq_cntr.irq_base + linux_irq_cntr.irq_num) + return 0; + + return metal_bitmap_is_bit_set(irqs_enabled, + irq - linux_irq_cntr.irq_base); +} diff --git a/lib/system/linux/irq.h b/lib/system/linux/irq.h index ff02b7e4..b6dedb71 100644 --- a/lib/system/linux/irq.h +++ b/lib/system/linux/irq.h @@ -29,6 +29,33 @@ */ void metal_linux_irq_register_dev(struct metal_device *dev, int irq); +/** + * @brief Unregister the metal device associated with a Linux IRQ. + * + * Metal Linux internal function to clear device bookkeeping for an IRQ. The + * IRQ consumer must disable the IRQ before unregistering the device. + * + * @param[in] irq interrupt id + * @return 0 on success, or -errno on error. + */ +int metal_linux_irq_unregister_dev(int irq); + +/** + * @brief Get the metal device associated with a Linux IRQ. + * + * @param[in] irq interrupt id + * @return Registered metal device, or NULL if none is registered. + */ +struct metal_device *metal_linux_irq_get_dev(int irq); + +/** + * @brief Check whether a Linux IRQ is enabled. + * + * @param[in] irq interrupt id + * @return 1 if the IRQ is enabled, or 0 otherwise. + */ +int metal_linux_irq_is_enabled(int irq); + #endif /* METAL_INTERNAL */ #define __METAL_LINUX_IRQ__H__