mirror of https://github.com/Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
527 lines
12 KiB
527 lines
12 KiB
// SPDX-License-Identifier: GPL-2.0+ |
|
/* |
|
* Copyright (C) 2003-2008 Takahiro Hirofuchi |
|
* Copyright (C) 2015-2016 Nobuo Iwata |
|
*/ |
|
|
|
#include <linux/kthread.h> |
|
#include <linux/file.h> |
|
#include <linux/net.h> |
|
#include <linux/platform_device.h> |
|
#include <linux/slab.h> |
|
|
|
/* Hardening for Spectre-v1 */ |
|
#include <linux/nospec.h> |
|
|
|
#include "usbip_common.h" |
|
#include "vhci.h" |
|
|
|
/* TODO: refine locking ?*/ |
|
|
|
/* |
|
* output example: |
|
* hub port sta spd dev sockfd local_busid |
|
* hs 0000 004 000 00000000 000003 1-2.3 |
|
* ................................................ |
|
* ss 0008 004 000 00000000 000004 2-3.4 |
|
* ................................................ |
|
* |
|
* Output includes socket fd instead of socket pointer address to avoid |
|
* leaking kernel memory address in: |
|
* /sys/devices/platform/vhci_hcd.0/status and in debug output. |
|
* The socket pointer address is not used at the moment and it was made |
|
* visible as a convenient way to find IP address from socket pointer |
|
* address by looking up /proc/net/{tcp,tcp6}. As this opens a security |
|
* hole, the change is made to use sockfd instead. |
|
* |
|
*/ |
|
static void port_show_vhci(char **out, int hub, int port, struct vhci_device *vdev) |
|
{ |
|
if (hub == HUB_SPEED_HIGH) |
|
*out += sprintf(*out, "hs %04u %03u ", |
|
port, vdev->ud.status); |
|
else /* hub == HUB_SPEED_SUPER */ |
|
*out += sprintf(*out, "ss %04u %03u ", |
|
port, vdev->ud.status); |
|
|
|
if (vdev->ud.status == VDEV_ST_USED) { |
|
*out += sprintf(*out, "%03u %08x ", |
|
vdev->speed, vdev->devid); |
|
*out += sprintf(*out, "%06u %s", |
|
vdev->ud.sockfd, |
|
dev_name(&vdev->udev->dev)); |
|
|
|
} else { |
|
*out += sprintf(*out, "000 00000000 "); |
|
*out += sprintf(*out, "000000 0-0"); |
|
} |
|
|
|
*out += sprintf(*out, "\n"); |
|
} |
|
|
|
/* Sysfs entry to show port status */ |
|
static ssize_t status_show_vhci(int pdev_nr, char *out) |
|
{ |
|
struct platform_device *pdev = vhcis[pdev_nr].pdev; |
|
struct vhci *vhci; |
|
struct usb_hcd *hcd; |
|
struct vhci_hcd *vhci_hcd; |
|
char *s = out; |
|
int i; |
|
unsigned long flags; |
|
|
|
if (!pdev || !out) { |
|
usbip_dbg_vhci_sysfs("show status error\n"); |
|
return 0; |
|
} |
|
|
|
hcd = platform_get_drvdata(pdev); |
|
vhci_hcd = hcd_to_vhci_hcd(hcd); |
|
vhci = vhci_hcd->vhci; |
|
|
|
spin_lock_irqsave(&vhci->lock, flags); |
|
|
|
for (i = 0; i < VHCI_HC_PORTS; i++) { |
|
struct vhci_device *vdev = &vhci->vhci_hcd_hs->vdev[i]; |
|
|
|
spin_lock(&vdev->ud.lock); |
|
port_show_vhci(&out, HUB_SPEED_HIGH, |
|
pdev_nr * VHCI_PORTS + i, vdev); |
|
spin_unlock(&vdev->ud.lock); |
|
} |
|
|
|
for (i = 0; i < VHCI_HC_PORTS; i++) { |
|
struct vhci_device *vdev = &vhci->vhci_hcd_ss->vdev[i]; |
|
|
|
spin_lock(&vdev->ud.lock); |
|
port_show_vhci(&out, HUB_SPEED_SUPER, |
|
pdev_nr * VHCI_PORTS + VHCI_HC_PORTS + i, vdev); |
|
spin_unlock(&vdev->ud.lock); |
|
} |
|
|
|
spin_unlock_irqrestore(&vhci->lock, flags); |
|
|
|
return out - s; |
|
} |
|
|
|
static ssize_t status_show_not_ready(int pdev_nr, char *out) |
|
{ |
|
char *s = out; |
|
int i = 0; |
|
|
|
for (i = 0; i < VHCI_HC_PORTS; i++) { |
|
out += sprintf(out, "hs %04u %03u ", |
|
(pdev_nr * VHCI_PORTS) + i, |
|
VDEV_ST_NOTASSIGNED); |
|
out += sprintf(out, "000 00000000 0000000000000000 0-0"); |
|
out += sprintf(out, "\n"); |
|
} |
|
|
|
for (i = 0; i < VHCI_HC_PORTS; i++) { |
|
out += sprintf(out, "ss %04u %03u ", |
|
(pdev_nr * VHCI_PORTS) + VHCI_HC_PORTS + i, |
|
VDEV_ST_NOTASSIGNED); |
|
out += sprintf(out, "000 00000000 0000000000000000 0-0"); |
|
out += sprintf(out, "\n"); |
|
} |
|
return out - s; |
|
} |
|
|
|
static int status_name_to_id(const char *name) |
|
{ |
|
char *c; |
|
long val; |
|
int ret; |
|
|
|
c = strchr(name, '.'); |
|
if (c == NULL) |
|
return 0; |
|
|
|
ret = kstrtol(c+1, 10, &val); |
|
if (ret < 0) |
|
return ret; |
|
|
|
return val; |
|
} |
|
|
|
static ssize_t status_show(struct device *dev, |
|
struct device_attribute *attr, char *out) |
|
{ |
|
char *s = out; |
|
int pdev_nr; |
|
|
|
out += sprintf(out, |
|
"hub port sta spd dev sockfd local_busid\n"); |
|
|
|
pdev_nr = status_name_to_id(attr->attr.name); |
|
if (pdev_nr < 0) |
|
out += status_show_not_ready(pdev_nr, out); |
|
else |
|
out += status_show_vhci(pdev_nr, out); |
|
|
|
return out - s; |
|
} |
|
|
|
static ssize_t nports_show(struct device *dev, struct device_attribute *attr, |
|
char *out) |
|
{ |
|
char *s = out; |
|
|
|
/* |
|
* Half the ports are for SPEED_HIGH and half for SPEED_SUPER, |
|
* thus the * 2. |
|
*/ |
|
out += sprintf(out, "%d\n", VHCI_PORTS * vhci_num_controllers); |
|
return out - s; |
|
} |
|
static DEVICE_ATTR_RO(nports); |
|
|
|
/* Sysfs entry to shutdown a virtual connection */ |
|
static int vhci_port_disconnect(struct vhci_hcd *vhci_hcd, __u32 rhport) |
|
{ |
|
struct vhci_device *vdev = &vhci_hcd->vdev[rhport]; |
|
struct vhci *vhci = vhci_hcd->vhci; |
|
unsigned long flags; |
|
|
|
usbip_dbg_vhci_sysfs("enter\n"); |
|
|
|
mutex_lock(&vdev->ud.sysfs_lock); |
|
|
|
/* lock */ |
|
spin_lock_irqsave(&vhci->lock, flags); |
|
spin_lock(&vdev->ud.lock); |
|
|
|
if (vdev->ud.status == VDEV_ST_NULL) { |
|
pr_err("not connected %d\n", vdev->ud.status); |
|
|
|
/* unlock */ |
|
spin_unlock(&vdev->ud.lock); |
|
spin_unlock_irqrestore(&vhci->lock, flags); |
|
mutex_unlock(&vdev->ud.sysfs_lock); |
|
|
|
return -EINVAL; |
|
} |
|
|
|
/* unlock */ |
|
spin_unlock(&vdev->ud.lock); |
|
spin_unlock_irqrestore(&vhci->lock, flags); |
|
|
|
usbip_event_add(&vdev->ud, VDEV_EVENT_DOWN); |
|
|
|
mutex_unlock(&vdev->ud.sysfs_lock); |
|
|
|
return 0; |
|
} |
|
|
|
static int valid_port(__u32 *pdev_nr, __u32 *rhport) |
|
{ |
|
if (*pdev_nr >= vhci_num_controllers) { |
|
pr_err("pdev %u\n", *pdev_nr); |
|
return 0; |
|
} |
|
*pdev_nr = array_index_nospec(*pdev_nr, vhci_num_controllers); |
|
|
|
if (*rhport >= VHCI_HC_PORTS) { |
|
pr_err("rhport %u\n", *rhport); |
|
return 0; |
|
} |
|
*rhport = array_index_nospec(*rhport, VHCI_HC_PORTS); |
|
|
|
return 1; |
|
} |
|
|
|
static ssize_t detach_store(struct device *dev, struct device_attribute *attr, |
|
const char *buf, size_t count) |
|
{ |
|
__u32 port = 0, pdev_nr = 0, rhport = 0; |
|
struct usb_hcd *hcd; |
|
struct vhci_hcd *vhci_hcd; |
|
int ret; |
|
|
|
if (kstrtoint(buf, 10, &port) < 0) |
|
return -EINVAL; |
|
|
|
pdev_nr = port_to_pdev_nr(port); |
|
rhport = port_to_rhport(port); |
|
|
|
if (!valid_port(&pdev_nr, &rhport)) |
|
return -EINVAL; |
|
|
|
hcd = platform_get_drvdata(vhcis[pdev_nr].pdev); |
|
if (hcd == NULL) { |
|
dev_err(dev, "port is not ready %u\n", port); |
|
return -EAGAIN; |
|
} |
|
|
|
usbip_dbg_vhci_sysfs("rhport %d\n", rhport); |
|
|
|
if ((port / VHCI_HC_PORTS) % 2) |
|
vhci_hcd = hcd_to_vhci_hcd(hcd)->vhci->vhci_hcd_ss; |
|
else |
|
vhci_hcd = hcd_to_vhci_hcd(hcd)->vhci->vhci_hcd_hs; |
|
|
|
ret = vhci_port_disconnect(vhci_hcd, rhport); |
|
if (ret < 0) |
|
return -EINVAL; |
|
|
|
usbip_dbg_vhci_sysfs("Leave\n"); |
|
|
|
return count; |
|
} |
|
static DEVICE_ATTR_WO(detach); |
|
|
|
static int valid_args(__u32 *pdev_nr, __u32 *rhport, |
|
enum usb_device_speed speed) |
|
{ |
|
if (!valid_port(pdev_nr, rhport)) { |
|
return 0; |
|
} |
|
|
|
switch (speed) { |
|
case USB_SPEED_LOW: |
|
case USB_SPEED_FULL: |
|
case USB_SPEED_HIGH: |
|
case USB_SPEED_WIRELESS: |
|
case USB_SPEED_SUPER: |
|
break; |
|
default: |
|
pr_err("Failed attach request for unsupported USB speed: %s\n", |
|
usb_speed_string(speed)); |
|
return 0; |
|
} |
|
|
|
return 1; |
|
} |
|
|
|
/* Sysfs entry to establish a virtual connection */ |
|
/* |
|
* To start a new USB/IP attachment, a userland program needs to setup a TCP |
|
* connection and then write its socket descriptor with remote device |
|
* information into this sysfs file. |
|
* |
|
* A remote device is virtually attached to the root-hub port of @rhport with |
|
* @speed. @devid is embedded into a request to specify the remote device in a |
|
* server host. |
|
* |
|
* write() returns 0 on success, else negative errno. |
|
*/ |
|
static ssize_t attach_store(struct device *dev, struct device_attribute *attr, |
|
const char *buf, size_t count) |
|
{ |
|
struct socket *socket; |
|
int sockfd = 0; |
|
__u32 port = 0, pdev_nr = 0, rhport = 0, devid = 0, speed = 0; |
|
struct usb_hcd *hcd; |
|
struct vhci_hcd *vhci_hcd; |
|
struct vhci_device *vdev; |
|
struct vhci *vhci; |
|
int err; |
|
unsigned long flags; |
|
struct task_struct *tcp_rx = NULL; |
|
struct task_struct *tcp_tx = NULL; |
|
|
|
/* |
|
* @rhport: port number of vhci_hcd |
|
* @sockfd: socket descriptor of an established TCP connection |
|
* @devid: unique device identifier in a remote host |
|
* @speed: usb device speed in a remote host |
|
*/ |
|
if (sscanf(buf, "%u %u %u %u", &port, &sockfd, &devid, &speed) != 4) |
|
return -EINVAL; |
|
pdev_nr = port_to_pdev_nr(port); |
|
rhport = port_to_rhport(port); |
|
|
|
usbip_dbg_vhci_sysfs("port(%u) pdev(%d) rhport(%u)\n", |
|
port, pdev_nr, rhport); |
|
usbip_dbg_vhci_sysfs("sockfd(%u) devid(%u) speed(%u)\n", |
|
sockfd, devid, speed); |
|
|
|
/* check received parameters */ |
|
if (!valid_args(&pdev_nr, &rhport, speed)) |
|
return -EINVAL; |
|
|
|
hcd = platform_get_drvdata(vhcis[pdev_nr].pdev); |
|
if (hcd == NULL) { |
|
dev_err(dev, "port %d is not ready\n", port); |
|
return -EAGAIN; |
|
} |
|
|
|
vhci_hcd = hcd_to_vhci_hcd(hcd); |
|
vhci = vhci_hcd->vhci; |
|
|
|
if (speed == USB_SPEED_SUPER) |
|
vdev = &vhci->vhci_hcd_ss->vdev[rhport]; |
|
else |
|
vdev = &vhci->vhci_hcd_hs->vdev[rhport]; |
|
|
|
mutex_lock(&vdev->ud.sysfs_lock); |
|
|
|
/* Extract socket from fd. */ |
|
socket = sockfd_lookup(sockfd, &err); |
|
if (!socket) { |
|
dev_err(dev, "failed to lookup sock"); |
|
err = -EINVAL; |
|
goto unlock_mutex; |
|
} |
|
if (socket->type != SOCK_STREAM) { |
|
dev_err(dev, "Expecting SOCK_STREAM - found %d", |
|
socket->type); |
|
sockfd_put(socket); |
|
err = -EINVAL; |
|
goto unlock_mutex; |
|
} |
|
|
|
/* create threads before locking */ |
|
tcp_rx = kthread_create(vhci_rx_loop, &vdev->ud, "vhci_rx"); |
|
if (IS_ERR(tcp_rx)) { |
|
sockfd_put(socket); |
|
err = -EINVAL; |
|
goto unlock_mutex; |
|
} |
|
tcp_tx = kthread_create(vhci_tx_loop, &vdev->ud, "vhci_tx"); |
|
if (IS_ERR(tcp_tx)) { |
|
kthread_stop(tcp_rx); |
|
sockfd_put(socket); |
|
err = -EINVAL; |
|
goto unlock_mutex; |
|
} |
|
|
|
/* get task structs now */ |
|
get_task_struct(tcp_rx); |
|
get_task_struct(tcp_tx); |
|
|
|
/* now begin lock until setting vdev status set */ |
|
spin_lock_irqsave(&vhci->lock, flags); |
|
spin_lock(&vdev->ud.lock); |
|
|
|
if (vdev->ud.status != VDEV_ST_NULL) { |
|
/* end of the lock */ |
|
spin_unlock(&vdev->ud.lock); |
|
spin_unlock_irqrestore(&vhci->lock, flags); |
|
|
|
sockfd_put(socket); |
|
kthread_stop_put(tcp_rx); |
|
kthread_stop_put(tcp_tx); |
|
|
|
dev_err(dev, "port %d already used\n", rhport); |
|
/* |
|
* Will be retried from userspace |
|
* if there's another free port. |
|
*/ |
|
err = -EBUSY; |
|
goto unlock_mutex; |
|
} |
|
|
|
dev_info(dev, "pdev(%u) rhport(%u) sockfd(%d)\n", |
|
pdev_nr, rhport, sockfd); |
|
dev_info(dev, "devid(%u) speed(%u) speed_str(%s)\n", |
|
devid, speed, usb_speed_string(speed)); |
|
|
|
vdev->devid = devid; |
|
vdev->speed = speed; |
|
vdev->ud.sockfd = sockfd; |
|
vdev->ud.tcp_socket = socket; |
|
vdev->ud.tcp_rx = tcp_rx; |
|
vdev->ud.tcp_tx = tcp_tx; |
|
vdev->ud.status = VDEV_ST_NOTASSIGNED; |
|
usbip_kcov_handle_init(&vdev->ud); |
|
|
|
spin_unlock(&vdev->ud.lock); |
|
spin_unlock_irqrestore(&vhci->lock, flags); |
|
/* end the lock */ |
|
|
|
wake_up_process(vdev->ud.tcp_rx); |
|
wake_up_process(vdev->ud.tcp_tx); |
|
|
|
rh_port_connect(vdev, speed); |
|
|
|
dev_info(dev, "Device attached\n"); |
|
|
|
mutex_unlock(&vdev->ud.sysfs_lock); |
|
|
|
return count; |
|
|
|
unlock_mutex: |
|
mutex_unlock(&vdev->ud.sysfs_lock); |
|
return err; |
|
} |
|
static DEVICE_ATTR_WO(attach); |
|
|
|
#define MAX_STATUS_NAME 16 |
|
|
|
struct status_attr { |
|
struct device_attribute attr; |
|
char name[MAX_STATUS_NAME+1]; |
|
}; |
|
|
|
static struct status_attr *status_attrs; |
|
|
|
static void set_status_attr(int id) |
|
{ |
|
struct status_attr *status; |
|
|
|
status = status_attrs + id; |
|
if (id == 0) |
|
strcpy(status->name, "status"); |
|
else |
|
snprintf(status->name, MAX_STATUS_NAME+1, "status.%d", id); |
|
status->attr.attr.name = status->name; |
|
status->attr.attr.mode = S_IRUGO; |
|
status->attr.show = status_show; |
|
sysfs_attr_init(&status->attr.attr); |
|
} |
|
|
|
static int init_status_attrs(void) |
|
{ |
|
int id; |
|
|
|
status_attrs = kcalloc(vhci_num_controllers, sizeof(struct status_attr), |
|
GFP_KERNEL); |
|
if (status_attrs == NULL) |
|
return -ENOMEM; |
|
|
|
for (id = 0; id < vhci_num_controllers; id++) |
|
set_status_attr(id); |
|
|
|
return 0; |
|
} |
|
|
|
static void finish_status_attrs(void) |
|
{ |
|
kfree(status_attrs); |
|
} |
|
|
|
struct attribute_group vhci_attr_group = { |
|
.attrs = NULL, |
|
}; |
|
|
|
int vhci_init_attr_group(void) |
|
{ |
|
struct attribute **attrs; |
|
int ret, i; |
|
|
|
attrs = kcalloc((vhci_num_controllers + 5), sizeof(struct attribute *), |
|
GFP_KERNEL); |
|
if (attrs == NULL) |
|
return -ENOMEM; |
|
|
|
ret = init_status_attrs(); |
|
if (ret) { |
|
kfree(attrs); |
|
return ret; |
|
} |
|
*attrs = &dev_attr_nports.attr; |
|
*(attrs + 1) = &dev_attr_detach.attr; |
|
*(attrs + 2) = &dev_attr_attach.attr; |
|
*(attrs + 3) = &dev_attr_usbip_debug.attr; |
|
for (i = 0; i < vhci_num_controllers; i++) |
|
*(attrs + i + 4) = &((status_attrs + i)->attr.attr); |
|
vhci_attr_group.attrs = attrs; |
|
return 0; |
|
} |
|
|
|
void vhci_finish_attr_group(void) |
|
{ |
|
finish_status_attrs(); |
|
kfree(vhci_attr_group.attrs); |
|
}
|
|
|