[prev in list] [next in list] [prev in thread] [next in thread] 

List:       linux-can
Subject:    [PATCH] peak_pci: fix use after free in netdev teardown
From:       "Christopher R. Baker" <cbaker () rec ! ri ! cmu ! edu>
Date:       2014-04-09 19:45:20
Message-ID: 1397072720.22713.19.camel () acrs-z800-1
[Download message RAW]

Hi All,

In the course of tracking down (and eventually backporting) a fix to one
of my systems that is still running a 3.2 kernel, I noticed what I
believe to be a pair of use-after-free bugs in peak_pci.c pertaining to
the linked list of netdevs that is maintained for multi-port cards.
These bugs persist in 3.14, so I thought I should send along a patch for
review.

Basically, the "prev_dev" pointer that is used for this list lives in
memory that is allocated by alloc_sja1000dev(), but it is referenced
after the call to free_sja1000dev() when walking the list during
teardown, both in the failure case of peak_pci_probe and in
peak_pci_remove.  Unless I'm missing something, these are toes waiting
to be stubbed...

Caveats:
 - This is a growing blob of copy-pasta that should probably be
refactored to a common location.  For example, peak_pci_remove could be
restructured to incrementally check and free allocated resources,
allowing the "failure_remove_channels" label to delegate the cleanup to
peak_pci_remove.  I didn't want to bite off too much this time, though,
so I left that alone.
 - I don't have an expresscard adapter to check the placement of the
pciec_remove stanzas.  By inspection, unregister_sja1000dev does not
appear to have a path back to the pciec stuff, but I may have missed
something.

-ChrisR

Signed-of-by: Christopher R. Baker <cbaker@rec.ri.cmu.edu>



["peak_pci_cleanup_use_after_free.diff" (peak_pci_cleanup_use_after_free.diff)]

diff --git a/drivers/net/can/sja1000/peak_pci.c b/drivers/net/can/sja1000/peak_pci.c
index 065ca49..4ff33df 100644
--- a/drivers/net/can/sja1000/peak_pci.c
+++ b/drivers/net/can/sja1000/peak_pci.c
@@ -686,17 +686,28 @@ failure_remove_channels:
 	/* Disable interrupts */
 	writew(0x0, cfg_base + PITA_ICR + 2);
 
-	chan = NULL;
-	for (dev = pci_get_drvdata(pdev); dev; dev = chan->prev_dev) {
-		unregister_sja1000dev(dev);
-		free_sja1000dev(dev);
+	/* Dealloc netdevs: notably replicated near-verbatim in peak_pci_remove */
+	dev = pci_get_drvdata(pdev);
+	while(dev)
+	{
+		/* priv and chan look into memory that is allocated by alloc_sja1000dev,
+		   so be careful to cache prev_dev before free_sja1000dev */
+		struct net_device *prev_dev;
 		priv = netdev_priv(dev);
 		chan = priv->priv;
+		prev_dev = chan->prev_dev;
+
+		/* free PCIeC resources for the first card, if present, and before free_sja1000dev. */
+		if (!prev_dev && chan->pciec_card)
+			peak_pciec_remove(chan->pciec_card);
+
+		unregister_sja1000dev(dev);
+		free_sja1000dev(dev);
+
+		dev = prev_dev;
 	}
+	pci_set_drvdata(pdev,NULL);
 
-	/* free any PCIeC resources too */
-	if (chan && chan->pciec_card)
-		peak_pciec_remove(chan->pciec_card);
 
 	pci_iounmap(pdev, reg_base);
 
@@ -717,28 +728,35 @@ static void peak_pci_remove(struct pci_dev *pdev)
 	struct net_device *dev = pci_get_drvdata(pdev); /* Last device */
 	struct sja1000_priv *priv = netdev_priv(dev);
 	struct peak_pci_chan *chan = priv->priv;
+
 	void __iomem *cfg_base = chan->cfg_base;
 	void __iomem *reg_base = priv->reg_base;
 
 	/* Disable interrupts */
 	writew(0x0, cfg_base + PITA_ICR + 2);
 
-	/* Loop over all registered devices */
-	while (1) {
-		dev_info(&pdev->dev, "removing device %s\n", dev->name);
+	/* Dealloc netdevs: notably replicated from above */
+	while(dev)
+	{
+		/* priv and chan look into memory that is allocated by alloc_sja1000dev,
+		   so be careful to cache prev_dev before free_sja1000dev */
+		struct net_device *prev_dev;
+		priv = netdev_priv(dev);
+		chan = priv->priv;
+		prev_dev = chan->prev_dev;
+		dev_info(&pdev->dev, "removing device '%s' at %p (prev_dev = %p, chan->pciec_card = %p)\n",
+						dev->name, dev, prev_dev,chan->pciec_card);
+
+		/* free PCIeC resources for the first card, if present, and before free_sja1000dev. */
+		if (!prev_dev && chan->pciec_card)
+			peak_pciec_remove(chan->pciec_card);
+
 		unregister_sja1000dev(dev);
 		free_sja1000dev(dev);
-		dev = chan->prev_dev;
 
-		if (!dev) {
-			/* do that only for first channel */
-			if (chan->pciec_card)
-				peak_pciec_remove(chan->pciec_card);
-			break;
-		}
-		priv = netdev_priv(dev);
-		chan = priv->priv;
+		dev = prev_dev;
 	}
+	pci_set_drvdata(pdev,NULL);
 
 	pci_iounmap(pdev, reg_base);
 	pci_iounmap(pdev, cfg_base);

--
To unsubscribe from this list: send the line "unsubscribe linux-can" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html

[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic