WARN_ON(irqs_disabled()) in dma_free_attrs?

Fredrik Noring noring at nocrew.org
Sat Mar 3 07:58:42 UTC 2018


Hi Robin,

> Historically, that particular line of code appears to date back to commit
> aa24886e379d (and tracking it's ancestry was quite fun).
> 
> Now, I'm sure not all of the considerations of 11-and-a-half years ago still
> apply, but one certainly does: ARM* still uses non-cacheable mappings for
> coherent allocations on systems which aren't hardware-coherent (i.e. most of
> them), thus the alloc and free paths may respectively set up and tear down
> those mappings, and the latter involves this guy:
> 
> 	void vunmap(const void *addr)
> 	{
> 		BUG_ON(in_interrupt());
> 		might_sleep();
> 		if (addr)
> 			__vunmap(addr, 0);
> 	}
> 
> Even in the non-coherent ARM case it *would* technically be viable to free a
> coherent buffer from interrupt context iff it was allocated with GFP_ATOMIC,
> as those are satisfied from a statically-mapped pool rather than dynamically
> vmapped, but that doesn't really expand to the general case, and I certainly
> can't speak for all the other architectures.

Ah, thanks, good to know!

> As Alan implies, I guess the way forward is to figure out how similar
> drivers manage - your backtrace suggests that you might be using
> HCD_LOCAL_MEM, which probably isn't the common case but does seem to appear
> in at least a couple of other OHCI drivers.

There are two other OHCI drivers that do (as far as I understand)
essentially the same thing with dma_declare_coherent_memory and
HCD_LOCAL_MEM:

	drivers/usb/host/ohci-sm501.c
	drivers/usb/host/ohci-tmio.c

They are simple, but I cannot see how they could possibly avoid this issue
given the design of the USB core and the offending call trace which does not
pass through the device specific HCD:

	   usb_hcd_irq
	-> ohci_irq
	-> ohci_work
	-> process_done_list
	-> takeback_td
	-> finish_urb
	-> usb_hcd_giveback_urb
	-> __usb_hcd_giveback_urb
	-> unmap_urb_for_dma
	-> usb_hcd_unmap_urb_for_dma
	-> hcd_free_coherent
	-> hcd_buffer_free
	-> dma_free_coherent
	-> dma_free_attrs

The hc_driver struct is set to defaults, and they don't manage DMA
allocations except for probe and remove. How could they avoid the WARN_ON?

[ For reference, I attached the PS2 OHCI driver below. It has been tested
and works well provided the WARN_ON in dma_free_attrs is removed. ]

Fredrik

/*
 * PlayStation 2 USB OHCI HCD (Host Controller Driver)
 *
 * Copyright (C) 2017 Jürgen Urban
 * Copyright (C) 2017 Fredrik Noring
 *
 * SPDX-License-Identifier: GPL-2.0
 */

#include <linux/dma-mapping.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/usb.h>
#include <linux/usb/hcd.h>

#include <asm/mach-ps2/iop-memory.h>

#include "ohci.h"

/* Enable USB OHCI hardware. */
#define DPCR2_ENA_USB 0x08000000

/* Enable PS2DEV (required for PATA and USB). */
#define DPCR2_ENA_PS2DEV 0x00000080

#define DRIVER_DESC "OHCI PS2 driver"
#define DRV_NAME "ohci-ps2"

/* Size allocated from IOP heap (maximum size of DMA memory). */
#define DMA_BUFFER_SIZE (256 * 1024)

/* Get driver private data. */
#define hcd_to_priv(hcd) (struct ps2_hcd *)(hcd_to_ohci(hcd)->priv)

struct ps2_hcd {
	void __iomem *dpcr2;
	dma_addr_t iop_dma_addr;
	bool wakeup;			/* Saved wake-up state for resume. */
};

static struct hc_driver __read_mostly ohci_ps2_hc_driver;
static irqreturn_t (*ohci_irq)(struct usb_hcd *hcd);

static void ohci_ps2_enable(struct usb_hcd *hcd)
{
	struct ohci_hcd *ohci = hcd_to_ohci(hcd);

	BUG_ON(!ohci->regs);

	/* This is needed to get USB working on PS2. */
	ohci_writel(ohci, 1, &ohci->regs->roothub.portstatus[11]);
}

static void ohci_ps2_disable(struct usb_hcd *hcd)
{
	struct ohci_hcd *ohci = hcd_to_ohci(hcd);

	WARN_ON(!ohci->regs);

	if (ohci->regs)
		ohci_writel(ohci, 0, &ohci->regs->roothub.portstatus[11]);
}

