[PATCH RFC] irqchip/gic-v3-its: Make LPI free path infallible

"syzbot" <[email protected]> Wed, 29 Jul 2026 09:50:24 +0000 (UTC)
Newsgroups dev.linux.lists.syzbot
Message-ID <[email protected]>
The ITS driver manages available LPI ranges using a linked list. When LPIs
are freed via free_lpi_range(), the driver allocates a new lpi_range node
to add the freed range back to this list. If this allocation fails (e.g.,
under memory pressure), free_lpi_range() returns -ENOMEM.

The caller, its_lpi_free(), has a void return type and cannot propagate the
error. It wraps the call in a WARN_ON(). When the allocation fails, the
warning is triggered, and the LPI range is permanently leaked because it is
never added back to the free list.

WARNING: drivers/irqchip/irq-gic-v3-its.c:2251 at its_lpi_free
drivers/irqchip/irq-gic-v3-its.c:2251 [inline]
WARNING: drivers/irqchip/irq-gic-v3-its.c:2251 at
its_msi_teardown+0x3a4/0x424 drivers/irqchip/irq-gic-v3-its.c:3644
Call trace:
 its_lpi_free drivers/irqchip/irq-gic-v3-its.c:2251 [inline]
 its_msi_teardown+0x3a4/0x424 drivers/irqchip/irq-gic-v3-its.c:3644
 its_msi_teardown+0xa0/0xb8 drivers/irqchip/irq-gic-its-msi-parent.c:249
 msi_remove_device_irq_domain+0x16c/0x27c kernel/irq/msi.c:1127
 msi_device_data_release+0x38/0x9c kernel/irq/msi.c:293

Free paths must generally be infallible because the caller usually cannot
handle a failure. WARN_ON must not be used for conditions that can
legitimately happen, and pr_err should be used instead if necessary.

Fix this by decoupling the memory allocation from the free path and
shifting it to the allocation path (alloc_lpi_range()), which is allowed to
fail. Introduce a global cache (lpi_free_cache) for lpi_range nodes.
Pre-allocate a node in alloc_lpi_range() and add it to the cache. In
free_lpi_range(), simply pop a node from the cache. Since every successful
allocation adds a node to the cache, free_lpi_range() is guaranteed to find
at least one node.

Additionally, fix two existing memory leaks in the allocation error paths
to keep the cache perfectly balanced. First, in its_lpi_alloc(), if
bitmap_zalloc() fails, free the successfully allocated LPI range before
returning. Second, in its_create_device(), if col_map allocation fails but
lpi_map was successfully allocated, call its_lpi_free() instead of
bitmap_free() to correctly free both the bitmap and the underlying LPI
range.

Fixes: 880cb3cddd16 ("irqchip/gic-v3-its: Refactor LPI allocator")
Assisted-by: Gemini:gemini-3.5-flash Gemini:gemini-3.1-pro-preview syzbot
Reported-by: [email protected]
Closes: https://syzkaller.appspot.com/bug?extid=229d761b8a110e6de517
Link: https://syzkaller.appspot.com/ai_job?id=09d24d0b-29ec-4b31-b433-6367b7670bfc
To: <[email protected]>
To: "Marc Zyngier" <[email protected]>
To: "Thomas Gleixner" <[email protected]>
To: "Marc Zyngier" <[email protected]>
Cc: <[email protected]>

