usbnet: fix skb traversing races during unlink(v2)

Commit 4231d47e6fe69f061f96c98c30eaf9fb4c14b96d(net/usbnet: avoid
recursive locking in usbnet_stop()) fixes the recursive locking
problem by releasing the skb queue lock before unlink, but may
cause skb traversing races:
	- after URB is unlinked and the queue lock is released,
	the refered skb and skb->next may be moved to done queue,
	even be released
	- in skb_queue_walk_safe, the next skb is still obtained
	by next pointer of the last skb
	- so maybe trigger oops or other problems

This patch extends the usage of entry->state to describe 'start_unlink'
state, so always holding the queue(rx/tx) lock to change the state if
the referd skb is in rx or tx queue because we need to know if the
refered urb has been started unlinking in unlink_urbs.

The other part of this patch is based on Huajun's patch:
always traverse from head of the tx/rx queue to get skb which is
to be unlinked but not been started unlinking.

Signed-off-by: Huajun Li <huajun.li.lee@gmail.com>
Signed-off-by: Ming Lei <tom.leiming@gmail.com>
Cc: Oliver Neukum <oneukum@suse.de>
Cc: stable@kernel.org
Signed-off-by: David S. Miller <davem@davemloft.net>
This commit is contained in:
Ming Lei 2012-04-26 11:33:46 +08:00 committed by David S. Miller
parent 8aa51d64c1
commit 5b6e9bcdeb
2 changed files with 40 additions and 17 deletions

View File