static void ohci_ps2_start_hc(struct usb_hcd *hcd)
{
	struct ps2_hcd *ps2priv = hcd_to_priv(hcd);

	/*
	 * Enable USB and PS2DEV.
	 *
	 * FIXME: What is the purpose of PS2DEV for USB?
	 * FIXME: As far as I remember the following call enables the clock,
	 * so that ohci->regs->fminterval can count.
	 */
	writel(readl(ps2priv->dpcr2) | DPCR2_ENA_USB | DPCR2_ENA_PS2DEV,
		ps2priv->dpcr2);
}

static void ohci_ps2_stop_hc(struct usb_hcd *hcd)
{
	struct ps2_hcd *ps2priv = hcd_to_priv(hcd);

	/* Disable USB. Leave PS2DEV enabled (could be still needed for PATA). */
	writel(readl(ps2priv->dpcr2) & ~DPCR2_ENA_USB, ps2priv->dpcr2);
}

static int ohci_ps2_reset(struct usb_hcd *hcd)
{
	int ret;

	ohci_ps2_start_hc(hcd);

	ret = ohci_setup(hcd);

	if (ret < 0) {
		ohci_ps2_stop_hc(hcd);
		return ret;
	}

	ohci_ps2_enable(hcd);

	return ret;
}

static irqreturn_t ohci_ps2_irq(struct usb_hcd *hcd)
{
	struct ohci_hcd	*ohci = hcd_to_ohci(hcd);
	struct ohci_regs __iomem *regs = ohci->regs;

	/*
	 * FIXME: For some reason OHCI_INTR_MIE is required in the
	 * IRQ handler. Without it, reading a large amount of data
	 * (> 1 GB) from a mass storage device results in a freeze.
	 */
	ohci_writel(ohci, OHCI_INTR_MIE, &regs->intrdisable);

	return ohci_irq(hcd); /* Call normal IRQ handler. */
}

static int iopheap_alloc_coherent(struct platform_device *pdev, size_t size,
	int flags)
{
	struct device *dev = &pdev->dev;
	struct usb_hcd *hcd = platform_get_drvdata(pdev);
	struct ps2_hcd *ps2priv = hcd_to_priv(hcd);

	if (ps2priv->iop_dma_addr != 0)
		return 0;

	ps2priv->iop_dma_addr = iop_alloc(size);
	if (ps2priv->iop_dma_addr == 0) {
		dev_err(dev, "iop_alloc failed\n");
		return -ENOMEM;
	}

	if (dma_declare_coherent_memory(dev,
			iop_bus_to_phys(ps2priv->iop_dma_addr),
			ps2priv->iop_dma_addr, size, flags)) {
		dev_err(dev, "dma_declare_coherent_memory failed\n");
		iop_free(ps2priv->iop_dma_addr);
		ps2priv->iop_dma_addr = 0;
		return -ENOMEM;
	}

	return 0;
}

static void iopheap_free_coherent(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct usb_hcd *hcd = platform_get_drvdata(pdev);
	struct ps2_hcd *ps2priv = hcd_to_priv(hcd);

	if (ps2priv->iop_dma_addr == 0)
		return;

	dma_release_declared_memory(dev);
	iop_free(ps2priv->iop_dma_addr);
	ps2priv->iop_dma_addr = 0;
}

static int ohci_hcd_ps2_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct resource *regs;
	struct resource *dpcr2;
	struct usb_hcd *hcd;
	struct ps2_hcd *ps2priv;
	int irq;
	int ret;

	irq = platform_get_irq(pdev, 0);
	if (irq < 0) {
		dev_err(dev, "platform_get_irq failed\n");
		return irq;
	}

	/* FIXME: Is request_mem_region recommended here? */

	hcd = usb_create_hcd(&ohci_ps2_hc_driver, dev, dev_name(dev));
	if (hcd == NULL)
		return -ENOMEM;

	ps2priv = hcd_to_priv(hcd);
	memset(ps2priv, 0, sizeof(*ps2priv));

	regs = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (regs == NULL) {
		dev_err(dev, "platform_get_resource 0 failed\n");
		ret = -ENOENT;
		goto err;
	}
	dpcr2 = platform_get_resource(pdev, IORESOURCE_MEM, 1);
	if (dpcr2 == NULL) {
		dev_err(dev, "platform_get_resource 1 failed\n");
		ret = -ENOENT;
		goto err;
	}

	hcd->rsrc_start = regs->start;
	hcd->rsrc_len = resource_size(regs);
	hcd->regs = ioremap(hcd->rsrc_start, hcd->rsrc_len);
	if (IS_ERR(hcd->regs)) {
		ret = PTR_ERR(hcd->regs);
		goto err;
	}

	ps2priv->dpcr2 = ioremap(dpcr2->start, resource_size(dpcr2));
	if (ps2priv->dpcr2 == NULL) {
		ret = -ENOMEM;
		goto err_ioremap_dpcr2;
	}

	ret = iopheap_alloc_coherent(pdev, DMA_BUFFER_SIZE, DMA_MEMORY_EXCLUSIVE);
	if (ret != 0)
		goto err_alloc_coherent;

	ret = usb_add_hcd(hcd, irq, 0);
	if (ret != 0)
		goto err_add_hcd;

	ret = device_wakeup_enable(hcd->self.controller);
	if (ret == 0)
		return ret;

	usb_remove_hcd(hcd);