---
diff --git a/drivers/irqchip/irq-gic-v3-its.c b/drivers/irqchip/irq-gic-v3-its.c
index 6f5811aae..634b0e154 100644
--- a/drivers/irqchip/irq-gic-v3-its.c
+++ b/drivers/irqchip/irq-gic-v3-its.c
@@ -2097,6 +2097,7 @@ static struct irq_chip its_irq_chip = {
 
 static DEFINE_MUTEX(lpi_range_lock);
 static LIST_HEAD(lpi_range_list);
+static LIST_HEAD(lpi_free_cache);
 
 struct lpi_range {
 	struct list_head	entry;
@@ -2120,8 +2121,13 @@ static struct lpi_range *mk_lpi_range(u32 base, u32 span)
 static int alloc_lpi_range(u32 nr_lpis, u32 *base)
 {
 	struct lpi_range *range, *tmp;
+	struct lpi_range *new_node;
 	int err = -ENOSPC;
 
+	new_node = mk_lpi_range(0, 0);
+	if (!new_node)
+		return -ENOMEM;
+
 	mutex_lock(&lpi_range_lock);
 
 	list_for_each_entry_safe(range, tmp, &lpi_range_list, entry) {
@@ -2140,6 +2146,11 @@ static int alloc_lpi_range(u32 nr_lpis, u32 *base)
 		}
 	}
 
+	if (!err)
+		list_add(&new_node->entry, &lpi_free_cache);
+	else
+		kfree(new_node);
+
 	mutex_unlock(&lpi_range_lock);
 
 	pr_debug("ITS: alloc %u:%u\n", *base, nr_lpis);
@@ -2162,11 +2173,18 @@ static int free_lpi_range(u32 base, u32 nr_lpis)
 {
 	struct lpi_range *new, *old;
 
-	new = mk_lpi_range(base, nr_lpis);
-	if (!new)
+	mutex_lock(&lpi_range_lock);
+
+	if (WARN_ON(list_empty(&lpi_free_cache))) {
+		mutex_unlock(&lpi_range_lock);
 		return -ENOMEM;
+	}
 
-	mutex_lock(&lpi_range_lock);
+	new = list_first_entry(&lpi_free_cache, struct lpi_range, entry);
+	list_del(&new->entry);
+
+	new->base_id = base;
+	new->span = nr_lpis;
 
 	list_for_each_entry_reverse(old, &lpi_range_list, entry) {
 		if (old->base_id < base)
@@ -2195,6 +2213,7 @@ static int __init its_lpi_init(u32 id_bits)
 {
 	u32 lpis = (1UL << id_bits) - 8192;
 	u32 numlpis;
+	struct lpi_range *range;
 	int err;
 
 	numlpis = 1UL << GICD_TYPER_NUM_LPIS(gic_rdists->gicd_typer);
@@ -2205,6 +2224,12 @@ static int __init its_lpi_init(u32 id_bits)
 			lpis);
 	}
 
+	range = mk_lpi_range(0, 0);
+	if (!range)
+		return -ENOMEM;
+
+	list_add(&range->entry, &lpi_free_cache);
+
 	/*
 	 * Initializing the allocator is just the same as freeing the
 	 * full range of LPIs.
@@ -2234,8 +2259,10 @@ static unsigned long *its_lpi_alloc(int nr_irqs, u32 *base, int *nr_ids)
 		goto out;
 
 	bitmap = bitmap_zalloc(nr_irqs, GFP_ATOMIC);
-	if (!bitmap)
+	if (!bitmap) {
+		free_lpi_range(*base, nr_irqs);
 		goto out;
+	}
 
 	*nr_ids = nr_irqs;
 
@@ -2248,7 +2275,7 @@ static unsigned long *its_lpi_alloc(int nr_irqs, u32 *base, int *nr_ids)
 
 static void its_lpi_free(unsigned long *bitmap, u32 base, u32 nr_ids)
 {
-	WARN_ON(free_lpi_range(base, nr_ids));
+	free_lpi_range(base, nr_ids);
 	bitmap_free(bitmap);
 }
 
@@ -3510,7 +3537,8 @@ static struct its_device *its_create_device(struct its_node *its, u32 dev_id,
 	if (!dev || !itt || !col_map || (!lpi_map && alloc_lpis)) {
 		kfree(dev);
 		itt_free_pool(itt, sz);
-		bitmap_free(lpi_map);
+		if (lpi_map)
+			its_lpi_free(lpi_map, lpi_base, nr_lpis);
 		kfree(col_map);
 		return NULL;
 	}


base-commit: f5098b6bae761e346ebcd9da7f95622c04733cff
-- 
This is an AI-generated patch subject to moderation.
Reply with '#syz upstream' to Sign-off the patch as a human author
and send it to the upstream kernel mailing lists.
Reply with '#syz reject' to reject it ('#syz unreject' to undo).

See https://goo.gle/syzbot-ai-patches for information about AI-generated patches.
You can comment on the patch as usual, syzbot will try to address
the comments and send a new version of the patch if necessary.
syzbot engineers can be reached at [email protected].