@ -282,17 +282,32 @@ int usbnet_change_mtu (struct net_device *net, int new_mtu)
} }
EXPORT_SYMBOL_GPL(usbnet_change_mtu); EXPORT_SYMBOL_GPL(usbnet_change_mtu);
/* The caller must hold list->lock */
static void __usbnet_queue_skb(struct sk_buff_head *list,
struct sk_buff *newsk, enum skb_state state)
{
struct skb_data *entry = (struct skb_data *) newsk->cb;
__skb_queue_tail(list, newsk);
entry->state = state;
}
/*-------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------*/
/* some LK 2.4 HCDs oopsed if we freed or resubmitted urbs from /* some LK 2.4 HCDs oopsed if we freed or resubmitted urbs from
* completion callbacks. 2.5 should have fixed those bugs... * completion callbacks. 2.5 should have fixed those bugs...
*/ */
static void defer_bh(struct usbnet *dev, struct sk_buff *skb, struct sk_buff_head *list) static enum skb_state defer_bh(struct usbnet *dev, struct sk_buff *skb,
struct sk_buff_head *list, enum skb_state state)
{ {
unsigned long flags; unsigned long flags;
enum skb_state old_state;
struct skb_data *entry = (struct skb_data *) skb->cb;
spin_lock_irqsave(&list->lock, flags); spin_lock_irqsave(&list->lock, flags);
old_state = entry->state;
entry->state = state;
__skb_unlink(skb, list); __skb_unlink(skb, list);
spin_unlock(&list->lock); spin_unlock(&list->lock);
spin_lock(&dev->done.lock); spin_lock(&dev->done.lock);
@ -300,6 +315,7 @@ static void defer_bh(struct usbnet *dev, struct sk_buff *skb, struct sk_buff_hea
if (dev->done.qlen == 1) if (dev->done.qlen == 1)
tasklet_schedule(&dev->bh); tasklet_schedule(&dev->bh);
spin_unlock_irqrestore(&dev->done.lock, flags); spin_unlock_irqrestore(&dev->done.lock, flags);
return old_state;
} }
/* some work can't be done in tasklets, so we use keventd /* some work can't be done in tasklets, so we use keventd
@ -340,7 +356,6 @@ static int rx_submit (struct usbnet *dev, struct urb *urb, gfp_t flags)
entry = (struct skb_data *) skb->cb; entry = (struct skb_data *) skb->cb;
entry->urb = urb; entry->urb = urb;
entry->dev = dev; entry->dev = dev;
entry->state = rx_start;
entry->length = 0; entry->length = 0;
usb_fill_bulk_urb (urb, dev->udev, dev->in, usb_fill_bulk_urb (urb, dev->udev, dev->in,
@ -372,7 +387,7 @@ static int rx_submit (struct usbnet *dev, struct urb *urb, gfp_t flags)
tasklet_schedule (&dev->bh); tasklet_schedule (&dev->bh);
break; break;
case 0: case 0:
__skb_queue_tail (&dev->rxq, skb); __usbnet_queue_skb(&dev->rxq, skb, rx_start);
} }
} else { } else {
netif_dbg(dev, ifdown, dev->net, "rx: stopped\n"); netif_dbg(dev, ifdown, dev->net, "rx: stopped\n");
@ -423,16 +438,17 @@ static void rx_complete (struct urb *urb)
struct skb_data *entry = (struct skb_data *) skb->cb; struct skb_data *entry = (struct skb_data *) skb->cb;
struct usbnet *dev = entry->dev; struct usbnet *dev = entry->dev;
int urb_status = urb->status; int urb_status = urb->status;
enum skb_state state;
skb_put (skb, urb->actual_length); skb_put (skb, urb->actual_length);
entry->state = rx_done; state = rx_done;
entry->urb = NULL; entry->urb = NULL;
switch (urb_status) { switch (urb_status) {
/* success */ /* success */
case 0: case 0:
if (skb->len < dev->net->hard_header_len) { if (skb->len < dev->net->hard_header_len) {
entry->state = rx_cleanup; state = rx_cleanup;
dev->net->stats.rx_errors++; dev->net->stats.rx_errors++;
dev->net->stats.rx_length_errors++; dev->net->stats.rx_length_errors++;
netif_dbg(dev, rx_err, dev->net, netif_dbg(dev, rx_err, dev->net,
@ -471,7 +487,7 @@ static void rx_complete (struct urb *urb)
"rx throttle %d\n", urb_status); "rx throttle %d\n", urb_status);
} }
block: block:
entry->state = rx_cleanup; state = rx_cleanup;
entry->urb = urb; entry->urb = urb;
urb = NULL; urb = NULL;
break; break;
@ -482,17 +498,18 @@ block:
// FALLTHROUGH // FALLTHROUGH
default: default:
entry->state = rx_cleanup; state = rx_cleanup;
dev->net->stats.rx_errors++; dev->net->stats.rx_errors++;
netif_dbg(dev, rx_err, dev->net, "rx status %d\n", urb_status); netif_dbg(dev, rx_err, dev->net, "rx status %d\n", urb_status);
break; break;
} }
defer_bh(dev, skb, &dev->rxq); state = defer_bh(dev, skb, &dev->rxq, state);
if (urb) { if (urb) {
if (netif_running (dev->net) && if (netif_running (dev->net) &&
!test_bit (EVENT_RX_HALT, &dev->flags)) { !test_bit (EVENT_RX_HALT, &dev->flags) &&
state != unlink_start) {
rx_submit (dev, urb, GFP_ATOMIC); rx_submit (dev, urb, GFP_ATOMIC);
usb_mark_last_busy(dev->udev); usb_mark_last_busy(dev->udev);
return; return;
@ -579,16 +596,23 @@ EXPORT_SYMBOL_GPL(usbnet_purge_paused_rxq);
static int unlink_urbs (struct usbnet *dev, struct sk_buff_head *q) static int unlink_urbs (struct usbnet *dev, struct sk_buff_head *q)
{ {
unsigned long flags; unsigned long flags;
struct sk_buff *skb, *skbnext; struct sk_buff *skb;
int count = 0; int count = 0;
spin_lock_irqsave (&q->lock, flags); spin_lock_irqsave (&q->lock, flags);
skb_queue_walk_safe(q, skb, skbnext) { while (!skb_queue_empty(q)) {
struct skb_data *entry; struct skb_data *entry;
struct urb *urb; struct urb *urb;
int retval; int retval;
entry = (struct skb_data *) skb->cb; skb_queue_walk(q, skb) {
entry = (struct skb_data *) skb->cb;
if (entry->state != unlink_start)
goto found;
}
break;
found:
entry->state = unlink_start;
urb = entry->urb; urb = entry->urb;
/* /*
@ -1039,8 +1063,7 @@ static void tx_complete (struct urb *urb)
} }
usb_autopm_put_interface_async(dev->intf); usb_autopm_put_interface_async(dev->intf);
entry->state = tx_done; (void) defer_bh(dev, skb, &dev->txq, tx_done);
defer_bh(dev, skb, &dev->txq);
} }
/*-------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------*/
@ -1096,7 +1119,6 @@ netdev_tx_t usbnet_start_xmit (struct sk_buff *skb,
entry = (struct skb_data *) skb->cb; entry = (struct skb_data *) skb->cb;
entry->urb = urb; entry->urb = urb;
entry->dev = dev; entry->dev = dev;
entry->state = tx_start;
entry->length = length; entry->length = length;
usb_fill_bulk_urb (urb, dev->udev, dev->out, usb_fill_bulk_urb (urb, dev->udev, dev->out,
@ -1155,7 +1177,7 @@ netdev_tx_t usbnet_start_xmit (struct sk_buff *skb,
break; break;
case 0: case 0:
net->trans_start = jiffies; net->trans_start = jiffies;
__skb_queue_tail (&dev->txq, skb); __usbnet_queue_skb(&dev->txq, skb, tx_start);
if (dev->txq.qlen >= TX_QLEN (dev)) if (dev->txq.qlen >= TX_QLEN (dev))
netif_stop_queue (net); netif_stop_queue (net);
} }

View File

@ -191,7 +191,8 @@ extern void usbnet_cdc_status(struct usbnet *, struct urb *);
enum skb_state { enum skb_state {
illegal = 0, illegal = 0,
tx_start, tx_done, tx_start, tx_done,
rx_start, rx_done, rx_cleanup rx_start, rx_done, rx_cleanup,
unlink_start
}; };
struct skb_data { /* skb->cb is one of these */ struct skb_data { /* skb->cb is one of these */