err_add_hcd:
	iopheap_free_coherent(pdev);
err_alloc_coherent:
	iounmap(ps2priv->dpcr2);
err_ioremap_dpcr2:
	iounmap(hcd->regs);
err:
	usb_put_hcd(hcd);

	return ret;
}

static int ohci_hcd_ps2_remove(struct platform_device *pdev)
{
	struct usb_hcd *hcd = platform_get_drvdata(pdev);
	struct ps2_hcd *ps2priv = hcd_to_priv(hcd);

	usb_remove_hcd(hcd);

	ohci_ps2_disable(hcd);
	ohci_ps2_stop_hc(hcd);

	iopheap_free_coherent(pdev);
	iounmap(ps2priv->dpcr2);
	iounmap(hcd->regs);

	usb_put_hcd(hcd);

	return 0;
}

#ifdef CONFIG_PM
static int ohci_hcd_ps2_suspend(struct platform_device *pdev,
				pm_message_t message)
{
	struct device *dev = &pdev->dev;
	struct usb_hcd *hcd = platform_get_drvdata(pdev);
	struct ps2_hcd *ps2priv = hcd_to_priv(hcd);
	int ret;

	ps2priv->wakeup = device_may_wakeup(dev);
	if (ps2priv->wakeup)
		enable_irq_wake(hcd->irq);

	ret = ohci_suspend(hcd, ps2priv->wakeup);
	if (ret)
		return ret;

	ohci_ps2_disable(hcd);
	ohci_ps2_stop_hc(hcd);

	return ret;
}

static int ohci_hcd_ps2_resume(struct platform_device *pdev)
{
	struct usb_hcd *hcd = platform_get_drvdata(pdev);
	struct ps2_hcd *ps2priv = hcd_to_priv(hcd);

	if (ps2priv->wakeup)
		disable_irq_wake(hcd->irq);

	ohci_ps2_start_hc(hcd);
	ohci_ps2_enable(hcd);

	ohci_resume(hcd, ps2priv->wakeup);

	return 0;
}
#endif

static struct platform_driver ohci_hcd_ps2_driver = {
	.probe		= ohci_hcd_ps2_probe,
	.remove		= ohci_hcd_ps2_remove,
	.shutdown	= usb_hcd_platform_shutdown,
#ifdef	CONFIG_PM
	.suspend	= ohci_hcd_ps2_suspend,
	.resume		= ohci_hcd_ps2_resume,
#endif
	.driver		= {
		.name	= DRV_NAME,
	},
};

static const struct ohci_driver_overrides ps2_overrides __initconst = {
	.reset		= ohci_ps2_reset,
	.product_desc	= DRIVER_DESC,
	.extra_priv_size = sizeof(struct ps2_hcd),
};

static int __init ohci_ps2_init(void)
{
	if (usb_disabled())
		return -ENODEV;

	pr_info("%s: " DRIVER_DESC "\n", DRV_NAME);

	ohci_init_driver(&ohci_ps2_hc_driver, &ps2_overrides);
	ohci_ps2_hc_driver.flags |= HCD_LOCAL_MEM;

	/*
	 * FIXME: For some reason
	 *
	 *   ohci_writel(ohci, OHCI_INTR_MIE, &regs->intrdisable);
	 *
	 * is required in the IRQ handler. Without it, reading a large
	 * amount of data (> 1 GB) from a mass storage device results in
	 * a freeze.
	 */
	ohci_irq = ohci_ps2_hc_driver.irq; /* Save normal IRQ handler. */
	ohci_ps2_hc_driver.irq = ohci_ps2_irq; /* Install IRQ workaround. */

	return platform_driver_register(&ohci_hcd_ps2_driver);
}
module_init(ohci_ps2_init);

static void __exit ohci_ps2_cleanup(void)
{
	platform_driver_unregister(&ohci_hcd_ps2_driver);
}
module_exit(ohci_ps2_cleanup);

MODULE_DESCRIPTION(DRIVER_DESC);
MODULE_LICENSE("GPL");
MODULE_ALIAS("platform:" DRV_NAME);


More information about the iommu mailing list