mirror of
https://github.com/AuxXxilium/linux_dsm_epyc7002.git
synced 2024-12-25 01:10:20 +07:00
833415a3e7
The driver enforces a strict one-to-one relationship between the received RESPONSE_AVAILABLE notifications and messages read from the device. At the same time, it will cancel the interrupt URB when there is no client holding the character device open. Many devices do not cope well with this behaviour. They maintain a FIFO queue of messages, and send notifications on a best effort basis. Messages are queued regardless of whether the notification is successful or not. So if the driver loses a single notification, which can easily happen when the interrupt URB is cancelled, then the device and driver becomes out-of-sync. New messages end up at the end of the queue, while the associated notification makes the driver read only the first message from the queue. This state is permanent from a user point of view. There is no no way to flush the device queue without resetting the device or using another driver. The problem is easy to hit with current QMI and MBIM command line tools, which typically close the character device after seeing the reply they expect. Any pending unsolicited messages from the device will then trigger the driver bug. Fix by always reading all queued messages from the device when the notification URB is first submitted. This is expected to end with an -EPIPE status when there are no more pending messages, so demote the printk associated with -EPIPE to debug level. The workaround has been tested on a large number of different MBIM and QMI devices, as well as the Ericsson F5521gw and H5321gw modems with real Device Management functions. Signed-off-by: Bjørn Mork <bjorn@mork.no> Acked-by: Oliver Neukum <oneukum@suse.com> Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
1192 lines
30 KiB
C
1192 lines
30 KiB
C
/*
|
|
* cdc-wdm.c
|
|
*
|
|
* This driver supports USB CDC WCM Device Management.
|
|
*
|
|
* Copyright (c) 2007-2009 Oliver Neukum
|
|
*
|
|
* Some code taken from cdc-acm.c
|
|
*
|
|
* Released under the GPLv2.
|
|
*
|
|
* Many thanks to Carl Nordbeck
|
|
*/
|
|
#include <linux/kernel.h>
|
|
#include <linux/errno.h>
|
|
#include <linux/ioctl.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/module.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/uaccess.h>
|
|
#include <linux/bitops.h>
|
|
#include <linux/poll.h>
|
|
#include <linux/usb.h>
|
|
#include <linux/usb/cdc.h>
|
|
#include <asm/byteorder.h>
|
|
#include <asm/unaligned.h>
|
|
#include <linux/usb/cdc-wdm.h>
|
|
|
|
/*
|
|
* Version Information
|
|
*/
|
|
#define DRIVER_VERSION "v0.03"
|
|
#define DRIVER_AUTHOR "Oliver Neukum"
|
|
#define DRIVER_DESC "USB Abstract Control Model driver for USB WCM Device Management"
|
|
|
|
static const struct usb_device_id wdm_ids[] = {
|
|
{
|
|
.match_flags = USB_DEVICE_ID_MATCH_INT_CLASS |
|
|
USB_DEVICE_ID_MATCH_INT_SUBCLASS,
|
|
.bInterfaceClass = USB_CLASS_COMM,
|
|
.bInterfaceSubClass = USB_CDC_SUBCLASS_DMM
|
|
},
|
|
{ }
|
|
};
|
|
|
|
MODULE_DEVICE_TABLE (usb, wdm_ids);
|
|
|
|
#define WDM_MINOR_BASE 176
|
|
|
|
|
|
#define WDM_IN_USE 1
|
|
#define WDM_DISCONNECTING 2
|
|
#define WDM_RESULT 3
|
|
#define WDM_READ 4
|
|
#define WDM_INT_STALL 5
|
|
#define WDM_POLL_RUNNING 6
|
|
#define WDM_RESPONDING 7
|
|
#define WDM_SUSPENDING 8
|
|
#define WDM_RESETTING 9
|
|
#define WDM_OVERFLOW 10
|
|
#define WDM_DRAIN_ON_OPEN 11
|
|
|
|
#define WDM_MAX 16
|
|
|
|
/* CDC-WMC r1.1 requires wMaxCommand to be "at least 256 decimal (0x100)" */
|
|
#define WDM_DEFAULT_BUFSIZE 256
|
|
|
|
static DEFINE_MUTEX(wdm_mutex);
|
|
static DEFINE_SPINLOCK(wdm_device_list_lock);
|
|
static LIST_HEAD(wdm_device_list);
|
|
|
|
/* --- method tables --- */
|
|
|
|
struct wdm_device {
|
|
u8 *inbuf; /* buffer for response */
|
|
u8 *outbuf; /* buffer for command */
|
|
u8 *sbuf; /* buffer for status */
|
|
u8 *ubuf; /* buffer for copy to user space */
|
|
|
|
struct urb *command;
|
|
struct urb *response;
|
|
struct urb *validity;
|
|
struct usb_interface *intf;
|
|
struct usb_ctrlrequest *orq;
|
|
struct usb_ctrlrequest *irq;
|
|
spinlock_t iuspin;
|
|
|
|
unsigned long flags;
|
|
u16 bufsize;
|
|
u16 wMaxCommand;
|
|
u16 wMaxPacketSize;
|
|
__le16 inum;
|
|
int reslength;
|
|
int length;
|
|
int read;
|
|
int count;
|
|
dma_addr_t shandle;
|
|
dma_addr_t ihandle;
|
|
struct mutex wlock;
|
|
struct mutex rlock;
|
|
wait_queue_head_t wait;
|
|
struct work_struct rxwork;
|
|
int werr;
|
|
int rerr;
|
|
int resp_count;
|
|
|
|
struct list_head device_list;
|
|
int (*manage_power)(struct usb_interface *, int);
|
|
};
|
|
|
|
static struct usb_driver wdm_driver;
|
|
|
|
/* return intfdata if we own the interface, else look up intf in the list */
|
|
static struct wdm_device *wdm_find_device(struct usb_interface *intf)
|
|
{
|
|
struct wdm_device *desc;
|
|
|
|
spin_lock(&wdm_device_list_lock);
|
|
list_for_each_entry(desc, &wdm_device_list, device_list)
|
|
if (desc->intf == intf)
|
|
goto found;
|
|
desc = NULL;
|
|
found:
|
|
spin_unlock(&wdm_device_list_lock);
|
|
|
|
return desc;
|
|
}
|
|
|
|
static struct wdm_device *wdm_find_device_by_minor(int minor)
|
|
{
|
|
struct wdm_device *desc;
|
|
|
|
spin_lock(&wdm_device_list_lock);
|
|
list_for_each_entry(desc, &wdm_device_list, device_list)
|
|
if (desc->intf->minor == minor)
|
|
goto found;
|
|
desc = NULL;
|
|
found:
|
|
spin_unlock(&wdm_device_list_lock);
|
|
|
|
return desc;
|
|
}
|
|
|
|
/* --- callbacks --- */
|
|
static void wdm_out_callback(struct urb *urb)
|
|
{
|
|
struct wdm_device *desc;
|
|
desc = urb->context;
|
|
spin_lock(&desc->iuspin);
|
|
desc->werr = urb->status;
|
|
spin_unlock(&desc->iuspin);
|
|
kfree(desc->outbuf);
|
|
desc->outbuf = NULL;
|
|
clear_bit(WDM_IN_USE, &desc->flags);
|
|
wake_up(&desc->wait);
|
|
}
|
|
|
|
static void wdm_in_callback(struct urb *urb)
|
|
{
|
|
struct wdm_device *desc = urb->context;
|
|
int status = urb->status;
|
|
int length = urb->actual_length;
|
|
|
|
spin_lock(&desc->iuspin);
|
|
clear_bit(WDM_RESPONDING, &desc->flags);
|
|
|
|
if (status) {
|
|
switch (status) {
|
|
case -ENOENT:
|
|
dev_dbg(&desc->intf->dev,
|
|
"nonzero urb status received: -ENOENT");
|
|
goto skip_error;
|
|
case -ECONNRESET:
|
|
dev_dbg(&desc->intf->dev,
|
|
"nonzero urb status received: -ECONNRESET");
|
|
goto skip_error;
|
|
case -ESHUTDOWN:
|
|
dev_dbg(&desc->intf->dev,
|
|
"nonzero urb status received: -ESHUTDOWN");
|
|
goto skip_error;
|
|
case -EPIPE:
|
|
dev_dbg(&desc->intf->dev,
|
|
"nonzero urb status received: -EPIPE\n");
|
|
break;
|
|
default:
|
|
dev_err(&desc->intf->dev,
|
|
"Unexpected error %d\n", status);
|
|
break;
|
|
}
|
|
}
|
|
|
|
desc->rerr = status;
|
|
if (length + desc->length > desc->wMaxCommand) {
|
|
/* The buffer would overflow */
|
|
set_bit(WDM_OVERFLOW, &desc->flags);
|
|
} else {
|
|
/* we may already be in overflow */
|
|
if (!test_bit(WDM_OVERFLOW, &desc->flags)) {
|
|
memmove(desc->ubuf + desc->length, desc->inbuf, length);
|
|
desc->length += length;
|
|
desc->reslength = length;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Handling devices with the WDM_DRAIN_ON_OPEN flag set:
|
|
* If desc->resp_count is unset, then the urb was submitted
|
|
* without a prior notification. If the device returned any
|
|
* data, then this implies that it had messages queued without
|
|
* notifying us. Continue reading until that queue is flushed.
|
|
*/
|
|
if (!desc->resp_count) {
|
|
if (!length) {
|
|
/* do not propagate the expected -EPIPE */
|
|
desc->rerr = 0;
|
|
goto unlock;
|
|
}
|
|
dev_dbg(&desc->intf->dev, "got %d bytes without notification\n", length);
|
|
set_bit(WDM_RESPONDING, &desc->flags);
|
|
usb_submit_urb(desc->response, GFP_ATOMIC);
|
|
}
|
|
|
|
skip_error:
|
|
wake_up(&desc->wait);
|
|
|
|
set_bit(WDM_READ, &desc->flags);
|
|
unlock:
|
|
spin_unlock(&desc->iuspin);
|
|
}
|
|
|
|
static void wdm_int_callback(struct urb *urb)
|
|
{
|
|
int rv = 0;
|
|
int responding;
|
|
int status = urb->status;
|
|
struct wdm_device *desc;
|
|
struct usb_cdc_notification *dr;
|
|
|
|
desc = urb->context;
|
|
dr = (struct usb_cdc_notification *)desc->sbuf;
|
|
|
|
if (status) {
|
|
switch (status) {
|
|
case -ESHUTDOWN:
|
|
case -ENOENT:
|
|
case -ECONNRESET:
|
|
return; /* unplug */
|
|
case -EPIPE:
|
|
set_bit(WDM_INT_STALL, &desc->flags);
|
|
dev_err(&desc->intf->dev, "Stall on int endpoint\n");
|
|
goto sw; /* halt is cleared in work */
|
|
default:
|
|
dev_err(&desc->intf->dev,
|
|
"nonzero urb status received: %d\n", status);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (urb->actual_length < sizeof(struct usb_cdc_notification)) {
|
|
dev_err(&desc->intf->dev, "wdm_int_callback - %d bytes\n",
|
|
urb->actual_length);
|
|
goto exit;
|
|
}
|
|
|
|
switch (dr->bNotificationType) {
|
|
case USB_CDC_NOTIFY_RESPONSE_AVAILABLE:
|
|
dev_dbg(&desc->intf->dev,
|
|
"NOTIFY_RESPONSE_AVAILABLE received: index %d len %d",
|
|
le16_to_cpu(dr->wIndex), le16_to_cpu(dr->wLength));
|
|
break;
|
|
|
|
case USB_CDC_NOTIFY_NETWORK_CONNECTION:
|
|
|
|
dev_dbg(&desc->intf->dev,
|
|
"NOTIFY_NETWORK_CONNECTION %s network",
|
|
dr->wValue ? "connected to" : "disconnected from");
|
|
goto exit;
|
|
case USB_CDC_NOTIFY_SPEED_CHANGE:
|
|
dev_dbg(&desc->intf->dev, "SPEED_CHANGE received (len %u)",
|
|
urb->actual_length);
|
|
goto exit;
|
|
default:
|
|
clear_bit(WDM_POLL_RUNNING, &desc->flags);
|
|
dev_err(&desc->intf->dev,
|
|
"unknown notification %d received: index %d len %d\n",
|
|
dr->bNotificationType,
|
|
le16_to_cpu(dr->wIndex),
|
|
le16_to_cpu(dr->wLength));
|
|
goto exit;
|
|
}
|
|
|
|
spin_lock(&desc->iuspin);
|
|
responding = test_and_set_bit(WDM_RESPONDING, &desc->flags);
|
|
if (!desc->resp_count++ && !responding
|
|
&& !test_bit(WDM_DISCONNECTING, &desc->flags)
|
|
&& !test_bit(WDM_SUSPENDING, &desc->flags)) {
|
|
rv = usb_submit_urb(desc->response, GFP_ATOMIC);
|
|
dev_dbg(&desc->intf->dev, "%s: usb_submit_urb %d",
|
|
__func__, rv);
|
|
}
|
|
spin_unlock(&desc->iuspin);
|
|
if (rv < 0) {
|
|
clear_bit(WDM_RESPONDING, &desc->flags);
|
|
if (rv == -EPERM)
|
|
return;
|
|
if (rv == -ENOMEM) {
|
|
sw:
|
|
rv = schedule_work(&desc->rxwork);
|
|
if (rv)
|
|
dev_err(&desc->intf->dev,
|
|
"Cannot schedule work\n");
|
|
}
|
|
}
|
|
exit:
|
|
rv = usb_submit_urb(urb, GFP_ATOMIC);
|
|
if (rv)
|
|
dev_err(&desc->intf->dev,
|
|
"%s - usb_submit_urb failed with result %d\n",
|
|
__func__, rv);
|
|
|
|
}
|
|
|
|
static void kill_urbs(struct wdm_device *desc)
|
|
{
|
|
/* the order here is essential */
|
|
usb_kill_urb(desc->command);
|
|
usb_kill_urb(desc->validity);
|
|
usb_kill_urb(desc->response);
|
|
}
|
|
|
|
static void free_urbs(struct wdm_device *desc)
|
|
{
|
|
usb_free_urb(desc->validity);
|
|
usb_free_urb(desc->response);
|
|
usb_free_urb(desc->command);
|
|
}
|
|
|
|
static void cleanup(struct wdm_device *desc)
|
|
{
|
|
kfree(desc->sbuf);
|
|
kfree(desc->inbuf);
|
|
kfree(desc->orq);
|
|
kfree(desc->irq);
|
|
kfree(desc->ubuf);
|
|
free_urbs(desc);
|
|
kfree(desc);
|
|
}
|
|
|
|
static ssize_t wdm_write
|
|
(struct file *file, const char __user *buffer, size_t count, loff_t *ppos)
|
|
{
|
|
u8 *buf;
|
|
int rv = -EMSGSIZE, r, we;
|
|
struct wdm_device *desc = file->private_data;
|
|
struct usb_ctrlrequest *req;
|
|
|
|
if (count > desc->wMaxCommand)
|
|
count = desc->wMaxCommand;
|
|
|
|
spin_lock_irq(&desc->iuspin);
|
|
we = desc->werr;
|
|
desc->werr = 0;
|
|
spin_unlock_irq(&desc->iuspin);
|
|
if (we < 0)
|
|
return usb_translate_errors(we);
|
|
|
|
buf = kmalloc(count, GFP_KERNEL);
|
|
if (!buf) {
|
|
rv = -ENOMEM;
|
|
goto outnl;
|
|
}
|
|
|
|
r = copy_from_user(buf, buffer, count);
|
|
if (r > 0) {
|
|
rv = -EFAULT;
|
|
goto out_free_mem;
|
|
}
|
|
|
|
/* concurrent writes and disconnect */
|
|
r = mutex_lock_interruptible(&desc->wlock);
|
|
rv = -ERESTARTSYS;
|
|
if (r)
|
|
goto out_free_mem;
|
|
|
|
if (test_bit(WDM_DISCONNECTING, &desc->flags)) {
|
|
rv = -ENODEV;
|
|
goto out_free_mem_lock;
|
|
}
|
|
|
|
r = usb_autopm_get_interface(desc->intf);
|
|
if (r < 0) {
|
|
rv = usb_translate_errors(r);
|
|
goto out_free_mem_lock;
|
|
}
|
|
|
|
if (!(file->f_flags & O_NONBLOCK))
|
|
r = wait_event_interruptible(desc->wait, !test_bit(WDM_IN_USE,
|
|
&desc->flags));
|
|
else
|
|
if (test_bit(WDM_IN_USE, &desc->flags))
|
|
r = -EAGAIN;
|
|
|
|
if (test_bit(WDM_RESETTING, &desc->flags))
|
|
r = -EIO;
|
|
|
|
if (r < 0) {
|
|
rv = r;
|
|
goto out_free_mem_pm;
|
|
}
|
|
|
|
req = desc->orq;
|
|
usb_fill_control_urb(
|
|
desc->command,
|
|
interface_to_usbdev(desc->intf),
|
|
/* using common endpoint 0 */
|
|
usb_sndctrlpipe(interface_to_usbdev(desc->intf), 0),
|
|
(unsigned char *)req,
|
|
buf,
|
|
count,
|
|
wdm_out_callback,
|
|
desc
|
|
);
|
|
|
|
req->bRequestType = (USB_DIR_OUT | USB_TYPE_CLASS |
|
|
USB_RECIP_INTERFACE);
|
|
req->bRequest = USB_CDC_SEND_ENCAPSULATED_COMMAND;
|
|
req->wValue = 0;
|
|
req->wIndex = desc->inum; /* already converted */
|
|
req->wLength = cpu_to_le16(count);
|
|
set_bit(WDM_IN_USE, &desc->flags);
|
|
desc->outbuf = buf;
|
|
|
|
rv = usb_submit_urb(desc->command, GFP_KERNEL);
|
|
if (rv < 0) {
|
|
desc->outbuf = NULL;
|
|
clear_bit(WDM_IN_USE, &desc->flags);
|
|
dev_err(&desc->intf->dev, "Tx URB error: %d\n", rv);
|
|
rv = usb_translate_errors(rv);
|
|
goto out_free_mem_pm;
|
|
} else {
|
|
dev_dbg(&desc->intf->dev, "Tx URB has been submitted index=%d",
|
|
le16_to_cpu(req->wIndex));
|
|
}
|
|
|
|
usb_autopm_put_interface(desc->intf);
|
|
mutex_unlock(&desc->wlock);
|
|
outnl:
|
|
return rv < 0 ? rv : count;
|
|
|
|
out_free_mem_pm:
|
|
usb_autopm_put_interface(desc->intf);
|
|
out_free_mem_lock:
|
|
mutex_unlock(&desc->wlock);
|
|
out_free_mem:
|
|
kfree(buf);
|
|
return rv;
|
|
}
|
|
|
|
/*
|
|
* clear WDM_READ flag and possibly submit the read urb if resp_count
|
|
* is non-zero.
|
|
*
|
|
* Called with desc->iuspin locked
|
|
*/
|
|
static int clear_wdm_read_flag(struct wdm_device *desc)
|
|
{
|
|
int rv = 0;
|
|
|
|
clear_bit(WDM_READ, &desc->flags);
|
|
|
|
/* submit read urb only if the device is waiting for it */
|
|
if (!desc->resp_count || !--desc->resp_count)
|
|
goto out;
|
|
|
|
set_bit(WDM_RESPONDING, &desc->flags);
|
|
spin_unlock_irq(&desc->iuspin);
|
|
rv = usb_submit_urb(desc->response, GFP_KERNEL);
|
|
spin_lock_irq(&desc->iuspin);
|
|
if (rv) {
|
|
dev_err(&desc->intf->dev,
|
|
"usb_submit_urb failed with result %d\n", rv);
|
|
|
|
/* make sure the next notification trigger a submit */
|
|
clear_bit(WDM_RESPONDING, &desc->flags);
|
|
desc->resp_count = 0;
|
|
}
|
|
out:
|
|
return rv;
|
|
}
|
|
|
|
static ssize_t wdm_read
|
|
(struct file *file, char __user *buffer, size_t count, loff_t *ppos)
|
|
{
|
|
int rv, cntr;
|
|
int i = 0;
|
|
struct wdm_device *desc = file->private_data;
|
|
|
|
|
|
rv = mutex_lock_interruptible(&desc->rlock); /*concurrent reads */
|
|
if (rv < 0)
|
|
return -ERESTARTSYS;
|
|
|
|
cntr = ACCESS_ONCE(desc->length);
|
|
if (cntr == 0) {
|
|
desc->read = 0;
|
|
retry:
|
|
if (test_bit(WDM_DISCONNECTING, &desc->flags)) {
|
|
rv = -ENODEV;
|
|
goto err;
|
|
}
|
|
if (test_bit(WDM_OVERFLOW, &desc->flags)) {
|
|
clear_bit(WDM_OVERFLOW, &desc->flags);
|
|
rv = -ENOBUFS;
|
|
goto err;
|
|
}
|
|
i++;
|
|
if (file->f_flags & O_NONBLOCK) {
|
|
if (!test_bit(WDM_READ, &desc->flags)) {
|
|
rv = cntr ? cntr : -EAGAIN;
|
|
goto err;
|
|
}
|
|
rv = 0;
|
|
} else {
|
|
rv = wait_event_interruptible(desc->wait,
|
|
test_bit(WDM_READ, &desc->flags));
|
|
}
|
|
|
|
/* may have happened while we slept */
|
|
if (test_bit(WDM_DISCONNECTING, &desc->flags)) {
|
|
rv = -ENODEV;
|
|
goto err;
|
|
}
|
|
if (test_bit(WDM_RESETTING, &desc->flags)) {
|
|
rv = -EIO;
|
|
goto err;
|
|
}
|
|
usb_mark_last_busy(interface_to_usbdev(desc->intf));
|
|
if (rv < 0) {
|
|
rv = -ERESTARTSYS;
|
|
goto err;
|
|
}
|
|
|
|
spin_lock_irq(&desc->iuspin);
|
|
|
|
if (desc->rerr) { /* read completed, error happened */
|
|
rv = usb_translate_errors(desc->rerr);
|
|
desc->rerr = 0;
|
|
spin_unlock_irq(&desc->iuspin);
|
|
goto err;
|
|
}
|
|
/*
|
|
* recheck whether we've lost the race
|
|
* against the completion handler
|
|
*/
|
|
if (!test_bit(WDM_READ, &desc->flags)) { /* lost race */
|
|
spin_unlock_irq(&desc->iuspin);
|
|
goto retry;
|
|
}
|
|
|
|
if (!desc->reslength) { /* zero length read */
|
|
dev_dbg(&desc->intf->dev, "%s: zero length - clearing WDM_READ\n", __func__);
|
|
rv = clear_wdm_read_flag(desc);
|
|
spin_unlock_irq(&desc->iuspin);
|
|
if (rv < 0)
|
|
goto err;
|
|
goto retry;
|
|
}
|
|
cntr = desc->length;
|
|
spin_unlock_irq(&desc->iuspin);
|
|
}
|
|
|
|
if (cntr > count)
|
|
cntr = count;
|
|
rv = copy_to_user(buffer, desc->ubuf, cntr);
|
|
if (rv > 0) {
|
|
rv = -EFAULT;
|
|
goto err;
|
|
}
|
|
|
|
spin_lock_irq(&desc->iuspin);
|
|
|
|
for (i = 0; i < desc->length - cntr; i++)
|
|
desc->ubuf[i] = desc->ubuf[i + cntr];
|
|
|
|
desc->length -= cntr;
|
|
/* in case we had outstanding data */
|
|
if (!desc->length)
|
|
clear_wdm_read_flag(desc);
|
|
spin_unlock_irq(&desc->iuspin);
|
|
rv = cntr;
|
|
|
|
err:
|
|
mutex_unlock(&desc->rlock);
|
|
return rv;
|
|
}
|
|
|
|
static int wdm_flush(struct file *file, fl_owner_t id)
|
|
{
|
|
struct wdm_device *desc = file->private_data;
|
|
|
|
wait_event(desc->wait, !test_bit(WDM_IN_USE, &desc->flags));
|
|
|
|
/* cannot dereference desc->intf if WDM_DISCONNECTING */
|
|
if (desc->werr < 0 && !test_bit(WDM_DISCONNECTING, &desc->flags))
|
|
dev_err(&desc->intf->dev, "Error in flush path: %d\n",
|
|
desc->werr);
|
|
|
|
return usb_translate_errors(desc->werr);
|
|
}
|
|
|
|
static unsigned int wdm_poll(struct file *file, struct poll_table_struct *wait)
|
|
{
|
|
struct wdm_device *desc = file->private_data;
|
|
unsigned long flags;
|
|
unsigned int mask = 0;
|
|
|
|
spin_lock_irqsave(&desc->iuspin, flags);
|
|
if (test_bit(WDM_DISCONNECTING, &desc->flags)) {
|
|
mask = POLLHUP | POLLERR;
|
|
spin_unlock_irqrestore(&desc->iuspin, flags);
|
|
goto desc_out;
|
|
}
|
|
if (test_bit(WDM_READ, &desc->flags))
|
|
mask = POLLIN | POLLRDNORM;
|
|
if (desc->rerr || desc->werr)
|
|
mask |= POLLERR;
|
|
if (!test_bit(WDM_IN_USE, &desc->flags))
|
|
mask |= POLLOUT | POLLWRNORM;
|
|
spin_unlock_irqrestore(&desc->iuspin, flags);
|
|
|
|
poll_wait(file, &desc->wait, wait);
|
|
|
|
desc_out:
|
|
return mask;
|
|
}
|
|
|
|
static int wdm_open(struct inode *inode, struct file *file)
|
|
{
|
|
int minor = iminor(inode);
|
|
int rv = -ENODEV;
|
|
struct usb_interface *intf;
|
|
struct wdm_device *desc;
|
|
|
|
mutex_lock(&wdm_mutex);
|
|
desc = wdm_find_device_by_minor(minor);
|
|
if (!desc)
|
|
goto out;
|
|
|
|
intf = desc->intf;
|
|
if (test_bit(WDM_DISCONNECTING, &desc->flags))
|
|
goto out;
|
|
file->private_data = desc;
|
|
|
|
rv = usb_autopm_get_interface(desc->intf);
|
|
if (rv < 0) {
|
|
dev_err(&desc->intf->dev, "Error autopm - %d\n", rv);
|
|
goto out;
|
|
}
|
|
|
|
/* using write lock to protect desc->count */
|
|
mutex_lock(&desc->wlock);
|
|
if (!desc->count++) {
|
|
desc->werr = 0;
|
|
desc->rerr = 0;
|
|
rv = usb_submit_urb(desc->validity, GFP_KERNEL);
|
|
if (rv < 0) {
|
|
desc->count--;
|
|
dev_err(&desc->intf->dev,
|
|
"Error submitting int urb - %d\n", rv);
|
|
rv = usb_translate_errors(rv);
|
|
} else if (test_bit(WDM_DRAIN_ON_OPEN, &desc->flags)) {
|
|
/*
|
|
* Some devices keep pending messages queued
|
|
* without resending notifications. We must
|
|
* flush the message queue before we can
|
|
* assume a one-to-one relationship between
|
|
* notifications and messages in the queue
|
|
*/
|
|
dev_dbg(&desc->intf->dev, "draining queued data\n");
|
|
set_bit(WDM_RESPONDING, &desc->flags);
|
|
rv = usb_submit_urb(desc->response, GFP_KERNEL);
|
|
}
|
|
} else {
|
|
rv = 0;
|
|
}
|
|
mutex_unlock(&desc->wlock);
|
|
if (desc->count == 1)
|
|
desc->manage_power(intf, 1);
|
|
usb_autopm_put_interface(desc->intf);
|
|
out:
|
|
mutex_unlock(&wdm_mutex);
|
|
return rv;
|
|
}
|
|
|
|
static int wdm_release(struct inode *inode, struct file *file)
|
|
{
|
|
struct wdm_device *desc = file->private_data;
|
|
|
|
mutex_lock(&wdm_mutex);
|
|
|
|
/* using write lock to protect desc->count */
|
|
mutex_lock(&desc->wlock);
|
|
desc->count--;
|
|
mutex_unlock(&desc->wlock);
|
|
|
|
if (!desc->count) {
|
|
if (!test_bit(WDM_DISCONNECTING, &desc->flags)) {
|
|
dev_dbg(&desc->intf->dev, "wdm_release: cleanup");
|
|
kill_urbs(desc);
|
|
spin_lock_irq(&desc->iuspin);
|
|
desc->resp_count = 0;
|
|
spin_unlock_irq(&desc->iuspin);
|
|
desc->manage_power(desc->intf, 0);
|
|
} else {
|
|
/* must avoid dev_printk here as desc->intf is invalid */
|
|
pr_debug(KBUILD_MODNAME " %s: device gone - cleaning up\n", __func__);
|
|
cleanup(desc);
|
|
}
|
|
}
|
|
mutex_unlock(&wdm_mutex);
|
|
return 0;
|
|
}
|
|
|
|
static long wdm_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
|
|
{
|
|
struct wdm_device *desc = file->private_data;
|
|
int rv = 0;
|
|
|
|
switch (cmd) {
|
|
case IOCTL_WDM_MAX_COMMAND:
|
|
if (copy_to_user((void __user *)arg, &desc->wMaxCommand, sizeof(desc->wMaxCommand)))
|
|
rv = -EFAULT;
|
|
break;
|
|
default:
|
|
rv = -ENOTTY;
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
static const struct file_operations wdm_fops = {
|
|
.owner = THIS_MODULE,
|
|
.read = wdm_read,
|
|
.write = wdm_write,
|
|
.open = wdm_open,
|
|
.flush = wdm_flush,
|
|
.release = wdm_release,
|
|
.poll = wdm_poll,
|
|
.unlocked_ioctl = wdm_ioctl,
|
|
.compat_ioctl = wdm_ioctl,
|
|
.llseek = noop_llseek,
|
|
};
|
|
|
|
static struct usb_class_driver wdm_class = {
|
|
.name = "cdc-wdm%d",
|
|
.fops = &wdm_fops,
|
|
.minor_base = WDM_MINOR_BASE,
|
|
};
|
|
|
|
/* --- error handling --- */
|
|
static void wdm_rxwork(struct work_struct *work)
|
|
{
|
|
struct wdm_device *desc = container_of(work, struct wdm_device, rxwork);
|
|
unsigned long flags;
|
|
int rv = 0;
|
|
int responding;
|
|
|
|
spin_lock_irqsave(&desc->iuspin, flags);
|
|
if (test_bit(WDM_DISCONNECTING, &desc->flags)) {
|
|
spin_unlock_irqrestore(&desc->iuspin, flags);
|
|
} else {
|
|
responding = test_and_set_bit(WDM_RESPONDING, &desc->flags);
|
|
spin_unlock_irqrestore(&desc->iuspin, flags);
|
|
if (!responding)
|
|
rv = usb_submit_urb(desc->response, GFP_KERNEL);
|
|
if (rv < 0 && rv != -EPERM) {
|
|
spin_lock_irqsave(&desc->iuspin, flags);
|
|
clear_bit(WDM_RESPONDING, &desc->flags);
|
|
if (!test_bit(WDM_DISCONNECTING, &desc->flags))
|
|
schedule_work(&desc->rxwork);
|
|
spin_unlock_irqrestore(&desc->iuspin, flags);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* --- hotplug --- */
|
|
|
|
static int wdm_create(struct usb_interface *intf, struct usb_endpoint_descriptor *ep,
|
|
u16 bufsize, int (*manage_power)(struct usb_interface *, int),
|
|
bool drain_on_open)
|
|
{
|
|
int rv = -ENOMEM;
|
|
struct wdm_device *desc;
|
|
|
|
desc = kzalloc(sizeof(struct wdm_device), GFP_KERNEL);
|
|
if (!desc)
|
|
goto out;
|
|
INIT_LIST_HEAD(&desc->device_list);
|
|
mutex_init(&desc->rlock);
|
|
mutex_init(&desc->wlock);
|
|
spin_lock_init(&desc->iuspin);
|
|
init_waitqueue_head(&desc->wait);
|
|
desc->wMaxCommand = bufsize;
|
|
/* this will be expanded and needed in hardware endianness */
|
|
desc->inum = cpu_to_le16((u16)intf->cur_altsetting->desc.bInterfaceNumber);
|
|
desc->intf = intf;
|
|
INIT_WORK(&desc->rxwork, wdm_rxwork);
|
|
|
|
rv = -EINVAL;
|
|
if (!usb_endpoint_is_int_in(ep))
|
|
goto err;
|
|
|
|
desc->wMaxPacketSize = usb_endpoint_maxp(ep);
|
|
|
|
desc->orq = kmalloc(sizeof(struct usb_ctrlrequest), GFP_KERNEL);
|
|
if (!desc->orq)
|
|
goto err;
|
|
desc->irq = kmalloc(sizeof(struct usb_ctrlrequest), GFP_KERNEL);
|
|
if (!desc->irq)
|
|
goto err;
|
|
|
|
desc->validity = usb_alloc_urb(0, GFP_KERNEL);
|
|
if (!desc->validity)
|
|
goto err;
|
|
|
|
desc->response = usb_alloc_urb(0, GFP_KERNEL);
|
|
if (!desc->response)
|
|
goto err;
|
|
|
|
desc->command = usb_alloc_urb(0, GFP_KERNEL);
|
|
if (!desc->command)
|
|
goto err;
|
|
|
|
desc->ubuf = kmalloc(desc->wMaxCommand, GFP_KERNEL);
|
|
if (!desc->ubuf)
|
|
goto err;
|
|
|
|
desc->sbuf = kmalloc(desc->wMaxPacketSize, GFP_KERNEL);
|
|
if (!desc->sbuf)
|
|
goto err;
|
|
|
|
desc->inbuf = kmalloc(desc->wMaxCommand, GFP_KERNEL);
|
|
if (!desc->inbuf)
|
|
goto err;
|
|
|
|
usb_fill_int_urb(
|
|
desc->validity,
|
|
interface_to_usbdev(intf),
|
|
usb_rcvintpipe(interface_to_usbdev(intf), ep->bEndpointAddress),
|
|
desc->sbuf,
|
|
desc->wMaxPacketSize,
|
|
wdm_int_callback,
|
|
desc,
|
|
ep->bInterval
|
|
);
|
|
|
|
desc->irq->bRequestType = (USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE);
|
|
desc->irq->bRequest = USB_CDC_GET_ENCAPSULATED_RESPONSE;
|
|
desc->irq->wValue = 0;
|
|
desc->irq->wIndex = desc->inum; /* already converted */
|
|
desc->irq->wLength = cpu_to_le16(desc->wMaxCommand);
|
|
|
|
usb_fill_control_urb(
|
|
desc->response,
|
|
interface_to_usbdev(intf),
|
|
/* using common endpoint 0 */
|
|
usb_rcvctrlpipe(interface_to_usbdev(desc->intf), 0),
|
|
(unsigned char *)desc->irq,
|
|
desc->inbuf,
|
|
desc->wMaxCommand,
|
|
wdm_in_callback,
|
|
desc
|
|
);
|
|
|
|
desc->manage_power = manage_power;
|
|
|
|
/*
|
|
* "drain_on_open" enables a hack to work around a firmware
|
|
* issue observed on network functions, in particular MBIM
|
|
* functions.
|
|
*
|
|
* Quoting section 7 of the CDC-WMC r1.1 specification:
|
|
*
|
|
* "The firmware shall interpret GetEncapsulatedResponse as a
|
|
* request to read response bytes. The firmware shall send
|
|
* the next wLength bytes from the response. The firmware
|
|
* shall allow the host to retrieve data using any number of
|
|
* GetEncapsulatedResponse requests. The firmware shall
|
|
* return a zero- length reply if there are no data bytes
|
|
* available.
|
|
*
|
|
* The firmware shall send ResponseAvailable notifications
|
|
* periodically, using any appropriate algorithm, to inform
|
|
* the host that there is data available in the reply
|
|
* buffer. The firmware is allowed to send ResponseAvailable
|
|
* notifications even if there is no data available, but
|
|
* this will obviously reduce overall performance."
|
|
*
|
|
* These requirements, although they make equally sense, are
|
|
* often not implemented by network functions. Some firmwares
|
|
* will queue data indefinitely, without ever resending a
|
|
* notification. The result is that the driver and firmware
|
|
* loses "syncronization" if the driver ever fails to respond
|
|
* to a single notification, something which easily can happen
|
|
* on release(). When this happens, the driver will appear to
|
|
* never receive notifications for the most current data. Each
|
|
* notification will only cause a single read, which returns
|
|
* the oldest data in the firmware's queue.
|
|
*
|
|
* The "drain_on_open" hack resolves the situation by draining
|
|
* data from the firmware until none is returned, without a
|
|
* prior notification.
|
|
*
|
|
* This will inevitably race with the firmware, risking that
|
|
* we read data from the device before handling the associated
|
|
* notification. To make things worse, some of the devices
|
|
* needing the hack do not implement the "return zero if no
|
|
* data is available" requirement either. Instead they return
|
|
* an error on the subsequent read in this case. This means
|
|
* that "winning" the race can cause an unexpected EIO to
|
|
* userspace.
|
|
*
|
|
* "winning" the race is more likely on resume() than on
|
|
* open(), and the unexpected error is more harmful in the
|
|
* middle of an open session. The hack is therefore only
|
|
* applied on open(), and not on resume() where it logically
|
|
* would be equally necessary. So we define open() as the only
|
|
* driver <-> device "syncronization point". Should we happen
|
|
* to lose a notification after open(), then syncronization
|
|
* will be lost until release()
|
|
*
|
|
* The hack should not be enabled for CDC WDM devices
|
|
* conforming to the CDC-WMC r1.1 specification. This is
|
|
* ensured by setting drain_on_open to false in wdm_probe().
|
|
*/
|
|
if (drain_on_open)
|
|
set_bit(WDM_DRAIN_ON_OPEN, &desc->flags);
|
|
|
|
spin_lock(&wdm_device_list_lock);
|
|
list_add(&desc->device_list, &wdm_device_list);
|
|
spin_unlock(&wdm_device_list_lock);
|
|
|
|
rv = usb_register_dev(intf, &wdm_class);
|
|
if (rv < 0)
|
|
goto err;
|
|
else
|
|
dev_info(&intf->dev, "%s: USB WDM device\n", dev_name(intf->usb_dev));
|
|
out:
|
|
return rv;
|
|
err:
|
|
spin_lock(&wdm_device_list_lock);
|
|
list_del(&desc->device_list);
|
|
spin_unlock(&wdm_device_list_lock);
|
|
cleanup(desc);
|
|
return rv;
|
|
}
|
|
|
|
static int wdm_manage_power(struct usb_interface *intf, int on)
|
|
{
|
|
/* need autopm_get/put here to ensure the usbcore sees the new value */
|
|
int rv = usb_autopm_get_interface(intf);
|
|
|
|
intf->needs_remote_wakeup = on;
|
|
if (!rv)
|
|
usb_autopm_put_interface(intf);
|
|
return 0;
|
|
}
|
|
|
|
static int wdm_probe(struct usb_interface *intf, const struct usb_device_id *id)
|
|
{
|
|
int rv = -EINVAL;
|
|
struct usb_host_interface *iface;
|
|
struct usb_endpoint_descriptor *ep;
|
|
struct usb_cdc_parsed_header hdr;
|
|
u8 *buffer = intf->altsetting->extra;
|
|
int buflen = intf->altsetting->extralen;
|
|
u16 maxcom = WDM_DEFAULT_BUFSIZE;
|
|
|
|
if (!buffer)
|
|
goto err;
|
|
|
|
cdc_parse_cdc_header(&hdr, intf, buffer, buflen);
|
|
|
|
if (hdr.usb_cdc_dmm_desc)
|
|
maxcom = le16_to_cpu(hdr.usb_cdc_dmm_desc->wMaxCommand);
|
|
|
|
iface = intf->cur_altsetting;
|
|
if (iface->desc.bNumEndpoints != 1)
|
|
goto err;
|
|
ep = &iface->endpoint[0].desc;
|
|
|
|
rv = wdm_create(intf, ep, maxcom, &wdm_manage_power, false);
|
|
|
|
err:
|
|
return rv;
|
|
}
|
|
|
|
/**
|
|
* usb_cdc_wdm_register - register a WDM subdriver
|
|
* @intf: usb interface the subdriver will associate with
|
|
* @ep: interrupt endpoint to monitor for notifications
|
|
* @bufsize: maximum message size to support for read/write
|
|
*
|
|
* Create WDM usb class character device and associate it with intf
|
|
* without binding, allowing another driver to manage the interface.
|
|
*
|
|
* The subdriver will manage the given interrupt endpoint exclusively
|
|
* and will issue control requests referring to the given intf. It
|
|
* will otherwise avoid interferring, and in particular not do
|
|
* usb_set_intfdata/usb_get_intfdata on intf.
|
|
*
|
|
* The return value is a pointer to the subdriver's struct usb_driver.
|
|
* The registering driver is responsible for calling this subdriver's
|
|
* disconnect, suspend, resume, pre_reset and post_reset methods from
|
|
* its own.
|
|
*/
|
|
struct usb_driver *usb_cdc_wdm_register(struct usb_interface *intf,
|
|
struct usb_endpoint_descriptor *ep,
|
|
int bufsize,
|
|
int (*manage_power)(struct usb_interface *, int))
|
|
{
|
|
int rv = -EINVAL;
|
|
|
|
rv = wdm_create(intf, ep, bufsize, manage_power, true);
|
|
if (rv < 0)
|
|
goto err;
|
|
|
|
return &wdm_driver;
|
|
err:
|
|
return ERR_PTR(rv);
|
|
}
|
|
EXPORT_SYMBOL(usb_cdc_wdm_register);
|
|
|
|
static void wdm_disconnect(struct usb_interface *intf)
|
|
{
|
|
struct wdm_device *desc;
|
|
unsigned long flags;
|
|
|
|
usb_deregister_dev(intf, &wdm_class);
|
|
desc = wdm_find_device(intf);
|
|
mutex_lock(&wdm_mutex);
|
|
|
|
/* the spinlock makes sure no new urbs are generated in the callbacks */
|
|
spin_lock_irqsave(&desc->iuspin, flags);
|
|
set_bit(WDM_DISCONNECTING, &desc->flags);
|
|
set_bit(WDM_READ, &desc->flags);
|
|
/* to terminate pending flushes */
|
|
clear_bit(WDM_IN_USE, &desc->flags);
|
|
spin_unlock_irqrestore(&desc->iuspin, flags);
|
|
wake_up_all(&desc->wait);
|
|
mutex_lock(&desc->rlock);
|
|
mutex_lock(&desc->wlock);
|
|
kill_urbs(desc);
|
|
cancel_work_sync(&desc->rxwork);
|
|
mutex_unlock(&desc->wlock);
|
|
mutex_unlock(&desc->rlock);
|
|
|
|
/* the desc->intf pointer used as list key is now invalid */
|
|
spin_lock(&wdm_device_list_lock);
|
|
list_del(&desc->device_list);
|
|
spin_unlock(&wdm_device_list_lock);
|
|
|
|
if (!desc->count)
|
|
cleanup(desc);
|
|
else
|
|
dev_dbg(&intf->dev, "%s: %d open files - postponing cleanup\n", __func__, desc->count);
|
|
mutex_unlock(&wdm_mutex);
|
|
}
|
|
|
|
#ifdef CONFIG_PM
|
|
static int wdm_suspend(struct usb_interface *intf, pm_message_t message)
|
|
{
|
|
struct wdm_device *desc = wdm_find_device(intf);
|
|
int rv = 0;
|
|
|
|
dev_dbg(&desc->intf->dev, "wdm%d_suspend\n", intf->minor);
|
|
|
|
/* if this is an autosuspend the caller does the locking */
|
|
if (!PMSG_IS_AUTO(message)) {
|
|
mutex_lock(&desc->rlock);
|
|
mutex_lock(&desc->wlock);
|
|
}
|
|
spin_lock_irq(&desc->iuspin);
|
|
|
|
if (PMSG_IS_AUTO(message) &&
|
|
(test_bit(WDM_IN_USE, &desc->flags)
|
|
|| test_bit(WDM_RESPONDING, &desc->flags))) {
|
|
spin_unlock_irq(&desc->iuspin);
|
|
rv = -EBUSY;
|
|
} else {
|
|
|
|
set_bit(WDM_SUSPENDING, &desc->flags);
|
|
spin_unlock_irq(&desc->iuspin);
|
|
/* callback submits work - order is essential */
|
|
kill_urbs(desc);
|
|
cancel_work_sync(&desc->rxwork);
|
|
}
|
|
if (!PMSG_IS_AUTO(message)) {
|
|
mutex_unlock(&desc->wlock);
|
|
mutex_unlock(&desc->rlock);
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
#endif
|
|
|
|
static int recover_from_urb_loss(struct wdm_device *desc)
|
|
{
|
|
int rv = 0;
|
|
|
|
if (desc->count) {
|
|
rv = usb_submit_urb(desc->validity, GFP_NOIO);
|
|
if (rv < 0)
|
|
dev_err(&desc->intf->dev,
|
|
"Error resume submitting int urb - %d\n", rv);
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
#ifdef CONFIG_PM
|
|
static int wdm_resume(struct usb_interface *intf)
|
|
{
|
|
struct wdm_device *desc = wdm_find_device(intf);
|
|
int rv;
|
|
|
|
dev_dbg(&desc->intf->dev, "wdm%d_resume\n", intf->minor);
|
|
|
|
clear_bit(WDM_SUSPENDING, &desc->flags);
|
|
rv = recover_from_urb_loss(desc);
|
|
|
|
return rv;
|
|
}
|
|
#endif
|
|
|
|
static int wdm_pre_reset(struct usb_interface *intf)
|
|
{
|
|
struct wdm_device *desc = wdm_find_device(intf);
|
|
|
|
/*
|
|
* we notify everybody using poll of
|
|
* an exceptional situation
|
|
* must be done before recovery lest a spontaneous
|
|
* message from the device is lost
|
|
*/
|
|
spin_lock_irq(&desc->iuspin);
|
|
set_bit(WDM_RESETTING, &desc->flags); /* inform read/write */
|
|
set_bit(WDM_READ, &desc->flags); /* unblock read */
|
|
clear_bit(WDM_IN_USE, &desc->flags); /* unblock write */
|
|
desc->rerr = -EINTR;
|
|
spin_unlock_irq(&desc->iuspin);
|
|
wake_up_all(&desc->wait);
|
|
mutex_lock(&desc->rlock);
|
|
mutex_lock(&desc->wlock);
|
|
kill_urbs(desc);
|
|
cancel_work_sync(&desc->rxwork);
|
|
return 0;
|
|
}
|
|
|
|
static int wdm_post_reset(struct usb_interface *intf)
|
|
{
|
|
struct wdm_device *desc = wdm_find_device(intf);
|
|
int rv;
|
|
|
|
clear_bit(WDM_OVERFLOW, &desc->flags);
|
|
clear_bit(WDM_RESETTING, &desc->flags);
|
|
rv = recover_from_urb_loss(desc);
|
|
mutex_unlock(&desc->wlock);
|
|
mutex_unlock(&desc->rlock);
|
|
return 0;
|
|
}
|
|
|
|
static struct usb_driver wdm_driver = {
|
|
.name = "cdc_wdm",
|
|
.probe = wdm_probe,
|
|
.disconnect = wdm_disconnect,
|
|
#ifdef CONFIG_PM
|
|
.suspend = wdm_suspend,
|
|
.resume = wdm_resume,
|
|
.reset_resume = wdm_resume,
|
|
#endif
|
|
.pre_reset = wdm_pre_reset,
|
|
.post_reset = wdm_post_reset,
|
|
.id_table = wdm_ids,
|
|
.supports_autosuspend = 1,
|
|
.disable_hub_initiated_lpm = 1,
|
|
};
|
|
|
|
module_usb_driver(wdm_driver);
|
|
|
|
MODULE_AUTHOR(DRIVER_AUTHOR);
|
|
MODULE_DESCRIPTION(DRIVER_DESC);
|
|
MODULE_LICENSE("GPL");
|