[PATCH 1/3] soc: qcom: smp2p: Fix use-after-free of irq domain on removal

From: Krzysztof Kozlowski

Date: Sun Nov 01 2026 - 15:24:32 EST


The SMP2P interrupt is requested with devm_request_threaded_irq(), so it
is freed only after qcom_smp2p_remove() returns or, in the probe error
path, after probe returns. Both paths, however, explicitly remove the
inbound irq domains, unregister outbound SMEM states and free the
mailbox channel before that.

If the interrupt is triggered in this window, the interrupt handler
would use freed previously freed data. Mixing manual probe() cleanup
amd resource-managed devm leads to such races, thus just convert
everything to devm to fix the issue and make code simpler.

Reported-by: Sashiko <sashiko-bot@xxxxxxxxxx>
Closes: https://sashiko.dev/#/bug/linux-02cea59a-8c4a-460c-aeed-69002ab3e72d
Fixes: 50e99641413e ("soc: qcom: smp2p: Qualcomm Shared Memory Point to Point")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Krzysztof Kozlowski <krzysztof.kozlowski@xxxxxxxxxxxxxxxx>
---
drivers/soc/qcom/smp2p.c | 96 +++++++++++++++++++---------------------
1 file changed, 46 insertions(+), 50 deletions(-)

diff --git a/drivers/soc/qcom/smp2p.c b/drivers/soc/qcom/smp2p.c
index 01b8580c9f27..7c1684c65e3b 100644
--- a/drivers/soc/qcom/smp2p.c
+++ b/drivers/soc/qcom/smp2p.c
@@ -619,6 +619,27 @@ static int smp2p_parse_ipc(struct qcom_smp2p *smp2p)
return 0;
}

+static void qcom_smp2p_release_mbox(void *data)
+{
+ struct qcom_smp2p *smp2p = data;
+
+ mbox_free_channel(smp2p->mbox_chan);
+}
+
+static void qcom_smp2p_unwind_interfaces(void *data)
+{
+ struct qcom_smp2p *smp2p = data;
+ struct smp2p_entry *entry;
+
+ list_for_each_entry(entry, &smp2p->inbound, node)
+ irq_domain_remove(entry->domain);
+
+ list_for_each_entry(entry, &smp2p->outbound, node)
+ qcom_smem_state_unregister(entry->state);
+
+ smp2p->out->valid_entries = 0;
+}
+
static int qcom_smp2p_probe(struct platform_device *pdev)
{
struct smp2p_entry *entry;
@@ -671,34 +692,49 @@ static int qcom_smp2p_probe(struct platform_device *pdev)
return ret;
}

+ ret = devm_add_action_or_reset(&pdev->dev, qcom_smp2p_release_mbox, smp2p);
+ if (ret)
+ return ret;
+
ret = qcom_smp2p_alloc_outbound_item(smp2p);
if (ret < 0)
- goto release_mbox;
+ return ret;
+
+ /*
+ * Register the cleanup handler, before actual initialization happens in
+ * qcom_smp2p_inbound_entry() and qcom_smp2p_outbound_entry(), to make
+ * the code simpler in error paths of below
+ * for_each_available_child_of_node_scoped)_
+ * loop. The cleanup handler will handle early failure well, because
+ * lists will be empty and smp2p->out is already allocated.
+ */
+ ret = devm_add_action_or_reset(&pdev->dev, qcom_smp2p_unwind_interfaces,
+ smp2p);
+ if (ret)
+ return ret;

for_each_available_child_of_node_scoped(pdev->dev.of_node, node) {
entry = devm_kzalloc(&pdev->dev, sizeof(*entry), GFP_KERNEL);
- if (!entry) {
- ret = -ENOMEM;
- goto unwind_interfaces;
- }
+ if (!entry)
+ return -ENOMEM;

entry->smp2p = smp2p;
spin_lock_init(&entry->lock);

ret = of_property_read_string(node, "qcom,entry-name", &entry->name);
if (ret < 0)
- goto unwind_interfaces;
+ return ret;

if (of_property_read_bool(node, "interrupt-controller")) {
ret = qcom_smp2p_inbound_entry(smp2p, entry, node);
if (ret < 0)
- goto unwind_interfaces;
+ return ret;

list_add(&entry->node, &smp2p->inbound);
} else {
ret = qcom_smp2p_outbound_entry(smp2p, entry, node);
if (ret < 0)
- goto unwind_interfaces;
+ return ret;

list_add(&entry->node, &smp2p->outbound);
}
@@ -715,7 +751,7 @@ static int qcom_smp2p_probe(struct platform_device *pdev)
IRQF_ONESHOT,
NULL, (void *)smp2p);
if (ret)
- goto unwind_interfaces;
+ return ret;

/*
* Treat smp2p interrupt as wakeup source, but keep it disabled
@@ -728,52 +764,13 @@ static int qcom_smp2p_probe(struct platform_device *pdev)
*/
device_set_wakeup_capable(&pdev->dev, true);

- ret = dev_pm_set_wake_irq(&pdev->dev, irq);
- if (ret)
- goto set_wake_irq_fail;
-
- return 0;
-
-set_wake_irq_fail:
- dev_pm_clear_wake_irq(&pdev->dev);
-
-unwind_interfaces:
- list_for_each_entry(entry, &smp2p->inbound, node)
- irq_domain_remove(entry->domain);
-
- list_for_each_entry(entry, &smp2p->outbound, node)
- qcom_smem_state_unregister(entry->state);
-
- smp2p->out->valid_entries = 0;
-
-release_mbox:
- mbox_free_channel(smp2p->mbox_chan);
-
- return ret;
+ return devm_pm_set_wake_irq(&pdev->dev, irq);

report_read_failure:
dev_err(&pdev->dev, "failed to read %s\n", key);
return -EINVAL;
}

-static void qcom_smp2p_remove(struct platform_device *pdev)
-{
- struct qcom_smp2p *smp2p = platform_get_drvdata(pdev);
- struct smp2p_entry *entry;
-
- dev_pm_clear_wake_irq(&pdev->dev);
-
- list_for_each_entry(entry, &smp2p->inbound, node)
- irq_domain_remove(entry->domain);
-
- list_for_each_entry(entry, &smp2p->outbound, node)
- qcom_smem_state_unregister(entry->state);
-
- mbox_free_channel(smp2p->mbox_chan);
-
- smp2p->out->valid_entries = 0;
-}
-
static const struct of_device_id qcom_smp2p_of_match[] = {
{ .compatible = "qcom,smp2p" },
{}
@@ -782,7 +779,6 @@ MODULE_DEVICE_TABLE(of, qcom_smp2p_of_match);

static struct platform_driver qcom_smp2p_driver = {
.probe = qcom_smp2p_probe,
- .remove = qcom_smp2p_remove,
.driver = {
.name = "qcom_smp2p",
.of_match_table = qcom_smp2p_of_match,
--
2.53.0