diff options
author | David Daney <ddaney@caviumnetworks.com> | 2009-06-23 16:20:56 -0700 |
---|---|---|
committer | Ralf Baechle <ralf@linux-mips.org> | 2009-06-24 18:34:41 +0100 |
commit | a620c1632629b42369e78448acc7b384fe1faf48 (patch) | |
tree | 3318683c03abb4ca45307c7df0019f74bcba3b13 /drivers/staging/octeon/ethernet.c | |
parent | f696a10838ffab85e5bc07e7cff0d0e1870a30d7 (diff) | |
download | linux-a620c1632629b42369e78448acc7b384fe1faf48.tar.bz2 |
Staging: octeon-ethernet: Fix race freeing transmit buffers.
The existing code had the following race:
Thread-1 Thread-2
inc/read in_use
inc/read in_use
inc tx_free_list[qos].len
inc tx_free_list[qos].len
The actual in_use value was incremented twice, but thread-1 is going
to free memory based on its stale value, and will free one too many
times. The result is that memory is freed back to the kernel while
its packet is still in the transmit buffer. If the memory is
overwritten before it is transmitted, the hardware will put a valid
checksum on it and send it out (just like it does with good packets).
If by chance the TCP flags are clobbered but not the addresses or
ports, the result can be a broken TCP stream.
The fix is to track the number of freed packets in a single location
(a Fetch-and-Add Unit register). That way it can never get out of sync
with itself.
We try to free up to MAX_SKB_TO_FREE (currently 10) buffers at a time.
If fewer are available we adjust the free count with the difference.
The action of claiming buffers to free is atomic so two threads cannot
claim the same buffers.
Signed-off-by: David Daney <ddaney@caviumnetworks.com>
Signed-off-by: Ralf Baechle <ralf@linux-mips.org>
Diffstat (limited to 'drivers/staging/octeon/ethernet.c')
-rw-r--r-- | drivers/staging/octeon/ethernet.c | 89 |
1 files changed, 46 insertions, 43 deletions
diff --git a/drivers/staging/octeon/ethernet.c b/drivers/staging/octeon/ethernet.c index 2d9356dfbca6..b8479517dce2 100644 --- a/drivers/staging/octeon/ethernet.c +++ b/drivers/staging/octeon/ethernet.c @@ -37,13 +37,14 @@ #include <asm/octeon/octeon.h> #include "ethernet-defines.h" +#include "octeon-ethernet.h" #include "ethernet-mem.h" #include "ethernet-rx.h" #include "ethernet-tx.h" #include "ethernet-mdio.h" #include "ethernet-util.h" #include "ethernet-proc.h" -#include "octeon-ethernet.h" + #include "cvmx-pip.h" #include "cvmx-pko.h" @@ -130,53 +131,55 @@ extern struct semaphore mdio_sem; */ static void cvm_do_timer(unsigned long arg) { + int32_t skb_to_free, undo; + int queues_per_port; + int qos; + struct octeon_ethernet *priv; static int port; - if (port < CVMX_PIP_NUM_INPUT_PORTS) { - if (cvm_oct_device[port]) { - int queues_per_port; - int qos; - struct octeon_ethernet *priv = - netdev_priv(cvm_oct_device[port]); - if (priv->poll) { - /* skip polling if we don't get the lock */ - if (!down_trylock(&mdio_sem)) { - priv->poll(cvm_oct_device[port]); - up(&mdio_sem); - } - } - queues_per_port = cvmx_pko_get_num_queues(port); - /* Drain any pending packets in the free list */ - for (qos = 0; qos < queues_per_port; qos++) { - if (skb_queue_len(&priv->tx_free_list[qos])) { - spin_lock(&priv->tx_free_list[qos]. - lock); - while (skb_queue_len - (&priv->tx_free_list[qos]) > - cvmx_fau_fetch_and_add32(priv-> - fau + - qos * 4, - 0)) - dev_kfree_skb(__skb_dequeue - (&priv-> - tx_free_list - [qos])); - spin_unlock(&priv->tx_free_list[qos]. - lock); - } - } - cvm_oct_device[port]->netdev_ops->ndo_get_stats(cvm_oct_device[port]); - } - port++; - /* Poll the next port in a 50th of a second. - This spreads the polling of ports out a little bit */ - mod_timer(&cvm_oct_poll_timer, jiffies + HZ / 50); - } else { + if (port >= CVMX_PIP_NUM_INPUT_PORTS) { + /* + * All ports have been polled. Start the next + * iteration through the ports in one second. + */ port = 0; - /* All ports have been polled. Start the next iteration through - the ports in one second */ mod_timer(&cvm_oct_poll_timer, jiffies + HZ); + return; + } + if (!cvm_oct_device[port]) + goto out; + + priv = netdev_priv(cvm_oct_device[port]); + if (priv->poll) { + /* skip polling if we don't get the lock */ + if (!down_trylock(&mdio_sem)) { + priv->poll(cvm_oct_device[port]); + up(&mdio_sem); + } + } + + queues_per_port = cvmx_pko_get_num_queues(port); + /* Drain any pending packets in the free list */ + for (qos = 0; qos < queues_per_port; qos++) { + if (skb_queue_len(&priv->tx_free_list[qos]) == 0) + continue; + skb_to_free = cvmx_fau_fetch_and_add32(priv->fau + qos * 4, + MAX_SKB_TO_FREE); + undo = skb_to_free > 0 ? + MAX_SKB_TO_FREE : skb_to_free + MAX_SKB_TO_FREE; + if (undo > 0) + cvmx_fau_atomic_add32(priv->fau+qos*4, -undo); + skb_to_free = -skb_to_free > MAX_SKB_TO_FREE ? + MAX_SKB_TO_FREE : -skb_to_free; + cvm_oct_free_tx_skbs(priv, skb_to_free, qos, 1); } + cvm_oct_device[port]->netdev_ops->ndo_get_stats(cvm_oct_device[port]); + +out: + port++; + /* Poll the next port in a 50th of a second. + This spreads the polling of ports out a little bit */ + mod_timer(&cvm_oct_poll_timer, jiffies + HZ / 50); } /** |