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 _______________________________________________ linux-usb-devel@lists.sourceforge.net To unsubscribe, use the last form field at: https://lists.sourceforge.net/lists/listinfo/linux-usb-devel