usb: gadget: udc: bdc: free IRQ and drain func_wake_notify before teardown

The Broadcom BDC UDC driver registers its IRQ handler with
devm_request_irq() in bdc_udc_init(), so the IRQ is released by devm
only after bdc_remove() returns.  devm releases resources in reverse
LIFO order, but bdc_remove() runs bdc_udc_exit() and bdc_hw_exit() ->
bdc_mem_free() manually before returning: bdc_udc_exit() tears down
individual endpoint objects via bdc_free_ep(), while bdc_hw_exit() ->
bdc_mem_free() frees and NULLs the DMA-coherent status-report ring
(bdc->srr.sr_bds) and kfree()s bdc->bdc_ep_array.  Both happen while
the IRQ handler (bdc_udc_interrupt, requested with IRQF_SHARED)
remains deliverable in the window up to the post-remove devm
free_irq().

On receipt of a shared interrupt in that window, bdc_udc_interrupt()
dereferences bdc->srr.sr_bds[bdc->srr.dqp_index] (NULL or freed DMA)
and dispatches sr_handler callbacks that index into bdc_ep_array,
causing a NULL-deref or use-after-free.

The same window affects the delayed_work bdc->func_wake_notify, which is
armed from the IRQ handler via bdc_sr_uspc() -> handle_link_state_change()
-> schedule_delayed_work() and may self-rearm from its own callback
bdc_func_wake_timer().  No cancel exists anywhere in the driver, so a
queued work item that fires after bdc_remove() returns and the bdc
structure is devm-freed dereferences freed memory.

Replace devm_request_irq() with request_irq() and add an explicit
free_irq(bdc->irq, bdc) in bdc_remove().  Clear BDC_GIE before
free_irq() to stop the device from asserting interrupts, then
free_irq() drains any in-flight handler, then cancel_delayed_work_sync()
drains the func_wake_notify delayed work.  This ordering ensures the
IRQ handler and delayed work cannot interfere with the subsequent
endpoint and DMA teardown in bdc_udc_exit() and bdc_hw_exit().  Wire the
matching free_irq() into the bdc_udc_init() error path so the IRQ is
released on probe failure, and route the bdc_init_ep() failure through
err0 instead of returning directly.

This issue was found by an in-house static analysis tool.

Fixes: efed421a94 ("usb: gadget: Add UDC driver for Broadcom USB3.0 device controller IP BDC")
Cc: stable <stable@kernel.org>
Assisted-by: Codex:gpt-5.5
Signed-off-by: Fan Wu <fanwu01@zju.edu.cn>
Link: https://patch.msgid.link/20260709020904.502611-1-fanwu01@zju.edu.cn
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
This commit is contained in:
Fan Wu 2026-07-09 02:09:04 +00:00 committed by Greg Kroah-Hartman
parent 7aa7d4bf9d
commit 0583f2fbf8
2 changed files with 24 additions and 3 deletions

View File

@ -585,9 +585,29 @@ static int bdc_probe(struct platform_device *pdev)
static void bdc_remove(struct platform_device *pdev)
{
struct bdc *bdc;
unsigned long flags;
u32 temp;
bdc = platform_get_drvdata(pdev);
dev_dbg(bdc->dev, "%s ()\n", __func__);
/*
* Disable the device interrupt source before freeing the IRQ:
* clear BDC_GIE so the controller stops asserting interrupts,
* then free_irq drains any in-flight handler.
*/
spin_lock_irqsave(&bdc->lock, flags);
temp = bdc_readl(bdc->regs, BDC_BDCSC);
temp &= ~BDC_GIE;
bdc_writel(bdc->regs, BDC_BDCSC, temp);
spin_unlock_irqrestore(&bdc->lock, flags);
free_irq(bdc->irq, bdc);
/*
* Drain func_wake_notify after free_irq: the IRQ handler arms this
* delayed_work via bdc_sr_uspc -> handle_link_state_change ->
* schedule_delayed_work (self-rearmed in bdc_func_wake_timer), so
* the IRQ must be released first to prevent re-arm after cancel.
*/
cancel_delayed_work_sync(&bdc->func_wake_notify);
bdc_udc_exit(bdc);
bdc_hw_exit(bdc);
bdc_phy_exit(bdc);

View File

@ -530,8 +530,8 @@ int bdc_udc_init(struct bdc *bdc)
bdc->gadget.name = BRCM_BDC_NAME;
ret = devm_request_irq(bdc->dev, bdc->irq, bdc_udc_interrupt,
IRQF_SHARED, BRCM_BDC_NAME, bdc);
ret = request_irq(bdc->irq, bdc_udc_interrupt, IRQF_SHARED,
BRCM_BDC_NAME, bdc);
if (ret) {
dev_err(bdc->dev,
"failed to request irq #%d %d\n",
@ -542,7 +542,7 @@ int bdc_udc_init(struct bdc *bdc)
ret = bdc_init_ep(bdc);
if (ret) {
dev_err(bdc->dev, "bdc init ep fail: %d\n", ret);
return ret;
goto err0;
}
ret = usb_add_gadget_udc(bdc->dev, &bdc->gadget);
@ -571,6 +571,7 @@ int bdc_udc_init(struct bdc *bdc)
err1:
usb_del_gadget_udc(&bdc->gadget);
err0:
free_irq(bdc->irq, bdc);
bdc_free_ep(bdc);
return ret;