| // SPDX-License-Identifier: GPL-2.0+ | |
| /* | |
| * Copyright (C) 2003-2008 Takahiro Hirofuchi | |
| */ | |
| #include <linux/string.h> | |
| #include <linux/module.h> | |
| #include <linux/device.h> | |
| #include <linux/scatterlist.h> | |
| #include "usbip_common.h" | |
| #include "stub.h" | |
| #define DRIVER_AUTHOR "Takahiro Hirofuchi" | |
| #define DRIVER_DESC "USB/IP Host Driver" | |
| struct kmem_cache *stub_priv_cache; | |
| /* | |
| * busid_tables defines matching busids that usbip can grab. A user can change | |
| * dynamically what device is locally used and what device is exported to a | |
| * remote host. | |
| */ | |
| #define MAX_BUSID 16 | |
| static struct bus_id_priv busid_table[MAX_BUSID]; | |
| static DEFINE_SPINLOCK(busid_table_lock); | |
| static void init_busid_table(void) | |
| { | |
| int i; | |
| /* | |
| * This also sets the bus_table[i].status to | |
| * STUB_BUSID_OTHER, which is 0. | |
| */ | |
| memset(busid_table, 0, sizeof(busid_table)); | |
| for (i = 0; i < MAX_BUSID; i++) | |
| spin_lock_init(&busid_table[i].busid_lock); | |
| } | |
| /* | |
| * Find the index of the busid by name. | |
| * Must be called with busid_table_lock held. | |
| */ | |
| static int get_busid_idx(const char *busid) | |
| { | |
| int i; | |
| int idx = -1; | |
| for (i = 0; i < MAX_BUSID; i++) { | |
| spin_lock(&busid_table[i].busid_lock); | |
| if (busid_table[i].name[0]) | |
| if (!strncmp(busid_table[i].name, busid, BUSID_SIZE)) { | |
| idx = i; | |
| spin_unlock(&busid_table[i].busid_lock); | |
| break; | |
| } | |
| spin_unlock(&busid_table[i].busid_lock); | |
| } | |
| return idx; | |
| } | |
| /* Returns holding busid_lock. Should call put_busid_priv() to unlock */ | |
| struct bus_id_priv *get_busid_priv(const char *busid) | |
| { | |
| int idx; | |
| struct bus_id_priv *bid = NULL; | |
| spin_lock(&busid_table_lock); | |
| idx = get_busid_idx(busid); | |
| if (idx >= 0) { | |
| bid = &(busid_table[idx]); | |
| /* get busid_lock before returning */ | |
| spin_lock(&bid->busid_lock); | |
| } | |
| spin_unlock(&busid_table_lock); | |
| return bid; | |
| } | |
| void put_busid_priv(struct bus_id_priv *bid) | |
| { | |
| if (bid) | |
| spin_unlock(&bid->busid_lock); | |
| } | |
| static int add_match_busid(char *busid) | |
| { | |
| int i; | |
| int ret = -1; | |
| spin_lock(&busid_table_lock); | |
| /* already registered? */ | |
| if (get_busid_idx(busid) >= 0) { | |
| ret = 0; | |
| goto out; | |
| } | |
| for (i = 0; i < MAX_BUSID; i++) { | |
| spin_lock(&busid_table[i].busid_lock); | |
| if (!busid_table[i].name[0]) { | |
| strscpy(busid_table[i].name, busid, BUSID_SIZE); | |
| if ((busid_table[i].status != STUB_BUSID_ALLOC) && | |
| (busid_table[i].status != STUB_BUSID_REMOV)) | |
| busid_table[i].status = STUB_BUSID_ADDED; | |
| ret = 0; | |
| spin_unlock(&busid_table[i].busid_lock); | |
| break; | |
| } | |
| spin_unlock(&busid_table[i].busid_lock); | |
| } | |
| out: | |
| spin_unlock(&busid_table_lock); | |
| return ret; | |
| } | |
| int del_match_busid(char *busid) | |
| { | |
| int idx; | |
| int ret = -1; | |
| spin_lock(&busid_table_lock); | |
| idx = get_busid_idx(busid); | |
| if (idx < 0) | |
| goto out; | |
| /* found */ | |
| ret = 0; | |
| spin_lock(&busid_table[idx].busid_lock); | |
| if (busid_table[idx].status == STUB_BUSID_OTHER) | |
| memset(busid_table[idx].name, 0, BUSID_SIZE); | |
| if ((busid_table[idx].status != STUB_BUSID_OTHER) && | |
| (busid_table[idx].status != STUB_BUSID_ADDED)) | |
| busid_table[idx].status = STUB_BUSID_REMOV; | |
| spin_unlock(&busid_table[idx].busid_lock); | |
| out: | |
| spin_unlock(&busid_table_lock); | |
| return ret; | |
| } | |
| static ssize_t match_busid_show(struct device_driver *drv, char *buf) | |
| { | |
| int i; | |
| char *out = buf; | |
| spin_lock(&busid_table_lock); | |
| for (i = 0; i < MAX_BUSID; i++) { | |
| spin_lock(&busid_table[i].busid_lock); | |
| if (busid_table[i].name[0]) | |
| out += sprintf(out, "%s ", busid_table[i].name); | |
| spin_unlock(&busid_table[i].busid_lock); | |
| } | |
| spin_unlock(&busid_table_lock); | |
| out += sprintf(out, "\n"); | |
| return out - buf; | |
| } | |
| static ssize_t match_busid_store(struct device_driver *dev, const char *buf, | |
| size_t count) | |
| { | |
| char busid[BUSID_SIZE]; | |
| if (count < 5) | |
| return -EINVAL; | |
| /* busid needs to include \0 termination */ | |
| if (strscpy(busid, buf + 4, BUSID_SIZE) < 0) | |
| return -EINVAL; | |
| if (!strncmp(buf, "add ", 4)) { | |
| if (add_match_busid(busid) < 0) | |
| return -ENOMEM; | |
| pr_debug("add busid %s\n", busid); | |
| return count; | |
| } | |
| if (!strncmp(buf, "del ", 4)) { | |
| if (del_match_busid(busid) < 0) | |
| return -ENODEV; | |
| pr_debug("del busid %s\n", busid); | |
| return count; | |
| } | |
| return -EINVAL; | |
| } | |
| static DRIVER_ATTR_RW(match_busid); | |
| static int do_rebind(char *busid, struct bus_id_priv *busid_priv) | |
| { | |
| int ret = 0; | |
| /* device_attach() callers should hold parent lock for USB */ | |
| if (busid_priv->udev->dev.parent) | |
| device_lock(busid_priv->udev->dev.parent); | |
| ret = device_attach(&busid_priv->udev->dev); | |
| if (busid_priv->udev->dev.parent) | |
| device_unlock(busid_priv->udev->dev.parent); | |
| if (ret < 0) | |
| dev_err(&busid_priv->udev->dev, "rebind failed\n"); | |
| return ret; | |
| } | |
| static void stub_device_rebind(void) | |
| { | |
| #if IS_MODULE(CONFIG_USBIP_HOST) | |
| struct bus_id_priv *busid_priv; | |
| int i; | |
| /* update status to STUB_BUSID_OTHER so probe ignores the device */ | |
| spin_lock(&busid_table_lock); | |
| for (i = 0; i < MAX_BUSID; i++) { | |
| if (busid_table[i].name[0] && | |
| busid_table[i].shutdown_busid) { | |
| busid_priv = &(busid_table[i]); | |
| busid_priv->status = STUB_BUSID_OTHER; | |
| } | |
| } | |
| spin_unlock(&busid_table_lock); | |
| /* now run rebind - no need to hold locks. driver files are removed */ | |
| for (i = 0; i < MAX_BUSID; i++) { | |
| if (busid_table[i].name[0] && | |
| busid_table[i].shutdown_busid) { | |
| busid_priv = &(busid_table[i]); | |
| do_rebind(busid_table[i].name, busid_priv); | |
| } | |
| } | |
| #endif | |
| } | |
| static ssize_t rebind_store(struct device_driver *dev, const char *buf, | |
| size_t count) | |
| { | |
| int ret; | |
| int len; | |
| struct bus_id_priv *bid; | |
| /* buf length should be less that BUSID_SIZE */ | |
| len = strnlen(buf, BUSID_SIZE); | |
| if (!(len < BUSID_SIZE)) | |
| return -EINVAL; | |
| bid = get_busid_priv(buf); | |
| if (!bid) | |
| return -ENODEV; | |
| /* mark the device for deletion so probe ignores it during rescan */ | |
| bid->status = STUB_BUSID_OTHER; | |
| /* release the busid lock */ | |
| put_busid_priv(bid); | |
| ret = do_rebind((char *) buf, bid); | |
| if (ret < 0) | |
| return ret; | |
| /* delete device from busid_table */ | |
| del_match_busid((char *) buf); | |
| return count; | |
| } | |
| static DRIVER_ATTR_WO(rebind); | |
| static struct stub_priv *stub_priv_pop_from_listhead(struct list_head *listhead) | |
| { | |
| struct stub_priv *priv, *tmp; | |
| list_for_each_entry_safe(priv, tmp, listhead, list) { | |
| list_del_init(&priv->list); | |
| return priv; | |
| } | |
| return NULL; | |
| } | |
| void stub_free_priv_and_urb(struct stub_priv *priv) | |
| { | |
| struct urb *urb; | |
| int i; | |
| for (i = 0; i < priv->num_urbs; i++) { | |
| urb = priv->urbs[i]; | |
| if (!urb) | |
| return; | |
| kfree(urb->setup_packet); | |
| urb->setup_packet = NULL; | |
| if (urb->transfer_buffer && !priv->sgl) { | |
| kfree(urb->transfer_buffer); | |
| urb->transfer_buffer = NULL; | |
| } | |
| if (urb->num_sgs) { | |
| sgl_free(urb->sg); | |
| urb->sg = NULL; | |
| urb->num_sgs = 0; | |
| } | |
| usb_free_urb(urb); | |
| } | |
| if (!list_empty(&priv->list)) | |
| list_del(&priv->list); | |
| if (priv->sgl) | |
| sgl_free(priv->sgl); | |
| kfree(priv->urbs); | |
| kmem_cache_free(stub_priv_cache, priv); | |
| } | |
| static struct stub_priv *stub_priv_pop(struct stub_device *sdev) | |
| { | |
| unsigned long flags; | |
| struct stub_priv *priv; | |
| spin_lock_irqsave(&sdev->priv_lock, flags); | |
| priv = stub_priv_pop_from_listhead(&sdev->priv_init); | |
| if (priv) | |
| goto done; | |
| priv = stub_priv_pop_from_listhead(&sdev->priv_tx); | |
| if (priv) | |
| goto done; | |
| priv = stub_priv_pop_from_listhead(&sdev->priv_free); | |
| done: | |
| spin_unlock_irqrestore(&sdev->priv_lock, flags); | |
| return priv; | |
| } | |
| void stub_device_cleanup_urbs(struct stub_device *sdev) | |
| { | |
| struct stub_priv *priv; | |
| int i; | |
| dev_dbg(&sdev->udev->dev, "Stub device cleaning up urbs\n"); | |
| while ((priv = stub_priv_pop(sdev))) { | |
| for (i = 0; i < priv->num_urbs; i++) | |
| usb_kill_urb(priv->urbs[i]); | |
| stub_free_priv_and_urb(priv); | |
| } | |
| } | |
| static int __init usbip_host_init(void) | |
| { | |
| int ret; | |
| init_busid_table(); | |
| stub_priv_cache = KMEM_CACHE(stub_priv, SLAB_HWCACHE_ALIGN); | |
| if (!stub_priv_cache) { | |
| pr_err("kmem_cache_create failed\n"); | |
| return -ENOMEM; | |
| } | |
| ret = usb_register_device_driver(&stub_driver, THIS_MODULE); | |
| if (ret) { | |
| pr_err("usb_register failed %d\n", ret); | |
| goto err_usb_register; | |
| } | |
| ret = driver_create_file(&stub_driver.driver, | |
| &driver_attr_match_busid); | |
| if (ret) { | |
| pr_err("driver_create_file failed\n"); | |
| goto err_create_file; | |
| } | |
| ret = driver_create_file(&stub_driver.driver, | |
| &driver_attr_rebind); | |
| if (ret) { | |
| pr_err("driver_create_file failed\n"); | |
| goto err_create_file; | |
| } | |
| return ret; | |
| err_create_file: | |
| usb_deregister_device_driver(&stub_driver); | |
| err_usb_register: | |
| kmem_cache_destroy(stub_priv_cache); | |
| return ret; | |
| } | |
| static void __exit usbip_host_exit(void) | |
| { | |
| driver_remove_file(&stub_driver.driver, | |
| &driver_attr_match_busid); | |
| driver_remove_file(&stub_driver.driver, | |
| &driver_attr_rebind); | |
| /* | |
| * deregister() calls stub_disconnect() for all devices. Device | |
| * specific data is cleared in stub_disconnect(). | |
| */ | |
| usb_deregister_device_driver(&stub_driver); | |
| /* initiate scan to attach devices */ | |
| stub_device_rebind(); | |
| kmem_cache_destroy(stub_priv_cache); | |
| } | |
| module_init(usbip_host_init); | |
| module_exit(usbip_host_exit); | |
| MODULE_AUTHOR(DRIVER_AUTHOR); | |
| MODULE_DESCRIPTION(DRIVER_DESC); | |
| MODULE_LICENSE("GPL"); |