This is a fix for race condition in usb-serial.c
between serial_open and usb_serial_disconnect
(kernel version 2.6.21-rc3)
Prior to this patch instances of usb_serial were not
ref-counted consistently, which caused serial_open
to reuse already deleted instance.
For more details search Web for
"Possible race condition in usb-serial.c"
discussion in Dec 2006.
The present patch ensures that instances
of usb_serial are ref-counted consistently
by calling kref_get in get_free_serial
and usb_serial_put in return_serial.
Another problem was that the code prior to
this patch called serial->type->shutdown(serial)
from destroy_serial, which may execute after
usb_serial_disconnect (if the serial device was opened).
This was an error because no IO should be done
on a USB device after usb_serial_disconnect returns.
To fix that the present patch moved call to
serial->type->shutdown(serial)
from destroy_serial to usb_serial_disconnect.
Also it introduces functions
serial_lock, serial_unlock and
serial_lock_and_wait_before_shutdown.
serial_lock and serial_unlock are called
from each serial_* routine and
serial_lock_and_wait_before_shutdown
is called from usb_serial_disconnect
to ensure that all serial_* calls exit before
usb_serial_disconnect calls
serial->type->shutdown(serial),
Signed-off-by: John Green <[EMAIL PROTECTED]>
---
diff -uprN a/drivers/usb/serial/usb-serial.c b/drivers/usb/serial/usb-serial.c
--- a/drivers/usb/serial/usb-serial.c 2007-03-15 15:56:56.848654500 -0800
+++ b/drivers/usb/serial/usb-serial.c 2007-03-19 11:43:13.236067600 -0800
@@ -62,6 +62,31 @@ static struct usb_serial *serial_table[S
static spinlock_t table_lock;
static LIST_HEAD(usb_serial_driver_list);
+/*
+ usb_serial objects are ref counted. This is necessary because pointers
to an object
+ may be stored in several places. Each time a pointer is duplicated, the
ref count
+ should be incremented. This done using kref_get(&serial->kref). When
pointer is
+ destroyed, the count should be decremented using usb_serial_put(serial).
+ A typical life cycle of usb_serial:
+
+ 1. usb_serial_probe creates usb_serial (ref count set to 1) and stores
the pointer
+ in the usb_interface using usb_set_intfdata.
+ 2. usb_serial_probe calls get_free_serial, which copies the pointer one
or more times
+ to serial_table indexed by TTY minor index (ref count is incremeted
for each copy).
+ 3. serial_open takes this pointer from serial_table (ref count
incremented) and passes it to TTY
+ (this is done indirectly: tty->driver_data = port, but port->serial
= serial).
+ 4. serial_close decrements the ref count (because tty object is
destroyed).
+ 5. usb_serial_disconnect calls return_serial, which erases all pointers
to the given serial
+ from serial_table (decrementing the ref count).
+ 6. usb_serial_disconnect destroys the pointer, which was stored in
usb_interface
+ (decrementing the ref count).
+ At this point the ref count normally reaches 0 and the object
destroyed.
+ Alternatively, serial_close may be called after usb_serial_disconnect
and the object destroyed
+ at that time.
+*/
+
+void usb_serial_put(struct usb_serial *serial);
+
struct usb_serial *usb_serial_get_by_index(unsigned index)
{
struct usb_serial *serial;
@@ -100,8 +125,10 @@ static struct usb_serial *get_free_seria
*minor = i;
dbg("%s - minor base = %d", __FUNCTION__, *minor);
- for (i = *minor; (i < (*minor + num_ports)) && (i <
SERIAL_TTY_MINORS); ++i)
+ for (i = *minor; (i < (*minor + num_ports)) && (i <
SERIAL_TTY_MINORS); ++i) {
+ kref_get(&serial->kref);
serial_table[i] = serial;
+ }
spin_unlock(&table_lock);
return serial;
}
@@ -121,10 +148,65 @@ static void return_serial(struct usb_ser
spin_lock(&table_lock);
for (i = 0; i < serial->num_ports; ++i) {
serial_table[serial->minor + i] = NULL;
+ usb_serial_put(serial);
}
spin_unlock(&table_lock);
}
+/*
+ * serial_lock expects a valid usb_serial * serial pointer.
+ * serial_lock returns 1 if usb_serial is still active and the caller can use
it.
+ * In this case the caller must call serial_unlock.
+ * serial_lock returns 0 if disconnection routine was already called on the
device.
+ * In this case the caller should not perform any IO operations, but
should return an error code.
+ */
+int serial_lock(struct usb_serial * serial)
+{
+ int ret;
+ unsigned long flags;
+
+ spin_lock_irqsave(&serial->lock, flags);
+ if (serial->shutdown_called) {
+ ret = 0;
+ } else {
+ serial->lock_count++;
+ ret = 1;
+ }
+ spin_unlock_irqrestore(&serial->lock, flags);
+ return ret;
+}
+
+void serial_unlock(struct usb_serial * serial)
+{
+ int wakeup = 0;
+ unsigned long flags;
+
+ spin_lock_irqsave(&serial->lock, flags);
+ serial->lock_count--;
+ if (serial->shutdown_called && serial->lock_count == 0) {
+ wakeup = 1;
+ }
+ spin_unlock_irqrestore(&serial->lock, flags);
+
+ if(wakeup) {
+ wake_up(&serial->shutdown_wait);
+ }
+}
+
+void serial_lock_and_wait_before_shutdown(struct usb_serial * serial)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&serial->lock, flags);
+ serial->shutdown_called = 1;
+ while(serial->lock_count > 0) {
+ spin_unlock_irqrestore(&serial->lock, flags);
+ wait_event(serial->shutdown_wait, serial->lock_count == 0);
+ spin_lock_irqsave(&serial->lock, flags);
+ }
+ spin_unlock_irqrestore(&serial->lock, flags);
+}
+
static void destroy_serial(struct kref *kref)
{
struct usb_serial *serial;
@@ -135,11 +217,6 @@ static void destroy_serial(struct kref *
dbg("%s - %s", __FUNCTION__, serial->type->description);
- serial->type->shutdown(serial);
-
- /* return the minor range that this device had */
- return_serial(serial);
-
for (i = 0; i < serial->num_ports; ++i)
serial->port[i]->open_count = 0;
@@ -294,8 +371,13 @@ static int serial_write (struct tty_stru
goto exit;
}
- /* pass on to the driver specific version of this function */
- retval = port->serial->type->write(port, buf, count);
+ if(serial_lock(port->serial)) {
+ /* pass on to the driver specific version of this function */
+ retval = port->serial->type->write(port, buf, count);
+ serial_unlock(port->serial);
+ } else {
+ retval = -ENODEV;
+ }
exit:
return retval;
@@ -316,9 +398,13 @@ static int serial_write_room (struct tty
goto exit;
}
- /* pass on to the driver specific version of this function */
- retval = port->serial->type->write_room(port);
-
+ if(serial_lock(port->serial)) {
+ /* pass on to the driver specific version of this function */
+ retval = port->serial->type->write_room(port);
+ serial_unlock(port->serial);
+ } else {
+ retval = -ENODEV;
+ }
exit:
return retval;
}
@@ -338,8 +424,13 @@ static int serial_chars_in_buffer (struc
goto exit;
}
- /* pass on to the driver specific version of this function */
- retval = port->serial->type->chars_in_buffer(port);
+ if(serial_lock(port->serial)) {
+ /* pass on to the driver specific version of this function */
+ retval = port->serial->type->chars_in_buffer(port);
+ serial_unlock(port->serial);
+ } else {
+ retval = -ENODEV;
+ }
exit:
return retval;
@@ -359,9 +450,13 @@ static void serial_throttle (struct tty_
return;
}
- /* pass on to the driver specific version of this function */
- if (port->serial->type->throttle)
- port->serial->type->throttle(port);
+ if(serial_lock(port->serial)) {
+ /* pass on to the driver specific version of this function */
+ if (port->serial->type->throttle)
+ port->serial->type->throttle(port);
+ serial_unlock(port->serial);
+ }
+
}
static void serial_unthrottle (struct tty_struct * tty)
@@ -378,9 +473,13 @@ static void serial_unthrottle (struct tt
return;
}
- /* pass on to the driver specific version of this function */
- if (port->serial->type->unthrottle)
- port->serial->type->unthrottle(port);
+ if(serial_lock(port->serial)) {
+ /* pass on to the driver specific version of this function */
+ if (port->serial->type->unthrottle)
+ port->serial->type->unthrottle(port);
+ serial_unlock(port->serial);
+ }
+
}
static int serial_ioctl (struct tty_struct *tty, struct file * file, unsigned
int cmd, unsigned long arg)
@@ -398,11 +497,17 @@ static int serial_ioctl (struct tty_stru
goto exit;
}
- /* pass on to the driver specific version of this function if it is
available */
- if (port->serial->type->ioctl)
- retval = port->serial->type->ioctl(port, file, cmd, arg);
- else
- retval = -ENOIOCTLCMD;
+ if(serial_lock(port->serial)) {
+ /* pass on to the driver specific version of this function if
it is available */
+ if (port->serial->type->ioctl)
+ retval = port->serial->type->ioctl(port, file, cmd,
arg);
+ else
+ retval = -ENOIOCTLCMD;
+ serial_unlock(port->serial);
+ } else {
+ retval = -ENODEV;
+ }
+
exit:
return retval;
@@ -422,9 +527,13 @@ static void serial_set_termios (struct t
return;
}
- /* pass on to the driver specific version of this function if it is
available */
- if (port->serial->type->set_termios)
- port->serial->type->set_termios(port, old);
+ if(serial_lock(port->serial)) {
+ /* pass on to the driver specific version of this function if
it is available */
+ if (port->serial->type->set_termios)
+ port->serial->type->set_termios(port, old);
+ serial_unlock(port->serial);
+ }
+
}
static void serial_break (struct tty_struct *tty, int break_state)
@@ -441,9 +550,12 @@ static void serial_break (struct tty_str
return;
}
- /* pass on to the driver specific version of this function if it is
available */
- if (port->serial->type->break_ctl)
- port->serial->type->break_ctl(port, break_state);
+ if(serial_lock(port->serial)) {
+ /* pass on to the driver specific version of this function if
it is available */
+ if (port->serial->type->break_ctl)
+ port->serial->type->break_ctl(port, break_state);
+ serial_unlock(port->serial);
+ }
}
static int serial_read_proc (char *page, char **start, off_t off, int count,
int *eof, void *data)
@@ -460,6 +572,8 @@ static int serial_read_proc (char *page,
serial = usb_serial_get_by_index(i);
if (serial == NULL)
continue;
+ if(!serial_lock(serial))
+ continue;
length += sprintf (page+length, "%d:", i);
if (serial->type->driver.owner)
@@ -476,6 +590,7 @@ static int serial_read_proc (char *page,
length += sprintf (page+length, "\n");
if ((length + begin) > (off + count)) {
+ serial_unlock(serial);
usb_serial_put(serial);
goto done;
}
@@ -483,6 +598,7 @@ static int serial_read_proc (char *page,
begin += length;
length = 0;
}
+ serial_unlock(serial);
usb_serial_put(serial);
}
*eof = 1;
@@ -496,6 +612,7 @@ done:
static int serial_tiocmget (struct tty_struct *tty, struct file *file)
{
struct usb_serial_port *port = tty->driver_data;
+ int retval;
if (!port)
return -ENODEV;
@@ -507,16 +624,24 @@ static int serial_tiocmget (struct tty_s
return -ENODEV;
}
- if (port->serial->type->tiocmget)
- return port->serial->type->tiocmget(port, file);
+ if(serial_lock(port->serial)) {
+ if (port->serial->type->tiocmget)
+ retval = port->serial->type->tiocmget(port, file);
+ else
+ retval = -EINVAL;
+ serial_unlock(port->serial);
+ } else {
+ retval = -ENODEV;
+ }
- return -EINVAL;
+ return retval;
}
static int serial_tiocmset (struct tty_struct *tty, struct file *file,
unsigned int set, unsigned int clear)
{
struct usb_serial_port *port = tty->driver_data;
+ int retval;
if (!port)
return -ENODEV;
@@ -528,10 +653,17 @@ static int serial_tiocmset (struct tty_s
return -ENODEV;
}
- if (port->serial->type->tiocmset)
- return port->serial->type->tiocmset(port, file, set, clear);
+ if(serial_lock(port->serial)) {
+ if (port->serial->type->tiocmset)
+ retval = port->serial->type->tiocmset(port, file, set,
clear);
+ else
+ retval = -EINVAL;
+ serial_unlock(port->serial);
+ } else {
+ retval = -ENODEV;
+ }
- return -EINVAL;
+ return retval;
}
/*
@@ -609,6 +741,11 @@ static struct usb_serial * create_serial
serial->interface = interface;
kref_init(&serial->kref);
+ spin_lock_init(&serial->lock);
+ serial->lock_count = 0;
+ serial->shutdown_called = 0;
+ init_waitqueue_head(&serial->shutdown_wait);
+
return serial;
}
@@ -1056,6 +1193,12 @@ void usb_serial_disconnect(struct usb_in
usb_set_intfdata (interface, NULL);
if (serial) {
+
+ /* return the minor range that this device had.
+ * after that all future serial_open calls on this device will
fail.
+ */
+ return_serial(serial);
+
for (i = 0; i < serial->num_ports; ++i) {
port = serial->port[i];
if (port) {
@@ -1064,8 +1207,21 @@ void usb_serial_disconnect(struct usb_in
kill_traffic(port);
}
}
- /* let the last holder of this object
- * cause it to be cleaned up */
+
+ /* The following call will block if any serial_* routine (on
our serial)
+ * is still in progress. It will wait for all such routines to
exit.
+ * After that all further calls to serial_* routines will
return immediately
+ * without performing any IO or touching the content of serial
or port.
+ * Therefore, we can cleanup serial and ports.
+ */
+ serial_lock_and_wait_before_shutdown(serial);
+
+ serial->type->shutdown(serial);
+
+ /* Now we destroy the pointer to serial, which was associated
with usb_interface.
+ * This is likely to delete serial object, unless some TTY
files are still open.
+ * In that case the object will be deleted from serial_close.
+ */
usb_serial_put(serial);
}
dev_info(dev, "device disconnected\n");
diff -uprN a/include/linux/usb/serial.h b/include/linux/usb/serial.h
--- a/include/linux/usb/serial.h 2007-03-15 15:57:22.472834500 -0800
+++ b/include/linux/usb/serial.h 2007-03-15 18:30:28.821108400 -0800
@@ -119,6 +119,11 @@ static inline void usb_set_serial_port_d
* @num_bulk_in: number of bulk in endpoints we have
* @num_bulk_out: number of bulk out endpoints we have
* @port: array of struct usb_serial_port structures for the different ports.
+ * @kref: refcount for storing the number of pointers to usb_serial
+ * @lock: spinlock to protect access to lock_count and shutdown_called
+ * @lock_count: number of serial_* routines, which locked usb_serial and are
using it.
+ * @shutdown_called: If this flag is set, serial_* routines must not use other
fields of usb_serial.
+ * @shutdown_wait: used by usb_serial_disconnect to block until all serial_*
routines exit.
* @private: place to put any driver specific information that is needed. The
* usb-serial driver is required to manage this data, the usb-serial core
* will not touch this. Use usb_get_serial_data() and
@@ -136,7 +141,11 @@ struct usb_serial {
char num_bulk_in;
char num_bulk_out;
struct usb_serial_port * port[MAX_NUM_PORTS];
- struct kref kref;
+ struct kref kref;
+ spinlock_t lock;
+ int lock_count;
+ int shutdown_called;
+ wait_queue_head_t shutdown_wait;
void * private;
};
#define to_usb_serial(d) container_of(d, struct usb_serial, kref)
-------------------------------------------------------
-------------------------------------------------------------------------
Take Surveys. Earn Cash. Influence the Future of IT
Join SourceForge.net's Techsay panel and you'll get the chance to share your
opinions on IT & business topics through brief surveys-and earn cash
http://www.techsay.com/default.php?page=join.php&p=sourceforge&CID=DEVDEV
_______________________________________________
[email protected]
To unsubscribe, use the last form field at:
https://lists.sourceforge.net/lists/listinfo/linux-usb-devel